蛮荆

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

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 大佬书籍列表:

链接地址: https://www.douban.com/personage/30082882/creations?sortby=vote&type=writer&role=&format=pic

整套丛书贴近实战,深入浅出,用实验设计和代码来演示网络编程中的各个知识点,真正做到了 “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,免费领取 英文/中文 电子书。

扩展阅读

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。