TCP的连接过程——三次握手和四次挥手
目录
- TCP连接过程
- TCP基本认识
- TCP连接建立
- TCP连接断开
- Socket编程
TCP连接过程
TCP基本认识
-
与连接有关的TCP头部字段
序列号,确认应答号以及标志位中的SYN,ACK,RST,FIN.
序列号:在建立连接时由计算机生成的随机数作为初始值传递给接收方,之后每发送一次数据序列号就增加这个数据的大小,可以解决网络包乱序问题.
确认应答号:指下一次期望收到的网络包序列号.发送端可以依据确认应答号来判断对方正常接受信息,可以解决网络丢包问题.
SYN:为1时表示希望建立连接,并初始化序列号的值
ACK:为1时表示确认应答有效,规定除了最初建立连接的SYN包其他包中该位必须为1
RST:为1时表示出现异常而强制中断连接.
FIN:为1时表示希望断开连接,用于TCP连接断开.
-
为什么需要TCP协议?TCP工作在哪一层?
TCP工作在传输层.
在网络层中IP只关注网络包的发送,不能保证网络包一定发送到接收端,更不能保证网络包以一种完整,有序的方式到达接收端.因此需要其上层传输层TCP来保证数据包的正确到达.
TCP是一个可靠的传输层协议,它能确保接收端接收的数据包是无损坏,无间隔,无冗余且有序的.
-
什么是TCP?
TCP是一种面向有连接,可靠的,面向字节流的传输层协议.
面向有连接:在通信前发送方和接收方要先建立好连接.且通信是一对一的(当然也可以多对一,多个发送端).不能像UDP那样一个主机同时向多个主机发送消息.
可靠:TCP能够保证数据包正确被接收方接收.
面向字节流:数据在发送时,应用层的数据会在传输层被切分成一个一个的段,TCP段是有序的,因此当接收方前一个TCP段没有接收到,后面的TCP段即使被接收到也不能传递给应用层处理,同时重复的TCP报文会被丢弃.
-
什么是TCP连接?
用于保障可靠性和流量控制维护的某些状态信息,这些信息包括Socket,序列号和窗口大小称为连接.
Socket:有IP地址和端口号组成
序列号:用于解决乱序问题等
窗口大小:用于流量控制
-
有一个IP的服务器监听了一个端口,它的TCP的最大连接数是多少?
服务器通常固定在某一个本地端口上监听,等待客户端的连接请求.
由于客户端的IP地址和端口号是可变的,因此理论上TCP的最大连接数=客户端的IP数 X 客户端的端口数
IPv4中,客户端的IP数最多为232,客户端的端口数最多为216.
尽管理论上服务器上TCP的最大连接数是2^48,但在实际应用中要远低于这个数,会受各种因素的影响:
- 文件描述符限制:每个TCP连接都是一个文件,如果文件描述符被占满了,会发生too many open file.
- 内存限制:每个TCP连接都要占用一定内存,当操作系统的内存被占满后,会发生OOM(Out Of Memory)
-
UDP和TCP的区别是什么呢?各自的应用场景有哪些?
UDP不提供复杂的控制机制,依靠IP实现无连接的通信.
TCP和UDP的区别
-
连接
TCP是面向有连接的协议
UDP是面向无连接的协议
-
服务对象
TCP传输中接收方只能有一个,只能是一对一,多对一的通信
UDP传输中接收方可以是多个,可以实现一对多的通信.
-
可靠性
TCP是可靠传输,数据包会无损,完整,有序的到达接收方
UDP是不可靠传输,不保证接收方能否接收到数据以及接收到的数据是否正确
-
传输过程
TCP依靠拥塞控制,流量控制等机制保证传输在安全可靠的前提下尽可能的快
UDP传输不受网络限制,网络拥堵不影响UDP的传输速率
-
首部开销
TCP首部长度要长于UDP,且当首部含有选项时,长度会变长且不确定
UDP首部长度为8个字节,是固定的
-
传输方式
TCP是面向字节流传输的,属于流式传输,没有边界,但可以保证数据的有序和完整.
UDP是面向数据报传输的,有边界,但可能会丢包或乱序
-
分片方式不同
应用层的数据到TCP传输中会被切分成一个个最大大小为MSS的段进行发送
应用层的数据到UDP传输中不会进行切分,到达IP层会切分成一个个最大大小为MTU的包进行发送.
TCP和UDP的应用场景
TCP是面向连接,保证可靠的传输.可以用于HTTP/HTTPS以及FTP文件传输
UDP是面向无连接的,但传输速度快,可以用于包总数比较少的通信,如DNS,SNMP等;视频,音频等多媒体通信;广播通信;实时性要求比较高的通信.
-
-
为什么UDP头部没有首部长度,而TCP头部有首部长度?
因为UDP头部的大小是固定的,为8个字节.而TCP头部因为选项的存在,长度不固定,所以需要首部长度字段去记录首部的大小
-
为什么UDP头部有包长度,TCP头部没有包长度?
TCP数据长度等于IP总长度-IP首部长度-TCP首部长度. IP总长度与IP首部长度在IP层可知,TCP首部长度在TCP层可知,所以TCP数据长度可以被计算出来.
而虽然UDP数据长度也可以用相同的方式计算出来,但为了方便处理首部长度应该是4的整数倍,而UDP首部的包长度可能是为了补齐4的整数倍
TCP连接建立
-
TCP三次握手过程和状态变迁
初始状态:开始客户端和服务器都处于CLOSED状态,服务器主动监听一个端口,处于LISTEN状态
第一次握手:之后客户端请求建立连接:客户端会生成一个随机值作为序列号的初始值,然后将标志位中的SYN设置为1,将报文发送给服务器,之后客户端处于SYN_SENT状态.
第二次握手:服务器收到SYN报文后,也会生成一个随机值作为自己端序列号的初始值,并将标志位中的SYN设置为1;同时会用客户端发送的SYN包中的序号+1作为确认序号返回给客户端,同时将标志位中的ACK设置为1,之后服务器处于SYN_RCVD状态.
第三次握手:客户端接收到服务器报文后,向服务器返回一个应答报文,应答报文中的确认序号为第二次握手中报文中的序号+1,同时将标志位中的ACK设置为1,这次发送过程是可以携带应用层数据的.之后客户端处于ESTABLISHED状态
服务器收到客户端的应答报文后,也会进入ESTABLISHED状态
前两次握手中不能携带数据,第三次握手时可以携带数据
在Linux3.7中提供了快速连接的功能,对于普通的连接建立,HTTP想要进行一次完整的HTTP请求交互最少需要2个RTT(TCP连接消耗1.5RTT+数据随着第三次握手发送+0.5RTT数据响应的发送),且之后的连接过程都是消耗至少2个RTT.而快速连接的第一次完整HTTP请求交互需要2个RTT,但在之后的连接过程中只需要1个RTT.
快速连接的流程
- 在第一次建立连接的过程,服务器会在第二次握手过程中产生一个经过加密的Cookie同SYN+ACK报文发送给客户端.客户端在本地缓存这个Cookie.第一次连接完成一次完整的HTTP请求交互仍需要2个RTT.
- 在下次建立连接时,客户端会将SYN包和Cookie以及数据请求一同发送给服务器,服务器通过解析Cookie获取到TCP连接的信息.然后服务器返回SYN+ACK以及数据响应.因此整个过程只需要消耗1个RTT.
-
在Linux中如何查看TCP状态
使用命令:netstat -napt
-
为什么是三次握手?不能是两次,为什么不是四次?
-
三次握手才可以阻止重复历史连接的初始化造成混乱(主要原因)
考虑一个场景,客户端发送了一个SYN(seq = 50)的包,发送后客户端宕机了,然后因为网络收敛包也没有传输到服务器,客户端重启后发送了一个新的SYN(seq = 70)的包,这时网络恢复正常,之前的SYN(seq = 50)的包优先于新的SYN包到达服务器,服务器会针对seq=50的请求报文做出确认应答ACK(ack = 51)的报文给客户端.
假设只有两次握手,此时服务器发送完报文后处于ESTABLISHED状态,也就是说可以发送数据给客户端了,这时因为网络收敛客户端迟迟收不到服务器的ACK报文,然后服务器又发送了数据给客户端,网络恢复,数据报文优先于ACK报文到客户端,尽管客户端可以通过上下文丢弃异常的ACK报文(因为客户端要的ACK报文的ack=71)而发送RST中断连接,但服务器发送的数据已经到达客户端,所以造成了资源的浪费.
而三次握手则可以处理历史连接的问题,第二次握手的ACK报文达到客户端后,服务器处于SYN_PRVD状态,客户端在接收到错误的ack后会发送RST报文终止异常连接,之后就可以通过正常的SYN(seq = 70)建立正确的连接了.
-
三次握手才可以同步双方的序列号
TCP协议通信的双方都需要维护一个序列号,序列号可以保证
- 接收方可以去除掉重复的数据
- 接收方可以保证接收的数据包有序
- 序列号和确认序号可以告知发送方接收方接收到了哪些数据
而一次交互可以让接收方获知发送方的序列号,所以需要两次交互,而中间两次交互可以优化成一次,所以是三次握手,四次握手也可以获取到对方的序列号,但会浪费不必要的资源,而两次握手只能获取到一方的序列号
-
三次握手才可以避免资源浪费
两次握手在面临客户端超时重传发送多次连接请求时会让服务器创建冗余的无效连接(由于是两次握手,服务器每次接收到请求报文后发送报文,然后都会建立一个新的连接),进而浪费服务器的资源
四次握手可以优化成三次握手,所以使用四次握手也会浪费不必要的资源
-
-
为什么每次建立TCP连接时生成的序列号都不一样?
-
防止历史报文被下一次相同的连接(四元组相同)接收
假设每一次初始生成的序列号都相同,在上一次连接中的数据报文因为网络拥塞和接收端宕机等原因被遗弃到了网络中,而当下一次连接建立好后,因为生成的序列号相同,所以有很大的概率接收到上一次连接中的数据报文.
而当每次生成的序列号都不相同时,历史连接中遗留的报文大概率就会因为序列号的不同而无法在新的连接中被接收端接收到.
-
防止黑客伪造序列号发送恶意报文
同理.
-
-
初始化序列号ISN是如何生成的?
初始ISN是基于时钟产生的,每4微妙+1,4.55个小时完成一圈
随机算法:ISN = M + F(localhost,localport,remotehost,remoteport),其中M是一个计时器,每4微妙+1,F是一个哈希算法,根据四元组生成的一个hash值
-
既然IP层会分片,那为什么TCP层还需要MSS?
MTU:一个网络包的最大长度
MSS:一个TCP段中数据(不包括TCP首部的长度)的最大长度
假设TCP层不用MSS,而是交给IP进行分片.这样一个TCP数据包在IP层因为超过MTU而被分片.当这些分片传输到接收方时会重组成一个数据包.但如果传输到接收方中的某个分片丢包需要重传,因为IP层没有重传值,而TCP层重传时则需要将这个TCP数据包重新发送.这样就造成了重复传输.因此为了提升效率,在TCP中引入了MSS,在TCP层分片后的报文长度在IP层就不会因为超过MTU再分片,这样即使需要重传,也是以MSS为单位重传而不需要重传整个数据段.
-
第一次握手丢失了,发生了什么?
首先第一次握手过程是客户端向服务器发送请求连接的SYN报文,在这之后,如何客户端迟迟收不到服务器的确认应答报文(ACK),就会触发客户端的超时重传机制,客户端会重新发送一份相同的SYN报文.
不同版本的操作系统触发超时重传的时间不同,1s,maybe 3s
对于重发次数而言,在Linux中,客户端的最大重传次数由tcp_syn_retries参数控制.默认一般是5.
通常第一次重传的等待时间是1s,之后每次重传的等待时间是前一次的2倍(2.4.8.16.32…)在重传5次后,如果仍接收不到ACK报文,则客户端会主动断开连接.所以重传总耗时约为1min(1+2+4+8+16+32=63)
-
第二次握手丢失了,发生了什么?
第二次握手过程中服务器向客户端发送ACK报文和SYN报文.因此当第二次握手的报文丢失后会导致客户端和服务器都触发超时重传机制.客户端因为没有收到服务器的ACK报文而触发,服务器因为没有接收到客户端的ACK报文而触发.
-
第三次握手丢失了,发生了什么?
第三次握手过程中,客户端发送ACK报文给服务器后客户端处于ESTLABLISH,可以向服务器发送数据.当服务器接收不到ACK报文时会触发超时重传机制.重传SYN-ACK报文.
当服务器发送多次后仍接收不到ACK报文就会中断连接.
对于客户端而言,客户端存在两种情况
-
客户端向服务器发送数据.
由于服务器已经断开连接所以会触发客户端的超时重传,在建立了连接后,超时重传的次数和没有建立连接的重传次数不同,前者是15次,后者是5次,所以重传15次后会断开连接
-
客户端不发送数据.会触发保活机制.多次发送保活探测报文后仍接收不到响应则中断连接
-
-
什么是SYN攻击?如何避免SYN攻击?
SYN攻击就是第三方在短时间内模拟不同的IP地址给服务器发送SYN报文.服务器收到后发送SYN-ACK报文无法被正确接收,于是服务器的半连接队列就会被占满,因此服务器就无法提供正常的服务.
避免SYN攻击的方式
- 修改Linux内核参数.当接收端处理数据包的速度小于发送端发送数据包的速度且接收端的队列已经满时直接发送RST报文,丢弃连接:net.ipv4.tcp_abort_on_overflow
控制队列的最大值:net.core.netdev_max_backlog
SYN_PRVD状态连接的最大数:net.ipv4.tcp_max_syn_backlog
服务器将客户端的SYN报文放入内核的半连接队列中;接着返回给客户端ACK+SYN报文;在接收到客户端的ACK报文后,会从半连接队列中删除对应的连接然后创建一个新的连接放到全连接队列中;应用通过调用accept()方法从全连接队列中取出连接.
当收到SYN攻击时,半连接队列会因为服务器收到多个SYN报文而服务器又无法接收到客户端的ACK报文而无法将连接从半连接队列移动到全连接队列.造成的结果就是半连接队列队满而全连接队列队空
可以通过调用命令net.ipv4.tcp_syncookies=1来应对SYN攻击.当半连接队列队满时,再次接收到的SYN报文不会再放入半连接队列中,而是计算出一个cookie值放入SYN_ACK报文中的序列号字段发送给客户端,服务器验证客户端返回的ACK报文是否有效,如果有效才会删除半连接队列中的连接然后创建一个新的连接放入全连接队列中.
-
减少SYN+ACK重传次数.
第二次握手丢失会造成客户端和服务器均重传报文,当重传次数超过某个限制后会断开连接,因此我们可以控制减少SYN+ACK重传次数,让客户端重传的SYN次数的速度加快,更快的断开连接.
TCP连接断开
-
TCP四次握手和状态变迁
四次握手过程
- 初始时客户端和服务器都处于ESTABLISHED状态.
- 第一次挥手:客户端向服务器发送FIN报文,报文首部的标识位FIN值设置为1.之后客户端进入FIN_WAIT1状态.
- 第二次挥手:服务器接收到FIN报文后会反馈一个ACK报文给客户端,之后服务器进入CLOSED_WAIT状态.客户端接收到ACK报文后进入FIN_WAIT2状态.
- 第三次挥手:服务器向客户端发送FIN报文,报文首部的标识位FIN值设置为1,之后服务器进入LAST_ACK状态.
- 第四次挥手:客户端收到FIN报文后会反馈一个ACK报文,之后进入TIME_WAIT状态.服务器收到ACK报文后进入CLOSE状态,客户端从发送报文后等待2MSL时间后进入CLOSE状态.
需要注意的是主动关闭连接的一方才有TIME_WAIT状态
-
为什么挥手需要四次,不能像握手一样三次吗?
和三次握手相比,四次挥手好像是将中间两次发送报文分开了,而没有合并.之所以不合并是因为此刻服务器还是可以向客户端发送数据.主动发送FIN报文给对方的一端表示在之后不再发送数据给对方,而不能确定对方也不再发送数据给自己.所以服务器的ACK报文和FIN报文之间服务器可能还会向客户端发送一些数据报文,所以不能合并.所以握手需要四次.
-
第一次挥手丢失会发生什么?
第一次握手后客户端会处于FIN_WAIT1状态,等待服务器的ACK报文.当第一次握手丢失后,客户端不能接收到服务器的ACK报文,所以会触发客户端的超时重传机制,所以客户端会重新发送FIN报文.当重传一定次数后仍不能收到服务器的ACK报文后会直接进入CLOSE状态
-
第二次挥手丢失会发生什么?
第二次挥手服务器会向客户端发送ACK报文,之后服务器进入CLOSED_WAIT状态,客户端接收到ACK报文后进入FIN_WAIT2状态.当第二次挥手丢失,客户端因为接收不到服务器的ACK报文而触发超时重传,重新发送FIN报文,当重传次数超过一定次数后会直接进入CLOSE状态
因为close函数关闭连接,所以无法再发送数据,因此客户端在FIN_WAIT2状态不能呆太长时间,通过tcp_fin_timeout控制FIN_WAIT2状态最多持续60s.
因此客户端在60s内如果接收不到服务器的FIN报文,客户端也会直接进入CLOSE状态.
上述是在利用close函数关闭连接的基础上的情况,如果使用shutdown函数关闭连接,且只关闭发送方.此时如果发送方无法接收到第三次挥手的FIN报文,则会一直在FIN_WAIT2状态持续(tcp_fin_timeout无法影响shutdown关闭连接)
-
第三次挥手丢失会发生什么?
服务器接收到客户端的FIN报文后,内核会自动回复ACK,然后进入CLOSE_WAIT状态,等待应用进程调用close函数,之后又进程调用close函数而发送FIN报文给客户端.如果第三次挥手丢失,服务器会触发超时重传重新发送FIN报文,发送一定次数仍接收不到ACK则会直接进入CLOSE状态.
-
第四次挥手丢失会发生什么?
第四次挥手客户端发送ACK报文给服务器,之后进入TIME_WAIT状态,然后等待2MSL后进入CLOSE状态.当ACK报文丢失后,服务器会触发超时重传重新发送FIN报文,当重传多次仍接收不到ACK后会进入CLOSE状态.
-
为什么TIME_WAIT状态等待的时间是2MSL?
MSL是报文最大生存时间.它指的是一个报文在网络中可以停留的最长时间,超过这个时间报文就会被丢弃.
之所以等待2MSL,是因为主动关闭方需要等待被动关闭方发送FIN报文,然后反馈给被动关闭方ACK报文,一来一回正好是2MSL
2MSL实际上是允许ACK报文丢包一次的,客户端从接收到FIN报文到发送ACK报文后开始计时,当ACK报文丢失后,服务器会重传FIN报文,这整个过程恰好是2MSL.
之所以不是3MSL,4MSL,是因为丢包率本来在网络中是很低的,连续丢两次包的概率就更低了,所以只考虑一次丢包的性价比更高.
Linux中规定1个MSL的时间是30s,所以TIME_WAIT状态等待的时间是60s.
-
为什么需要TIME_WAIT状态?
-
防止历史连接中的数据被后面的相同连接错误接收.
首先初始序列号ISN的生成不是无限递增的,它也会发生一个回绕为原来值的情况(计时器是依靠时钟进行递增的).
因此如果TIME_WAIT状态没有或等待时间很短,则再下一次建立相同连接(四元组一致)后,历史连接中遗留的报文就有可能因为序列号匹配而被新的连接中的某一端接收.
设置TIME_WAIT为2MSL足以保证接收方和发送方发送的报文随着此次连接的关闭而全部被丢弃,保证了在下一次连接中的数据包绝不会是历史连接中遗留的数据包.
-
保证被动关闭一方能够正常关闭连接.
当客户端发送ACK报文进入TIME_WAIT状态后,假如发送的ACK报文丢包了,此时就会触发服务器的超时重传机制重传FIN包,如果没有TIME_WAIT状态,则客户端在发送完ACK报文后就进入CLOSE状态了,当再次接收到服务器的FIN报文后,会发送RST报文给服务器,服务器接收到RST报文后会认为此次关闭是一种异常关闭.所以为了尽量避免这种状况的发生,引入了TIME_WAIT状态.
-
-
TIME_WAIT状态过多有什么危害?
-
占用系统资源
当被动发起方的TIME_WAIT过多时,服务器上会同时建立多个连接,尽管服务器可以同时和多个客户端进行通信,但过多的连接会占用系统资源而影响服务器的服务效率
-
占用端口资源
当主动发起方的TIME_WAIT过多时,占用过多的端口是,客户端就没有可用的源端口,就无法再次和相同的服务器建立连接,进行通信.
-
-
如何优化TIME_WAIT?
-
打开net.ipv4.tcp_tw_reuse和net.ipv4.tcp_timetemps选项
打开这两个参数后,可以复用处于TIME_WAIT状态的socket为新的连接所用.
net.ipv4.tcp_tw_reuse参数只能用于客户端.开启该功能后,在调用connect()函数时,会随机找一个TIME_WAIT状态超过1s的连接给新的连接复用
-
net.ipv4.tcp_max_tw_buckets
这个值默认为18000.当系统中处于TIME_WAIT状态的值超过这个数时,系统就会将TIME_WAIT状态的连接重置.
-
程序中使用SO_LINGER
当开启SO_LINGER后,调用close函数后,会立刻发送一个RST给对端,该连接会跳过四次挥手状态直接关闭.
-
-
如果已经建立了连接,但是客户端突然出现了故障怎么办?
TCP中有一个保活机制:在某个时间段内,如果通信双方没有再进行任何通信,服务器就会发送一个探测报文给对方,这个报文包含非常少的数据.如果连续多个探测报文都没有得到响应,则认为该连接已经死亡.
Linux中可以设置报活时间,保活探测次数,保活探测时间间隔
net.ipv4.tcp_keepalive_time=7200 2h无活动后触发保活机制
net.ipv4.tcp_keepalive_intvl=75 保活探测间隔为75s
net.ipv4.tcp_keepalive_probes=9 保活次数达到9次后中断连接
TCP保活情况
- 对端正常工作,给予了保活探测报文正常的响应,则保活时间会被重置.
- 对端程序崩溃并重启,给予了保活探测报文错误的响应,则会发送RST报文表示连接发生异常
- 对端程序崩溃,无法基于正常的响应,多次发送保活探测报文后会中断本次连接.
-
如果已经建立了连接,服务器的进程崩溃会发生什么?
服务器会发送FIN报文,与客户端进行四次挥手
Socket编程
-
针对TCP应该如何Socket编程?
- 服务器和客户端初始化Socket,得到文件描述符.
- 服务器调用bind,绑定IP地址和端口.
- 服务器调用listen,监听某个端口.
- 服务器调用accept,等待客户端连接
- 客户端调用connect,向服务器的地址和端口发起连接请求
- 服务器accept返回用于传输的文件描述符
- 客户端调用write写入数据,服务器调用read读取数据(服务器调用write写入数据,客户端调用read读取数据)
- 客户端调用close断开连接.服务器在read时读取到了EOF,中断读取,待处理完数据后,服务器调用close断开连接
注意:服务器listen时的socket和accept后得到的用于数据传输的socket是两个socket
-
Listen时参数backlog的意义?
在建立连接时,服务器内核中会建立两个队列:半连接队列(SYN队列)和全连接队列(Accept队列),具体过程可以参考[TCP连接建立(SYN攻击)章节]
参数backlog在早期Linux内核中是SYN队列的大小,在Linux2.2之后是Accept队列的大小
-
accept发生在三次握手的哪一步?
accept是在建立连接前开启,在第三次握手后成功返回.
-
客户端调用close后,连接断开的流程是什么样的?