半夜调一个采集程序,客户端一启动就打印 connect failed,errno 10061。服务端进程明明在跑,netstat 也能看到端口处于 LISTEN 状态。折腾半小时才发现防火墙拦了回环以外的地址。socket 编程的坑,大多出在「以为连上了」和「以为发完了」这两件事上。

socket 在系统里只是一个文件描述符

不管 C 还是 Python,socket() 拿到的都是一个整数句柄,内核里对应一条连接的状态机。TCP 的三次握手由内核完成,代码能感知的只有 connect 阻塞多久、accept 什么时候返回。所以排查连接问题先看状态:netstat -ano 里的 SYN_SENT 说明包发出去了没回来,LISTEN 只代表有人在等。

accept 返回的是新句柄

监听 socket 只负责接客,真正收发数据的是 accept 出来的那个新 fd。新手常把监听 fd 关掉,导致后续连接全断。服务端骨架大致是这样:

int fd = socket(AF_INET, SOCK_STREAM, 0); bind(fd, ...); listen(fd, 128); int c = accept(fd, ...);

accept 之后要把 c 交给工作线程或事件循环,别在循环里同步处理,否则第二个连接就得排队等着。

发出去不等于对方收到

send 返回的是拷进内核发送缓冲区的字节数,不是对端的确认数。缓冲区满了就阻塞,非阻塞模式下返回 -1 并置 EAGAIN。写完一定要 shutdown 一次写端,让对方读到 EOF,否则对端 recv 会一直挂在那里等数据。

粘包不是 TCP 的错

TCP 是字节流,没有消息边界。一次 send 的内容可能被拆成两次 recv,两次 send 也可能一次全读上来。解决办法只有两种:固定长度,或者长度前缀加内容。定长头部 4 字节存长度,读满头再读体,这个方案最稳,也不用担心分隔符出现在正文里。

端口占用与 TIME_WAIT

重启服务报 Address already in use,多半是主动关闭方处在 TIME_WAIT,默认要等两个 MSL。开发时给监听 socket 设 SO_REUSEADDR 就能立刻复用,线上别乱动 tcp_tw_reuse。排查顺序是 netstat 看状态、看谁占端口、再看代码有没有漏 close。

阻塞模型写小工具够用,连接上千就必须换 epoll 或 IOCP,或者直接用现成框架。手写 reactor 只在需要极致控制时才值得。