网络编程
写一个能同时处理上万连接的网络编程服务器,用传统阻塞 I/O 基本是网络编程自找麻烦。每个连接开一个线程,网络编程内存和上下文切换的网络编程开销能把 CPU 吃干净。C10K 问题喊了二十多年,网络编程现在 C10M 都有人在实际生产里跑了,网络编程靠的网络编程就是 I/O 多路复用这套东西。 select 是网络编程最早的方案,fd_set 用位图存文件描述符,网络编程上限 1024。网络编程poll 换成了数组,网络编程没了数量限制,网络编程但每次调用还是网络编程得把整个集合从用户态拷到内核态,返回后再线性扫描一遍。网络编程连接数一上来,网络编程光这两步就够呛。epoll 在 Linux 2.6 引入,用红黑树管理 fd,就绪事件通过回调直接挂到就绪链表上,epoll_wait 只取有事件的。O(1) 的复杂度不是吹的,Nginx 就是靠这个撑住高并发的。 不过 epoll 也不是万能药。它的边缘触发模式(ET)要求一次把数据读干净,直到返回 EAGAIN,否则事件不会再通知。水平触发(LT)省心,但可能频繁唤醒。写 Nginx 模块那会儿,ET 模式下漏读一个字节,连接就卡死了,排查半天才发现是 recv 循环没写对。 跨平台的话,macOS 用 kqueue,Windows 用 IOCP。IOCP 是真正的异步 I/O,完成端口模型,和 epoll 的事件通知机制有本质区别。libevent、libuv 这些库把差异封装掉,但底层行为不一样,性能特征也不同。Redis 在 Linux 上默认用 epoll,在 macOS 上编译时自动切到 kqueue。 说到网络编程,绕不开 TCP 粘包。TCP 是字节流,没有消息边界。发两次 100 字节,接收端可能一次收到 200 字节,也可能分四次每次 50 字节。解决办法无非三种:固定长度、分隔符、长度前缀。Protobuf 用长度前缀,HTTP 用 Content-Length 加 chunked 编码,Redis 协议用 CRLF 分隔。选哪种看场景,没有银弹。 阻塞 I/O 也不是完全没用了。连接数少、逻辑简单的场景,阻塞写起来直观,调试也方便。一个内部管理后台,几十个连接,用阻塞 + 线程池足够了。硬上 epoll 反而增加复杂度,代码可读性下降。技术选型得看实际需求,别为了炫技把简单问题搞复杂。 零拷贝这块,sendfile 系统调用让数据直接从内核页缓存送到网卡,不经过用户态。Nginx 配 sendfile on 能明显降低 CPU 占用。但 sendfile 不能改数据,要加密或者压缩就得走用户态。splice 更灵活,能在两个 fd 之间移动数据,管道操作不用拷贝到用户空间。这些系统调用在文件服务器、代理场景下收益很大。 写网络程序,抓包工具得备好。tcpdump 抓原始包,Wireshark 看协议细节。有次线上服务响应慢,tcpdump 一看,大量 TCP retransmission,查下来是网卡 offload 和虚拟化环境不兼容。这种问题看应用日志根本找不到,必须下到协议层。 最后提一嘴,网络编程的坑大多在边界条件上。连接半开、对端 RST、超时重传、TIME_WAIT 堆积,这些才是真正花时间的地方。协议本身不复杂,复杂的是异常处理。把错误路径写扎实,比优化正常路径的微秒级延迟重要得多。
-
上一篇
-
下一篇