最新消息:

TCP连接的建立和终止

tcp admin 3443浏览 0评论

TCP连接的建立和终止

TCP连接建立过程

三次握手

TCP连接建立过程需要经过三次握手,如图所示,三次握手的具体过程如下:

  • 客户端发送SYN包,指明打算连接的服务器端口,以及初始序号ISN(SYN包占用一个序号,seq=X,SYN)
  • 服务端收到客户端SYN包,返回包含ISN的SYN包进行确认,确认序号为客户端的ISN+1(seq=Y,SYN,ACK=X+1)
  • 客户端接收到服务端的确认SYN包,返回确认包,确认序号为服务端的ISN+1(ACK=Y+1)

为什么建立连接需要经过三次握手,两次握手行不行?如果采用两次握手即服务器返回SYN包之后就表示连接已经建立成功,正常情况下没有问题,但异常情况下就会存在问题。但是如果服务端的确认SYN包丢失,此时服务端认为TCP连接已经建立成功可以进行数据交互,客户端等待超时会进行重传SYN包,此时服务端会将此包当成新建连接的SYN包而非重传的SYN包。

连接过程中异常处理
  • SYN/SYN-ACK重传:在建立连接过程中,如果客户端发送SYN包由于丢失或服务器主动丢弃一直未收到服务端的SYN-ACK回复,等待超时后客户端会进行重传,重传的次数由参数内核参数tcp_syn_retries决定(/proc/sys/net/ipv4/tcp_retries,默认为6);同样,服务端在发送SYN-ACK包后在等待客户端回复ACK超时后也会进行重传,重传次数由内核参数tcp_synack_retries决定(/proc/sys/net/ipv4/tcp_synack_retries,默认为5)。两者每次重传的时间间隔都是指数增长的(eg: 1s、2s、4s…),如果超过重传次数后还未收到回复,则客服端放弃连接,服务端断开处于中间状态连接。
  • SYN FLOOD攻击:服务端在内核中维护着一个SYN队列,队列长度由内核参数tcp_max_syn_backlog决定,当这个队列满了之后,服务端会直接丢弃客户端请求的SYN包。基于此产生了SYN FLOOD攻击,给服务端发完SYN包后就下线,服务端要重传SYN-ACK一定次数后才会断开此连接,这样会将SYN队列耗尽,让正常的请求不能得到处理。为了应对此种情况,TCP设计了syncookie功能,该功能有内核参数tcp_syncookies开关控制(默认为1,打开),SYN队列满了后,服务端会通过<src_port, dst_port,timestamp>产生一个特殊ISN回复客户端,正常连接将此SYN cookie发回来之后建立连接。这种方式不建议用,在负载大情况下可以通过调大tcp_max_syn_backlog、调小tcp_synack_retries、打开tcp_abort_on_overflow
建链中SOCKET系统调用处理过程

内核中处理TCP连接时维护着两个队列:SYN队列和ACCEPT队列,如上图所示,服务端在建立连接过程中内核的处理过程如下:

  • 客户端使用connect调用向服务端发起TCP连接,内核将此连接信息放入SYN队列,返回SYN-ACK
  • 服务端收到客户端的ACK后,将此连接从SYN队列中取出,放入ACCEPT队列
  • 服务端使用accept调用将连接从ACCEPT队列中取出

队列长度是有限制的,SYN队列长度由内核参数tcp_max_syn_backlog决定,ACCEPT队列长度可以在调用listen(backlog)通过backlog,但总最大值受到内核参数somaxconn(/proc/sys/net/core/somaxconn)限制。

若SYN队列满了,新的SYN包会被直接丢弃。若ACCEPT队列满了,建立成功的连接不会从SYN队列中移除,同时也不会拒绝新的连接,这会加剧SYN队列的增长,最终会导致SYN队列的溢出。当ACCEPT队列溢出之后,只要打开tcp_abort_on_flow内核参数(默认为0,关闭),建立连接后直接回RST,拒绝连接(可以通过/proc/net/netstat中ListenOverflows和ListenDrops查看拒绝的数目)。

抓包示例
net.ipv4.tcp_abort_on_overflow = 0
net.ipv4.tcp_max_syn_backlog = 128
net.ipv4.tcp_syn_retries = 6
net.ipv4.tcp_synack_retries = 5
net.ipv4.tcp_syncookies = 1
listen.backlog = 1 /* listen参数 */

ACCEPT队列溢出,SYN队列正常

  • 服务端一直不调用accept导致ACCEPT队列溢出,此时客户端发出SYN包请求连接,三次握手成功,此时客户端此连接状态为ESTABLISHED,而服务端连接状态为SYN_SEND
  • 此时服务端打算将连接从SYN队列移到ACCEPT队列,由于ACCEPT队列溢出移动失败,为连接建立定时器,超时后重新发送SYN-ACK,如包所示,在49/51/55/03/19时刻都重传了SYN-ACK,达到tcp_synack_retries次数,服务端断开此连接
  • 服务端上此连接已经被断开,但在客户端上此连接状态为ESTABLISHED,此时客户端给服务端发包,将会导致服务端回RST
net.ipv4.tcp_abort_on_overflow = 1

在tcp_abort_on_overflow=1情况下,当ACCEPT队列溢出,经过三次握手建立连接之后,服务端立马发送RST包断链

net.ipv4.tcp_abort_on_overflow = 0
net.ipv4.tcp_max_syn_backlog = 1
net.ipv4.tcp_syn_retries = 6
net.ipv4.tcp_synack_retries = 5
net.ipv4.tcp_syncookies = 0/1
listen.backlog = 1 /* listen参数 */

SYN队列溢出

  • 服务端一直不调用accept导致ACCEPT队列溢出,最终导致了SYN队列溢出
  • SYN队列溢出,客户端发起SYN连接请求,如果服务端开启syncookies,正常建立连接
  • 若服务端未开启syncookies,则直接丢弃客户端SYN,客户端超时重发SYN包tcp_syn_retries次后还未收到SYN-ACK包,放弃连接请求

TCP断链过程

断链的四次握手

TCP断开连接需要经过四次握手,如图所示,四次握手交互过程如下:

  • 客户端关闭连接,发送FIN包(FIN包和SYN包一样,占用一个序号,FIN seq=X+2 ACK=Y+1)
  • 服务端收到FIN包,回复ACK(ACK=X+3)
  • 服务端完成清理工作,发送FIN包关闭连接(FIN seq=Y+1 ACK=X+3)
  • 客户端对FIN包进行确认,发送ACK(ACK=Y+2),至此连接断开

TIME_WAIT状态

TCP经过四次握手断开连接后,主动关闭方进入的是TIME_WAIT状态,经过2MSL(60s)时间后再进入CLOSED状态。TIME_WAIT状态设置的原因主要有两个: 1) 保证有足够的事件确认对端收到ACK,如果ACK丢失就会触发对方重发FIN包,一个RTT正好是2MSL。 2) 保证连接上旧的数据包在网络中消逝。旧连接上的数据包由于路由器缓存等原因一直未到达对端,旧的连接关闭,然后在相同的地址和端口上又建立新连接,这时旧连接分包到达,新连接会把它当做新的数据包。而在TIME_WAIT状态停留2MSL能保证旧的数据包消逝。

处于TIME_WAIT状态连接的端口是不能被使用的,如果服务端程序主动关闭需要立马重启的,可以使用SO_REUSEADDR选项,允许处于TIME_WAIT状态连接的端口的重复绑定,但这可能有非期望数据到达,引起服务器程序混乱。

在大并发短连接的情景下,TIME_WAIT就会太多,会占用大量的系统资源,可以通过以下方法控制TIME_WAIT数量:

  • 最有效的是服务端不主动关闭连接,尽量让客户端主动关闭连接,只有主动关闭方才会进入TIME_WAIT状态
  • 设置SO_LINGER选项,在关闭时,直接发送RST,不经过TIME_WAIT直接进入CLOSED状态
  • 开启内核参数tcp_tw_recycle不用等待2MSL等待1个重传事件就释放TIME_WAIT连接,旧的数据包通过IP和时间戳去区分。但对端如果是NAT网络使用同一个IP情况下会导致连接失败
  • 开启内核参数tcp_tw_reuse重用TIME_WAIT连接
  • 调整内核参数tcp_tw_max_buckets,控制TIME_WAIT连接数量,超过时将多余的TIME_WAIT连接删除掉并记录日志,能够预防简单的DOS攻击
close/shutdown系统调用

关闭socket连接有两个系统调用,分别是close和shutdown,函数原型为

int close(int fd);
int shutdown(int sockfd, int how);

close调用只是将socket描述符的引用计数减一,在多进程共享一个socket情况下,直至引用计数变成0时,才会关闭socket连接,close进行的是全关闭,即关闭后既不能进行读也不能进行写,若对端在关闭后还发送数据,直接返回RST包,如果继续发送数据,系统会返回SIGPIPE信号给进程。

shutdown调用直接关闭socket,如果该socket是多进程共享,那么其它进程在关闭后就无法使用。shutdown通过how参数控制其关闭的方向,可以关闭任一方向(读或写或读写)。

调用close时,选项SO_LINGER将决定如何处理残留在发送缓冲区中的数据。选项参数结构体如下:

typedef struct linger { 
  u_short l_onoff;    //开关,零或者非零 
  u_short l_linger;   //优雅关闭最长时限 
} linger; 

下表为close的在不同参数下的行为

l_onoff l_linger close 行为
0 任意 默认处理是close立即返回,系统将发送缓冲区数据
非零 0 丢弃发送缓冲区中数据,立即发送RST包,不经过TIME_WAIT状态直接进入CLOSED状态
非零 非零 阻塞到超时或数据全部发送完成,超时close返回EWOULDBLOCK错误

close和shutdown在不同情况下的行为如下表所示:

函数 说明
shutdown SHUT_RD 不再接收数据,可以继续发送数据
丢弃接收缓冲区数据,再接收到数据由TCP层丢弃
发送缓冲区不受影响可
shutdown SHUT_WR 不再发送数据,可以继续接收数据
接收缓冲区不受影响
|发送缓冲数据被发送到对端后发送FIN
close l_onoff=0 不再发送和接收数据
丢弃接收缓冲区数据
发送缓冲数据被发送到对端后发送FIN
close l_onoff=1
l_linger=0
不再发送和接收数据
丢弃接收缓冲区数据
丢弃发送缓冲数据
发送RST到对端
close l_onoff=1
l_linger!=0
不再发送和接收数据
丢弃接收缓冲区数据
发送缓冲数据被发送到对端后发送FIN,若在发送完成前超时,close返回EWOULDBLOCK

TCP状态机

RST包

RST标识复位,用来异常的关闭连接。发送RST包,不必等缓冲区里面包都发送出去,直接丢弃缓冲区包,然后发送RST。接收到RST包,不用回复ACK来确认。

在以下几种情况下,会发送RST包

  • 当连接请求到达时,该端口没有进程正在监听会回复RST包
  • 异常终止一个连接,使用SO_LINGER选项在close时直接发送RST包,而不没有四次握手
  • 如果一方已经关闭或异常终止连接而对端不知道,此时对端发送数据返回RST包

参考文档

  1. 高性能网络编程1–accept建立连接
  2. 高性能网络编程4–TCP连接的关闭
  3. TCP/IP详解卷1
  4. UNIX网络编程卷1

转载请注明:爱开源 » TCP连接的建立和终止

您必须 登录 才能发表评论!