TCP之旅

前言

前面两篇介绍 Socket 的文章中,简单描述了 Socket 网络编程的过程。这一篇,主要介绍一下 TCP 的工作原理。包含以下六点:

  1. TCP连接简介
  2. TCP报文段
  3. 连接的建立和断开(三次握手,四次挥手)
  4. TCP可靠在哪里(序号、ACK回复、超时重传)
  5. 滑窗机制
  6. 拥塞控制机制

一、TCP连接简介

TCP 全称 Transmission Control Protocol,传输控制协议,是传输层的协议。它的任务是将应用层的数据准确地交付给网络层。

两个应用程序,如果用TCP来发送数据,那么他们必须先互相“握手”。也就是说,在正式传数据之前,要先互相发送一些预备报文段,以建立确保数据传输的参数。所以我们说,TCP是面向连接的(connection-oriented),也是可靠的传输

虽然TCP是面向连接的,但是这种“连接”,不是端到端线路意义上的连接,也不是虚电路。TCP的连接,是保留在两个端系统的状态当中的。因此,中间路由器对TCP连接是视而不见的。

TCP提供的是全双工服务,数据可以从A到B,也能从B到A。同时,TCP是点对点的,TCP连接是单个发送方与单个接收方之间的连接,不能多播。(但这并不代表一个服务器不能与多个客户端建立连接,只是说,一个连接只能在两台主机之间,如果一台主机要接收另外两台主机的数据,需要为那两台主机分别建立TCP连接)

TCPconnection

TCP是怎样建立连接的(三次握手)?

  1. 首先,客户端先发送一个特殊的TCP报文段
  2. 服务器返回另一个特殊的报文段响应
  3. 客户端用第三个特殊报文段作为响应

前两个报文段,不承载“有效载荷”(不包含应用层的数据),仅仅是打招呼。后一个报文段,可以承载有效载荷。由于在这两台主机之间发送了 3 个报文段,所以这种连接建立的过程,我们称之为“三次握手”,稍后我们详细讨论。

TCP3wayhandshake


二、TCP报文段

TCP将应用程序的报文(message)分组,再为每个分组加上TCP首部,形成多个TCP报文段(TCP segment),再下传给网络层。报文段的首部如下图:

TCPsegment

首部包含了以下信息:

  1. 源端口号(source port)目的端口号(destination port) :各16位,因为16位最大能表示的数字是65535,所以端口号最大值也只能是65535。
  2. 序号字段(sequence number field) :32位,用来实现可靠数据传输,例如一个500000字节的文件被分为500个TCP报文段,初始序号可以随意指定,后续的序号是前面序号+携带的字节数,例如初始序号79,每个报文段携带1000字节,第二个序号就是1079
  3. 确认号字段(Acknowledgement number field) :32位,也是用来实现可靠数据传输,表示期望对方发送的下一字节的序号
  4. 接收窗口字段(receive windows field) :16位,用于流量控制,表示接收方愿意接受的字节数量
  5. 首部长度字段(header length field) :4位,TCP的首部长度是可变的,这个字段表示首部长度
  6. 选项字段(options field) :不定位数,用作窗口调节因子
  7. 标志字段(flag field) :6位,包括ACK、RST、SYN、FIN、PSH、URG,各占1位

我们主要关注三个点:

  1. 一个TCP头部需要包含出发端口和目的地端口。这些与IP头中的两个IP地址共同确定了连接。
  2. 每个TCP片段都有序号。这些序号最终将数据部分的文本片段整理成为文本流。
  3. 只有标志字段的ACK位设定的时候,确认号字段才有效。ACK确认号说明了接收方期待接收的下一个片段,所以ACK确认号为最后接收到的片段序号加1。

三、TCP连接的建立和断开

三次握手

前面提到,TCP建立连接的过程称为三次握手。其详细过程如下:

  1. 第一步:客户端先发送一个特殊的TCP报文段,不包含应用层数据。但首部标志字段的SYN位被置为1(称为SYN报文段),并随机指定一个初始序号(seq)。(为什么初始序号要随机,主要是安全方面考虑)。
  2. 第二步:服务器收到客户端的SYN报文段后,就会为该TCP连接分配TCP缓存和变量,并回复一个报文段(称为SYNACK报文段)。在这个回复报文段里,SYN位也是被置为1,确认号ack置为客户端刚刚发来的初始序号seq+1,并随机指定一个自己的初始序号seq。
  3. 第三步:客户端收到服务器的SYNACK报文段后,在客户端为该TCP连接分配缓存和变量,同时也回复了一个报文段,此时连接已经建立,所以SYN置为0,ACK确认号为服务器的初始序号seq+1,seq为自己第一步的seq+1

四次挥手

当TCP连接的任意一方想断开连接时,将会发起断开信号,连接结束后,主机里的TCP连接资源(缓存和变量)随即被释放。断开的过程需要通信四次,所以称为四次挥手。其详细过程如下:

  1. 第一步:客户端发起关闭连接命令。此时会向服务器发送一个特殊的报文段,标志字段的FIN位被置为1,随后客户端进入 FIN_WAIT_1 状态,该状态表示客户端正在等待服务器的ACK确认。
  2. 第二步:服务器收到这个特殊的报文段后,回复一个确认报文段,收到这个确认报文段后,客户端进入 FIN_WAIT_2 状态,该状态表示客户端期望收到服务器的可以终止命令。
  3. 第三步:稍后,服务器也向客户端发起一个FIN位为1的特殊报文段,表示可以终止。
  4. 第四步:客户端回复一个确认报文段进行确认,之后进入 TIME_WAIT 状态等待30秒(目的是确认报文若丢失可以重传),之后连接断开。

tcp


四、TCP 为何可靠

“流”和次序

我们之前提到,Linux的哲学是“一切皆文件”,一切都可以用“打开open –> 读写write/read –> 关闭close”的模式来操作。计算机程序之间的通信,是一个进程写入文本流(byte stream),而另一个进程读取这个流。TCP协议虚拟了文本流的通信

要知道,在两台相隔很远的主机中,隔着许许多多的路由器和交换机,因此数据发送的时候,先发的数据可能会后到。好在TCP协议确保了数据到达的顺序与文本流顺序相符。这就是为什么TCP报文段里面需要有序号的原因。TCP接收方将接收到的许许多多报文段按照序号排列起来,组成原始数据。

“收到请回复”

TCP为什么需要确认号呢?我们知道,TCP是全双工的协议,在A主机向B主机发送数据的同时,B主机也能向A主机发送数据(在同一个TCP连接中)。例如,A向B发送8字节数据时携带了序号,seq=79,B向A发数据时也需要携带一个序号seq=45,同时,“回复”确认收到了A主机seq=79的序号,并期望接收A主机seq=87(为什么不是80,因为前一个序号有8字节)的序号,所以就有了确认号ack=87。

也就是说,TCP发送方每发送一个报文段,接收方都会回复一个确认号。以确保这个片段收到了。比如已经接收到了片段1,片段2,片段3,那么接收方就开始期待片段4。值得注意的是,如果此时收到了片段5 片段6,也不会丢弃,会暂时存起来,等到片段4到达再拼接,但是如果此时收到了片段9,那么接收方就可能拒绝接收了。

报文段丢了,重发

IP协议是不可靠的,也就是说,交付给网络层的报文段,可能会传着传着就丢了。那咋办? TCP有一个定时器超时,规定如果一段时间之后,还没有收到接收方的确认(ACK),就默认这个报文段丢了。就会重新发一个过去,直到接收方回复确认收到。

那么,如果说,这个报文段发过去了,接收方也接收到了。接收方返回给发送方的“确认”信息丢了,怎么办呢? 这种情况下,发送方依然会重发。 接收方一看,咦你居然发了两个一样的报文段?就会自动丢弃其中的一个了,当然,还要给发送方再发一次“收到”回复,以免发送方孜孜不倦、不辞劳苦地一直重发、一直重发。


五、滑窗

我们已经知道,TCP发送方片段1发出去,接收方回复ACK已收到1,发送方再发片段2,接收方再回复ACK已收到2……

在这种模式下,发送方保持发送->等待ACK->发送->等待ACK…的状态,虽然很“可靠”,但是效率太慢了。为了提高效率,同时发多个片段,又怕后发的先到了,怎么办呢?

我们可以这么做:利用缓存保留一些“不那么乱”的片段,期望能在短时间内补充上之前的片段(暂不处理,但发送相应的ACK);对于“乱”的比较厉害的片段,则将它们拒绝(不处理,也不发送对应的ACK)。

滑窗(sliding window)被同时应用于接收方和发送方,以解决以上问题。发送方和接收方各有一个滑窗。当片段位于滑窗中时,表示TCP正在处理该片段。滑窗中可以有多个片段,也就是可以同时处理多个片段。滑窗越大,越大的滑窗同时处理的片段数目越多(当然,计算机也必须分配出更多的缓存供滑窗使用)。

TCP有专门的算法动态调整滑窗的大小。


六、TCP拥塞控制

在TCP协议中,我们使用连接记录TCP两端的状态,使用序号和分段保证了TCP传输的有序,使用滑窗(中的某些机制)来实现发送方和接收方处理能力的匹配,并使用重复发送来实现TCP传输的可靠性。

一切看似都很美好,但是,随着互联网的发展,越来越多的主机之间要相互发送数据,有时候中间路由器会处理不过来从而发生丢包,一丢包,这些基于TCP的主机就又重新发送,路由器不堪重负,就会发生更严重的丢包,构成了一个恶性循环。这称为堵塞崩溃。

因此,为了避免发送堵塞崩溃,TCP加入了拥塞控制机制。当TCP发送方探测到网络拥堵时,会控制自己发送片段的速率,以缓解网络的交通状况。

如何探测网络拥堵

当发生ACK超时和重复ACK。发送方就认为TCP片段丢失,则认为网络中出现堵塞。

如何控制速率

TCP协议通过控制滑窗(sliding window)大小来控制发送速率。在TCP滑窗管理中,有一个窗口限制,就是advertised window size,以实现TCP流量控制。TCP还会维护一个congestion window size,以根据网络状况来调整滑窗大小。真实滑窗大小取这两个滑窗限制的最小值,从而同时满足两个限制 (流量控制和堵塞控制)。

(TCP拥塞控制和滑窗的内容来自vamei的博客,vamei写得太好了,值得好好学习)