《UNIX 网络编程-卷1》阅读笔记27: 高级UDP编程

作者: 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的不可靠性
  • 深入理解UDPconnect的本地语义及其对多宿问题的影响
  • 设计并发UDP服务器,处理需要多次交换的UDP应用
  • 理解UDP路径MTU发现的机制,避免不必要的IP分片

1.2 本章内容结构

节号标题核心内容
26.1接收标志、目的IP地址和接口索引使用recvmsg和辅助数据获取UDP数据报的详细信息
26.2数据报截断MSG_TRUNC标志的处理以及不同系统的差异
26.3何时使用UDP而不是TCPUDP与TCP的选择标准与权衡
26.4给UDP应用添加可靠性超时和重传、序列号匹配应答
26.5并发UDP服务器两种UDP服务器类型的并发处理设计
26.6UDP与路径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_TRUNCMSG_CTRUNCMSG_BCASTMSG_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返回的,而recvfromflags参数是输入而非输出,无法返回此信息。因此,需要处理截断通知的应用必须使用recvmsg

四、何时使用UDP而不是TCP

4.1 选择UDP的适用场景

场景类型推荐协议理由
广播或多播✅ UDP只有UDP支持广播和多播,TCP不支持
实时音视频应用✅ UDP允许少量丢包,对延迟更敏感
简单请求-应答应用✅ UDP无需维护连接状态,实现简单
海量数据传输❌ TCP需自行实现流量控制和拥塞避免,相当于再造TCP

4.2 使用UDP的潜在挑战

UDP缺乏TCP内置的可靠性机制。在请求-应答式应用中使用UDP,必须自行添加:

  • 超时和重传:处理丢失的数据报
  • 序列号:将应答与对应的请求进行匹配
  • 流量控制(非必要,仅大流量时需要)
  • 拥塞避免和慢启动(非必要,仅大流量时需要)

五、给UDP应用增加可靠性

5.1 核心挑战:TCP已解决的三大问题

  1. 可靠性:TCP通过超时和重传确保所有数据被确认
  2. 窗口式流量控制:接收端告知发送端缓冲区空间,避免溢出
  3. 拥塞控制:发送端通过慢启动和拥塞避免动态调节发送速率

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 超时与重传:挑战重传二义性

为每个请求设置一个定时器,如果在规定时间内未收到应答,则重传请求。问题在于当重传定时器超时时,可能的原因有:

  1. 原始请求丢失了
  2. 应答丢失了
  3. 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服务器的并发设计

核心问题是:客户只知道服务器的知名端口号,当服务器需要与同一客户交换多个数据报时,如何将后续数据报区分开来?

解决方案:服务器为每个客户创建一个新套接字,绑定一个临时端口,然后通过该套接字发送所有对该客户的应答。

设计步骤

  1. 服务器在知名端口接收客户端第一个请求
  2. 创建一个新的UDP套接字
  3. 为新的套接字绑定一个临时端口(系统自动分配)
  4. 使用新套接字将应答发送给客户端,并在应答中告知客户端临时端口号
  5. 客户端后续数据报应发送到该临时端口,而非知名端口
  6. 服务器收到后续数据报时,通过临时端口区分不同的客户端

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端口上监听,当第一个数据报到达时,inetdfork子进程并启动相应的服务程序
  • 套接字继承: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发现机制,其核心原理如下:

  1. 发送端发送数据包时,设置IP首部的DF(Don‘t Fragment,不分片)标志
  2. 如果中间路由器的MTU小于数据包大小,路由器会丢弃该数据包,并向源主机返回ICMP“需要分片” 错误消息(IPv4 Type 3,Code 4;IPv6 Type 2,Packet Too Big),同时指示该链路的MTU
  3. 发送端收到ICMP错误后,降低发送MTU值,重新尝试
  4. 重复该过程,最终找到从源到目的路径上的最小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_FIRSTHDRCMSG_NXTHDR遍历辅助数据,避免直接计算指针
MSG_CTRUNC标志如果辅助数据缓冲区太小,msg_flags会设置MSG_CTRUNC标志
IPv4 vs IPv6差异IPv4使用IP_RECVDSTADDRIP_RECVIF;IPv6使用IPV6_PKTINFO

9.3 系统兼容性注意事项

系统特殊处理
Solaris使用IP_RECVDSTADDRIP_RECVIF,但结构体名称可能不同
Linux使用IP_PKTINFO套接字选项,返回的辅助数据结构略有差异
Mac OS X支持IP_RECVDSTADDRIP_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协议的高级编程主题:

  1. SCTP通知机制详解SCTP_ASSOC_CHANGESCTP_PEER_ADDR_CHANGE等通知类型
  2. SCTP多流编程:如何在SCTP关联中使用多个独立流,解决TCP队头阻塞问题
  3. 部分递送与无序数据:SCTP的部分递送API和无序消息发送机制
  4. 地址捆绑与地址子集sctp_bindx的完整用法,获取对端地址列表
  5. SCTP的心搏(Heartbeat)与地址不可达检测:多宿环境下的故障检测
  6. SCTP关联剥离(Peel-off):将一到多套接字中的关联剥离为独立的一到一套接字

学习目标:学完第二十七章后,你将能够——

  • 编写完整的SCTP一到多式客户/服务器程序
  • 使用SCTP的多流特性提高应用并行度
  • 正确处理SCTP的各种事件通知
  • 在多宿环境中利用SCTP的多宿特性实现路径冗余

敬请期待!

参考资料

  1. W. Richard Stevens, Bill Fenner, Andrew M. Rudoff. 《UNIX网络编程 卷1:套接字联网API(第3版)》. 北京:人民邮电出版社
  2. UNPv1第二十章:高级UDP套接口编程,腾讯云开发者社区,https://cloud.tencent.cn/developer/article/1393686
  3. UNIX网络编程卷一 学习笔记 第二十二章 高级UDP套接字编程,CSDN,https://blog.csdn.net/tus00000/article/details/131408150
  4. UNP总结 Chapter 22~25 高级UDP套接字编程、高级SCTP 套接字编程、带外数据、信号驱动I/O,博客园,https://www.cnblogs.com/biyeymyhjob/archive/2012/08/07/2626899.html
  5. 《UNIX网络编程 卷1:套接字联网API(第3版)》——8.14 UDP中的外出接口的确定,阿里云开发者社区,https://developer.aliyun.com/article/673832
  6. 速读原著-TCP/IP(采用UDP的路径MTU发现),腾讯云开发者社区,https://cloud.tencent.cn/developer/article/1601325
  7. UDP 使用六点注意事项,CSDN,https://blog.csdn.net/yannanxiu/article/details/52204832
  8. 重传二义性,CSDN,https://blog.csdn.net/weixin_37890450/article/details/103973668
  9. 25.1 UDP Echo Server With recvfrom_flags,UNP online,https://books.gigatux.nl/mirror/unixnetworkprogramming/0131411551_ch25lev1sec1.html

本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

andylin02

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值