using func_t std::functionvoid(); func_t func std::bind(TcpServer::Service, this, sockfd, addr); ThreadPoolfunc_t::GetInstance()-Push(func);CP 与 UDP 的对比为什么 TCP 更“可靠”在开始之前先看一下 TCP 和 UDP 的区别特性TCPUDP连接方式面向连接三次握手无连接可靠性可靠确认、重传、排序不可靠传输方式面向字节流面向数据报适用场景文件传输、网页、邮件视频、语音、DNS服务器处理需要 accept 接受连接直接 recvfrom 收数据TCP Socket API 详解所有函数都在sys/socket.h中。1. socket() —— 创建通信端点int socket(int domain, int type, int protocol);domain协议族IPv4 用AF_INETtypeTCP 用SOCK_STREAM面向流protocol一般填0成功返回文件描述符fd失败返回-1类比socket()就像open()打开一个网络通信的“文件”。2. bind() —— 绑定地址和端口int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);服务器需要绑定固定的 IP 和端口客户端才能找到它。成功返回0失败返回-1。初始化sockaddr_in的典型步骤bzero(servaddr, sizeof(servaddr)); servaddr.sin_family AF_INET; servaddr.sin_addr.s_addr htonl(INADDR_ANY); // 监听所有本机 IP servaddr.sin_port htons(SERV_PORT); // 端口号如 9999INADDR_ANY表示本机任意 IP适合多网卡服务器。htonl/htons将主机字节序转为网络字节序。3. listen() —— 进入监听状态int listen(int sockfd, int backlog);将 socket 设置为监听状态准备接受连接。backlog等待连接队列的最大长度一般设为 5 或 6。成功返回0失败返回-1。4. accept() —— 接受连接int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);三次握手完成后服务器调用accept()接受连接。如果没有客户端连接accept()会阻塞等待。addr传出参数返回客户端的地址和端口。addrlen传入传出参数传入缓冲区大小传出实际长度。返回值一个新的 socket fd用于和该客户端通信。类比accept()就像饭店拉客拉到一个客人就分配一个服务员新 fd。5. connect() —— 客户端发起连接int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);客户端调用connect()连接服务器。addr是对方的地址与 bind 相反。成功返回0失败返回-1。客户端不需要显式bind()操作系统会自动分配一个随机端口。V1最简单的 Echo Server单连接版服务器结构while (true) { int sockfd accept(_listensock, ...); Service(sockfd); // 一直 read/write close(sockfd); }问题Service()是一个死循环处理完一个客户端才能接受下一个。第二个客户端无法连接因为accept()没被再次调用。这就是“单连接”版本的缺陷。V2多进程版 Echo Server思路每来一个连接fork()一个子进程处理。父进程继续accept()新连接。关键代码pid_t id fork(); if (id 0) { close(_listensock); // 子进程关闭监听 socket if (fork() 0) exit(0); // 孙子进程处理避免僵尸 Service(sockfd); close(sockfd); exit(0); } else { close(sockfd); // 父进程关闭通信 socket waitpid(id, nullptr, 0); // 回收子进程 }优点支持多客户端并发。缺点进程创建开销大。需要处理僵尸进程。V3多线程版 Echo Server思路每来一个连接创建一个线程处理。线程分离pthread_detach避免 join。关键代码class ThreadData { int _sockfd; InetAddr _addr; }; static void *threadExecute(void *args) { pthread_detach(pthread_self()); ThreadData *td static_castThreadData *(args); TcpServer::Service(*td); close(td-_sockfd); delete td; return nullptr; }优点比多进程轻量。支持多客户端并发。缺点线程创建仍有开销。如果连接数暴增线程数也会暴增。V3-1多线程远程命令执行思路客户端发送命令服务器执行后返回结果。使用popen()执行命令fgets()读取输出。安全性只允许白名单命令ls,pwd,who,whoami等。防止执行危险命令。关键代码FILE *fp popen(command.c_str(), r); while (fgets(buffer, sizeof(buffer), fp)) { result buffer; } pclose(fp);popen是一个 POSIX 标准 C 库函数它的核心作用是创建一个管道并启动一个 shell 来执行指定的命令从而实现与子进程的单向通信。FILE *popen(const char *command, const char *type); int pclose(FILE *stream);调用popen后内部会依次执行以下操作创建管道调用pipe()创建一个单向管道。创建子进程调用fork()复制当前进程。启动 shell子进程调用/bin/sh -c command来执行你传入的命令字符串。这意味着你传入的命令字符串会由 shell 进行解析因此支持管道、重定向、通配符等所有 shell 语法。type参数决定了管道的方向r(读取模式)子进程的标准输出连接到管道的写端。父进程通过返回的FILE*流读取子进程的输出。w(写入模式)子进程的标准输入连接到管道的读端。父进程通过返回的FILE*流向子进程的输入写入数据。为什么需要管道在popen的场景中父进程调用了fork()产生了一个子进程子进程去执行ls之类的命令它的输出默认会打印到终端父进程的 stdout父进程想捕获这段输出存到字符串里返回给调用者问题来了子进程的输出写到终端父进程怎么读到答案就是先在fork()之前创建一个管道让父子进程通过fork继承同一个管道的文件描述符然后把子进程的 stdout 重定向到管道的写端。这样子进程以为自己在写终端实际上写进了管道父进程从管道的读端读就拿到了输出。V4线程池版 Echo Server思路使用线程池避免频繁创建销毁线程。将任务封装为std::functionvoid()放入线程池队列。关键代码using func_t std::functionvoid(); func_t func std::bind(TcpServer::Service, this, sockfd, addr); ThreadPoolfunc_t::GetInstance()-Push(func);TCP 通信的宏观图景TCPTransmission Control Protocol传输控制协议是一种面向连接的、可靠的、基于字节流的传输层协议。一个完整的 TCP 通信过程可以分为三个阶段┌─────────────┐ ┌─────────────┐ │ Client │ │ Server │ └──────┬──────┘ └──────┬──────┘ │ │ │ ① 建立连接三次握手 │ │ ◄─────────────────────────────────────► │ │ │ │ ② 数据传输read/write │ │ ◄─────────────────────────────────────► │ │ │ │ ③ 断开连接四次挥手 │ │ ◄─────────────────────────────────────► │ │ │生活类比打电话建立连接 拨号、对方接听数据传输 双方对话断开连接 说“再见”、挂断第一阶段建立连接三次握手1. 为什么需要三次握手TCP 是可靠的双方必须确认我能发数据给你你能收我的数据你能发数据给我我能收你的数据这四件事需要三次握手才能全部确认。2. 三次握手过程Client Server │ │ │ ① SYN (seqx) │ │ ──────────────────────────────────► │ 服务器进入 SYN_RCVD 状态 │ │ │ ② SYN ACK (seqy, ackx1) │ │ ◄────────────────────────────────── │ │ │ │ ③ ACK (acky1) │ │ ──────────────────────────────────► │ 双方进入 ESTABLISHED 状态 │ │详细说明步骤方向报文含义①Client → ServerSYN, seqx我想连接你我的初始序列号是 x②Server → ClientSYNACK, seqy, ackx1我同意连接我的初始序列号是 y确认收到你的 x③Client → ServerACK, acky1确认收到你的 y连接建立3. 对应到 Socket API阶段服务器客户端创建 socketsocket()socket()绑定地址bind()不需要OS 自动分配监听listen()无等待/发起连接accept()阻塞等待connect()发起握手连接建立accept()返回新 fdconnect()返回 0关键点三次握手由内核自动完成应用程序只需调用connect()和accept()。accept()返回的是新的 socket fd用于和该客户端通信原来的监听 fd 继续等待其他连接。服务器可以同时与多个客户端建立连接每个连接对应一个独立的 fd。4. 为什么 accept 返回新的 fd类比饭店拉客listen()是饭店开门营业accept()是服务员拉客每拉到一个客人就分配一个专属服务员新 fd原来的“拉客”工作继续接待更多客人第二阶段数据传输1. TCP 是全双工通信TCP 连接建立后双方可以同时发送和接收数据就像打电话可以同时说话。Client Server │ │ │ write() ──────────────────────► read() │ │ │ read() ◄────────────────────── write() │ │2. 数据是“字节流”没有边界TCP 传输的是字节流不像 UDP 那样保留消息边界。例子客户端连续write()三次Hello、World、!服务器可能一次read()就读到 HelloWorld!解决方案自定义协议如先发送长度再发送内容使用分隔符如\n作为消息结束标志固定长度消息3. 常用的读写函数函数说明read()从 socket 读取数据write()向 socket 写入数据recv()带标志的读取如 MSG_WAITALLsend()带标志的发送如 MSG_NOSIGNALread 返回值含义返回值含义 0实际读取的字节数 0对端关闭了连接EOF 0出错如 EINTR、ECONNRESET第三阶段断开连接四次挥手1. 为什么需要四次挥手TCP 是全双工的双方都需要独立关闭自己的发送方向。2. 四次挥手过程Client Server │ │ │ ① FIN (seqx) │ │ ──────────────────────────────────► │ 客户端不再发送数据 │ │ │ ② ACK (ackx1) │ │ ◄────────────────────────────────── │ 服务器确认 │ │ │ ③ FIN (seqy) │ │ ◄────────────────────────────────── │ 服务器也不再发送数据 │ │ │ ④ ACK (acky1) │ │ ──────────────────────────────────► │ 客户端确认连接关闭 │ │3. 对应到 Socket API操作说明close(fd)关闭 socket触发 FINshutdown(fd, SHUT_WR)只关闭写方向仍可读shutdown(fd, SHUT_RDWR)关闭读写注意close()只是减少引用计数真正关闭要等所有引用都释放。服务器处理完一个客户端后应close(sockfd)关闭通信 fd但_listensock继续保留。一句话总结TCP 通信 三次握手建立连接 全双工字节流传输 四次挥手断开连接内核负责可靠性应用负责业务逻辑。