一文读懂TCP三次握手与四次挥手
TCP基本认识
TCP头格式有哪些?
我们先来看看TCP头的格式,标注颜色的表示与本文关联比较大的字段,其他字段不做详细阐述。
序列号:在建立连接时由计算机生成的随机数作为其初始值,通过SYN包传给接收端主机,每发送一次数据,就累加一次该数据字节数的大小。用来解决网络包乱序问题。
确认应答号:指下一次期望收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
控制位:ACK:该位为1时,确认应答的字段变为有效,TCP规定除了最初建立连接时的SYN包之外该位必须设置为1。RST:该位为1时,表示TCP连接中出现异常必须强制断开连接。SYN:该位为1时,表示希望建立连接,并在其序列号的字段进行序列号初始值的设定。FIN:该位为1时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换FIN位为1的TCP段。
为什么需要TCP协议?TCP工作在哪一层?
IP层是不可靠的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。
如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的TCP协议来负责。
因为TCP是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。
什么是TCP?
TCP是面向连接的、可靠的、基于字节流的传输层通信协议。
面向连接:一定是一对一才能连接,不能像UDP协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;可靠的:无论的网络链路中出现了怎样的链路变化,TCP都可以保证一个报文一定能够到达接收端;字节流:用户消息通过TCP协议传输时,消息可能会被操作系统分组成多个的TCP报文,如果接收方的程序如果不知道消息的边界,是无法读出一个有效的用户消息的。并且TCP报文是有序的,当前一个TCP报文没有收到的时候,即使它先收到了后面的TCP报文,那么也不能扔给应用层去处理,同时对重复的TCP报文会自动丢弃。
什么是TCP连接?
我们来看看RFC793是如何定义连接的:
Connections:ThereliabilityandflowcontrolmechanismsdescribedaboverequirethatTCPsinitializeandmaintaincertainstatusinformationforeachdatastream。Thecombinationofthisinformation,includingsockets,sequencenumbers,andwindowsizes,iscalledaconnection。
简单来说就是,用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。
所以我们可以知道,建立一个TCP连接是需要客户端与服务端端达成上述三个信息的共识。Socket:由IP地址和端口号组成序列号:用来解决乱序问题等窗口大小:用来做流量控制
如何唯一确定一个TCP连接呢?
TCP四元组可以唯一的确定一个连接,四元组包括如下:源地址源端口目的地址目的端口
源地址和目的地址的字段(32位)是在IP头部中,作用是通过IP协议发送报文给对方主机。
源端口和目的端口的字段(16位)是在TCP头部中,作用是告诉TCP协议应该把报文发给哪个进程。
有一个IP的服务端监听了一个端口,它的TCP的最大连接数是多少?
服务端通常固定在某个本地端口上监听,等待客户端的连接请求。
因此,客户端IP和端口是可变的,其理论值计算公式如下:
对IPv4,客户端的IP数最多为2的32次方,客户端的端口数最多为2的16次方,也就是服务端单机最大TCP连接数,约为2的48次方。
当然,服务端最大并发TCP连接数远不能达到理论上限,会受以下因素影响:文件描述符限制,每个TCP连接都是一个文件,如果文件描述符被占满了,会发生toomanyopenfiles。Linux对可打开的文件描述符的数量分别作了三个方面的限制:系统级:当前系统可打开的最大数量,通过catprocsysfsfilemax查看;用户级:指定用户可打开的最大数量,通过catetcsecuritylimits。conf查看;进程级:单个进程可打开的最大数量,通过catprocsysfsnropen查看;内存限制,每个TCP连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会发生OOM。
UDP和TCP有什么区别呢?分别的应用场景是?
UDP不提供复杂的控制机制,利用IP提供面向无连接的通信服务。
UDP协议真的非常简,头部只有8个字节(64位),UDP的头部格式如下:
目标和源端口:主要是告诉UDP协议应该把报文发给哪个进程。包长度:该字段保存了UDP首部的长度跟数据的长度之和。校验和:校验和是为了提供可靠的UDP首部和数据而设计,防止收到在网络传输中受损的UDP包。
TCP和UDP区别:
1。连接TCP是面向连接的传输层协议,传输数据前先要建立连接。UDP是不需要连接,即刻传输数据。
2。服务对象TCP是一对一的两点服务,即一条连接只有两个端点。UDP支持一对一、一对多、多对多的交互通信
3。可靠性TCP是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。UDP是尽最大努力交付,不保证可靠交付数据。但是我们可以基于UDP传输协议实现一个可靠的传输协议,比如QUIC协议,具体可以参见这篇文章:如何基于UDP协议实现可靠传输?(opensnewwindow)
4。拥塞控制、流量控制TCP有拥塞控制和流量控制机制,保证数据传输的安全性。UDP则没有,即使网络非常拥堵了,也不会影响UDP的发送速率。
5。首部开销TCP首部长度较长,会有一定的开销,首部在没有使用选项字段时是20个字节,如果使用了选项字段则会变长的。UDP首部只有8个字节,并且是固定不变的,开销较小。
6。传输方式TCP是流式传输,没有边界,但保证顺序和可靠。UDP是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
7。分片不同TCP的数据大小如果大于MSS大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装TCP数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。UDP的数据大小如果大于MTU大小,则会在IP层进行分片,目标主机收到后,在IP层组装完数据,接着再传给传输层。
TCP和UDP应用场景:
由于TCP是面向连接,能保证数据的可靠性交付,因此经常用于:FTP文件传输;HTTPHTTPS;
由于UDP面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于:包总量较少的通信,如DNS、SNMP等;视频、音频等多媒体通信;广播通信;
为什么UDP头部没有首部长度字段,而TCP头部有首部长度字段呢?
原因是TCP有可变长的选项字段,而UDP头部长度则是不会变化的,无需多一个字段去记录UDP的首部长度。
为什么UDP头部有包长度字段,而TCP头部则没有包长度字段呢?
先说说TCP是如何计算负载数据长度:
其中IP总长度和IP首部长度,在IP首部格式是已知的。TCP首部长度,则是在TCP首部格式已知的,所以就可以求得TCP数据的长度。
大家这时就奇怪了问:UDP也是基于IP层的呀,那UDP的数据长度也可以通过这个公式计算呀?为何还要有包长度呢?
这么一问,确实感觉UDP包长度是冗余的。
我查阅了很多资料,我觉得有两个比较靠谱的说法:第一种说法:因为为了网络设备硬件设计和处理方便,首部长度需要是4字节的整数倍。如果去掉UDP包长度字段,那UDP首部长度就不是4字节的整数倍了,所以我觉得这可能是为了补全UDP首部长度是4字节的整数倍,才补充了包长度字段。第二种说法:如今的UDP协议是基于IP协议发展的,而当年可能并非如此,依赖的可能是别的不提供自身报文长度或首部长度的网络层协议,因此UDP报文首部需要有长度字段以供计算。
TCP和UDP可以使用同一个端口吗?
答案:可以的。
在数据链路层中,通过MAC地址来寻找局域网中的主机。在网际层中,通过IP地址来寻找网络中互连的主机或路由器。在传输层中,需要通过端口进行寻址,来识别同一计算机中同时通信的不同应用程序。
所以,传输层的端口号的作用,是为了区分同一个主机上不同应用程序的数据包。
传输层有两个传输协议分别是TCP和UDP,在内核中是两个完全独立的软件模块。
当主机收到数据包后,可以在IP包头的协议号字段知道该数据包是TCPUDP,所以可以根据这个信息确定送给哪个模块(TCPUDP)处理,送给TCPUDP模块的报文根据端口号确定送给哪个应用程序处理。
因此,TCPUDP各自的端口号也相互独立,如TCP有一个80号端口,UDP也可以有一个80号端口,二者并不冲突。
关于端口的知识点,还是挺多可以讲的,比如还可以牵扯到这几个问题:多个TCP服务进程可以同时绑定同一个端口吗?重启TCP服务进程时,为什么会出现Addressinuse的报错信息?又该怎么避免?客户端的端口可以重复使用吗?客户端TCP连接TIMEWAIT状态过多,会导致端口资源耗尽而无法建立新的连接吗?
TCP连接建立
TCP三次握手过程是怎样的?
TCP是面向连接的协议,所以使用TCP前必须先建立连接,而建立连接是通过三次握手来进行的。三次握手的过程如下图:
一开始,客户端和服务端都处于CLOSE状态。先是服务端主动监听某个端口,处于LISTEN状态
客户端会随机初始化序号(clientisn),将此序号置于TCP首部的序号字段中,同时把SYN标志位置为1,表示SYN报文。接着把第一个SYN报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYNSENT状态。
服务端收到客户端的SYN报文后,首先服务端也随机初始化自己的序号(serverisn),将此序号填入TCP首部的序号字段中,其次把TCP首部的确认应答号字段填入clientisn1,接着把SYN和ACK标志位置为1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于SYNRCVD状态。
客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文TCP首部ACK标志位置为1,其次确认应答号字段填入serverisn1,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于ESTABLISHED状态。服务端收到客户端的应答报文后,也进入ESTABLISHED状态。
从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这也是面试常问的题。
一旦完成三次握手,双方都处于ESTABLISHED状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
如何在Linux系统中查看TCP状态?
TCP的连接状态查看,在Linux可以通过netstatnapt命令查看。
为什么是三次握手?不是两次、四次?
相信大家比较常回答的是:因为三次握手才能保证双方具有接收和发送的能力。
这回答是没问题,但这回答是片面的,并没有说出主要的原因。
在前面我们知道了什么是TCP连接:用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。
所以,重要的是为什么三次握手才可以初始化Socket、序列号和窗口大小并建立TCP连接。
接下来,以三个方面分析三次握手的原因:三次握手才可以阻止重复历史连接的初始化(主要原因)三次握手才可以同步双方的初始序列号三次握手才可以避免资源浪费
原因一:避免历史连接
我们来看看RFC793指出的TCP连接使用三次握手的首要原因:
Theprinciplereasonforthethreewayhandshakeistopreventoldduplicateconnectioninitiationsfromcausingconfusion。
简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。
我们考虑一个场景,客户端先发送了SYN(seq90)报文,然后客户端宕机了,而且这个SYN报文还被网络阻塞了,服务端并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了SYN(seq100)报文(注意!不是重传SYN,重传的SYN的序列号是一样的)。
看看三次握手是如何阻止历史连接的:
客户端连续发送多次SYN(都是同一个四元组)建立连接的报文,在网络拥堵情况下:一个旧SYN报文比最新的SYN报文早到达了服务端,那么此时服务端就会回一个SYNACK报文给客户端,此报文中的确认号是91(901)。客户端收到后,发现自己期望收到的确认号应该是1001,而不是901,于是就会回RST报文。服务端收到RST报文后,就会释放连接。后续最新的SYN抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。
上述中的旧SYN报文称为历史连接,TCP使用三次握手建立连接的最主要原因就是防止历史连接初始化了连接。
TIP
有很多人问,如果服务端在收到RST报文之前,先收到了新SYN报文,也就是服务端收到客户端报文的顺序是:旧SYN报文新SYN报文,此时会发生什么?
当服务端第一次收到SYN报文,也就是收到旧SYN报文时,就会回复SYNACK报文给客户端,此报文中的确认号是91(901)。
然后这时再收到新SYN报文时,就会回challengeack(opensnewwindow)报文给客户端,这个ack报文并不是确认收到新SYN报文的,而是上一次的ack确认号,也就是91(901)。所以客户端收到此ACK报文时,发现自己期望收到的确认号应该是101,而不是91,于是就会回RST报文。
如果是两次握手连接,就无法阻止历史连接,那为什么TCP两次握手为什么无法阻止历史连接呢?
我先直接说结论,主要是因为在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。
你想想,在两次握手的情况下,服务端在收到SYN报文后,就进入ESTABLISHED状态,意味着这时可以给对方发送数据,但是客户端此时还没有进入ESTABLISHED状态,假设这次是历史连接,客户端判断到此次连接为历史连接,那么就会回RST报文来断开连接,而服务端在第一次握手的时候就进入ESTABLISHED状态,所以它可以发送数据的,但是它并不知道这个是历史连接,它只有在收到RST报文后,才会断开连接。
可以看到,如果采用两次握手建立TCP连接的场景下,服务端在向客户端发送数据前,并没有阻止掉历史连接,导致服务端建立了一个历史连接,又白白发送了数据,妥妥地浪费了服务端的资源。
因此,要解决这种现象,最好就是在服务端发送数据前,也就是建立连接之前,要阻止掉历史连接,这样就不会造成资源浪费,而要实现这个功能,就需要三次握手。
所以,TCP使用三次握手建立连接的最主要原因是防止历史连接初始化了连接。
TIP
有人问:客户端发送三次握手(ack报文)后就可以发送数据了,而被动方此时还是synreceived状态,如果ack丢了,那客户端发的数据是不是也白白浪费了?
不是的,即使服务端还是在synreceived状态,收到了客户端发送的数据,还是可以建立连接的,并且还可以正常收到这个数据包。这是因为数据报文中是有ack标识位,也有确认号,这个确认号就是确认收到了第二次握手。如下图:
所以,服务端收到这个数据报文,是可以正常建立连接的,然后就可以正常接收这个数据包了。
原因二:同步双方初始序列号
TCP协议的通信双方,都必须维护一个序列号,序列号是可靠传输的一个关键因素,它的作用:接收方可以去除重复的数据;接收方可以根据数据包的序列号按序接收;可以标识发送出去的数据包中,哪些是已经被对方收到的(通过ACK报文中的序列号知道);
可见,序列号在TCP连接中占据着非常重要的作用,所以当客户端发送携带初始序列号的SYN报文的时候,需要服务端回一个ACK应答报文,表示客户端的SYN报文已被服务端成功接收,那当服务端发送初始序列号给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了三次握手。
而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
原因三:避免资源浪费
如果只有两次握手,当客户端发生的SYN报文在网络中阻塞,客户端没有接收到ACK报文,就会重新发送SYN,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的ACK报文,所以服务端每收到一个SYN就只能先主动建立一个连接,这会造成什么情况呢?
如果客户端发送的SYN报文在网络中阻塞了,重复发送多次SYN报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
即两次握手会造成消息滞留情况下,服务端重复接受无用的连接请求SYN报文,而造成重复分配资源。
TIP
很多人问,两次握手不是也可以根据上下文信息丢弃syn历史报文吗?
我这里两次握手是假设由于没有第三次握手,服务端不清楚客户端是否收到了自己发送的建立连接的ACK确认报文,所以每收到一个SYN就只能先主动建立一个连接这个场景。
当然你要实现成类似三次握手那样,根据上下文丢弃syn历史报文也是可以的,两次握手没有具体的实现,怎么假设都行。
小结
TCP建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
不使用两次握手和四次握手的原因:两次握手:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;四次握手:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
为什么每次建立TCP连接时,初始化的序列号都要求不一样呢?
主要原因有两个方面:为了防止历史报文被下一个相同四元组的连接接收(主要方面);为了安全性,防止黑客伪造的相同序列号的TCP报文被对方接收;
接下来,详细说说第一点。
假设每次建立连接,客户端和服务端的初始化序列号都是从0开始:
过程如下:客户端和服务端建立一个TCP连接,在客户端发送数据包被网络阻塞了,然后超时重传了这个数据包,而此时服务端设备断电重启了,之前与客户端建立的连接就消失了,于是在收到客户端的数据包的时候就会发送RST报文。紧接着,客户端又与服务端建立了与上一个连接相同四元组的连接;在新连接建立完成后,上一个连接中被网络阻塞的数据包正好抵达了服务端,刚好该数据包的序列号正好是在服务端的接收窗口内,所以该数据包会被服务端正常接收,就会造成数据错乱。
可以看到,如果每次建立连接,客户端和服务端的初始化序列号都是一样的话,很容易出现历史报文被下一个相同四元组的连接接收的问题。
如果每次建立连接客户端和服务端的初始化序列号都不一样,就有大概率因为历史报文的序列号不在对方接收窗口,从而很大程度上避免了历史报文,比如下图:
相反,如果每次建立连接客户端和服务端的初始化序列号都一样,就有大概率遇到历史报文的序列号刚好在对方的接收窗口内,从而导致历史报文被新连接成功接收。
所以,每次初始化序列号不一样很大程度上能够避免历史报文被下一个相同四元组的连接接收,注意是很大程度上,并不是完全避免了(因为序列号会有回绕的问题,所以需要用时间戳的机制来判断历史报文。
初始序列号ISN是如何随机产生的?
起始ISN是基于时钟的,每4微秒1,转一圈要4。55个小时。
RFC793提到初始化序列号ISN随机生成算法:ISNMF(localhost,localport,remotehost,remoteport)。M是一个计时器,这个计时器每隔4微秒加1。F是一个Hash算法,根据源IP、目的IP、源端口、目的端口生成一个随机数值。要保证Hash算法不能被外部轻易推算得出,用MD5算法是一个比较好的选择。
可以看到,随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。
既然IP层会分片,为什么TCP层还需要MSS呢?
我们先来认识下MTU和MSS
MTU:一个网络包的最大长度,以太网中一般为1500字节;MSS:除去IP和TCP头部之后,一个网络包所能容纳的TCP数据的最大长度;
如果在TCP的整个报文(头部数据)交给IP层进行分片,会有什么异常呢?
当IP层有一个超过MTU大小的数据(TCP头部TCP数据)要发送,那么IP层就要进行分片,把数据分片成若干片,保证每一个分片都小于MTU。把一份IP数据报进行分片以后,由目标主机的IP层来进行重新组装后,再交给上一层TCP传输层。
这看起来井然有序,但这存在隐患的,那么当如果一个IP分片丢失,整个IP报文的所有分片都得重传。
因为IP层本身没有超时重传机制,它由传输层的TCP来负责超时和重传。
当某一个IP分片丢失后,接收方的IP层就无法组装成一个完整的TCP报文(头部数据),也就无法将数据报文送到TCP层,所以接收方不会响应ACK给发送方,因为发送方迟迟收不到ACK确认报文,所以会触发超时重传,就会重发整个TCP报文(头部数据)。
因此,可以得知由IP层进行分片传输,是非常没有效率的。
所以,为了达到最佳的传输效能TCP协议在建立连接的时候通常要协商双方的MSS值,当TCP层发现数据超过MSS时,则就先会进行分片,当然由它形成的IP包的长度也就不会大于MTU,自然也就不用IP分片了。
经过TCP层分片后,如果一个TCP分片丢失后,进行重发时也是以MSS为单位,而不用重传所有的分片,大大增加了重传的效率。
第一次握手丢失了,会发生什么?
当客户端想和服务端建立TCP连接的时候,首先第一个发的就是SYN报文,然后进入到SYNSENT状态。
在这之后,如果客户端迟迟收不到服务端的SYNACK报文(第二次握手),就会触发超时重传机制,重传SYN报文,而且重传的SYN报文的序列号都是一样的。
不同版本的操作系统可能超时时间不同,有的1秒的,也有3秒的,这个超时时间是写死在内核里的,如果想要更改则需要重新编译内核,比较麻烦。
当客户端在1秒后没收到服务端的SYNACK报文后,客户端就会重发SYN报文,那到底重发几次呢?
在Linux里,客户端的SYN报文最大重传次数由tcpsynretries内核参数控制,这个参数是可以自定义的,默认值一般是5。catprocsysnetipv4tcpsynretries5
通常,第一次超时重传是在1秒后,第二次超时重传是在2秒,第三次超时重传是在4秒后,第四次超时重传是在8秒后,第五次是在超时重传16秒后。没错,每次超时的时间是上一次的2倍。
当第五次超时重传后,会继续等待32秒,如果服务端仍然没有回应ACK,客户端就不再发送SYN包,然后断开TCP连接。
所以,总耗时是1248163263秒,大约1分钟左右。
举个例子,假设tcpsynretries参数值为3,那么当客户端的SYN报文一直在网络中丢失时,会发生下图的过程:
具体过程:当客户端超时重传3次SYN报文后,由于tcpsynretries为3,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的2倍),如果还是没能收到服务端的第二次握手(SYNACK报文),那么客户端就会断开连接。
第二次握手丢失了,会发生什么?
当服务端收到客户端的第一次握手后,就会回SYNACK报文给客户端,这个就是第二次握手,此时服务端会进入SYNRCVD状态。
第二次握手的SYNACK报文其实有两个目的:第二次握手里的ACK,是对第一次握手的确认报文;第二次握手里的SYN,是服务端发起建立TCP连接的报文;
所以,如果第二次握手丢了,就会发生比较有意思的事情,具体会怎么样呢?
因为第二次握手报文里是包含对客户端的第一次握手的ACK确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的SYN报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传SYN报文。
然后,因为第二次握手中包含服务端的SYN报文,所以当客户端收到后,需要给服务端发送ACK确认报文(第三次握手),服务端才会认为该SYN报文被客户端收到了。
那么,如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传SYNACK报文。
在Linux下,SYNACK报文的最大重传次数由tcpsynackretries内核参数决定,默认值是5。catprocsysnetipv4tcpsynackretries5
因此,当第二次握手丢失了,客户端和服务端都会重传:客户端会重传SYN报文,也就是第一次握手,最大重传次数由tcpsynretries内核参数决定;服务端会重传SYNACK报文,也就是第二次握手,最大重传次数由tcpsynackretries内核参数决定。
举个例子,假设tcpsynretries参数值为1,tcpsynackretries参数值为2,那么当第二次握手一直丢失时,发生的过程如下图:
具体过程:当客户端超时重传1次SYN报文后,由于tcpsynretries为1,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的2倍),如果还是没能收到服务端的第二次握手(SYNACK报文),那么客户端就会断开连接。当服务端超时重传2次SYNACK报文后,由于tcpsynackretries为2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的2倍),如果还是没能收到客户端的第三次握手(ACK报文),那么服务端就会断开连接。
第三次握手丢失了,会发生什么?
客户端收到服务端的SYNACK报文后,就会给服务端回一个ACK报文,也就是第三次握手,此时客户端状态进入到ESTABLISH状态。
因为这个第三次握手的ACK是对第二次握手的SYN的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传SYNACK报文,直到收到第三次握手,或者达到最大重传次数。
注意,ACK报文是不会有重传的,当ACK丢失了,就由对方重传对应的报文。
举个例子,假设tcpsynackretries参数值为2,那么当第三次握手一直丢失时,发生的过程如下图:
具体过程:当服务端超时重传2次SYNACK报文后,由于tcpsynackretries为2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的2倍),如果还是没能收到客户端的第三次握手(ACK报文),那么服务端就会断开连接。
什么是SYN攻击?如何避免SYN攻击?
我们都知道TCP连接建立是需要三次握手,假设攻击者短时间伪造不同IP地址的SYN报文,服务端每接收到一个SYN报文,就进入SYNRCVD状态,但服务端发送出去的ACKSYN报文,无法得到未知IP主机的ACK应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。
先跟大家说一下,什么是TCP半连接和全连接队列。
在TCP三次握手的时候,Linux内核会维护两个队列,分别是:半连接队列,也称SYN队列;全连接队列,也称accept队列;
我们先来看下Linux内核的SYN队列(半连接队列)与Accpet队列(全连接队列)是如何工作的?
正常流程:当服务端接收到客户端的SYN报文时,会创建一个半连接的对象,然后将其加入到内核的SYN队列;接着发送SYNACK给客户端,等待客户端回应ACK报文;服务端接收到ACK报文后,从SYN队列取出一个半连接对象,然后创建一个新的连接对象放入到Accept队列;应用通过调用accpet()socket接口,从Accept队列取出连接对象。
不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,默认情况都会丢弃报文。
SYN攻击方式最直接的表现就会把TCP半连接队列打满,这样当TCP半连接队列满了,后续再在收到SYN报文就会丢弃,导致客户端无法和服务端建立连接。
避免SYN攻击方式,可以有以下四种方法:调大netdevmaxbacklog;增大TCP半连接队列;开启tcpsyncookies;减少SYNACK重传次数
方式一:调大netdevmaxbacklog
当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数,默认值是1000,我们要适当调大该参数的值,比如设置为10000:net。core。netdevmaxbacklog10000
方式二:增大TCP半连接队列
增大TCP半连接队列,要同时增大下面这三个参数:增大net。ipv4。tcpmaxsynbacklog增大listen()函数中的backlog增大net。core。somaxconn
具体为什么是三个参数决定TCP半连接队列的大小,可以看这篇:可以看这篇:TCP半连接队列和全连接队列满了会发生什么?又该如何应对?(opensnewwindow)
方式三:开启net。ipv4。tcpsyncookies
开启syncookies功能就可以在不使用SYN半连接队列的情况下成功建立连接,相当于绕过了SYN半连接来建立连接。
具体过程:当SYN队列满之后,后续服务端收到SYN包,不会丢弃,而是根据算法,计算出一个cookie值;将cookie值放到第二次握手报文的序列号里,然后服务端回第二次握手给客户端;服务端接收到客户端的应答报文时,服务端会检查这个ACK包的合法性。如果合法,将该连接对象放入到Accept队列。最后应用程序通过调用accpet()接口,从Accept队列取出的连接。
可以看到,当开启了tcpsyncookies了,即使受到SYN攻击而导致SYN队列满时,也能保证正常的连接成功建立。
net。ipv4。tcpsyncookies参数主要有以下三个值:0值,表示关闭该功能;1值,表示仅当SYN半连接队列放不下时,再启用它;2值,表示无条件开启功能;
那么在应对SYN攻击时,只需要设置为1即可。echo1procsysnetipv4tcpsyncookies
方式四:减少SYNACK重传次数
当服务端受到SYN攻击时,就会有大量处于SYNREVC状态的TCP连接,处于这个状态的TCP会重传SYNACK,当重传超过次数达到上限后,就会断开连接。
那么针对SYN攻击的场景,我们可以减少SYNACK的重传次数,以加快处于SYNREVC状态的TCP连接断开。
SYNACK报文的最大重传次数由tcpsynackretries内核参数决定(默认值是5次),比如将tcpsynackretries减少到2次:echo2procsysnetipv4tcpsynackretries
TCP连接断开
TCP四次挥手过程是怎样的?
天下没有不散的宴席,对于TCP连接也是这样,TCP断开连接是通过四次挥手方式。
双方都可以主动断开连接,断开连接后主机中的资源将被释放,四次挥手的过程如下图:
客户端打算关闭连接,此时会发送一个TCP首部FIN标志位被置为1的报文,也即FIN报文,之后客户端进入FINWAIT1状态。服务端收到该报文后,就向客户端发送ACK应答报文,接着服务端进入CLOSEWAIT状态。客户端收到服务端的ACK应答报文后,之后进入FINWAIT2状态。等待服务端处理完数据后,也向客户端发送FIN报文,之后服务端进入LASTACK状态。客户端收到服务端的FIN报文后,回一个ACK应答报文,之后进入TIMEWAIT状态服务端收到了ACK应答报文后,就进入了CLOSE状态,至此服务端已经完成连接的关闭。客户端在经过2MSL一段时间后,自动进入CLOSE状态,至此客户端也完成连接的关闭。
你可以看到,每个方向都需要一个FIN和一个ACK,因此通常被称为四次挥手。
这里一点需要注意是:主动关闭连接的,才有TIMEWAIT状态。
为什么挥手需要四次?
再来回顾下四次挥手双方发FIN包的过程,就能理解为什么需要四次了。关闭连接时,客户端向服务端发送FIN时,仅仅表示客户端不再发送数据了但是还能接收数据。服务端收到客户端的FIN报文时,先回一个ACK应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送FIN报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的ACK和FIN一般都会分开发送,因此是需要四次挥手。
但是在特定情况下,四次挥手是可以变成三次挥手的,具体情况可以看这篇:TCP四次挥手,可以变成三次吗?(opensnewwindow)
第一次挥手丢失了,会发生什么?
当客户端(主动关闭方)调用close函数后,就会向服务端发送FIN报文,试图与服务端断开连接,此时客户端的连接进入到FINWAIT1状态。
正常情况下,如果能及时收到服务端(被动关闭方)的ACK,则会很快变为FINWAIT2状态。
如果第一次挥手丢失了,那么客户端迟迟收不到被动方的ACK的话,也就会触发超时重传机制,重传FIN报文,重发次数由tcporphanretries参数控制。
当客户端重传FIN报文的次数超过tcporphanretries后,就不再发送FIN报文,则会在等待一段时间(时间为上一次超时时间的2倍),如果还是没能收到第二次挥手,那么直接进入到close状态。
举个例子,假设tcporphanretries参数值为3,当第一次挥手一直丢失时,发生的过程如下图:
具体过程:当客户端超时重传3次FIN报文后,由于tcporphanretries为3,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的2倍),如果还是没能收到服务端的第二次挥手(ACK报文),那么客户端就会断开连接。
第二次挥手丢失了,会发生什么?
当服务端收到客户端的第一次挥手后,就会先回一个ACK确认报文,此时服务端的连接进入到CLOSEWAIT状态。
在前面我们也提了,ACK报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传FIN报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。
举个例子,假设tcporphanretries参数值为2,当第二次挥手一直丢失时,发生的过程如下图:
具体过程:当客户端超时重传2次FIN报文后,由于tcporphanretries为2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的2倍),如果还是没能收到服务端的第二次挥手(ACK报文),那么客户端就会断开连接。
这里提一下,当客户端收到第二次挥手,也就是收到服务端发送的ACK报文后,客户端就会处于FINWAIT2状态,在这个状态需要等服务端发送第三次挥手,也就是服务端的FIN报文。
对于close函数关闭的连接,由于无法再发送和接收数据,所以FINWAIT2状态不可以持续太久,而tcpfintimeout控制了这个状态下连接的持续时长,默认值是60秒。
这意味着对于调用close关闭的连接,如果在60秒后还没有收到FIN报文,客户端(主动关闭方)的连接就会直接关闭,如下图:
但是注意,如果主动关闭方使用shutdown函数关闭连接,指定了只关闭发送方向,而接收方向并没有关闭,那么意味着主动关闭方还是可以接收数据的。
此时,如果主动关闭方一直没收到第三次挥手,那么主动关闭方的连接将会一直处于FINWAIT2状态(tcpfintimeout无法控制shutdown关闭的连接)。如下图:
第三次挥手丢失了,会发生什么?
当服务端(被动关闭方)收到客户端(主动关闭方)的FIN报文后,内核会自动回复ACK,同时连接处于CLOSEWAIT状态,顾名思义,它表示等待应用进程调用close函数关闭连接。
此时,内核是没有权利替代进程关闭连接,必须由进程主动调用close函数来触发服务端发送FIN报文。
服务端处于CLOSEWAIT状态时,调用了close函数,内核就会发出FIN报文,同时连接进入LASTACK状态,等待客户端返回ACK来确认连接关闭。
如果迟迟收不到这个ACK,服务端就会重发FIN报文,重发次数仍然由tcporphanretries参数控制,这与客户端重发FIN报文的重传次数控制方式是一样的。
举个例子,假设tcporphanretries3,当第三次挥手一直丢失时,发生的过程如下图:
具体过程:当服务端重传第三次挥手报文的次数达到了3次后,由于tcporphanretries为3,达到了重传最大次数,于是再等待一段时间(时间为上一次超时时间的2倍),如果还是没能收到客户端的第四次挥手(ACK报文),那么服务端就会断开连接。客户端因为是通过close函数关闭连接的,处于FINWAIT2状态是有时长限制的,如果tcpfintimeout时间内还是没能收到服务端的第三次挥手(FIN报文),那么客户端就会断开连接。
第四次挥手丢失了,会发生什么?
当客户端收到服务端的第三次挥手的FIN报文后,就会回ACK报文,也就是第四次挥手,此时客户端连接进入TIMEWAIT状态。
在Linux系统,TIMEWAIT状态会持续2MSL后才会进入关闭状态。
然后,服务端(被动关闭方)没有收到ACK报文前,还是处于LASTACK状态。
如果第四次挥手的ACK报文没有到达服务端,服务端就会重发FIN报文,重发次数仍然由前面介绍过的tcporphanretries参数控制。
举个例子,假设tcporphanretries为2,当第四次挥手一直丢失时,发生的过程如下:
具体过程:当服务端重传第三次挥手报文达到2时,由于tcporphanretries为2,达到了最大重传次数,于是再等待一段时间(时间为上一次超时时间的2倍),如果还是没能收到客户端的第四次挥手(ACK报文),那么服务端就会断开连接。客户端在收到第三次挥手后,就会进入TIMEWAIT状态,开启时长为2MSL的定时器,如果途中再次收到第三次挥手(FIN报文)后,就会重置定时器,当等待2MSL时长后,客户端就会断开连接。
为什么TIMEWAIT等待的时间是2MSL?
MSL是MaximumSegmentLifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为TCP报文基于是IP协议的,而IP头中有一个TTL字段,是IP数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。
MSL与TTL的区别:MSL的单位是时间,而TTL是经过路由跳数。所以MSL应该要大于等于TTL消耗为0的时间,以确保报文已被自然消亡。
TTL的值一般是64,Linux将MSL设置为30秒,意味着Linux认为数据报文经过64个路由器的时间不会超过30秒,如果超过了,就认为报文已经消失在网络中了。
TIMEWAIT等待2倍的MSL,比较合理的解释是:网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待2倍的时间。
比如,如果被动关闭方没有收到断开连接的最后的ACK报文,就会触发超时重发FIN报文,另一方接收到FIN后,会重发ACK给被动关闭方,一来一去正好2个MSL。
可以看到2MSL时长这其实是相当于至少允许报文丢失一次。比如,若ACK在一个MSL内丢失,这样被动方重发的FIN会在第2个MSL内到达,TIMEWAIT状态的连接可以应对。
为什么不是4或者8MSL的时长呢?你可以想象一个丢包率达到百分之一的糟糕网络,连续两次丢包的概率只有万分之一,这个概率实在是太小了,忽略它比解决它更具性价比。
2MSL的时间是从客户端接收到FIN后发送ACK开始计时的。如果在TIMEWAIT时间内,因为客户端的ACK没有传输到服务端,客户端又接收到了服务端重发的FIN报文,那么2MSL时间将重新计时。
在Linux系统里2MSL默认是60秒,那么一个MSL也就是30秒。Linux系统停留在TIMEWAIT的时间为固定的60秒。
其定义在Linux内核代码里的名称为TCPTIMEWAITLEN:defineTCPTIMEWAITLEN(60HZ)howlongtowaittodestroyTIMEWAITstate,about60seconds
如果要修改TIMEWAIT的时间长度,只能修改Linux内核代码里TCPTIMEWAITLEN的值,并重新编译Linux内核。
为什么需要TIMEWAIT状态?
主动发起关闭连接的一方,才会有TIMEWAIT状态。
需要TIMEWAIT状态,主要是两个原因:防止历史连接中的数据,被后面相同四元组的连接错误的接收;保证被动关闭连接的一方,能被正确的关闭;
原因一:防止历史连接中的数据,被后面相同四元组的连接错误的接收
为了能更好的理解这个原因,我们先来了解序列号(SEQ)和初始序列号(ISN)。序列号,是TCP一个头部字段,标识了TCP发送端到TCP接收端的数据流的一个字节,因为TCP是面向字节流的可靠协议,为了保证消息的顺序性和可靠性,TCP为每个传输方向上的每个字节都赋予了一个编号,以便于传输成功后确认、丢失后重传以及在接收端保证不会乱序。序列号是一个32位的无符号数,因此在到达4G之后再循环回到0。初始序列号,在TCP建立连接的时候,客户端和服务端都会各自生成一个初始序列号,它是基于时钟生成的一个随机数,来保证每个连接都拥有不同的初始序列号。初始化序列号可被视为一个32位的计数器,该计数器的数值每4微秒加1,循环一次需要4。55小时。
给大家抓了一个包,下图中的Seq就是序列号,其中红色框住的分别是客户端和服务端各自生成的初始序列号。
通过前面我们知道,序列号和初始化序列号并不是无限递增的,会发生回绕为初始值的情况,这意味着无法根据序列号来判断新老数据。
假设TIMEWAIT没有等待时间或时间过短,被延迟的数据包抵达后会发生什么呢?
如上图:服务端在关闭连接之前发送的SEQ301报文,被网络延迟了。接着,服务端以相同的四元组重新打开了新连接,前面被延迟的SEQ301这时抵达了客户端,而且该数据报文的序列号刚好在客户端接收窗口内,因此客户端会正常接收这个数据报文,但是这个数据报文是上一个连接残留下来的,这样就产生数据错乱等严重的问题。
为了防止历史连接中的数据,被后面相同四元组的连接错误的接收,因此TCP设计了TIMEWAIT状态,状态会持续2MSL时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
原因二:保证被动关闭连接的一方,能被正确的关闭
在RFC793指出TIMEWAIT另一个重要的作用是:
TIMEWAITrepresentswaitingforenoughtimetopasstobesuretheremoteTCPreceivedtheacknowledgmentofitsconnectionterminationrequest。
也就是说,TIMEWAIT作用是等待足够的时间以确保最后的ACK能让被动关闭方接收,从而帮助其正常关闭。
如果客户端(主动关闭方)最后一次ACK报文(第四次挥手)在网络中丢失了,那么按照TCP可靠性原则,服务端(被动关闭方)会重发FIN报文。
假设客户端没有TIMEWAIT状态,而是在发完最后一次回ACK报文就直接进入CLOSE状态,如果该ACK报文丢失了,服务端则重传的FIN报文,而这时客户端已经进入到关闭状态了,在收到服务端重传的FIN报文后,就会回RST报文。
服务端收到这个RST并将其解释为一个错误(Connectionresetbypeer),这对于一个可靠的协议来说不是一个优雅的终止方式。
为了防止这种情况出现,客户端必须等待足够长的时间,确保服务端能够收到ACK,如果服务端没有收到ACK,那么就会触发TCP重传机制,服务端会重新发送一个FIN,这样一去一来刚好两个MSL的时间。
客户端在收到服务端重传的FIN报文时,TIMEWAIT状态的等待时间,会重置回2MSL。
TIMEWAIT过多有什么危害?
过多的TIMEWAIT状态主要的危害有两种:第一是占用系统资源,比如文件描述符、内存资源、CPU资源、线程资源等;第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为32768~61000,也可以通过net。ipv4。iplocalportrange参数指定范围。
客户端和服务端TIMEWAIT过多,造成的影响是不同的。
如果客户端(主动发起关闭连接方)的TIMEWAIT状态过多,占满了所有端口资源,那么就无法对目的IP目的PORT都一样的服务端发起连接了,但是被使用的端口,还是可以继续对另外一个服务端发起连接的。具体可以看我这篇文章:客户端的端口可以重复使用吗?(opensnewwindow)
因此,客户端(发起连接方)都是和目的IP目的PORT都一样的服务端建立连接的话,当客户端的TIMEWAIT状态连接过多的话,就会受端口资源限制,如果占满了所有端口资源,那么就无法再跟目的IP目的PORT都一样的服务端建立连接了。
不过,即使是在这种场景下,只要连接的是不同的服务端,端口是可以重复使用的,所以客户端还是可以向其他服务端发起连接的,这是因为内核在定位一个连接的时候,是通过四元组(源IP、源端口、目的IP、目的端口)信息来定位的,并不会因为客户端的端口一样,而导致连接冲突。
如果服务端(主动发起关闭连接方)的TIMEWAIT状态过多,并不会导致端口资源受限,因为服务端只监听一个端口,而且由于一个四元组唯一确定一个TCP连接,因此理论上服务端可以建立很多连接,但是TCP连接过多,会占用系统资源,比如文件描述符、内存资源、CPU资源、线程资源等。
如何优化TIMEWAIT?
这里给出优化TIMEWAIT的几个方式,都是有利有弊:打开net。ipv4。tcptwreuse和net。ipv4。tcptimestamps选项;net。ipv4。tcpmaxtwbuckets程序中使用SOLINGER,应用强制使用RST关闭。
方式一:net。ipv4。tcptwreuse和tcptimestamps
如下的Linux内核参数开启后,则可以复用处于TIMEWAIT的socket为新的连接所用。
有一点需要注意的是,tcptwreuse功能只能用客户端(连接发起方),因为开启了该功能,在调用connect()函数时,内核会随机找一个timewait状态超过1秒的连接给新的连接复用。net。ipv4。tcptwreuse1
使用这个选项,还有一个前提,需要打开对TCP时间戳的支持,即net。ipv4。tcptimestamps1(默认即为1)
这个时间戳的字段是在TCP头部的选项里,它由一共8个字节表示时间戳,其中第一个4字节字段用来保存发送该数据包的时间,第二个4字节字段用来保存最近一次接收对方发送到达数据的时间。
由于引入了时间戳,我们在前面提到的2MSL问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。
方式二:net。ipv4。tcpmaxtwbuckets
这个值默认为18000,当系统中处于TIMEWAIT的连接一旦超过这个值时,系统就会将后面的TIMEWAIT连接状态重置,这个方法比较暴力。
方式三:程序中使用SOLINGER
我们可以通过设置socket选项,来设置调用close关闭连接行为。structlingersolinger;solinger。lonoff1;solinger。llinger0;setsockopt(s,SOLSOCKET,SOLINGER,solinger,sizeof(solinger));
如果lonoff为非0,且llinger值为0,那么调用close后,会立该发送一个RST标志给对端,该TCP连接将跳过四次挥手,也就跳过了TIMEWAIT状态,直接关闭。
但这为跨越TIMEWAIT状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。
前面介绍的方法都是试图越过TIMEWAIT状态的,这样其实不太好。虽然TIMEWAIT状态持续的时间是有一点长,显得很不友好,但是它被设计来就是用来避免发生乱七八糟的事情。
《UNIX网络编程》一书中却说道:TIMEWAIT是我们的朋友,它是有助于我们的,不要试图避免这个状态,而是应该弄清楚它。
如果服务端要避免过多的TIMEWAIT状态的连接,就永远不要主动断开连接,让客户端去断开,由分布在各处的客户端去承受TIMEWAIT。
服务器出现大量TIMEWAIT状态的原因有哪些?
首先要知道TIMEWAIT状态是主动关闭连接方才会出现的状态,所以如果服务器出现大量的TIMEWAIT状态的TCP连接,就是说明服务器主动断开了很多TCP连接。
服务器主动断开了连接的场景有下面这些:在使用HTTP1。1长连接的时候,当发起HTTP请求的客户端数量超过服务器指定的最大长连接个数(比如nginx配置中的keepaliverequests参数),那么服务器会主动断开这个连接,此时服务器上就会出现大量的TIMEWAIT状态。解决方式:调大最大长连接个数。如果服务器是反向代理服务器,nginx(在服务器上跑)与后端进行大量的短连接请求,由于nginx会主动挂断这个连接,在服务器上就会出现大量的TIMEWAIT状态。解决方式:使用长连接HTTP1。1长连接的时候,客户端在超时时间内没有新的数据发送,那么服务器会主动挂断这个连接,在服务器上就会出现TIMEWAIT状态。这种情况一般不会出现大量的TIMEWAIT状态,因为很难出现同一时间出现大量客户端一直不发送新的数据。如果出现了,可以往网络问题的方向排查,比如是否是因为网络问题,导致客户端发送的数据一直没有被服务器接收到。服务器的进程挂掉了,内核会发起FIN报文,这时候相当于服务器会主动挂断这个连接,于是在服务器上就会出现大量的TIMEWAIT状态。
如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP有一个机制是保活机制。这个机制的原理是这样的:
定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的TCP连接已经死亡,系统内核将错误信息通知给上层应用程序。
在Linux内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:net。ipv4。tcpkeepalivetime7200net。ipv4。tcpkeepaliveintvl75net。ipv4。tcpkeepaliveprobes9tcpkeepalivetime7200:表示保活时间是7200秒(2小时),也就2小时内如果没有任何连接相关的活动,则会启动保活机制tcpkeepaliveintvl75:表示每次检测间隔75秒;tcpkeepaliveprobes9:表示检测9次无响应,认为对方是不可达的,从而中断本次的连接。
也就是说在Linux系统中,最少需要经过2小时11分15秒才可以发现一个死亡连接。
注意,应用程序若想使用TCP保活机制需要通过socket接口设置SOKEEPALIVE选项才能够生效,如果没有设置,那么就无法使用TCP保活机制。
如果开启了TCP保活,需要考虑以下几种情况:第一种,对端程序是正常工作的。当TCP保活的探测报文发送给对端,对端会正常响应,这样TCP保活时间会被重置,等待下一个TCP保活时间的到来。第二种,对端程序崩溃并重启。当TCP保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个RST报文,这样很快就会发现TCP连接已经被重置。第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当TCP保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP会报告该TCP连接已经死亡。
TCP保活的这个机制检测的时间是有点长,我们可以自己在应用层实现一个心跳机制。
比如,web服务软件一般都会提供keepalivetimeout参数,用来指定HTTP长连接的超时时间。如果设置了HTTP长连接的超时时间是60秒,web服务软件就会启动一个定时器,如果客户端在完成一个HTTP请求后,在60秒内都没有再发起新的请求,定时器的时间一到,就会触发回调函数来释放该连接。
如果已经建立了连接,但是服务端的进程崩溃会发生什么?
TCP的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有TCP连接资源,于是内核会发送第一次挥手FIN报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成TCP四次挥手的过程。
我自己做了个实验,使用kill9来模拟进程崩溃的情况,发现在kill掉进程后,服务端会发送FIN报文,与客户端进行四次挥手。
Socket编程
针对TCP应该如何Socket编程?
服务端和客户端初始化socket,得到文件描述符;服务端调用bind,将socket绑定在指定的IP地址和端口;服务端调用listen,进行监听;服务端调用accept,等待客户端连接;客户端调用connect,向服务端端的地址和端口发起连接请求;服务端accept返回用于传输的socket的文件描述符;客户端调用write写入数据;服务端调用read读取数据;客户端断开连接时,会调用close,那么服务端read读取数据的时候,就会读取到了EOF,待处理完数据后,服务端调用close,表示连接关闭。
这里需要注意的是,服务端调用accept时,连接成功了会返回一个已完成连接的socket,后续用来传输数据。
所以,监听的socket和真正用来传送数据的socket,是两个socket,一个叫作监听socket,一个叫作已完成连接socket。
成功连接建立之后,双方开始通过read和write函数来读写数据,就像往一个文件流里面写东西一样。
listen时候参数backlog的意义?
Linux内核中会维护两个队列:半连接队列(SYN队列):接收到一个SYN建立连接请求,处于SYNRCVD状态;全连接队列(Accpet队列):已完成TCP三次握手过程,处于ESTABLISHED状态;
intlisten(intsocketfd,intbacklog)参数一socketfd为socketfd文件描述符参数二backlog,这参数在历史版本有一定的变化
在早期Linux内核backlog是SYN队列大小,也就是未完成的队列大小。
在Linux内核2。2之后,backlog变成accept队列,也就是已完成连接建立的队列长度,所以现在通常认为backlog是accept队列。
但是上限值是内核参数somaxconn的大小,也就说accpet队列长度min(backlog,somaxconn)。
想详细了解TCP半连接队列和全连接队列,可以看这篇:TCP半连接队列和全连接队列满了会发生什么?又该如何应对?(opensnewwindow)
accept发生在三次握手的哪一步?
我们先看看客户端连接服务端时,发送了什么?
客户端的协议栈向服务端端发送了SYN包,并告诉服务端端当前发送序列号clientisn,客户端进入SYNSENT状态;服务端端的协议栈收到这个包之后,和客户端进行ACK应答,应答的值为clientisn1,表示对SYN包clientisn的确认,同时服务端也发送一个SYN包,告诉客户端当前我的发送序列号为serverisn,服务端端进入SYNRCVD状态;客户端协议栈收到ACK之后,使得应用程序从connect调用返回,表示客户端到服务端端的单向连接建立成功,客户端的状态为ESTABLISHED,同时客户端协议栈也会对服务端端的SYN包进行应答,应答数据为serverisn1;ACK应答包到达服务端端后,服务端端的TCP连接进入ESTABLISHED状态,同时服务端端协议栈使得accept阻塞调用返回,这个时候服务端端到客户端的单向连接也建立成功。至此,客户端与服务端两个方向的连接都建立成功。
从上面的描述过程,我们可以得知客户端connect成功返回是在第二次握手,服务端accept成功返回是在三次握手成功之后。
客户端调用close了,连接是断开的流程是什么?
我们看看客户端主动调用了close,会发生什么?
客户端调用close,表明客户端没有数据需要发送了,则此时会向服务端发送FIN报文,进入FINWAIT1状态;服务端接收到了FIN报文,TCP协议栈会为FIN包插入一个文件结束符EOF到接收缓冲区中,应用程序可以通过read调用来感知这个FIN包。这个EOF会被放在已排队等候的其他已接收的数据之后,这就意味着服务端需要处理这种异常情况,因为EOF表示在该连接上再无额外数据到达。此时,服务端进入CLOSEWAIT状态;接着,当处理完数据后,自然就会读到EOF,于是也调用close关闭它的套接字,这会使得服务端发出一个FIN包,之后处于LASTACK状态;客户端接收到服务端的FIN包,并发送ACK确认包给服务端,此时客户端将进入TIMEWAIT状态;服务端收到ACK确认包后,就进入了最后的CLOSE状态;客户端经过2MSL时间之后,也进入CLOSE状态;