UNIX网络编程 - Chapter 5 TCP 客户/服务器程序示例
Chapter 5 TCP 客户/服务器程序示例
这一章主要把 TCP 客户端/服务器在代码层面的关键点串起来:read() 的返回值语义、socket 的“身份信息”(本地/远端地址端口)、以及并发服务器里子进程回收(僵尸进程)的问题。
read() 返回值语义
> 0:实际读到的字节数== 0:对端关闭(EOF),再读也读不到数据-1:出错,需要结合errno判断EAGAIN/EWOULDBLOCK:非阻塞读且当前无数据可读EINTR:慢速系统调用被信号中断(通常可以重试)- 其他:真实错误,需要按场景处理
Socket 地址结构(内核视角)
下面是一个简化版示意:内核为每个 socket 维护状态、本地/远端地址端口、收发缓冲区等信息。
1 | // 内核为每个 socket 维护的数据结构(简化版) |
flowchart LR
subgraph UserSpace [用户进程空间]
direction TB
APP["你的应用程序<br>(Client/Server)"]
FD["文件描述符 (FD)"]
APP <-->|读 / 写| FD
end
subgraph KernelSpace [操作系统内核]
direction TB
SOCK{"struct socket<br/>(套接字大管家)"}
STATE(("连接状态<br/>(state)"))
subgraph Addr [身份信息]
LOCAL["本地地址<br/>(local_addr)"]
REMOTE["远程地址<br/>(remote_addr)"]
end
subgraph Buffers [缓冲区]
RECV["接收队列<br/>(receive_queue)"]
SEND["发送队列<br/>(send_queue)"]
end
TCPCB["TCP 状态控制<br/>(tcp_state)"]
SOCK --- STATE
SOCK --- Addr
SOCK --- Buffers
SOCK --- TCPCB
end
subgraph Hardware [物理网络设备]
NIC["网卡驱动 & 物理网卡"]
INTERNET(("Internet"))
NIC <--> INTERNET
end
%% 数据流转路径
FD == "read() / recv()" === RECV
FD == "write() / send()" === SEND
RECV -. "内核协议栈解析后放入" .-> NIC
SEND -. "内核协议栈打包后发出" .-> NIC
子进程回收:为什么要处理 SIGCHLD
在多进程并发服务器中,子进程退出后会向父进程发送 SIGCHLD 信号:
- 默认行为通常是忽略,但这不等价于“自动回收”。
- 子进程终止后,内核会保留进程表条目(PID、退出状态等)。父进程若不
wait()/waitpid(),子进程会变成僵尸进程。 - 僵尸进程大量积累会导致 PID 资源被耗尽,影响后续
fork()。
典型做法:注册 SIGCHLD 处理函数,在里面循环 waitpid(-1, ..., WNOHANG) 回收所有已退出的子进程。
1 | // 1) 定义信号处理函数来回收子进程 |
补充:read()/write() 不一定“读满/写满”
- TCP 是字节流协议:一次
read()可能读到一部分数据;一次write()也可能只写出部分数据。 - 需要按协议自行“组包/拆包”,或者使用
readn/writen这类循环读写封装。