Linux网络TCP和UDP协议解析
Linux网络TCP和UDP协议解析
- 1. TCP协议解析
- 1.1 TCP数据格式
- 1.2 如何唯一确定一个 TCP 连接呢?
- 1.3 三次握手
- 1.3.1 三次握手过程
- 1.3.2 第一次握手丢失了,会发生什么?
- 1.3.3 第二次握手丢失了,会发生什么?
- 1.3.4 第三次握手丢失了,会发生什么?
- 1.4 四次挥手
- 1.4.1 四次挥手过程
- 1.4.2 第一次挥手丢失了,会发生什么?
- 1.4.3 第二次挥手丢失了,会发生什么?
- 1.4.4 第三次挥手丢失了,会发生什么?
- 1.4.5 第四次挥手丢失了,会发生什么?
- 1.4.6 为什么 TIME_WAIT 等待的时间是 2MSL?
- 1.4.7 为什么需要 TIME_WAIT 状态?
- 1.4.7.1 防止历史连接中的数据,被后面相同四元组的连接错误的接收
- 1.4.7.2 保证「被动关闭连接」的一方,能被正确的关闭
- 1.3 TCP协议传输流程
- 1.3.1 TCP相关数据编号和校验
- 1.3.2 大文件传输
- 1.3.3 窗口滑动机制
- 1.3.4 数据重传和快速重传
- 2. UDP协议解析
- 2.1 UDP传输流程
- 2.2 大文件传输
- 3. 疑问和思考
- 3.1 TCP协议和UDP协议的使用场景差异
- 3.2 TCP和UDP能够监听相同的端口吗?
- 3.2 使用TCP协议是否一定不会丢包?
- 3.3 为什么是三次握手?不是两次、四次?
- 3.3.1 不能使用二次的原因
- 3.3.1.1 避免历史连接
- 3.3.1.2 同步双方初始序列号
- 3.3.1.3 避免资源浪费
- 3.2.2 不使用四次的原因
- 3.4 为什么挥手需要四次?
- 3.5 有什么常见应用是使用UDP进行数据传输?
- 4. 参考文档
TCP(传输控制协议)和UDP(用户数据报协议)是两种基于IP(Internet协议)的传输层协议。
TCP协议是一种面向连接的协议,它提供可靠的数据传输。TCP通过三次握手建立连接,然后进行数据传输,最后通过四次挥手释放连接。TCP保证数据的可靠性,通过序号、确认和重传机制来实现数据的完整性和可靠性。TCP协议适用于需要确保数据完全传输的应用,如网页浏览、文件传输等。
UDP协议是一种无连接的协议,它提供不可靠的数据传输。UDP不需要建立连接,直接发送数据包。UDP不保证数据的可靠性,也不提供重传机制,因此可能导致数据的丢失或乱序。UDP协议适用于实时性要求较高的应用,如音视频传输、实时游戏等。
本文探讨tcp和udp协议的传输流程以及相关重点事项,并比较两者的异同。
TCP(传输控制协议)和UDP(用户数据报协议)是两种基于IP(Internet协议)的传输层协议。
- TCP协议是一种面向连接的协议,它提供可靠的数据传输。TCP通过三次握手建立连接,然后进行数据传输,最后通过四次挥手释放连接。TCP保证数据的可靠性,通过序号、确认和重传机制来实现数据的完整性和可靠性。TCP协议适用于需要确保数据完全传输的应用,如网页浏览、文件传输等。
- UDP协议是一种无连接的协议,它提供不可靠的数据传输。UDP不需要建立连接,直接发送数据包。UDP不保证数据的可靠性,也不提供重传机制,因此可能导致数据的丢失或乱序。UDP协议适用于实时性要求较高的应用,如音视频传输、实时游戏等。
在Linux系统中,TCP和UDP协议都可以通过套接字(socket)接口来使用。用户可以通过编程调用相应的系统调用,如socket()、bind()、listen()、accept()等来创建和管理套接字,并使用send()、recv()等函数来发送和接收数据。
1. TCP协议解析
1.1 TCP数据格式
-
序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
-
确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
-
控制位:
- ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。
- RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
- SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
- FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。
1.2 如何唯一确定一个 TCP 连接呢?
TCP 四元组可以唯一的确定一个连接,四元组包括如下:
- 源地址
- 源端口
- 目的地址
- 目的端口
实际上,在网络层面进行数据传输,依赖于5元组,分别是协议、源地址、源端口、目的地址、目的端口
- 源地址和目的地址的字段(32 位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机。
- 源端口和目的端口的字段(16 位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。
有一个 IP 的服务端监听了一个端口,它的 TCP 的最大连接数是多少?服务端通常固定在某个本地端口上监听,等待客户端的连接请求。
因此,客户端 IP 和端口是可变的,单台服务器上监听的服务,其理论值计算公式如下:
最大 T C P 链接数 = 客户端的 I P 数 ∗ 客户端端口数 最大TCP链接数 = 客户端的IP数 * 客户端端口数 最大TCP链接数=客户端的IP数∗客户端端口数
对 IPv4,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是服务端单机最大 TCP 连接数,约为 2 的 48 次方。
当然,服务端最大并发 TCP 连接数远不能达到理论上限,会受以下因素影响:
- 文件描述符限制,每个 TCP 连接都是一个文件,如果文件描述符被占满了,会发生 Too many open files。Linux 对可打开的文件描述符的数量分别作了三个方面的限制:
- 系统级:当前系统可打开的最大数量,通过 cat /proc/sys/fs/file-max 查看;
- 用户级:指定用户可打开的最大数量,通过 cat /etc/security/limits.conf 查看;
- 进程级:单个进程可打开的最大数量,通过 cat /proc/sys/fs/nr_open 查看;
- 内存限制,每个 TCP 连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会发生 OOM。
1.3 三次握手
1.3.1 三次握手过程
TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手来进行的。三次握手的过程如下图:
3次握手过程的服务端和客户端的相关状态机
握手次数 | 发送状态 | 客户端状态机 | 服务端状态机 | 备注 |
---|---|---|---|---|
尚未握手 | 无 | CLOSE | Listen | 握手尚未开始,服务端监听相关服务,等待客户端连接 |
一次握手 | SYN | SYNC_SEND | SYNC_RCVD | 客户端发送SYN请求给服务端,服务端收到了请求包后状态机转为SYNC_RCVD |
二次握手 | SYN + ACK | ESTABLISHED | SYNC_RCVD | 客户端收到SYN + ACK状态机变成ESTABLISHED,可以发送数据,服务端状态机为SYNC_RCVD |
三次握手 | ACK | ESTABLISHED | ESTABLISHED | 服务端收到ACK后状态机变成ESTABLISHED,服务端可以开始发送数据 |
从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的。一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
我们使用如下代码模拟一个完整的tcp链接,并抓取对应的报文进行简单分析。
- 使用如下python脚本listen.py本地监听9011端口
# Filename: listen.py
import threading
import socket
encoding = 'utf-8'
BUFSIZE = 1024
# a read thread, read data from remote
class Reader(threading.Thread):
def __init__(self, client):
threading.Thread.__init__(self)
self.client = client
def run(self):
while True:
data = self.client.recv(BUFSIZE)
if(data):
string = bytes.decode(data, encoding)
print(string, end='')
else:
break
print("close:", self.client.getpeername())
def readline(self):
rec = self.inputs.readline()
if rec:
string = bytes.decode(rec, encoding)
if len(string)>2:
string = string[0:-2]
else:
string = ' '
else:
string = False
return string
# a listen thread, listen remote connect
# when a remote machine request to connect, it will create a read thread to handle
class Listener(threading.Thread):
def __init__(self, port):
threading.Thread.__init__(self)
self.port = port
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.bind(("0.0.0.0", port))
self.sock.listen(0)
def run(self):
print("listener started")
while True:
client, cltadd = self.sock.accept()
Reader(client).start()
cltadd = cltadd
print("accept a connect")
lst = Listener(19011) # create a listen thread
lst.start() # then start
执行python listen.py
执行脚本,并完成9011端口的监听
2. 另外起一个控制台,sudo tcpdump port 19011 -w 19011.pcap
,开始抓包
3. 另外再起一个控制台,执行telnet localhost 19011
命令,并在控制台上随意输入几个字符,作为数据传输
4. 使用wireshark打开抓取的包19011.pcap,并分析相关报文
三次握手情况如下
完成3次握手,并初始化客户端、服务端的seq,分别是1和1
简单数据传输报文情况解析
编号 | 客户端(43762)seq | 客户端(43762)ack | 服务端(19001)seq | 服务端(19001)ack | 备注 |
---|---|---|---|---|---|
6 | 1 | 1 | - | - | 客户端表示,我要开始传输数据了,seq=1,ack=1(要求回复的报文seq=1),len=3 |
7 | - | - | 1 | 4 | 服务端端表示,我接受到数据了,seq=1(回复报文6的ack=1要求),ack=4(通过报文6的seq+len=4推算出,要求回复的报文seq=4),len=0 |
8 | 4 | 1 | - | - | 客户端表示,我要开始传输数据了,seq=4(回复报文7的ack=4要求),ack=1(要求回复的报文seq=1),len=3 |
9 | - | - | 1 | 7 | 服务端端表示,我接受到数据了,seq=1(回复报文8的ack=1要求),ack=7(通过报文8的seq+len=7推算出,要求回复的报文seq=7),len=0 |
后续报文如此类推。
1.3.2 第一次握手丢失了,会发生什么?
当客户端想和服务端建立 TCP 连接的时候,首先第一个发的就是 SYN 报文,然后进入到 SYN_SENT 状态。
在这之后,如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的。
不同版本的操作系统可能超时时间不同,有的 1 秒的,也有 3 秒的,这个超时时间是写死在内核里的,如果想要更改则需要重新编译内核,比较麻烦。
当客户端在 1 秒后没收到服务端的 SYN-ACK 报文后,客户端就会重发 SYN 报文,那到底重发几次呢?
在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries内核参数控制,这个参数是可以自定义的,默认值一般是 5。
cat /proc/sys/net/ipv4/tcp_syn_retries
5
通常,第一次超时重传是在 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 报文),那么客户端就会断开连接。
1.3.3 第二次握手丢失了,会发生什么?
当服务端收到客户端的第一次握手后,就会回 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_syn_retries
5
因此,当第二次握手丢失了,客户端和服务端都会重传:
-
客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由 tcp_syn_retries内核参数决定;
-
服务端会重传 SYN-ACK 报文,也就是第二次握手,最大重传次数由 tcp_synack_retries 内核参数决定。
具体过程: -
当客户端超时重传 1 次 SYN 报文后,由于 tcp_syn_retries 为 1,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK 报文),那么客户端就会断开连接。
-
当服务端超时重传 2 次 SYN-ACK 报文后,由于 tcp_synack_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。
1.3.4 第三次握手丢失了,会发生什么?
客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH 状态,可以发送报文。
因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。
注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。
举个例子,假设 tcp_synack_retries 参数值为 2,那么当第三次握手一直丢失时,发生的过程如下图:
具体过程:
当服务端超时重传 2 次 SYN-ACK 报文后,由于 tcp_synack_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。
1.4 四次挥手
1.4.1 四次挥手过程
- 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
- 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSE_WAIT 状态。
- 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
- 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
- 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
- 服务端收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
- 客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。
你可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。
这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态
挥手次数 | 发送状态 | 客户端状态机 | 服务端状态机 | 备注 |
---|---|---|---|---|
尚未挥手 | 无 | ESTABLISHED | ESTABLISHED | |
一次挥手 | FIN | FIN_WAIT1 | ESTABLISHED | 客户端发送FIN请求给服务端,表示客户端不再发送数据了,但是还能接收数据。 |
二次挥手 | ACK | FIN_WAIT1 | CLOSED_WAIT | 服务端发送ACK,并进行内部处理 |
三次挥手 | FIN | FIN_WAIT2 | LAST_ACK | 服务端完成内部数据处理,服务端发FIN,服务端不在发送数据,同意关闭链接 |
四次挥手 | ACK | TIME_WAIT | LAST_ACK | 客户端收到服务端的FIN,并进行回复确认,链接关闭 |
1.4.2 第一次挥手丢失了,会发生什么?
当客户端(主动关闭方)调用 close 函数后,就会向服务端发送 FIN 报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1 状态。
正常情况下,如果能及时收到服务端(被动关闭方)的 ACK,则会很快变为 FIN_WAIT2状态。
如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由 tcp_orphan_retries 参数控制。
当客户端重传 FIN 报文的次数超过 tcp_orphan_retries 后,就不再发送 FIN 报文,则会在等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到第二次挥手,那么直接进入到 close 状态。
具体过程:
- 当客户端超时重传 3 次 FIN 报文后,由于 tcp_orphan_retries 为 3,已达到最大重传次数
- 再等待一段时间(时间为上一次超时时间的 2 倍)
- 如果还是没能收到服务端的第二次挥手(ACK报文),那么客户端就会断开连接。
1.4.3 第二次挥手丢失了,会发生什么?
当服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入到 CLOSE_WAIT 状态。
在前面我们也提了,ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。
举个例子,假设 tcp_orphan_retries 参数值为 2,当第二次挥手一直丢失时,发生的过程如下图:
具体过程:
- 当客户端超时重传 2 次 FIN 报文后,由于 tcp_orphan_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次挥手(ACK 报文),那么客户端就会断开连接。
这里提一下,当客户端收到第二次挥手,也就是收到服务端发送的 ACK 报文后,客户端就会处于 FIN_WAIT2 状态,在这个状态需要等服务端发送第三次挥手,也就是服务端的 FIN 报文。
对于 close 函数关闭的连接,由于无法再发送和接收数据,所以FIN_WAIT2 状态不可以持续太久,而 tcp_fin_timeout 控制了这个状态下连接的持续时长,默认值是 60 秒。
这意味着对于调用 close 关闭的连接,如果在 60 秒后还没有收到 FIN 报文,客户端(主动关闭方)的连接就会直接关闭,如下图:
但是注意,如果主动关闭方使用 shutdown 函数关闭连接,指定了只关闭发送方向,而接收方向并没有关闭,那么意味着主动关闭方还是可以接收数据的。
此时,如果主动关闭方一直没收到第三次挥手,那么主动关闭方的连接将会一直处于 FIN_WAIT2 状态(tcp_fin_timeout 无法控制 shutdown 关闭的连接)。如下图:
1.4.4 第三次挥手丢失了,会发生什么?
当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT 状态,顾名思义,它表示等待应用进程调用 close 函数关闭连接。
此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。
服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。
如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。
举个例子,假设 tcp_orphan_retries = 3,当第三次挥手一直丢失时,发生的过程如下图:
具体过程:
- 当服务端重传第三次挥手报文的次数达到了 3 次后,由于 tcp_orphan_retries 为 3,达到了重传最大次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第四次挥手(ACK报文),那么服务端就会断开连接。
- 客户端因为是通过 close 函数关闭连接的,处于 FIN_WAIT_2 状态是有时长限制的,如果 tcp_fin_timeout 时间内还是没能收到服务端的第三次挥手(FIN 报文),那么客户端就会断开连接。
1.4.5 第四次挥手丢失了,会发生什么?
当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT 状态。
在 Linux 系统,TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。
然后,服务端(被动关闭方)没有收到 ACK 报文前,还是处于 LAST_ACK 状态。
如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。
举个例子,假设 tcp_orphan_retries 为 2,当第四次挥手一直丢失时,发生的过程如下:
具体过程:
- 当服务端重传第三次挥手报文达到 2 时,由于 tcp_orphan_retries 为 2, 达到了最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第四次挥手(ACK 报文),那么服务端就会断开连接。
- 客户端在收到第三次挥手后,就会进入 TIME_WAIT 状态,开启时长为 2MSL 的定时器,如果途中再次收到第三次挥手(FIN 报文)后,就会重置定时器,当等待 2MSL 时长后,客户端就会断开连接。
1.4.6 为什么 TIME_WAIT 等待的时间是 2MSL?
MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。
MSL 与 TTL 的区别: MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。
TTL 的值一般是 64,Linux 将 MSL 设置为 30 秒,意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒,如果超过了,就认为报文已经消失在网络中了。
TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
比如,如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 FIN 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。
可以看到 2MSL时长 这其实是相当于至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对。
1.4.7 为什么需要 TIME_WAIT 状态?
主动发起关闭连接的一方,才会有 TIME-WAIT 状态。需要 TIME-WAIT 状态,主要是两个原因:
- 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
- 保证「被动关闭连接」的一方,能被正确的关闭;
1.4.7.1 防止历史连接中的数据,被后面相同四元组的连接错误的接收
序列号和初始化序列号并不是无限递增的,会发生回绕为初始值的情况,这意味着无法根据序列号来判断新老数据。假设 TIME-WAIT 没有等待时间或时间过短,被延迟的数据包抵达后会发生什么呢?
如上图:
- 服务端在关闭连接之前发送的 SEQ = 301 报文,被网络延迟了。
- 接着,服务端以相同的四元组重新打开了新连接,前面被延迟的 SEQ = 301 这时抵达了客户端,而且该数据报文的序列号刚好在客户端接收窗口内,因此客户端会正常接收这个数据报文,但是这个数据报文是上一个连接残留下来的,这样就产生数据错乱等严重的问题。
为了防止历史连接中的数据,被后面相同四元组的连接错误的接收,因此 TCP 设计了 TIME_WAIT 状态,状态会持续 2MSL 时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
1.4.7.2 保证「被动关闭连接」的一方,能被正确的关闭
也就是说,TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。
如果客户端(主动关闭方)最后一次 ACK 报文(第四次挥手)在网络中丢失了,那么按照 TCP 可靠性原则,服务端(被动关闭方)会重发 FIN 报文。
假设客户端没有 TIME_WAIT 状态,而是在发完最后一次回 ACK 报文就直接进入 CLOSE 状态,如果该 ACK 报文丢失了,服务端则重传的 FIN 报文,而这时客户端已经进入到关闭状态了,在收到服务端重传的 FIN 报文后,就会回 RST 报文。
服务端收到这个 RST 并将其解释为一个错误(Connection reset by peer),这对于一个可靠的协议来说不是一个优雅的终止方式。
为了防止这种情况出现,客户端必须等待足够长的时间,确保服务端能够收到 ACK,如果服务端没有收到 ACK,那么就会触发 TCP 重传机制,服务端会重新发送一个 FIN,这样一去一来刚好两个 MSL 的时间。
TIME-WAIT 时间正常,确保了连接正常关闭。客户端在收到服务端重传的 FIN 报文时,TIME_WAIT 状态的等待时间,会重置回 2MSL。
1.3 TCP协议传输流程
1.3.1 TCP相关数据编号和校验
在Linux操作系统中,TCP校验码用于验证TCP报文的完整性和准确性。TCP校验码是在TCP报文的头部中的一个字段,由发送端计算得出,并在接收端进行校验。
计算TCP校验码的方法是将TCP报文的伪首部、TCP报文头部和TCP数据作为输入,通过CRC(循环冗余校验)算法计算得出。伪首部包括源IP地址、目的IP地址、协议类型(TCP)、数据长度等信息,它的目的是为了提高校验的准确性。然后,将伪首部、TCP报文头部和TCP数据按照字节进行计算,得出16位的校验码。
在Linux中,TCP校验码的计算和校验是由内核自动完成的,应用程序无需关心具体的计算过程。当发送TCP报文时,内核会自动计算校验码并填充到TCP报文头部中的校验码字段中。当接收到TCP报文时,内核会自动进行校验,如果校验失败,则丢弃该报文。
总结起来,Linux中的TCP校验码是通过CRC算法计算得出的,用于验证TCP报文的完整性和准确性。计算和校验工作由内核自动完成,应用程序无需关心具体的计算过程。
1.3.2 大文件传输
在Linux系统中,TCP协议单个包的大小限制由两个因素决定:MTU(Maximum Transmission Unit,最大传输单元)和MSS(Maximum Segment Size,最大分段大小)。
MTU是网络链路层(如以太网)规定的最大传输单元大小,通常为1500字节。如果数据包大小超过MTU,数据包会被分片传输。
MSS是TCP协议中定义的最大分段大小,它是指TCP报文段中TCP数据部分的最大长度。MSS的大小由TCP连接的两端协商确定,一般默认为MTU减去TCP/IP头部的长度(40字节),即1460字节。
因此,在Linux系统中,单个TCP包的大小限制通常为MSS(1460字节)
1.3.3 窗口滑动机制
TCP滑动窗口机制是一种用来控制数据流动的机制。它通过动态调整发送方和接收方之间的数据传输速率,以便在可靠的网络中实现高效的数据传输。
在TCP通信过程中,发送方和接收方都有一个滑动窗口。发送方的滑动窗口表示发送方能够接受的数据量,而接收方的滑动窗口表示接收方能够处理的数据量。
当发送方发送数据时,它会将数据分成一个个的TCP段,并将它们放入滑动窗口中进行发送。接收方收到数据后,会根据滑动窗口的大小进行确认,并告诉发送方可以发送更多的数据。
滑动窗口的大小会根据网络状况进行动态调整。当网络拥塞时,滑动窗口的大小会减小,以避免丢失和重传的情况发生。而当网络畅通时,滑动窗口的大小会增大,以提高数据传输的效率。
通过滑动窗口机制,TCP可以根据网络状况自适应地调整数据的传输速率,从而在不同的网络环境下实现高效的数据传输。
1.3.4 数据重传和快速重传
TCP协议是一种可靠的传输协议,它通过重传机制确保数据可靠地传输。当发送方发送一个数据段后,它会等待接收到该数据段的确认(ACK),如果在超时时间内没有收到确认,发送方会认为数据丢失,触发重传机制。
TCP的重传机制包括以下几个步骤:
- 发送方发送数据段,并启动计时器等待接收确认;
- 如果计时器超时,发送方认为数据丢失,触发重传;
- 发送方重新发送丢失的数据段,并重新启动计时器;
- 接收方收到重传的数据段后,发送确认(ACK);
- 发送方收到确认后,停止计时器。
快速重传是一种针对丢失数据段的优化机制。
- 当接收方收到一个乱序的数据段时,它会发送一个重复的确认(DUP-ACK)给发送方,告诉发送方接收到了乱序的数据段。
- 发送方收到三个连续的重复确认后,就知道有数据段丢失了,触发快速重传。
- 发送方会立即重传丢失的数据段,而不需要等待计时器超时。
- 快速重传的主要优势是可以更早地发现和修复网络中的问题,减少了等待计时器超时的时间延迟。这样可以提高传输的效率和速度。
总结起来,TCP的重传机制通过计时器超时来检测丢失的数据段,并进行重传。而快速重传是一种优化机制,通过接收方发送重复确认来触发发送方的快速重传,减少等待计时器超时的时间延迟。
2. UDP协议解析
2.1 UDP传输流程
使用UDP进行通信,服务器和客户端的处理步骤比TCP要简单很多,并且两端是对等的 (通信的处理流程几乎是一样的),也就是说并没有严格意义上的客户端和服务器端。UDP的通信流程如下:
udp协议不需要建立可靠链接,因此不需要类似tcp进行三次握手。
2.2 大文件传输
在Linux中,单个UDP包的大小限制是由操作系统的网络层协议栈决定的。默认情况下,Linux的UDP包大小限制为64KB。这个限制主要是由于IPv4和IPv6协议头的大小限制以及内核对UDP数据报的内存分配策略所决定的。
然而,可以通过修改操作系统内核的配置参数来改变UDP包的大小限制。在/sys/class/net//mtu文件中可以查看和修改网络接口的最大传输单元(MTU)值,这个值决定了UDP包的最大大小。一般情况下,将MTU设置为大于64KB的值是不常见的,因为这可能会导致网络性能下降和数据丢失。
需要注意的是,即使操作系统允许发送和接收大于64KB的UDP包,但网络设备(如路由器、交换机等)也有其自身的MTU限制,如果超过该限制,UDP包可能会被分片或丢弃。因此,在进行网络应用开发时,需要综合考虑网络设备和操作系统的UDP包大小限制。
3. 疑问和思考
3.1 TCP协议和UDP协议的使用场景差异
3.2 TCP和UDP能够监听相同的端口吗?
可以。
3.2 使用TCP协议是否一定不会丢包?
tcp协议本身保证了数据的可靠传输,但是应用使用tcp协议进行数据传输,却并不能保证一定不丢包。
理由是
- 在网络很差时,如果客户端发送数据始终不能重传到服务端,超过重传次数后,会主动端开链接,相关的处理流程需要由客户端进行处理。由于客户端的缓冲队列有限,如果积压的数据过多,会导致相关数据在缓冲队列丢失。
- 同理,在服务端层面也会有相同的问题,服务端的缓存队列长度有限,如果数据不断的重传或者数据积压,有可能会导致缓冲队列超出,从而导致数据丢包。
3.3 为什么是三次握手?不是两次、四次?
答案:可以的。
在数据链路层中,通过 MAC 地址来寻找局域网中的主机。在网际层中,通过 IP 地址来寻找网络中互连的主机或路由器。在传输层中,需要通过端口进行寻址,来识别同一计算机中同时通信的不同应用程序。
所以,传输层的「端口号」的作用,是为了区分同一个主机上不同应用程序的数据包。
传输层有两个传输协议分别是 TCP 和 UDP,在内核中是两个完全独立的软件模块。
3.3.1 不能使用二次的原因
相信大家比较常回答的是:“因为三次握手才能保证双方具有接收和发送的能力。”
这回答是没问题,但这回答是片面的,并没有说出主要的原因。
在前面我们知道了什么是 TCP 连接:
用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括 Socket、序列号和窗口大小称为连接。
所以,重要的是为什么三次握手才可以初始化 Socket、序列号和窗口大小并建立 TCP 连接。
接下来,以三个方面分析三次握手的原因:
- 三次握手才可以阻止重复历史连接的初始化(主要原因)
- 三次握手才可以同步双方的初始序列号
- 三次握手才可以避免资源浪费
3.3.1.1 避免历史连接
我们来看看 RFC 793 指出的 TCP 连接使用三次握手的首要原因:
The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.
简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。重点说明一下,如何避免历史连接。
我们考虑一个场景,客户端先发送了 SYN(seq = 90,期望接受90+1)报文,然后客户端宕机了,而且这个 SYN 报文还被网络阻塞了,服务端并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了 SYN(seq = 100,期望接受100+1)报文(注意!不是重传 SYN,重传的 SYN 的序列号是一样的)。
看看三次握手是如何阻止历史连接的:
客户端连续发送多次 SYN(都是同一个四元组)建立连接的报文,在网络拥堵情况下:
- 一个「旧 SYN 报文」比「最新的 SYN」 报文早到达了服务端,那么此时服务端就会回一个 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 (opens new window)报文给客户端,这个 ack 报文并不是确认收到「新 SYN 报文」的,而是上一次的 ack 确认号,也就是91(90+1)。所以客户端收到此 ACK 报文时,发现自己期望收到的确认号应该是 101,而不是 91,于是就会回 RST 报文。
我们在说明二次握手为什么不能达到这样的效果
主要是因为在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。
你想想,在两次握手的情况下,服务端在收到 SYN 报文后,就进入 ESTABLISHED 状态,意味着这时可以给对方发送数据,但是客户端此时还没有进入 ESTABLISHED 状态。
- 假设这次是历史连接,客户端判断到此次连接为历史连接,那么就会回 RST 报文来断开连接
- 而服务端在第一次握手的时候就进入 ESTABLISHED 状态,所以它可以发送数据的,但是它并不知道这个是历史连接,它只有在收到 RST 报文后,才会断开连接。虽然最后也能够通过RST重建建立连接,但是由于给力历史连接发送数据,因此造成了资源的浪费,是不必要的。
可以看到,如果采用两次握手建立 TCP 连接的场景下,服务端在向客户端发送数据前,并没有阻止掉历史连接,导致服务端建立了一个历史连接,又白白发送了数据,妥妥地浪费了服务端的资源。
因此,要解决这种现象,最好就是在服务端发送数据前,也就是建立连接之前,要阻止掉历史连接,这样就不会造成资源浪费,而要实现这个功能,就需要三次握手。所以,TCP 使用三次握手建立连接的最主要原因是防止「历史连接」初始化了连接。
3.3.1.2 同步双方初始序列号
TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
- 接收方可以去除重复的数据;
- 接收方可以根据数据包的序列号按序接收;
- 可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道);
可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。
而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
3.3.1.3 避免资源浪费
如果只有「两次握手」,当客户端发生的 SYN 报文在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK 报文,所以服务端每收到一个 SYN 就只能先主动建立一个连接,这会造成什么情况呢?
如果客户端发送的 SYN 报文在网络中阻塞了,重复发送多次 SYN 报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
即两次握手会造成消息滞留情况下,服务端重复接受无用的连接请求 SYN 报文,而造成重复分配资源。
3.2.2 不使用四次的原因
三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
所以总结起来
- 「不能使用两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
- 「不使用四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
3.4 为什么挥手需要四次?
再来回顾下四次挥手双方发 FIN 包的过程,就能理解为什么需要四次了。
- 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
- 服务端收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,因此是需要四次挥手。
但是在特定情况下,四次挥手是可以变成三次挥手的,具体情况可以看这篇:TCP 四次挥手,可以变成三次吗?(opens new window)
3.5 有什么常见应用是使用UDP进行数据传输?
常用的UDP协议的服务有:
- DNS(Domain Name System):将域名解析为IP地址(DNS常规域名使用udp进行解析 。对于大多数的查询,DNS会使用UDP进行传输,而对于特殊情况下的查询,如查询结果过大或者查询过程中出现问题,DNS会使用TCP进行传输。)
- DHCP(Dynamic Host Configuration Protocol):自动分配IP地址、子网掩码、网关等网络配置信息。
- TFTP(Trivial File Transfer Protocol):简单文件传输协议,用于通过UDP快速传输小文件。
- SNMP(Simple Network Management Protocol):简单网络管理协议,用于网络设备的监控和管理。
- NTP(Network Time Protocol):网络时间协议,用于同步网络中各个设备的时间。
- RIP(Routing Information Protocol):路由信息协议,用于在互联网中的路由器之间交换路由信息。
- Syslog:日志传输协议,用于将设备产生的日志信息发送到指定的日志服务器。
- BOOTP(Bootstrap Protocol):引导协议,用于向网络中的主机分配IP地址和其他配置信息。
- NFS(Network File System):网络文件系统,用于在网络上共享文件和目录。
- RTP(Real-time Transport Protocol):实时传输协议,用于音视频流媒体的传输。
这些服务使用UDP协议进行通信,UDP协议是无连接的、不可靠的传输协议,适用于一些对数据传输延迟要求较高、对数据可靠性要求较低的应用场景。
4. 参考文档
暂无