作者: andylin02
学习章节: 第二十六章 高级UDP编程
关键词: UDP扩展, 超时重传, 序列号, 多宿, recvmsg, recvfrom_flags, 数据报截断, 并发UDP服务器, 路径MTU发现, 窗口式流量控制
一、章节概述
1.1 本章焦点
第二十六章深入探讨UDP编程中那些不为人熟知却至关重要的高级话题。UDP是一个不可靠的、无连接的、面向数据报的协议,这既是它的优势(简洁高效),也是它的劣势(可靠性不足)。在实际应用中,许多系统(如DNS、NFS、TFTP、SNMP)选择UDP而非TCP,是因为请求-应答模型的简洁性和不需要维护连接状态的开销,在这些应用中需要在应用层为UDP补充可靠性。
本章将系统讲解如何为UDP应用添加可靠机制,讨论在多宿(multihoming) 服务器上响应时源IP地址的选择问题,剖析数据报截断的处理方式,探究UDP并发服务器的设计模式,介绍通过辅助数据获取数据报目的地址和接收接口索引的方法,并简要讨论UDP的路径MTU发现机制。
💡 本章核心价值:读完第二十六章,你将能够——
- 使用
recvmsg及其辅助数据识别接收UDP数据报的目的地址和接收接口- 判断UDP数据报是否被截断(
MSG_TRUNC),并理解不同系统的处理差异- 为UDP应用添加超时、重传和序列号,弥补UDP的不可靠性
- 深入理解UDP
connect的本地语义及其对多宿问题的影响- 设计并发UDP服务器,处理需要多次交换的UDP应用
- 理解UDP路径MTU发现的机制,避免不必要的IP分片
1.2 本章内容结构
| 节号 | 标题 | 核心内容 |
|---|---|---|
| 26.1 | 接收标志、目的IP地址和接口索引 | 使用recvmsg和辅助数据获取UDP数据报的详细信息 |
| 26.2 | 数据报截断 | MSG_TRUNC标志的处理以及不同系统的差异 |
| 26.3 | 何时使用UDP而不是TCP | UDP与TCP的选择标准与权衡 |
| 26.4 | 给UDP应用添加可靠性 | 超时和重传、序列号匹配应答 |
| 26.5 | 并发UDP服务器 | 两种UDP服务器类型的并发处理设计 |
| 26.6 | UDP与路径MTU发现 | 避免IP分片的路径MTU发现机制 |
二、接收标志、目的IP地址和接口索引
2.1 问题的来源
多宿主机(multihomed host)是指拥有多个网络接口的服务器,例如同时连接公网和内网的主机,或者配置了多个IP地址的服务器。
当UDP客户端向多宿服务器发送数据报时,服务器调用recvfrom返回的是数据报的源IP地址(即客户端发来的IP地址)。但服务器在应答时,通常使用源IP地址作为目的地址进行回复——多宿服务器上recvfrom返回的源IP地址可能并不是客户端最初发送时所使用的目的IP地址,而是服务器主机上某个接口的IP地址,这可能不是客户端期望的通信接口。
💡 关键理解:在多宿服务器上,
recvfrom返回的源IP地址可能不是客户端发往服务器时所使用的目的IP地址。这一差异可能导致服务器的应答被路由到错误的网络接口,造成“不对称路由”(asymmetric routing)问题。例如,服务器可能错误地通过公网接口回复了原本从内网发来的请求,从而导致通信失败。
更具体的场景是:服务器主机拥有两个IP地址——10.0.0.1(内网)和192.168.1.1(外网)。客户端从外网向192.168.1.1发送请求,到达服务器后,由于多宿主机的路由选择逻辑,服务器可能选择10.0.0.1作为源IP地址回复客户端。客户端收到来自10.0.0.1的应答后,由于其路由表中没有通向10.0.0.1的路由,可能导致通信失败。
要正确应对多宿场景,服务器需要在应答时使用与客户端请求相同的源IP地址(即客户端发送请求时使用的目的IP地址),这正是本章需要解决的核心问题。
2.2 解决方案:IP_RECVDSTADDR套接字选项
为了获取UDP数据报的真正目的IP地址,需要设置IP_RECVDSTADDR套接字选项。开启后,内核会将数据报的目的IP地址通过辅助数据(ancillary data) 机制随数据一起返回,配合recvmsg使用。
#include <sys/socket.h>
#include <netinet/in.h>
int on = 1;
setsockopt(sockfd, IPPROTO_IP, IP_RECVDSTADDR, &on, sizeof(on));
2.3 获取接收接口索引:IP_RECVIF套接字选项
除了目的IP地址,在某些场景下还需知道数据报是从哪个网络接口接收的。可以设置IP_RECVIF套接字选项,通过辅助数据获取接收接口索引。
int on = 1;
setsockopt(sockfd, IPPROTO_IP, IP_RECVIF, &on, sizeof(on));
2.4 in_pktinfo结构
IPv4中使用struct in_pktinfo结构来封装目的地址和接口索引:
struct in_pktinfo {
struct in_addr ipi_addr; /* 数据报的目的IPv4地址 */
int ipi_ifindex; /* 接收数据报的接口索引 */
};
2.5 recvfrom_flags函数设计
recvfrom函数存在两个设计问题:其一,flags参数是整数类型,只能从进程到内核传递标志,而不能从内核返回标志;其二,它无法返回目的IP地址和接收接口索引。
为此,书中设计了recvfrom_flags函数,它不仅完成recvfrom的基本功能,还额外返回三个信息:
msg_flags:从内核返回的标志(如MSG_TRUNC、MSG_CTRUNC、MSG_BCAST、MSG_MCAST等)- 数据报的目的IP地址(通过
IP_RECVDSTADDR选项获取) - 接收数据报的接口索引(通过
IP_RECVIF选项获取)
#include "unp.h"
#include <sys/param.h> /* ALIGN macro */
ssize_t recvfrom_flags(int fd, void *ptr, size_t nbytes, int *flagsp,
SA *sa, socklen_t *salenptr,
struct unp_in_pktinfo *pktp)
{
struct msghdr msg;
struct iovec iov[1];
ssize_t n;
iov[0].iov_base = ptr;
iov[0].iov_len = nbytes;
msg.msg_name = sa;
msg.msg_namelen = *salenptr;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
msg.msg_control = NULL;
msg.msg_controllen = 0;
/* 如果支持辅助数据,分配空间存放IP_RECVDSTADDR和IP_RECVIF返回的值 */
#ifdef HAVE_MSGHDR_MSG_CONTROL
union {
struct cmsghdr cm;
char control[CMSG_SPACE(sizeof(struct in_addr)) +
CMSG_SPACE(sizeof(struct in_addr))];
} control_un;
msg.msg_control = control_un.control;
msg.msg_controllen = sizeof(control_un.control);
#endif
if ((n = recvmsg(fd, &msg, 0)) < 0)
return n;
*salenptr = msg.msg_namelen;
*flagsp = msg.msg_flags;
/* 处理辅助数据,提取目的地址和接口索引 */
#ifdef HAVE_MSGHDR_MSG_CONTROL
if (pktp) {
bzero(pktp, sizeof(*pktp));
for (struct cmsghdr *cmptr = CMSG_FIRSTHDR(&msg); cmptr;
cmptr = CMSG_NXTHDR(&msg, cmptr)) {
if (cmptr->cmsg_level == IPPROTO_IP) {
if (cmptr->cmsg_type == IP_RECVDSTADDR) {
memcpy(&pktp->ipi_addr, CMSG_DATA(cmptr),
sizeof(struct in_addr));
} else if (cmptr->cmsg_type == IP_RECVIF) {
struct sockaddr_dl *sdl = (struct sockaddr_dl *)CMSG_DATA(cmptr);
pktp->ipi_ifindex = sdl->sdl_index;
}
}
}
}
#endif
return n;
}
2.6 在多宿服务器中处理目的IP地址
在多宿UDP服务器上,recvfrom_flags使服务器能够获取客户端请求的真正目的IP地址,并用此地址作为应答的源IP地址,确保应答数据包能够沿着正确的网络路径返回客户端。
以下示例展示了如何在多宿UDP回射服务器中使用recvfrom_flags:
void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)
{
socklen_t len;
char mesg[MAXLINE];
int flags;
struct unp_in_pktinfo pktinfo;
struct sockaddr_in cliaddr, srcaddr;
for ( ; ; ) {
len = clilen;
flags = 0;
n = Recvfrom_flags(sockfd, mesg, MAXLINE, &flags,
&cliaddr, &len, &pktinfo);
/* 构建应答地址结构:目的IP地址 = 收到的数据报的目的IP地址 */
bzero(&srcaddr, sizeof(srcaddr));
srcaddr.sin_family = AF_INET;
srcaddr.sin_addr = pktinfo.ipi_addr; /* 真正的目的IP地址 */
srcaddr.sin_port = ((struct sockaddr_in *)&cliaddr)->sin_port;
/* 使用正确的源地址发送应答 */
Sendto(sockfd, mesg, n, 0, (SA *)&srcaddr, sizeof(srcaddr));
}
}
💡 关键理解:在多宿主机环境中,UDP服务器必须使用
IP_RECVDSTADDR获取数据报的目的IP地址,以确保应答数据包使用正确的源IP地址。当响应数据的源IP地址与客户端发送请求时的目的IP地址一致时,就能实现对称路由(symmetric routing),保证通信路径的一致性和可靠性。
三、数据报截断(Datagram Truncation)
3.1 UDP数据报截断问题
UDP面向数据报,每个recvfrom调用对应接收一个完整的UDP数据报。当应用程序提供的接收缓冲区小于实际到达的UDP数据报大小时,会发生“数据报截断”。TCP是字节流协议,没有记录边界的概念,因此不存在类似问题。
3.2 不同系统的处理差异
| 系统类型 | 截断处理方式 | 通知机制 |
|---|---|---|
| 源自Berkeley的系统 | 截断数据,丢弃超出部分 | recvmsg返回MSG_TRUNC标志 |
| 部分System V实现 | 丢弃超出部分,但不通知应用 | 无任何标志返回 |
| 其他一些实现 | 保留超出字节,在后续读取中返回 | 可能破坏协议 |
因此,如果应用程序希望正确处理数据报截断,必须调用recvmsg,只有它能够返回MSG_TRUNC标志。recvfrom的设计限制是无法从内核向进程传递标志信息。
3.3 处理截断的示例
#define MAXLINE 20 /* 故意设小以演示截断 */
void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)
{
socklen_t len;
char mesg[MAXLINE];
int flags;
struct unp_in_pktinfo pktinfo;
for ( ; ; ) {
len = clilen;
flags = 0;
n = Recvfrom_flags(sockfd, mesg, MAXLINE, &flags,
pcliaddr, &len, &pktinfo);
/* 打印源地址和目的地址信息 */
printf("%d-byte datagram from %s", n,
Sock_ntop(pcliaddr, len));
if (memcmp(&pktinfo.ipi_addr, &inaddr_any,
sizeof(pktinfo.ipi_addr)) != 0) {
printf(", destined to %s",
Inet_ntop(AF_INET, &pktinfo.ipi_addr,
mesg, sizeof(mesg)));
}
/* 检测截断标志 */
if (flags & MSG_TRUNC)
printf(" (datagram truncated)");
if (flags & MSG_BCAST)
printf(" (broadcast)");
if (flags & MSG_MCAST)
printf(" (multicast)");
printf("\n");
/* 回射处理 */
Sendto(sockfd, mesg, n, 0, pcliaddr, len);
}
}
💡 关键理解:
MSG_TRUNC标志是recvmsg返回的,而recvfrom的flags参数是输入而非输出,无法返回此信息。因此,需要处理截断通知的应用必须使用recvmsg。
四、何时使用UDP而不是TCP
4.1 选择UDP的适用场景
| 场景类型 | 推荐协议 | 理由 |
|---|---|---|
| 广播或多播 | ✅ UDP | 只有UDP支持广播和多播,TCP不支持 |
| 实时音视频应用 | ✅ UDP | 允许少量丢包,对延迟更敏感 |
| 简单请求-应答应用 | ✅ UDP | 无需维护连接状态,实现简单 |
| 海量数据传输 | ❌ TCP | 需自行实现流量控制和拥塞避免,相当于再造TCP |
4.2 使用UDP的潜在挑战
UDP缺乏TCP内置的可靠性机制。在请求-应答式应用中使用UDP,必须自行添加:
- 超时和重传:处理丢失的数据报
- 序列号:将应答与对应的请求进行匹配
- 流量控制(非必要,仅大流量时需要)
- 拥塞避免和慢启动(非必要,仅大流量时需要)
五、给UDP应用增加可靠性
5.1 核心挑战:TCP已解决的三大问题
- 可靠性:TCP通过超时和重传确保所有数据被确认
- 窗口式流量控制:接收端告知发送端缓冲区空间,避免溢出
- 拥塞控制:发送端通过慢启动和拥塞避免动态调节发送速率
5.2 在UDP应用层需补充的两个核心特性
在UDP应用(如DNS、SNMP、TFTP、RPC)中,至少需要补充以下两个特性:
| 特性 | 作用 | 实现方式 |
|---|---|---|
| 序列号 | 匹配应答与请求 | 客户给每个请求附加递增的序列号,服务器在应答中返回该序列号 |
| 超时和重传 | 处理丢失的数据报 | 使用自适应重传定时器,考虑测量到的RTT和RTT变化 |
5.3 序列号的设计
/* 客户端:带序列号的请求 */
struct request {
uint32_t seq; /* 请求序列号 */
char data[]; /* 应用层数据 */
};
/* 服务器:带序列号的应答 */
struct reply {
uint32_t seq; /* 对应请求的序列号 */
char data[]; /* 响应数据 */
};
💡 关键理解:序列号使客户端能够区分接收到的应答是对应哪一个请求的。尤其在重传场景中,客户端可能发送多个相同的请求,通过序列号可以避免将同一请求的重复应答误认为是新的响应。
5.4 超时与重传:挑战重传二义性
为每个请求设置一个定时器,如果在规定时间内未收到应答,则重传请求。问题在于当重传定时器超时时,可能的原因有:
- 原始请求丢失了
- 应答丢失了
- RTO太小(往返时间估算值过短)
简单地线性增加等待时间(如每次等待N秒)无法适应网络环境的变化。正确做法是实现自适应重传算法,如TCP所用的算法:持续测量RTT(往返时间)及其变化,动态调整超时重传时间(RTO)。
5.5 可靠性补充的核心设计要点
| 设计要点 | 说明 |
|---|---|
| 每个请求需有唯一序列号 | 用于匹配应答,防止重复应答造成的混乱 |
| 重传定时器应自适应 | 基于测量RTT及其变化动态计算RTO,而非固定等待时间 |
| 处理重传二义性 | 无法确定超时原因是请求丢失还是应答丢失,需结合序列号处理 |
| 避免重复应答 | 使用序列号去重,防止将同一请求的多个应答误认为是新的响应 |
| 需要考虑RTT变化 | 跨广域网RTT变化可能达数个数量级,必须自适应 |
⚠️ 注意:对于海量数据传输(如文件传输),不建议使用UDP。因为除了超时和重传,还需要窗口式流量控制、慢启动和拥塞避免等机制,在应用层实现这些无异于再造一个TCP。
六、并发UDP服务器
6.1 问题背景
UDP服务器默认是迭代式的——在一个循环中调用recvfrom接收请求,处理并发送应答,然后继续等待下一个请求。然而,某些UDP服务(如TFTP)可能需要与同一客户端交换多个数据报,这会导致服务器在处理一个客户端时阻塞,其他客户端无法得到服务。
6.2 两种UDP服务器类型
| 类型 | 特点 | 并发策略 |
|---|---|---|
| 类型1:简单UDP服务器 | 读入请求,发送应答,随后与客户不再关联 | fork子进程处理请求即可 |
| 类型2:多交换UDP服务器 | 需要与同一客户交换多个数据报 | 需要更复杂的并发设计 |
6.3 类型2:多交换UDP服务器的并发设计
核心问题是:客户只知道服务器的知名端口号,当服务器需要与同一客户交换多个数据报时,如何将后续数据报区分开来?
解决方案:服务器为每个客户创建一个新套接字,绑定一个临时端口,然后通过该套接字发送所有对该客户的应答。
设计步骤:
- 服务器在知名端口接收客户端第一个请求
- 创建一个新的UDP套接字
- 为新的套接字绑定一个临时端口(系统自动分配)
- 使用新套接字将应答发送给客户端,并在应答中告知客户端临时端口号
- 客户端后续数据报应发送到该临时端口,而非知名端口
- 服务器收到后续数据报时,通过临时端口区分不同的客户端
6.4 类型1:简单UDP服务器的fork并发设计
对于只交换单个数据报的UDP服务,可以使用fork实现并发,而无需创建新套接字。
#include "unp.h"
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr, cliaddr;
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(sockfd, (SA *)&servaddr, sizeof(servaddr));
for ( ; ; ) {
socklen_t len = sizeof(cliaddr);
char mesg[MAXLINE];
int n = Recvfrom(sockfd, mesg, MAXLINE, 0, (SA *)&cliaddr, &len);
if (fork() == 0) {
/* 子进程处理请求 */
Sendto(sockfd, mesg, n, 0, (SA *)&cliaddr, len);
exit(0);
}
/* 父进程继续循环 */
}
}
⚠️ 注意:简单的
fork模型对于UDP也是有效的,因为子进程继承了父进程的内存映像,包括接收到的数据报内容和客户端地址结构。但如果同一时间有大量并发请求,频繁fork可能导致性能下降,这种情况更适合使用线程池模型。
6.5 inetd启动的UDP并发服务器
对于通过inetd启动的UDP服务,实现方式有所不同:
- inetd监听:在UDP端口上监听,当第一个数据报到达时,inetd
fork子进程并启动相应的服务程序 - 套接字继承:inetd将套接字描述符传递给子进程,子进程直接接收该套接字
- 并发处理:子进程继续在同一套接字上接收该客户的所有后续数据报
6.6 UDP与TCP并发模型的对比
| 对比维度 | TCP并发 | UDP简单并发 | UDP多交换并发 |
|---|---|---|---|
| 区分客户的方式 | 唯一的TCP套接字对 | 无法区分后续数据报 | 需创建新套接字+临时端口 |
| fork可行性 | ✅ 标准做法 | ✅ 有效,适用于单请求 | ❌ 不够,需创建新套接字 |
| 性能考虑 | 频繁fork开销 | 频繁fork开销 | 创建套接字开销 |
| 典型应用 | HTTP、FTP等 | 简单的请求-应答服务 | TFTP、RPC等需要多交换的服务 |
七、UDP与路径MTU发现
7.1 IP分片与MTU
当UDP数据报的大小超过某个网络链路的**最大传输单元(MTU)**时,IP层会将其分片。例如,标准以太网的MTU为1500字节,减去IP首部(20字节)和UDP首部(8字节),UDP数据部分的有效载荷最多为1472字节。IP分片会降低网络效率:任何一个分片丢失,整个数据报都必须重传,且分片重组会增加接收端开销。
7.2 路径MTU发现(Path MTU Discovery)的工作原理
为尽量减少分片,现代TCP/IP实现支持路径MTU发现机制,其核心原理如下:
- 发送端发送数据包时,设置IP首部的DF(Don‘t Fragment,不分片)标志
- 如果中间路由器的MTU小于数据包大小,路由器会丢弃该数据包,并向源主机返回ICMP“需要分片” 错误消息(IPv4 Type 3,Code 4;IPv6 Type 2,Packet Too Big),同时指示该链路的MTU
- 发送端收到ICMP错误后,降低发送MTU值,重新尝试
- 重复该过程,最终找到从源到目的路径上的最小MTU(路径MTU,PMTU)
7.3 UDP应用程序中的路径MTU发现
UDP应用无法自动从IP层获取路径MTU信息,必须通过以下方式处理:
| 方法 | 描述 | 适用场景 |
|---|---|---|
| 控制数据报大小 | 确保UDP数据报大小不超过典型MTU(如1472字节) | 简单应用 |
| 处理ICMP消息 | 接收并响应“需要分片”的ICMP错误,动态调整发送大小 | 复杂应用 |
| 原始套接字 | 通过原始套接字接收ICMP“需要分片”消息,实现路径MTU发现 | 高级应用 |
7.4 实现UDP路径MTU发现的挑战
由于UDP是无连接的,IP层接收到的ICMP错误消息无法直接关联到特定的UDP套接字。要解决此问题,需要调用connect将UDP套接字变成已连接UDP套接字,这样内核才能将ICMP错误消息转发给相应的套接字。这一过程与第8章讨论的UDP异步错误处理机制本质相同。
八、关键图表
8.1 多宿服务器通信问题示意图
┌─────────────────────────────────────────────────────────────────────────────┐
│ 多宿服务器:recvfrom_flags使用场景示意 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 服务器主机(双宿) │
│ ┌─────────────────────────────┐ │
│ │ 接口A: 10.0.0.1(内网) │ │
│ │ 接口B: 192.168.1.1(外网) │ │
│ └─────────────┬───────────────┘ │
│ │ │
│ 问题场景(不使用IP_RECVDSTADDR) ┌─────────┐ │
│ 客户端发送请求到 192.168.1.1 │ 客户端 │ │
│ recvfrom返回的源地址可能是 10.0.0.1 └────┬────┘ │
│ 服务器应答使用错误的源地址 10.0.0.1 │ │
│ │ │ │
│ │ │ │
│ └─────────────────────────────┘ │
│ │ 应答可能无法到达! │
│ │
│ 解决场景(使用IP_RECVDSTADDR) │
│ 客户端发送请求到 192.168.1.1 ┌─────────┐ │
│ recvfrom_flags返回目的地址 = 192.168.1.1 │ 客户端 │ │
│ 服务器应答使用正确的源地址 192.168.1.1 └────┬────┘ │
│ │ │ │
│ └─────────────────────────────┘ │
│ │ 应答正确到达! │
└─────────────────────────────────────────────────────────────────────────────┘
8.2 recvfrom_flags函数的数据流图
┌─────────────────────────────────────────────────────────────────────────────┐
│ recvfrom_flags 函数数据流图 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ 内核 │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ UDP数据报 │ │ │
│ │ │ 源IP: 192.168.1.100 │ │ │
│ │ │ 目的IP: 192.168.1.1 (服务器地址) │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ │ recvmsg │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ msghdr结构 │ │ │ │
│ │ │ msg_name → 源地址 192.168.1.100 │ │ │
│ │ │ msg_control → 辅助数据 │ │ │
│ │ │ ├─ IP_RECVDSTADDR: 192.168.1.1 │ │ │
│ │ │ └─ IP_RECVIF: 接口索引 │ │ │
│ │ │ msg_flags → MSG_TRUNC / MSG_BCAST / MSG_MCAST等 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ 返回到用户进程 │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ 用户进程 │ │
│ │ │ │
│ │ recvfrom_flags函数返回: │ │
│ │ 1. 数据内容 │ │
│ │ 2. flagsp → 接收到的标志 │ │
│ │ 3. sa → 源IP地址(192.168.1.100) │ │
│ │ 4. pktp.ipi_addr → 目的IP地址(192.168.1.1) │ │
│ │ 5. pktp.ipi_ifindex → 接口索引 │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
8.3 辅助数据(Ancillary Data)结构布局
┌─────────────────────────────────────────────────────────────────────────────┐
│ 辅助数据布局(用于IP_RECVDSTADDR和IP_RECVIF) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ msg_control → │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ struct cmsghdr (目的地址) │ │
│ │ ├─ cmsg_len = CMSG_LEN(sizeof(struct in_addr)) │ │
│ │ ├─ cmsg_level = IPPROTO_IP │ │
│ │ ├─ cmsg_type = IP_RECVDSTADDR │ │
│ │ └─ 数据区 → 目的IP地址 (如 192.168.1.1) │ │
│ ├─────────────────────────────────────────────────────────────────────┤ │
│ │ struct cmsghdr (接收接口索引) │ │
│ │ ├─ cmsg_len = CMSG_LEN(sizeof(struct sockaddr_dl)) │ │
│ │ ├─ cmsg_level = IPPROTO_IP │ │
│ │ ├─ cmsg_type = IP_RECVIF │ │
│ │ └─ 数据区 → struct sockaddr_dl (包含sdl_index) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 💡 关键点:辅助数据在msghdr结构中通过msg_control字段传递,每个辅助数据对象 │
│ 都包含一个cmsghdr头部,使用CMSG_FIRSTHDR和CMSG_NXTHDR进行遍历。 │
└─────────────────────────────────────────────────────────────────────────────┘
九、常见问题与注意事项
9.1 常见错误速查表
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 多宿服务器应答被丢弃 | 服务器使用了错误的源IP地址 | 启用IP_RECVDSTADDR,使用接收到的目的IP地址作为应答的源IP地址 |
| UDP数据报被截断但未检测到 | 使用了recvfrom而非recvmsg | 使用recvmsg并检查msg_flags中的MSG_TRUNC标志 |
| 重传定时器设置不合理 | 采用固定的线性等待时间 | 实现自适应重传算法,基于测量RTT动态计算RTO |
| 收到错误的应答 | 未使用序列号区分请求 | 为每个请求分配唯一序列号,并在应答中返回 |
| 收到ICMP错误但无法处理 | UDP套接字未调用connect | 调用connect变为已连接UDP套接字,使ICMP错误能够返回给应用程序 |
| 多交换UDP服务器的后续请求被忽略 | 未创建临时端口区分不同客户端 | 为每个客户创建新套接字并绑定临时端口 |
9.2 辅助数据处理注意事项
| 注意事项 | 说明 |
|---|---|
| 内存对齐要求 | msg_control指向的辅助数据缓冲区必须满足8字节对齐要求,可使用CMSG_SPACE宏分配内存 |
| 使用CMSG_宏遍历 | 使用CMSG_FIRSTHDR和CMSG_NXTHDR遍历辅助数据,避免直接计算指针 |
| MSG_CTRUNC标志 | 如果辅助数据缓冲区太小,msg_flags会设置MSG_CTRUNC标志 |
| IPv4 vs IPv6差异 | IPv4使用IP_RECVDSTADDR和IP_RECVIF;IPv6使用IPV6_PKTINFO |
9.3 系统兼容性注意事项
| 系统 | 特殊处理 |
|---|---|
| Solaris | 使用IP_RECVDSTADDR和IP_RECVIF,但结构体名称可能不同 |
| Linux | 使用IP_PKTINFO套接字选项,返回的辅助数据结构略有差异 |
| Mac OS X | 支持IP_RECVDSTADDR和IP_RECVIF,与BSD实现一致 |
| Windows | 通过WSARecvMsg支持辅助数据 |
9.4 UDP可靠性补充的权衡
| 权衡点 | 说明 |
|---|---|
| 增加开销 | 超时和重传、序列号机制会增加CPU和内存开销 |
| 复杂度上升 | 需要在应用层处理丢包、乱序、重复等异常情况 |
| 适用场景判断 | 对于小数据量、丢包率低的环境,UDP的简洁性优势明显 |
| 协议选择建议 | 跨广域网传输大量数据时,应优先考虑TCP而非在UDP上再造可靠机制 |
十、本章小结
10.1 核心知识点回顾
| 知识点 | 关键要点 |
|---|---|
| 接收数据报的目的IP地址 | 通过IP_RECVDSTADDR套接字选项 + 辅助数据获取,解决多宿服务器通信问题 |
| 数据报截断处理 | 使用recvmsg检查MSG_TRUNC标志;不同系统处理方式不同 |
| UDP可靠性补充 | 序列号(匹配应答)+ 超时/重传(处理丢失),需实现自适应重传 |
| 多宿服务器 | 需使用IP_RECVDSTADDR获取真正目的地址,实现对称路由 |
| 并发UDP服务器 | 简单模型可用fork;多交换模型需为新客户创建临时端口 |
| 路径MTU发现 | 利用ICMP“需要分片”消息动态调整UDP数据报大小,避免IP分片 |
| UDP vs TCP选择 | 广播/多播、实时应用、简单请求-应答适用UDP;海量数据用TCP |
10.2 本章思维导图
第二十六章 高级UDP编程
├── 接收标志、目的IP地址和接口索引
│ ├── 问题:多宿服务器无法获取数据报的真正目的IP地址
│ ├── 解决方案:IP_RECVDSTADDR + IP_RECVIF + recvmsg
│ ├── 结构体:in_pktinfo(ipi_addr, ipi_ifindex)
│ └── recvfrom_flags函数:封装recvmsg,返回额外信息
├── 数据报截断
│ ├── UDP数据报可能超过接收缓冲区
│ ├── MSG_TRUNC标志(需recvmsg获取)
│ ├── 不同系统处理差异
│ └── MSG_CTRUNC(辅助数据截断)
├── 何时使用UDP而不是TCP
│ ├── 广播或多播
│ ├── 实时音频/视频应用
│ ├── 简单请求-应答应用
│ └── 海量数据→TCP(避免再造可靠机制)
├── 给UDP应用增加可靠性
│ ├── 超时和重传
│ │ ├── 挑战:重传二义性(请求丢失、应答丢失、RTO太小)
│ │ └── 自适应重传:基于测量RTT和RTT变化
│ ├── 序列号匹配应答
│ └── 可选的窗口式流量控制和拥塞避免(大流量时需要)
├── 并发UDP服务器
│ ├── 类型1:简单UDP服务器 → fork子进程处理
│ ├── 类型2:多交换UDP服务器 → 创建新套接字+临时端口
│ ├── inetd启动的UDP服务
│ └── 与TCP并发模型的对比
├── UDP与路径MTU发现
│ ├── IP分片:降低网络效率
│ ├── DF标志 + ICMP“需要分片”消息
│ └── UDP应用需自行处理ICMP错误(需connect)
└── 关键数据结构
├── struct in_pktinfo(IPv4目的地址和接口)
├── struct msghdr(recvmsg/sendmsg)
├── struct cmsghdr(辅助数据头部)
└── CMSG_*宏(辅助数据遍历)
十一、下一章预告
📌 下一篇:《UNIX网络编程》读书笔记(二十七):第二十七章 高级SCTP套接字编程
第二十七章将深入探讨SCTP协议的高级编程主题:
- SCTP通知机制详解:
SCTP_ASSOC_CHANGE、SCTP_PEER_ADDR_CHANGE等通知类型 - SCTP多流编程:如何在SCTP关联中使用多个独立流,解决TCP队头阻塞问题
- 部分递送与无序数据:SCTP的部分递送API和无序消息发送机制
- 地址捆绑与地址子集:
sctp_bindx的完整用法,获取对端地址列表 - SCTP的心搏(Heartbeat)与地址不可达检测:多宿环境下的故障检测
- SCTP关联剥离(Peel-off):将一到多套接字中的关联剥离为独立的一到一套接字
学习目标:学完第二十七章后,你将能够——
- 编写完整的SCTP一到多式客户/服务器程序
- 使用SCTP的多流特性提高应用并行度
- 正确处理SCTP的各种事件通知
- 在多宿环境中利用SCTP的多宿特性实现路径冗余
敬请期待!
参考资料
- W. Richard Stevens, Bill Fenner, Andrew M. Rudoff. 《UNIX网络编程 卷1:套接字联网API(第3版)》. 北京:人民邮电出版社
- UNPv1第二十章:高级UDP套接口编程,腾讯云开发者社区,https://cloud.tencent.cn/developer/article/1393686
- UNIX网络编程卷一 学习笔记 第二十二章 高级UDP套接字编程,CSDN,https://blog.csdn.net/tus00000/article/details/131408150
- UNP总结 Chapter 22~25 高级UDP套接字编程、高级SCTP 套接字编程、带外数据、信号驱动I/O,博客园,https://www.cnblogs.com/biyeymyhjob/archive/2012/08/07/2626899.html
- 《UNIX网络编程 卷1:套接字联网API(第3版)》——8.14 UDP中的外出接口的确定,阿里云开发者社区,https://developer.aliyun.com/article/673832
- 速读原著-TCP/IP(采用UDP的路径MTU发现),腾讯云开发者社区,https://cloud.tencent.cn/developer/article/1601325
- UDP 使用六点注意事项,CSDN,https://blog.csdn.net/yannanxiu/article/details/52204832
- 重传二义性,CSDN,https://blog.csdn.net/weixin_37890450/article/details/103973668
- 25.1 UDP Echo Server With recvfrom_flags,UNP online,https://books.gigatux.nl/mirror/unixnetworkprogramming/0131411551_ch25lev1sec1.html
本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!


3607

被折叠的 条评论
为什么被折叠?



