Effective TCP/IP Programming
2021-05-01 计算机网络
概述
虽然这本书的名字是 《Effective TCP/IP Programming》, 但是其中也有一些 UDP 的内容,所以主要写的是网络编程的一些最佳实践。
注意: 本文中的 Tips 是笔者在原文的基础上,结合自己的理解所写的,部分 Tips 可能和原文完全不同。
1. 理解面向连接协议和无连接协议之间的区别
IP (网络层) 提供的是一种尽力而为的、不可靠的无连接服务。通过接收来自其上层的数据分组,然后将它们封装在一个 IP 分组中,根据路由为分组选择正确的硬件接口将分组发送出去。一旦将分组发送出去了,IP 就不再关心这个分组了。至于传输过程中的丢包、延迟等问题,IP 统统不管。
这保持了 IP 协议的简洁性和适应性,任何可以承载数据分组的物理链路都可以运行 IP 协议,所以 IP 层无法顾及到的工作,就要交给传输层来处理了。
传输层主要分为两种类型协议,面向连接协议和无连接协议,面向两者之间的本质区别在于:
- 面向连接的协议中 (典型的如 TCP),每个数据包 (TCP 称为数据段) 都维护了后续数据包的状态信息,通常提供可靠传输保证
- 无连接的协议中 (典型的如 UDP),每个数据包 (UDP 称为数据报) 都是独立的,和其他数据包没有任何关系 (需要由应用层来处理),不保证数据传输可靠 (也就是可能出现丢包、乱序等问题)
至于 TCP 和 UDP 的区别,老生常谈的八股文了 :-)
扩展阅读: TCP 和 UDP 的区别
2. 理解子网和 CIDR 的概念
扩展阅读: 公网和子网是如何分配的?
3. 理解内网地址和 NAT
扩展阅读:
4. 使用成熟的应用层开发框架
这一点无需解释。
- Go 的原生 netpoll, 开源框架 gnet
- Java 的 Netty
5. 套接字接口比 XTI/TLI 更好用
XTI (X/OpenTransportInterface, X/开放传輸接口),关于 XTI (一般用不到,忽略即可),感兴趣的读者可以阅读《UNIX 网络编程》。
套接字提供了更简单、可移植性更好的接口,所以网络编程中可以忽视掉 XTI 了。
6. TCP 是一个字节流协议
理解这一点,就不用再去研究 所谓 TCP 粘包/拆包 之类的问题 了。
正如 Richard Stevens 大佬在 TCP/IP Illustrated 中所说:
网络编程中,开发者遇到的实际问题,大约有 90% 都和开发者对于 TCP/IP 的理解有关。
7. 不要低估 TCP 的性能
TCP 传输数据时,会有建立连接的三次握手和断开连接的四次 (或者三次) 挥手开销,在短连接业务场景中,建立连接和断开连接占据相当一大部分比重,所以 (大部分情况下) UDP 的性能要比 TCP 高。
但是如果在长连接业务场景中,当连接持续的时间很长,并且传输了大量的数据 (同时 TCP 的接收缓冲区大小以及其他内核参数设置合理),TCP 的性能并不比 UDP 弱,最重要的是:省心。
8. 不要试图在应用层实现 TCP
很多开发者使用 UDP 作为应用的传输层协议,然后在应用实现一堆 TCP 的功能,例如 超时重传
、数据乱序处理
等,但其实这是不必要的,最好的方案是直接使用 TCP 作为应用的传输层协议。
如果应用即想拥有 TCP 的可靠性,同时又想避免 TCP 建立连接和断开连接带开的开销,可以了解一下 T/TCP (TCP 事务扩展), 虽然 T/TCP 已经推出多年了,但是一直没有得到广泛应用。
当然,这一条建议需要辩证地看待,比如 QUIC 协议 (RFC 9000, RFC 9001, RFC 9002)。
9. TCP 是一个可靠协议,但不是百分百绝对可靠
TCP 所谓的 “可靠性” 是一个逻辑概念,这个概念建立在这些前提下:
- 物理基础设施正常运行 (如物理光缆、机房供电)
- 网络链路中的软硬件正常运行 (如中间路由器、NAT 设备、防火墙)
- 发送方、接收方操作系统配置参数合理 (如缓冲区大小、IP/网卡配置、路由配置)
- 发送方、接收方应用程序正常运行 (无 Bug)
- 应用程序的用户非正常操作 (如直接关机、拔掉路由器电源)
书中精心设计了几个常见的 TCP 状态错误小例子:
Connection reset by peer
RST
RST + SIGPIPE
EOF
ETIMEDOUT, ECONNRESET
对理解 TCP 状态机比较有帮助,感兴趣的读者可以通过附录连接查看相关代码。
10. TCP 不是轮询模式
如果连接中断/丢失了,是不会通知到应用层的,应用层只有在下次发送数据时,才能发现这个问题。
所以,应用层实现心跳是必要且必须的。
11. 永远检测对端发送的数据
除了程序的健壮性之外,网络编程中也需要关注安全方面,和应用层的 “安全理念” 一致,永远不要相信用户的输出,任何时候都要做好 “防御式编程”,书中罗列了几个常见的网络编程防御性技巧。
- 检测客户端的连接断开
- 服务端可以对连接设置一个 “读取数据定时器” (心跳),如果客户端在指定时间内没有发送数据,就认为该客户端连接已经断开
- 检测无效的输入数据
在实际项目中,一般不会自己造轮子,而是直接使用成熟的开发框架,开发者大部分情况下,只需要遵守选择的开源框架的最佳实践即可。
12. 理解网络程序在 单机/局域网/互联网 各个环境中的区别
同一个网络程序,在单机、局域网、互联网中运行,可能会有不同的问题和结果 (例如单机/局域网中基本不可能出现丢包、超时、传输性能等问题),在单机和局域网中运行正常,但是到了互联网中,程序就会出现问题,主要是后者的规模性要比前两者多出不止一个量级,以及复杂的网络链路中各种不可控的因素。
这条也是网络编程的核心难点和竞争力所在。
13. 了解协议是如何工作的
多多阅读 RFC (https://www.rfc-editor.org/), 多思考,多实践。
此外,作者还推荐了 Stevens 大佬的几本著作,以及下面这本书。
14. 忽视 OSI 七层模型
言外之意,理解 TCP/IP 四层模型即可,这其中的最重要/最常见的部分:当然也就是 TCP 了。
当然,作者建议开发者在编写文档,表示相关术语时,还是要保证准确性,例如 七层负载均衡
还是 四层负载均衡
。
15. 理解 TCP 的可靠传输机制
应用对连接执行写入操作后,首先将应用数据从用户缓冲区复制到内核 TCP 发送缓冲区,然后剩下的工作就交给 TCP 了,应用层无需再关注,所以从应用视角来看,写入操作是不会被阻塞的 (除非 TCP 发送缓冲区已满)。
应用层调用写入操作返回时,数据可能还在排队等待发送,不能想当然认为此时写入数据成功等同于数据已经发送到接收方了。
TCP 可以将发送缓冲区的数据全部、部分发送出去,或者不发送任何数据,这取决于 TCP 的滑动窗口和拥塞控制,所以可能会出现一种极端场景:应用程序已经崩溃/退出了,但是 TCP 会继续尝试将数据全部发送给对端。
扩展阅读:
16. 理解 TCP 的连接终止过程
也就是理解 TCP 4 次挥手过程中,发送方和接受方各自的状态变化。
扩展阅读:
17. 考虑使用 inetd 启动应用程序
这一条忽略即可,在现代操作系统中一般直接使用 systemd
。
18. 使用 tcpmux 管理服务器端口分配
这一条忽略即可。
开发者了解常用的 “知名” 端口号即可 (如 80. 443, 3306),不管是开发环境还是生产环境,不要使用这些端口号就可以。
19. 考虑使用两个 (或多个) TCP 连接
开发者需要根据具体的业务场景来确定的连接方案,有时候可能需要不止一个 TCP 连接,这一点没有特别多可以展开讲的,具体情况具体分析就行。
20. 保持应用程序为事件驱动类型(1)
这一条忽略即可,毕竟现代服务端程序 epoll
是标配。
21. 保持应用程序为事件驱动类型(2)
同上。
22. 设置合理的 TIME-WAIT 参数值
扩展阅读:
23. 服务端设置 SO_REUSEADDR 选项
重启服务端程序时,有时候会发生 Address alreadyinuse
(地址已用) 错误,这是因为重启时使用的是和之前程序相同的 IP 地址和端口号,受到了 TCP 四元组的限制,因为此时之前的程序可能仍有连接处于 TIME_WAIT
状态。
如果程序设置了 SO_REUSEADDR
选项,就不会出现这个报错。
当然,现代化的服务端程序 (例如 Nginx) 都内置了热 (优雅) 重启和关闭方案,所以一般情况下其实也不会遇到这个问题。
24. 批量写操作
老生常谈的技巧,在操作文件系统、数据库、应用批量处理场景中随处可见。
扩展阅读:
25. 理解如何使 connect 调用超时
这一条忽略即可 (老生常谈了)。
26. 避免数据复制
这一条忽略即可 (老生常谈了)。
可以研究更加流行的高性能网络方案,如 AIO, EBPF, DPDK 等。
27. 使用前将结构体 sockadddr_in 初始化
这一条忽略即可 (常见的编程空指针引用陷阱)。
28. 注意字节序 (大端序和小端序)
扩展阅读
29. 不要将 IP 地址或端口号硬编码
这一条忽略即可 (常见的编程陷阱)。
应用层的解决方案: 服务发现
。
30. 理解已连接的 UDP 套接字
理解 UDP 的工作方式。
31. 记住,并不是所有程序都是用 C 语言编写的
笔者觉得,虽然语言不重要,但是熟悉 C 语言之后,再看一些基础软件的源代码确实会非常高效。
32. 理解缓冲区长度带来的影响
BWD: 带宽时延积
如果 RTT 以秒为单位,那么:
$$ BWD = 带宽 × RTT = bits/second × seconds $$
理想情况下,可以将发送和接收缓冲区设置为 BWD 大小来获得最佳吞吐量,但是实际情况中,这基本不可行,主要原因在于网络链路的复杂性。
为此,作者给出了一个规则: 对于非实时交互性应用 (例如 telnet) 来说,至少应该将发送缓冲区设置为 MSS 的 3倍大小。当然,具体情况具体分析就行。
在 BWD 的基础上,就是 BDP, 瓶颈链路带宽和最小 RTT 时延的乘积时, 也就是 Google BBR 算法实现的核心指标参数。
33. 掌握 ping
调试网络和在网络应用程序的最基本、最有效的工具之一,即使在更高层网络 (比如 TCP ) 或者应用层服务 (比如 telnet) 都无法工作的情况下,ping 常常还能发挥作用。
使用 ping 最多的场景,就是通过观察应答的 RTT 值及其变化情况,还有丢包情况,基本可以推断出网络链路状况。
扩展阅读:
34. 掌握 tcpdump 或类似的工具
当然,同时要掌握的还有 WireShark, 搭配使用,效率更高。
35. 熟悉 traceroute 实现原理
扩展阅读:
36. 学习使用 ttcp
TCP 和 UDP 性能测试工具。
文档链接: https://linux.die.net/man/1/ttcp
37. 掌握 lsof
需要注意的是: lsof
是 list opened files 的缩写,也就是列出已打开的文件。
这意味着,处于 TIME_WAIT
状态的 TCP 连接的相关信息,无法通过 lsof 命令来获得,因为没有对应的已打开的套接字文件可以进行关联,此时可以使用 netstat
命令作为补充。
此外,lsof 只有 Linux, Mac 版本。
38. 掌握 netstat
无需解释 :-)。
39. 学习使用系统中的调用追踪工具
最常用的,也就是 strace
命令了,当然,还有各个编程语言的配套工具。
文档链接: https://man7.org/linux/man-pages/man1/strace.1.html
40. 构建并使用捕获 ICMP 报文的工具
说实话,自己造轮子的意义不大,最重要的是理解理论知识,以及熟练掌握前文中提到的工具。
感兴趣的读者可以通过附录连接查看相关代码。
41. 读 Stevens 的书
无需解释,Stevens 大佬书籍列表:
整套丛书贴近实战,深入浅出,用实验设计和代码来演示网络编程中的各个知识点,真正做到了 “talk is cheap show me the code”。
摘抄一段笔者喜欢的书评:
Richard Stevens 以独特的写作风格和严谨的实验代码,在 “网络编程” 领域一骑绝尘,即使几十年后的今天来看,依然令大多数开发者和作者高山仰止,心向往之。
42. 阅读源代码
阅读 Linux 网络实现源代码、优秀的开源网络框架代码,所谓站在巨人的肩膀上 :-)
43. 访问 RFC 编辑者的页面
开发者要把 RFC (https://www.rfc-editor.org/) 文档当成小说看 :-)。
44. 经常访问相关论坛
comp.protocols.tcp-ip 是一个优秀的新闻组,覆盖了 TCP/IP 的各种问题及其编程,链接如下。
https://groups.google.com/g/comp.protocols.tcp-ip?pli=1
Linux 板块: https://groups.google.com/g/comp.os.linux.networking
也就是: 熟练掌握 Google、各类 LLM GPT, StackOverFlow 等各类技术论坛 :-)
免费领取电子书
公众号主页发送关键字: Effective-TCP/IP-Programming
,免费领取 英文/中文 电子书。