0. 概述

这两天对一个问题突然有点兴趣,那就是因为我前面有一篇文章(CentOS 下的 TUN 与 TAP 应用)说过,我经常会通过 VPN 连接到公司的内网进行远程开发调试,这其实经常给我带来一些问题,其中一个问题就是当我连接上之后,我在连接之前建立的 SSH 远程主机的 Terminal 都被卡住了,这个时候很尴尬,连 Ctrl + C/D 都不起作用。这个问题其实一直都有,但是没有去想为什么,这两天突然灵光一闪,这难道是 TCP 的一个问题?

1. 问题

想必对 TCP 有些微了解的同学应该对 <TCP/IP 详解卷一> 中的这个 TCP 状态图印象深刻,但就我个人而言,我觉得对之了解并不深入,所以都是有机会就能够学到其中一两点知识,也算是庆幸:

对于正常情况来说,客户端服务器都按照我们预期中的连接、关闭,那么我对这张图可以解释相对清楚,但是,如果对于一些异常场景,我就不能很快得给出答案了,例如这个一个问题:

我们都知道 TCP 是一个虚拟连接,虽然可以看成一条虚拟的专用链路,但是,毕竟是虚拟的,不是真实的,我们没法真实得实时获知线路是断还是连着的,就以图中的 ESTABLISHED 状态来说,图中的状态迁移都是需要有数据来触发,才会走向各种 FIN 或者 CLOSE,那么如果客户端和服务器一天都不说话呢?按照这个图来说,那么不就是一直停留在 ESTABLISHED 状态了么?这可怎么办?

好在,《TCP/IP 详解卷一》特地开了一章小小只有几页的章节,叫做《23:TCP 的保活定时器》用来描述这种场景,这里主要考虑的是客户端或者服务器的不辞而别时应该怎么办,然而,在 《详解》 中并没有提供太好的思路,而是给了一个 TCP 的实现,那就是每间隔两小时,TCP 就会发送 Keep Alive 给对方(客户端和服务器都会发),如果对方是正常的,那么就等下一个两小时的周期;没有对方消息之后,TCP 就会间隔 75 秒再发送一次,直到 10 次之后就认定对方为失联状态,做结束处理。

这里其实有一个小细节需要注意的就是,如果客户端在两小时之内掉了然后又回来,或者服务器在两小时之内掉了又回来了,那么收到这个 Keep Alive 会怎么算,这个时候因为对方已经没有自己的 TCP 连接信息了,那么就会发送一个 RST 响应,重新进行 TCP 的连接。

这个机制虽然在 《详解》 中作者说争议很大,但是,甭管争议大不大,我想这在现实应用中大多数情况都是不适用的,因为两小时对于大多数应用来说是不可接受的,例如因为一个网络抖动就导致一个 API block 两个小时,这能让人舒服,所以在应用级别,其实应该有自己的一些手段。

2. 解决方式一

一个最简单的解决方法那肯定是不使用 TCP 啦,这样就把工作交给上层处理啦,例如 UDP 啥的,甭管你网络怎么样,我只管对端收到了我的数据并且给我响应,只要我没收到响应,那么就是不对的。

这样的好处就是不受上面说的问题的影响,但是这样有个问题,一个是增加了我应用的复杂性,我的管理 UDP 这些协议的东西,例如确保数据传输的可靠性这些;另一个就是这样也增加了我网络的负载呀,本来一个 TCP 长连接只管传数据就好了,现在每次传输都是一个完整的 UDP Packet,也是很冗余的。

于是在想有没有其他选择。

3. 解决方式二

既然两小时不可接受,那么能不能将这个时间调短一些呢?完全没问题呀,应该现在的 *NIX 系统都支持配置这个时间参数了,但是,在系统级配置你怎么能保证其他运行在这个系统的系统应用和用户应用都需要这个特性呢,万一他们就是要这么长怎么办?

TCP 其实也支持在单个 Socket 级别设置 keepalive 的空闲时长和心跳间隔,但是在实际实践中,更多地是在应用级做这个 Keep Alive 实现,例如每隔个 1 分钟,应用的 Client 就往应用的 Server 发一个:”Server, 你还在么”,然后 Server 就回一个 “在的,一切正常!”,然后大家就这样愉快得工作了。理由是,这样应用层可以做定制化的实现。虽然这种方式看上去是不错,但是我们需要做一些额外的工作:

4. Reference

  1. TCP/IP 详解卷一
  2. Long-lived TCP connections and Load Balancers
  3. 随手记之TCP Keepalive笔记