前言:
最近学习了计算机网络的知识,看了很多的视频,并参考了很多资料,写下了这篇文章,如果有什么写的并不准确的地方,还请大佬不吝赐教。
什么是 TCP
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
- 面向连接:只能一对一才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息。
- 可靠的:TCP提供可靠交付的服务。通过TCP连接传送的数据,无差错、不丢失、不重复、并且按序到达;
- 字节流:用户消息通过 TCP 协议传输时,消息可能会被操作系统分组成多个的 TCP 报文,并且 TCP 报文是有序的,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对重复的 TCP 报文会自动丢弃。
TCP 头格式
- 源端口和目的端口,各占2个字节。
- seq Sequence Number 序列号,4字节,用来标识TCP发端向TCP收端发送的数据字节流。用来解决网络包乱序问题。
- ack Acknowledgment Number,确认应答号,4字节,表示接收方期望收到发送方下一个报文段的第一个字节数据的序列号。用来解决丢包的问题
- 控制位:
- ACK 表示应答,该位为 1 时,确认应答的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。
- RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
- SYN:该位为 1 时,表示希望建立连接,并在其**序列号 (seq)**的字段进行序列号初始值的设定。
- FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时。
- 紧急URG,当URG=1,表明紧急指针字段有效。告诉系统此报文段中有紧急数据;
- 推送PSH,当两个应用进程进行交互式通信时,有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应,这时候就将PSH=1;
- 数据偏移(首部长度),占4位,它指出TCP报文的数据距离TCP报文段的起始处有多远;
保留,占6位,保留今后使用,但目前应都位0; - 检验和,占2字节,校验首部和数据这两部分;
- 紧急指针,占2字节,指出本报文段中的紧急数据的字节数;
- 窗口,占2字节,指的是通知接收方,发送本报文你需要有多大的空间来接受;
- 选项,长度可变,定义一些其他的可选的参数。
什么是 TCP 连接?
用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为TCP连接。
- Socket:由 IP 地址和端口号组成
- 序列号:用来解决乱序问题等
- 窗口大小:用来做流量控制
如何唯一确定一个 TCP 连接呢?
- 源地址
- 源端口
- 目的地址
- 目的端口
源地址和目的地址的字段(32位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机。
源端口和目的端口的字段(16位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。
TCP 连接建立
三次握手过程(重点)
首先举一个生活中常见的例子:
此时蔡蔡和new出来的对象拨通了电话:
你 :“喂,能听到我说话吗?”
你对象 :“亲爱的,我可以听到!,你听得到吗”
你 :“当然啦。。。。。。”
然后就可以正常交流了。
- 一开始,客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口,处于 LISTEN 状态
- 客户端会随机初始化序号,将此序号置于 TCP 首部的序号seq字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,之后客户端处于 SYN-SENT 状态。TCP规定,(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
- 服务端收到客户端的 SYN 报文后,首先服务端也随机初始化序号,将此序号填入 TCP 首部的序号seq字段中,其次把 TCP 首部的确认应答号ack字段填入客户端的seq+ 1, 接着把 SYN 和 ACK 标志位置为 1。最后把该报文发给客户端,之后服务端处于 SYN-RCVD 状态。这个报文也不能携带数据,但是同样要消耗一个序号。
- 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次确认应答号字段填入服务端的seq + 1,最后把报文发送给服务端,之后客户端处于 ESTABLISHED 状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。
为什么是三次握手?不是两次、四次?(重点)
1.确认双方具有接收和发送的能力
第一次握手:服务端接受数据正常
第二次握手:客户端发送和接受数据正常
第二次握手:服务端发送和接受数据正常
2.三次握手才可以初始化Socket、序列号和窗口大小并建立 TCP 连接。
- 避免历史连接
- 一个旧 SYN 报文比最新的 SYN 报文早到达了服务端;
- 那么此时服务端就会回一个 SYN + ACK 报文给客户端;
- 客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送 RST 报文给服务端,表示中止这一次连接。
如果是两次握手连接,就无法阻止历史连接,那为什么 TCP 两次握手为什么无法阻止历史连接呢?
在两次握手的情况下,服务端没有中间状态(SYNC RECV)给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。两次握手的情况下,服务端在收到 SYN 报文后,就进入 ESTABLISHED 状态,意味着这时可以给对方发送数据,但是客户端此时还没有进入 ESTABLISHED 状态,服务端在向客户端发送数据前,并没有阻止掉历史连接,导致服务端建立了一个历史连接,又白白发送了数据,妥妥地浪费了服务端的资源。
- 同步双方初始序列号
序列号是可靠传输的一个关键因素,它的作用:
1.接收方可以去除重复的数据;
2.接收方可以根据数据包的序列号按序接收;
3.可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道);
四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了三次握手。而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
不使用两次握手和四次握手的原因:
两次握手:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
四次握手:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数
第一次握手丢失了,会发生什么?
如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的。每次超时的时间是上一次的 2 倍。
第二次握手丢失了,会发生什么?
第二次握手的 SYN-ACK 报文其实有两个目的 :
- 第二次握手里的 ACK, 是对第一次握手的确认报文;
- 第二次握手里的 SYN,是服务端发起建立 TCP 连接的报文;
因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文。
然后,因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了。
那么,如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文。
第三次握手丢失了,会发生什么?
客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH 状态。
因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数**。注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。**
半连接队列和全连接队列
半连接队列(syn queue)
客户端发送SYN包,服务端收到后回复SYN+ACK后,服务端进入SYN_RCVD状态,此时双方还没有完全建立连接,这个时候的socket会放到半连接队列。
全连接队列(accept queue)
当服务端收到客户端的ACK后,socket会从半连接队列移出到全连接队列。当调用accpet函数的时候,会从全连接队列的头部返回可用socket给用户进程。全连接队列中存放的是已完成TCP三次握手的过程,等待被处理的连接,在客户端及服务端的状态均为 ESTABLISHED。
不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,默认情况都会丢弃报文。
SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。
避免 SYN 攻击方式,可以有以下四种方法:
- 调大 netdev_max_backlog;
- 增大 TCP 半连接队列;
- **开启 tcp_syncookies:**可以在不使用 SYN 半连接队列的情况下成功建立连接,相当于绕过了 SYN 半连接来建立连接。
- 减少 SYN+ACK 重传次数
什么是 SYN 攻击?如何避免 SYN 攻击?
我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的半连接队列,使得服务器不能为正常用户服务。
四次挥手
- 刚开始客户端和服务端都处于ESTABLISHED状态,假如客户端主动发起关闭请求:
- 第一次挥手:TCP客户端向服务端发送一个FIN报文,用来关闭客户端到服务端的数据传送,其中包含一个序列号seq=x,发送完后,客户端进入FIN_WAIT_1状态(不再发送数据,但是可以接收服务器发来的报文)。TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
- 第二次挥手:服务端接收到FIN报文后,会发回一个ACK,表明自己已经接收到了此报文,但是服务端可能还有数据要传),此时服务端进入CLOSE_WAIT关闭等待状态。这个时候TCP处于半关闭状态,当客户端接收到服务端的回复后,进入FIN_WAIT_2状态。
- 第三次挥手:服务器关闭客户端的连接,发送一个FIN报文给客户端,并且指定一个序列号,ack的值为x+1,此时服务器处于LAST_ACK最后确认状态,等待客户端回应。
- 第四次挥手 :客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答(ack = w+1),且把服务端的序列值 +1 作为自己 ACK 报文的序号值,此时客户端处于 TIME_WAIT(时间等待状态)。TIME-WAIT状态是为了等待足够的时间以确保远程TCP接收到连接中断请求的确认。
为什么挥手需要四次?
第一次挥手:仅仅表示客户端不再发送数据了但是还能接收数据。
第二次挥手:服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送。
第三次挥手:等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
第四次挥手:客户端收到服务端FIN报文。
第一次挥手丢失了,会发生什么?
正常情况下: 如果能及时收到服务端(被动关闭方)的 ACK,则会很快变为 FIN_WAIT2状态。
第一次挥手丢失了:客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文。
客户端重传 FIN 报文的次数超过,就不再发送 FIN 报文,则会在等待一段时间,如果还是没能收到第二次挥手,那么直接进入到 close 状态。
第二次挥手丢失了,会发生什么?
正常情况下: 服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入到 CLOSE_WAIT 状态。
但是ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。
第三次挥手丢失了,会发生什么?
正常情况下:服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。
异常:服务端迟迟收不到这个 ACK,服务端就会重发 FIN 报文。
第四次挥手丢失了,会发生什么?
正常:客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT 状态。TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。
异常:第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文。客户端在收到第三次挥手后,就会进入 TIME_WAIT 状态,开启时长为 2MSL 的定时器,如果途中再次收到第三次挥手(FIN 报文)后,就会重置定时器,当等待 2MSL 时长后,客户端就会断开连接。
为什么需要 TIME_WAIT 状态,为什么 TIME_WAIT 等待的时间是 2MSL?
主动发起关闭连接的一方,才会有 TIME-WAIT 状态。
1、防⽌客户端最后⼀次发给服务器的确认在⽹络中丢失以⾄于客户端关闭,⽽服务端并未关闭,导致资源的浪费。
2、等待最⼤的2msl可以让本次连接的所有的⽹络包在链路上消失,以防造成不必要的⼲扰。
客户端直接closed,然后⼜向服务端发起了⼀个新连接,我们不能保证这个新连接和刚关闭的连接的端⼝号是不同的。假设新连接和已经关闭的⽼端⼝号是⼀样的,如果前⼀次滞留的某些数据仍然在⽹络中(防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中),这些延迟数据会在新连接建⽴后到达服务端,所以socket就认为那个延迟的数据是属于新连接的,数据包就会发⽣混淆。所以客户端要在TIME_WAIT状态等待2倍的MSL,这样保证本次连接的所有数据都从⽹络中消失。
TIME_WAIT 过多有什么危害?
第一是占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等;(服务端)
第二是占用端口资源,端口资源也是有限的。(客户端)
客户端和服务端 TIME_WAIT 过多,造成的影响是不同的。
如果客户端的 TIME_WAIT 状态过多,占满了所有端口资源,那么就无法对目的 IP+ 目的 PORT都一样的服务器发起连接了。不过,只要连接的是不同的服务器,端口是可以重复使用的,所以客户端还是可以向其他服务器发起连接的,这是因为内核在定位一个连接的时候,是通过四元组(源IP、源端口、目的IP、目的端口)信息来定位的。
如果服务端的 TIME_WAIT 状态过多,并不会导致端口资源受限,因为服务端只监听一个端口,而且由于一个四元组唯一确定一个 TCP 连接,因此理论上服务端可以建立很多连接,但是 TCP 连接过多,会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等。
如何优化 TIME_WAIT?
- 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;-有一点需要只能用客户端(连接发起方),因为开启了该功能,在调用 connect() 函数时,内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用。
net.ipv4.tcp_tw_reuse = 1
使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即
net.ipv4.tcp_timestamps=1(默认即为 1)
- net.ipv4.tcp_max_tw_buckets
这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将后面的 TIME_WAIT 连接状态重置,这个方法比较暴力。
- 程序中使用 SO_LINGER
我们可以通过设置 socket 选项,来设置调用 close 关闭连接行为。
如果服务端要避免过多的 TIME_WAIT 状态的连接,就永远不要主动断开连接,让客户端去断开,由分布在各处的客户端去承受 TIME_WAIT。
文章到这里就结束了。如果您看完了此文章,觉得有什么不准确或者需要改进的地方,请大佬指正。或者有什么不懂的地方,都可以提出来,会耐心解答的。