简单易懂的 TCP
理解 TCP 不需要背八股文,只需要记住一点:千万不要用上帝视角。
搜索任何 TCP 相关的资料,你都会看到这样的示意图:左边一条竖线,右边一条竖线,代表通信的两端;中间一个从左画到右的箭头,表示一方给另一方发了一个包。
看着这张图,你很自然会想:线不是连过去了吗?箭头就画在那里,我亲眼看见的,消息不就发过去了吗?为什么还需要那么多复杂的机制来保证可靠传输?
这就是上帝视角带来的误解。画图的人站在高处,两端和中间的一切都看得见;而通信中的任何一方,都没有这个视角。
密闭房间里的你
对通信的任何一方来说,真实处境更像这样:你(发送方)坐在一个密闭的房间里,看不见外面,只能委托邮递员替你送信。
正常流程是这样的:
- 邮递员来到你的房间,你把信交给他;
- 他出门,通过某种途径把信送到对方那里;
- 对方家的门卫签收后,写一张回执(ACK);
- 回执被带回你的房间,你这才知道信确实送到了。
注意,签收的是门卫,不是对方本人。回执由对方的操作系统(TCP 协议栈)发出,只说明信进了对方的信箱(接收缓冲区),不代表对方的应用程序已经读过、处理过。如果你的业务需要确认“对方确实处理了”,得在应用层自己确认,TCP 管不到。
对坐在密闭房间里的你来说,能知道的只有:
- 你在某个时间点把信交给了邮递员;
- 之后某个时间点,你收到了带回来的回执,并能看到回执上写的内容;
- 或者,你给自己定的闹钟响了,回执还没到。
除此之外,你一无所知。比如:
- 信是不是在路上丢了?(丢包:路由器队列满了直接丢弃,或者链路出错)
- 信是不是在路上被弄脏了,字迹认不清?(比特错误:对方用校验和检出后直接扔掉,不回任何消息)
- 对方其实收到了,但回执在回来的路上丢了?(ACK 丢失)
- 信和回执都没丢,只是路上堵车,还在慢慢走?(延迟)
这四种情况,你完全无法区分。你能感知到的只有一件事:信发出去了,闹钟响了,回执还没来。
寄出几封信试试。
但这四种情况的真相其实很不一样。前两种,对方确实没收到;后两种,对方要么已经收到了,要么马上就会收到。如果你统一按“丢了”处理、再发一封,对方就可能收到两封一模一样的信。
还有一点:对方也坐在一个密闭的房间里。他发出回执之后,同样不知道回执有没有送到你手上。通信的双方,谁都没有上帝视角。
从房间里往外推
带着这个视角,TCP 里那些看似复杂的机制,几乎都能自己推出来。
超时重传
既然“没收到回执”是你唯一能感知的异常信号,最直接的办法就是:闹钟响了还没回执,就再发一次。
问题是闹钟定多久?定短了,信只是慢了点你就重发,白白浪费;定长了,信真丢了你要干等很久。所以 TCP 会持续测量信从发出到收回执的往返时间(RTT),据此动态调整闹钟时长(RTO)。
序列号
重发会造成重复;同时在路上的多封信可能走不同的路,到达顺序也会乱。所以每封信都要编号(序列号,严格说编的是字节而不是信)。对方按编号去重、排序,再按顺序交给应用程序。
回执上也写编号,意思是“这个号之前的我都收到了,下一封请从这个号开始发”(累计确认)。
校验和
信上附一个根据内容算出来的校验值。对方收到后重新算一遍,对不上就说明信在路上被弄脏了,直接扔掉。于是“弄脏”被转化成了“丢失”,交给重传来处理。
校验和只防意外出错,不防有人故意篡改,那是 TLS 的职责。
三次握手
双方都在密闭房间里,正式通信前要先对齐一件事:各自的信从几号开始编(初始序列号)。每一方都要把自己的起始编号告诉对方,并拿到对方的确认。两个方向各一来一回,本来要四封信;中间“确认你的编号”和“告诉你我的编号”可以合成一封,于是成了三次。
此外,网络里可能还飘着很久以前延迟的旧连接请求。第三步给了发起方一个机会,对这种过期请求说“不”,避免对方被旧请求骗着建立连接。
滑动窗口
如果每发一封信都要干等回执回来才发下一封,大部分时间都浪费在等待上。所以你可以同时托多个邮递员送信,只要“已发出但还没确认”的量不超过一个上限(发送窗口)。收到回执后窗口往前滑,再发新的。
窗口还带来一个额外线索:如果连续收到好几张一模一样的回执,都写着“下一封请发 5 号”,说明 5 号之后的信陆续到了,唯独 5 号没到。这时你不必等闹钟响,可以提前重发 5 号(快速重传)。这是房间里除闹钟之外,另一条能推断丢包的线索。
流量控制
对方的信箱容量有限。如果他的应用程序取信慢,信箱塞满了,你再发也只会被扔掉。所以对方在每张回执上顺带写一句“我的信箱还能放多少”(接收窗口),你据此控制发送量。
拥塞控制
你看不见路况,只能从回执的情况推测。传统做法是把丢信当作堵车的信号:一开始小心试探,逐步加大发送量;一旦发现丢信,就主动减速(慢启动、拥塞避免)。这不只是为了你自己,路是所有人共用的,大家都猛发,谁都过不去。
四次挥手
TCP 的两个方向是独立的,“我没什么要发了”不代表“你也没什么要发了”。所以每个方向各自说一次“我发完了”,各自得到确认,一共四封信。
最后一张确认发出后,主动关闭的一方还要在房间里再等一段时间(TIME_WAIT),因为他不知道这张确认有没有送到。如果没送到,对方会重发“我发完了”,他得还在场,才能再确认一次。
回到那张图
现在再看那张示意图:左边一条线,右边一条线,中间一个箭头。
那个箭头不是事实,只是一种期望。对图里的任何一方来说,箭头的另一头都是黑的。TCP 的全部复杂性,就是在双方都看不见彼此的前提下,只靠“发出去的信”和“收回来的回执”,把一条不可靠的网络,变成一条按顺序、不丢不重的字节流。