TCP系列01—概述及协议头格式
一、TCP简单介绍
我们经常听人说TCP是一个面向连接的(connection-oriented)、可靠的(reliable)、字节流式(byte stream)传输协议, TCP的这三个特性该怎么理解呢?
-
面向连接:在应用TCP协议进行通信之前双方通常需要通过三次握手来建立TCP连接,连接建立后才能进行正常的数据传输,因此广播和多播不会承载在TCP协议上。(谷歌提交了一个RFC文档,建议在TCP三次握手的过程允许SYN数据包中带数据,即 TFO(TCP Fast Open),目前ubuntu14.04已经支持该TFO功能)。但是同时面向连接的特性给TCP带来了复杂的连接管理以及用于检测连接状态的存活检测机制。
-
可靠性:由于TCP处于多跳通信的IP层之上,而IP层并不提供可靠的传输,因此在TCP层看来就有四种常见传输错误问题,分别是比特错误(packet bit errors)、包乱序(packet reordering)、包重复(packet duplication)、丢包(packet erasure或称为packet drops),TCP要提供可靠的传输,就需要有额外的机制处理这几种错误。因此个人理解可靠性体现在三个方面,首先TCP通过超时重传和快速重传两个常见手段来保证数据包的正确传输,也就是说接收端在没有收到数据包或者收到错误的数据包的时候会触发发送端的数据包重传(处理比特错误和丢包)。其次TCP接收端会缓存接收到的乱序到达数据,重排序后在向应用层提供有序的数据(处理包乱序)。最后TCP发送端会维持一个发送"窗口"动态的调整发送速率以适用接收端缓存限制和网络拥塞情况,避免了网络拥塞或者接收端缓存满而大量丢包的问题(降低丢包率)。因此可靠性需要TCP协议具有超时与重传管理、窗口管理、流量控制、拥塞控制等功能。另外TFO下TCP有可能向应用层提供重复的数据,也就是不可靠传输,但是只会发生在连接建立阶段,我们后续会进行介绍。
-
字节流式:应用层发送的数据会在TCP的发送端缓存起来,统一分片(例如一个应用层的数据包分成两个TCP包)或者打包(例如两个或者多个应用层的数据包打包成一个TCP数据包)发送,到接收端的时候接收端也是直接按照字节流将数据传递给应用层。作为对比,同样是传输层的协议,UDP并不会对应用层的数据包进行打包和分片的操作,一般一个应用层的数据包就对应一个UDP包。这个也是伴随TCP窗口管理、拥塞控制等。
在接下来的TCP系列中,我们将会依次介绍TCP协议的连接管理、超时与重传、流量控制、窗口管理、拥塞控制、存活检测等机制。在深入介绍这些内容之前我们先来看一下TCP的封装和协议头的格式(TCP/IP的网络分层等基础网络概念本处不在介绍)
二、TCP的封装和协议头的格式
TCP封装在IP报文中的时候,如下图所示,TCP头紧接着IP头(IPV6有扩展头的时候,则TCP头在扩展头后面),不携带选项(option)的TCP头长为20bytes,携带选项的TCP头最长可到60bytes。
其中不携带选项的TCP头如下图所示(其中阴影部分的四个字段表示了相反方向的数据流信息),其中header length字段由4比特构成,最大为15,单位是32比特(32-bit word),即头长的最大值为15*32 bits = 60bytes,因此上面说携带选项的TCP头长最长为60bytes。
TCP头中的相关字段顺序解释如下:
TCP源端口(Source Port):16位的源端口其中包含发送方应用程序对应的端口。源端口和源IP地址标示报文发送端的地址。
TCP目的端口(Destination port):16位的目的端口域定义传输的目的。这个端口指明报文接收计算机上的应用程序地址接口。
TCP的源端口、目的端口、以及IP层的源IP地址、目的IP地址四元组唯一的标识了一个TCP连接,一个IP地址和一个端口号的组合叫做一个endpoint或者socket。也即一对endpoint或者一对socket唯一的标识了一个TCP连接。接收端的TCP层就是根据不同的端口号来将数据包传送给应用层的不同程序,这个过程叫做解复用(demultiplex)。相应的发送端会把应用层不同程序的数据映射到不同的端口号,这个过程叫做复用(multiplex)。
TCP序列号(序列码SN,Sequence Number):32位的序列号标识了TCP报文中第一个byte在对应方向的传输中对应的字节序号。当SYN出现,序列码实际上是初始序列码(ISN),而第一个数据字节是ISN+1,单位是byte。比如发送端发送的一个TCP包净荷(不包含TCP头)为12byte,SN为5,则发送端接着发送的下一个数据包的时候,SN应该设置为5+12=17。通过系列号,TCP接收端可以识别出重复接收到的TCP包,从而丢弃重复包,同时对于乱序数据包也可以依靠系列号进行重排序,进而对高层提供有序的数据流。另外SYN标志和FIN标志在逻辑上也占用一个byte,当SYN标志位有效的时候,该字段也称为ISN(initial sequence number),详细请参考后续的TCP连接管理。
TCP应答号(Acknowledgment Number简称ACK Number或简称为ACK Field):32位的ACK Number标识了报文发送端期望接收的字节序列。如果设置了ACK控制位,这个值表示一个准备接收的包的序列码,注意是准备接收的包,比如当前接收端接收到一个净荷为12byte的数据包,SN为5,则发送端可能会回复一个确认收到的数据包,如果这个数据包之前的数据也都已经收到了,这个数据包中的ACK Number则设置为12+5=17,表示17byte之前的数据都已经收到了。在举一个例子,如果在这个数据包之前有个SN为3,净荷为2byte的数据包丢失,则在接受端接收到这个SN为5的乱序数据包的时候,协议要求接收端必须要回复一个ACK确认包,这个确认包中的Ack Number只能设置为3。
头长(Header Length):4位包括TCP头大小,指示TCP头的长度,即数据从何处开始。最大为15,单位是32比特(32-bit word)。
保留(Reserved):4位值域,这些位必须是0。为了将来定义新的用途所保留,其中RFC3540将Reserved字段中的最后一位定义为Nonce标志。后续拥塞控制部分的讲解我们会简单介绍Nonce标志位。
标志(Code Bits):8位标志位,下面介绍。
窗口大小(Window Size):16位,该值指示了从Ack Number开始还愿意接收多少byte的数据量,也即用来表示当前接收端的接收窗还有多少剩余空间。用于TCP的流量控制。
校验位(Checksum):16位TCP头。发送端基于数据内容计算一个数值,接收端要与发送端数值结果完全一样,才能证明数据的有效性。接收端checksum校验失败的时候会直接丢掉这个数据包。CheckSum是根据伪头+TCP头+TCP数据三部分进行计算的。另外对于大的数据包,checksum并不能可靠的反应比特错误,应用层应该再添加自己的校验方式。
优先指针(紧急,Urgent Pointer):16位,指向后面是优先数据的字节,在URG标志设置了时才有效。如果URG标志没有被设置,紧急域作为填充。加快处理标示为紧急的数据段。
选项(Option):长度不定,但长度必须以是32bits的整数倍。常见的选项包括MSS、SACK、Timestamp等等,后续的内容会分别介绍相关选项。
在标志(Code Bits)中的八位标志位分别介绍如下
CWR(Congestion Window Reduce):拥塞窗口减少标志被发送主机设置,用来表明它接收到了设置ECE标志的TCP包,发送端通过降低发送窗口的大小来降低发送速率
ECE(ECN Echo):ECN响应标志被用来在TCP3次握手时表明一个TCP端是具备ECN功能的,并且表明接收到的TCP包的IP头部的ECN被设置为11。更多信息请参考RFC793。
URG(Urgent):该标志位置位表示紧急(The urgent pointer) 标志有效。该标志位目前已经很少使用参考后面流量控制和窗口管理部分的介绍。
ACK(Acknowledgment):取值1代表Acknowledgment Number字段有效,这是一个确认的TCP包,取值0则不是确认包。后续文章介绍中当ACK标志位有效的时候我们称呼这个包为ACK包,使用大写的ACK称呼。
PSH(Push):该标志置位时,一般是表示发送端缓存中已经没有待发送的数据,接收端不将该数据进行队列处理,而是尽可能快将数据转由应用处理。在处理 telnet 或 rlogin 等交互模式的连接时,该标志总是置位的。
RST(Reset):用于复位相应的TCP连接。通常在发生异常或者错误的时候会触发复位TCP连接。
SYN(Synchronize):同步序列编号(Synchronize Sequence Numbers)有效。该标志仅在三次握手建立TCP连接时有效。它提示TCP连接的服务端检查序列编号,该序列编号为TCP连接初始端(一般是客户端)的初始序列编号。在这里,可以把TCP序列编号看作是一个范围从0到4,294,967,295的32位计数器。通过TCP连接交换的数据中每一个字节都经过序列编号。在TCP报头中的序列编号栏包括了TCP分段中第一个字节的序列编号。类似的后续文章介绍中当这个SYN标志位有效的时候我们称呼这个包为SYN包。
FIN(Finish):带有该标志置位的数据包用来结束一个TCP会话,但对应端口仍处于开放状态,准备接收后续数据。当FIN标志有效的时候我们称呼这个包为FIN包。
另外我们一般称呼链路层的发出去的数据包为帧(frame),称呼网络层发给链路层的数据包为包(packet),称呼传输层发给网络层的数据包为段(segment)。但是正如我们描述所用,段、包、帧也经常统称为数据包或者数据报文。
对应用层来说TCP是一个双向对称的全双工(full-duplex)协议,也就是说应用层可以同时发送数据和接收数据。这就意味着数据流在一个方向上的传输是独立于另一个方向的传输的,每个方向上都有独立的SN。
三、TCP中的数据包窗口和滑窗
在TCP的发送端和接收端都会维持一个窗口,因为一个TCP连接是双向的,因此实际上一个TCP连接一共有四个窗口。此处我们先简单介绍一个发送端的窗口如下。图中的数字表示byte也就是和上面介绍的TCP协议头中的SN是对应的,3号byte以及3号之前的数据表示已经发送并且收到了接收端的ACK确认包的数据;4、5、6三个byte表示当前可以发送的数据包,也有可能已经已经发送了但是还没有收到ACK确认包;7号byte及之后的数据表示为了控制发送速率暂时不能发送的数据。其中4-6这三个byte就称呼为窗口大小(window size)。当TCP连接建立的时候,双方会通过TCP头中的窗口大小字段向对方通告自己接收端的窗口大小,发送端依据接收端通告的窗口大小来设置发送端的发送窗口大小,另外在拥塞控制的时候也是通过调整发送端的发送窗口来调整发送速率的。窗口这个词的来源就是当我们从这一个数据序列中单独看4、5、6这几个byte的时候,我们仿佛是从一个"窗口"中观察的一样。此处先简单有个滑窗的概念后续我们讲到TCP的窗口管理的时候会继续进一步介绍TCP的滑窗。
相关补充:
1、TCPIP最初传输层和IP层是同一个层的,关于网络分层的小故事可以参考http://news.cnblogs.com/n/187131/ , IETF上还专门有一个愚人节系列的RFC,参考https://en.wikipedia.org/wiki/April_Fools%27_Day_Request_for_Comments
2、TCP头中CheckSum的计算可以参考资料
http://www.roman10.net/2011/11/27/how-to-calculate-iptcpudp-checksumpart-1-theory/这个连接里面一共三个系列分别是理论、实现、用例和验证。
http://www.tcpipguide.com/free/t_TCPChecksumCalculationandtheTCPPseudoHeader-2.htm
或者参考TCPIP详解卷一第二版(暂时只有英文版,名字TCP/IP Illustrated Volume 1 Second Edition The Protocols) P475页
3、后面涉及的相关RFC文档可以去IETF官网 http://www.ietf.org/查询 直接搜索RFC号码就行了。整个TCP相关的协议体系,IETF梳理后在2015年以RFC7414发布,想了解或者查询TCP相关RFC协议的可以先看看RFC7414协议,以对整个TCP协议有个概括了解
4、目前linux4.4实现上还不支持Nonce标志,可以参考内核代码中struct tcphdr结构对TCP头的定义。关于Llinux不同版本内核TCP实现相关的更新可以查看linux的changelog,网址https://kernelnewbies.org/LinuxVersions 其中有每个版本的changelog 还有对应的修改代码
5、TCP传输中的包重复,不见得是TCP重传导致的,也可能是因为IP层提供的不可靠传输导致的 http://stackoverflow.com/questions/12871760/packet-loss-and-packet-duplication
6、在网络传输过程中NAT可能会修改checksum甚至系列号seq,后面我们讨论窗口管理等内容的时候为了简化不考虑nat修改seq的场景,即认为发送端发包的seq与接收端接收这个包的时候seq相同。