先上定义,TCP是一种面向连接的可靠的基于字节流的传输层通信协议。
TCP属于传输层协议,传输层协议与网络层协议之间的主要区别是,网络层协议是为主机之间提供逻辑通信的,而传输层协议是为应用进程之间提供端到端的逻辑通信的。
这里的端到端指的就是套接字(Socket)。

TCP的全部功能都体现在它首部中各字段的作用。

具体这些字段的作用,在接下来的各个小节进行解释。

可靠传输

理想的传输条件有以下两个特点:

  1. 传输信道不产生差错
  2. 不管发送方以多快地速度发送数据,接收方总是来得及处理收到的数据。

实际的网络不具备这两个条件,可以采取一些措施解决这两个问题。

停止等待协议

解决丢包问题的最简单方式是停止等待协议:发送方每次发送完一个数据包后等待接收方的确认,然后才发送下一个数据包。
为了标识发送数据包和对应的确认,也就引出了TCP首部的两个标志位:序号(Sequence Num)确认号(Acknowledgment Number)

序号:
32位,4字节。序号范围是[0, 23212^{32} -1] TCP传输的每一个字节都按照顺序编号。整个要传输的字节流的起始编号(一般不为0)必须在连接建立时设置。首部中的序号字段指的是本报文段所发送的数据的第一个字节的序号。

确认号:
32位,4字节。是期望收到对方下一个报文段的第一个数据字节的序号,即下一个报文段的序号标志位。若确认号=N,则表明:到序号N-1为止的所有数据(字节)都正确收到。只有当控制位ACK设置为1时,确认号才有效。TCP规定,连接建立后,所有的报文段都必须将ACK置为1。

无差错时是下面这个样子:

出现差错时是有以下几种情况:

  1. 发送方的问题:接收方接收到数据包后发现数据包有错误或者发送方发送的数据包确实丢失了,总之就是发送方没有接收到接收方响应的确认。解决这种情况的方法是发送方每发送完一个数据包就设置一个超时计时器,如果直到超时都没有收到确认,就重传数据包。
  2. 接收方的问题:接收方的确认丢失了。那么当接收方再次收到发送方重传的数据包时,直接丢弃,并且再次发送确认
  3. 网络的问题:接收方的确认迟到了,那么无论是接收方还是发送方,收到重复发送的数据包和确认时,都直接丢弃就可以了。

连续ARQ协议

停止等待协议简单是简单,但是效率太低,信道利用率太差。
连续ARQ协议维护一个窗口,窗口中的数据包可以按顺序连续发送出去,无需等待对方确认,这样效率就上来了。

发送方每收到一个确认,就把发送窗口向前滑动一个数据包。接收方一般采用累积确认的方式。这就是说接收方不需要对收到的数据包逐个确认,而是在收到几个数据包后,对按序到达的最后一个数据包发送确认,表示到这个数据包为止的所有数据包都已正确收到。
很明显,使用ARQ协议后,接收方即使发送的确认丢失了也不用重传,因为之后的数据包到达,接收方向发送方反馈相应的确认,发送方在超时之前接收到了这个确认,那么就代表之前的数据包已经正确接收。
但是这也无法向发送方反映已经正确的收到数据包信息,例如:发送方发送了5个数据包,其中第3个数据包丢失了。这时接收方只能对前两个数据包进行确认,最后两个数据包即使到了也无法进行确认,那么最终发送方需要把后3个数据包进行重传。在网路不好的时候,影响很大。

滑动窗口

在TCP首部中,有一个字段叫做窗口。窗口字段指定了允许对方发送的数据量。窗口值是经常变化的。这个字段指的是发送此报文段的一方的接收窗口
在连接建立之初,发送方会收到接收方指定的窗口值(单位:字节)。发送方会根据这个值建立发送窗口。

发送窗口:在没有收到接收方的确认的情况下,发送方可以连续把窗口里的数据都发送出去。凡是已经发送过的数据,在未收到确认之前都必须暂存在发送方的发送缓存中,以便在超时重传时使用。在收到某个数据包的确认后,发送窗口的下边界位置只会增加或者不变,不会减小;上边界位置可能增加或者不变,也有可能减小,但是TCP标准强烈不赞成这样做。
发送方的应用进程把字节流写入TCP的发送缓存,接收方的应用进程从TCP的接收缓存中读取字符流。
发送窗口通常只是发送缓存的一部分。发送应用程序必须控制写入缓存的速率,不能太快,否则发送缓存就没有空间存放数据。
接收缓存用来存放:1)按序到达,但尚未被应用程序读取的数据;2)未按序到达的数据。接收窗口最大不会超过接收缓存的大小。
TCP使用滑动窗口来进行流量控制。

拥塞控制

拥塞控制与流量控制的关系密切,但是流量控制主要还是针对接收方的接收能力而言,拥塞控制则主要是防止过多的数据注入到网络当中。
1999年公布的RFC 2581定义了进行拥塞控制的四种算法,慢启动是其中的一种。

慢启动

拥塞控制的解决办法依然是通过设置一定的窗口大小,只不过,流量控制的窗口大小是接收方直接告诉发送方的,而拥塞控制的窗口大小按理说就应该是网络环境主动告诉发送方。但网络环境怎么可能主动告诉发送方呢?只能发送方单方面通过试探,不断感知网络环境的好坏,进而确定自己的拥塞窗口的大小。
慢启动的主要思路:在最开始时将窗口设置为1个最大报文段MSS。每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值,用这种方法逐步增大发送方的拥塞窗口。

窗口大小=min(拥塞窗口的大小,流量控制窗口的大小)窗口大小 = min(拥塞窗口的大小, 流量控制窗口的大小)
  1. 第一发送1个包
  2. 收到确认后窗口增大为2,然后在发送2个包,然后又会收到2个确认,每次收到确认都会将窗口值增加1,这样窗口值就是4了
  3. 然后再发送4个包

可以看出来,每一轮扩大的窗口都是上一轮的2倍,因此慢启动的“慢”并不是指拥塞控制的窗口大小增长的慢,而是说刚开始只发送一个数据包,这肯定比上来就把所有数据注入到网络中“慢得多”。
因为TCP慢启动的特性,这会导致通信双方在刚刚建立连接时的传输速度是最低的,后面再逐步加速直至稳定。

面向连接

我们知道,与同为传输层协议的UDP相比,TCP的最大区别就是TCP是面向连接的。这是指,应用程序使用TCP协议之前,必须先建立TCP连接,在传输完毕后,必须释放已经创建的TCP连接。

三次握手

因为TCP提供的是可靠的传输,而由上面的可靠传输机制我们知道,序号是可靠传输的基础。TCP传输的每一个包都必须带有序号,TCP发送方和接收方必须根据一个包的序号才能对这个包进行确认或者重传。而更具效率的累计确认机制,更是依赖序号才能确认X序号之前(不包括X)的包都被接收到了。
可以说序号保证了TCP协议的可靠和效率。
但是,在连接建立之初,通信双方需要交换或者说同步各自的初始序号(Initial Sequence Number)。为什么需要这么做呢?在RFC的Initial Sequence Number Selection一节中有描述。

The protocol places no restriction on a particular connection being used over and over again. A connection is defined by a pair of
sockets. New instances of a connection will be referred to as incarnations of the connection. The problem that arises from this is
– “how does the TCP identify duplicate segments from previous incarnations of the connection?” This problem becomes apparent if the
connection is being opened and closed in quick succession, or if the connection breaks with loss of memory and is then reestablished.

如果将一对socket定义为一个连接,TCP协议并不限制一个特定的连接被重复使用。那么在这两个socket之间建立的不同TCP连接中,可能出现TCP无法分辨出相同序列号的包是属于当前的TCP连接还是之前建立的TCP连接的情况。
为了避免这种情况就需要避免不同的TCP连接中出现相同的序列号。这就需要在每次建立TCP连接时,生成一个ISN(初始序号)。
那么又因为TCP协议是全双工的,所以通信双方都需要告知自己的ISN。所以建立连接的具体描述就如下了:
1) A –> B SYN my sequence number is X
2) A <– B ACK your sequence number is X
3) A <– B SYN my sequence number is Y
4) A –> B ACK your sequence number is Y
2与3都是 B 发送给 A,因此可以合并在一起,于是就是大名鼎鼎的三次握手啦~
至于为什么必须是三次握手,网络上真是众说纷云,我觉的说的都是正确的,只不过是看问题的角度不同,本篇文章就主要以RFC的以交换ISN为目的这种说法为主了。
初始序号有不同的生成方法,比如32位时钟等等。但是因为没有一个全局的网络时钟(如果有就直接用时间戳做序号就好啦),所以通信双方就必须交换各自的ISN了。
刚才有讲过,每个TCP包都是有序号的,包括发送方建立请求的第一个SYN包。那就会有上面说到的问题,接收方接收到第一个SYN包的时候,它并不知道这个包是本次要建立的TCP连接的第一个SYN包还是上次建立的TCP连接延迟了的包。除非它能记住上次建立的连接最后的序号,这显然并不总是可行的。所以接收方必须找发送方确认这个SYN。这就是三次握手的前两次了,那么如果没有第三次呢?也就是说,发送方不再确认收到的接收方的ISN,悲剧的是第二次握手的包真的丢了。这种情况下,接收方知道发送方的ISN,而发送方不知道接收方的ISN。就会造成发送方给接收方的包是可靠的,而接收方发送给发送方的包是不可靠的。所以必须三次握手喽~
如果接收方不需要给发送方发送数据,那我觉得第三次握手也可以不握。

四次挥手

  1. A的应用进程先向其TCP发出连接释放报文段,并停止再发送数据,主动关闭TCP连接。接释放报文段的FIN置为1,序号seq=u,它等于前面已经传送过的数据的最后一个字节序号加1。这时A进入FIN-WAIT-1状态,进入FIN-WAIT-1状态后再收到B主动传输的数据,A会响应RST。可以看看这篇文章
  2. B收到连接释放报文后即发出确认,确认号是ack=u+1,自己的序号是v,等于B前面已经传送过的最后一个字节的序号+1。B进入CLOSE-WAIT状态。此时TCP处于半关闭状态,即A已经没有数据要发送了,但B如果要发送数据,A仍然要接收。A收到B的确认后,A进入FIN-WAIT-2状态。
  3. B如果也没有数据要传输了,B发送连接释放报文段,FIN为1。确认号仍是ack=u+1,无论在半关闭状态期间B是否发送过数据。B进入LAST-ACK状态。
  4. A必须确认B发过来的连接释放报文,然后进入TIME-WAIT状态。然后经过2MSL后,才真正释放掉了本次连接。

等待2MSL的意义

  1. 为了保证A发送的最后一个ACK到达B,从而使得B能按照正常步骤进入CLOSED状态。
  2. 经过2MSL,本连接中的所有包都会从网络中消失,不会出现在下一个新的连接中。

Misc

通过上面的种种特性描述,可以看出来TCP协议本身是面向长时间大数据传输来设计的。所以只有在一段较长的时间尺度内,TCP协议才能展现出稳定性和可靠性的优势,不会因为建立连接的成本太高,成为了使用瓶颈。
这也导致了HTTP over TCP 这种搭配在目标特征上本身是有矛盾的。因为HTTP传输对象的主要特征是数量多、时间短、资源小、切换快。因此在HTTP协议的各个版本,针对TCP的各种缺点进行了解决:

HTTP/1.1

HTTP/1.1默认开启了Keep-Alive机制,复用连接,尽量较少TCP三次握手这种可能高达“百毫秒”为计时尺度的事件。
Keep-Alive机制的原理是让客户端对同一个域名长期持有一个或多个不会用完即断的 TCP 连接。典型做法是在客户端维护一个 FIFO 队列,每次取完数据之后的一段时间内,不自动断开连接,以便获取下一个资源时可以直接复用,避免创建 TCP 连接的成本。
但是这个机制的问题是队首阻塞:浏览器对多个资源的请求服用同一个TCP连接的情况下,第一个进入队列的请求可能会让服务器陷入长时间的运算状态,其他请求都被阻塞,就算服务端可以并行处理这些请求,但是只使用一个 TCP 连接来传输多个资源的话,一旦顺序乱了,客户端就很难区分清楚哪个数据包归属哪个资源了。

HTTP/2

在 HTTP/1.x 中,HTTP 请求就是传输过程中最小粒度的信息单位了,所以如果将多个请求切碎,再混杂在一块传输,客户端势必难以分辨重组出有效信息。
而在HTTP/2中,帧(Frame)才是最小粒度的信息单位,它可以用来描述各种数据,比如请求的 Headers、Body,或者是用来做控制标识,如打开流、关闭流。这里的流可以理解为对某个资源的数据通道,每个帧都附带有一个流 ID,以标识这个帧属于哪个流。这样就解决了HTTP/1.1中无法区分哪个数据包归属哪个资源的问题了。
这个技术特征叫做多路复用技术
但是多路复用技术也有缺点,由于 TCP协议重传机制的影响,一个错误的TCP包会导致所有的流都必须等待这个包重传成功。这使得HTTP/2更适合传输小资源。比如传输1000张10K的小图,HTTP/2要比HTTP/1.x快,但传输10张1000K的大图,则应该HTTP/1.x会更快。

HTTP/3.0

HTTP 是应用层协议,而不是传输层协议,它的设计原本并不应该过多地考虑底层的传输细节。从职责上来讲,持久连接、多路复用这些能力,已经或多或少超过了应用层的范畴。所以说,要想从根本上改进HTTP,就必须直接替换掉HTTP over TCP的根基,即TCP传输协议,这便是最新一代HTTP/3协议的设计重点。
2018年末,IETF正式批准了HTTP over QUIC使用HTTP/3的版本号,将其确立为最新一代的互联网标准。QUIC以UDP协议作为基础。UDP协议没有丢包自动重传的特性,因此QUIC的可靠传输能力并不是由底层协议提供的,而是完全由自己来实现。由QUIC自己实现的好处是能对每个流能做单独的控制,如果在一个流中发生错误,协议栈仍然可以独立地继续为其他流提供服务。这也就解决了HTTP/2多路复用存在的问题。