一、前言
🔥 之前在这篇文章 初识网络 中说过关于传输层的内容,以及在 Socket编程应用层UDP 也做过关于 UDP 的练习,如下:
- 传输层: 负责两台主机之间的数据传输。 如传输控制协议 (TCP),能够确保数据可靠的从源主机发送到目标主机,负责数据能够从发送端传输接收端
二、再谈端口号
端口号(Port)
标识了一个主机上进行通信的不同的应用程序
- 在 TCP/IP 协议中, 用 “源 IP”、“源端口号”、“目的 IP”、“目的端口号”、“协议号” 这样一个五元组来标识一个通信(可以通过
netstat -n
查看)
🐇 端口号范围划分
- 0 - 1023: 知名端口号、HTTP、FTP、SSH 等这些广为使用的应用层协议, 他们的端口号都是固定的.
- 1024 - 65535:操作系统动态分配的端口号、客户端程序的端口号,就是由操作系统从这个范围分配的.
🐇 认识知名端口号(Well-Know Port Number)
有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号:
- ssh 服务器:使用 22 端口
- ftp 服务器:使用 21 端口
- telnet 服务器:使用 23 端口
- http 服务器:使用 80 端口
- https 服务器:使用 443
执行下面的命令, 可以看到知名端口号
- 注意:我们自己写一个程序使用端口号时, 要避开这些知名端口号.
🐇 端口号和 bind 关系
① 一个进程可以 bind 多个端口号
- 一个进程可以通过多次调用 bind() 函数绑定多个端口号(前提是这些端口号未被占用)。
- 每个绑定的端口号可以用于不同的通信需求。例如,一个服务器进程可以同时监听多个端口,分别处理不同类型的请求。
② 一个端口号不可以被多个进程 bind
- 端口号用于唯一标识一个通信端点。如果多个进程绑定同一个端口号,操作系统无法确定将数据包交给哪个进程。
- 当尝试绑定一个已被占用的端口号时,bind() 函数会返回错误(如
EADDRINUSE
)。
但是值得注意的是:在特定情况下是可以的
- 使用
SO_REUSEADDR
选项: - 通过设置套接字选项
SO_REUSEADDR
,多个进程可以绑定到同一个端口号。 - 适用场景:
- 快速重启服务器时,避免等待
TIME_WAIT
状态结束。 - 多进程服务器模型中,多个子进程共享同一个监听端口。
int optval = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
- 多播或广播:
- 多个进程可以绑定到同一个端口号,用于接收多播或广播数据包。
- 适用场景:
- 多播组通信(如视频流分发)。
- 广播消息(如 DHCP 发现)。
- 不同协议:
- 不同协议的套接字可以绑定到同一个端口号。例如,TCP 和 UDP 可以同时使用同一个端口号。
- 不同 IP 地址:
- 如果主机有多个 IP 地址,不同进程可以绑定到同一个端口号,但使用不同的 IP 地址
三、UDP 协议
1. UDP 协议端格式
16 位 UDP
长度:表示整个数据报 (UDP 首部+UDP 数据) 的最大长度- 如果校验和出错, 就会直接丢弃;
现在我们有两个问题,UDP 是 如何做到解包和分用的,按照上面的逻辑
- 解包:UDP 直接读取报文前 8 个字节(16 位源端口号)来进行解包
- 分用: UDP 根据16位端口号去 OS 中查找进程与这16位端口号关联的
从这就可以知道我们在 Socket编程应用层UDP 文章这 UDP的端口号只有两字节,因为 OS 在协议中规定端口号是 16 位的,因此我们当时 ip 地址是 4 字节,而 端口号却是 2 字节,这就是 协议源代码
- 这里 UDP 携带16 位 UDP 长度,之前在网络版本计算器和 http 的相关文章 都采用到了 类似方案,这种自己描述自己的特性-> 叫作 自描述字段 ,方便把报头固定下来,然后保证对方能够读到完整的报文
2. UDP 特点
UDP 传输的过程类似于寄信:
- 无连接: 知道对端的 IP 和端口号就直接进行传输, 不需要建立连接;
- 不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方,UDP 协议层也不会给应用层返回任何错误信息;
- 注意:这里的不可靠不一定是缺点,而是其特性,相比于 TCP 更可靠需要更多操作,这里 UDP 操作更简单
- 适用场景:实时应用、广播/多播,就是因为其更简单
- 面向数据报: 不能够灵活的控制读写数据的次数和数量;
3. 面向数据报
应用层交给 UDP 多长的报文, UDP 原样发送, 既不会拆分, 也不会合并
- 用 UDP 传输 100 个字节的数据:
- 如果发送端调用一次
sendto
, 发送 100 个字节, 那么接收端也必须调用对应的一次 recvfrom
,接收 100 个字节; 而不能循环调用 10 次 recvfrom
,每次接收 10 个字节
4. UDP 缓冲区
- UDP 没有真正意义上的 发送缓冲区. 调用 sendto 会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;
- UDP 具有接收缓冲区. 但是这个接收缓冲区不能保证收到的 UDP 报的顺序和发送 UDP 报的顺序一致; 如果缓冲区满了, 再到达的 UDP 数据就会被丢弃;
UDP 的 socket
既能读, 也能写, 这个概念叫做 全双工
5. UDP 使用注意事项
💡 我们注意到:UDP 协议首部中有一个 16 位的最大长度,也就是说一个 UDP 能传输的数据最大长度是 64K(包含 UDP 首部),然而 64K 在当今的互联网环境下, 是一个非常小的数字。
- 如果我们需要传输的数据超过 64K,就需要在应用层手动的分包,多次发送,并在接收端手动拼装;
6. 基于UDP 的应用层协议
- NFS: 网络文件系统
- TFTP: 简单文件传输协议
- DHCP: 动态主机配置协议
- BOOTP: 启动协议(用于无盘设备启动)
- DNS: 域名解析协议
当然, 也包括你自己写 UDP 程序时自定义的应用层协议