一个TCP连接由一对端点或套接字构成末期至通信的每一端都由一对(IP地址,端口号)所唯一标识。一个TCP连接通常分为启动,数据传输(连接已建立),退出
TCP连接建立及终止的示意图如下:
一开始,双方都处于CLOSED
状态,先是服务端主动监听某个端口,处于LISTEN
状态
SYN_SENT
状态SYN_RCVD
状态ESTABLISHED
状态。服务端接收到ACK后也进入ESTABLISHED
状态通过上述三个报文段就能完成一个连接的建立,通常称为三次握手
只要记住:需要对方确认的报文段都需要消耗序列号。SYN报文需要对方确认,所以需要消耗序列号;而发送(不携带数据的)ACK不需要对方确认,所以不需要消耗序列号
在一个连接中,TCP报文段在经过网络路由后可能会存在延迟抵达与排序混乱的情况,为了解决这一问题,需要仔细选择初始序列号,序列号从初始序列号开始有规律地变化,对应着每个报文段原本的顺序,从而尽管会有到达接收方时报文段排序混乱的情况,也能根据序列号进行调整
初始序列号会随时间而改变 (注意,说的是初始序列号而不是序列号),因此每一个连接都拥有不同的初始序列号。初始序列号可被视为一个32位的计数器,该计数器的数值每4微秒加一,其目的在于为一个连接的报文段安排序列号时防止出现与其它连接的序列号重叠
初始序列号除了需要防止重叠以外,还有一个是安全性问题。试想一下,如果选择合适的序列号,IP地址以及端口号,那么任何人都能伪造出一个TCP报文段。抵御这种行为的方法是使初始序列号变得相对难以猜出,另一种方法是加密
现代系统通常采用半随机的方法选择初始序列号
在Linux系统中,采用基于时钟的方案,并且针对每一个连接为时钟设置随机的偏移量,随机偏移量是在连接标识(即四元组)的基础上利用加密散列函数得到的,而散列函数的输入每隔5分钟就会改变一次,也就是说对于同一个连接的不同实例得到的随即偏移量也是不同的。在32位的初始序列号中,最高的8位是一个保密的序列号,而剩余的各位则由散列函数生成
>>> 为什么不能是两次?
主要原因是为了防止已失效的连接请求报文突然又传送到了B。考虑这样一种情况,客户端发送的第一个连接请求报文段并没有丢失,但是在某些网络节点长时间滞留,或者由于网络拥塞长时间未到达服务端。那么客户端会重发SYN,然后建立起连接。假设第一个连接请求报文段延误到客户端跟服务端的连接释放后的某个时间才到达服务端,本来这个请求报文已经失效了。但服务端收到后误以为是客户端的又一次新的连接请求,于是就向客户端返回ACK同意建立连接,并进入ESTABLISHED
状态等待客户端发送数据,对服务端来说,新的连接就已经建立了,这样服务端就浪费了许多资源。服务端不能判断请求报文段是否有效,但客户端可以(根据ACK的序列号/时间戳)判断本次连接是否有效,所以需要让客户端判断ACK是否有效,有效就进行第三次握手,告知服务端可以建立连接
换句话说,只有两次握手的话,服务端在接收到SYN后就会进入ESTABLISHED
状态,而不论SYN是否有效,这就会导致服务端浪费资源,解决办法就是在服务端建立连接之前阻止掉无效的历史连接,这就需要第三次握手来完成
其次,可以确保双方的序列号同步。客户端发送SYN到服务端,服务端响应了ACK,就说明从客户端到服务端这个方向上的序列号成功同步了;同理,服务端发送ACK时也会发送自己的SYN,然后客户端响应ACK,就说明从服务端到客户端的这个方向上的序列号同步了,一共三步,确保了双方的序列号都成功接收了
>>> 为什么不是四次?
其实在服务端发给客户端ACK以及SYN这里可以看成两步,即发响应客户端SYN的ACK,以及发自己的SYN,但可以合并为一步合并了。像这样三步已经理论上足够一个可靠连接的建立,所以不需要使用更多的通信次数了
第一次握手丢失,即客户端发出的连接建立超时,那么会触发超时重传机制,重传SYN报文,如果一直没有收到ACK应答,就会一直重传。重传的相隔时间是上一次重传相隔时间的两倍,这一行为被称为指数回退,每次回退数值都是前一次数值的两倍
在Linux中,系统配置变量net.ipv4.tcp_syn_retries
表示在一次主动打开申请中尝试重新发送SYN报文段后的最大次数;相应地,变量net.ipv4.tcp_synack_retries
表示在响应对方的主动打开请求时尝试重新发送SYN + ACK的最大次数,它们默认数值为5
第二次握手,包含了服务端对第一次握手,即客户端的SYN报文的确认ACK;还包含了服务端发送给客户端的SYN。所以需要从这两个方面分析
对客户端的SYN报文的ACK丢失了,就会触发客户端超时重传SYN。其实对于客户端来说,第二次握手丢失触发的事件跟第一次握手丢失触发的事件是一样的
服务端的SYN丢失了,对服务端来说就相当于自己发出了SYN却没有得到对方的ACK,那么也会触发超时重传,重传SYN + ACK报文。SYN + ACK报文最大重传次数由参数net.ipv4.tcp_synack_retries
决定。可以思考一下,第三次握手丢失会发生什么,其实第三次握手丢失,对于服务端来说,跟第二次握手是一样的,不管是第二次握手丢失,还是第三次丢失,对于服务端来说,他只知道自己没有收到对方的ACK,所以会重传SYN + ACK (ACK值与上次的SYN + ACK报文中的ACK值相等)
注意,虽然说的是重传 SYN + ACK 报文,但是ACK是不会重传的,当ACK丢失了,就由发送方重传对应的报文
连接的任何一方都能够发起一个关闭操作。通常发起关闭连接的是客户端,然后一些服务器如Web服务器在对请求做出响应之后也会发起一个关闭操作。通常一个关闭操作是由应用程序提出关闭连接的请求而引发的 (如使用系统调用close())
TCP一端通过发送一个FIN报文段 (即FIN控制位置位的报文段)来发起关闭操作,而且只有当连接双方都完成关闭操作后才构成一个完整的关闭
FIN_WAIT_1
状态CLOSE_WAIT
状态。此时上层的应用程序会被告知连接的另一端已经提出了关闭操作,通常 (TCP支持半关闭,见下文),这将导致应用程序发起自己的关闭操作。客户端接收到服务端的ACK应答报文后,进入FIN_WAIT_2
状态LAST_ACK
状态TIME_WAIT
状态。如果出现FIN丢失的情况,发送方将重新传输直到接收到一个ACK确认为止。服务端接收到ACK应答报文后,就进入CLOSED
状态,至此,服务端已经完成连接的关闭。客户端在TIME_WAIT
状态结束后自动进入CLOSED
状态,至此客户端的关闭操作也完成了综上所述,连接的关闭需要四个报文段,又称为四次挥手。每一方关闭其到对方的连接都需要两个报文段,加起来一共四个报文。两次关闭操作在分析时可以对称地分析
当主动关闭方发送FIN报文段后,长时间没有收到对方的ACK应答,也会触发超时重传机制;最大重发次数由tcp_orphan_retries
参数控制。当重发次数达到tcp_orphan_retries
后 (且最后一次重发后在等待时间内也没有收到ACK),就不再发送FIN,直接进入CLOSED
可能会出现有的应用程序在自己的数据已经发送完成了,发送FIN关闭从自己到对方的连接,但它仍然希望能接收来自对方的数据,直至对方发送了FIN。即只关闭自己到对方的连接而不关闭对方到自己的连接,这就是半关闭。应用程序可以通过调用shutdown() 来代替close() 函数,就能实现上述操作
FIN_WAIT_2状态表示某通信端已主动发送一个FIN并已得到另一端的确认。从示意图中可以看出,在这种状态下,只有等对方发送了FIN并且这一端接收到了FIN,这一端才会进入TIME_WAIT
状态,这意味着这一端能够一直保持这种状态,对方也能一直保持CLOSE_WAIT
状态,直到对方的应用程序决定关闭连接
为了防止连接处于这种无限等待状态,如果主动关闭的应用程序执行的是一个完全关闭操作,而不是一个半关闭,那么就会设置一个计时器,如果当计时器超时时连接是空闲的,主动关闭的这一端就会从FIN_WAIT_2
进入到CLOSED
状态。在Linux中通过变量net.ipv4.tcp_fin_timeout
来设置计时器的秒数,默认为60s。这里说的半关闭操作其实就是shutdown() 函数,net.ipv4.tcp_fin_timeout
参数无法控制调用shutdown()函数进行关闭连接的通信端,所以主动关闭的通信端调用的是shutdown()函数的话,在接收到对方的FIN之前,都会处于FIN_WAIT_2
状态
连接的主动关闭者在发出自己的FIN,并接收到对方的ACK后,就会进入FIN_WAIT_2
状态,等待对方的FIN报文。在接收到对方的FIN报文后,就会发送ACK给对方,并进入TIME_WAIT状态。这个状态下有一个时间等待计时器,其设置的时间为2MSL ,有时也称为加倍等待,TCP需要等待这个时间,才会进入CLOSED
状态
MSL, M a x i m u m S e g m e n t L i f e t i m e Maximum\ Segment\ Lifetime Maximum Segment Lifetime,最大段生命,代表任何报文段在被丢弃前在网络中被允许存在的最长时间。2MSL在Linux中是通过一个宏定义TCP_TIMEWAIT_LEN
设置的,默认是60s,想要修改的话需要修改这个参数并重新编译内核。2MSL的时间是从客户端收到FIN后发送ACK,才开始计时的,也就是说,假设在TIME_WAIT
时间内,由于客户端发送的ACK超时,导致了服务端重传FIN,那么客户端再次接收到FIN,并发送ACK,计时器将重新计时
LAST_ACK
状态的服务端就收不到客户端对自己发出的FIN的确认报文,那么它就会超时重传这个FIN报文,而客户端就能在2MSL时间内收到这个重传的FIN,然后再次发送确认报文,重新启动2MSL计时器。假如客户端不在TIME_WAIT
状态等待一段时间,而是发完最后一个ACK后立即释放连接,那么就无法收到服务端重传的FIN报文,也就不会再发送一次ACK,服务端也就不能按照正常步骤进入CLOSED
状态。也正是考虑到这种ACK丢失,服务端重传FIN的情况,客户端需要等待重传的FIN到达,一个ACK加上一个重传FIN,两个报文,所以需要2MSL作为TIME_WAIT
的时间net.ipv4.ip_local_port_range
来修改。如果发起连接方的TIME_WAIT
状态过多,端口被占满,就无法创建新的连接;而对于服务端,虽然它可能只需要监听一个端口,然后不同的客户端访问这一个端口产生不同连接,也就是说它不太会因为TCP连接过多而导致端口资源受限,但连接过多也会占用系统资源,如文件描述符,内存资源,CPU,线程等等net.ipv4.tcp_tw_reuse
参数为1,表示可以复用处于TIME_WAIT的socket供新的连接使用。前提是需要打开时间戳选项的支持net.ipv4.tcp_max_tw_buckets
参数的值,该值表示当系统中处于TIME_WAIT
状态的连接一旦超过这个数值时系统就会将后面的TIME_WAIT
连接状态重置so_linger
选项,其为linger类型:struct linger {
int l_onoff;
int l_linger;
};
当l_onff为0时,表示整个选项关闭,l_linger的值也会被忽略;当l_onff为1,且l_linger为0,那么主动关闭方调用close()函数后会立刻发送一个RST报文,所以连接会跳过四次挥手,自然也就跳过了TIME_WAIT
状态;当l_linger为非0,连接会设置一个超时时间,在这段时间内可以发送缓冲区内残留的数据,如果时间到了之前所有数据都被发送且收到确认应答,那么内核就会用正常的四次挥手来关闭连接,否则就用RST方式来关闭
每一个选项的头一个字节为”种类“,指明该选项的类型。种类值为0或1的选项仅占用1个字节。每种选项根据自己的种类确定自身的字节数len,选项字段的总长度就包括种类与len个字节。上图中的长度指的是选项字段的总长度 (包括种类)
NOP ( N o O p e r a t i o n No\ Operation No Operation)选项的目的是允许发送者在必要的时候填充某个字段,使整个TCP头部长度达到32bit的倍数
EOL ( E n d O f O p t i o n L i s t End\ Of\ Option\ List End Of Option List)选项指出了选项列表的结尾,说明无需对选项列表再处理
最大段大小 (MSS, M a x i m u m S e g m e n t S i z e Maximum\ Segment\ Size Maximum Segment Size)是指TCP协议所允许的从对方接收到的最大报文段,也即通信双方发送数据时能够使用的最大报文段,最大段大小只记录TCP数据的字节数,不包括其它相关的TCP及IP头部。其默认数值为536字节
由于接收的数据是无序的,所以接收到的数据的序列号也是不连续的,那么TCP接收方的数据队列中就会出现空洞的情况,如收到了序列号1-3,6-7的数据,那么就有一个空洞4-5。而由于TCP提供的是字节流的服务,它要保证交付给应用程序的数据是有序的,需要防止应用程序使用超出空洞的数据
如果TCP发送方能够知道接收方当前的空洞情况,他就能在报文段丢失或者被接收方遗漏时更好地进行重传工作。这就是选择确认的功能
通过接收SYN(或者SYN + ACK)中的”允许选择确认“选项,TCP通信方就知道自己能发布SACK的信息。当接收到乱序的数据时,它就能提供一个SACK选项来描述这些乱序的数据。SACK选项中包含了接收方已经成功接收的数据块的序列号范围,每个范围被称为一个SACK块,由一对32位的序列号表示。因此,一个SACK选项包含n个SACK块的话,其长度就为8n + 2字节,2个字节由于保存种类和长度。通过序列号范围就能得到空洞的范围
由于头部长度的限制,一个报文段中发送的最大SACK块数目为3(假设使用了时间戳选项,这是现代TCP实现中的典型情况)
窗口缩放选项 (WSCALE或WSOPT, W i n d o w S c a l e O p t i o n Window\ Scale\ Option Window Scale Option)能够有效地将TCP窗口广告字段的范围从16位增加至30位。选项总长度为3字节,其中一个字节表示种类,一个字节表示长度,剩下一个字节就是窗口缩放的比例因子
假设比例因子设值为s,那么窗口数值就会扩大到原先的2s倍。原本的窗口大小最大值为65535 (216 - 1),比例因子的最大值允许是14,也就是说窗口的实际最大值是65535 * 214,这个数值接近1GB。RFC 7323中提到,如果一个通信端接收到了一个大于14的字段值,他也只能使用14。
该选项只能出现于一个SYN报文段中,因此当连接建立后比例因子是与方向绑定的,要调整窗口大小的话就通过修改窗口大小字段的值来调整
为了保证窗口调整,通信双方都需要在SYN报文段中包含该选项,当然,这还是由通信端自己选择包不包含,如果双方有一方没有包含该选项,那么双方都不会启用这个选项。主动打开连接的一方发送了SYN,而被动打开的一方只有在收到的SYN报文中指出了该选项自己才能发送该选项;而如果主动打开连接的一方发送了SYN,且包含了该选项,但是没有接收到来自对方的窗口缩放选项,它会将自己发送与接收的比例因子数值都设为0
时间戳选项 (TSOPT或TSopt)的示意图如下:
时间戳选项要求发送方在每一个报文段中添加2个4字节的时间戳数值,其总长度为10字节(还有1个字节种类,1个字节长度),接收方将会在确认ACK报文中反映(回显)这些数值,允许发送方针对每一个接收到的ACK (注意不是每一个报文,因为TCP采用的是累积确认) 估算TCP连接的往返时间
发送方将一个32位的数值填充到时间戳数值字段 (TSV/TSval) 作为时间戳选项的第一个部分;接收方则将收到的时间戳数值原封不动地填充到第二部分的时间戳回显重试字段 (TSER/TSecr),然后在第一部分处填充自己的时间戳数值
估算一条TCP连接的往返时间主要是为了设置重传时间,时间戳选项使我们获得了更多的往返时间样本,从而提升了精确估算往返时间的能力
除此之外,时间戳选项也为接收者提供了避免接收旧报文段与判断报文段正确性的方法,即防回绕序列号( P r o t e c t i o n A g a i n s t W r a p p e d S e q u e n c e n u m b e r s Protection\ Against\ Wrapped\ Sequence\ numbers Protection Against Wrapped Sequence numbers,PAWS)。试想一下,一个相对高速的连接中,序列号即使有232个,也很快循环完一轮,假设当前这一轮中有某些序列号对应的数据丢失了,然后重传,但是在序列号循环的第二轮才出现,接收方只根据序列号的话是很难区分这些数据是最近发送的还是以前发送的。而有了时间戳选项。就可以判断报文段的时间戳是否小于最近接收到的报文段的时间戳,是的话防回绕序列号算法就将其丢弃
防回绕序列号算法并不要求在发送者跟接收者之间有任何形式的时钟同步,接收者所需要的是保证时间戳数值单调递增,并且每一个窗口的数据至少增加1
可以看出来,时间戳选项对于前面我们提到的“来自历史连接的报文段”也可以进行处理,判断出其是来自历史连接还是本次连接
将参数net.ipv4.tcp_timestamps
的值设为1就可以开启时间戳选项的支持。默认即为1
用户超时选项数值 ( U S E R _ T I M E O U T USER\_TIMEOUT USER_TIMEOUT) 指明了TCP发送者在确认对方未能成功接收数据之前愿意等待该数据ACK确认的时间
TCP认证选项 ( T C P A u t h e n t i c a t i o n O p t i o n TCP\ Authentication\ Option TCP Authentication Option,TCP-AO) 是用于增强连接的安全性的。它使用了一种加密散列算法以及TCP连接双方共同维护的一个秘密值来认证每一个报文段。当发送数据时,TCP会根据共享密钥生成一个通信密钥,并根据一个特殊的算法计算散列值,接收者装配有相同的密钥,同样也能够生成通信密钥,借助通信密钥接收者就可以确认到达的报文段是否在传输过程中被篡改过。由于需要创建并分发一个共享密钥,该选项并没有得到广泛使用
RFC 5925中提到,每个连接的通信密钥跟连接本身一样唯一,即一个连接(实例)只会有一个通信密钥
一个将RST控制位置位的报文段即称为重置报文段。当发现一个到达的报文段对于相关连接而言是不正确的时,TCP就会发送一个重置报文段
产生报文段的场景有以下几种:
SO_LINGER
数值设为0实现上述功能,这意味着不会在终止之前为了确定数据是否到达另一端而逗留任何时间,那么四次挥手将会被跳过,TIME_WAIT
状态也会被跳过,这也是去除TIME_WAIT
状态的一种手段TIME_WAIT
状态的通信端通常不需要做任何操作,只需要维持着当前状态直到2MSL计时结束,而如果它在这段时间内接收到来自这条连接的一些报文段,或是更加特殊的重置报文段,它就会被破坏,这种情况称为时间等待错误 ( T I M E − W A I T A s s a s s i n a t i o n TIME-WAIT\ Assassination TIME−WAIT Assassination,TWA)TIME_WAIT
状态,而服务端已处于CLOSED
状态,此时有可能客户端会接收到本次连接产生的旧的报文段 (到达时间比较晚),那么他就会发送一个ACK作为响应。然而,当服务端接收到这个报文段之后,它却没有关于这条连接的任何信息,因此它发送一个重置报文段作为响应,这不是服务端的问题,但却会使客户端过早地从TIME_WAIT
状态转移到CLOSED
状态TIME_WAIT
状态时不对重置报文段作出反应,从而避免了上述问题服务端接收到客户端的SYN请求后,内核会把连接放到半连接队列 (也称SYN队列) 中,然后向客户端响应SYN + ACK报文,等待客户端返回ACK,即第三次握手后,再把连接从半连接队列中移出,创建完整的连接添加到全连接队列 (也称accept队列) 中,等待进程调用accept()时再把连接取出来
全连接队列中连接数目的最大值为内核参数net.core.somaxconn
(默认为128) 与 listen()函数中backlog参数两者的较小值
当全连接队列已满,服务端自然就无法接受一个新的连接。系统控制变量net.ipv4.tcp_abort_on_overflow
可以设置服务端的行为:当其值为0,服务端只会忽略客户端的ACK;当其值为1,服务端还会发送一个重置报文段给客户端,默认情况下这个功能是不开启的。因为此时客户端处于的是ESTABLISHED
状态,它会尝试与服务器联系,发出请求。如果它收到了重置报文段,那它可能会认为没有服务器存在,但实际上服务器是因为繁忙而不是不存在,繁忙跟不存在完全不一样。假设服务端繁忙的状况能够在短时间内得到改善,那么它是有机会去接收刚才繁忙时没能接受的连接的 (只要客户端有在继续尝试联系,因为请求报文会携带ACK,能触发服务端继续完成连接) ,而如果直接发送了重置报文,那么客户端也会放弃主动打开的操作,就算服务端短时间内不繁忙了,也不能再来接受这个连接了。所以只有当肯定全连接队列会长期溢出时,才应该将net.ipv4.tcp_abort_on_overflow
参数设为1以尽快通知客户端不再尝试连接,节省其资源
如果半连接队列已满,不一定只能丢弃连接,在开启syncookies功能的情况下就可以不使用SYN半连接队列而完成连接的建立。半连接队列的最大数目不止与参数net.ipv4.tcp_max_syn_backlog
(默认为1000) 有关,还跟全连接队列的最大数目有关
SYN泛洪是一种TCP拒绝服务攻击,指一个或多个恶意的客户端产生一系列TCP连接尝试 (SYN报文段),并将它们发送给一台服务器,服务器会为每一条连接分配一定数量的连接资源,由于连接尚未完全建立 (服务端响应了这些SYN并发送自己的SYN + ACK报文段,处于SYN_RCVD
状态并等待对方的ACK,但恶意的客户端并不会继续响应ACK,即第三次握手),服务端为了维护大量的半打开连接会在耗尽自身内存后拒绝为后续的合法连接请求服务
一种针对这种问题的机制为SYN cookies。其主要思想是,当一个SYN到达时,这条连接存储的大部分信息都会被编码并保存在SYN + ACK报文段的序列号字段。采用SYN cookies的目标主机并不需要为进入的连接请求分配任何存储资源,只有当SYN + ACK本身被确认后 (并且已返回初始序列号)才会分配真正的内存,在这种情况下,所有重要的连接参数都能重新获得,同时连接也能够被设置为ESTABLISHED
状态。该功能通过net.ipv4.tcp_syncookies
参数设定,当其值为0时表示不开启,为1时表示仅当SYN队列已满时再开启,为2时表示无条件开启功能
Linux中,服务端在接收到一个SYN后会采用下面的方法设置初始序列号 (保存在SYN + ACK报文段中):首5位是t模32的结果,其中t是一个32位的计数器,每隔64秒增1;接着3位是对服务器最大段大小的编码值;剩余的24位保存了四元组(即连接标识)与t值的散列值,该值是根据服务器选定的散列加密算法计算得到的
在采用该方法时,服务端总是以一个SYN + ACK报文段作为响应,在接收到ACK后,如果根据其中的t值可以计算出与加密的散列值相同的结果,那么服务器才会为该SYN重新构建队列
除此之外还可以通过减少SYN + ACK的重传次数来抵御SYN泛洪,即减小参数net.ipv4.tcp_synack_retries
的值,重传次数减少了,处于SYN_RCVD
状态的连接也就可以更快被断开
TCP概述