做网络编程这些年,我踩过的坑比看过的文档多得多。Linux 下 Socket 编程、IO 多路复用这些概念,说实话刚接触的时候真会被绕晕,尤其是 epoll 的读写就绪通知、TCP 粘包拆包、连接超时这些细节,不亲手调几遍根本理解不透。这篇内容我就把 Socket 基础、IO 多路复用、非阻塞 IO 以及那些让人抓狂的异常错误一次讲明白,适合刚入门 Linux 网络编程的学生、日后端开发想补底子的同行,以及被线上 socket 问题折磨过的运维。我把实操中验证过的代码结构和排查思路都放进来,你照着敲、照着查就行。
1. Socket 基础:先把地基打牢
1.1 Socket 到底是什么,以及为什么大家都在学它
Socket 是操作系统提供的一套网络编程接口,翻译成人话,它就是"两个进程之间进行网络通信的一根管子"。进程 A 往管子里写数据,进程 B 从管子里读数据,谁都不用关心对端跑在哪台机器、IP 是多少、走的什么协议。Linux 下网络编程几乎绕不开 Socket,像 Nginx、Redis、MySQL 这些你耳熟能详的服务,底层全是它在撑。
有人会问:现在不都是 HTTP 接口、RPC 框架吗,自己写 Socket 还有必要吗?有,而且非常有必要。HTTP 是基于 TCP 的应用层协议,RPC 框架最终也要落到 TCP 连接上。框架帮你屏蔽了细节,可一旦线上出现连接超时、数据读到一半、服务假死,不懂 Socket 你连排查方向都没有。我见过不少同事,框架用得很溜,遇到 "socket read timed out" 就直接抓瞎,其实就是不理解底层连接状态和超时模型。
Socket 编程的核心其实就三件事:建立连接、传输数据、关闭连接。围绕这三件事,衍生出大量细节:三次握手和四次挥手的状态流转、缓冲区与粘包、IO 模型选择、异常处理。下面我从一个最小可运行的服务端和客户端代码说起,把整条链路先立起来。
1.2 从最简单的服务端和客户端代码说起
以 C 语言为例,服务端大致是这样一个流程:
c复制// 服务端:创建 socket -> bind 绑定地址 -> listen 监听 -> accept 接受连接
int lfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(8888);
bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
listen(lfd, 64);
// 循环 accept 处理连接
int cfd = accept(lfd, NULL, NULL);
客户端就更简单了:
c复制int cfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);
inet_pton(AF_INET, "127.0.0.1", &saddr.sin_addr);
connect(cfd, (struct sockaddr*)&saddr, sizeof(saddr));
这段代码虽然简单,但有几个点新手特别容易翻车。
第一个是 htons 和 htonl。网络字节序是大端,而 x86 机器是小端,端口号和 IP 地址必须转换成网络字节序才能用。我见过有人直接给 sin_port 赋值 8888,结果服务端监听在了一个莫名其妙的端口上,查了半天才发现是字节序问题。
第二个是 bind 之前一定要设置 SO_REUSEADDR。如果不设置,服务端程序崩溃重启时经常会报 "Address already in use",因为端口还处在 TIME_WAIT 状态。这个坑几乎人人都会踩,加了这行才能保证快速重启:
c复制int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
第三个是关于 accept 的位置。很多初学者的错误写法是把 accept 放在 for 循环外面,导致只能处理一个连接,后面客户端全都连不上。实际项目里 accept 必然在一个无限循环里,而且一般不会在循环里直接同步处理业务,而是把新连接交给线程、进程或者事件循环去处理,这就引出了后面要讲的 IO 多路复用。
1.3 TCP 连接生命周期:三次握手、四次挥手与状态机
Socket 编程不只是调几个 API,每个 API 背后都对应 TCP 状态机的跃迁。三次握手大家应该都背过:客户端发 SYN,服务端回 SYN+ACK,客户端再回 ACK,连接建立。但在代码层面,这三个包是操作系统协议栈自动完成的,你调 connect() 返回成功,不代表对方进程已经把数据准备好,只代
