本书 《网络协议栈入门》 采用的代码是 基于 linux 内核 4.4.4 版本的。linux 内核源码下载地址: mirrors.edge.kernel.org
在《UDP为什么没有epoll》里面讲了,协议栈里面的链接状态,只是一个内存变量,虚拟的东西。而且不会非常快地更新。
所谓的TCP连接不是物理的连接,是为了实现数据的可靠传输由通信双方进行三次握手交互而建立的逻辑上的连接,通信双方都需要维护这样的连接状态信息。比如netstat经常看到连接的状态为ESTABLISHED,表示当前处于连接状态(这里需要注意的是这个ESTABLISHED的连接状态只是操作系统认为当前还处在连接状态)。可能链路已经不通,只是TCP层还没有感知到这一信息,操作系统层面显示的状态依然是连接状态,而且因为TCP层还认为连接是ESTABLISHED,所以作为应用层自然也就无法感知当前的链路不通。
TCP 的重传机制要 19 秒 左右才知道底层链路通信异常,而且要发数据那一刻开始过19秒才知道,空闲的时候不能知道。
TCP 协议由于无法及时感知当前的链路不通,,所以需要上层协议实现心跳来 快速 知道底层链路通信是否正常。
举个例子,我基于 TCP 协议,设计了一个 TXP 协议,TXP 协议是这样的。
TXP 客户端内部,会每隔 1秒发一个 TCP 心跳包给服务器,这个心跳包没什么数据,就是一个标示字段,指名是心跳包。
TXP 服务器收到客户端心跳包,就会回复 X-ACK,这里的 X-ACK 是TXP 的,不是 TCP 的ACK。
在上面这个流程里面,TXP 的客户端服务器都能及时知道跟对方的通信是否正常,TXP 客户端 3秒之后,还没收到 X-ACK,就会认为跟服务器的通信有问题。
然后 TXP 客户端 就可以切换另一个 服务器IP进行 建立 TXP 链接。3秒就能感知到 通信链路问题,什么是通信链路问题,就是TXP 客户端到 服务器A 中间的某个路由器挂了,但是 TXP 客户端到 服务器B 中间的路由器没问题。有可能会有这种情况,或者服务器A死机了发 RST 丢包客户端没收到,都会导致 无法及时感知当前的链路不通。
所以 心跳可以让应用层尽快知道底层链路 有问题,尽快切换服务器。或者跟原来的服务器重新建立链接。
上面 的 "跟原来的服务器重新建立链接" 是针对 TCP 的,对于 基于 UDP 实现的协议没有这个问题,不需要重新建立链接。
举个例子,TCP 服务器会把链接状态存进去内存,然后如果 服务器重启了,一般TCP 会发RST 给客户端,但是如果重启的同时,网络抖动了,RST 客户端没收到,所以此时,TCP 服务器已经没有之前的 客户端 IP+PORT 的链接信息在内存里面,因为他重启,而客户端没有 收到RST,导致客户端一直以为这个 TCP 链接是好的,但是过了5分钟,客户端要发 TCP 数据包A,因为TCP 服务器没有这个客户端 IP+PORT 的链接信息在内存里面,服务器收到TCP 数据包A 就会立即返回RST给客户端。这时候客户端才知道 TCP 链接断开,需要重新建立链接。
在这种情况下,客户端就没法及时知道TCP链接不可用,用心跳可以解决。
然后我为什么说 基于 UDP 实现的面向链接的协议可以不用管这个问题。
是因为基于UDP 实现的面向链接的协议,可以把链接状态,已建立连接的客户端IP+PORT等内存数据,我把它们写进去文件永久存储,服务器重启后拿出来用。
例如 客户端 每隔1秒发一个UDP 心跳包,3次失败后执行3次握手重新建立链接,我服务器重启只需2秒,所以不会触发客户端的重新建立链接。因为链接信息我可以从内存写到文件,重启后再从文件拿回来内存里面。
而TCP 里面的链接状态只放在内存,没有接口可以导出到文件存储,也没法从文件导进去TCP内核。重启之后,TCP 链接就丢了。
相关阅读:
由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。QQ:2338195090。