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 放入循环
每连接一个线程
每连接一个子进程
线程参数与资源回收
描述符关闭与僵尸进程回收
多个客户端并发
老师这一节解决两个问题:
- 怎样观察 TCP 连接真实经历的状态和报文;
- 一个服务端怎样同时服务多个客户端。
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
主线程只做:
accept();- 为连接准备独立参数;
pthread_create();- 立即回到下一次
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。原因是:
- 父进程接受连接,最小可用描述符是 4;
fork()后子进程继承这个描述符;- 父进程立即关闭自己的 4;
- 下一次
accept()又分配父进程当前最小可用描述符 4; - 不同子进程各自拥有独立的文件描述符表。
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 辅助观察
测试顺序:
- 启动服务端,确认
LISTEN; - 启动客户端 A,确认一个
ESTABLISHED; - 启动客户端 B,确认第二个
ESTABLISHED; - 两个客户端分别发送不同字符串;
- 确认服务端能区分两个
client_fd; - 在客户端 A 输入 Ctrl+D,只断开 A 的发送方向;
- 确认客户端 B 仍能正常通信;
- 多线程版本检查线程退出后资源是否回收;
- 多进程版本用
ps检查是否存在<defunct>僵尸进程; - 停止服务端,检查所有监听描述符和子任务是否退出。
22. 录音与教材表述校正
- 转写中的
PCP、PPT、TGP、GDP等多数应为 TCP。 netstat或ss显示的是 Socket 与连接状态,不是笼统的"TCP 进程"。- 服务端监听端口可以同时出现在监听 Socket 和多个已连接 Socket 中,连接靠四元组区分。
- Wireshark 显示的初始
seq=0通常是相对序列号,不是真实初始序列号固定为 0。 - Ctrl+D 表示终端输入 EOF,教材程序随后调用
shutdown(SHUT_WR)才触发 TCP 半关闭。 pthread_t是供 pthread API 使用的线程标识,不应称为进程 PID,也不保证能当整数打印。pthread_detach()不等待线程,它只让线程终止后自动释放资源。pthread_create()失败返回错误码,不能只按系统调用习惯检查< 0或直接依赖perror()。- 把循环局部变量
client_fd的地址交给新线程存在竞态;每个连接必须拥有独立参数存储。 recv()不会自动补字符串结束符,不能无条件使用%s。send(..., 1024, ...)会尝试发送 1024 字节;回复字符串较短时应按实际长度发送。fork()后父进程应关闭client_fd,子进程应关闭listen_fd,否则引用计数和资源生命周期会出错。- 同一个描述符数字只在当前进程内有意义;不同进程的 fd 4 可以指向不同连接。
- 退出子进程必须由父进程
wait/waitpid回收,否则会形成僵尸进程。 - 信号处理函数中不能调用
printf()等非异步信号安全函数。 signal()可完成入门实验,正式代码更适合使用行为明确的sigaction()。- 子进程处理完连接后应在一个明确位置关闭
client_fd,教材函数内外重复关闭存在误关复用描述符的风险。 - 端口暂时不能绑定不一定表示旧进程仍在运行;应区分监听进程与
TIME-WAIT连接。
23. 本节最低掌握标准
学完后应能回答:
listen_fd与client_fd分别负责什么?- 为什么多个连接可以共享服务端本地端口?
- 怎样用
ss找到LISTEN和ESTABLISHED? - Ctrl+D 为什么会在教材程序中触发 TCP 半关闭?
- 单连接服务端为什么无法同时服务多个客户端?
- 多线程模型中主线程和工作线程怎样分工?
- 为什么不能把循环中
client_fd的地址直接交给线程? pthread_detach()解决什么问题?fork()后父子进程各自应关闭哪个描述符?- 为什么退出的子进程会变成僵尸进程?
- 为什么一次
SIGCHLD处理中要循环调用waitpid()? - 为什么信号处理函数中不能
printf()? - 为什么两个子进程都显示 fd 4,却可以对应不同连接?
- 多线程模型与多进程模型的主要取舍是什么?
最低实践要求:
- 能启动一个服务端和两个客户端同时通信;
- 能让主流程持续
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 |
进一步核对资料:
- Linux man-pages:accept(2)
- Linux man-pages:pthread_create(3)
- Linux man-pages:pthread_detach(3)
- Linux man-pages:fork(2)
- Linux man-pages:wait/waitpid(2)
- Linux man-pages:sigaction(2)
- Linux man-pages:signal-safety(7)
- Linux man-pages:tcp(7)
25. 与测试 A 的对应关系
此前学习规划中的测试 A 要求 TCP Server/Client、三个客户端和多线程服务器。本节已经进入其核心实现:
| 测试 A 能力 | 本节对应内容 | 还需要补什么 |
|---|---|---|
| TCP 服务端监听 | socket/bind/listen/accept |
错误处理、地址复用 |
| 多客户端 | accept() 循环 |
至少同时运行三个客户端验证 |
| 多线程服务端 | 每连接一个线程 | 安全传参、线程退出、共享状态同步 |
| 客户端通信 | 自动分配本地临时端口 | 消息协议和循环收发 |
| 稳定断开 | 半关闭、连接状态 | SIGPIPE、异常掉线、超时 |
| 后续设备控制 | 每个连接独立处理命令 | 再接入 GPIO/I²C/SPI 设备文件 |
当前最重要的练习不是继续扩展协议,而是先把"一个服务端 + 三个客户端 + 每连接一个线程"稳定跑通,并能证明:
- 三个客户端可同时收发;
- 一个客户端退出不影响另外两个;
- 工作线程退出后资源被回收;
- 服务端仍继续监听;
- 没有错误共享
client_fd; - 没有固定发送 1024 字节造成的无效数据。
26. 下一步学习
完成本节后,建议先做一次精简版多线程 TCP 练习,再继续后续课程:
text
修正线程参数竞态
↓
同时连接三个客户端
↓
按实际长度收发消息
↓
测试一个客户端异常退出
↓
检查线程和描述符是否释放
↓
再学习 I/O 多路复用或接入设备控制
多进程版本用于理解 fork()、描述符继承和子进程回收;就测试 A 的直接目标而言,应优先熟练掌握多线程版本。