TCP 传输控制协议(一、初步了解)
引言
上一章我们讨论的是自身不包含可靠传递数据机制的协议。一般这种协议会使用一种像校验和或CRC这样的数学函数来检测接收到的数据是否有差错,但是他们不会尝试去纠正差错。不管是IP还是UDP都没有实现差错纠正。差错纠正一般有两种大的思路:校正和重传
- 校正:通信媒介可能会丢失或改变被传递的消息,使用差错校正码来纠正通信问题是处理差错的一种非常重要的方法,本质上就是添加一些冗余的比特,是的即使某些比特被毁,真实的信息也可以被恢复。
- 重传:通过
尝试重新发送
,知道信息最终被接收,这种方式称为重复请求(Automatic Repeat-reQuest,ARQ),ARQ是许多通信协议的基础。ARQ是OSI模型中数据链路层和传输层的错误纠正协议之一。通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。
这一章我们就会初步了解运输层可靠数据传输协议TCP(由于TCP的内容实在是太多,所以将会分几章介绍)
对于一个协议,核心要实现的特性其实只有两点:可靠和高效。下面先介绍下TCP协议的一些前置知识。
ARQ和重传
由于大部分情况不仅仅是单个信道通信,而是几个的多跳级联,除了遇到分组比特差错之外,还会遇到例如:分组重新排序、分组复制、分组泯灭等在中间路由器上发生的问题。为了在多跳通信信道上使用而设计的带纠错的协议必须要处理这些问题。
一个最简单处理分组丢失和比特差错的方法就是重发分组知道它被正确接收。这时候需要判断的问题有两个:
- 接收方是否已收到分组。
- 接收方接收到的分组是否与之前发送方发送的一样。
要处理这两个问题也很简单,无非就是接收方告诉发送方自己已经正确接收到一个分组,这个方法称作确认(acknowledgment)或 ACK。基本流程就是,发送方发送一个分组然后等待ACK。接收方接收到分组后发送对应的ACK,发送方接收到ACK之后发送下一个分组。流程本身没有什么复杂的地方,但是这个时候有三个新的问题值得被注意:
- 发送方对一个ACK的等待时长应该是多久。
- 如果ACK本身丢失怎么处理。
- 如果分组被接收到,但是里面有差错的话要怎么办。
对于这三个问题我们一个个来看,首先第一个问题确定等待时长其实是三个问题中实现起来最麻烦的,因为这个其实是没有一个标准的时间,这个问题放在后面讨论TCP超时重传的问题里面套路。
第二个问题处理方式就比较粗暴了,因为发送其实很难区分ACK丢失和原分组丢失这两种情况,所以解决方案就是直接不区分,统一按照原分组丢失来处理,直接重传(最近的学习过程中发现,越是这种偏底层的技术面对问题处理的方式越粗暴)。选择直接重传的方式固然快捷,但是会引入一个新的问题,接收方可能会接收到两个或多个同一分组的拷贝,那么解决这个问题的方法就是序列号 (sequence number)。在发送方发送的时候,每个唯一的分组都会分配一个新的序列号,这个序列号由分组自身携带,接收方就是通过序列号来区分该分组是否是重复是否需要丢弃(这个时候如果你足够聪明,就是想到一个新的问题序列号不可能是无限大的,那么序列号的使用肯定是重复使用某个区间的数值,那就意味着如果使用速度足够快,网络中是完全有可能出现两个不同的分组但是序列号相同
,那么接收方要怎么区分呢?在后面讲到防回绕的时候回提到,现在先略过)
第三个问题一般是使用编码技术来解决,校验和或者CRC都是比较受欢迎的解决方式,检查差错一般只需要使用比自身小很多的比特就可以纠正,所以简单的编码一般是不能纠正差错,只能检测。当接收方接收到一个有差错的分组时,就不发送ACK,直到发送方重新发送完整到达的无差错分组。
这里发现了一个很有意思的事情,就是上面这三个问题处理方式的最终表现,对于发送方来说都是一样的,就是在规定的时间内收不到ACK,然后重传。我觉得这里的设计真的挺厉害的,这个设计不但是解决了上面说到的问题,而且大大简化了发送方需要处理的情况,几乎没有额外引入新的问题。
讨论到这里,协议的可靠性得到了基本的解决,分组的丢失是错误基本都得到了保障,但是效率并不高。如果发送方发送一个哪怕很小的分组都要用很长时间的话(这个是完全有可能的,如果链路层是走卫星链路,一到两秒是正常时间。我们知道人类目前所能认知的信息传播的最大速度是光速,我们暂时是没有办法突破这个物理极限)。发送方发送一个分组后,就停下来等待ACK,此时协议被称为“停止和等待”。假设最理想的情况,整个传输过程中没有出现丢失和损坏,我们把M设为分组大小,R是往返时间(RTT),那么该协议的吞吐量性能与M/R成正比。但是如果有分组丢失和信息损坏的情况下,“吞吐质”(单位时间传输的有效数据量)则要低于吞吐量。
对于不会损害和丢失太多分组的网络来说,造成上面低吞吐量的的原因其实就是网络没有时刻处于繁忙状态。因为对于一条连接来说,同一时间只有一个分组存在网络中,如果想提高效率,最直接有效的方法无疑是同一时间允许多个分组存在网络中,从而提高吞吐量(是不是闻到了多线程那个熟悉的味道)
允许多个分组同时进入网络,我们就要解决新的问题:
- 发送方什么时候注入一个分组到网络中,注入多少个。
- 在等待ACK的时候,如何维护计数器。
- 需要保存每个还没确认的分组的一个副本以防需要重传。
除了上面这三个比较明显的问题