050 传输层 —— UDP

传输层 —— UDP

1. 再谈端口号

1. 端口号与五元组通信模型

端口号(Port)标识了一个主机上进行通信的不同的应用程序。

PixPin_2025-09-09_11-55-33

在 TCP/IP 协议中,一个 通信连接 由五元组唯一标识:(源 IP, 源端口, 目的 IP, 目的端口, 协议号)

PixPin_2025-09-09_11-56-47

为什么需要五元组?

  • 一台主机可以同时与多个远程主机通信。
  • 同一个远程主机可以同时提供多个服务(如 HTTP + SSH)。
  • 同一个服务可以被多个本地进程(客户端)访问。
  • 所以必须用五元组才能 唯一标识一条连接

查看当前连接:

1
2
3
netstat -n
# 或更现代的:
ss -n

2. 端口号范围划分

范围 名称 说明
0 - 1023 知名端口 系统保留,通常需要 root 权限绑定。如 80(HTTP), 443(HTTPS), 22(SSH) 等。
1024 - 49151 注册端口 用户程序或第三方服务可注册使用(如 MySQL 3306, Redis 6379)
49152 - 65535 动态/私有端口 操作系统自动分配给客户端程序的临时端口

注:不同系统对“动态端口”的起始值可能不同(Linux 默认从 32768 开始,可通过 cat /proc/sys/net/ipv4/ip_local_port_range 查看)。在我的主机上运行结果是 32768 60999,这就表示 Linux 会随机选择 32768 到 60999 之间的端口号给我自己的客户端进程进行分配。

查看知名端口映射:

1
cat /etc/services

编辑(不要随意修改!):

1
sudo vim /etc/services			# cat 不好查看这样的长内容,用 vim 观看体验会好一点,但不要进行修改!!

我们自定义程序端口建议避开 0-1023,最好使用 1024 以上未被占用的端口,从而避开这些知名端口号。

3. 两个关键问题

1. 一个进程是否可以 bind 多个端口号?

可以! 一个进程可以创建多个 socket,分别 bind 到不同的端口。例如:Nginx 可以监听 80 和 443;一个程序可以同时提供 HTTP 和管理 API。

1
2
3
// 伪代码示意
socket1 = socket(); bind(socket1, port=8080);
socket2 = socket(); bind(socket2, port=8081);

2. 一个端口号是否可以被多个进程 bind?

我们一般接触到的情况都不行!

  • 操作系统规定:同一时间,一个端口(如 80)只能有一个进程处于 LISTENING 状态。否则无法区分到底该把流量交给哪个进程。
  • 如果强行 bind,会报错 Address already in use

但也存在例外情况(了解):

  • 使用 SO_REUSEADDRSO_REUSEPORT 选项时,多个进程/线程可以监听同一个端口,但一般用于 负载均衡(如 Nginx 多 worker)。
  • 这种情况下,内核会把新连接分配给其中一个进程。

4. 命令详解:netstat, pidof

1. netstat —— 查看网络连接、路由、接口等

常用选项(netstat -tulnp):

  • -t:仅显示 TCP 相关选项。
  • -u:仅显示 UDP 相关选项。
  • -l:仅列出有在 Listening(监听中的)服务状态。
  • -n:拒绝显示别名,能显示数字的全部转化成数字。
  • -p:显示建立相关链接的程序名、进程 PID(需要 root)。
  • -a(all):显示所有选项默认不显示 LISTEN 相关。

示例:

1
2
3
4
5
6
7
8
netstat -n      				# 用数字显示,不反查域名/服务名
netstat -tlnp # 显示 TCP 监听端口 + 进程号
netstat -anp # 显示所有连接 + 进程号
netstat -tulnp | grep :80 # 查看谁在监听 80 端口

ss -tulnp
ss -tuln # 查监听
ss -tan # 查所有 TCP 连接

在较新 Linux 系统中,ss(socket statistics)开始逐渐替代 netstat

2. pidof —— 通过程序名获取进程 PID

语法: pidof [程序名]

示例:

1
2
pidof httpd      # 查 httpd 的进程号
pidof nginx # 查 nginx 的进程号

常见用法:

1
2
3
4
5
pidof httpd
# 确认无误后再
pidof httpd | xargs kill -9
# 或
kill -9 $(pidof httpd) # 杀死所有 httpd 进程

3. 命令行参数转化问题

命令行示例:

1
ps ajx | head -1 && ps ajx | grep httpd | awk '{print $2}' | xargs kill -9

命令行本意:

  1. ps ajx | head -1 → 打印 ps ajx 的表头(列名),方便对照列号
  2. && → 表示前面命令成功(退出码 0)才执行后面的命令,这里 head -1 几乎总是成功,所以后面一定会执行。
  3. ps ajx | grep httpd → 找出所有含有 httpd 的进程。
  4. awk '{print $2}' → 取第 2 列(以为这一列就是 PID)。
  5. xargs kill -9 → 把 PID 传给 kill -9,强制结束进程。

目的就是:先看看进程表头 → 再杀掉所有 httpd(这个 d 就表示是守护进程 ) 进程。

为什么有问题?

本来只想杀掉 httpd,结果不小心连 grep httpd 这个查找命令自己也一起杀了 —— 虽然它“死得快”,但逻辑是错的,还可能误杀别的!

  • grep httpd 会把自己也列出来,把自己运行的查找命令也一起杀掉,虽然 grep 命令通常 0.1 秒就结束了,杀它没实际影响,但 逻辑错误!不专业!
  • 如果系统里没有 httpd,命令会报错!

怎么解决?

  1. 最简单 —— 直接用 pkillpkill -9 httpd(进程名,非ID) → 系统自带的“精准杀手”,专门干这个活,不会杀自己,找不到也不报错。
  2. pidofpidof httpd | xargs kill -9pidof 只找“程序名叫 httpd”的进程,不会找 grep,很干净。
  3. 非要自己写,加个“防自杀”技巧:ps ajx | grep [h]ttpd | awk '{print $2}' | xargs kill -9[h]ttpdhttpd 效果一样,但命令名是 grep [h]ttpd,不会匹配自己(也叫“grep 防自爆写法”)。

4. 关于 iostat

  • iostat:I/O statistics → 磁盘、CPU 使用情况。
  • netstat:network statistics → 网络、端口情况。

它和网络/端口无关,它只是一个 查看磁盘 I/O 统计 的工具,用于收集 CPU 使用情况块设备(磁盘/分区)I/O 性能数据,帮助我们了解系统的 I/O 瓶颈。安装命令:sudo yum install sysstat

1
2
iostat		  # 显示 CPU 使用率 + 每个设备的 I/O 概况
iostat -x 1 # 每秒刷新,显示扩展统计(x → 显示扩展统计,更详细)

指标讲解(磁盘部分):

字段 含义
r/s 每秒读请求数(读 IOPS)
w/s 每秒写请求数(写 IOPS)
rkB/s 每秒读的 KB 数(读吞吐量)
wkB/s 每秒写的 KB 数(写吞吐量)
await 平均每个 I/O 请求的等待时间(毫秒),越小越好
svctm 服务一个 I/O 请求的时间(毫秒)
%util 磁盘繁忙程度(100% = 跑满了)

直观判断:

  • %util 接近 100%,磁盘被打爆了。
  • await 很大,说明 I/O 延迟严重。
  • IOPS (r/s+w/s) 很高,但 %util 没满 → 磁盘性能还行。

CPU 部分指标:

字段 含义
%user 用户态 CPU 占比(应用程序消耗)
%system 内核态 CPU 占比(系统调用消耗)
%iowait 等待 I/O 的时间占比(越高越说明磁盘慢)
%idle 空闲时间

直观判断:

  • %iowait 很高,说明 CPU 大部分时间都在等磁盘。
  • %user 高 → 应用很吃 CPU。
  • %idle 高 → 系统很闲。

2. UDP 协议

1. UDP 协议段格式

PixPin_2025-09-09_11-58-32

16 位指 16 个二进制位,即 0 或 1,8 位等于 1 字节,16 位就是 2 字节。

UTF-16 不常用,UTF-8 更常用。它是一种变长字符编码,简单且直观的来说就是在 UTF-8 中,一个英文字符就占 1 字节,一个中文字符就占 3 字节,少数生僻字等占 4 字节。

UDP 报头总是 8 字节(64 bit),分成四个 16 位字段,按顺序排放:

  1. 源端口号(16 位)
  2. 目的端口号(16 位)
  3. UDP 长度(16 位) —— 包括整个 UDP 数据报(头部 + 数据)的长度。
  4. 校验和(16 位) —— 校验 UDP 报文在传输中是否出错(发送方用特定算法,将伪首部、UDP 首部和数据部分计算出校验和存于报头。接收方同样计算并对比,若不一致 通常丢弃数据包,但也可交给上层并附错误报告 )。

紧接着就是 数据(有效载荷),长度由 “UDP 长度”字段减去 8(报头大小)得出。

在 Linux 下用 C 语言实现时,UDP 报头是一个自定义结构体。具体表现为(cat /usr/include/netinet/udp.h):

1
2
3
4
5
6
7
8
9
struct udphdr
{
u_int16_t source;
u_int16_t dest;
u_int16_t len;
u_int16_t check;
};

// u_int16_t是一种类型定义,本质上是 16 位无符号整数,用来定义 UDP 报头中各字段的数据类型,像源端口号、目的端口号等都是 16 位,就用它来定义。

2. UDP 报文是“矩形”的还是线性的?

图中“矩形”矩形结构只是一种 逻辑结构图/一种可视化方式,用来帮助理解字段布局、说明各个字段的位置和大小。实际网络上传输时(物理传输层面),报文是 一维线性的比特流(字节流)。换句话说,发送出去的数据包在网线上就是一串 0 和 1,不是二维矩形。

实际在网络中,UDP 数据报是一个连续的字节序列,按顺序排列如下:[源端口][目的端口][长度][检验和][数据...],举个例子:假设一个 UDP 数据报有 100 字节数据,则总长度为 8(头部)+ 100 = 108 字节。这些字节会依次封装进 IP 包中,通过网络传输。

在协议层面 面向数据报,UDP 以数据报为单位处理数据,有独立边界;在物理传输层面,为适配网络介质,数据报要转化为 比特流(字节流)传输,一个是逻辑处理方式,一个是实际传输形式 。


3. 报头和有效载荷是如何分离的?

关键点是 UDP 长度字段,通过它确定报头后 8 字节的边界,从而分离出有效载荷。接收方先看前 8 字节是报头,再找出 “UDP 长度” 这个信息,从第 9 字节开始,用 “UDP 长度” 减 8 得到的字节数就是有效载荷。或者简单说,先确定 8 字节报头,IP 层把数据给 UDP 后,剩下的就是有效载荷。

4. 有效载荷如何交付给上层?

当 UDP 收到数据报后:

  1. 根据 目的端口号 查找对应的应用程序(如 DNS、NTP、游戏等)。
  2. 将有效载荷交给该应用程序处理。
  3. 应用程序通常通过 socket 接口接收数据(例如 recvfrom())。

比如在线视频播放时,视频流数据通过 UDP 传输,目的端口号对应视频播放应用程序,UDP 把有效载荷交给它,就能播放视频了。

UDP 不保证送达、不排序、不重传,所以交付是“尽力而为”。

5. UDP 的应用场景有哪些?

UDP 的特点:简单、无连接、不保证可靠传输、面向数据报,但开销小、延迟低。 适用于:

  • 实时通信:语音通话(VoIP)、视频会议、在线游戏(延迟比可靠性更重要)。
  • 简单查询服务:DNS(域名解析)、SNMP(网络管理)、DHCP(动态主机配置)。
  • 广播、多播:如局域网内的服务发现。

6. 如果 UDP 数据包太大怎么办?

分片就像是把大包裹拆成几个小包裹来寄。UDP 数据包太大时,IP 层会把它拆分。丢包就是这些小包裹在运输(网络传输)过程中丢失了。一旦有小包裹丢了,因为 UDP 不会重新发送,整个数据就用不了。

UDP 没分段机制,数据包大小受 MTU(最大传输单元)限制。 比如 IPv4 的 MTU 一般 1500 字节,减去 IP 头部(20 字节)和 UDP 头部(8 字节)后就是数据最大长度(最大 UDP 载荷约为 1472 字节)。超了就由 IP 层分片,像把大文件拆成小文件。但分片可能丢包,现代设备还可能拒绝分片。应对办法有控制数据大小、用路径 MTU 发现或改用 TCP 协议等。

7. UDP 的缓冲区

UDP 无真正发送缓冲区,sendto 调用后数据直给内核再传网络层。它有接收缓冲区,但不能保证接收顺序与发送一致,若缓冲区满,新到的 UDP 数据会被丢弃。UDP 的 socket 可同时读写,具备全双工特性 。