一切的起点:Socket 套接字
Unix 网络编程的核心抽象是 Socket。它把网络连接变成文件描述符,让 read、write、close 这些系统调用可以直接作用于网络通信。TCP 服务端的典型流程是 socket → bind → listen → accept → 读写 → close;客户端则是 socket → connect → 读写。
理解两个细节很关键:listen 的 backlog 参数控制的是已完成三次握手、等待 accept 的连接队列长度,不是并发上限;accept 返回的是新描述符,原监听描述符继续服务。忽略这点,容易在压测时遇到连接被拒却查不出原因。
阻塞模型为什么不够用
最直观的写法是一连接一线程或一进程。阻塞 I/O 下,accept 和 read 都会挂起,线程只能干等。连接数上去后,线程切换和内存开销迅速成为瓶颈,C10K 问题正是由此而来。
更隐蔽的问题是阻塞语义带来的错误处理复杂度:read 返回 0 表示对端关闭,返回 -1 要区分 EINTR(被信号打断,应重试)和真正的错误。短读短写也必须循环处理,不能假设一次调用读写完整。
I/O 多路复用:select、poll 与 epoll
I/O 多路复用让单线程同时监视多个描述符。select 有 FD_SETSIZE 限制且每次调用都要重设集合;poll 用数组摆脱了数量限制,但仍需线性扫描;Linux 的 epoll 通过内核事件表做到只返回就绪的描述符,在海量连接下优势明显。
epoll 分水平触发(LT)和边缘触发(ET)。LT 是默认行为,未处理完的事件会反复通知,编程简单;ET 只在状态变化时通知一次,必须配合非阻塞描述符循环读到 EAGAIN 为止,否则会丢事件。选 ET 是用复杂度换性能,不是默认最优解。
几个容易踩的坑
第一,忽略 SIGPIPE。向已关闭的连接写数据会触发该信号,默认终止进程,应忽略它并检查 EPIPE。
第二,TCP 是字节流,没有消息边界。必须自己设计长度前缀或分隔符协议,不能假设一次 read 对应一条消息。
第三,SOREUSEADDR 要设置,否则服务重启时可能因 TIMEWAIT 状态无法绑定端口。
第四,阻塞描述符上做 connect 超时控制很别扭,非阻塞加 select/poll 等待可写更可控。
怎么选技术路线
连接数少、逻辑简单,阻塞加多线程足够;连接数中等且需兼容多平台,poll 是稳妥选择;Linux 上的高并发服务,epoll 配合非阻塞 I/O 是主流。再往上,异步 I/O 与协程框架能进一步降低心智负担,但底层仍是这些机制。
建议的学习路径是:先用阻塞模型写出能跑通的 echo 服务,再改造成 epoll 版本,亲手处理短读、半包和连接关闭,比只读文档理解得深。