socket编程TCP

TCP Socket 编程


零、TCP Socket 编程接口速查

TCP 相比 UDP 多了三个核心接口:listen()accept()connect()socket()bind() 与 UDP 共享------唯一区别是 socket()type 参数填 SOCK_STREAM 而非 SOCK_DGRAM

0.1 核心系统调用

函数 头文件 作用
socket() <sys/socket.h> 创建套接字(TCP 用 SOCK_STREAM
bind() <sys/socket.h> 绑定本地 IP + 端口
listen() <sys/socket.h> 将套接字设为被动模式,开始监听连接
accept() <sys/socket.h> 从连接队列中取出一个已完成三次握手的连接
connect() <sys/socket.h> 向服务器发起连接请求
send() / write() <sys/socket.h> / <unistd.h> TCP 发送数据(已建立连接后)
recv() / read() <sys/socket.h> / <unistd.h> TCP 接收数据(已建立连接后)
close() <unistd.h> 关闭套接字
listen()
c 复制代码
int listen(int sockfd, int backlog);
  • sockfd:已经 socket() + bind() 的套接字
  • backlog:连接队列的最大长度------内核为这个套接字维护两个队列(半连接 + 全连接),backlog 限制的是全连接队列的大小
  • 返回值:成功 0,失败 -1

注意listen() 只做一件事------把主动套接字变为被动套接字,告诉内核"我要开始接客了"。它不阻塞,调用完立刻返回。

accept()
c 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd:监听套接字(即调用过 listen() 的那个 fd)
  • addr输出型参数 ------内核把客户端的 IP + 端口填进去。不关心可以传 NULL
  • addrlen值-结果参数 ------传入 addr 的大小,传出实际填充长度
  • 返回值 :成功返回一个新的文件描述符(service socket),失败返回 -1
  • 如果连接队列为空,accept() 默认阻塞等待,直到有连接到来

核心要点accept() 返回的是一个全新的文件描述符,和原来的监听套接字不是同一个东西。

connect()
c 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:客户端自己的套接字(socket() 创建后即可调用)
  • addr:服务器地址(IP + 端口)
  • addrlen:地址结构体大小
  • 返回值:成功 0,失败 -1
  • connect() 做两件事:① 如果 socket 尚未 bind,OS 自动绑定本地随机端口 → ② 向服务器发起三次握手

0.2 需要引入的头文件

cpp 复制代码
#include <sys/types.h>      // socket 相关类型定义
#include <sys/socket.h>     // socket / bind / listen / accept / connect
#include <netinet/in.h>     // sockaddr_in 结构体
#include <arpa/inet.h>      // inet_addr / inet_ntoa / inet_pton / inet_ntop
#include <unistd.h>         // close / read / write
#include <cstring>          // memset / strlen
#include <string>           // std::string
#include <iostream>         // cout / cerr

0.3 从 UDP 到 TCP------面向连接的网络编程

UDP 是无连接的,每次 sendto() 都要带上目标地址,数据报各自独立发送。TCP 是有连接的------通信之前必须先建立连接,之后的收发都在这个连接上进行,不再需要每次指定目标。

这个"连接"的本质差异,就体现在 TCP 多出来的三个接口上:
TCP 服务器 TCP 客户端 TCP 服务器 TCP 客户端 #mermaid-svg-HpkhEMoZDimZC4zT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:15px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HpkhEMoZDimZC4zT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HpkhEMoZDimZC4zT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HpkhEMoZDimZC4zT .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-HpkhEMoZDimZC4zT .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-HpkhEMoZDimZC4zT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HpkhEMoZDimZC4zT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HpkhEMoZDimZC4zT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HpkhEMoZDimZC4zT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HpkhEMoZDimZC4zT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HpkhEMoZDimZC4zT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HpkhEMoZDimZC4zT .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-HpkhEMoZDimZC4zT .marker.cross{stroke:#0b0b0b;}#mermaid-svg-HpkhEMoZDimZC4zT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:15px;}#mermaid-svg-HpkhEMoZDimZC4zT p{margin:0;}#mermaid-svg-HpkhEMoZDimZC4zT .actor{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-HpkhEMoZDimZC4zT text.actor>tspan{fill:#333;stroke:none;}#mermaid-svg-HpkhEMoZDimZC4zT .actor-line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);}#mermaid-svg-HpkhEMoZDimZC4zT .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-HpkhEMoZDimZC4zT .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-HpkhEMoZDimZC4zT .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-HpkhEMoZDimZC4zT #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-HpkhEMoZDimZC4zT .sequenceNumber{fill:#f4f4f4;}#mermaid-svg-HpkhEMoZDimZC4zT #sequencenumber{fill:#333;}#mermaid-svg-HpkhEMoZDimZC4zT #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-HpkhEMoZDimZC4zT .messageText{fill:#333;stroke:none;}#mermaid-svg-HpkhEMoZDimZC4zT .labelBox{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-HpkhEMoZDimZC4zT .labelText,#mermaid-svg-HpkhEMoZDimZC4zT .labelText>tspan{fill:#333;stroke:none;}#mermaid-svg-HpkhEMoZDimZC4zT .loopText,#mermaid-svg-HpkhEMoZDimZC4zT .loopText>tspan{fill:#333;stroke:none;}#mermaid-svg-HpkhEMoZDimZC4zT .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:hsl(40.5882352941, 60%, 83.3333333333%);}#mermaid-svg-HpkhEMoZDimZC4zT .note{stroke:hsl(52.6829268293, 60%, 73.9215686275%);fill:#fff5ad;}#mermaid-svg-HpkhEMoZDimZC4zT .noteText,#mermaid-svg-HpkhEMoZDimZC4zT .noteText>tspan{fill:#333;stroke:none;}#mermaid-svg-HpkhEMoZDimZC4zT .activation0{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-HpkhEMoZDimZC4zT .activation1{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-HpkhEMoZDimZC4zT .activation2{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-HpkhEMoZDimZC4zT .actorPopupMenu{position:absolute;}#mermaid-svg-HpkhEMoZDimZC4zT .actorPopupMenuPanel{position:absolute;fill:#fff4dd;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-HpkhEMoZDimZC4zT .actor-man line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-HpkhEMoZDimZC4zT .actor-man circle,#mermaid-svg-HpkhEMoZDimZC4zT line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;stroke-width:2px;}#mermaid-svg-HpkhEMoZDimZC4zT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} socket() + bind() + listen() 连接建立,可以通信 listen()------开门迎客 socket() connect()------发起连接(三次握手) accept()------从队列取连接 → 返回新 fd send() / write() recv() / read() close() close()

TCP 比 UDP 多了三步listen 开门 → connect 敲门 → accept 迎进门。门里门外一旦对上,之后就是纯粹的读写,不再需要目标地址。

0.4 listen------开门迎客

0.4.1 概念

UDP 服务器在 socket() + bind() 之后直接就能 recvfrom()。但 TCP 不行------TCP 是面向连接的,服务器必须先表明"我愿意接受连接",客户端才能连进来。

listen() 就是做这件事:把一个普通的套接字转换成"监听套接字",告诉内核------这个端口可以接收连接请求了。

0.4.2 为什么要排队 ------ backlog 参数

面向连接意味着什么?客户端的连接请求不是瞬间完成的------要经过三次握手。如果一个服务器很忙,同时来了很多客户端请求,处理不过来怎么办?那就排队

listen() 的第二个参数 backlog 就是这个排队机制------连接队列的最大长度
#mermaid-svg-sLKJ2XBa2UrAoHvE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sLKJ2XBa2UrAoHvE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sLKJ2XBa2UrAoHvE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sLKJ2XBa2UrAoHvE .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-sLKJ2XBa2UrAoHvE .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-sLKJ2XBa2UrAoHvE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sLKJ2XBa2UrAoHvE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sLKJ2XBa2UrAoHvE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sLKJ2XBa2UrAoHvE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sLKJ2XBa2UrAoHvE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sLKJ2XBa2UrAoHvE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sLKJ2XBa2UrAoHvE .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-sLKJ2XBa2UrAoHvE .marker.cross{stroke:#0b0b0b;}#mermaid-svg-sLKJ2XBa2UrAoHvE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;}#mermaid-svg-sLKJ2XBa2UrAoHvE p{margin:0;}#mermaid-svg-sLKJ2XBa2UrAoHvE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sLKJ2XBa2UrAoHvE .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-sLKJ2XBa2UrAoHvE .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-sLKJ2XBa2UrAoHvE .cluster-label span p{background-color:transparent;}#mermaid-svg-sLKJ2XBa2UrAoHvE .label text,#mermaid-svg-sLKJ2XBa2UrAoHvE span{fill:#333;color:#333;}#mermaid-svg-sLKJ2XBa2UrAoHvE .node rect,#mermaid-svg-sLKJ2XBa2UrAoHvE .node circle,#mermaid-svg-sLKJ2XBa2UrAoHvE .node ellipse,#mermaid-svg-sLKJ2XBa2UrAoHvE .node polygon,#mermaid-svg-sLKJ2XBa2UrAoHvE .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-sLKJ2XBa2UrAoHvE .rough-node .label text,#mermaid-svg-sLKJ2XBa2UrAoHvE .node .label text,#mermaid-svg-sLKJ2XBa2UrAoHvE .image-shape .label,#mermaid-svg-sLKJ2XBa2UrAoHvE .icon-shape .label{text-anchor:middle;}#mermaid-svg-sLKJ2XBa2UrAoHvE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sLKJ2XBa2UrAoHvE .rough-node .label,#mermaid-svg-sLKJ2XBa2UrAoHvE .node .label,#mermaid-svg-sLKJ2XBa2UrAoHvE .image-shape .label,#mermaid-svg-sLKJ2XBa2UrAoHvE .icon-shape .label{text-align:center;}#mermaid-svg-sLKJ2XBa2UrAoHvE .node.clickable{cursor:pointer;}#mermaid-svg-sLKJ2XBa2UrAoHvE .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-sLKJ2XBa2UrAoHvE .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-sLKJ2XBa2UrAoHvE .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-sLKJ2XBa2UrAoHvE .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-sLKJ2XBa2UrAoHvE .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-sLKJ2XBa2UrAoHvE .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-sLKJ2XBa2UrAoHvE .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-sLKJ2XBa2UrAoHvE .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-sLKJ2XBa2UrAoHvE .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-sLKJ2XBa2UrAoHvE .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-sLKJ2XBa2UrAoHvE .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-sLKJ2XBa2UrAoHvE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sLKJ2XBa2UrAoHvE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sLKJ2XBa2UrAoHvE rect.text{fill:none;stroke-width:0;}#mermaid-svg-sLKJ2XBa2UrAoHvE .icon-shape,#mermaid-svg-sLKJ2XBa2UrAoHvE .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-sLKJ2XBa2UrAoHvE .icon-shape p,#mermaid-svg-sLKJ2XBa2UrAoHvE .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-sLKJ2XBa2UrAoHvE .icon-shape .label rect,#mermaid-svg-sLKJ2XBa2UrAoHvE .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-sLKJ2XBa2UrAoHvE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sLKJ2XBa2UrAoHvE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sLKJ2XBa2UrAoHvE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 三次握手完成
accept 取出
客户端 SYN 到达
半连接队列

syn queue

SYN_RCVD 状态
全连接队列

accept queue

ESTABLISHED 状态
服务端处理
backlog 限制的是

全连接队列大小

内核为每个监听套接字维护两个队列:

  • 半连接队列(syn queue) :收到 SYN、回复 SYN+ACK 后,正在等客户端 ACK 的连接。处于 SYN_RCVD 状态。
  • 全连接队列(accept queue) :三次握手已完成、等待 accept() 取走的连接。处于 ESTABLISHED 状态。

backlog 限制的是全连接队列的大小。当全连接队列满了,新的已完成连接会被丢弃,新来的 SYN 也可能被忽略。

backlog 就像餐厅门口的等位区------坐满了就告诉新来的客人"您稍等",等不及的就走了。

0.4.3 接口
c 复制代码
int listen(int sockfd, int backlog);
// sockfd:  已经 bind 的套接字
// backlog: 全连接队列最大长度
// 返回 0 成功,-1 失败

listen() 调用完立刻返回,不阻塞 。它只是在内核中标记这个 fd 为"可接受连接",设置好队列长度上限,然后就完事了------真正"等客户来"是在 accept() 里完成的。


0.5 accept------获取连接,两种套接字的分工

0.5.1 概念

listen() 只是贴出告示"本店营业中",真正把客人领进门的是 accept()

accept() 做的事:从全连接队列中取出一个已完成三次握手的连接,返回一个新的文件描述符。通过后两个输出型参数,还能知道是谁连过来的 ------这和 UDP 的 recvfrom() 差不多,recvfrom() 在拿数据的同时顺便拿地址,而 TCP 因为面向连接,得先知道是谁连的,后面才能通信

0.5.2 接口
c 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// sockfd:  监听套接字
// addr:    输出型参数------客户端地址
// addrlen: 值-结果参数------地址长度
// 返回值:   成功返回一个新的 fd,失败返回 -1
  • sockfd:必须是调用过 listen() 的监听套接字
  • addr / addrlen:和 recvfrom() 的后两个参数完全一样的语义------输出型参数,传出客户端的 IP 和端口
  • 最重要的------返回值 :成功时返回一个新的文件描述符
0.5.3 为什么 accept 要返回一个新 fd?

这是 TCP 编程面试最爱问的问题。

之前的 socket() 已经创建了一个 fd,为什么 accept() 又要返回一个 fd?这两个 fd 有什么区别?

类比:餐厅经营。
#mermaid-svg-2QGSTyxAjEi7I9tl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2QGSTyxAjEi7I9tl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2QGSTyxAjEi7I9tl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2QGSTyxAjEi7I9tl .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-2QGSTyxAjEi7I9tl .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-2QGSTyxAjEi7I9tl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2QGSTyxAjEi7I9tl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2QGSTyxAjEi7I9tl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2QGSTyxAjEi7I9tl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2QGSTyxAjEi7I9tl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2QGSTyxAjEi7I9tl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2QGSTyxAjEi7I9tl .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-2QGSTyxAjEi7I9tl .marker.cross{stroke:#0b0b0b;}#mermaid-svg-2QGSTyxAjEi7I9tl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;}#mermaid-svg-2QGSTyxAjEi7I9tl p{margin:0;}#mermaid-svg-2QGSTyxAjEi7I9tl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2QGSTyxAjEi7I9tl .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-2QGSTyxAjEi7I9tl .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-2QGSTyxAjEi7I9tl .cluster-label span p{background-color:transparent;}#mermaid-svg-2QGSTyxAjEi7I9tl .label text,#mermaid-svg-2QGSTyxAjEi7I9tl span{fill:#333;color:#333;}#mermaid-svg-2QGSTyxAjEi7I9tl .node rect,#mermaid-svg-2QGSTyxAjEi7I9tl .node circle,#mermaid-svg-2QGSTyxAjEi7I9tl .node ellipse,#mermaid-svg-2QGSTyxAjEi7I9tl .node polygon,#mermaid-svg-2QGSTyxAjEi7I9tl .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-2QGSTyxAjEi7I9tl .rough-node .label text,#mermaid-svg-2QGSTyxAjEi7I9tl .node .label text,#mermaid-svg-2QGSTyxAjEi7I9tl .image-shape .label,#mermaid-svg-2QGSTyxAjEi7I9tl .icon-shape .label{text-anchor:middle;}#mermaid-svg-2QGSTyxAjEi7I9tl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2QGSTyxAjEi7I9tl .rough-node .label,#mermaid-svg-2QGSTyxAjEi7I9tl .node .label,#mermaid-svg-2QGSTyxAjEi7I9tl .image-shape .label,#mermaid-svg-2QGSTyxAjEi7I9tl .icon-shape .label{text-align:center;}#mermaid-svg-2QGSTyxAjEi7I9tl .node.clickable{cursor:pointer;}#mermaid-svg-2QGSTyxAjEi7I9tl .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-2QGSTyxAjEi7I9tl .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-2QGSTyxAjEi7I9tl .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-2QGSTyxAjEi7I9tl .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-2QGSTyxAjEi7I9tl .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-2QGSTyxAjEi7I9tl .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-2QGSTyxAjEi7I9tl .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-2QGSTyxAjEi7I9tl .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-2QGSTyxAjEi7I9tl .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-2QGSTyxAjEi7I9tl .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-2QGSTyxAjEi7I9tl .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-2QGSTyxAjEi7I9tl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2QGSTyxAjEi7I9tl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2QGSTyxAjEi7I9tl rect.text{fill:none;stroke-width:0;}#mermaid-svg-2QGSTyxAjEi7I9tl .icon-shape,#mermaid-svg-2QGSTyxAjEi7I9tl .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-2QGSTyxAjEi7I9tl .icon-shape p,#mermaid-svg-2QGSTyxAjEi7I9tl .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-2QGSTyxAjEi7I9tl .icon-shape .label rect,#mermaid-svg-2QGSTyxAjEi7I9tl .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-2QGSTyxAjEi7I9tl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2QGSTyxAjEi7I9tl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2QGSTyxAjEi7I9tl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 餐厅
accept() 分配
accept() 分配
🚪 门口迎宾

------站在门口大喊'里面请!'

让客人排成队
🍽️ 服务员

------领一桌客人到座位

只为一桌服务
🍽️ 服务员

------领另一桌客人

只为另一桌服务
listen_socket

(监听套接字)
service_socket₁

(服务套接字)
service_socket₂

(服务套接字)

  • 监听套接字 (listen socket,即 socket() 创建的那个):相当于饭店门口拉客的 。他的工作就是喊人、让客人排队,每一次调用 accept() 就喊一个人进来。他从不亲自"服务"------他只负责引入。
  • 服务套接字 (service socket,即 accept() 返回的新 fd):相当于餐厅里的服务员。每领一桌客人进店,就分配一个专属服务员。这个服务员只为这一桌服务------递菜单、上菜、结账。
对比维度 listen socket(监听套接字) service socket(服务套接字)
来源 socket() 创建 accept() 返回
数量 一个服务器通常只有 1 个 每个客户端连接一个
生命周期 服务器运行期间一直存在 对应连接的客户端断开后就 close
职责 只负责"迎客"------执行 listen() + accept() 只负责"服务"------执行 recv() / send()
绑定端口 绑定了服务器的端口 复用同一个端口(四元组区分)

这个设计的好处是:一个服务器可以同时服务多个客户端 。主线程在监听 fd 上 accept() 等新连接,拿到 service fd 后丢给线程/线程池去处理,自己回去继续 accept() 等下一个------这就是并发服务器的经典模型。

一句话:监听套接字是"引客的",服务套接字是"待客的"。 每接到一个连接,内核就"克隆"出一个新的 fd 专门服务它------原来那个 fd 继续站在门口等下一个客人。


0.6 connect------发起连接

0.6.1 概念

服务器准备好了(listen),客户端怎么连过去?用 connect()

connect() 是客户端的接口------向指定服务器发起 TCP 三次握手。

0.6.2 接口
c 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// sockfd:  客户端自己的套接字
// addr:    目标服务器的地址(IP + 端口)
// addrlen: 地址长度
// 返回 0 成功,-1 失败
  • sockfd:客户端通过 socket(AF_INET, SOCK_STREAM, 0) 创建的套接字
  • addr:指向 sockaddr_in,填服务器的 IP 和端口
  • 返回值:0 表示三次握手成功,-1 表示连接失败
0.6.3 connect 做两件事

调用 connect(),内核在背后做了两件事:

  1. 隐式绑本地随机端口 ------如果这个 socket 之前没有调过 bind(),内核自动为它分配一个可用的随机端口。这就是为什么客户端通常不需要显式 bind()
  2. 发起三次握手 ------向目标服务器发送 SYN,等待 SYN+ACK,回复 ACK。这三次报文在 connect() 返回之前全部完成。

服务器 内核 客户端程序 服务器 内核 客户端程序 #mermaid-svg-9DqGRZemDkFJw8z6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9DqGRZemDkFJw8z6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9DqGRZemDkFJw8z6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9DqGRZemDkFJw8z6 .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-9DqGRZemDkFJw8z6 .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-9DqGRZemDkFJw8z6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9DqGRZemDkFJw8z6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9DqGRZemDkFJw8z6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9DqGRZemDkFJw8z6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9DqGRZemDkFJw8z6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9DqGRZemDkFJw8z6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9DqGRZemDkFJw8z6 .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-9DqGRZemDkFJw8z6 .marker.cross{stroke:#0b0b0b;}#mermaid-svg-9DqGRZemDkFJw8z6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;}#mermaid-svg-9DqGRZemDkFJw8z6 p{margin:0;}#mermaid-svg-9DqGRZemDkFJw8z6 .actor{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-9DqGRZemDkFJw8z6 text.actor>tspan{fill:#333;stroke:none;}#mermaid-svg-9DqGRZemDkFJw8z6 .actor-line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);}#mermaid-svg-9DqGRZemDkFJw8z6 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-9DqGRZemDkFJw8z6 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-9DqGRZemDkFJw8z6 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-9DqGRZemDkFJw8z6 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-9DqGRZemDkFJw8z6 .sequenceNumber{fill:#f4f4f4;}#mermaid-svg-9DqGRZemDkFJw8z6 #sequencenumber{fill:#333;}#mermaid-svg-9DqGRZemDkFJw8z6 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-9DqGRZemDkFJw8z6 .messageText{fill:#333;stroke:none;}#mermaid-svg-9DqGRZemDkFJw8z6 .labelBox{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-9DqGRZemDkFJw8z6 .labelText,#mermaid-svg-9DqGRZemDkFJw8z6 .labelText>tspan{fill:#333;stroke:none;}#mermaid-svg-9DqGRZemDkFJw8z6 .loopText,#mermaid-svg-9DqGRZemDkFJw8z6 .loopText>tspan{fill:#333;stroke:none;}#mermaid-svg-9DqGRZemDkFJw8z6 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:hsl(40.5882352941, 60%, 83.3333333333%);}#mermaid-svg-9DqGRZemDkFJw8z6 .note{stroke:hsl(52.6829268293, 60%, 73.9215686275%);fill:#fff5ad;}#mermaid-svg-9DqGRZemDkFJw8z6 .noteText,#mermaid-svg-9DqGRZemDkFJw8z6 .noteText>tspan{fill:#333;stroke:none;}#mermaid-svg-9DqGRZemDkFJw8z6 .activation0{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-9DqGRZemDkFJw8z6 .activation1{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-9DqGRZemDkFJw8z6 .activation2{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-9DqGRZemDkFJw8z6 .actorPopupMenu{position:absolute;}#mermaid-svg-9DqGRZemDkFJw8z6 .actorPopupMenuPanel{position:absolute;fill:#fff4dd;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-9DqGRZemDkFJw8z6 .actor-man line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-9DqGRZemDkFJw8z6 .actor-man circle,#mermaid-svg-9DqGRZemDkFJw8z6 line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;stroke-width:2px;}#mermaid-svg-9DqGRZemDkFJw8z6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① 检查是否 bind 了? 没有 → 自动分配随机端口 连接建立,可以 send/recv connect(server_addr) ② 发送 SYN ③ 回复 SYN+ACK ④ 回复 ACK 返回 0(三次握手完成)

connect() 不是"把数据发过去",而是"把路铺好"。路通了,后面 send()recv() 才走得稳。


0.7 TCP 编程的基本流程

把上面三个接口串起来,TCP 通信的完整流程如下:
#mermaid-svg-0wV27M4dPfn3ntEf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0wV27M4dPfn3ntEf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0wV27M4dPfn3ntEf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0wV27M4dPfn3ntEf .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-0wV27M4dPfn3ntEf .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-0wV27M4dPfn3ntEf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0wV27M4dPfn3ntEf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0wV27M4dPfn3ntEf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0wV27M4dPfn3ntEf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0wV27M4dPfn3ntEf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0wV27M4dPfn3ntEf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0wV27M4dPfn3ntEf .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-0wV27M4dPfn3ntEf .marker.cross{stroke:#0b0b0b;}#mermaid-svg-0wV27M4dPfn3ntEf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;}#mermaid-svg-0wV27M4dPfn3ntEf p{margin:0;}#mermaid-svg-0wV27M4dPfn3ntEf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0wV27M4dPfn3ntEf .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-0wV27M4dPfn3ntEf .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-0wV27M4dPfn3ntEf .cluster-label span p{background-color:transparent;}#mermaid-svg-0wV27M4dPfn3ntEf .label text,#mermaid-svg-0wV27M4dPfn3ntEf span{fill:#333;color:#333;}#mermaid-svg-0wV27M4dPfn3ntEf .node rect,#mermaid-svg-0wV27M4dPfn3ntEf .node circle,#mermaid-svg-0wV27M4dPfn3ntEf .node ellipse,#mermaid-svg-0wV27M4dPfn3ntEf .node polygon,#mermaid-svg-0wV27M4dPfn3ntEf .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-0wV27M4dPfn3ntEf .rough-node .label text,#mermaid-svg-0wV27M4dPfn3ntEf .node .label text,#mermaid-svg-0wV27M4dPfn3ntEf .image-shape .label,#mermaid-svg-0wV27M4dPfn3ntEf .icon-shape .label{text-anchor:middle;}#mermaid-svg-0wV27M4dPfn3ntEf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0wV27M4dPfn3ntEf .rough-node .label,#mermaid-svg-0wV27M4dPfn3ntEf .node .label,#mermaid-svg-0wV27M4dPfn3ntEf .image-shape .label,#mermaid-svg-0wV27M4dPfn3ntEf .icon-shape .label{text-align:center;}#mermaid-svg-0wV27M4dPfn3ntEf .node.clickable{cursor:pointer;}#mermaid-svg-0wV27M4dPfn3ntEf .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-0wV27M4dPfn3ntEf .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-0wV27M4dPfn3ntEf .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-0wV27M4dPfn3ntEf .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-0wV27M4dPfn3ntEf .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-0wV27M4dPfn3ntEf .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-0wV27M4dPfn3ntEf .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-0wV27M4dPfn3ntEf .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-0wV27M4dPfn3ntEf .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-0wV27M4dPfn3ntEf .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-0wV27M4dPfn3ntEf .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-0wV27M4dPfn3ntEf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0wV27M4dPfn3ntEf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0wV27M4dPfn3ntEf rect.text{fill:none;stroke-width:0;}#mermaid-svg-0wV27M4dPfn3ntEf .icon-shape,#mermaid-svg-0wV27M4dPfn3ntEf .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-0wV27M4dPfn3ntEf .icon-shape p,#mermaid-svg-0wV27M4dPfn3ntEf .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-0wV27M4dPfn3ntEf .icon-shape .label rect,#mermaid-svg-0wV27M4dPfn3ntEf .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-0wV27M4dPfn3ntEf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0wV27M4dPfn3ntEf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0wV27M4dPfn3ntEf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端
服务器
返回 service fd
三次握手
socket() 创建套接字
bind() 绑定端口
listen() 开始监听
accept() 阻塞等待连接
recv() / send() 收发数据
close() 关闭服务套接字
socket() 创建套接字
connect() 发起连接
send() / recv() 收发数据
close() 关闭套接字

知识点 :TCP 的每一步都有明确的"谁先谁后"------服务器必须先 listen,客户端才能 connect;服务器必须先 accept,才能 recv。这和 UDP"谁先发都行"的松散模式完全不同。


一、EchoTcpServer------单进程版

1.1 前言

零章中,我们把 TCP 比 UDP 多出来的三个接口过了一遍。现在把这些理论落成代码------先写一个最简单的 TCP Echo Server,收到什么就回什么。

和 UDP 博客中的 V1 EchoServer 一样,将服务器封装为 TcpServer 类:

  • Init() 初始化------创建字节流套接字、绑定端口、开始监听(三步变四步)
  • Start() 启动------死循环等待连接、收发数据

Echo Server 是网络编程的 "Hello World"。TCP 版本多了一步 listen(),整个服务器的语义就从"邮筒"变成了"营业厅"。

1.2 TcpServer 类------骨架

首先搭出类的骨架。一个 TCP 服务器最基本的私有成员就两个:监听套接字 fd 和端口号。

cpp 复制代码
class TcpServer
{
public:
    TcpServer(uint16_t port = gport) : _listen_sockfd(gsockfd), _port(port) {}
    ~TcpServer() { close(_listen_sockfd); }

private:
    int _listen_sockfd;  // 监听套接字------"门口迎宾"
    uint16_t _port;       // 服务器端口
};

_listen_sockfd 就是[零章 0.5.3](#零章 0.5.3)中说的监听套接字 ------它的唯一职责是站在门口等 accept(),不参与实际的数据收发。

知识点: 命名 _listen_sockfd 而不是 _sockfd,就是为了和后续 accept() 返回的 service socket 区分开。这是 TCP 独有的"一个服务器、两类 fd"问题。

1.3 Init()------初始化四步走

UDP 的 Init() 是三步:socket → 填充信息 → bind。TCP 在最后多了一步 listen(),告诉内核"这个端口可以接受连接了"。

cpp 复制代码
void Init()
{
    // 1. 创建套接字------注意是 SOCK_STREAM(字节流),不是 SOCK_DGRAM(数据报)
    _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (_listen_sockfd < 0)
    {
        LOG(LogLevel::FATAL) << "Create Socket Error";
        exit(SOCKET_ERR);
    }
    LOG(LogLevel::DEBUG) << "Create Socket Success: " << _listen_sockfd;

    // 2. 填充本地套接字信息
    struct sockaddr_in local;
    bzero(&local, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = INADDR_ANY;
    local.sin_port = htons(_port);

    // 3. bind------将 socket 与本地地址绑定
    int n = bind(_listen_sockfd, (struct sockaddr *)&local, sizeof(local));
    if (n < 0)
    {
        LOG(LogLevel::FATAL) << "Bind Socket Error";
        exit(BIND_ERR);
    }
    LOG(LogLevel::DEBUG) << "Bind Cocket Cuccess";

    // 4. listen------TCP 独有:将主动套接字变为被动套接字,开始监听
    n = listen(_listen_sockfd, gbacklog);
    if (n < 0)
    {
        LOG(LogLevel::FATAL) << "Listen Error";
        exit(LISTEN_ERR);
    }
    LOG(LogLevel::DEBUG) << "Listen Cuccess";
}
步骤 操作 UDP TCP
1 socket() SOCK_DGRAM SOCK_STREAM
2 填充 sockaddr_in 相同 相同
3 bind() 相同 相同
4 listen() 不需要 必须------设 backlog,转监听态

知识点: TCP 的 Init() 比 UDP 就多了最后一行的 listen()。但这一行是关键------没有它,accept() 会失败,客户端 connect() 也连不上。listen() 不阻塞,只是在内核中标记状态。

1.4 Start()------accept 获取连接 + ServiceIO

Init() 让服务器进入监听态,Start() 开始真正工作。和 UDP 的"直接 recvfrom() 收包"不同,TCP 必须先 accept() 获取连接,拿到服务套接字后才能通信

1.4.1 accept 获取连接
cpp 复制代码
void Start()
{
    while (true)
    {
        // 1. accept------从连接队列中取出一个客户端
        struct sockaddr_in clientaddr;
        socklen_t len = sizeof(clientaddr);
        int service_sockfd = accept(_listen_sockfd, (struct sockaddr *)&clientaddr, &len);
        if (service_sockfd < 0)
        {
            LOG(LogLevel::WARNING) << "Accept Error";
            continue;                        // 失败不退出,继续等下一个
        }
        LOG(LogLevel::DEBUG) << "Accept Success, service_sockfd: " << service_sockfd;

        // 2. 地址转换 → 交给 ServiceIO 做 Echo
        InetAddr client_address(clientaddr);
        ServiceIO(service_sockfd, client_address);
    }
}

拿到 service_sockfd 后,TCP 和 UDP 一样是全双工 的------可以在同一条连接上同时收发。拿到地址后转成 InetAddr(复用 UDP 项目中的封装),然后交给 ServiceIO() 做 Echo 服务。

注意 :当前是单进程模型------ServiceIO() 结束之前,accept() 不会再次调用。也就是说,同一时间只能服务一个客户端。这只是接口说明,后续会改进为并发模型。

1.4.2 ServiceIO------单进程 Echo 服务

TCP 是面向字节流的,可以直接用 read() / write() 收发消息(另一套接口是 recv() / send(),功能等价)。

cpp 复制代码
void ServiceIO(int sockfd, InetAddr &address)
{
    LOG(LogLevel::DEBUG) << "Client Info: " << address.ToString();

    while (true)
    {
        char inbuffer[1024];
        ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
        if (n > 0)
        {
            // 读到数据------加上前缀,原样写回
            inbuffer[n] = 0;
            LOG(LogLevel::INFO) << address.ToString() << " say# " << inbuffer;

            std::string echo_string = "Server say# ";
            echo_string += inbuffer;
            write(sockfd, echo_string.c_str(), echo_string.size());
        }
        else if (n == 0)
        {
            // 对端关闭连接(发送了 FIN)
            LOG(LogLevel::INFO) << "Client quit, address: " << address.ToString();
            break;
        }
        else
        {
            // n < 0:读出错
            LOG(LogLevel::ERROR) << "Client read error, address: " << address.ToString();
            break;
        }
    }
}

read() 的三种返回值是 TCP 编程的基本功:

返回值 含义 处理
n > 0 读到了 n 字节数据 正常处理------这里做 Echo 回写
n == 0 对端关闭了连接(FIN) break 退出循环,结束本次服务
n < 0 读出错 break 退出,记录错误日志

知识点: read() 返回 0 是 TCP 独有的信号------UDP 的 recvfrom() 永远不会返回 0(因为 UDP 无连接,不存在"对端关闭"这回事)。这是 TCP 面向连接的本质体现。

1.5 main()------组装启动

和 UDP 博客中的 EchoServer 一样,main() 负责解析命令行参数 → 创建 TcpServerInitStart

cpp 复制代码
#include <memory>
#include "TcpServer.hpp"

static void Usage(const std::string &process)
{
    std::cerr << "Usage: \n\t";
    std::cerr << process << " local_port" << std::endl;
}

// ./server port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
    }

    uint16_t server_port = std::stoi(argv[1]);

    std::unique_ptr<TcpServer> tcsr = std::make_unique<TcpServer>(server_port);
    tcsr->Init();
    tcsr->Start();
    return 0;
}

unique_ptr 管理 TcpServer 生命周期,Init() 失败直接 exit(),不会泄漏资源。

1.6 EchoTcpClient------客户端

客户端的流程和 UDP 版本几乎一样:获取服务端 IP + 端口 → 创建 socket → 发起连接 → 通信。

关于"要不要 bind()"这个问题,TCP 客户端的答案和 UDP 一样:需要端口号,但不显式调用 bind() 客户端软件是各个公司各自开发的,谁也不知道别人用了什么端口号。如果写死一个端口,万一和另一个客户端冲突,两个程序都起不来。所以正确做法是------让 OS 随机分配一个空闲端口,端口号是几不重要,只要唯一就可以了。

TCP 的优势在于:connect() 内部就包含了"需要时自动 bind"的逻辑------一步搞定两件事。

cpp 复制代码
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return -1;
    }

    // 1. 解析服务器地址
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    // 2. 创建 TCP 套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    // 3. connect------隐式 bind + 三次握手
    InetAddr server_address(server_ip, server_port);
    int n = connect(sockfd, (struct sockaddr*)server_address.GetNetAddress(), server_address.Len());
    if (n < 0)
    {
        std::cerr << "Connect to " << server_address.ToString() << " Failed!" << std::endl;
        exit(3);
    }
    std::cout << "Connect to " << server_address.ToString() << " Success!" << std::endl;

    // 4. 通信------读用户输入、发送、读回显
    while (true)
    {
        std::string line;
        std::cout << "Please Enter# ";
        std::getline(std::cin, line);

        write(sockfd, line.c_str(), line.size());

        char inbuffer[1024];
        ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
        if (n > 0)
        {
            inbuffer[n] = 0;
            std::cout << inbuffer << std::endl;
        }
        else if (n == 0)
        {
            std::cout << "Read End Of File!" << std::endl;
            break;
        }
        else
        {
            std::cerr << "Read Error!" << std::endl;
            break;
        }
    }
    return 0;
}

同样用 read() 的三种返回值来判断:读到了数据、连接断开、还是出错。

知识点: 对比 UDP 客户端需要调 sendto() + recvfrom()(每次带目标地址),TCP 客户端用 write() + read()(连接建立后不再需要地址)。这不是接口不同的问题,而是面向连接 vs 无连接的编程模型差异------连接一旦建立,内核记住了四元组,后面只管读写就行了。

1.7 运行验证------netstat 看 LISTEN 状态

编译运行,看日志确认三步全部成功:

shell 复制代码
root@ALiServer:1.EchoTcpServer# ./server_echo 12345
[2026-07-22 19:41:41.34132] [DEBUG] [1067047] [TcpServer.hpp] [40]  - Create Socket Success: 3
[2026-07-22 19:41:41.34443] [DEBUG] [1067047] [TcpServer.hpp] [56]  - Bind Cocket Cuccess
[2026-07-22 19:41:41.34461] [DEBUG] [1067047] [TcpServer.hpp] [65]  - Listen Cuccess

这是最关键的一步验证------用 netstat -antp 看一下端口状态:

shell 复制代码
root@ALiServer:1.EchoTcpServer# netstat -antp
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:12345           0.0.0.0:*               LISTEN      1067047/./server_ec

看到 State: LISTEN,说明 listen() 生效了------内核已把这个端口标记为"可接受连接"。

UDP 用 netstat -anup 看的时候,State 那一列是空的 ------因为 UDP 无连接,没有"监听态"这个概念。TCP 的 LISTEN 状态就是"开门营业"的信号灯。

知识点: netstat 的 State 列是 TCP 独有的诊断利器。LISTEN = 服务器已就绪,ESTABLISHED = 连接已建立。UDP 没有状态机,所以这一列永远为空。


二、并发模型演进------多进程 → 多线程 → 线程池

2.1 单进程的瓶颈

第一章的单进程 EchoServer 有一个致命问题:ServiceIO() 是一个死循环,一个客户端连上来后,服务器就陷在里面出不来了------accept() 不会再被调用,其他客户端只能等着。
#mermaid-svg-aTPrEYOo8Q7v6fyW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aTPrEYOo8Q7v6fyW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aTPrEYOo8Q7v6fyW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aTPrEYOo8Q7v6fyW .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-aTPrEYOo8Q7v6fyW .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-aTPrEYOo8Q7v6fyW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aTPrEYOo8Q7v6fyW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aTPrEYOo8Q7v6fyW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aTPrEYOo8Q7v6fyW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aTPrEYOo8Q7v6fyW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aTPrEYOo8Q7v6fyW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aTPrEYOo8Q7v6fyW .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-aTPrEYOo8Q7v6fyW .marker.cross{stroke:#0b0b0b;}#mermaid-svg-aTPrEYOo8Q7v6fyW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;}#mermaid-svg-aTPrEYOo8Q7v6fyW p{margin:0;}#mermaid-svg-aTPrEYOo8Q7v6fyW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aTPrEYOo8Q7v6fyW .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-aTPrEYOo8Q7v6fyW .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-aTPrEYOo8Q7v6fyW .cluster-label span p{background-color:transparent;}#mermaid-svg-aTPrEYOo8Q7v6fyW .label text,#mermaid-svg-aTPrEYOo8Q7v6fyW span{fill:#333;color:#333;}#mermaid-svg-aTPrEYOo8Q7v6fyW .node rect,#mermaid-svg-aTPrEYOo8Q7v6fyW .node circle,#mermaid-svg-aTPrEYOo8Q7v6fyW .node ellipse,#mermaid-svg-aTPrEYOo8Q7v6fyW .node polygon,#mermaid-svg-aTPrEYOo8Q7v6fyW .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-aTPrEYOo8Q7v6fyW .rough-node .label text,#mermaid-svg-aTPrEYOo8Q7v6fyW .node .label text,#mermaid-svg-aTPrEYOo8Q7v6fyW .image-shape .label,#mermaid-svg-aTPrEYOo8Q7v6fyW .icon-shape .label{text-anchor:middle;}#mermaid-svg-aTPrEYOo8Q7v6fyW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aTPrEYOo8Q7v6fyW .rough-node .label,#mermaid-svg-aTPrEYOo8Q7v6fyW .node .label,#mermaid-svg-aTPrEYOo8Q7v6fyW .image-shape .label,#mermaid-svg-aTPrEYOo8Q7v6fyW .icon-shape .label{text-align:center;}#mermaid-svg-aTPrEYOo8Q7v6fyW .node.clickable{cursor:pointer;}#mermaid-svg-aTPrEYOo8Q7v6fyW .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-aTPrEYOo8Q7v6fyW .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-aTPrEYOo8Q7v6fyW .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-aTPrEYOo8Q7v6fyW .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-aTPrEYOo8Q7v6fyW .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-aTPrEYOo8Q7v6fyW .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-aTPrEYOo8Q7v6fyW .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-aTPrEYOo8Q7v6fyW .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-aTPrEYOo8Q7v6fyW .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-aTPrEYOo8Q7v6fyW .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-aTPrEYOo8Q7v6fyW .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-aTPrEYOo8Q7v6fyW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aTPrEYOo8Q7v6fyW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aTPrEYOo8Q7v6fyW rect.text{fill:none;stroke-width:0;}#mermaid-svg-aTPrEYOo8Q7v6fyW .icon-shape,#mermaid-svg-aTPrEYOo8Q7v6fyW .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-aTPrEYOo8Q7v6fyW .icon-shape p,#mermaid-svg-aTPrEYOo8Q7v6fyW .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-aTPrEYOo8Q7v6fyW .icon-shape .label rect,#mermaid-svg-aTPrEYOo8Q7v6fyW .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-aTPrEYOo8Q7v6fyW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aTPrEYOo8Q7v6fyW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aTPrEYOo8Q7v6fyW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} A 断开
客户端 A connect
accept 获取 A
ServiceIO(A) 死循环
客户端 B connect
排队等待...

本质问题: accept()ServiceIO() 在同一个执行流里串行------一个客户端没服务完,下一个就进不来。解决方法就是把"服务"这个动作从主线程剥离出去


2.2 多进程版------fork 子进程处理服务

2.2.1 基本思路

父进程 accept() 拿到连接后,fork() 一个子进程去跑 ServiceIO(),自己立刻回到循环顶部继续 accept() 等下一个客户端。

cpp 复制代码
void Start()
{
    signal(SIGCHLD, SIG_IGN);  // 子进程退出后自动回收,不产生僵尸进程
    while (true)
    {
        struct sockaddr_in clientaddr;
        socklen_t len = sizeof(clientaddr);
        int service_sockfd = accept(_listen_sock_fd, (struct sockaddr *)&clientaddr, &len);
        if (service_sockfd < 0)
        {
            LOG(LogLevel::WARNING) << "Accept Error";
            continue;
        }
        LOG(LogLevel::DEBUG) << "Accept Success, service_sockfd: " << service_sockfd;

        // fork 子进程处理服务,父进程立刻回到 accept
        pid_t id = fork();
        if (id < 0)
        {
            LOG(LogLevel::FATAL) << "Fork Error";
            exit(FORK_ERR);
        }
        else if (id == 0)
        {
            // 子进程------处理客户端通信
            InetAddr client_address(clientaddr);
            ServiceIO(service_sockfd, client_address);
            close(service_sockfd);
            exit(0);
        }
        // 父进程------什么也不做,立刻回到 accept 等下一个
    }
}

signal(SIGCHLD, SIG_IGN) 是关键------没有它,每个子进程退出后变成僵尸进程,父进程还得 wait(),那就又阻塞了。设置 SIG_IGN 后,子进程退出时内核自动回收,父进程无需干预。

2.2.2 孤儿进程的取巧做法

还有一种"孙子进程"的取巧做法------让子进程再 fork() 一次然后立刻退出,真正干活的是孙子进程(孤儿,由 OS 回收):

cpp 复制代码
else if (id == 0)
{
    // 子进程------再 fork 一次,然后自己立刻退出
    if (fork() > 0) exit(0);

    // 孙子进程------变成孤儿,由 init(pid=1) 收养,OS 自动回收
    InetAddr client_address(clientaddr);
    ServiceIO(service_sockfd, client_address);
    close(service_sockfd);
    exit(0);
}
else
{
    // 父进程------wait 立刻返回(子进程已退出),不阻塞
    pid_t rid = waitpid(id, nullptr, 0);
    (void)rid;
}

逻辑链条:子进程创建孙子进程后立刻 exit(0) → 父进程 waitpid() 立刻返回 → 孙子进程的父进程没了,变成孤儿进程 ,被 init(pid=1)收养 → 退出时由 OS 自动回收。父进程不阻塞,也不产生僵尸。

运行两个客户端,ps -ajx 可以看到两个孙子进程的 PPID 是 1:

shell 复制代码
root@ALiServer:~# ps -ajx | grep server_echo
1087065 1091628 1091628 1087065 pts/2    1091628 S+       0   0:00 ./server_echo 12345
      1 1091647 1091628 1087065 pts/2    1091628 S+       0   0:00 ./server_echo 12345  # ← PPID=1
      1 1091664 1091628 1087065 pts/2    1091628 S+       0   0:00 ./server_echo 12345  # ← PPID=1

两种做法都可以------SIGCHLD + SIG_IGN 更简洁,孤儿进程法不需要信号。最佳实践还是信号。

2.2.3 关闭多余的文件描述符

fork() 之后父子进程各有一份文件描述符表的拷贝,必须关闭自己不需要的 fd:

cpp 复制代码
else if (id == 0)
{
    // 子进程------关闭不需要的 listen fd
    close(_listen_sock_fd);

    InetAddr client_address(clientaddr);
    ServiceIO(service_sockfd, client_address);
    close(service_sockfd);
    exit(0);
}
else
{
    // 父进程------关闭不需要的 service fd
    close(service_sockfd);
}

知识点: fork 之后 fd 的引用计数 +1。父进程不 close service_sockfd,即使子进程 close 了,对端也收不到 FIN------因为内核里这个 socket 的引用计数还不为零。

2.3 多线程版------pthread 轻量级服务

多进程的问题是:每来一个客户端就要 fork() 一个新进程------创建进程、拷贝页表、分配 PID......成本太高。

线程方案: 每个客户端分配一个线程去跑 ServiceIO()。线程共享进程的文件描述符表,所以传参只需把 service_sockfdthis 指针传进去。

cpp 复制代码
class ThreadData
{
public:
    ThreadData(TcpServer *ts, int sockfd, const InetAddr &addr)
        : _this(ts), _service_sockfd(sockfd), _addr(addr)
    {
    }
    ~ThreadData()
    {
        close(_service_sockfd);  // 线程退出时关闭 socket
    }

public:
    int _service_sockfd;
    TcpServer *_this;
    InetAddr _addr;
};

static void *ThreadRoutinue(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    pthread_detach(pthread_self());             // 分离------退出时自动回收
    td->_this->ServiceIO(td->_service_sockfd, td->_addr);
    delete td;                                  // 析构时 close(sockfd)
    return nullptr;
}

void Start()
{
    while (true)
    {
        // ... accept ...

        // 创建线程处理服务
        pthread_t tid;
        InetAddr client_address(clientaddr);
        ThreadData *td = new ThreadData(this, service_sockfd, client_address);
        pthread_create(&tid, nullptr, ThreadRoutinue, (void *)td);
    }
}

几个设计要点:

说明
ThreadData 打包传参------this 指针 + sockfd + 地址,避免用全局变量
pthread_detach 线程分离------主线程不需要 pthread_join,退出时自动回收
~ThreadData()close 线程结束 = 服务结束 = socket 该关了。注意这里不能在多线程中随便 close------文件描述符表是共享的,多线程版本中线程们看到的 fd 是同一个表,只在服务结束时才 close
static 成员函数 线程例程必须是普通函数指针,不能是成员函数(有隐式 this)。static 去掉了 this 依赖,再通过 ThreadDatathis 传回来

两个客户端同时在线,ps -aL 可以看到两个轻量级线程:

shell 复制代码
root@ALiServer:~# ps -aL
    PID     LWP TTY          TIME CMD
1095528 1095528 pts/2    00:00:00 server_echo     # ← 主线程
1095528 1095541 pts/2    00:00:00 server_echo     # ← 服务线程 1
1095528 1095573 pts/2    00:00:00 server_echo     # ← 服务线程 2

ps -aLps -ajx 多了一列 LWP(Light Weight Process,线程 ID)。同一个 PID、不同 LWP = 同一个进程内的多个线程。

当前是长连接长服务 模型------一个线程始终绑定在一个客户端上,直到客户端断开。这适用于小规模应用。客户端多了之后,线程数 = 连接数,切换开销会成为瓶颈。多路转接(I/O 多路复用) 是后面的课题。

2.4 线程池版------消灭创建开销

多线程版还有一个隐含问题:每次来一个连接就 pthread_create 一个新线程,服务结束就销毁。 创建和销毁线程本身也有开销。如果客户端频繁连了断、断了连,这个开销不可忽略。

解决思路:预创建一组线程,来一个新连接就把任务往队列里扔。 线程池是现成的------直接复用 UDP ChatServer 中的 ThreadPool 单例。

cpp 复制代码
// 线程池------任务入队
InetAddr client_address(clientaddr);
ThreadPool<task_t>::Instance()->Enqueue([this, service_sockfd, client_address]()
    {
        this->ServiceIO(service_sockfd, client_address);
        close(service_sockfd);
    });

对比:

维度 多线程 线程池
线程生命周期 来一个建一个,服务结束销毁 线程常驻,只取任务执行
创建开销 每次 pthread_create 无(线程已预热)
并发上限 等于连接数(可能到几百上千) 等于线程池大小(固定,如 8 个)
适用场景 连接数少且稳定 频繁连接断开

这里不再需要 pthread_detach、不需要 ThreadData------lambda 捕获了 thisservice_sockfdclient_address,入队后工作线程自动取任务执行。ServiceIO 结束后 close,线程回到池中等下一个任务。

知识点: 单进程 → 多进程 → 多线程 → 线程池的演进,本质上是在解决同一个问题------如何让"等客户端"和"服务客户端"不互相阻塞。 三种并发手段的复杂度和效率依次提升。

2.5 总结

三种并发模型的演进,本质上在解决同一个问题------如何让"等客户端"和"服务客户端"不互相阻塞:

模型 并发单元 开销 适用场景
多进程 进程(fork 大------拷贝页表、分配 PID 需要强隔离的场景
多线程 线程(pthread_create 中------创建/销毁线程 连接数少且稳定
线程池 预创建线程 + 任务队列 小------无创建销毁开销 频繁连接断开的通用场景

知识点: 单进程 → 多进程 → 多线程 → 线程池------每一步都是在找"更轻量的并发单元"。进程太重就换线程,线程创建太贵就预热成池。这个演进思路适用于所有需要并发的 TCP 服务端。


三、V2 RemoteSSH------远程命令执行器

3.1 前言------从 Echo 到"远程终端"

第一章的 EchoServer 收到什么就回什么,第二章解决了并发问题。但 EchoServer 有一个根本局限:业务逻辑硬编码在 TcpServer 里。

能不能让 TcpServer 只做"搬数据的人",不关心数据是什么?这样同一个 TcpServer,上面可以跑 Echo、跑远程命令、甚至跑在线 OJ------只需要换一个回调函数。

第二个动机是找一个更贴近实际应用场景的项目。我们每天都在用的 SSH 本质上就是:客户端发一条命令 → 服务器执行 → 把结果返回。把字符串当作命令去执行,逻辑链条是:
#mermaid-svg-DS00sFTmqpZh47m3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DS00sFTmqpZh47m3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DS00sFTmqpZh47m3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DS00sFTmqpZh47m3 .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-DS00sFTmqpZh47m3 .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-DS00sFTmqpZh47m3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DS00sFTmqpZh47m3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DS00sFTmqpZh47m3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DS00sFTmqpZh47m3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DS00sFTmqpZh47m3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DS00sFTmqpZh47m3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DS00sFTmqpZh47m3 .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-DS00sFTmqpZh47m3 .marker.cross{stroke:#0b0b0b;}#mermaid-svg-DS00sFTmqpZh47m3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;}#mermaid-svg-DS00sFTmqpZh47m3 p{margin:0;}#mermaid-svg-DS00sFTmqpZh47m3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DS00sFTmqpZh47m3 .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-DS00sFTmqpZh47m3 .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-DS00sFTmqpZh47m3 .cluster-label span p{background-color:transparent;}#mermaid-svg-DS00sFTmqpZh47m3 .label text,#mermaid-svg-DS00sFTmqpZh47m3 span{fill:#333;color:#333;}#mermaid-svg-DS00sFTmqpZh47m3 .node rect,#mermaid-svg-DS00sFTmqpZh47m3 .node circle,#mermaid-svg-DS00sFTmqpZh47m3 .node ellipse,#mermaid-svg-DS00sFTmqpZh47m3 .node polygon,#mermaid-svg-DS00sFTmqpZh47m3 .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-DS00sFTmqpZh47m3 .rough-node .label text,#mermaid-svg-DS00sFTmqpZh47m3 .node .label text,#mermaid-svg-DS00sFTmqpZh47m3 .image-shape .label,#mermaid-svg-DS00sFTmqpZh47m3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-DS00sFTmqpZh47m3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DS00sFTmqpZh47m3 .rough-node .label,#mermaid-svg-DS00sFTmqpZh47m3 .node .label,#mermaid-svg-DS00sFTmqpZh47m3 .image-shape .label,#mermaid-svg-DS00sFTmqpZh47m3 .icon-shape .label{text-align:center;}#mermaid-svg-DS00sFTmqpZh47m3 .node.clickable{cursor:pointer;}#mermaid-svg-DS00sFTmqpZh47m3 .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-DS00sFTmqpZh47m3 .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-DS00sFTmqpZh47m3 .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-DS00sFTmqpZh47m3 .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-DS00sFTmqpZh47m3 .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-DS00sFTmqpZh47m3 .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-DS00sFTmqpZh47m3 .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-DS00sFTmqpZh47m3 .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-DS00sFTmqpZh47m3 .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-DS00sFTmqpZh47m3 .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-DS00sFTmqpZh47m3 .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-DS00sFTmqpZh47m3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DS00sFTmqpZh47m3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DS00sFTmqpZh47m3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-DS00sFTmqpZh47m3 .icon-shape,#mermaid-svg-DS00sFTmqpZh47m3 .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-DS00sFTmqpZh47m3 .icon-shape p,#mermaid-svg-DS00sFTmqpZh47m3 .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-DS00sFTmqpZh47m3 .icon-shape .label rect,#mermaid-svg-DS00sFTmqpZh47m3 .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-DS00sFTmqpZh47m3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DS00sFTmqpZh47m3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DS00sFTmqpZh47m3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 字符串

ls -a -l
拆分为

argv\[\]
fork + exec*
结果写入管道
write 回客户端

更进一步------把字符串换成一段 C 代码,写入文件,fork + exec*("gcc", ...) 编译,再 fork + exec* 执行------这就是在线 OJ 判题系统的雏形。

RemoteSSH 是 EchoServer 的"回调改造版"------把 Echo 逻辑从 TcpServer 中剥离,换成了通用的"命令执行"回调。同一个 TcpServer 骨架,从此可以承载任意业务。

3.2 架构------三层分离

EchoServer 的问题:ServiceIO() 里写死了 "Server say# " + inbuffer。如果想把 Echo 改成执行 shell 命令,得改 TcpServer 的源码------这违背了"对修改封闭、对扩展开放"的原则。

RemoteSSH 引入了回调模式,把软件分成三层:
#mermaid-svg-2uV2C2QiooL1HXyy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2uV2C2QiooL1HXyy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2uV2C2QiooL1HXyy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2uV2C2QiooL1HXyy .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-2uV2C2QiooL1HXyy .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-2uV2C2QiooL1HXyy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2uV2C2QiooL1HXyy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2uV2C2QiooL1HXyy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2uV2C2QiooL1HXyy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2uV2C2QiooL1HXyy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2uV2C2QiooL1HXyy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2uV2C2QiooL1HXyy .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-2uV2C2QiooL1HXyy .marker.cross{stroke:#0b0b0b;}#mermaid-svg-2uV2C2QiooL1HXyy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;}#mermaid-svg-2uV2C2QiooL1HXyy p{margin:0;}#mermaid-svg-2uV2C2QiooL1HXyy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2uV2C2QiooL1HXyy .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-2uV2C2QiooL1HXyy .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-2uV2C2QiooL1HXyy .cluster-label span p{background-color:transparent;}#mermaid-svg-2uV2C2QiooL1HXyy .label text,#mermaid-svg-2uV2C2QiooL1HXyy span{fill:#333;color:#333;}#mermaid-svg-2uV2C2QiooL1HXyy .node rect,#mermaid-svg-2uV2C2QiooL1HXyy .node circle,#mermaid-svg-2uV2C2QiooL1HXyy .node ellipse,#mermaid-svg-2uV2C2QiooL1HXyy .node polygon,#mermaid-svg-2uV2C2QiooL1HXyy .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-2uV2C2QiooL1HXyy .rough-node .label text,#mermaid-svg-2uV2C2QiooL1HXyy .node .label text,#mermaid-svg-2uV2C2QiooL1HXyy .image-shape .label,#mermaid-svg-2uV2C2QiooL1HXyy .icon-shape .label{text-anchor:middle;}#mermaid-svg-2uV2C2QiooL1HXyy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2uV2C2QiooL1HXyy .rough-node .label,#mermaid-svg-2uV2C2QiooL1HXyy .node .label,#mermaid-svg-2uV2C2QiooL1HXyy .image-shape .label,#mermaid-svg-2uV2C2QiooL1HXyy .icon-shape .label{text-align:center;}#mermaid-svg-2uV2C2QiooL1HXyy .node.clickable{cursor:pointer;}#mermaid-svg-2uV2C2QiooL1HXyy .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-2uV2C2QiooL1HXyy .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-2uV2C2QiooL1HXyy .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-2uV2C2QiooL1HXyy .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-2uV2C2QiooL1HXyy .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-2uV2C2QiooL1HXyy .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-2uV2C2QiooL1HXyy .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-2uV2C2QiooL1HXyy .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-2uV2C2QiooL1HXyy .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-2uV2C2QiooL1HXyy .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-2uV2C2QiooL1HXyy .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-2uV2C2QiooL1HXyy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2uV2C2QiooL1HXyy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2uV2C2QiooL1HXyy rect.text{fill:none;stroke-width:0;}#mermaid-svg-2uV2C2QiooL1HXyy .icon-shape,#mermaid-svg-2uV2C2QiooL1HXyy .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-2uV2C2QiooL1HXyy .icon-shape p,#mermaid-svg-2uV2C2QiooL1HXyy .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-2uV2C2QiooL1HXyy .icon-shape .label rect,#mermaid-svg-2uV2C2QiooL1HXyy .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-2uV2C2QiooL1HXyy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2uV2C2QiooL1HXyy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2uV2C2QiooL1HXyy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 系统层
网络层
业务层
回调注入
Command 类

Execute() / GetCommandString()
TcpServer 类

只搬运字符串,不管内容
OS TCP/IP 协议栈

EchoServer(V1) RemoteSSH(V2)
业务逻辑 硬编码在 ServiceIO() Command 类独立封装
TcpServer 知道自己在做 Echo 不知道------只负责 read / 回调 / write
如何扩展 ServiceIO() 源码 换一个回调,不改 TcpServer 一行代码

本质变化:TcpServer 从"Echo 服务器"变成了"TCP 字节流搬运工"。 业务逻辑通过 Register() 注入,网络层不再依赖具体的业务。

3.3 TcpServer 改造------回调模式

3.3.1 回调类型定义

要支持任意的"命令执行"业务,需要两个回调:

cpp 复制代码
using handler_execute_t = std::function<std::string(std::string)>;
using handler_command_t = std::function<std::string()>;
回调 签名 职责
handler_execute string → string 输入一条命令字符串,返回执行结果
handler_command void → string 返回终端提示符(如 [root@ALiServer ~]#
3.3.2 Service()------两阶段交互协议

Echo 的 ServiceIO() 是一个简单的"读 → 回写"循环。RemoteSSH 的 Service() 设计了一个两阶段协议

cpp 复制代码
void Service(int sockfd, InetAddr address)
{
    while (true)
    {
        // 阶段 1:发送提示符 → 等待客户端输入命令
        HandlerCommand(sockfd, address);

        // 阶段 2:读取命令 → 回调执行 → 返回结果
        if (HandlerExecute(sockfd, address) <= 0)
            break;
    }
}

void HandlerCommand(int sockfd, InetAddr address)
{
    std::string result = _handler_command();      // 回调:获取提示符
    write(sockfd, result.c_str(), result.size()); // 发送给客户端
}

int HandlerExecute(int sockfd, InetAddr address)
{
    char buffer[1024];
    ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
    if (n > 0)
    {
        buffer[n] = 0;
        std::string result;
        if (nullptr != _handler_execute)
            result = _handler_execute(buffer);   // 回调:执行命令
        write(sockfd, result.c_str(), result.size());
    }
    return n;
}

交互时序:
Command(回调) TcpServer Client Command(回调) TcpServer Client #mermaid-svg-soPj4MnYVNBhD4hW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-soPj4MnYVNBhD4hW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-soPj4MnYVNBhD4hW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-soPj4MnYVNBhD4hW .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-soPj4MnYVNBhD4hW .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-soPj4MnYVNBhD4hW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-soPj4MnYVNBhD4hW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-soPj4MnYVNBhD4hW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-soPj4MnYVNBhD4hW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-soPj4MnYVNBhD4hW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-soPj4MnYVNBhD4hW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-soPj4MnYVNBhD4hW .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-soPj4MnYVNBhD4hW .marker.cross{stroke:#0b0b0b;}#mermaid-svg-soPj4MnYVNBhD4hW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;}#mermaid-svg-soPj4MnYVNBhD4hW p{margin:0;}#mermaid-svg-soPj4MnYVNBhD4hW .actor{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-soPj4MnYVNBhD4hW text.actor>tspan{fill:#333;stroke:none;}#mermaid-svg-soPj4MnYVNBhD4hW .actor-line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);}#mermaid-svg-soPj4MnYVNBhD4hW .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-soPj4MnYVNBhD4hW .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-soPj4MnYVNBhD4hW .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-soPj4MnYVNBhD4hW #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-soPj4MnYVNBhD4hW .sequenceNumber{fill:#f4f4f4;}#mermaid-svg-soPj4MnYVNBhD4hW #sequencenumber{fill:#333;}#mermaid-svg-soPj4MnYVNBhD4hW #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-soPj4MnYVNBhD4hW .messageText{fill:#333;stroke:none;}#mermaid-svg-soPj4MnYVNBhD4hW .labelBox{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-soPj4MnYVNBhD4hW .labelText,#mermaid-svg-soPj4MnYVNBhD4hW .labelText>tspan{fill:#333;stroke:none;}#mermaid-svg-soPj4MnYVNBhD4hW .loopText,#mermaid-svg-soPj4MnYVNBhD4hW .loopText>tspan{fill:#333;stroke:none;}#mermaid-svg-soPj4MnYVNBhD4hW .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:hsl(40.5882352941, 60%, 83.3333333333%);}#mermaid-svg-soPj4MnYVNBhD4hW .note{stroke:hsl(52.6829268293, 60%, 73.9215686275%);fill:#fff5ad;}#mermaid-svg-soPj4MnYVNBhD4hW .noteText,#mermaid-svg-soPj4MnYVNBhD4hW .noteText>tspan{fill:#333;stroke:none;}#mermaid-svg-soPj4MnYVNBhD4hW .activation0{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-soPj4MnYVNBhD4hW .activation1{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-soPj4MnYVNBhD4hW .activation2{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-soPj4MnYVNBhD4hW .actorPopupMenu{position:absolute;}#mermaid-svg-soPj4MnYVNBhD4hW .actorPopupMenuPanel{position:absolute;fill:#fff4dd;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-soPj4MnYVNBhD4hW .actor-man line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-soPj4MnYVNBhD4hW .actor-man circle,#mermaid-svg-soPj4MnYVNBhD4hW line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;stroke-width:2px;}#mermaid-svg-soPj4MnYVNBhD4hW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop 每一轮命令 _handler_command() "root@host \~ write(提示符) write("ls -l") _handler_execute("ls -l") 白名单检查 → popen → 输出 write(执行结果)

知识点: Echo 的协议是"客户端先发",RemoteSSH 的协议是"服务器先发提示符"。这个设计模仿了 SSH 的体验------客户端连接后先看到 [root@xxx]#,用户才知道可以输入命令了。

3.3.3 Register()------注入业务回调

回调通过 Register() 从外部注入:

cpp 复制代码
void Register(handler_execute_t handler_execute, handler_command_t handler_command)
{
    _handler_execute = handler_execute;
    _handler_command = handler_command;
}

TcpServer 不知道这两个回调具体做什么------它只知道"需要提示符就调 _handler_command(),需要执行就调 _handler_execute()"。换一个回调,TcpServer 的代码一行不变,但服务器的行为完全不同。

这和 UDP 博客中 V2 DictServer 的回调模式完全一致------把"做什么"从"怎么通信"中抽离,是网络编程的通用设计。

3.4 Command 模块------命令白名单与 popen 执行

Command 类是业务层,只做三件事:加载白名单、执行命令、生成提示符。

3.4.1 LoadWhiteList()------安全第一

远程执行命令最大的风险是任意命令注入------rm -rf /curl xxx | bash......所以必须加白名单:

cpp 复制代码
void LoadWhiteList()
{
    std::string path = "./whitelist.txt";
    std::fstream in(path);
    if (!in.is_open())
    {
        LOG(LogLevel::FATAL) << "Open File Error";
        exit(2);
    }
    std::string line;
    while (std::getline(in, line))
    {
        _whitelist.emplace_back(line);
    }
}

白名单配置文件 whitelist.txt,一行一条允许的命令:

复制代码
ls -a -l
ls -l
pwd
whoami
who
ps -al
netstat -nltp
netstat -nutp
netstat -natp
netstat -naup
cpp 复制代码
bool isSafe(const std::string &cmd)
{
    for (auto &elem : _whitelist)
        if (elem == cmd)
            return true;
    return false;
}

不在白名单的直接拒绝:

cpp 复制代码
if (!isSafe(cmd))
    return "Bad Man!";
3.4.2 Execute()------popen 管道执行

popen() 在内部做了三件事:fork 子进程 → 启动 /bin/sh -c <cmd> → 创建管道连接父子进程。父进程像读文件一样读管道的输出端,拿到命令的执行结果。

cpp 复制代码
std::string Execute(const std::string &cmd)
{
    if (!isSafe(cmd))
        return "Bad Man!";

    FILE *fp = popen(cmd.c_str(), "r");
    if (fp == nullptr)
        return "Execute Error";

    std::string result;
    char buffer[1024];
    while (fgets(buffer, sizeof(buffer), fp))
    {
        result += buffer;
    }
    pclose(fp);
    return result;
}
3.4.3 GetCommandString()------构建终端提示符

模仿 Linux 终端,动态生成 [username@hostname cwd]# 格式的提示符:

cpp 复制代码
std::string GetCommandString()
{
    struct passwd *pw = getpwuid(getuid());
    std::string username = (pw ? pw->pw_name : "unknown");

    char hostname[256];
    if (gethostname(hostname, sizeof(hostname)) != 0)
        snprintf(hostname, sizeof(hostname), "unknown");

    char cwd[1024];
    if (getcwd(cwd, sizeof(cwd)) == NULL)
        snprintf(cwd, sizeof(cwd), "unknown");
    char *pcwd = cwd + strlen(cwd) - 1;
    while (*pcwd != '/')
        pcwd--;
    return "[" + username + "@" + hostname + " " + pcwd + "]# ";
}

三个系统调用的链式调用:getuid()getpwuid()pw_name 拿用户名,gethostname() 拿主机名,getcwd() 拿当前目录。提示符最终长这样:

复制代码
[root@ALiServer ~]# 

3.5 SSH_Server------组装

main() 中创建 CommandTcpServer,通过 Register() 将两者绑定:

cpp 复制代码
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
    }

    uint16_t server_port = std::stoi(argv[1]);
    std::unique_ptr<Command> pcmd = std::make_unique<Command>();
    std::unique_ptr<TcpServer> ptcsr = std::make_unique<TcpServer>(server_port);
    ptcsr->Init();
    ptcsr->Register(
        [&pcmd](std::string cmd) -> std::string {
            return pcmd->Execute(cmd);          // handler_execute
        },
        [&pcmd]() -> std::string {
            return pcmd->GetCommandString();    // handler_command
        }
    );
    ptcsr->Start();
    return 0;
}

unique_ptr 管理两个对象的生命周期,lambda 捕获 pcmd 指针将回调路由到 Command 的方法。整个组装就三步:InitRegisterStart

3.6 SSH_Client------交互式客户端

客户端的行为更接近真实 SSH:先收到服务器的提示符 → 显示 → 用户输入命令 → 发送 → 等待结果 → 循环。

cpp 复制代码
while (true)
{
    // 1. 接收服务器的提示符
    char commandline[1024];
    std::string commandlinestr;
    ssize_t n = read(sockfd, commandline, sizeof(commandline) - 1);
    if (n > 0)
    {
        commandline[n] = 0;
        commandlinestr = commandline;
    }
    else
    {
        commandlinestr = "[unknow@unknown None]# ";
    }
    std::cout << commandlinestr;          // 显示提示符,不换行

    // 2. 用户输入命令
    std::string command;
    std::getline(std::cin, command);
    write(sockfd, command.c_str(), command.size());

    // 3. 接收并显示执行结果
    char inbuffer[1024];
    n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
    if (n > 0)
    {
        inbuffer[n] = 0;
        std::cout << inbuffer << std::endl;
    }
    else if (n == 0)
    {
        std::cout << "Read End Of File!" << std::endl;
        break;
    }
    else
    {
        std::cerr << "Read Error!" << std::endl;
        break;
    }
}

对比 EchoClient:

维度 EchoClient SSH_Client
谁先发 客户端先发用户输入 服务器先发提示符
交互步骤 发 → 收(一步) 收提示符 → 发命令 → 收结果(三步)
用户体验 Please Enter# 固定提示 [root@host cwd]# 动态提示符

知识点: 客户端从"我来开头"变成了"等服务端先说"。这个倒转是因为 RemoteSSH 的协议设计更接近真实终端------服务器告诉客户端"我准备好了",客户端才发送命令。

3.7 TcpServer 的演化总结

把 EchoServer 和 RemoteSSH 的 TcpServer 放在一起比较,核心变化只有一点:ServiceIO() 变成了 Service() + 回调。
#mermaid-svg-iEnWBFEtWnURpkOF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iEnWBFEtWnURpkOF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iEnWBFEtWnURpkOF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iEnWBFEtWnURpkOF .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-iEnWBFEtWnURpkOF .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-iEnWBFEtWnURpkOF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iEnWBFEtWnURpkOF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iEnWBFEtWnURpkOF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iEnWBFEtWnURpkOF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iEnWBFEtWnURpkOF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iEnWBFEtWnURpkOF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iEnWBFEtWnURpkOF .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-iEnWBFEtWnURpkOF .marker.cross{stroke:#0b0b0b;}#mermaid-svg-iEnWBFEtWnURpkOF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;}#mermaid-svg-iEnWBFEtWnURpkOF p{margin:0;}#mermaid-svg-iEnWBFEtWnURpkOF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iEnWBFEtWnURpkOF .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-iEnWBFEtWnURpkOF .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-iEnWBFEtWnURpkOF .cluster-label span p{background-color:transparent;}#mermaid-svg-iEnWBFEtWnURpkOF .label text,#mermaid-svg-iEnWBFEtWnURpkOF span{fill:#333;color:#333;}#mermaid-svg-iEnWBFEtWnURpkOF .node rect,#mermaid-svg-iEnWBFEtWnURpkOF .node circle,#mermaid-svg-iEnWBFEtWnURpkOF .node ellipse,#mermaid-svg-iEnWBFEtWnURpkOF .node polygon,#mermaid-svg-iEnWBFEtWnURpkOF .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-iEnWBFEtWnURpkOF .rough-node .label text,#mermaid-svg-iEnWBFEtWnURpkOF .node .label text,#mermaid-svg-iEnWBFEtWnURpkOF .image-shape .label,#mermaid-svg-iEnWBFEtWnURpkOF .icon-shape .label{text-anchor:middle;}#mermaid-svg-iEnWBFEtWnURpkOF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iEnWBFEtWnURpkOF .rough-node .label,#mermaid-svg-iEnWBFEtWnURpkOF .node .label,#mermaid-svg-iEnWBFEtWnURpkOF .image-shape .label,#mermaid-svg-iEnWBFEtWnURpkOF .icon-shape .label{text-align:center;}#mermaid-svg-iEnWBFEtWnURpkOF .node.clickable{cursor:pointer;}#mermaid-svg-iEnWBFEtWnURpkOF .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-iEnWBFEtWnURpkOF .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-iEnWBFEtWnURpkOF .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-iEnWBFEtWnURpkOF .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-iEnWBFEtWnURpkOF .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-iEnWBFEtWnURpkOF .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-iEnWBFEtWnURpkOF .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-iEnWBFEtWnURpkOF .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-iEnWBFEtWnURpkOF .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-iEnWBFEtWnURpkOF .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-iEnWBFEtWnURpkOF .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-iEnWBFEtWnURpkOF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iEnWBFEtWnURpkOF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iEnWBFEtWnURpkOF rect.text{fill:none;stroke-width:0;}#mermaid-svg-iEnWBFEtWnURpkOF .icon-shape,#mermaid-svg-iEnWBFEtWnURpkOF .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-iEnWBFEtWnURpkOF .icon-shape p,#mermaid-svg-iEnWBFEtWnURpkOF .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-iEnWBFEtWnURpkOF .icon-shape .label rect,#mermaid-svg-iEnWBFEtWnURpkOF .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-iEnWBFEtWnURpkOF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iEnWBFEtWnURpkOF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iEnWBFEtWnURpkOF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} RemoteSSH
accept()
Service()
HandlerCommand()

→ 回调: 获取提示符 → write
HandlerExecute()

→ read → 回调: 执行命令 → write
EchoServer
accept()
ServiceIO()
read()
硬编码: 加前缀 write 回去

Echo 的"收到什么回什么"和 RemoteSSH 的"收到命令去执行",对于 TcpServer 来说是一样的结构------都是 read → 回调处理 → write。 不同业务只需要换不同的回调,骨架不需要动。

知识点: 从 V1(Echo)到 V2(RemoteSSH),TcpServer 从"应用服务器"变成了"网络框架"。这是所有网络编程框架(gRPC、Thrift、Netty)的底层思想------通信层和业务层分离,通过回调/中间件连接。

相关推荐
ShirleyWang0121 小时前
Linux中Vim编辑器快速找到需要改的行
linux·编辑器·vim
MXsoft6183 小时前
在大型混合云网络环境中,如何高效实现自动化运维?
运维·网络·自动化
盐焗鹌鹑蛋3 小时前
【Linux】基本指令
linux
海阔天空任鸟飞~10 小时前
Linux 权限 777
linux·运维·服务器
Kina_C13 小时前
Linux iptables 防火墙原理与实操——从四表五链到 NAT 配置
linux·运维·服务器·iptables
wdfk_prog13 小时前
嵌入式面试真题第 15 题:不可恢复异常后的通用崩溃快照、调用栈保存与离线分析架构
linux·开发语言·面试·架构
xian_wwq17 小时前
【案例分析】Hugging Face生产基础设施入侵攻击分析
网络·安全
灵机一物17 小时前
企业选型参考:2026 CXL 内存扩展方案六强测评与落地指南
服务器·网络·数据库·人工智能
雷工笔记17 小时前
什么是Ubuntu?
linux·运维·ubuntu