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 + 端口填进去。不关心可以传NULLaddrlen:值-结果参数 ------传入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(),内核在背后做了两件事:
- 隐式绑本地随机端口 ------如果这个 socket 之前没有调过
bind(),内核自动为它分配一个可用的随机端口。这就是为什么客户端通常不需要显式bind()。 - 发起三次握手 ------向目标服务器发送 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() 负责解析命令行参数 → 创建 TcpServer → Init → Start。
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_sockfd 和 this 指针传进去。
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 依赖,再通过 ThreadData 把 this 传回来 |
两个客户端同时在线,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 -aL 比 ps -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 捕获了 this、service_sockfd、client_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() 中创建 Command 和 TcpServer,通过 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 的方法。整个组装就三步:Init → Register → Start。
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)的底层思想------通信层和业务层分离,通过回调/中间件连接。