尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

重读《Unix网络编程卷1》:从socket到epoll的IO模型与实战

重读《Unix网络编程卷1》:从socket到epoll的IO模型与实战 1. 从面试翻车到回头啃经典我为什么重读《Unix网络编程卷1》先说个丢人的事。有一年我去面一个做基础架构的岗位前面聊得都挺好面试官最后问了一句你给我讲讲select、poll、epoll在处理大量连接时各自的瓶颈到底卡在哪儿我当时脑子里闪过的全是面试题里的标准答案——select 有 FD_SETSIZE 限制epoll 是事件驱动的所以快——但真让我展开说为什么select在内核态要遍历全部 fd、为什么poll没有数量上限却仍有性能问题、为什么epoll用红黑树加就绪链表能解决这些问题我说得支支吾吾明显透着背过但没真懂的气息。那次面试自然没下文。后来我花了两周时间把 W.Richard Stevens 的《Unix网络编程卷1套接字联网API》从第一章老老实实翻到第十六章边读边写代码验证才把这块短板真正补上。也是从那时候起这本书成了我案头最厚、翻得最旧的一本工具书。如果你也是做后端、客户端、嵌入式或者运维的大概率听过这本书的大名——它是 1990 年出版、后来多次再版的经典封面上那幅海滩上的海鸟油画几乎是整个编程圈共同的记忆点。但经典两个字容易让人敬畏也容易让人误以为它已经过时。我的看法恰恰相反TCP/IP 这一层的网络编程核心机制二十年没有变过变的是外围封装和性能优化手段而 Stevens 把这层机制的每一个细节都拆到了骨头上。这篇分享不是书评更像是我重读这本书时的笔记加实战复盘。我会把这本书最核心的几块内容——socket 生命周期、TCP/UDP 协议心智模型、五种 IO 模型、以及大量排错经验——还原成一套可以抄作业的知识体系也会附上我自己后来在真实项目中踩过的坑。如果你刚入手这本书不知道怎么读或者读了一遍觉得云里雾里这篇文章应该能帮你找到抓手。2. 为什么这本书二十年不过时它讲的是协议机制不是API用法2.1 先搞清楚卷1到底讲什么《Unix网络编程卷1》英文原名叫Unix Network Programming, Volume 1: The Sockets Networking API中文版最常见的译本由人民邮电出版社出版。很多人把这本书和《TCP/IP详解》系列搞混其实分工非常清楚《TCP/IP详解·卷一协议》讲的是 TCP、IP、ICMP、UDP 这些协议本身怎么工作是协议栈内部的故事《Unix网络编程卷1》讲的是用户态程序怎么通过 socket API 和内核协议栈打交道是应用层和内核之间的接口故事。书的内容结构大致可以分成四块章节范围核心内容我的定位第 1-4 章socket 基础、地址结构、TCP/UDP 基础客户端服务端必须精读这是地基第 5-7 章TCP 客户/服务端完整实现、I/O 复用、select 与 poll面试高频区必须吃透第 8-15 章UDP 编程、名字与地址转换、IPv4/IPv6、路由套接字、守护进程按需精读项目用到再回头第 16-30 章非阻塞 IO、ioctl、线程与多进程并发、带外数据、调试工具进阶内容单独攻克这里有一个很多初学者会踩的误区以为这本书是API 字典需要用哪个函数就查哪个函数。实际上 Stevens 的思路是以场景为单位——他给同一个功能写三四个版本的实现从最简单的阻塞式、到多进程、多线程、再到 IO 复用版本让你在对比中理解每种方案的适用边界。这种写法读起来很慢但一旦读懂你获得的不是知道几个函数而是面对任何网络场景都能选对方案的判断力。2.2 鸿蒙和 Unix 的区别顺手聊两句翻了近期热门搜索词发现鸿蒙和 unix 的区别被高频检索。简单说一句作为旁注鸿蒙的底座是自研微内核再加 Linux 内核的混合路线Unix 是几十年前贝尔实验室发展出的经典操作系统体系两者在内核设计哲学、系统调用接口、生态上差异极大。如果你在看《Unix网络编程卷1》说明你关心的是POSIX 风格系统调用与 socket 接口这套接口广泛存在于 Linux、BSD、macOS 以及类 Unix 衍生系统上和你最后跑在哪个国产系统上没有冲突——大家最终都是通过 socket()、bind()、listen() 这一套标准接口来写网络程序的。这也是这本书到今天依然能作为底层教材的原因接口标准化了阅读价值就不随平台迁移而衰减。2.3 读这本书需要什么基础我的建议是三条底线会 C 语言至少能看懂指针、结构体、回调函数。书的代码全是 C你不会 C 而只写过 Python/Go遇到struct sockaddr_in强制转换和指针传参可能会崩溃但也别怕语法障碍两周内能补上。了解最基本的 TCP 概念比如三次握手、四次挥手、端口。不需要精但得知道 TCP 是可靠字节流。有一台 Linux 机器能执行gcc、./a.out、netstat就行。macOS 也可以但有些行为细节比如SO_REUSEADDR的表现和 Linux 稍有差异。满足这三条你就可以开始啃了。3. 核心链路拆解从 socket() 到 close()一次完整连接的旅程这一节我快要写成一个最小可运行的网络程序解剖报告。我建议每个读者都亲手写一遍这段代码——它比任何面试题都更能检验你是不是真的懂 socket。3.1 一个最简 TCP 服务端的骨架#include stdio.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include string.h #include unistd.h int main() { int listen_fd, conn_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len sizeof(client_addr); char buffer[1024]; // 1. 创建 socket listen_fd socket(AF_INET, SOCK_STREAM, 0); if (listen_fd 0) { perror(socket); return 1; } // 2. 绑定地址 memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_addr.s_addr htonl(INADDR_ANY); server_addr.sin_port htons(9877); if (bind(listen_fd, (struct sockaddr *)server_addr, sizeof(server_addr)) 0) { perror(bind); return 1; } // 3. 监听 if (listen(listen_fd, 1024) 0) { perror(listen); return 1; } // 4. 接受连接 while (1) { conn_fd accept(listen_fd, (struct sockaddr *)client_addr, client_len); if (conn_fd 0) { perror(accept); continue; } // 5. 收数据 回显 ssize_t n read(conn_fd, buffer, sizeof(buffer) - 1); if (n 0) { buffer[n] \0; printf(received: %s\n, buffer); write(conn_fd, buffer, n); } // 6. 关闭连接 close(conn_fd); } close(listen_fd); return 0; }这段代码是教科书级的实现也是绝大部分真实服务端代码的原型。但真正要命的问题全都藏在细节里下面逐个拆。3.2 第一个隐藏细节socket() 的第三个参数为什么是 0socket(AF_INET, SOCK_STREAM, 0)里第三个参数是 protocol。你把它写成 0意思是让内核根据前两个参数自动选一个协议。对于SOCK_STREAM来说AF_INET下自动选中 TCP对于SOCK_DGRAM来说自动选中 UDP。所以 90% 的场景写 0 就够了。但面试官爱追问的是什么场景下不能写 0答案很直接——当你用原始套接字时比如SOCK_RAW IPPROTO_ICMP必须显式指定协议号因为同一个 socket 类型下可以对应多种网络层协议内核没法替你决定。这个知识点《Unix网络编程卷1》第二章就讲了很多人跳过去后面做 ping 工具、写 traceroute 时才发现要回头查。3.3 bind() 不只是在绑地址它还在给内核交代我是谁服务端调bind()是为了把自己和某个本地 IP 端口绑定在一起。这里有一个菜鸟最容易出 bug 的地方htons()和htonl()用反。htons(9877)是把主机字节序的端口转成网络字节序。为什么端口要转因为网络协议规定多字节整数统一用大端序而 x86 主机是小端序。如果你不转端口号 9877 在内存里的字节排列会是 0xd5 0x26 而不是 0x26 0xd5监听出去的端口就不是你以为的那个。真实服务器的bind()还有另一个容易忽略的点INADDR_ANY代表绑定本机所有网卡的 IP。如果用inet_addr(192.168.1.10)写死单 IP那客户端只能通过这台机器的这个 IP 访问到服务如果用INADDR_ANY所有网卡都能服务。做测试机没问题生产环境绑单 IP 反而更安全减少暴露面。3.4 listen() 的 backlog 到底代表什么书里第六章专门分析了 backlog 参数。在老版本 Linux 上它代表的是已完成连接队列 未完成连接队列之和的上限Linux 2.2 之后包括现在的所有主流内核它主要代表已完成三次握手、还没被 accept() 取走的连接数量上限。实际测试中你会发现你写listen(fd, 1024)但系统可能只允许 128因为内核参数net.core.somaxconn默认值很小。当你发现并发量一高、accept() 来不及处理客户端的 connect() 开始报超时时先查一下这个参数。现在的服务端一般把 backlog 设成 256 到 1024并且同步调高somaxconn。注意backlog 只影响内核排队的连接数不是最大并发连接数。真正的并发上限在你自己的进程能 hold 多少个 fd以及你用的 IO 模型能同时等多少个 fd。3.5 accept()不创建连接只取连接这是我认为整本书里最容易误解的函数没有之一。很多初学者以为accept()是接受一个连接请求然后完成三次握手。完全错误。三次握手是在你调用connect()之后、由内核协议栈自动完成的。accept()做的事情极其朴素从内核的已完成连接队列里取一个已经握好手的连接给你返回一个全新的 fd。这个新 fd 才代表和客户端的这条连接而原来的 listen_fd 继续专门监听新连接。这个理解为什么重要因为它直接关系到你设计高并发服务时的模型选择。你可以在accept()前写好查询空闲 fd 的逻辑因为 accept 是常数级操作你也可以用select/epoll去监听 listen_fd 的可读事件因为有新连接完成了握手这个事件本质上就是 listen_fd 变成可读。理解了这一点你再看惊群问题就容易了多个进程同时阻塞在同一个 listen_fd 的 accept() 上一个连接到来时内核把多个进程都唤醒最后只有一个能 accept 成功——在 Linux 较新版本上内核做了互斥避免大部分惊群但理解这个场景本身是读懂 nginx worker 为什么共享 listen socket 的前提。3.6 close() 和 shutdown()一个引号引发的血案TCP 关闭过程有两个关键函数。close()是引用计数减一如果这个 fd 被 fork 过父子进程各持有一份引用只有引用计数归零时才会真正发 FINshutdown()则是切断它直接对 socket 本身生效可以让后续的数据收发立即停掉。说个我真实的经历。曾经写一个服务主进程 fork 了子进程处理连接父进程顺手close(conn_fd)想表明这个连接归子进程管了结果因为没有 shutdownTCP 连接一直不关闭子进程退出后系统里全是 FIN_WAIT_2 的残留连接。后来把 close 改成shutdown(fd, SHUT_RDWR)才解决问题。这个坑书里第十九章其实有完整论述我当时眼神飘了没看仔细线上故障教育了我。4. TCP 与 UDP 的分岔路协议心智模型决定你写不出正确的网络程序4.1 TCP 是字节流UDP 是报文——这两个词决定了所有设计书里反复强调的一个核心思想TCP 是无记录边界的可靠字节流UDP 是有记录边界的不可靠数据报。什么叫无记录边界你send()了 4 次数据每次 100 字节对端read()4 次未必能各拿到 100 字节。它可能一次读到 50 字节下次读到 150 字节再下次读到 200 字节。你能依赖的只是这些字节的顺序不变 不会丢但每一包切到哪儿完全由内核决定。所以应用层必须自己设计消息边界常见方案是固定长度头 长度字段。这直接决定了 HTTP 协议的报文格式为什么要有Content-Length头就是因为 TCP 不保证你一次 read 就拿到完整请求体。而 UDP 就简单粗暴得多一次sendto()对应一次recvfrom()报文边界由内核替你保留发多大的包收就多大的包不考虑截断情况。代价是不保证送达、不保证顺序、不保证不重复。所以用 UDP 时应用层自己得处理超时重传、序号去重、丢包容忍。维度TCPUDP连接状态有连接无连接可靠性可靠有序列号确认不可靠不确认消息边界无边界字节流有边界报文拥塞控制有无性能相对低但公平相对高但不公平典型场景HTTP、数据库连接、文件传输实时音视频、DNS、游戏状态同步4.2 connect() 和三次握手的发生地服务端调listen()之后客户端一调connect()TCP 三次握手就由内核自动跑起来了客户端发出 SYN 包服务端内核返回 SYNACK客户端返回 ACK。这个全过程对用户态程序是透明的。你作为开发者看不到任何回调。三次握手期间如果客户端发来的 SYN 到达了一个不存在的端口服务端会回 RST——很多端口扫描工具就是拿这个特性来判断端口是否开放的。书里讨论了一个非常实用的对比connect() 一个本地不存在的端口时返回错误是 ECONNREFUSED连接拒绝connect() 一个 IP 地址不可达时返回错误是 EHOSTUNREACH 或 ETIMEDOUT取决于网络状况。这个差异能帮你在排错时快速判断问题发生在本机协议栈、局域网路由还是远端服务。4.3 TIME_WAIT 不是 bug它是协议安全的代价每次主动关闭连接的一方在发送完最后一个 ACK 之后会进入 TIME_WAIT 状态持续 2MSL两倍最大报文段生存期Linux 默认 60 秒左右。很多人不知道为什么需要它书里讲清楚了两个原因保证最后的 ACK 如果丢了能重发。如果直接关闭对端会一直重发 FIN而本地已经没这个连接状态了就没法完成挥手。让旧连接的延迟报文在网络中自然消亡避免它们串到新连接上。代价是高并发服务大量主动关闭连接时TIME_WAIT 连接堆积占用四元组资源。经典解法是SO_REUSEADDR——在 bind 前设置它能让你在 TIME_WAIT 未清空的情况下重新绑定同一个地址。书中对SO_REUSEADDR和SO_REUSEPORT的区别讲得很细前者解决端口被 TIME_WAIT 占用后者解决多个进程同时 bind 同一端口实测中SO_REUSEPORT在 Linux 3.9 之后还能做内核级负载均衡。4.4 UDP 的 connect() 是干什么的很多人以为 UDP 是无连接的就不能调connect()。书里专门有一节讲UDP connect调了connect()之后并不真的去建立连接而是把默认对端地址记录在内核里。好处有三个send()而不是sendto()少填一遍地址结构内核只接受来自这个对端的报文其它来源一律丢弃相当于在内核层做了一次源地址过滤出错时能更早收到 ICMP 错误比如端口不可达。真实游戏服务器里客户端连不上服务端端口时经常要排查是不是 UDP connect 之后 ICMP 错误没有及时返回——这个知识点几乎只有读过卷1的人才能一下子反映出来。5. 五种 IO 模型一张大表对照着看你会突然看懂 epoll 为什么快5.1 经典五模型速览《Unix网络编程卷1》第 6 章开头给出了那张流传全网的五种 IO 模型对比图我这辈子见过最多次的教科书插图大概就是它。这里我不画图用一张表对照IO 模型阻塞特征数据从内核到用户态的拷贝时机典型调用适用场景阻塞式 IO进程挂在 read 上等数据数据就绪后 read 返回再拷贝read / recvfrom简单低并发服务非阻塞式 IO进程反复尝试读没数据立即返回数据就绪后 read 返回read 配合 O_NONBLOCK低并发 自己控制循环IO 复用进程阻塞在 select/poll/epoll 上等待一组 fd 就绪数据就绪后 read 返回select / poll / epoll_wait高并发连接管理信号驱动 IO进程不阻塞内核发 SIGIO 通知数据来了收到信号后再调 read 拷贝数据sigaction fcntl(SIGIO)偏门用得少异步 IO进程完全不阻塞内核完成拷贝后再通知处理好了内核自己把数据拷到用户态缓冲区io_uring / AIO高性能存储/网络偏用户态这张表里最容易搞混的是信号驱动 IO和异步 IO。区别在于通知时机信号驱动内核说数据已经可以读了然后你自己去读异步 IO内核说数据已经放到你的缓冲区里了你什么都不用干直接处理数据就行。这个谁来做数据拷贝的差异就是你理解 io_uring 为什么能大幅降低 CPU 开销的钥匙。5.2 从 select 到 epoll一条完整的性能演进线书里只写了 select 和 poll因为 Stevens 写书的时候 Linux epoll 还没出现。但你把 select 的机制理解透了epoll 的改进点全是反着 select 的缺陷设计的。select()的三个致命问题fd_set 位图太小默认 1024 位。FD_SETSIZE决定了它能监视的最大 fd 编号超过就得修改编译宏非常憋屈每次调用都要把整个 fd 集合从用户态拷到内核态如果集合很大光拷贝就是 O(n) 的开销内核返回后用户态要遍历全部 fd 来逐个检查哪个就绪了又是 O(n)。连接数一多这个遍历时间就成了瓶颈。poll()解决了第一个问题用链表式数组代替位图但后两个问题依旧。epoll的关键改进在于维护一个常驻内核的事件表而不是每次重新传入。你用epoll_ctl把 fd 和对应事件填进内核里的红黑树之后epoll_wait只帮你把就绪的 fd 从就绪链表里取出来。这样每次等待不需要重新拷贝全部 fd 列表只拷贝就绪事件的数组返回一条小链表用户态只需要线性扫一遍就绪的 fd而不是几万个全部扫一遍复杂度从 O(n) 降到了 O(就绪数)。所以高并发框架nginx、redis、Netty 底层清一色选 epoll不是没有道理的。不过这里我要给个实操补充如果你的连接数只有几百select 和 epoll 的性能差距几乎可以忽略代码复杂度反而 select 更简单。不要为了炫技上 epoll选型看规模不是看年份。5.3 IO 复用模型的最小代码范式用 select 写一个简单的多连接 echo 服务是检验你理解程度的好方法。下面是最核心的循环结构fd_set readfds; FD_ZERO(readfds); FD_SET(listen_fd, readfds); int max_fd listen_fd; while (1) { fd_set tmp readfds; int ready select(max_fd 1, tmp, NULL, NULL, NULL); if (ready 0) { perror(select); break; } // 检查 listen_fd 可读有新的连接 if (FD_ISSET(listen_fd, tmp)) { int conn accept(listen_fd, NULL, NULL); FD_SET(conn, readfds); if (conn max_fd) max_fd conn; } // 遍历所有客户端 fd看谁可读 for (int i listen_fd 1; i max_fd; i) { if (FD_ISSET(i, tmp)) { char buf[1024]; ssize_t n read(i, buf, sizeof(buf) - 1); if (n 0) { buf[n] \0; // 处理消息... write(i, buf, n); } else if (n 0) { // 客户端关闭 close(i); FD_CLR(i, readfds); } } } }这份代码你运行一下再用几个客户端同时连上来玩一玩你会立刻体会到select 要从 fd 1 到 max_fd 全体扫一遍是什么意思。试着把 fd 数量加到一万个再对比 epoll 版本的性能这种体感比任何文章都直观。6. 书里没写透的实战排错我在真实环境踩过的四个网络坑6.1 Docker API 报 permission deniedUNIX domain socket 的权限模型最近热搜里有个很典型的报错permission denied while trying to connect to the docker api at unix:///var/run/docker.sock。这正好是《Unix网络编程卷1》里UNIX domain socket本地域套接字的知识在真实世界的投影。场景是这样的你装好 Docker执行docker ps正常但用 curl 直连本地 socket 就报错curl --unix-socket /var/run/docker.sock http://localhost/containers/json # 报错permission denied while trying to connect to the docker api at unix:///var/run/docker.sock排查链路建议按下面三步走每一步书里都有对应的底层解释第一步看 socket 文件的属主和权限ls -l /var/run/docker.sock # srw-rw---- 1 root docker 0 .../var/run/docker.sock是一个 UNIX domain socket它的权限位含义和普通文件几乎一样rw-rw----表示 owner 是 root、group 是 docker、其它用户无权限。UNIX socket 在文件系统里体现为特殊文件连接它时必须通过 VFS 层的权限检查你在用户态先得能对这个文件有写权限内核才允许你把 socket 接上去。第二步确认自己属于哪个组groups如果你不在 docker 组里你对这个 socket 文件没有写权限内核直接返回 EACCES也就是你看到的 permission denied。这个错误的本质不是网络不通而是文件系统权限检查失败。第三步解决办法二选一# 方案A把自己加入 docker 组要重新登录才生效 sudo usermod -aG docker $USER # 方案B用 sudo 执行 curl sudo curl --unix-socket /var/run/docker.sock http://localhost/containers/json这里补充一个我在生产环境见过的更隐蔽的坑有的团队把 docker 的 socket 挂到容器里用 bind mount然后容器内进程以普通用户跑一样报这个错——排查后确认不是网络问题而是容器内的用户对挂载进来的 socket 文件缺乏写权限。权限模型在容器里依然生效不会因为你在容器里就网开一面。6.2 EAGAIN 和 EWOULDBLOCK非阻塞 IO 的第一课把 socket 设置成非阻塞后read()在没有数据时会立即返回 -1errno设为EAGAIN或EWOULDBLOCK。很多新手第一次遇到这个返回直接当成读出错处理把连接关了。正确的做法是EAGAIN不是错误是现在没数据下次再来的礼貌通知。这个语义在《Unix网络编程卷1》第 16 章讲得非常清楚。真实服务里非阻塞 socket 的读循环写法是ssize_t n read(fd, buf, sizeof(buf) - 1); if (n -1) { if (errno EAGAIN || errno EWOULDBLOCK) { // 没数据可读正常等下一次 epoll/select 通知 return 0; } // 真正的错误在这里处理 return -1; }我把这个判断漏过一次结果线上服务在高峰期频繁自己断开空闲连接用户十分钟一次的探活请求触发了一堆错误日志。最后查下来就是把 EAGAIN 当错误这一行代码的问题。6.3 SIGPIPE一次 send() 直接干崩整个进程向一个对方已经关闭的连接写数据send()会触发SIGPIPE 信号默认行为是终止进程。这大概是最早在真实服务里让人莫名其妙进程消失的原因之一。书里给的经典方案是忽略 SIGPIPE 信号然后自己检查 send 返回值signal(SIGPIPE, SIG_IGN); // 之后 send 返回 -1、errno 为 EPIPE 时自己处理 if (send(fd, data, len, 0) 0) { if (errno EPIPE) { // 对端已关闭清理这条连接的资源 } }用MSG_NOSIGNAL标志也能避免 SIGPIPE 信号Linux 下常用这个发法。这个坑几乎人人都会踩一次但你要是提前读过书里关于信号和关闭的章节遇到时就能秒懂。6.4 accept() 返回 EMFILE高并发服务的隐藏杀手当进程的文件描述符用完时accept() 会返回 EMFILE。这个错误最头疼的地方在于你每 accept 失败一次那个已经握好手的连接就无人认领内核里积压的全连接会越来越多客户端以为自己在连接服务端却根本处理不了。生产中常用的方案是提前留一个空闲 fd进程启动时打开一个 fd比如 /dev/null当 accept 返回 EMFILE 时先 close 掉这个 fd空出一个位置 accept 掉这个连接再立刻 close 这个新连接最后重新打开 /dev/null 补回空闲 fd。这样不给客户端留下连接建立了但没消息回的困惑至少服务端能优雅拒绝。这个骚操作我在书里没见过是线上故障时从别处学到的经验但底层用到的东西全是卷1里教的fd 是有限资源、accept 不负责握手、socket 是文件描述符的一种。可见书是死的思路是活的。7. 怎么读这本书效率最高我的路线和实验方法7.1 两遍阅读法第一遍通读。从第一章到第七章每章的示例代码都在自己的机器上敲一遍跑一遍。不要追求马上看懂所有细节重点是建立完整连接生命周期的直觉socket 创建、绑定、监听、接受、收发、关闭、超时。第一遍速度要快两到三周搞定做不到的章节标个记号跳过别卡死自己。第二遍扎进去精读。重点是我在第 5 章列出的 IO 模型对比以及第 11 到 14 章的名字与地址转换。这一遍要慢配合抓包工具tcpdump Wireshark逐个验证书上说的每一个状态变化。比如三次握手发生的时候抓包看到几条TIME_WAIT 出现的时机是什么SO_RCVBUF调大后TCP 窗口有什么变化两遍读完你的网络编程知识体系基本成型。之后再遇到新协议比如 HTTP/2、gRPC、QUIC你都有足够的能力快速理解它们的底层为什么这样设计。7.2 必须动手做的三个实验实验一慢速客户端观察 accept 与 recv 的顺序。写一个服务端打印每条 log 的时间戳再写一个慢速客户端两次 send 之间 sleep 三秒。观察 TCP 的粘包现象——你会发现第一次 recv 拿到的字节数可能和 send 的字节数不一样。这是理解字节流概念最快的方法。实验二用 select 实现转发代理。写一个简单 TCP 代理select 同时监听两个 socket一个连接上游一个连接下游把数据从一边搬到另一边。这个实验做完你就真正理解了 select 的 fd 集合管理逻辑也理解了为什么代理类中间件的高并发能力那么依赖 IO 复用机制。实验三抓一次三次握手和四次挥手。用tcpdump -i lo port 9877抓本地回环接口的包然后启动上面那个 echo 服务用客户端连接、断开。你会完整看到 SYN、SYNACK、ACK、FIN、ACK 的序列。配合netstat -tanp观察本地 socket 状态从 LISTEN 到 ESTABLISHED 再到 TIME_WAIT 的过程记忆比任何书上的状态图都深。7.3 结合现代实战再补充哪些阅读材料卷1 成书太早缺少这些必须的补充知识epoll 本身。读过 select 之后直接看 man page 或内核文档理解epoll_ctl、epoll_wait、边沿触发ET与水平触发LT的区别。你对 select 理解越深越能体会 epoll 的设计动机。Reactor 与 Proactor 模式。这是网络编程的行业级设计模式在 Java Netty、C libevent / Boost.Asio 等框架里被广泛使用。卷1 没有讲这些抽象层但你要从 socket API 层面理解它们才能自如使用。io_uring。Linux 最新的异步 IO 接口代表了异步模型的工业级实现是读异步 IO 章节时最好的现代对照。高性能网络编程的一手源码redis 的网络模块单线程 epoll、nginx 的 worker 模型多进程 accept 锁 epoll这两个是卷1 知识的最佳现代应用示范。8. 写在最后的一些个人体会如果你问这本书对我的最大改变是什么我的答案不是会写 socket 代码了而是遇到任何网络疑难杂症时我脑子里有一套完整的排查坐标系这是连接建立阶段的问题、还是数据传输阶段的问题是内核协议栈的行为、还是用户态代码的失误是 socket 选项没设置好、还是 IO 模型选错了有一次线上服务半夜报警连接全部超时同事怀疑是防火墙策略变了。我用书里教的办法看了一眼错误码分布——大量 ETIMEDOUT 而非 ECONNREFUSED说明 SYN 发出去根本没回应这更像是网络路径被丢弃而不是端口拒绝。后来查下来果然是云平台的安全组配置被误改。这种排查思路基本就是从卷1 的 TCP 状态分析和错误码语义里养出来的。对于正在犹豫要不要啃这本书的朋友我的建议很直接花两个月时间把它当一门严肃的能力投资来做。这本书的信息密度极高不可能读一遍就全吸收但只要你把其中一张 IO 模型对比表、一个 accept 生命周期、一个超时与关闭的处理逻辑想透了你的网络编程水平就能超过很大一部分工作了好几年的同行。最后分享一个小技巧如果你跟我一样习惯电子书建议同时买一本纸质版放在工位上。网络编程这行书越翻越旧遇到问题随手翻一翻的体感和在 PDF 里 CtrlF 完全是两回事。遇到连接异常、状态异常、socket 选项拿不准先翻到对应章节再动手改代码——这个习惯帮我挡下了不少本可以避免的线上事故。
返回列表