程序员阿沛
发布于 2026-06-27 / 0 阅读
0
0

计算机网络基础五传输层之UDP协议和TCP协议概述和报文格式

计算机网络基础(五) 传输层之UDP协议和TCP协议概述和报文格式

上一节我们介绍了网络层如何以接力的方式靠路由器转发网络包,使其在多个网络之间传递最终到达目标主机。这一节我们开始介绍传输层,它主要有UDP和TCP两种传输协议,传输层的章节将分为3节。第一节对传输层的作用作出概述,并简单介绍UDP和TCP协议;后二节介绍TCP如何实现可靠传输,包括但不限于三次握手四次挥手、确认应答、超时重传、滑动窗口、流量控制和拥塞控制等概念。

01 传输层概述

传输层的基本功能

1. 传输层协议(也叫运输层协议)为运行在不同主机上的应用进程之间提供了逻辑通信功能,使得运行在不同主机的进程像直连一样。

2. 复用和分解。

3. 差错检验。

什么是复用和分解,差错检验是对什么进行差错检验,这些后面会有详细介绍。

运输层把从应用层——具体是运行在应用层的用户进程,所接收到的报文划分为一个个更小的数据段,交给网络层传输。

我们已知运输层有UDP(用户数据报协议)和TCP(传输控制协议)2种传输协议,其中UDP协议的通信是无连接不可靠的,而TCP协议的通信是有连接且可靠的,应用程序在创建套接字时要指定使用哪种运输层协议。

多路复用和多路分解(即进程到进程的数据交付)和差错检查是两个最低限度的运输层服务,也是UDP能提供的仅有的两种服务。

多路复用和多路分解

运输层从协议栈底层(也就是网络层)接收到数据后是不会直接交付到用户进程的,而是交给介于协议栈和进程之间的套接字。

在接收端,将运输层报文段中的数据交付给正确的套接字就是 多路分解 。

在发送端,运输层从不同套接字收集来自用户进程的数据块并交付给网络层就是 多路复用 。

复用强调的是多个用户进程能够复用相同的运输层协议,例如不同进程的UDP套接字的发送缓冲区数据能被收集后统一交给运输层的UDP协议处理和封装。

运输层和网络层的关系

运输层为不同主机的应用程序提供了逻辑通信和数据交付服务,网络层为主机和主机提供了逻辑通信和数据交付服务。说人话就是,网络层负责数据包正确的从主机A交付给主机B,而运输层负责将数据包正确的从主机A的A进程交付给主机B的B进程。

运输层是基于网络层的服务的,只有实现了主机间逻辑通信才能再此基础上实现端到端进程间通信;

运输层是网络层的功能扩展,网络层不能保证数据的可靠传输,而运输层则扩展了这个功能。

总结就是:网络层是运输层的基础和服务者,运输层是网络层的扩展,其实协议栈的每个上下层都是这个关系。

端口号

运输层用一个16位的端口号标志一个端口,作用是标志本应用层各个进程和运输层交互的层间接口,一个端口也是本机某个进程的标识。说白了,端口就是用户进程(应用层)与运输层之间通信的接口。

端口号分为服务端使用端口和客户端使用端口:

1. 服务端使用端口

A. 熟知端口(0~1023号端口)

作为指定用途的端口(如80是Web服务器端口),不会随机对口进行分配。

B. 登记端口(1024~49151)

可作为服务器进程被随机分配的端口。

2.客户端使用的端口(49152~65535)

客户端在与对端连接通信时动态选择,连接关闭后这些端口就关闭,被系统收回,因此又叫短暂端口。

无连接下的多路分解——UDP通信的代码实现

介绍无连接下的多路分解其实就是介绍无连接下运输层如何将数据交付给正确的UDP套接字完成通信。

要介绍这一点,我们得看一下UDP的套接字编程:

# 服务端代码import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

使用IPv4和UDP运输协议s.bind((“127.0.0.1”, 8081))

while True: msg, addr = s.recvfrom(2048)

无需接收连接,直接接收消息 print(“addr: %s:%s; msg: %s” % (addr[0], addr[1], msg.decode())) s.sendto(“OK”.encode(), addr)

s.close()

# 客户端代码import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

使用IPv4和UDP运输协议addr = (“127.0.0.1”, 8081)

while True: data = input("enter something: ") if not data or data == “quit”: break s.sendto(data.encode(), addr)

直接消息,无需建立连接,由于没有和服务端交换控制信息,所以每次发送消息都要告诉协议栈目标IP和端口 msg, _ = s.recvfrom(2048) print(msg.decode())

s.close()

1. 无论是客户端还是服务端,都需要先通过socket()创建一个UDP套接字,协议栈运输层会为这个套接字分配一个1024~65535的未被占用的端口。

0~1024是有特定用途的端口,系统不会随意自动分配(如80、21、25等端口)。

应用程序也可以通过bind()方法为这个UDP套接字主动指定一个特定端口。套接字对象会对应一个能代表套接字标识的文件描述符fd。

2. 内核把(IP,端口)作为成员属性写到这个套接字对象中。并且内核会保存(IP,端口)与套接字文件描述符fd的映射关系。

3. 当主机A(A可能是发送端或者接收端)接收到报文段,该报文段如何交付给正确的套接字进而交付给正确的进程呢?

答案是通过报文段头部的目标IP和端口,从映射表找到对应的fd,再找到对应的套接字对象。

报文段的数据会拷贝到该套接字的接收缓冲区完成交付。

用户进程从套接字的缓冲区把数据拷贝到用户进程的空间即可。

另外看到上面这段代码我们了解到,UDP无需建立连接就可以recvfrom()接收消息,说明对于UDP通信而言,接收端能够接收任何一个发送端的消息。而TCP的一端只能接收到建立了连接的另一端的消息。

因此UDP是一个一对多通信,而TCP是一对一通信。

面向连接的多路分解——TCP通信的代码实现

刚刚我们通过代码实现介绍了UDP通信下,数据交付的过程和特点。现在介绍TCP通信下,运输层如何将数据交付给正确的TCP套接字。

要介绍这一点,我们得看一下TCP的套接字编程:

# 服务端代码
# 创建套接字server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定ip和端口ip = "127.0.0.1"port = 8000server.bind((ip, port))
# 监听套接字server.listen()

print(“服务已开启”)
def contact(client): print(“客户端 %s 已成功连接” % currentThread().name)
msg = client.recv(1024).decode(“utf-8”)

接收客户端发送到服务端的消息,这里也会收到阻塞 while msg:

允许接收客户端发送多次消息,如果对方发送空字符,则认为客户端断开连接,此时结束该线程 print(“客户端 %s 发送信息:%s” % (currentThread().name, msg)) msg = client.recv(1024).decode(“utf-8”)

print("客户端 %s 断开连接" % currentThread().name)

while True: print(“等待接收客户端连接”) client,addr = server.accept()

接受连接, 这里会受到阻塞

创建线程用于客户端和服务端通信 thread = Thread(target=contact, args=(client,)) thread.start()

# 客户端代码
# 创建套接字client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定ip和端口addr = ("127.0.0.1", 8000)

client.connect(addr)

while True: msg = input() if msg: client.send(msg.encode(“utf-8”)) else:

如果直接输入换行则断开连接 client.close() break

1.
对于TCP通信,TCP服务端中有2种TCP套接字,一种是负责接收连接的TCP套接字(代码中的server),它绑定了本机的IP和端口(8000端口),当客户端发送
连接请求 报文,运输层可以通过报文的目标IP和端口找到该套接字,实现报文段交付,然后客户端和服务端的连接就建立成功了。

这种只用于接收TCP连接的套接字称为server套接字。

2. 另一种套接字是建立连接后协议栈拷贝server套接字生成的client套接字,它专门用于和对应的客户端进行收发数据的工作。

client套接字包含了(源IP, 源端口, 目标IP,
目标端口)信息,内核会保存(源IP端口,目标IP端口)与client套接字的文件描述符fd之间的映射关系。

接收端接收到报文段后可以根据报文头部的源IP/端口和目标IP/端口(其实根据源IP端口+目标端口这3个字段就行)定位到正确的client套接字进行数据交付。

总结:

UDP协议根据报文段的目标IP和端口定位到对应的socket完成数据向用户进程的交付;而TCP协议则要根据源IP/端口+目标IP/端口这4个字段定位。

如果2个不同源主机(也就是两个发送方)的报文段的目标IP和端口相同:对于UDP,这2个报文段会交付到同一个接收方的socket。对于TCP,这2个报文会交付到同一个接收方的两个不同的client
socket。

套接字的逻辑本质是一个存储 IP + 端口 信息的对象。连接 = 发送方套接字 + 接收方套接字。

TCP套接字是一个四元组套接字(源IP和端口 + 目的IP和端口),UDP套接字是一个二元组套接字(源IP和端口)。

02 UDP协议概述

UDP是一种无连接的、面向报文的、尽最大努力交付(不保证可靠交付)的传输层协议。

UDP的特点如下

不建立连接: 减少了3次握手的时延。

不保证可靠交付 :报文可能丢失、乱序(但不会比特差错),而且不负责重发。丢失时可以通知应用层,让应用层组织重发。

因为不保证可靠交付,所以也没有确认应答机制。下节会介绍这个概念,你可以理解为你发了一封信给对方但你不知道对方有没有收到。有确认应答就是你发了一封邮件给对方,但你知道对方有没有收到,因为对方无论有没有收到系统都会告知你发送成功还是失败。

面向报文 :一次交付一个完整报文。

即使一个应用层报文很大,UDP协议也不会对应用层交付给运输层的报文进行切分,而是直接对应用层报文加上首部就交给网络层。

当然啦,即使如此,网络层还是会对这个庞大的报文进行分片的以避免其超过MTU的大小,不过这么一来起码在运输层上,报文的边界得以保留。

运输层不切分的坏处是,需要应用层决定一次发送多少数据以避免发生IP分片,从而导致降低协议栈的传输效率。

没有拥塞控制 :UDP协议在网络出现拥塞时也不限制发送端的发送速率,这对发送端自己有利,但对其所在的网络不利。

这里的网络拥塞具体是指路由器拥塞,路由器的缓存已经塞满了网络数据。

没有拥塞控制意味着发送速率稳定,但发生拥塞时会因为路由器缓存溢出而丢失分组,而且不限速的发送也会导致拥塞和加重拥塞。

支持多种交互通信 :支持一对一,一对多,多对多,多对一的交互通信,而TCP只能做到一对一。

这里的一对一和一对多是指socket,具体是指一个服务端的soket可以为多个客户端的socket通信和服务。

而TCP服务端的一个client套接字只能为一个客户端socket收发消息。

首部开销小 :8个字节,比TCP的20字节首部短。

UDP使用场景

实时应用:语音电话或视频会议。利用了UDP开销小(因为不用建立连接)、效率高、没有拥塞的特点。

一次性传小量数据的应用:如果一次性大量数据则不利于UDP不切分数据的特点。

多媒体应用:如播放视频。因为视频对传输可靠性没那么高,即使小部分数据丢失也不影响视频播放。

UDP协议报文格式

UDP头部包括源端口、目的端口、长度(包括头部长度+数据长度)和校验和这4个字段,每个字段2个字节。极其简单,不做解释。

UDP差错检查 (不是重点,不感兴趣的朋友可以跳过蛤)

UDP的差错检查既检查头部也检查数据部分。差错校验过程如下:

1、在发送端,运输层封装好UDP报文之后,会为其生成一个伪首部,整个报文被分成多段,每段16个位,如果所有比特数量不是16的倍数则补0。

需要注意,此时UDP首部的校验和为空,用16位的0填充占位。

2、将所有16位的二进制串相加求和得到16位的一个二进制串(如果溢出则把溢出的部分回卷),最后再取反,即为校验和。

如上图所示(图中伪首部的17表示使用UDP协议,15表示UDP报文的长度),报文的第一个16位是伪首部的前2个字节,也是源主机IP的前两个字节153和19,转为二进制是10011001和00010011,同理列出报文剩下的所有16位的二进制串。其中校验和也占2个字节,但是由于校验和目前为空,因此校验和的16个位是
00000000 00000000。最后数据部分不足16位的部分用0填充到满足16位。

上图所有16位比特求和最终为 10 10010110 11101011,一共18位,溢出的两位“10”需要回卷,也就是把“10”加回到最低位得到
10010110 11101101,将其取反就得到校验和 01101001 00010010,它会被填充到报文头部的校验和字段。

之后UDP报文往网络层传递,传递的报文不含伪首部,伪首部只是为了生成校验和的。

3、在接收端,接收到UDP报文后,运输层会把报文加上伪首部之后分为多个16位的段,数据部分不足16位的补0,并将所有16位二进制串相加。由于校验和 和
非校验和的其他字段的16位二进制之和 是互相取反的关系,因此如果求和得到 11111111 11111111 这样的16位1,说明整个UDP报文没有差错。

运输层的差错检验检验了如目标IP和端口、源IP和端口、其他头部字段和数据部分(因为校验和16位二进制是由这些信息构成的)。

TCP的差错校验和UDP的一样。

运输层差错检验的必要性

虽然链路层也提供了差错检验,运输层依旧有检验的必要,原因有二:

A. 无法保证源和目的之间所有链路都提供差错检验,因此当报文到达主机后,依旧要由目标主机的运输层再做一次差错检验。

B. 即使在链路中没发生比特差错,报文段存储在某个路由器的内存中也可能引入比特差错。

UDP提供差错检验,但不会恢复差错,也就是不会重发出错的分组。接收端如果检测出差错只能丢弃受损报文段,或者是将错误的报文段交给接收端应用程序并由应用程序发出警告。

需要注意,网络层也会做校验,但它只对网络包的头部校验而不对数据部分校验,原因是网络层的职责是为了尽可能高效快速的传输数据,如果检验数据部分就会降低这个传输速率。

而传输层的职责之一是数据的交付,保证数据的正确性是传输层该做的。这点和协议栈的每一层的功能职责有关。

03 TCP协议概述

TCP协议是一种面向连接、面向字节流、提供可靠传输服务的一对一通信传输层协议。

TCP特点如下

面向连接 :数据收发前需要建立连接,数据收发要通过这个逻辑的虚拟信道。

全双工通信 :通信两端都可收可发,而且双向的收发可以同时发生。

一对一通信 :一个client socket只能与一个客户端进程的socket进行数据收发。

面向字节流 :TCP只把数据看成一连串有序而无结构的字节流。

可靠传输服务 :保证数据不丢失、不重复、无差错、不乱序。

TCP功能在UDP功能(数据交付和差错检验)的基础上加上了连接管理、超时重传、流量控制和拥塞控制。

TCP报文段格式

TCP首部最小长度为20字节,最大长度60。TCP首部的前20个字节是固定的,后面4n字节是按需增加的选项。下面对TCP头部的每个部分进行介绍。

序号(seq) :TCP数据的每个字节是按序编号的,TCP首部的“序号”字段就是本报文携带数据的第一个字节的编号,范围在0~2^32-1,
超过则下一个序号会回到0重新增长。

PS:初始序号(ISN)在建立连接时设置,ISN是一个随时间动态增长的非0序号,这也是为了防止被攻击者伪造初始序号和TCP报文。

序号是TCP实现可靠传输的基础,其作用有:防止接收方接收重复分组;确认应答机制基于序号;给字节标记序号便于超时重传触发时发送方知道自己应该重发哪个分组。

确认号(ack) :是一端期望收到对端下一个报文段数据的第一个字节的序号,也是本端上一次发送的报文段的最后一个字节序号+1。

数据偏移:表示TCP首部的长度,占4个比特,每个比特的单位是4字节。因此TCP首部最大长度为60字节。

6个标志位

紧急位 URG :URG = 1时报头的紧急指针字段生效,表示报文段有紧急数据要尽快传送。

socket有发送缓冲区,运输层会择机将发送缓冲区的数据发送出去(3个时机,后面会介绍),但如果URG = 1的报文段则无需等待这3个时机,可以直接发送。

确认位 ACK :根据TCP的确认应答机制,接收方收到一个报文之后,应该要回复发送方一个确认报文。ACK =
1时,报头的确认号(ack)字段才有效,才能表示本报文是一个确认报文。TCP规定在连接建立后所有传送的报文段都必须把ACK置为1。

推送位 PSH :PSH=1的报文段,PSH=1是在告诉接收方,要尽快把数据上交给应用进程,而非在接收缓冲区中缓存。该标志位是针对接收方的。

重置连接位 RST :RST =1 的报文段标明TCP连接出现差错,必须释放连接,然后重新建立连接。

同部位 SYN :SYN = 1表示这是一个连接请求报文,当接收方接收到SYN=1的报文时,说明发送方正尝试与我方建立TCP连接。

终止位 FIN :FIN = 1表示这是一个请求释放连接的报文。

窗口字段
:接收方告诉发送方,下一次容许发送方能够发送的最大数据长度。发送方的窗口有多大,就意味着下一次发送方最多可以发送多少数据给接收方,而这取决于接收方的接收缓冲区还剩多少空间。

检验和 :同UDP校验和。

紧急指针
:指出本报文段中紧急数据有多少字节,紧急数据放在报文段数据的最前面,所以紧急指针等于紧急数据在本报文的最后一个字节的位置。窗口为0时也可以发送紧急数据。

选项字段(最大40字节 ):该字段长度可变。有如下可选项:

所有可选选项都包含该选项的类型长度还有实际内容,例如下面的时间戳选项,kind=8表示这是一个时间戳选项,length表示时间戳选项长度为10字节。

所有可选选项都在建立连接时的SYN报文中指定是否开启。

窗口扩大选项 使用窗口扩大项后,可以使窗口大小从原本最大 2^16 - 1 扩大到最大 2^30 - 1个字节。

这是考虑到网络链路可能有长又肥(即带宽很大,且链路很长,即时延很长),如果一次发的数据太少就无法充分利用带宽,吞吐率也低。

发送端的发送窗口(TCP头部的窗口大小)由 接收方的接收缓存大小 (流量窗口) 以及 链路带宽和拥塞情况(拥塞窗口) 共同决定的。

选择确认选项 :接收方告诉发送方自己收到的连续字节块。用于数据段失序到达时,发送端重复发送数据段。

时间戳选项 :占10字节,包含最主要的是时间戳值字段(4字节)和时间戳回送回答字段(4字节)。

时间戳字段有两个作用:

A. 计算报文在两端传输层的往返时延(接近于RTT)

A发送报文时会将发送时间戳放入 timestamp(时间戳值字段), B接收到报文后将timestamp复制到timestamp
echo(时间戳回显重试字段),并在返回ack报文时将当前时间戳放入timestamp。

回复报文到达A后,A可以用当前时间戳 - timestamp echo得到往返时间,而且该往返时间可认为就是RTT。

B. 防止序号回绕带来的问题

需要注意,填充的时间戳不是真实的时间戳,而是一个自增的整型,而且发送方填入的timestamp和接收方填入的timestamp可以是独立的,例如发送方填入timestamp
= 5012, 接收方填入timestamp = 197720862,也就是说两端的时间戳可以不用同步。

MSS 最大报文段大小

MSS 最大报文段数据大小,用于告诉对端,我所在的局域网链路能容纳的最大报文段的数据长度。

MSS和窗口无关,和网络带宽有关。 在建立TCP连接时,通信双方都要在SYN报文指明自己允许的MSS大小,MSS是双向的。

MSS可以控制TCP的传输效率,MSS太大可能导致报文段在网络层分片,太小可能导致传输效率降低,假设MSS设为1个字节,那么一个报文段的数据包含只有1个字节但头部有20个字节,你说效率低不低。

应该尽量设置MSS接近网络层一个分片的最大大小,使得该报文段刚好不用分片,这取决于从源主机到目的主机链路的最小MTU(MTU是网络层的包的最大长度)。

MSS + TCP头部 = TCP报文段长度。

MSS + TCP头部 + IP头部 <= MTU

(下面了解即可)

MSS默认是536字节(这也是合理的最小MSS),因为任何主机都应该至少处理576字节的IPv4数据报(含IP头部),如果按最小的IPv4和TCP头部计算,最小IPv4数据报下的最大MSS
= 576 - 20 - 20 = 536。

在以太网中IPv4协议下,MSS应该设置的比较合适的值是略小于1460,理由是以太网MTU=1500, 而TCP和IP头部分别为20字节,1460 + 20
+20 = 1500,刚好达到网络层不用对IP包分片的最大包大小,也刚好到达以太网链路的最大报文数据的传输大小。

在以太网中IPv6协议下,MSS应该设置的比较合适的值是略小于1440,因为IPv6的头部为40字节。


评论