【计算机网络 篇】TCP基本认识和TCP三次握手相关问题
目录
TCP基本认识
1. TCP头格式有哪些?编辑
2. 为什么需要TCP协议?TCP工作在哪一层?
3. 什么是TCP?
4. 什么是TCP连接?
5. 如何唯一确定一个TCP连接呢?
6. 有一个IP的服务器监听了一个端口,它的TCP的最大连接数是多少?
7. UDP和TCP有什么区别呢?分别的应用场景是?
TCP和UDP区别:
TCP和UDP应用场景:
8. TCP如何保证可靠性
9. TCP和UDP可以使用同一个端口么?
10. 如何理解是TCP面向字节流协议?
如何理解字节流?
为什么UDP是面向报文的协议?
为什么TCP是面向字节流的协议?
11. 如何解决粘包?
TCP连接建立
1. TCP三次握手过程是怎样的?编辑
2. 如何在Linux系统中查看TCP状态?
3. 为什么是三次握手?不是两次、四次?
4. 为什么每次建立TCP连接时,初始化的序列号都要求不一样呢?
5. 初始序列号ISN是如何随机产生的?
6. 既然IP层会分片,为什么TCP层还需要MSS呢?
7. 第一次握手丢失了,会发生什么?
8. 第二次握手丢失了,会发生什么?
9. 第三次握手丢失了,会发生什么?
10. 什么是SYN攻击?如何避免SYN攻击?
TCP基本认识
1. TCP头格式有哪些?
序列号:在建立连接时由计算机生成的随机数作为其初始值,通过SYN包传给接收端主机,每发送一次数据,就累加一次该数据字节数的大小。用来解决网络包乱序问题。
确认应答号(ack_seq):指下一次期望收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
控制位:
-
ACK:该位为1时,确认应答号的字段变为有效的,接收方已经成功收到数据,TCP规定除了最初建立连接时的SYN包之外该位必须设置为1(如果第一个包只是确认没有携带数据,那ACK=0,如果第一个包也携带数据那ACK=1),确认应答号字段指明“期望收到的下一个序列号”,例如:收到序列号1000-1099的数据包后,会回复ACK=1且确认应答号=1100
-
RST:该位为1时,表示TCP连接中出现异常必须强制断开连接。
-
SYN:该位为1时,表示希望建立连接,并在其序列号的字段进行序列号初始值的设定。只在建立连接的第一次握手(客户端->服务器)和第二次握手(服务器->客户端)时设置为1,用于同步初始序列号
-
FIN:该位为1时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换FIN位为1的TCP段。当一方发送FIN=1时,表示自己不再发送数据,但还可以接收数据,需要双方都发送FIN=1才能完全断开连接。
补充:
数据偏移:4bit,表示TCP首部长度,以×4B为单位
4bit,取值范围0~15。因此TCP首部最长=15×4=60B
保留:6bit,暂时没啥用,通常全部置为0
URG:1bit,URG=1时,紧急指针有效。表示这是紧急数据,应尽快插队发送
PSH:1bit,PSH=1时,表示希望接收方尽快回复(用于交互式通信)
紧急指针:紧急数据专用序号,原理与上面那个“序号”字段相同
窗口:16bit,表示接收窗口的大小。即从本报文段首部中的ask_seq算起,接收方还能接收多少数据(以字节为单位)
2. 为什么需要TCP协议?TCP工作在哪一层?
IP层是不可靠的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。
如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的TCP协议来负责。
因为TCP是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。
3. 什么是TCP?
TCP是面向连接的、可靠的、基于字节流的传输层通信协议。
面向连接:一定是一对一才能连接,不能像UDP协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
可靠的:无论网络链路中出现了怎样的链路变化,TCP都可以保证一个报文一定能够到达接收端。
字节流:用户消息通过TCP协议传输时,消息可能会被操作系统分组成多个的TCP报文,如果接收方的程序不知道消息的边界,是无法读出一个有效的用户消息的。并且TCP报文是有序的,当前一个TCP报文没有收到的时候,即使它先收到了后面的TCP报文,那么也不能扔给应用层去处理,同时对重复的TCP报文会自动丢弃。
TCP字节流是指TCP协议再传输数据时,将应用层交付的数据视为连续的、无结构的字节序列,而不是像UDP那样保留消息边界。
优点:
-
TCP的字节流保证了数据完整有序,即使网络乱序或丢包,接收方最终会得到正确排序的字节流,只重传丢失的报文段而非整个消息,效率更高。
-
大数据传输时TCP自动拆分/重组数据,应用层无需关心分组细节。动态消息边界,通过长度前缀或分隔符标记边界,适应不同需求。
-
相比UDP的固定边界,可以减少头部开销,TCP复用连接流式传输(一次连接可传输无数条消息,只需在建立连接时协商一次参数如窗口大小,后续报文复用相同连接,后续数据只需携带基础头部),避免为每条小消息重复添加头部(小数据可合并到一个报文,只需1各头部)。
4. 什么是TCP连接?
首先我们要知道连接的定义:用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括socket、序列号和窗口大小称为连接。
所以我们可以知道,建立一个TCP连接是需要客户端与服务器达成上述三个信息的共识。
-
socket:由IP地址和端口号组成
-
序列号:用来解决乱序问题等
-
窗口大小:用来做流量控制
5. 如何唯一确定一个TCP连接呢?
TCP四元组可以唯一的确定一个连接,四元组包括如下:
源地址和目的地址的字段(32位)是在IP头部中,作用是通过IP协议发送报文给对方主机。
源端口和目的端口的字段(16位)是在TCP头部中,作用是告诉TCP协议应该把报文发给哪个进程。
6. 有一个IP的服务器监听了一个端口,它的TCP的最大连接数是多少?
服务器通常固定在某个本地端口上监听,等待客户端的连接请求。
因此,客户端IP和端口是可变的,其理论值计算公式如下:
对IPV4,客户端的IP数最多为2的32次方,客户端的端口数最多为2的16次方,也就是服务器单机最大TCP连接数,约为2的48次方。
当然,服务器最大并发TCP连接数远不能达到理论上限,会受以下因素影响:
文件描述符限制,每个TCP连接都是一个文件,占用1个fd,如果文件描述符被占满了,会发送Too many open files(这是Unix/Linux系统的资源保护机制,表示进程或系统打开的文件描述符(File Descriptor, fd)数量已达到上限,无法继续创建新连接或文件操作。)。Linux对可打开的文件描述符的数量分别作了三个方面的限制:
-
系统级:当前系统可打开的最大数量,通过 cat /proc/sys/fs/file-max查看;
-
用户级:指定用户可打开的最大数量,通过 cat /etc/security/limits.conf查看;
-
进程级:单个进程可打开的最大数量,通过cat /proc/sys/fs/nr_open 查看;
7. UDP和TCP有什么区别呢?分别的应用场景是?
内存限制,每个TCP连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会发生OOM错误(“out of memory”内存耗尽)。
UDP不提供复杂的控制机制,利用IP提供面向无连接的通信服务。
UDP协议真的非常简,头部只有8个字节(64位),UDP的头部格式如下:
目标和源端口:主要是告诉UDP协议应该把报文发给哪个进程。
包长度:该字段保存了UDP首部的长度跟数据的长度之和。
校验和:校验和是为了提供可靠的UDP首部和数据而设计,防止收到网络传输中受损的UDP包。
TCP和UDP区别:
1. 连接:
-
TCP是面向连接的传输层协议,传输数据前先要建立连接。
-
UDP是不需要连接,即刻传输数据。
2.服务对象
-
TCP是一对一的两点服务,即一条连接只有两个端点。
-
UDP支持一对一、一对多、多对多的交互通信
3.可靠性
-
TCP是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。
-
UDP是尽最大努力交付,不保证可靠交付数据。但是我们可以基于UDP传输协议实现一个可靠的传输协议,比如QUIC协议。
4. 拥塞控制、流量控制
-
TCP有拥塞控制和流量控制机制,保证数据传输的安全性。
-
UDP则没有,即使网络非常拥堵了,也不会影响UDP的发生速率。
5. 首部开销
-
TCP首部长度较长,会有一定的开销,首部有没有使用选项字段时是20字节,如果使用了选项字段则会变长的。选项字段一般是在握手阶段中协商参数,后续数据传输阶段通常不再使用(除时间戳等少数选项)
-
UDP首部只有8个字节,并且是固定不变的,开销较小。
6. 传输方式
- TCP是流式传输,没有边界,但保证顺序和可靠。
- UDP是一个包一个包的发生,是有边界的,但可能会丢包和乱序。
7. 分片不同
-
TCP的数据大小如果大于MSS大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装TCP数据报,如果中途丢失了一个分片,只需要传输丢失的这个分片。
-
UDP的数据大小如果大于MTU大小,则会在IP层进行分片,目标主机收到后,在IP层组装完数据,接着再传给传输层。
TCP和UDP应用场景:
由于TCP是面向连接,能保证数据的可靠性交付,因此经常用于:
-
FTP文件传输:是用于在网络上双向传输文件的标准协议,属于应用层协议,核心功能:上传,将本地文件传至远程服务器;下载,从服务器获取文件到本地;目录操作:列出、创建、删除远程目录;文件管理,重命名、删除、设置权限等。原理使用双通道模式(控制连接+数据连接),支持主动/被动模式以适应不同网络环境。缺点是明文传输不安全。
-
HTTP/HTTPS;
由于UDP面向无连接,它可以随时发生数据,再加上UDP本身的处理即简单又高效,因此经常用于:
-
包总量较少的通信,如DNS\SNMP等;
-
视频、音频等多媒体通信;
-
广播通信;
为什么UDP头部没有首部长度字段,而TCP头部有首部长度字段呢?
原因是TCP有可变长的选项字段,而UDP头部长度则是不会变化的,无需多一个字段去记录UDP的首部长度。
为什么UDP头部有包长度字段,而TCP头部则没有包长度字段呢?
TCP数据的长度=IP总长度-IP首部长度-TCP首部长度
其中IP总长度和IP首部长度,在IP首部格式是已知的。TCP首部长度,则是在TCP首部格式已知的,所以就可以求得TCP数据的长度。
而UDP:
因为为了网络设备硬件设计和处理方便,首部长度需要是4字节的整数倍。如果去掉UDP的包长度字段,那UDP首部长度就不是4字节的整数倍了,所以我觉得这可能是为了补全UDP首部长度是4字节的整数倍,才补充了包长度字段。
如今的UDP协议是基于IP协议发展的,而当前可能并非如此,依赖的可能是别的不提供自身报文长度或首部长度的网络层协议,因此UDP报文首部需要有长度字段以供计算。
8. TCP如何保证可靠性
面向连接:发生数据之前需要建立连接,停止发送数据之后需要断开连接
确认应答机制:在接收方收到数据包之后需要回复一个数据包告诉发送方某些数据接收方已经接到了
重传机制:超时重传:发送完数据包之后在规定的时间内没有收到对该数据包的确认应答,发送方会重新传这个包,快重传。
流量控制
拥塞控制
9. TCP和UDP可以使用同一个端口么?
可以的
在数据链路层中,通过MAC地址来寻找局域网中的主机。在网际层中,通过IP地址来寻找网络中互连的主机或路由器。在传输层中,需要通过端口进行寻址,来识别同一计算机中同时通信的不同应用程序。
所以,传输层的端口号的作用,是为了区分同一个主机上不同应用程序的数据包。
传输层有两个传输协议分别是TCP和UDP,在内核中是两个完全独立的软件模块。
当主机收到数据包后,可以在IP包头的协议号字段知道该数据包是TCP/UDP,所以可以根据这个信息确定送给哪个模块(TCP/UDP)处理,送给TCP/UDP模块的报文根据端口号确定送给哪个应用程序处理。
因此TCP/UDP各自的端口号也相互独立,如TCP有一个80号端口,UDP也可以有一个80号端口,二者并不冲突。
10. 如何理解是TCP面向字节流协议?
如何理解字节流?
之所以会说TCP是面向字节流的协议,UDP是面向报文的协议,是因为操作系统对TCP和UDO协议的发送机制不同,也就是问题原因在发送方。
为什么UDP是面向报文的协议?
当用户消息通过UDP协议传输时,操作系统不会对消息进行拆分,在组装好UDP头部后就交给网络层来处理,所以发出去的UDP报文中的数据部分就是完整的用户消息,每个UDP报文就是一个用户消息的边界,这样接收方在接收到UDP报文后,读一个UDP报文就能读取到完整的用户信息。
如果收到两个UDP报文,操作系统是怎么区分开的?
操作系统在收到UDP报文后,会将其插入到队列里,队列里的每一个元素就是一个UDP报文,这样当用户调用recvfrom()系统调用读数据的时候,就会从队列里取出一个数据,然后从内核里拷贝给用户缓冲区。
为什么TCP是面向字节流的协议?
当用户消息通过TCP协议传输时,消息可能会被操作系统分组成多个的TCP报文,也就是一个完整的用户消息被拆分成多个TCP报文进行传输。
这时,接收方的程序如果不知道发送方发送的消息的长度,也就是不知道消息的边界时,是无法读出一个有效的用户消息的,因为用户消息被拆分成多个TCP报文后,并不能像UDP那样,一个UDP报文就能代表一个完整的用户消息。
举个实际的例子来说明
发送方准备发送 「Hi.」和「I am Xiaolin」这两个消息。
在发送端,当我们调用 send 函数完成数据“发送”以后,数据并没有被真正从网络上发送出去,只是从应用程序拷贝到了操作系统内核协议栈中。
至于什么时候真正被发送,取决于发送窗口、拥塞窗口以及当前发送缓冲区的大小等条件。也就是说,我们不能认为每次 send 调用发送的数据,都会作为一个整体完整地消息被发送出去。
如果我们考虑实际网络传输过程中的各种影响,假设发送端陆续调用 send 函数先后发送 「Hi.」和「I am Xiaolin」 报文,那么实际的发送很有可能是这几种情况。
第一种情况,这两个消息被分到同一个 TCP 报文,像这样:
第二种情况,「I am Xiaolin」的部分随 「Hi」 在一个 TCP 报文中发送出去,像这样:
第三种情况,「Hi.」 的一部分随 TCP 报文被发送出去,另一部分和 「I am Xiaolin」 一起随另一个 TCP 报文发送出去,像这样。
类似的情况还能举例很多种,这里主要是想说明,我们不知道 「Hi.」和 「I am Xiaolin」 这两个用户消息是如何进行 TCP 分组传输的。
因此,我们不能认为一个用户消息对应一个 TCP 报文,正因为这样,所以 TCP 是面向字节流的协议。
当两个消息的某个部分内容被分到同一个 TCP 报文时,就是我们常说的 TCP 粘包问题,这时接收方不知道消息的边界的话,是无法读出有效的消息。
要解决这个问题,要交给应用程序。
11. 如何解决粘包?
粘包的问题出现是因为不知道一个用户消息的边界在哪,如果知道了边界在哪,接收方就可以通过边界来划分出有效的用户消息。
一般有三种方式分包:
-
固定长度的消息;
-
特殊字符作为边界;
-
自定义消息结构;
固定长度的消息
这种是最简单方法,即每个用户消息都是固定长度的,比如规定一个消息的长度是 64 个字节,当接收方接满 64 个字节,就认为这个内容是一个完整且有效的消息。
但是这种方式灵活性不高,实际中很少用。
特殊字符作为边界
我们可以在两个用户消息之间插入一个特殊的字符串,这样接收方在接收数据时,读到了这个特殊字符,就把认为已经读完一个完整的消息。
HTTP 是一个非常好的例子。
HTTP 通过设置回车符(\r)、换行符(\n)作为 HTTP 报文协议的边界。
有一点要注意,这个作为边界点的特殊字符,如果刚好消息内容里有这个特殊字符,我们要对这个字符转义,避免被接收方当作消息的边界点而解析到无效的数据。
自定义消息结构?
我们可以自定义一个消息结构,由包头和数据组成,其中包头包是固定大小的,而且包头里有一个字段来说明紧随其后的数据有多大。
比如这个消息结构体,首先 4 个字节大小的变量来表示数据长度,真正的数据则在后面。
struct { u_int32_t message_length; char message_data[];
} message;
当接收方接收到包头的大小(比如 4 个字节)后,就解析包头的内容,于是就可以知道数据的长度,然后接下来就继续读取数据,直到读满数据的长度,就可以组装成一个完整到用户消息来处理了。
TCP连接建立
1. TCP三次握手过程是怎样的?
-
一开始,客户端和服务器都处于close状态。先是服务器主动监听某个端口,处于LISTEN(socket正在监听指定端口)状态
-
客户端会随机初始化序号,将此序号置于TCP首部的序号字段中,同时把SYN标志位置为1,表示SYN报文。接着把第一个SYN报文发送给服务器,表示向服务器发起连接,该报文不包含应用层数据,之后客户端处于SYN-SENT(syn寄出)状态。
-
服务器收到客户端的SYN报文后,首先服务端也随机初始化自己的序号,将此序号填入TCP首部的序号字段中,其次把TCP首部的确认应答号字段填入收到客户端的ISN+1,接着把SYN和ACK标志位置为1。最后把该报文发送给客户端,该报文也不包含应用层数据,之后服务端处于SYN-RCVD(SYN已收到)状态。
-
客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 收到服务器的ISN+1 ,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED (正式成立、建立)状态。
-
服务端收到客户端的应答报文后,也进入ESTABLISHED状态。
从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这也是面试常问的题。
一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
2. 如何在Linux系统中查看TCP状态?
TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。
-
-a:列出所有端口
-
-at:列出所有TCP端口
-
-au:列出所有UDP端口
-
-p:显示正在使用Socket的程序识别码和程序名称
-
-n:显示 IP 地址和端口号的数字格式,而不进行反向解析。
3. 为什么是三次握手?不是两次、四次?
首先TCP连接:用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括 Socket、序列号和窗口大小称为连接。
所以,重要的是为什么三次握手才可以初始化 Socket、序列号和窗口大小并建立 TCP 连接。
三次握手的原因:
-
三次握手才可以阻止重复历史连接的初始化(主要原因)
-
三次握手才可以同步双方的初始序列号
-
三次握手才可以避免资源浪费
原因一:避免历史连接
简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。
我们考虑一个场景,客户端先发送了 SYN(seq = 90)报文,然后客户端宕机了,而且这个 SYN 报文还被网络阻塞了,服务端并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了 SYN(seq = 100)报文(注意!不是重传 SYN,重传的 SYN 的序列号是一样的)。
看看三次握手是如何阻止历史连接的:
客户端连续发送多次 SYN(都是同一个四元组)建立连接的报文,在网络拥堵情况下:
-
一个「旧 SYN 报文(seq = 90)」比「最新的 SYN(seq = 100)」 报文早到达了服务端,那么此时服务端就会回一个 SYN + ACK 报文给客户端,此报文中的确认号是 91(90+1)。
-
客户端收到后,发现自己期望收到的确认号应该是 100 + 1,而不是 90 + 1,于是就会回 RST 报文。
-
服务端收到 RST 报文后,就会释放连接。
-
后续最新的 SYN 抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。
上述中的「旧 SYN 报文」称为历史连接,TCP 使用三次握手建立连接的最主要原因就是防止「历史连接」初始化了连接。
有很多人问,如果服务端在收到 RST 报文之前,先收到了「新 SYN 报文」,也就是服务端收到客户端报文的顺序是:「旧 SYN 报文」->「新 SYN 报文」,此时会发生什么?
当服务端第一次收到 SYN 报文,也就是收到 「旧 SYN 报文」时,就会回复 SYN + ACK 报文给客户端,此报文中的确认号是 91(90+1)。
然后这时再收到「新 SYN 报文」时,就会回 Challenge Ack 报文给客户端,这个 ack 报文并不是确认收到「新 SYN 报文」的,而是上一次的 ack 确认号,也就是91(90+1)。所以客户端收到此 ACK 报文时,发现自己期望收到的确认号应该是 101,而不是 91,于是就会回 RST 报文。
如果是两次握手连接,就无法阻止历史连接,那为什么TCP两次握手无法阻止历史连接呢?
我先直接说结论,主要是因为在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。
你想想,在两次握手的情况下,服务端在收到 SYN 报文后,就进入 ESTABLISHED 状态,意味着这时可以给对方发送数据,但是客户端此时还没有进入 ESTABLISHED 状态,假设这次是历史连接,客户端判断到此次连接为历史连接,那么就会回 RST 报文来断开连接,而服务端在第一次握手的时候就进入 ESTABLISHED 状态,所以它可以发送数据的,但是它并不知道这个是历史连接,它只有在收到 RST 报文后,才会断开连接。
可以看到,如果采用两次握手建立 TCP 连接的场景下,服务端在向客户端发送数据前,并没有阻止掉历史连接,导致服务端建立了一个历史连接,又白白发送了数据,妥妥地浪费了服务端的资源。
因此,要解决这种现象,最好就是在服务端发送数据前,也就是建立连接之前,要阻止掉历史连接,这样就不会造成资源浪费,而要实现这个功能,就需要三次握手。
所以,TCP 使用三次握手建立连接的最主要原因是防止「历史连接」初始化了连接。
客户端发送三次握手(ack 报文)后就可以发送数据了,而被动方此时还是 syn_received 状态,如果 ack 丢了,那客户端发的数据是不是也白白浪费了?
不是的,即使服务端还是在 syn_received 状态,收到了客户端发送的数据,还是可以建立连接的,并且还可以正常收到这个数据包。这是因为数据报文中是有 ack 标识位,也有确认号,这个确认号就是确认收到了第二次握手。如下图:
所以,服务端收到这个数据报文,是可以正常建立连接的,然后就可以正常接收这个数据包了。
原因二:同步双方初始序列号
TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
-
接收方可以去除重复的数据;
-
接收方可以根据数据包的序列号按序接收;
-
可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道);
可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。
而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
原因三:避免资源浪费
如果只有「两次握手」,当客户端发生的 SYN 报文在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK 报文,所以服务端每收到一个 SYN 就只能先主动建立一个连接,这会造成什么情况呢?
如果客户端发送的 SYN 报文在网络中阻塞了,重复发送多次 SYN 报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
即两次握手会造成消息滞留情况下,服务端重复接受无用的连接请求 SYN 报文,而造成重复分配资源。
两次握手不是也可以根据上下文信息丢弃 syn 历史报文吗?
我这里两次握手是假设「由于没有第三次握手,服务端不清楚客户端是否收到了自己发送的建立连接的 ACK 确认报文,所以每收到一个 SYN 就只能先主动建立一个连接」这个场景。
当然你要实现成类似三次握手那样,根据上下文丢弃 syn 历史报文也是可以的,两次握手没有具体的实现,怎么假设都行。
小结
TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
不使用「两次握手」和「四次握手」的原因:
-
「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
-
「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
4. 为什么每次建立TCP连接时,初始化的序列号都要求不一样呢?
主要原因有两个方面:
-
为了防止历史报文被下一个相同四元组的连接接收(主要方面);
-
为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收(比较片面);
过程如下:
-
客户端和服务端建立一个 TCP 连接,在客户端发送数据包被网络阻塞了,然后超时重传了这个数据包,而此时服务端设备断电重启了,之前与客户端建立的连接就消失了,于是在收到客户端的数据包的时候就会发送 RST 报文。
-
紧接着,客户端又与服务端建立了与上一个连接相同四元组的连接;
-
在新连接建立完成后,上一个连接中被网络阻塞的数据包正好抵达了服务端,刚好该数据包的序列号正好是在服务端的接收窗口内,所以该数据包会被服务端正常接收,就会造成数据错乱。
可以看到,如果每次建立连接,客户端和服务端的初始化序列号都是一样的话,很容易出现历史报文被下一个相同四元组的连接接收的问题。
如果每次建立连接客户端和服务端的初始化序列号都「不一样」,就有大概率因为历史报文的序列号「不在」对方接收窗口,从而很大程度上避免了历史报文,比如下图:
相反,如果每次建立连接客户端和服务端的初始化序列号都「一样」,就有大概率遇到历史报文的序列号刚「好在」对方的接收窗口内,从而导致历史报文被新连接成功接收。
所以,每次初始化序列号不一样很大程度上能够避免历史报文被下一个相同四元组的连接接收,注意是很大程度上,并不是完全避免了(因为序列号会有回绕的问题,所以需要用时间戳的机制来判断历史报文。
5. 初始序列号ISN是如何随机产生的?
起始的ISN是基于时钟的,每 4 微秒 + 1,转一圈要 4.55 个小时。
RFC793 提到初始化序列号 ISN 随机生成算法:ISN = M + F(localhost, localport, remotehost, remoteport)。
-
M 是一个计时器,这个计时器每隔 4 微秒加 1。
-
F 是一个 Hash 算法,根据源 IP、源端口、目的 IP、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。
可以看到,随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。
6. 既然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 为单位,而不用重传所有的分片,大大增加了重传的效率。
7. 第一次握手丢失了,会发生什么?
当客户端想和服务端建立 TCP 连接的时候,首先第一个发的就是 SYN 报文,然后进入到 SYN_SENT 状态。
在这之后,如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的。
不同版本的操作系统可能超时时间不同,有的 1 秒的,也有 3 秒的,这个超时时间是写死在内核里的,如果想要更改则需要重新编译内核,比较麻烦。
当客户端在 1 秒后没收到服务端的 SYN-ACK 报文后,客户端就会重发 SYN 报文,那到底重发几次呢?
在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries内核参数控制,这个参数是可以自定义的,默认值一般是 6。
# cat /proc/sys/net/ipv4/tcp_syn_retries //查看当前值
6//默认是6次
通常,第一次超时重传是在 1 秒后,第二次超时重传是在 2 秒,第三次超时重传是在 4 秒后,第四次超时重传是在 8 秒后,第五次是在超时重传 16 秒后。没错,每次超时的时间是上一次的 2 倍。
当第五次超时重传后,会继续等待 32 秒,如果服务端仍然没有回应 ACK,客户端就不再发送 SYN 包,然后断开 TCP 连接。
所以,总耗时是 1+2+4+8+16+32=63 秒,大约 1 分钟左右。
举个例子,假设 tcp_syn_retries 参数值为 3,那么当客户端的 SYN 报文一直在网络中丢失时,会发生下图的过程:
-
当客户端超时重传 3 次 SYN 报文后,由于 tcp_syn_retries 为 3,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK 报文),那么客户端就会断开连接。
8. 第二次握手丢失了,会发生什么?
当服务端收到客户端的第一次握手后,就会回 SYN-ACK 报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD 状态。
第二次握手的 SYN-ACK 报文其实有两个目的 :
-
第二次握手里的 ACK, 是对第一次握手的确认报文;
-
第二次握手里的 SYN,是服务端发起建立 TCP 连接的报文;
所以,如果第二次握手丢了,就会发生比较有意思的事情,具体会怎么样呢?
因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文。
然后,因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了。
那么,如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文。
在 Linux 下,SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定,默认值是 5。
# cat /proc/sys/net/ipv4/tcp_synack_retries
5//默认是5
因此,当第二次握手丢失了,客户端和服务端都会重传:
-
客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由 tcp_syn_retries内核参数决定;
-
服务端会重传 SYN-ACK 报文,也就是第二次握手,最大重传次数由 tcp_synack_retries 内核参数决定。
举个例子,假设 tcp_syn_retries 参数值为 1,tcp_synack_retries 参数值为 2,那么当第二次握手一直丢失时,发生的过程如下图:
-
当客户端超时重传 1 次 SYN 报文后,由于 tcp_syn_retries 为 1,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK 报文),那么客户端就会断开连接。
-
当服务端超时重传 2 次 SYN-ACK 报文后,由于 tcp_synack_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。
9. 第三次握手丢失了,会发生什么?
客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH 状态。
因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。就是重传第二次握手
注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。
举个例子,假设 tcp_synack_retries 参数值为 2,那么当第三次握手一直丢失时,发生的过程如下图:
具体过程:
-
当服务端超时重传 2 次 SYN-ACK 报文后,由于 tcp_synack_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。
10. 什么是SYN攻击?如何避免SYN攻击?
我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。
先跟大家说一下,什么是 TCP 半连接和全连接队列。
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:
-
半连接队列,也称 SYN 队列;
-
全连接队列,也称 accept 队列;
我们先来看下 Linux 内核的 SYN 队列(半连接队列)与 Accpet 队列(全连接队列)是如何工作的?
正常流程:
-
当服务端接收到客户端的 SYN 报文时,会创建一个半连接的对象,然后将其加入到内核的「 SYN 队列」;
-
接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文;
-
服务端接收到 ACK 报文后,从「 SYN 队列」取出一个半连接对象,然后创建一个新的连接对象放入到「 Accept 队列」;
-
应用通过调用 accpet() socket 接口,从「 Accept 队列」取出连接对象。
不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,默认情况都会丢弃报文。
SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。
避免 SYN 攻击方式,可以有以下四种方法:
-
调大 netdev_max_backlog;
-
增大 TCP 半连接队列;
-
开启 tcp_syncookies;
-
减少 SYN+ACK 重传次数
方式一:调大 netdev_max_backlog
当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包(包括SYN报文)。控制该队列的最大值如下参数,默认值是 1000,我们要适当调大该参数的值,比如设置为 10000:
net.core.netdev_max_backlog = 10000
但是这种方式只能够间接缓解,有局限性,不区分报文类型:队列中混合了SYN、正常数据包等其他报文,无法针对性保护。也不能根本解决问题,攻击者仍可以耗尽半连接队列或系统资源。
方式二:增大 TCP 半连接队列
增大 TCP 半连接队列,要同时增大下面这三个参数:
-
增大 net.ipv4.tcp_max_syn_backlog
-
增大 listen() 函数中的 backlog
-
增大 net.core.somaxconn
这三个参数共同控制服务端处理SYN报文的能力,需协同调整以有效防御SYN攻击并提升并发性能
net.ipv4.tcp_max_syn_backlog:作用是定义系统级半连接队列的最大长度,即处于SYN_RCVD状态的连接数上限。通常为128或256
cat /proc/sys/net/ipv4/tcp_max_syn_backlog # 通常为128或256
持续更新......