TCP 并发服务器:连接分析、多线程与多进程

TCP 并发服务器:连接分析、多线程与多进程

课程:尚硅谷《嵌入式 Linux 应用层开发》第 6 章 Socket 编程

依据:2026-09-30 10:30 录音转写;课程 PDF 第 299---325 页。

本节边界:先用命令和 Wireshark 观察 TCP 连接,再分别用"每连接一个线程"和"每连接一个子进程"支持多个客户端。

页码校正:用户输入的"299-3164页"与录音内容不符;录音在第 316 页后继续完整讲解多进程服务端,实际对应到第 325 页。

1. 本节知识路线

#mermaid-svg-s0lh2K92caaDh2B9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-s0lh2K92caaDh2B9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-s0lh2K92caaDh2B9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-s0lh2K92caaDh2B9 .error-icon{fill:#552222;}#mermaid-svg-s0lh2K92caaDh2B9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-s0lh2K92caaDh2B9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-s0lh2K92caaDh2B9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-s0lh2K92caaDh2B9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-s0lh2K92caaDh2B9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-s0lh2K92caaDh2B9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-s0lh2K92caaDh2B9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-s0lh2K92caaDh2B9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-s0lh2K92caaDh2B9 .marker.cross{stroke:#333333;}#mermaid-svg-s0lh2K92caaDh2B9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-s0lh2K92caaDh2B9 p{margin:0;}#mermaid-svg-s0lh2K92caaDh2B9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-s0lh2K92caaDh2B9 .cluster-label text{fill:#333;}#mermaid-svg-s0lh2K92caaDh2B9 .cluster-label span{color:#333;}#mermaid-svg-s0lh2K92caaDh2B9 .cluster-label span p{background-color:transparent;}#mermaid-svg-s0lh2K92caaDh2B9 .label text,#mermaid-svg-s0lh2K92caaDh2B9 span{fill:#333;color:#333;}#mermaid-svg-s0lh2K92caaDh2B9 .node rect,#mermaid-svg-s0lh2K92caaDh2B9 .node circle,#mermaid-svg-s0lh2K92caaDh2B9 .node ellipse,#mermaid-svg-s0lh2K92caaDh2B9 .node polygon,#mermaid-svg-s0lh2K92caaDh2B9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-s0lh2K92caaDh2B9 .rough-node .label text,#mermaid-svg-s0lh2K92caaDh2B9 .node .label text,#mermaid-svg-s0lh2K92caaDh2B9 .image-shape .label,#mermaid-svg-s0lh2K92caaDh2B9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-s0lh2K92caaDh2B9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-s0lh2K92caaDh2B9 .rough-node .label,#mermaid-svg-s0lh2K92caaDh2B9 .node .label,#mermaid-svg-s0lh2K92caaDh2B9 .image-shape .label,#mermaid-svg-s0lh2K92caaDh2B9 .icon-shape .label{text-align:center;}#mermaid-svg-s0lh2K92caaDh2B9 .node.clickable{cursor:pointer;}#mermaid-svg-s0lh2K92caaDh2B9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-s0lh2K92caaDh2B9 .arrowheadPath{fill:#333333;}#mermaid-svg-s0lh2K92caaDh2B9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-s0lh2K92caaDh2B9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-s0lh2K92caaDh2B9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-s0lh2K92caaDh2B9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-s0lh2K92caaDh2B9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-s0lh2K92caaDh2B9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-s0lh2K92caaDh2B9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-s0lh2K92caaDh2B9 .cluster text{fill:#333;}#mermaid-svg-s0lh2K92caaDh2B9 .cluster span{color:#333;}#mermaid-svg-s0lh2K92caaDh2B9 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(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-s0lh2K92caaDh2B9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-s0lh2K92caaDh2B9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-s0lh2K92caaDh2B9 .icon-shape,#mermaid-svg-s0lh2K92caaDh2B9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-s0lh2K92caaDh2B9 .icon-shape p,#mermaid-svg-s0lh2K92caaDh2B9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-s0lh2K92caaDh2B9 .icon-shape .label rect,#mermaid-svg-s0lh2K92caaDh2B9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-s0lh2K92caaDh2B9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-s0lh2K92caaDh2B9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-s0lh2K92caaDh2B9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 查看监听与连接状态
抓取握手 数据 挥手
单连接服务端的限制
accept 放入循环
每连接一个线程
每连接一个子进程
线程参数与资源回收
描述符关闭与僵尸进程回收
多个客户端并发

老师这一节解决两个问题:

  1. 怎样观察 TCP 连接真实经历的状态和报文;
  2. 一个服务端怎样同时服务多个客户端。

2. 先分清监听 Socket 和连接 Socket

服务端调用:

text 复制代码
socket → bind → listen

得到的 listen_fd 只负责等待新连接。每次 accept() 成功,内核返回一个新的 client_fd,专门用于与某个客户端通信。

text 复制代码
listen_fd,local = 0.0.0.0:6666
    │
    ├─ accept → client_fd_1,peer = 127.0.0.1:43001
    ├─ accept → client_fd_2,peer = 127.0.0.1:43002
    └─ accept → client_fd_3,peer = 127.0.0.1:43003

这些连接 Socket 可以共享相同的服务端本地端口,因为 TCP 连接由四元组区分:

text 复制代码
源 IP + 源端口 + 目的 IP + 目的端口

因此,"一个端口只能属于一个进程,所以只能有一个 Socket"是不准确的。一个监听 Socket 可以产生很多连接 Socket;同一进程也可以同时拥有大量 Socket。

3. 用命令查看 TCP 状态

教材使用:

bash 复制代码
sudo netstat -tanlp | grep 6666

参数含义:

参数 含义
-t 只显示 TCP
-a 显示监听与非监听 Socket
-n 直接显示数字 IP 和端口,不解析名称
-l 显示监听 Socket
-p 显示关联进程,需要相应权限

现代 Linux 也可使用:

bash 复制代码
sudo ss -tanlp | grep ':6666'

常见状态:

状态 说明
LISTEN 服务端正在等待新连接
ESTABLISHED TCP 连接已建立,可双向传输
FIN-WAIT-1 主动关闭方已发送 FIN,等待确认或对方 FIN
FIN-WAIT-2 本方 FIN 已确认,等待对方 FIN
CLOSE-WAIT 已收到对方 FIN,等待本地应用关闭
TIME-WAIT 主动关闭方等待旧报文过期,并能重发最后 ACK

4. 用 Wireshark 对照握手、数据和挥手

抓取本机客户端与本机服务端通信时,应选择 loopback 接口,并使用显示过滤器:

text 复制代码
tcp.port == 6666

4.1 三次握手

服务端 客户端 服务端 客户端 #mermaid-svg-VYJGsZHlDAv5QWlP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VYJGsZHlDAv5QWlP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VYJGsZHlDAv5QWlP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VYJGsZHlDAv5QWlP .error-icon{fill:#552222;}#mermaid-svg-VYJGsZHlDAv5QWlP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VYJGsZHlDAv5QWlP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VYJGsZHlDAv5QWlP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VYJGsZHlDAv5QWlP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VYJGsZHlDAv5QWlP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VYJGsZHlDAv5QWlP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VYJGsZHlDAv5QWlP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VYJGsZHlDAv5QWlP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VYJGsZHlDAv5QWlP .marker.cross{stroke:#333333;}#mermaid-svg-VYJGsZHlDAv5QWlP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VYJGsZHlDAv5QWlP p{margin:0;}#mermaid-svg-VYJGsZHlDAv5QWlP .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VYJGsZHlDAv5QWlP text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-VYJGsZHlDAv5QWlP .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-VYJGsZHlDAv5QWlP .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-VYJGsZHlDAv5QWlP .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-VYJGsZHlDAv5QWlP .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-VYJGsZHlDAv5QWlP #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-VYJGsZHlDAv5QWlP .sequenceNumber{fill:white;}#mermaid-svg-VYJGsZHlDAv5QWlP #sequencenumber{fill:#333;}#mermaid-svg-VYJGsZHlDAv5QWlP #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-VYJGsZHlDAv5QWlP .messageText{fill:#333;stroke:none;}#mermaid-svg-VYJGsZHlDAv5QWlP .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VYJGsZHlDAv5QWlP .labelText,#mermaid-svg-VYJGsZHlDAv5QWlP .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-VYJGsZHlDAv5QWlP .loopText,#mermaid-svg-VYJGsZHlDAv5QWlP .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-VYJGsZHlDAv5QWlP .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-VYJGsZHlDAv5QWlP .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-VYJGsZHlDAv5QWlP .noteText,#mermaid-svg-VYJGsZHlDAv5QWlP .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-VYJGsZHlDAv5QWlP .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VYJGsZHlDAv5QWlP .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VYJGsZHlDAv5QWlP .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VYJGsZHlDAv5QWlP .actorPopupMenu{position:absolute;}#mermaid-svg-VYJGsZHlDAv5QWlP .actorPopupMenuPanel{position:absolute;fill:#ECECFF;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-VYJGsZHlDAv5QWlP .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VYJGsZHlDAv5QWlP .actor-man circle,#mermaid-svg-VYJGsZHlDAv5QWlP line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-VYJGsZHlDAv5QWlP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} SYN, seq=x SYN+ACK, seq=y, ack=x+1 ACK, ack=y+1

Wireshark 常把初始序列号显示为相对值 0,便于阅读。真实 TCP 初始序列号不是固定为 0。

4.2 数据阶段

TCP 两个方向各有独立的序列号空间:

  • 客户端发送数据时,客户端 seq 与服务端返回的 ack 对应;
  • 服务端发送数据时,服务端 seq 与客户端返回的 ack 对应;
  • 序列号按有效负载字节数前进,而不是按 send() 次数前进。

教材程序固定执行 send(fd, buf, 1024, 0),所以抓包中序列号可能每次增加 1024。这个现象来自示例实际发送了 1024 字节,并不是 TCP 每次固定发送 1024 字节。

4.3 Ctrl+D 与半关闭

Ctrl+D 是终端向程序报告输入结束 EOF。教材客户端或服务端的写线程读到 EOF 后调用:

c 复制代码
shutdown(fd, SHUT_WR);

这会关闭本端发送方向并触发 TCP FIN,但接收方向仍然可用:

text 复制代码
客户端先 SHUT_WR
    ↓
客户端:FIN-WAIT-1 → FIN-WAIT-2
服务端:CLOSE-WAIT
    ↓
服务端仍可发送,客户端仍可接收
    ↓
服务端也关闭发送方向
    ↓
客户端发送最后 ACK 并进入 TIME-WAIT

因此 Ctrl+D 本身不是 TCP 控制报文;是应用检测到 stdin EOF 后调用 shutdown(),内核才执行半关闭。

5. 为什么单连接服务端无法支持多个客户端

单连接程序通常按直线执行:

text 复制代码
accept 一个连接
    ↓
持续 recv/send 处理该客户端
    ↓
客户端断开
    ↓
程序结束

当主线程一直阻塞在第一个客户端的 recv() 上,它无法再次调用 accept(),后续客户端即使完成部分连接流程,也得不到应用层处理。

并发服务端要把职责拆开:

text 复制代码
主执行流:持续 accept 新连接
工作执行流:处理一个已经连接的 client_fd

工作执行流可以是线程,也可以是进程。

6. 多线程模型:每个连接一个线程

#mermaid-svg-usPtXD4X64CpDgKZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-usPtXD4X64CpDgKZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-usPtXD4X64CpDgKZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-usPtXD4X64CpDgKZ .error-icon{fill:#552222;}#mermaid-svg-usPtXD4X64CpDgKZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-usPtXD4X64CpDgKZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-usPtXD4X64CpDgKZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-usPtXD4X64CpDgKZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-usPtXD4X64CpDgKZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-usPtXD4X64CpDgKZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-usPtXD4X64CpDgKZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-usPtXD4X64CpDgKZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-usPtXD4X64CpDgKZ .marker.cross{stroke:#333333;}#mermaid-svg-usPtXD4X64CpDgKZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-usPtXD4X64CpDgKZ p{margin:0;}#mermaid-svg-usPtXD4X64CpDgKZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-usPtXD4X64CpDgKZ .cluster-label text{fill:#333;}#mermaid-svg-usPtXD4X64CpDgKZ .cluster-label span{color:#333;}#mermaid-svg-usPtXD4X64CpDgKZ .cluster-label span p{background-color:transparent;}#mermaid-svg-usPtXD4X64CpDgKZ .label text,#mermaid-svg-usPtXD4X64CpDgKZ span{fill:#333;color:#333;}#mermaid-svg-usPtXD4X64CpDgKZ .node rect,#mermaid-svg-usPtXD4X64CpDgKZ .node circle,#mermaid-svg-usPtXD4X64CpDgKZ .node ellipse,#mermaid-svg-usPtXD4X64CpDgKZ .node polygon,#mermaid-svg-usPtXD4X64CpDgKZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-usPtXD4X64CpDgKZ .rough-node .label text,#mermaid-svg-usPtXD4X64CpDgKZ .node .label text,#mermaid-svg-usPtXD4X64CpDgKZ .image-shape .label,#mermaid-svg-usPtXD4X64CpDgKZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-usPtXD4X64CpDgKZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-usPtXD4X64CpDgKZ .rough-node .label,#mermaid-svg-usPtXD4X64CpDgKZ .node .label,#mermaid-svg-usPtXD4X64CpDgKZ .image-shape .label,#mermaid-svg-usPtXD4X64CpDgKZ .icon-shape .label{text-align:center;}#mermaid-svg-usPtXD4X64CpDgKZ .node.clickable{cursor:pointer;}#mermaid-svg-usPtXD4X64CpDgKZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-usPtXD4X64CpDgKZ .arrowheadPath{fill:#333333;}#mermaid-svg-usPtXD4X64CpDgKZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-usPtXD4X64CpDgKZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-usPtXD4X64CpDgKZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-usPtXD4X64CpDgKZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-usPtXD4X64CpDgKZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-usPtXD4X64CpDgKZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-usPtXD4X64CpDgKZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-usPtXD4X64CpDgKZ .cluster text{fill:#333;}#mermaid-svg-usPtXD4X64CpDgKZ .cluster span{color:#333;}#mermaid-svg-usPtXD4X64CpDgKZ 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(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-usPtXD4X64CpDgKZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-usPtXD4X64CpDgKZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-usPtXD4X64CpDgKZ .icon-shape,#mermaid-svg-usPtXD4X64CpDgKZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-usPtXD4X64CpDgKZ .icon-shape p,#mermaid-svg-usPtXD4X64CpDgKZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-usPtXD4X64CpDgKZ .icon-shape .label rect,#mermaid-svg-usPtXD4X64CpDgKZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-usPtXD4X64CpDgKZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-usPtXD4X64CpDgKZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-usPtXD4X64CpDgKZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 主线程 listen_fd
accept client_fd_1
accept client_fd_2
accept client_fd_3
工作线程 1
工作线程 2
工作线程 3
recv send close client_fd_1
recv send close client_fd_2
recv send close client_fd_3

主线程只做:

  1. accept();
  2. 为连接准备独立参数;
  3. pthread_create();
  4. 立即回到下一次 accept()。

工作线程只负责一个 client_fd 的收发和关闭。

7. 为什么工作线程要分离

新线程默认是 joinable。若主线程对每个工作线程立刻 pthread_join(),它就会等待当前客户端退出,仍然无法继续接受新客户端。

教材使用:

c 复制代码
pthread_detach(thread);

分离线程退出时,系统会自动回收其线程资源,主线程不必 pthread_join()。

需要准确理解:pthread_detach() 不是"非阻塞等待",它不等待线程;它只是把线程标记为 detached。分离后不能再对该线程调用 pthread_join()。

pthread_create() 和 pthread_detach() 成功返回 0,失败直接返回错误码,不保证设置 errno:

c 复制代码
int rc = pthread_create(&tid, NULL, worker, ctx);
if (rc != 0) {
    fprintf(stderr, "pthread_create: %s\n", strerror(rc));
}

8. 教材线程参数写法存在竞态

教材核心结构近似为:

c 复制代码
while (1) {
    int client_fd = accept(...);
    pthread_create(&tid, NULL, worker, &client_fd);
    pthread_detach(tid);
}

问题是所有循环都把同一块栈位置的地址交给线程。线程还没读取参数时,主线程可能已经进入下一次循环并覆盖 client_fd。结果可能包括:

  • 两个线程读到同一个新描述符;
  • 原来的描述符无人处理;
  • 某线程关闭了另一个连接的描述符;
  • 问题随调度时序出现,难以稳定复现。

正确方式是为每个连接创建独立上下文,并明确所有权。

9. 安全的多线程参数与资源所有权

9.1 连接上下文

c 复制代码
struct client_ctx {
    int fd;
    struct sockaddr_in peer;
};

9.2 主线程接受连接

c 复制代码
for (;;) {
    struct client_ctx *ctx = malloc(sizeof(*ctx));
    if (ctx == NULL) {
        perror("malloc");
        continue;
    }

    socklen_t len = sizeof(ctx->peer);
    ctx->fd = accept(listen_fd,
                     (struct sockaddr *)&ctx->peer,
                     &len);
    if (ctx->fd == -1) {
        perror("accept");
        free(ctx);
        continue;
    }

    pthread_t tid;
    int rc = pthread_create(&tid, NULL, client_worker, ctx);
    if (rc != 0) {
        fprintf(stderr, "pthread_create: %s\n", strerror(rc));
        close(ctx->fd);
        free(ctx);
        continue;
    }

    rc = pthread_detach(tid);
    if (rc != 0) {
        fprintf(stderr, "pthread_detach: %s\n", strerror(rc));
    }
}

accept() 的 addrlen 是"值-结果参数",每次调用前都应重新设置为目标结构大小。

9.3 工作线程接管连接

c 复制代码
static void *client_worker(void *arg)
{
    struct client_ctx *ctx = arg;
    int fd = ctx->fd;
    free(ctx);                 /* 参数已经复制到局部变量 */

    char buf[1024];

    for (;;) {
        ssize_t n = recv(fd, buf, sizeof(buf), 0);

        if (n > 0) {
            printf("fd=%d received: %.*s\n",
                   fd, (int)n, buf);

            const char reply[] = "received\n";
            if (send(fd, reply, sizeof(reply) - 1, 0) == -1) {
                perror("send");
                break;
            }
        } else if (n == 0) {
            break;            /* 对端有序关闭发送方向 */
        } else if (errno != EINTR) {
            perror("recv");
            break;
        }
    }

    close(fd);
    return NULL;
}

这是教学骨架,正式程序还应循环处理部分发送,并决定怎样处理 SIGPIPE、超时和异常客户端。

10. 收发代码必须修正的四个细节

10.1 不能默认 recv() 得到字符串

recv() 返回的是字节数,不会自动在末尾写 \0。安全打印可使用长度:

c 复制代码
printf("%.*s", (int)n, buf);

或者只读取 sizeof(buf) - 1,随后手动补零:

c 复制代码
ssize_t n = recv(fd, buf, sizeof(buf) - 1, 0);
if (n > 0) {
    buf[n] = '\0';
}

10.2 不要固定发送整个缓冲区

教材多处使用:

c 复制代码
send(fd, write_buf, 1024, 0);

若有效字符串只有 10 字节,这会把后面无效或未初始化的内容也发出去。字符串回复应发送实际长度:

c 复制代码
send(fd, reply, strlen(reply), 0);

10.3 send() 可能只发送一部分

正式代码要根据返回值循环发送剩余部分。一次返回正数不保证等于请求长度。

10.4 分别处理 recv() 三种结果

text 复制代码
n > 0   收到 n 字节
n == 0  对端有序关闭
n < 0   出错;EINTR 可重试,其他错误通常结束连接

写成 while ((n = recv(...)) != 0) 会让 -1 也进入循环体,容易继续处理无效数据。

11. 为什么多客户端通常不用手工绑定客户端端口

客户端若都绑定相同本地 IP 和本地端口,会发生地址冲突。大多数 TCP 客户端无需调用 bind():

c 复制代码
int fd = socket(AF_INET, SOCK_STREAM, 0);
connect(fd, (struct sockaddr *)&server_addr, sizeof(server_addr));

connect() 会让内核自动选择合适的本地地址和临时端口。多个客户端因此可以连接同一个服务端端口。

服务端端口固定,是为了让客户端知道去哪里连接;客户端本地端口通常由内核临时分配。

12. 多进程模型:每个连接一个子进程

#mermaid-svg-KFVBG9ktORn8TbMu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KFVBG9ktORn8TbMu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KFVBG9ktORn8TbMu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KFVBG9ktORn8TbMu .error-icon{fill:#552222;}#mermaid-svg-KFVBG9ktORn8TbMu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KFVBG9ktORn8TbMu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KFVBG9ktORn8TbMu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KFVBG9ktORn8TbMu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KFVBG9ktORn8TbMu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KFVBG9ktORn8TbMu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KFVBG9ktORn8TbMu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KFVBG9ktORn8TbMu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KFVBG9ktORn8TbMu .marker.cross{stroke:#333333;}#mermaid-svg-KFVBG9ktORn8TbMu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KFVBG9ktORn8TbMu p{margin:0;}#mermaid-svg-KFVBG9ktORn8TbMu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-KFVBG9ktORn8TbMu .cluster-label text{fill:#333;}#mermaid-svg-KFVBG9ktORn8TbMu .cluster-label span{color:#333;}#mermaid-svg-KFVBG9ktORn8TbMu .cluster-label span p{background-color:transparent;}#mermaid-svg-KFVBG9ktORn8TbMu .label text,#mermaid-svg-KFVBG9ktORn8TbMu span{fill:#333;color:#333;}#mermaid-svg-KFVBG9ktORn8TbMu .node rect,#mermaid-svg-KFVBG9ktORn8TbMu .node circle,#mermaid-svg-KFVBG9ktORn8TbMu .node ellipse,#mermaid-svg-KFVBG9ktORn8TbMu .node polygon,#mermaid-svg-KFVBG9ktORn8TbMu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-KFVBG9ktORn8TbMu .rough-node .label text,#mermaid-svg-KFVBG9ktORn8TbMu .node .label text,#mermaid-svg-KFVBG9ktORn8TbMu .image-shape .label,#mermaid-svg-KFVBG9ktORn8TbMu .icon-shape .label{text-anchor:middle;}#mermaid-svg-KFVBG9ktORn8TbMu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-KFVBG9ktORn8TbMu .rough-node .label,#mermaid-svg-KFVBG9ktORn8TbMu .node .label,#mermaid-svg-KFVBG9ktORn8TbMu .image-shape .label,#mermaid-svg-KFVBG9ktORn8TbMu .icon-shape .label{text-align:center;}#mermaid-svg-KFVBG9ktORn8TbMu .node.clickable{cursor:pointer;}#mermaid-svg-KFVBG9ktORn8TbMu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-KFVBG9ktORn8TbMu .arrowheadPath{fill:#333333;}#mermaid-svg-KFVBG9ktORn8TbMu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-KFVBG9ktORn8TbMu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-KFVBG9ktORn8TbMu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KFVBG9ktORn8TbMu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-KFVBG9ktORn8TbMu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KFVBG9ktORn8TbMu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-KFVBG9ktORn8TbMu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-KFVBG9ktORn8TbMu .cluster text{fill:#333;}#mermaid-svg-KFVBG9ktORn8TbMu .cluster span{color:#333;}#mermaid-svg-KFVBG9ktORn8TbMu 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(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-KFVBG9ktORn8TbMu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-KFVBG9ktORn8TbMu rect.text{fill:none;stroke-width:0;}#mermaid-svg-KFVBG9ktORn8TbMu .icon-shape,#mermaid-svg-KFVBG9ktORn8TbMu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KFVBG9ktORn8TbMu .icon-shape p,#mermaid-svg-KFVBG9ktORn8TbMu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-KFVBG9ktORn8TbMu .icon-shape .label rect,#mermaid-svg-KFVBG9ktORn8TbMu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KFVBG9ktORn8TbMu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-KFVBG9ktORn8TbMu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-KFVBG9ktORn8TbMu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 父进程 listen_fd
accept client_fd_1
accept client_fd_2
fork 子进程 1
fork 子进程 2
处理 client_fd_1
处理 client_fd_2
SIGCHLD 回收退出子进程

父进程持续 accept()。每接受一个连接就 fork():

  • 父进程关闭自己的 client_fd 副本,继续接受连接;
  • 子进程关闭不需要的 listen_fd,处理该 client_fd;
  • 子进程结束后,父进程调用 waitpid() 回收。

13. fork() 后为什么双方都要关闭描述符

fork() 后,子进程继承父进程文件描述符表的副本。父子两份描述符引用同一个底层 Socket 对象。

text 复制代码
accept 返回 client_fd
          │
        fork
       /    \
父进程 fd   子进程 fd
   │            │
close         处理连接
   │            │
继续 accept    close

若父进程不关闭自己的 client_fd:

  • 即使子进程关闭连接,底层 Socket 仍有父进程引用;
  • 连接资源和终止时机可能不符合预期;
  • 长时间运行会积累文件描述符泄漏。

若子进程不关闭 listen_fd,它会无意义地持有监听 Socket,影响资源生命周期。

14. 多进程服务端的核心结构

c 复制代码
for (;;) {
    struct sockaddr_in peer;
    socklen_t peer_len = sizeof(peer);

    int client_fd = accept(listen_fd,
                           (struct sockaddr *)&peer,
                           &peer_len);
    if (client_fd == -1) {
        if (errno == EINTR)
            continue;
        perror("accept");
        continue;
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        close(client_fd);
        continue;
    }

    if (pid == 0) {
        close(listen_fd);      /* 子进程不接受新连接 */
        handle_client(client_fd);
        close(client_fd);      /* 只在一个明确位置关闭 */
        _exit(EXIT_SUCCESS);
    }

    close(client_fd);          /* 父进程不处理该连接 */
}

子进程使用 _exit() 可以避免把 fork() 前复制的 stdio 缓冲再次刷新。若 handle_client() 已经负责 close(client_fd),外层就不能再次关闭;资源所有权应只交给一个位置。

15. 为什么会出现僵尸进程

子进程退出后,内核仍保留少量退出信息,等待父进程读取。如果父进程始终不调用 wait() 或 waitpid(),退出的子进程会保持 zombie 状态。

并发服务端的父进程长期阻塞在 accept() 循环中,不能为了回收某个子进程而执行阻塞 waitpid(..., 0)。常见方案是处理 SIGCHLD,再用非阻塞方式回收所有已退出子进程。

16. 为什么 waitpid() 要放在循环里

c 复制代码
while (waitpid(-1, NULL, WNOHANG) > 0) {
}
参数 含义
-1 任意子进程
NULL 当前不关心退出状态
WNOHANG 没有已退出子进程时立即返回,不阻塞

标准信号不会为每个事件无限排队。多个子进程几乎同时退出时,父进程可能只得到一次待处理的 SIGCHLD 通知。因此每次进入处理逻辑都要循环回收,直到没有可回收的子进程。

17. 更稳妥的 SIGCHLD 处理

教材使用 signal(SIGCHLD, handler),并在处理函数里 printf()。更稳妥的基础写法使用 sigaction():

c 复制代码
static void reap_children(int signo)
{
    (void)signo;
    int saved_errno = errno;

    while (waitpid(-1, NULL, WNOHANG) > 0) {
    }

    errno = saved_errno;
}

static int install_sigchld_handler(void)
{
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = reap_children;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;

    return sigaction(SIGCHLD, &sa, NULL);
}

原因:

  • SA_NOCLDSTOP 避免子进程暂停、继续时也触发处理;
  • SA_RESTART 让部分被信号打断的系统调用自动重启;
  • 保存并恢复 errno,避免破坏主流程正在检查的错误;
  • printf()、malloc() 等 stdio 或复杂库函数不具备异步信号安全保证,不能在信号处理函数里调用;
  • waitpid() 可以用于这里的回收循环。

调试"回收了哪个 PID"时,基础阶段可先在正常主循环中记录,或使用自管道等机制把事件通知给主循环;不要直接在信号处理函数里 printf()。

18. 多线程与多进程怎么选

对比项 每连接一个线程 每连接一个进程
地址空间 线程共享 进程彼此隔离
创建与切换成本 通常较低 通常较高
共享数据 方便,但需要同步 需要 IPC
单个任务崩溃影响 可能终止整个进程 通常局限于子进程
资源回收 join 或 detach wait/waitpid
描述符要点 明确线程间所有权 fork 后父子关闭不用的副本
典型风险 竞态、死锁、共享变量错误 僵尸进程、描述符引用未关闭

这两种模型适合理解并发基础,但大量连接时不能无限创建线程或进程。后续通常会学习线程池、非阻塞 I/O、select/poll/epoll 等模型。

19. 文件描述符数字相同不代表同一个连接

教材多进程测试中,不同连接的 client_fd 都可能显示为 4。原因是:

  1. 父进程接受连接,最小可用描述符是 4;
  2. fork() 后子进程继承这个描述符;
  3. 父进程立即关闭自己的 4;
  4. 下一次 accept() 又分配父进程当前最小可用描述符 4;
  5. 不同子进程各自拥有独立的文件描述符表。
text 复制代码
子进程 A:fd 4 → 连接 A
子进程 B:fd 4 → 连接 B

描述符只是某个进程内部的整数索引。比较两个不同进程中的数字,不能据此判断是否指向同一个底层对象。

20. 地址占用与重启服务端

服务端退出后立即重启,bind() 可能报告:

text 复制代码
Address already in use

先用 ss -tanlp 判断:

  • 仍有旧服务端处于 LISTEN:找到并正常终止旧进程;
  • 只有连接处于 TIME-WAIT:没有对应的运行进程可以"杀掉",应理解 TCP 状态;
  • 开发服务器需要快速重启:在 bind() 前设置 SO_REUSEADDR。
c 复制代码
int on = 1;
if (setsockopt(listen_fd, SOL_SOCKET,
               SO_REUSEADDR, &on, sizeof(on)) == -1) {
    perror("setsockopt");
}

设置地址复用不是忽略所有端口冲突。已有进程仍在同一地址端口监听时,新程序通常仍不能直接绑定。

21. 建议的测试步骤

准备四个终端:

text 复制代码
终端 1:服务端
终端 2:客户端 A
终端 3:客户端 B
终端 4:ss / ps / Wireshark 辅助观察

测试顺序:

  1. 启动服务端,确认 LISTEN;
  2. 启动客户端 A,确认一个 ESTABLISHED;
  3. 启动客户端 B,确认第二个 ESTABLISHED;
  4. 两个客户端分别发送不同字符串;
  5. 确认服务端能区分两个 client_fd;
  6. 在客户端 A 输入 Ctrl+D,只断开 A 的发送方向;
  7. 确认客户端 B 仍能正常通信;
  8. 多线程版本检查线程退出后资源是否回收;
  9. 多进程版本用 ps 检查是否存在 <defunct> 僵尸进程;
  10. 停止服务端,检查所有监听描述符和子任务是否退出。

22. 录音与教材表述校正

  1. 转写中的 PCP、PPT、TGP、GDP 等多数应为 TCP。
  2. netstat 或 ss 显示的是 Socket 与连接状态,不是笼统的"TCP 进程"。
  3. 服务端监听端口可以同时出现在监听 Socket 和多个已连接 Socket 中,连接靠四元组区分。
  4. Wireshark 显示的初始 seq=0 通常是相对序列号,不是真实初始序列号固定为 0。
  5. Ctrl+D 表示终端输入 EOF,教材程序随后调用 shutdown(SHUT_WR) 才触发 TCP 半关闭。
  6. pthread_t 是供 pthread API 使用的线程标识,不应称为进程 PID,也不保证能当整数打印。
  7. pthread_detach() 不等待线程,它只让线程终止后自动释放资源。
  8. pthread_create() 失败返回错误码,不能只按系统调用习惯检查 < 0 或直接依赖 perror()。
  9. 把循环局部变量 client_fd 的地址交给新线程存在竞态;每个连接必须拥有独立参数存储。
  10. recv() 不会自动补字符串结束符,不能无条件使用 %s。
  11. send(..., 1024, ...) 会尝试发送 1024 字节;回复字符串较短时应按实际长度发送。
  12. fork() 后父进程应关闭 client_fd,子进程应关闭 listen_fd,否则引用计数和资源生命周期会出错。
  13. 同一个描述符数字只在当前进程内有意义;不同进程的 fd 4 可以指向不同连接。
  14. 退出子进程必须由父进程 wait/waitpid 回收,否则会形成僵尸进程。
  15. 信号处理函数中不能调用 printf() 等非异步信号安全函数。
  16. signal() 可完成入门实验,正式代码更适合使用行为明确的 sigaction()。
  17. 子进程处理完连接后应在一个明确位置关闭 client_fd,教材函数内外重复关闭存在误关复用描述符的风险。
  18. 端口暂时不能绑定不一定表示旧进程仍在运行;应区分监听进程与 TIME-WAIT 连接。

23. 本节最低掌握标准

学完后应能回答:

  1. listen_fd 与 client_fd 分别负责什么?
  2. 为什么多个连接可以共享服务端本地端口?
  3. 怎样用 ss 找到 LISTEN 和 ESTABLISHED?
  4. Ctrl+D 为什么会在教材程序中触发 TCP 半关闭?
  5. 单连接服务端为什么无法同时服务多个客户端?
  6. 多线程模型中主线程和工作线程怎样分工?
  7. 为什么不能把循环中 client_fd 的地址直接交给线程?
  8. pthread_detach() 解决什么问题?
  9. fork() 后父子进程各自应关闭哪个描述符?
  10. 为什么退出的子进程会变成僵尸进程?
  11. 为什么一次 SIGCHLD 处理中要循环调用 waitpid()?
  12. 为什么信号处理函数中不能 printf()?
  13. 为什么两个子进程都显示 fd 4,却可以对应不同连接?
  14. 多线程模型与多进程模型的主要取舍是什么?

最低实践要求:

  • 能启动一个服务端和两个客户端同时通信;
  • 能让主流程持续 accept();
  • 能把每个连接交给独立线程,并安全传递参数;
  • 能把每个连接交给子进程,并正确关闭父子描述符;
  • 能回收子进程,不留下僵尸;
  • 能用 ss 和 Wireshark 对照连接状态;
  • 能正确处理 recv() 的正数、0 和负数返回值。

24. PDF 页码与录音时间索引

主题 PDF 页码 录音时间
netstat 查看监听与连接 299---300 00:00---01:34
Wireshark 安装、接口和过滤器 300---304 01:35---02:28
三次握手与序列号、确认号 304---305 02:29---03:33
双向数据、半关闭与四次挥手 305---307 03:34---05:21
多线程服务器总体设计 307---310 05:22---11:28
工作线程接收、回复与释放资源 308---310 11:29---16:25
多连接客户端与自动分配端口 311---313 16:26---17:43
多线程版本编译和两个客户端测试 313---316 17:44---19:41
多进程服务器与 fork() 分工 316---320 19:55---22:37
SIGCHLD、waitpid() 与僵尸回收 316---319 22:38---24:39
多进程版本编译与测试 320---325 24:40---26:14

进一步核对资料:

25. 与测试 A 的对应关系

此前学习规划中的测试 A 要求 TCP Server/Client、三个客户端和多线程服务器。本节已经进入其核心实现:

测试 A 能力 本节对应内容 还需要补什么
TCP 服务端监听 socket/bind/listen/accept 错误处理、地址复用
多客户端 accept() 循环 至少同时运行三个客户端验证
多线程服务端 每连接一个线程 安全传参、线程退出、共享状态同步
客户端通信 自动分配本地临时端口 消息协议和循环收发
稳定断开 半关闭、连接状态 SIGPIPE、异常掉线、超时
后续设备控制 每个连接独立处理命令 再接入 GPIO/I²C/SPI 设备文件

当前最重要的练习不是继续扩展协议,而是先把"一个服务端 + 三个客户端 + 每连接一个线程"稳定跑通,并能证明:

  1. 三个客户端可同时收发;
  2. 一个客户端退出不影响另外两个;
  3. 工作线程退出后资源被回收;
  4. 服务端仍继续监听;
  5. 没有错误共享 client_fd;
  6. 没有固定发送 1024 字节造成的无效数据。

26. 下一步学习

完成本节后,建议先做一次精简版多线程 TCP 练习,再继续后续课程:

text 复制代码
修正线程参数竞态
    ↓
同时连接三个客户端
    ↓
按实际长度收发消息
    ↓
测试一个客户端异常退出
    ↓
检查线程和描述符是否释放
    ↓
再学习 I/O 多路复用或接入设备控制

多进程版本用于理解 fork()、描述符继承和子进程回收;就测试 A 的直接目标而言,应优先熟练掌握多线程版本。

相关推荐
北京恒星科通刘军1 小时前
灾害监测预警系统中,应急疏散广播的“应急叫应”实际送达率与有效性研究
网络·数据库·人工智能
Chill601 小时前
服务器home文件夹太小怎么把配置迁移到其他盘
运维·服务器
TDengine (老段)1 小时前
集群从“能用“到“高可用“:副本、选主与故障切换到底怎么配
大数据·网络·数据库·物联网·oracle·时序数据库·tdengine
自己的九又四分之三站台1 小时前
TCP 与 UDP:从可靠字节流到无连接数据报,怎么选、怎么测
网络协议·tcp/ip·udp
rest10241 小时前
做io测试
linux·服务器·网络
liangshanbo12151 小时前
面试题:游戏、直播为什么常用 UDP?KCP 是什么?为什么比 TCP 更适合低延迟场景?
tcp/ip·游戏·udp
一条破秋裤1 小时前
Linux 网络编程基础:网络分类、分层模型与数据封装
linux·运维·网络
欣欣之王来了1 小时前
5G的行业应用:工业互联网、车联网、智慧医疗中的网络支撑
网络·学习·计算机网络·安全·教程
她说彩礼65万2 小时前
C语言 单引号与双引号
java·服务器·c语言