TCP协议
TCP(Transmission Control Protocol, 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP协议特点如下:
1 2 3 4 5 |
|
1)面向链接:TCP客户端和服务器之间通信之前首先要建立一个虚拟的链接,这个链接由 源IP地址,源端口号,目的IP地址,目的端口号
唯一确定;同时TCP层需要为每一个连接维护一个状态机。
2)可靠性(弥补了IP协议尽力而为服务的不足):TCP为了保证数据正确的传到对端,引入了重传机制;TCP层发送的每一个数据包都有一个唯一的序号,对端收到后需要对收到的数据包进行确认;本端TCP层对发送的每一个TCP数据包都在缓存中保留一个副本,只有在收到对端的确认后才将其删除,否则在定时器超时后仍没收到确认就会重传;
3)有序性:因为TCP发送到每一个数据包都有一个唯一的序号,该序号是结合本次发送端字节数依次增长的,所以对端收到后很容易按照其发送顺序进行排序,然后依次交个应用层;
4)面向字节流:和UDP不同,应用层调用发送消息的接口发送一块数据时,TCP为了不让IP层发生分片,会对用户的数据块分片后分别加上TCP头部然后交给IP层,所以基于TCP的程序很难使IP层发生分片;这样一来用户的数据块就被分割了,到达对端TCP层也不会被合并,就直接交给应用层,所以对端应用层收到的可能是被分割后的数据块,而不是源端最初发送的整个数据块;如果应用层需要对源端发送的整个数据块进行处理则需要应用层自己负责将TCP送上来的分片合并起来。
5)拥塞控制:
6)流量控制(滑动窗口):
TCP报文段首部格式
TCP首部格式如下:
首部中的各个字段含义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
简单说明一下首部中最重要几个字段:
- Sequence Number是包的序号(本报文段所发送的数据的第一个字节的序号),用来解决网络包乱序(reordering)问题。
- Acknowledgement Number(期待收到对方下一个报文段的第一个数据字节的序号)就是ACK——用于确认收到,用来解决不丢包的问题。
- Window就是著名的滑动窗口(Sliding Window),用于解决流控的。
- TCP Flag ,也就是包的类型,主要是用于操控TCP的状态机的。
- 确认ACK:占1位,仅当ACK=1时,确认号字段才有效。ACK=0时,确认号无效
- 同步SYN:连接建立时用于同步序号。当SYN=1,ACK=0时表示:这是一个连接请求报文段。若同意连接,则在响应报文段中使得SYN=1,ACK=1。因此,SYN=1表示这是一个连接请求,或连接接受报文。
- 终止FIN:用来释放一个连接。FIN=1表示:此报文段的发送方的数据已经发送完毕,并要求释放运输连接。
连接管理
TCP连接的状态机如下:
下面将具体描述TCP连接和释放的各个状态。
TCP连接的建立 (三次握手)
TCP通过三次握手(three-way handshake)创建一个连接。连接过程如下:
Client端:
当Client端调用socket
函数时,相当于Client端产生了一个处于Closed状态的套接字。
- (1)第一次握手:SYN=1,seq=x
当Client端调用connect
函数时,系统为会为Client随机分配一个端口,连同传入connect中的参数(Server的IP和端口),这就形成了一个连接四元组。客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的报文1。connect调用后Client端的socket处于就处于SYN_SENT状态,等待服务器确认。
- (2)第二次握手:SYN=1,ACK=1,seq=y,ack=x+1
服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态。
- (3) 第三次握手:ACK=1,seq=x+1,ack=y+1
客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户器和客务器进入ESTABLISHED状态,完成三次握手。连接已经可以进行读写操作。
一个完整的三次握手也就是: 请求—应答—再次确认。
Server端:
- (1)调用
socket
函数时,相当于Server端产生了一个处于Closed状态的监听套接字。 - (2)调用
bind
操作,将监听套接字与指定的地址和端口关联。 - (3)调用
listen
函数,系统会为其分配未完成队列和完成队列,此时的监听套接字可以接受Client的连接,监听套接字状态处于LISTEN状态。 - (4)调用
accept
操作时,会从完成队列中取出一个已经完成的client连接,同时在server这段会产生一个会话套接字,用于和client端套接字的通信,这个会话套接字的状态是ESTABLISH。
从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了 SYN=1,seq=x
包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN 包,调用accept函数接收请求向客户端发送 SYN=1,ACK=1,seq=y,ack=x+1
,这时accept进入阻塞状态;客户端收到服务器的SYN=1,ACK=1,seq=y,ack=x+1
之后,这时connect返回,并对 ACK=1,seq=x+1,ack=y+1
进行确认;服务器收到ACK=1,seq=x+1,ack=y+1
时,accept返回,至此三次握手完毕,连接建立。
TCP连接过程状态机
- LISTENING
首先服务端需要打开一个socket进行监听,状态为LISTEN。有提供某种服务才会处于LISTENING状态,TCP状态变化就是某个端口的状态变化,提供一个服务就打开一个端口,例如:提供www服务默认开的是80端口,提供ftp服务默认的端口为21,当提供的服务没有被连接时就处于LISTENING状态。FTP服务启动后首先处于侦听(LISTENING)状态。处于侦听LISTENING状态时,该端口是开放的,等待连接,但还没有被连接。就像你房子的门已经敞开的,但还没有人进来。
看LISTENING状态最主要的是看本机开了哪些端口,这些端口都是哪个程序开的,关闭不必要的端口是保证安全的一个非常重要的方面,服务端口都对应一个服务(应用程序),停止该服务就关闭了该端口,例如要关闭21端口只要停止IIS服务中的FTP服务即可。
- SYN-SENT
当请求连接时客户端首先要发送同步信号给要访问的机器,此时状态为SYN_SENT,如果连接成功了就变为ESTABLISHED,正常情况下SYN_SENT状态非常短暂。如果发现有很多SYN_SENT出现,那一般是你要访问的网站不存在或线路不好。
- SYN-RECEIVED
当服务器收到客户端发送的同步信号时,将标志位ACK和SYN置1发送给客户端,此时服务器端处于SYN_RCVD状态,如果连接成功了就变为ESTABLISHED,正常情况下SYN_RCVD状态非常短暂。如果发现有很多SYN_RCVD状态,那你的机器有可能被SYN Flood的DoS(拒绝服务攻击)攻击了。
- ESTABLISHED
ESTABLISHED状态是表示两台机器已经建立了连接。
- linux查看tcp的状态命令
在Linux上可以通过如下命令查看机器上的各种连接的情况:
1 2 3 4 |
|
如在作者本机上执行 netstat -nat
看到如下这些TCP连接:
TCP连接建立中的几个疑惑点
- 为什么建立连接需要三次握手?
需要三次握手(还要再发送一次确认)的原因:为了防止已失效的连接请求报文段突然又传到了服务器B,导致服务器误认为客户端想请求连接而发生连接的错误。
产生错误的场景:
在两次握手的前提下,A发出连接请求,但因为丢失了,故而不能收到B的确认。于是A重新发出请求,然后收到确认,建立连接,数据传输完毕后,释放连接,A发了2个,一个丢掉,一个到达,没有“已失效的报文段”。
但是,某种情况下,A发出的第一个连接请求在某个节点滞留了,延误到达B。假设此时B已经释放连接,那么B在收到此实现的连接请求后,就误认为A又发出一次连接请求,在两次握手的情况下(A发生请求,B接受请求并确认),B就认为A又发出一次新连接请求。此时B就又给A发生一个确认,表示同意建立连接。因为是两次握手,A收到后,也不再次发出确认连接。此时B会等待A发送的数据,而A本来就没有要求发送数据,肯定也无动于衷。此时B的资源就被浪费了。
为什么三次握手能够处理这个错误场景?
采用三次握手的话,A收到B的确认后,由于A本来就不想发送数据,所以A就不发送确认,而B收不到确认,也就知道A并没有要求建立连接。
- SYN队列和ACCEPT队列
回顾一下连接的建立过程,客户端向server发送SYN包,server回复SYN+ACK,同时将这个处于SYN_RECV状态的连接保存到半连接队列。客户端返回ACK包完成三次握手,server将ESTABLISHED状态的连接移入accept队列,等待应用调用accept()。
可以看到建立连接涉及两个队列:
1 2 |
|
accept队列中的backlog是listen中指定的参数 int listen(int sockfd, int backlog)
。 如果我们设置的backlog大于net.core.somaxconn,accept队列的长度将被设置为net.core.somaxconn。
SYN队列和ACCEPT队列是内核实现的,当服务器绑定、监听了某个端口后,这个端口的SYN队列和ACCEPT队列就建立好了。客户端使用connect向服务器发起TCP连接,当图中1.1步骤客户端的SYN包到达了服务器后,内核会把这一报文放到SYN队列(即未完成握手队列)中,同时返回一个SYN+ACK包给客户端。一段时间后,在2.1步骤中客户端再次发来了针对服务器SYN包的ACK网络报文时,内核会把连接从SYN队列中取出,再把这个连接放到ACCEPT队列(即已完成握手队列)中。而服务器在第3步调用accept时,其实就是直接从ACCEPT队列中取出已经建立成功的连接套接字而已。
SYN队列和ACCEPT队列都不是无限长度的,它们的长度限制与调用listen监听某个地址端口时传递的backlog参数有关。既然队列长度是一个值,那么,队列会满吗?当然会,如果上图中第1步执行的速度大于第2步执行的速度,SYN队列就会不断增大直到队列满;如果第2步执行的速度远大于第3步执行的速度,ACCEPT队列同样会达到上限。第1、2步不是应用程序可控的,但第3步却是应用程序的行为,假设进程中调用accept获取新连接的代码段长期得不到执行,例如获取不到锁、IO阻塞等。
那么,这两个队列满了后,新的请求到达了又将发生什么?
若SYN队列满,则会直接丢弃请求,即新的SYN网络分组会被丢弃;如果ACCEPT队列满,则不会导致放弃连接,也不会把连接从SYN列队中移出,这会加剧SYN队列的增长。所以,对应用服务器来说,如果ACCEPT队列中有已经建立好的TCP连接,却没有及时的把它取出来,这样,一旦导致两个队列满了后,就会使客户端不能再建立新连接,引发严重问题。
- server端SYN超时
当client端给server端发送SYN报文时,如果server端没有返回SYN-ACK报文,那么client端会重发SYN报文给server端,重发的次数由参数 tcp_syn_retries
参数设置,这个值默认是5,超过5次重发server端还是不返回SYN-ACk报文,那么本次连接失败。server端没有返回SYN-ACK报文主要有两种情况,第一种是由于网络问题SYN报文丢失导致server端没有接收到SYN报文,另一种情况是server端SYN队列满,导致连接被丢弃。
- 客户端ACK超时
如果server端接到了clien发的SYN后回了SYN-ACK后client掉线了,server端没有收到client回来的ACK,那么,这个连接处于一个中间状态,既没成功也没失败。于是,server端如果在一定时间内没有收到client端的ACK,那么server端会重发SYN-ACK。在Linux下,默认重试次数为5次,重发的间隔时间从1s开始每次都翻番,5次的重发的时间间隔分别1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才会把断开这个连接。
- SYN Flood
一些恶意的人利用客户端ACK超时制造了 SYN Flood
攻击——给服务器发了一个SYN后,就下线了,于是服务器需要默认等63s才会断开连接,这样,攻击者就可以把服务器的syn连接的队列耗尽,让正常的连接请求不能处理。为了应对SYNflooding(即客户端只发送SYN包发起握手而不回应ACK完成连接建立,填满server端的半连接队列,让它无法处理正常的握手请求),Linux实现了一种称为SYN cookie的机制,通过 net.ipv4.tcp_syncookies
控制,设置为1表示开启。当SYN队列满了后,TCP会通过源地址端口、目标地址端口和时间戳打造出一个特别的Sequence Number发回去(又叫cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie发回来,然后服务端可以通过cookie建连接(即使你不在SYN队列中)。
一般不建议用 tcp_syncookies
来处理正常的大负载的连接的情况。因为,synccookies是妥协版的TCP协议,并不严谨。对于正常的请求,你应该调整三个TCP参数可供你选择,第一个是:tcp_synack_retries
可以用他来减少重试次数;第二个是:tcp_max_syn_backlog
,可以增大SYN连接数;第三个是:tcp_abort_on_overflow
处理不过来干脆就直接拒绝连接了。
- 握手过程做了哪些事?
从TCP抓包过程中可以看到在建立连接过程中server端和cliet端除了发送SYN、ACK,还会协商如下信息:
1 2 3 |
|
TCP连接过程参数调优
这部分简要总结一下TCP连接建立过程中的一些参数,这些参数可以根据实际应用场景进行修改:
1 2 3 4 5 6 |
|
- tcp_max_syn_backlog
1
|
|
半连接缓冲队列(SYN队列)长度。对于那些依然还未获得客户端确认的连接请求需要保存在队列中最大数目。对于超过 128Mb 内存的系统默认值是 1024 ,低于 128Mb 的则为 128。如果服务器经常出现过载﹐可以尝试增加这个数字。
- somaxconn
1
|
|
处于listen状态单个端口上面accept队列长度,用来限制监听(LISTEN)队列最大数据包的数量。web应用中listen函数的backlog默认会给我们内核参数的net.core.somaxconn限制到128,而nginx定义的NGX_LISTEN_BACKLOG默认为511,所以有必要调整这个值。对繁忙的服务器,增加该值有助于网络性能。
- tcp_synack_retries
1
|
|
连接被动打开方的确认连接的应答最大重试次数。对于一个新建连接,内核要发送多少个 SYN 连接请求才决定放弃。
- tcp_syn_retries
1
|
|
连接主动打开方的syn尝试次数。对于一个新建连接,client端要发送多少个 SYN 连接请求才决定放弃。
- tcp_syncookies
1
|
|
开启此功能,防止sync flood攻击,在将链接加入syn_backlog之前先发出syn并带上cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
- tcp_abort_on_overflow
1
|
|
如果上层accept应用无法处理过来,就对新来的connection直接rst。
TCP连接释放 (四次握手)
TCP通过4次握手释放连接,释放连接过程如下:
调用过程如下:
- 1) 当client想要关闭它与server之间的连接。client(某个应用进程)首先调用close主动关闭连接,这时TCP发送一个FIN;client端处于FIN_WAIT1状态。
- 2) 当server端接收到FIN之后,执行被动关闭。对这个FIN进行确认,返回给client ACK。当server端返回给client ACK后,client处于FIN_WAIT2状态,server处于CLOSE_WAIT状态。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
- 3) 一段时间之后,当server端检测到client端的关闭操作(read返回为0)。接收到文件结束符的server端调用close关闭它的socket。这导致server端的TCP也发送一个FIN;此时server的状态为LAST_ACK。
- 4) 当client收到来自server的FIN后。client端的套接字处于TIME_WAIT状态,它会向server端再发送一个ack确认,此时server端收到ack确认后,此套接字处于CLOSED状态。
TCP连接释放过程状态机
1 2 3 4 5 |
|
- FIN-WAIT-1:等待远程TCP连接中断请求,或先前的连接中断请求的确认
主动关闭(active close)端应用程序调用close,于是其TCP发出FIN请求主动关闭连接,之后进入FIN_WAIT1状态。如果服务器出现shutdown再重启,使用netstat-nat查看,就会看到很多FIN-WAIT-1的状态。就是因为服务器当前有很多客户端连接,直接关闭服务器后,无法接收到客户端的ACK。
- FIN-WAIT-2:从远程TCP等待连接中断请求
主动关闭端接到ACK后,就进入了FIN-WAIT-2。这就是著名的半关闭的状态了,这是在关闭连接时,客户端和服务器两次握手之后的状态。在这个状态下,应用程序还有接受数据的能力,但是已经无法发送数据,但是也有一种可能是,客户端一直处于FIN_WAIT_2状态,而服务器则一直处于WAIT_CLOSE状态,而直到应用层来决定关闭这个状态。
- CLOSE-WAIT:等待从本地用户发来的连接中断请求
被动关闭(passive close)端TCP接到FIN后,就发出ACK以回应FIN请求(它的接收也作为文件结束符传递给上层应用程序),并进入CLOSE_WAIT。
- LAST-ACK:等待原来的发向远程TCP的连接中断请求的确认
被动关闭端一段时间后,接收到文件结束符的应用程序将调用CLOSE关闭连接。这导致它的TCP也发送一个 FIN,等待对方的ACK.就进入了LAST-ACK 。
- TIME-WAIT:等待足够的时间以确保远程TCP接收到连接中断请求的确认
在主动关闭端接收到FIN后,TCP就发送ACK包,并进入TIME-WAIT状态。这个状态又叫做2MSL状态,说的是在TIME_WAIT2发送了最后一个ACK数据报以后,要进入TIME_WAIT状态,这个状态是防止最后一次握手的数据报没有传送到对方那里而准备的。这个状态在很大程度上保证了双方都可以正常结束,但是,问题也来了。
TCP连接释放过程中的几个疑惑点
- 为什么需要4次握手?
TCP使用4次握手拆除一条连接,为何需要4次呢?因为TCP是一个全双工协议,必须单独拆除每一条信道。注意,4次挥手和3次握手的意义是不同的,很多人都会问为何建立连接是3次握手,而拆除连接是4次挥手。3次握手的目的很简单,就是分配资源,初始化序列号,这时还不涉及数据传输,3次就足够做到这个了,而4次挥手的目的是终止数据传输,并回收资源,此时两个端点两个方向的序列号已经没有了任何关系,必须等待两方向都没有数据传输时才能拆除虚链路,不像初始化时那么简单,发现SYN标志就初始化一个序列号并确认SYN的序列号。因此必须单独分别在一个方向上终止该方向的数据传输。
- 为什么需要CLOSE-WAIT状态?
服务器接收到客户端关闭连接的请求,而自身还未向客户端发送关闭连接请求的这段时间。通常情况下,CLOSE_WAIT状态的持续时间应该很短,但是如果服务器有很多数据需要传输或读写时,就不能关闭连接,此时就会出现连接长时间处于CLOSE_WAIT状态的情况。
- 为什么需要TIME-WAIT状态?
为什么要这有TIME_WAIT?为什么不直接给转成CLOSED状态呢?
主要有两个原因:
1)TIME_WAIT确保有足够的时间让对端收到了ACK,如果被动关闭的那方没有收到ACK,就会触发被动端重发FIN,一来一去正好2个MSL。
为了保证A发送的最后一个ACK报文段能够到达B。即最后这个确认报文段很有可能丢失,那么B会超时重传,然后A再一次确认,同时启动2MSL计时器,如此下去。如果没有等待时间,发送完确认报文段就立即释放连接的话,B就无法重传了(连接已被释放,任何数据都不能出传了),因而也就收不到确认,就无法按照步骤进入CLOSE状态,即必须收到确认才能close。
2)有足够的时间让这个连接不会跟后面的连接混在一起。
防止“已失效的连接请求报文段”出现在连接中。经过2MSL,那些在这个连接持续的时间内,产生的所有报文段就可以都从网络中消失。即在这个连接释放的过程中会有一些无效的报文段滞留在结点,但是呢,经过2MSL这些无效报文段就肯定可以发送到目的地,不会滞留在网络中。这样的话,在下一个连接中就不会出现上一个连接遗留下来的请求报文段了。
如果没有TIME_WAIT的话,假设连接1已经断开,然而其被动方最后重发的那个FIN(或者FIN之前发送的任何TCP分段)还在网络上,然而连接2重用了连接1的所有的5元素(源IP,目的IP,TCP,源端口,目的端口),刚刚将建立好连接,连接1迟到的FIN到达了,这个FIN将以比较低但是确实可能的概率终止掉连接2。
为何说是概率比较低呢?这涉及到一个匹配问题,迟到的FIN分段的序列号必须落在连接2的一方的期望序列号范围之内。虽然这种巧合很少发生,但确实会发生,毕竟初始序列号是随机产生了。因此终止连接的主动方必须在接受了被动方且回复了ACK之后等待2*MSL时间才能进入CLOSE状态,之所以乘以2是因为这是保守的算法,最坏情况下,针对被动方的ACK在以最长路线(经历一个MSL)经过互联网马上到达被动方时丢失。
- 重用一个连接和重用一个套接字
这是根本不同的,单独重用一个套接字一般不会有任何问题,因为TCP是基于连接的。比如在服务器端出现了一个TIME_WAIT连接,那么该连接标识了一个五元素,只要客户端不使用相同的源端口,连接服务器是没有问题的,因为迟到的FIN永远不会到达这个连接。记住,一个五元素标识了一个连接,而不是一个套接字(当然,对于BSD套接字而言,服务端的accept套接字确实标识了一个连接)。
TCP释放过程参数调优
TCP释放过程中主要有如下参数:
1 2 3 4 5 6 7 8 |
|
- tcp_orphan_reties
1
|
|
探测对端已经close的次数。在丢弃TCP连接之前要进行多少次重试,默认值是7个。如果系统是负载很大的web服务器那么应该降低该值。
- tcp_max_orphans
1
|
|
系统所能处理不属于任何进程的TCP sockets最大数量。假如超过这个数量﹐那么不属于任何进程的连接会被立即reset,并同时显示警告信息。之所以要设定这个限制。纯粹为了抵御那些简单的 DoS 攻击。
- tcp_fin_timeout
1
|
|
对于本端断开的socket连接,TCP保持在FIN-WAIT-2状态的时间。对方可能会断开连接或一直不结束连接或不可预料的进程死亡。默认值为 60 秒。
- tcp_tw_recycle
1
|
|
启用 TIME_WAIT 状态SOCKET的回收。
- tcp_tw_reuse
1
|
|
表示是否允许重新应用处于TIME-WAIT状态的socket用于新的TCP连接(这个对快速重启动某些服务,而启动后提示端口已经被使用的情形非常有帮助)
- tcp_timestamps
1
|
|
tcp_tw_recycle 与 tcp_tw_reuse 都依赖此特性,这个选项是让通信双方都加上时间戳,能够避免冲突。
time_wait 时长
C++ time_wait 120(默认值) 120(建议值)
time_wait状态超时时间,超过该时间就清除该连接tcp_max_tw_buckets
1
|
|
最大tw状态的链接数,超过这个数字,tw的状态链接被直接丢弃。设置这个值为了防止攻击造成SOCKET被大量 TIME_WAIT 占用。