《网络江湖盟主令:套接字九式破阵图谱》下篇

目录

一、前言

二、正文

1.简单的TCP网络程序

1.1 TCP socket API 详解

1.2 封装 TCP socket

1.3 TCP通用服务器

1.4 英译汉服务器

1.5 TCP通用客户端

1.6 英译汉客户端

2. 简单的TCP网络程序(多进程版本)

3. 简单的TCP网络程序(多线程版本)

4. 线程池版本的 TCP 服务器

5. TCP协议通讯流程

6. TCP 和 UDP 对比

三、结语


一、前言

本文帮助小伙伴们能够实现一个简单的tcp客户端/服务器(单连接版本, 多进程版本, 多线程版本);并能够理解tcp服务器建立连接, 发送数据, 断开连接的流程,希望大家能够从中有所收!!!

二、正文

1.简单的TCP网络程序

和刚才UDP类似. 实现一个简单的英译汉的功能

1.1 TCP socket API 详解

下面介绍程序中用到的socket API,这些函数都在sys/socket.h中。

①socket()

● socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;

● 应用程序可以像读写文件一样用read/write在网络上收发数据;

● 如果socket()调用出错则返回-1;

● 对于IPv4, family参数指定为AF_INET;

● 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议

● protocol参数的介绍从略,指定为0即可

② bind()

● 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后 就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;

● bind()成功返回0,失败返回-1。

● bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听 myaddr所描述的地址和端口号;

● 前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结 构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度;

我们的程序中对myaddr参数是这样初始化的:

1. 将整个结构体清零;

2. 设置地址类型为AF_INET;

3. 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑 定多个IP地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;

4. 端口号为SERV_PORT, 我们定义为9999;

③ listen();

● listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多 的连接请求就忽略, 这里设置不会太大(一般是5), 具体细节同学们课后深入研究; ● listen()成功返回0,失败返回-1;

④accept()

● 三次握手完成后, 服务器调用accept()接受连接;

● 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来; ● ● addr是一个传出参数,accept()返回时传出客户端的地址和端口号;

● 如果给addr 参数传NULL,表示不关心客户端的地址;

● addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度 以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满

我们的服务器程序结构是这样的:

        到这里,我们会发现TCP的服务器结构与上节课讲过的UDP有些不同的地方,对于UDP来说,往往我们在服务端绑定完套接字后就可以直接利用绑定后的sockfd来进行接受客户端的请求,并进行处理,但是在TCP这里,我们是将绑定后的sockd设置为监听状态,并在接受到客户端请求后,返回一个fd。那我们要如何理解这样的现象呢,以餐馆举例子,我们会发现往往每个餐厅门口都会有一个人站着揽客,将客人拉进自己的餐馆吃饭,而一旦客人被拉揽成功后,进去后不是刚刚那个人来为他进行服务,而是餐馆内会另外有服务人员对其进行接待。而我们TCP服务器的linstenfd起的就是揽客,即接受网络中客户端的作用,而返回的fd即起着后续服务人员的作用,即与客户端进行通信。

⑤connect()

● 客户端需要调用connect()连接服务器;

● connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;

● connect()成功返回0,出错返回-1;

1.2 封装 TCP socket

 tcp_socket.hpp

#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <string>
#include <cassert>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
 
#define CHECK_RET(exp) if (!(exp)) {\
 return false;\
}

class TcpSocket {
public:
 TcpSocket() : fd_(-1) {}
 TcpSocket(int fd) : fd_(fd) {}
 
 bool Socket() 
 {
     fd_ = socket(AF_INET, SOCK_STREAM, 0);
     if (fd_ < 0) 
     {
         perror("socket");
         return false;
     }

     printf("open fd = %d\n", fd_);
     return true;
 }
 
 bool Close() const 
 {
     close(fd_);
     printf("close fd = %d\n", fd_);
     return true;
 }
 
 bool Bind(const std::string& ip, uint16_t port) const 
 {
     sockaddr_in addr;
     addr.sin_family = AF_INET;
     addr.sin_addr.s_addr = inet_addr(ip.c_str());
     addr.sin_port = htons(port);
     int ret = bind(fd_, (sockaddr*)&addr, sizeof(addr));
     if (ret < 0) 
     {
         perror("bind");
         return false;
     }
     return true;
 }
 
 bool Listen(int num) const 
 {
     int ret = listen(fd_, num);
     if (ret < 0) 
     {
         perror("listen");
         return false;
     }
     return true;
 }
 
 bool Accept(TcpSocket* peer, std::string* ip = NULL, uint16_t* port = NULL) const 
 {
     sockaddr_in peer_addr;
     socklen_t len = sizeof(peer_addr);
     int new_sock = accept(fd_, (sockaddr*)&peer_addr, &len);
     if (new_sock < 0) 
     {
         perror("accept");
         return false;
     }
     printf("accept fd = %d\n", new_sock);
     peer->fd_ = new_sock;
     if (ip != NULL) 
     {
         *ip = inet_ntoa(peer_addr.sin_addr);
     }
     if (port != NULL) 
    {
         *port = ntohs(peer_addr.sin_port);
     }
     return true;
 }

bool Recv(std::string* buf) const 
{
     buf->clear();
     char tmp[1024 * 10] = {0};
 // [注意!] 这里的读并不算很严谨, 因为一次 recv 并不能保证把所有的数据都全部读完
 // 参考 man 手册 MSG_WAITALL 节. 
     ssize_t read_size = recv(fd_, tmp, sizeof(tmp), 0);
     if (read_size < 0) 
     {
         perror("recv");
         return false;
     }
     if (read_size == 0)
     {
         return false;
     }
     buf->assign(tmp, read_size);
     return true;
 }
 
 bool Send(const std::string& buf) const
 {
     ssize_t write_size = send(fd_, buf.data(), buf.size(), 0);
     if (write_size < 0) {
         perror("send");
         return false;
     }
     return true;
 }
 
 bool Connect(const std::string& ip, uint16_t port) const 
 {
     sockaddr_in addr;
     addr.sin_family = AF_INET;
     addr.sin_addr.s_addr = inet_addr(ip.c_str());
     addr.sin_port = htons(port);
     int ret = connect(fd_, (sockaddr*)&addr, sizeof(addr));
     if (ret < 0) {
     perror("connect");
     return false;
     }
     return true;
 }
 
 int GetFd() const {
 return fd_;
 }

private:
 int fd_;
};

1.3 TCP通用服务器

tcp_server.hpp

#pragma once
#include <functional>
#include "tcp_socket.hpp"
 
typedef std::function<void (const std::string& req, std::string* resp)> Handler;
 
class TcpServer 
{
public:
 TcpServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) { }
 
 bool Start(Handler handler) {
 // 1. 创建 socket;
 CHECK_RET(listen_sock_.Socket());
 // 2. 绑定端口号
 CHECK_RET(listen_sock_.Bind(ip_, port_));
 // 3. 进行监听
 CHECK_RET(listen_sock_.Listen(5));
 // 4. 进入事件循环
 for (;;) 
 {
     // 5. 进行 accept
     TcpSocket new_sock;
     std::string ip;
     uint16_t port = 0;
     if (!listen_sock_.Accept(&new_sock, &ip, &port)) {
         continue;
     }
     printf("[client %s:%d] connect!\n", ip.c_str(), port);
     // 6. 进行循环读写
     for (;;) 
     {
         std::string req;
         // 7. 读取请求. 读取失败则结束循环
         bool ret = new_sock.Recv(&req);
         if (!ret) {
             printf("[client %s:%d] disconnect!\n", ip.c_str(), port);
             // [注意!] 需要关闭 socket
             new_sock.Close();
             break;
         }
     // 8. 计算响应
     std::string resp;
     handler(req, &resp);
     
    // 9. 写回响应
     new_sock.Send(resp);
     printf("[%s %d] req: %s, resp: %s\n",ip.c_str(),port,req.c_str(),resp.c_str());
     }
 }
return true;
}

private:
    TcpSocket listen_sock_;
    std::string ip_;
    uint64_t port_;
};
     

1.4 英译汉服务器

1.5 TCP通用客户端

tcp_client.hpp

1.6 英译汉客户端

#include "tcp_client.hpp"
#include <iostream>
 
 int main(int argc, char* argv[])
 {
     if (argc != 3) {
     printf("Usage ./dict_client [ip] [port]\n");
     return 1;
     }
     TcpClient client(argv[1], atoi(argv[2]));
 
     bool ret = client.Connect();
     if (!ret) {
         return 1;
     }
 
    for (;;) 
    {
        std::cout << "请输入要查询的单词:" << std::endl;
        std::string word;
        std::cin >> word;
        if (!std::cin) {
            break;
        }
 
    client.Send(word);
    std::string result;
    client.Recv(&result);
    std::cout << result << std::endl;
    }
 return 0;
}

由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配.

注意:

● 客户端不是不允许调用bind(), 只是没有必要调用bind()固定一个端口号. 否则如果在同一台机器上启动 多个客户端, 就会出现端口号被占用导致不能正确建立连接;

● 服务器也不是必须调用bind(), 但如果服务器不调用bind(), 内核会自动给服务器分配监听端口, 每次启动 服务器时端口号都不一样, 客户端要连接服务器就会遇到麻烦;

测试多个连接的情况

再启动一个客户端, 尝试连接服务器, 发现第二个客户端, 不能正确的和服务器进行通信. 分析原因, 是因为我们accecpt了一个请求之后, 就在一直while循环尝试read, 没有继续调用到accecpt, 导致不能接 受新的请求. 我们当前的这个TCP, 只能处理一个连接, 这是不科学的.

2. 简单的TCP网络程序(多进程版本)

通过每个请求, 创建子进程的方式来支持多连接;

tcp_process_server.hpp

#pragma once
#include <functional>
#include <signal.h>
#include "tcp_socket.hpp"
 
typedef std::function<void (const std::string& req, std::string* resp)> Handler;
 
// 多进程版本的 Tcp 服务器
class TcpProcessServer {
public:
     TcpProcessServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {
     // 需要处理子进程
         signal(SIGCHLD, SIG_IGN);
     }
 
     void ProcessConnect(const TcpSocket& new_sock, const std::string& ip, uint16_t port,
                        Handler handler) 
    {
         int ret = fork();
         if (ret > 0) {
         // father
         //父进程不需要做额外的操作, 直接返回即可. 
         // 思考, 这里能否使用 wait 进行进程等待?
         // 如果使用 wait , 会导致父进程不能快速再次调用到 accept, 仍然没法处理多个请求
         // [注意!!] 父进程需要关闭 new_sock
         new_sock.Close();
         return;
     } else if (ret == 0) {
         // child
         // 处理具体的连接过程. 每个连接一个子进程
         for (;;) 
         {
             std::string req;
             bool ret = new_sock.Recv(&req);
             if (!ret) {
                 // 当前的请求处理完了, 可以退出子进程了. 注意, socket 的关闭在析构函数中就完成了
                 printf("[client %s:%d] disconnected!\n", ip.c_str(), port);
                 exit(0);
             }
             std::string resp;
             handler(req, &resp);
             new_sock.Send(resp);
             printf("[client %s:%d] req: %s, resp: %s\n", ip.c_str(), port, 
             req.c_str(), resp.c_str());
         }
     } else {
     perror("fork");
     }
 }
 
 bool Start(Handler handler) {

 // 1. 创建 socket;
 CHECK_RET(listen_sock_.Socket());
 
// 2. 绑定端口号
 CHECK_RET(listen_sock_.Bind(ip_, port_));

 // 3. 进行监听
 CHECK_RET(listen_sock_.Listen(5));

 // 4. 进入事件循环
 for (;;) {

 // 5. 进行 accept
 TcpSocket new_sock;
 std::string ip;
 uint16_t port = 0;
 if (!listen_sock_.Accept(&new_sock, &ip, &port)) {
     continue;
 }
 printf("[client %s:%d] connect!\n", ip.c_str(), port);
 ProcessConnect(new_sock, ip, port, handler);
 }
 return true;
 }

private:
 TcpSocket listen_sock_;
 std::string ip_;
 uint64_t port_;
};

3. 简单的TCP网络程序(多线程版本)

通过每个请求, 创建一个线程的方式来支持多连接;

tcp_thread_server.hpp

#pragma once
#include <functional>
#include <pthread.h>
#include "tcp_socket.hpp"
 
typedef std::function<void (const std::string&, std::string*)> Handler;
 
struct ThreadArg {
 TcpSocket new_sock;
 std::string ip;
 uint16_t port;
 Handler handler;
};
 
class TcpThreadServer {
public:
 TcpThreadServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {}
 
 bool Start(Handler handler) {
 // 1. 创建 socket;
 CHECK_RET(listen_sock_.Socket());
 
// 2. 绑定端口号
 CHECK_RET(listen_sock_.Bind(ip_, port_));

 // 3. 进行监听
 CHECK_RET(listen_sock_.Listen(5));

 // 4. 进入循环
 for (;;) {
 
 // 5. 进行 accept
 ThreadArg* arg = new ThreadArg();
 arg->handler = handler;
 bool ret = listen_sock_ .Accept(&arg->new_sock, &arg->ip, &arg->port);
 if (!ret) {
     continue;
 }
 printf("[client %s:%d] connect\n", arg->ip.c_str(), arg->port);
 
// 6. 创建新的线程完成具体操作
 pthread_t tid;
 pthread_create(&tid, NULL, ThreadEntry, arg);
 pthread_detach(tid);
 }
 return true;
 }
 

 static void* ThreadEntry(void* arg) {
     ThreadArg* p = reinterpret_cast<ThreadArg*>(arg);
     ProcessConnect(p);
     // 一定要记得释放内存!!! 也要记得关闭文件描述符
     p->new_sock.Close();
     delete p;
     return NULL;
 }
 
 // 处理单次连接. 这个函数也得是 static 
 static void ProcessConnect(ThreadArg* arg) {
     // 1. 循环进行读写
     for (;;) {
         std::string req;
 
        // 2. 读取请求
         bool ret = arg->new_sock.Recv(&req);
         if (!ret) {
             printf("[client %s:%d] disconnected!\n", arg->ip.c_str(), arg->port);
             break;
         }
         std::string resp;
 
         // 3. 根据请求计算响应
         arg->handler(req, &resp);
     
         // 4. 发送响应
         arg->new_sock.Send(resp);
         printf("[client %s:%d] req: %s, resp: %s\n", arg->ip.c_str(),
         arg->port, req.c_str(), resp.c_str());
     }
 }
private:
 TcpSocket listen_sock_;
 std::string ip_;
 uint16_t port_;
};

4. 线程池版本的 TCP 服务器

关于线程池版本的TCP服务器,博主会在后续的资源中进行上传

5. TCP协议通讯流程

下图是基于TCP协议的客户端/服务器程序的一般流程:

服务器初始化:

● 调用socket, 创建文件描述符;

调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;

调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;

调用accecpt, 并阻塞, 等待客户端连接过来;

建立连接的过程:

● 调用socket, 创建文件描述符;

● 调用connect, 向服务器发起连接请求;

● connect会发出SYN段并阻塞等待服务器应答; (第一次)

● 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接";

(第二次)

● 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)

这个建立连接的过程, 通常称为 三次握手;

数据传输的过程

● 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方 可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;

● 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;

● 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期 间客户端调用read()阻塞等待服务器的应答;

● 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;

● 客户端收到后从read()返回, 发送下一条请求,如此循环下去;

断开连接的过程:

● 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);

● 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);

● read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送 一个FIN; (第三次)

● 客户端收到FIN, 再返回一个ACK给服务器; (第四次)

这个断开连接的过程, 通常称为 四次挥手

6. TCP 和 UDP 对比

● 可靠传输 vs 不可靠传输

● 有连接 vs 无连接

● 字节流 vs 数据报

三、结语

        到此为止,本文关于网络编程套接字(下)的内容到此结束了,如有不足之处,欢迎小伙伴们指出呀!

         关注我 _麦麦_分享更多干货:_麦麦_-优快云博客

         大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下期见!

评论 229
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_麦麦_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值