skaiuijing

UDP是一个无连接的协议。

笔者使用的源码:lwip2.20版本,4.4BSD-lite,Linux内核2.6.24版本。

lwip是一个轻量级的TCP/IP实现,通常被使用在单片机这些资源受限的场景上,往往搭配RTOS使用。

4.4BSD-lite是一个跨时代的操作系统,TCP/IP三卷中的协议栈源码就来自该操作系统。

Linux2.6是非常经典的版本,也是公认可商用版本。

操作系统中的中断控制

中断是操作系统中一个非常重要的概念,而协议栈往往作为操作系统的一部分,因此我们需要了解一些中断概念。

很多熟悉CPU架构的小伙伴都有过使用中断的经验,但中断也是有区别的,我们通常使用的是硬件层面的中断。

软件中断

一般由操作系统定义的中断,可由软件主动触发的中断,例如使用INT n指令触发该中断,这种被称之为软件中断。

我们执行的系统调用其实也是软件中断,通常执行软件中断时,程序会从用户态转变为内核态,

硬件中断

硬件层面的中断,比如串口、网口中断,这些中断本质上是一种硬件资源。

软中断

可以缓期执行的中断,软中断在适当的时候被内核调度执行,它本质上是由硬件触发。

中断与异常

中断通常是外部触发,异常是内部触发,例如除0操作就会触发异常。但是在arm中,中断是异常的一种。

异常处理其实就是一种软件中断。

软件触发的硬件中断

我们可以往某个特定寄存器写入特殊值,这样就会触发中断执行,这种中断被称为软件触发的硬件中断,本质上还是硬件中断。

中断控制

我们都知道信息传递到硬件上是会触发中断接收信息的,但有些场合不能接收信息,此时,我们可以通过控制中断达到目的。

我们这里要屏蔽的中断是硬件中断和软件中断。

在RTOS中,lwip以线程的形式运行,通常是操作硬件的寄存器达到中断控制的目的,通常会屏蔽较低优先级的中断。

BSD中提供了不同的中断等级,可以通过调用不同的API实现屏蔽对应优先级以下的中断。

它的实现原理很简单,就是提高当前代码的优先级,从而达到屏蔽低优先级的目的。

1
2
3
4
int s = splnet():提高优先级,屏蔽网络级别及以下优先级的中断,可以防止IP层对输入处理。
....
splx(s);//恢复到原先的优先级
使用s保存变量的好处是可以嵌套使用。

简单概述,一共两种实现:

1.设置一个优先级,低于该优先级的中断不得执行

2.提高程序优先级,这样低优先级的任务就不能打断当前程序

以上都是通过屏蔽中断方式实现程序的管理。

在Linux中,使用lock_sock这些锁实现对程序竞态的管理。通过对套接字(struct sock *sk)进行加锁,确保在对套接字的操作期间,不会发生竞态条件。在操作IP选项时,也通过rcu_read_lock()之类读写锁进行保护。甚至可以说,内核中大部分竞态问题都是通过锁解决的,例如内存锁、总线锁等等。

网络输入输出

网络输入如下,

网卡这些属于硬件资源,而硬件级别的中断是最高的优先级。产生硬件中断并使用splimp函数提高当前程序优先级,这样就可以确保传递信息给协议层之前没有干扰。因为如果此时网卡处理硬件输入,那么传递信息的程序会被打断,可能会插入其他数据到当前信息。如果硬件一直有输入,那么结果会是没有一个信息会传递到协议层,而就算只有一次打断,信息的完整性也会被破坏。

而协议层也是同理,触发软件中断并提升优先级,防止IP层处理输入。

1
2
graph LR
Aa(硬件接口层)--硬件中断与splimp调用-->Ab(协议层)--软件中断与splnet调用-->Ac(socket层)-->Ad(进程)

在TCP/IP卷二中描述得非常奇怪,原文相关的内容是(硬件中断splimp),并且描述splimp这些函数的功能是提升CPU优先级。笔者不清楚是翻译的原因还是操作系统的区别,按照笔者个人理解splimp应该只是提升当前中断优先级的,splimp函数更不是硬件中断,它应该在网卡产生的硬件中断中使用才对。

网络上大部分文章都是照本宣科,笔者也没有找到答案。本文大部分内容均为个人理解,写错勿怪。

应用层使用

通常我们调用API如下:

本文的socket指插口,也指套接字。

**创建套接字socket()**:无论是客户端还是服务器,首先需要创建一个套接字。

(服务器)绑定套接字bind():服务器需要绑定到一个固定的IP地址和端口,以便客户端能够找到它。

(客户端)可选的绑定套接字:对于UDP客户端,通常不需要绑定套接字,系统会自动分配一个临时端口。

发送和接收数据API,例如sendmsg() 和 recvmsg():数据通信通过这两个函数完成,它们不需要建立连接即可发送和接收数据。

关闭套接字close():通信结束后,关闭套接字,释放系统资源。

代码实现如下:

服务端建立

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
int main() {
int sockfd;
struct sockaddr_in servaddr, cliaddr;
char buffer[BUFSIZE];

// 1. 创建 UDP 套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
// 2. 配置服务器地址
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET; // IPv4
servaddr.sin_addr.s_addr = INADDR_ANY; // 接收任意 IP
servaddr.sin_port = htons(PORT); // 端口
// 3. 绑定套接字到指定端口
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0 )
while (1) {
struct msghdr msg;
struct iovec iov;
char ctrl_buf[CMSG_SPACE(sizeof(int))]; // 控制信息缓冲区
struct cmsghdr *cmsg;
int n;
socklen_t len = sizeof(cliaddr);
// 4. 设置消息结构体
msg.msg_name = &cliaddr; // 存储客户端地址
msg.msg_namelen = sizeof(cliaddr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = ctrl_buf; // 控制信息缓冲区
msg.msg_controllen = sizeof(ctrl_buf);
// 5. 接收消息
n = recvmsg(sockfd, &msg, 0);
// 6. 发送回复消息
// 设置控制信息(例如设置 IP_TTL 为 64)
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = IPPROTO_IP;
cmsg->cmsg_type = IP_TTL;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
int ttl = 64;
memcpy(CMSG_DATA(cmsg), &ttl, sizeof(ttl));
// 发送消息
if (sendmsg(sockfd, &msg, 0) < 0)
}
close(sockfd);
return 0;
}

以上就是我们调用API的流程,我们关注的是发送数据这里。

在BSD中,函数调用关系有:

1
2
3
graph LR
Aa(库函数send)-->Ab(系统调用sendto)-->Ac(sendit)-->Ad(sosend)-->Ae(udp_usrreq)-->Af(udp_output)-->D(ip_output)
Ba(系统调用sendmsg)-->Ac

其中的UDP是我们感兴趣的部分,现在让我们看看具体实现,不过在此之前需要介绍一些基础数据结构。

首先介绍的是buf结构,在执行sosend函数时,会将用户空间拷贝待发送的数据到内核空间的缓冲区,用户数据被复制到mbuf中,由mbuf链表挂载分片的数据。

pbuf、mbuf和skbuff

在三种协议栈中,携带数据包的实现不同,但都采用链表形式。

pbuf是lwip的实现,作为一个轻量级协议栈,pbuf的结构体比较简单,但具有多种形式适用于不同场景。

mbuf是BSD的实现,通过小内存块和外部数据区域相结合,提高内存利用率,该实现强调内存效率和灵活性。

skbuff则是Linux的实现,采用引用计数机制,包含了数据包处理所需的各种元数据,如协议头部指针、时间戳、路由信息等,便于内核对数据包进行灵活操控。

skbuff的数据挂载具有两种实现,并使用union关键字修饰:一种是传统的链表,另一种是红黑树。

Linux中的协议栈实现是比较繁琐的,这与Linux是宏内核实现有关,协议栈中有大量的抽象层匹配,不过正因如此,Linux才能应对复杂的网络数据处理。

mbuf

mbuf共有四种,其中一种是mbuf簇。

mbuf簇

mbuf簇是一种固定大小的大块内存缓冲区,mbuf簇的大小通常为2KB或4KB,具体取决于系统的配置

M_PREPEND函数

该函数用于在给定的 mbufm 的开头添加 plen 字节的空间,如果头部空间不足,那么就会分配一个新的mbuf(新的mbuf在旧的之前),这个机制也与分片有关。当数据报大于MTU(链路层最大传输)时,就会产生分片,一旦一个分片丢失,那么整个数据报都无效了,UDP常常产生分片,即使丢失,重发即可。但在TCP协议中,分片应该被避免,因为TCP协议会重传整个IP层,不仅仅是重发这么简单。

1
2
3
4
5
6
7
8
9
10
11
#define M_PREPEND(m, plen, how) do {                               \
if (M_LEADINGSPACE(m) >= (plen)) { \
(m)->m_data -= (plen); \
(m)->m_len += (plen); \
if ((m)->m_flags & M_PKTHDR) \
(m)->m_pkthdr.len += (plen); \
} else { \
(m) = m_prepend((m), (plen), (how)); \
} \
} while (0)

其中M_LEADINGSPACE是计算mbuf簇前可用的空间,如果sosend函数将数据放到一个mbuf簇中,那么通常该簇前面有56个字节未使用,可以为IP,UDP等其他首部提供空间。

mtod函数

该函数十分简单,就是指针类型转换。

1
#define mtod(m, t)    ((t)((m)->m_data))

在长篇大论UDP之前,我们先讲一讲IP报头结构:

IP报头结构

IP报头结构如下:

字段 长度(字节)
版本/头部长度 1
服务类型 1
总长度 2
标识 2
片偏移字段 2
生存时间(TTL) 1
协议 1
校验和 2
源IP地址 4
目的IP地址 4

好了,现在该进入UDP了。

报文首部结构体

报文首部通常有四个参数:源端口 、目的端口 、长度、校验和。

UDP控制块

UDP控制块会记录UDP通信的信息。

规范

根据协议规范,UDP布局如下:

字段 长度(位)
**源端口 与 目的端口 ** 都是2字节
长度 与 校验和 也都是2字节
数据 变长(最小可为0)

前四个元素组成UDP头部。

在前文笔者给出了IP层的报文结构,对于IP层来说,发送给哪个传输层协议需要根据字段中的值进行判断,也就是服务类型,IP层根据这个值把数据报分离到特定的传输协议。

因此不同协议可以使用的端口号是独立的,不同的协议使用相同的端口号是不会冲突的。同样的,对于两个不同的服务器来说,只要它们使用不同的传输协议,那么它们可以使用相同的端口号和IP地址。

UDP伪头部

字段 大小
源IP地址 4字节
目的IP地址 4字节
全0 1字节
协议号 1字节
UDP长度 2字节

UDP伪头部中的全0是协议规定需要填充的字段,确保UDP伪头部的大小是12字节。

UDP伪头部由IP首部构建而来,构建后的UDP伪头部下面是UDP头部和数据部分,数据部分后面可能会有一个填充值。

填充值的作用是字节对齐,这样方便计算校验和,因为校验和算法把所有内容视为一系列16位字。我们都知道字的大小基本由CPU的寻址大小决定(32位的CPU,字的大小往往是4字节,不过也可能由编译器决定),但是协议栈中的字的大小与CPU这些无关,固定为16位,也就是两字节,所以计算校验和必定需要偶数个字节。

数据部分后面的填充字节实际上不会被发送出去的(UDP伪头部也是),目的仅仅是辅助计算(如果数据报的长度是奇数那么就会填充该字节)。

校验和

UDP的校验和字段是端到端的,这意味着校验和的计算和验证都是在通信的发送端接收端完成的,而中间的网络设备不参与这个过程。

(除非它通过一个NAT)

IPv4中,UDP校验和是可选的,如果发送端未计算校验和,校验和字段可以填0,接收端则不会进行校验

IPv6中,UDP校验和是强制性的,必须进行计算和验证。

校验和是通过对UDP伪头部计算得到的(使用IP头部中的源IP地址和目的IP地址这些信息),也就是说,NAT在把私有IP地址转换为公有IP时,必须重新对UDP校验和进行修改。

发送方校验

整理一下UDP伪头部相关的校验过程:

初始化校验和字段:发送方首先将UDP首部中的校验和字段设置为0。

构建校验和数据块:添加伪首部,如果数据报的长度是奇数那么就填充字节。

计算校验和:将上述所有内容视为一系列16位字,将它们相加,采用二进制反码求和(即每次求和后取一位补码)。

填写校验和字段:将计算得到的二进制反码和的反码(即取反)填入UDP首部的校验和字段。

接收方校验

构建校验和数据块:添加伪首部,如果数据报的长度是奇数那么就填充字节。

计算校验和:方法同发送方校验。

验证校验和

将计算得到的校验和与0xFFFF(全1)比较:

如果结果为0xFFFF:表示数据完整,未检测到错误。

如果结果不为0xFFFF:表示数据在传输过程中发生了错误。

处理数据报

无错误:如果校验和验证通过,接收方可以将数据报交给上层应用程序处理。

有错误:如果校验和验证失败,接收方应丢弃该UDP数据报。

UDP-Lite

传统的UDP要么启用校验覆盖全部数据,要么完全不启用(IPv4)。UDP-Lite通过修改传统的UDP协议,通过部分校验和来解决这个问题。结构体中会多出成员用以表示校验和覆盖范围。

例如lwip,以下参数来自struct udp_pcb:

1
2
3
4
#if LWIP_UDPLITE
/** used for UDP_LITE only */
u16_t chksum_len_rx, chksum_len_tx;
#endif /* LWIP_UDPLITE */

这两个参数分别表示接收和发送时的校验和覆盖范围。

简单表示如下:

字段 大小
源端口号 2字节
目的端口号 2字节
校验和覆盖范围 2字节
校验和 2字节

覆盖范围是被覆盖的字节数,是从UDP-Lite头部的第一个参数开始。该参数除了特殊的值0外,最小值是8(字节),因为头部肯定是要被覆盖的。当值为0时,表示全覆盖,等效于传统的UDP协议开启校验。

lwip实现

IP结构体

lwip中定义结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct ip_hdr {
/* version / header length */
PACK_STRUCT_FLD_8(u8_t _v_hl);
/* type of service */
PACK_STRUCT_FLD_8(u8_t _tos);
/* total length */
PACK_STRUCT_FIELD(u16_t _len);
/* identification */
PACK_STRUCT_FIELD(u16_t _id);
/* fragment offset field */
PACK_STRUCT_FIELD(u16_t _offset);
#define IP_RF 0x8000U /* reserved fragment flag */
#define IP_DF 0x4000U /* don't fragment flag */
#define IP_MF 0x2000U /* more fragments flag */
#define IP_OFFMASK 0x1fffU /* mask for fragmenting bits */
/* time to live */
PACK_STRUCT_FLD_8(u8_t _ttl);
/* protocol*/
PACK_STRUCT_FLD_8(u8_t _proto);
/* checksum */
PACK_STRUCT_FIELD(u16_t _chksum);
/* source and destination IP addresses */
PACK_STRUCT_FLD_S(ip4_addr_p_t src);
PACK_STRUCT_FLD_S(ip4_addr_p_t dest);
} PACK_STRUCT_STRUCT;

UDP结构体

在lwip中定义的UDP报文首部数据结构如下:

1
2
3
4
5
6
struct udp_hdr{
uint16_t src;
uint16_t dest;
uint16_t len;
uint16_t chksum;
}

lwip中udp控制块定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct udp_pcb {
/** Common members of all PCB types */
IP_PCB;

/* Protocol specific PCB members */

struct udp_pcb *next;

u8_t flags;
/** ports are in host byte order */
u16_t local_port, remote_port;

#if LWIP_MULTICAST_TX_OPTIONS
#if LWIP_IPV4
//指定多播数据包的传出网络接口的IPv4地址
ip4_addr_t mcast_ip4;
#endif /* LWIP_IPV4 */
//指定物理接口
u8_t mcast_ifindex;
//多播数据包的生存时间,即被丢弃前可经过的最大路由跳数
u8_t mcast_ttl;
#endif

#if LWIP_UDPLITE
//仅用于UDP_LITE的校验
u16_t chksum_len_rx, chksum_len_tx;
#endif /* LWIP_UDPLITE */

/** receive callback function */
udp_recv_fn recv;
/** user-supplied argument for the recv callback */
void *recv_arg;
};

IP_PCB控制块结构体定义,准确来说,它是所有控制块的父类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** This is the common part of all PCB types. It needs to be at the
beginning of a PCB type definition. It is located here so that
changes to this common part are made in one location instead of
having to change all PCB structs. */
#define IP_PCB \
/* ip addresses in network byte order */ \
ip_addr_t local_ip; \
ip_addr_t remote_ip; \
/* Bound netif index */ \
u8_t netif_idx; \
/* Socket options */ \
u8_t so_options; \
/* Type Of Service */ \
u8_t tos; \
/* Time To Live */ \
u8_t ttl \
/* link layer address resolution hint */ \
IP_PCB_NETIFHINT

在RTOS中,线程取代了进程,应用线程持有特定的端口号,当UDP收到一个报文时,会对链表上的PCB进行遍历并匹配本地持有特定端口号的UDP控制块。

UDP控制块中的callbak函数会在lwip接收数据时被调用,它与recv字段有关。

UDP发送

从线程发送的数据会被udp_sendto_if_src进行处理,最后转交到IP层(如果不出意外的话)。

发送函数如下:

其实就是先检查几遍,再把UDP首部添加到pbuf中,然后填写UDP各个字段,最后输出到IP层。

1.判断套接字是否绑定本地端口,如果没有则进行绑定

2.判断数据包是否太大,导致无法添加首部,如果pbuf中没有足够的空间,那么就再申请一个pbuf

3.获取pbuf地址,填写udp_hdr的四个成员

4.如果设置了多播选项,多播循环被启用且目标地址是多播地址时,设置标志位

5.设置生存时间(其实这里省略了非常多的宏定义及代码),并输出数据报到IP层

6.如果额外申请了pbuf存放首部,在输出到IP层后可以回收该首部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/** @ingroup udp_raw
* Same as @ref udp_sendto_if, but with source address */
err_t
udp_sendto_if_src(struct udp_pcb *pcb, struct pbuf *p,
const ip_addr_t *dst_ip, u16_t dst_port, struct netif *netif, const ip_addr_t *src_ip)
{
struct udp_hdr *udphdr;
err_t err;
struct pbuf *q; /* q will be sent down the stack */
u8_t ip_proto;
u8_t ttl;

/* if the PCB is not yet bound to a port, bind it here */
if (pcb->local_port == 0) {
err = udp_bind(pcb, &pcb->local_ip, pcb->local_port);
if (err != ERR_OK) {
return err;
}
}

/* packet too large to add a UDP header without causing an overflow? */
if ((u16_t)(p->tot_len + UDP_HLEN) < p->tot_len) {
return ERR_MEM;
}

/* not enough space to add an UDP header to first pbuf in given p chain? */
if (pbuf_add_header(p, UDP_HLEN)) {
/* allocate header in a separate new pbuf */
q = pbuf_alloc(PBUF_IP, UDP_HLEN, PBUF_RAM);
if (q == NULL) {
return ERR_MEM;
}
if (p->tot_len != 0) {
/* chain header q in front of given pbuf p */
pbuf_chain(q, p);
}
} else {
q = p;
}
udphdr = (struct udp_hdr *)q->payload;
udphdr->src = lwip_htons(pcb->local_port);
udphdr->dest = lwip_htons(dst_port);
udphdr->chksum = 0x0000;

/* Multicast Loop? */
#if LWIP_MULTICAST_TX_OPTIONS
if (((pcb->flags & UDP_FLAGS_MULTICAST_LOOP) != 0) && ip_addr_ismulticast(dst_ip)) {
q->flags |= PBUF_FLAG_MCASTLOOP;
}
#endif /* LWIP_MULTICAST_TX_OPTIONS */

/* Determine TTL to use */
ttl = pcb->ttl;

/* output to IP */
err = ip_output_if_src(q, src_ip, dst_ip, ttl, pcb->tos, IP_PROTO_UDP, netif);

if (q != p) {
pbuf_free(q);
}

return err;
}

UDP接收

从IP层接收的数据会被UDP_input进行处理,最后转交到线程(如果不出意外的话)。

接收函数:

1.检查长度,如果PCB中记录UDP头部长度小于8字节,那么程序报错

2.迭代PCB,找到符合的端口号,如果没有,就使用第一个端口。

具体实现逻辑:判断uncon_pcb为NULL时,使用指针uncon_pcb保存的pcb。因为uncon_pcb被初始化为NULL,程序只有第一次会保存,那么uncon_pcb一定是第一个迭代到的pcb。如果到链表尾部都没找到,循环判断pcb为NULL并终止,那么就赋值pcb为uncon_pcb。

3.找到对应的控制块后,使用callback函数递交给具体线程应用。

4.如果找不到对应控制块,这种情况说明链表上没有pcb,如果启用了ICMP选项,那么返回端口不可达ICMP报文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
接收函数有点长,所以笔者删除了一些条件编译和不重要的注释。
/**
* If no pcb is found or the datagram is incorrect, the
* pbuf is freed.
* @param p pbuf to be demultiplexed to a UDP PCB (p->payload pointing to the UDP header)
* @param inp network interface on which the datagram was received.
*/
void
udp_input(struct pbuf *p, struct netif *inp)
{
struct udp_hdr *udphdr;
struct udp_pcb *pcb, *prev;
struct udp_pcb *uncon_pcb;
u16_t src, dest;
u8_t broadcast;
u8_t for_us = 0;

/* Check minimum length (UDP header) */
if (p->len < UDP_HLEN) {
pbuf_free(p);
goto end;
}

udphdr = (struct udp_hdr *)p->payload;

/* is broadcast packet ? */
broadcast = ip_addr_isbroadcast(ip_current_dest_addr(), ip_current_netif());

/* convert src and dest ports to host byte order */
src = lwip_ntohs(udphdr->src);
dest = lwip_ntohs(udphdr->dest);

pcb = NULL;
prev = NULL;
uncon_pcb = NULL;

for (pcb = udp_pcbs; pcb != NULL; pcb = pcb->next) {
/* compare PCB local addr+port to UDP destination addr+port */
if ((pcb->local_port == dest) &&
(udp_input_local_match(pcb, inp, broadcast) != 0)) {
if ((pcb->flags & UDP_FLAGS_CONNECTED) == 0) {
if (uncon_pcb == NULL) {
/* the first unconnected matching PCB */
uncon_pcb = pcb;
#if LWIP_IPV4
} else if (broadcast && ip4_current_dest_addr()->addr == IPADDR_BROADCAST) {
/* global broadcast address (only valid for IPv4; match was checked before) */
if (!IP_IS_V4_VAL(uncon_pcb->local_ip) || !ip4_addr_eq(ip_2_ip4(&uncon_pcb->local_ip), netif_ip4_addr(inp))) {
/* uncon_pcb does not match the input netif, check this pcb */
if (IP_IS_V4_VAL(pcb->local_ip) && ip4_addr_eq(ip_2_ip4(&pcb->local_ip), netif_ip4_addr(inp))) {
/* better match */
uncon_pcb = pcb;
}
}
#endif /* LWIP_IPV4 */
}
}

/* compare PCB remote addr+port to UDP source addr+port */
if ((pcb->remote_port == src) &&
(ip_addr_isany_val(pcb->remote_ip) ||
ip_addr_eq(&pcb->remote_ip, ip_current_src_addr()))) {
/* the first fully matching PCB */
if (prev != NULL) {
/* move the pcb to the front of udp_pcbs so that is
found faster next time */
prev->next = pcb->next;
pcb->next = udp_pcbs;
udp_pcbs = pcb;
} else {
UDP_STATS_INC(udp.cachehit);
}
break;
}
}

prev = pcb;
}
/* no fully matching pcb found? then look for an unconnected pcb */
if (pcb == NULL) {
pcb = uncon_pcb;
}

/* Check checksum if this is a match or if it was directed at us. */
if (pcb != NULL) {
for_us = 1;
} else {
#if LWIP_IPV4
if (!ip_current_is_v6()) {
for_us = ip4_addr_eq(netif_ip4_addr(inp), ip4_current_dest_addr());
}
#endif /* LWIP_IPV4 */
}

if (for_us) {
#if LWIP_UDPLITE
if (ip_current_header_proto() == IP_PROTO_UDPLITE) {
/* Do the UDP Lite checksum */
u16_t chklen = lwip_ntohs(udphdr->len);
if (chklen < sizeof(struct udp_hdr)) {
if (chklen == 0) {
/* For UDP-Lite, checksum length of 0 means checksum
over the complete packet (See RFC 3828 chap. 3.1) */
chklen = p->tot_len;
} else {
/* At least the UDP-Lite header must be covered by the
checksum! (Again, see RFC 3828 chap. 3.1) */
goto chkerr;
}
}
if (ip_chksum_pseudo_partial(p, IP_PROTO_UDPLITE,
p->tot_len, chklen,
ip_current_src_addr(),
ip_current_dest_addr()) != 0) {
goto chkerr;
}
} else
#endif /* LWIP_UDPLITE */
if (pbuf_remove_header(p, UDP_HLEN)) {
pbuf_free(p);
goto end;
}

if (pcb != NULL) {
/* callback */
if (pcb->recv != NULL) {
/* now the recv function is responsible for freeing p */
pcb->recv(pcb->recv_arg, pcb, p, ip_current_src_addr(), src);
} else {
/* no recv function registered? then we have to free the pbuf! */
pbuf_free(p);
goto end;
}
} else {
#if LWIP_ICMP || LWIP_ICMP6
/* No match was found, send ICMP destination port unreachable unless
destination address was broadcast/multicast. */
if (!broadcast && !ip_addr_ismulticast(ip_current_dest_addr())) {
/* move payload pointer back to ip header */
pbuf_header_force(p, (s16_t)(ip_current_header_tot_len() + UDP_HLEN));
icmp_port_unreach(ip_current_is_v6(), p);
}
#endif /* LWIP_ICMP || LWIP_ICMP6 */
pbuf_free(p);
}
} else {
pbuf_free(p);
}
end:
return;
}

BSD实现

在BSD中UDP头部数据结构如下:

1
2
3
4
5
6
7
8
9
10
/*
* Udp protocol header.
* Per RFC 768, September, 1981.
*/
struct udphdr {
u_short uh_sport; /* source port */
u_short uh_dport; /* destination port */
short uh_ulen; /* udp length */
u_short uh_sum; /* udp checksum */
};

IP首部实现可以参考lwip,不过笔者简单讲一讲一个相似的结构体struct ipovly。

struct ipovly是在BSD内部使用的一个覆盖IP首部之上的结构,其实并不是真正的IP首部,这是为了方便计算校验和。

1
2
3
4
5
6
7
8
9
10
11
/*
* Overlay for ip header used by other protocols (tcp, udp).
*/
struct ipovly {
caddr_t ih_next, ih_prev; /* for protocol sequence q's */
u_char ih_x1; /* (unused) */
u_char ih_pr; /* protocol */
short ih_len; /* protocol length */
struct in_addr ih_src; /* source internet address */
struct in_addr ih_dst; /* destination internet address */
};

IP/UDP首部结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

/*
* UDP kernel structures and variables.
*/
struct udpiphdr {
struct ipovly ui_i; /* overlaid ip structure */
struct udphdr ui_u; /* udp header */
};
#define ui_next ui_i.ih_next
#define ui_prev ui_i.ih_prev
#define ui_x1 ui_i.ih_x1
#define ui_pr ui_i.ih_pr
#define ui_len ui_i.ih_len
#define ui_src ui_i.ih_src
#define ui_dst ui_i.ih_dst
#define ui_sport ui_u.uh_sport
#define ui_dport ui_u.uh_dport
#define ui_ulen ui_u.uh_ulen
#define ui_sum ui_u.uh_sum

UDP发送

UDP发送实现简单介绍如下:

参数

inp:UDP 套接字在内核中的表示,包含了套接字的所有状态和配置信息,例如IP/UDP首部等信息

m:指向输出mbuf链

addr:一个可选指针,指向sockaddr_in结构中的目的地址

control:一个可选指针,指向sendmsg的控制信息

程序执行

1.如果我们没有使用sendmsg这些带控制信息的API,那么就丢弃control参数。

2.指定了目的地址:判断套接字传递过来的目的地址是不是任意地址(0.0.0.0),如果是,那么该地址无意义,返回错误。如果不是,需要通过in_pcbconnect函数临时连接到该目的地址,不过在连接之前需要暂时提升当前程序优先级并保存本地地址。因为临时连接会改变套接字中的外部地址、端口和本地地址, 此时的套接字信息是不完整的。如果此时有 UDP 数据报到达,协议栈在查找匹配的套接字时,可能因地址信息变化而将数据报交给错误的套接字,导致数据被错误的进程接收。

未指定目的地址:判断是不是已经连接上了(例如调用 connect() 函数实现连接),否则也报错。

connect() 函数的本质也是调用in_pcbconnect函数。

3.关于M_PREPEND和mtod可以看mbuf那一节。程序先申请内存,再设置IP/UDP首部,接下来就是计算校验和。

4.检验与计算校验和,在前文笔者简单介绍过校验和的计算,分为初始化、构建、计算、填写四个步骤,读者可以返回前文查看。

初始化与构建:将IP首部覆盖为ui_next,ui->ui_prev,并覆盖其他参数,这两个参数不会影响校验和结果,因为它们被设置为0。现在IP首部被覆盖为:ui_next,ui_prev,ui_x1,ui_pr,ui_len。其中IP首部中的ui_len、ui_src和ui_dst是真正影响校验和结果的参数。计算和填写就不多赘述了,是通过in_cksum实现的。

正如我们前面所说的,UDP的IPv4是允许不进行校验的,只要把校验和填0。而如果计算出来的结果是0,校验和会填写为0xffff。

5.udp_output会填充IP头部的三个字段:服务类型TOS、全长len、生存时间TTL,而ip_output则会填充其他字段。

6.使用ip_output发送数据报。

7.如果socket是临时连接上的,那么调用in_pcbdisconnect断连临时连接的socket,同时恢复本地地址并恢复程序优先级,之后程序结束。

其中in_pcbconnect将会耗费程序近三分之一的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
int udp_output(inp, m, addr, control)
register struct inpcb *inp;
register struct mbuf *m;
struct mbuf *addr, *control;
{
register struct udpiphdr *ui;
register int len = m->m_pkthdr.len;
struct in_addr laddr;
int s, error = 0;

if (control)
m_freem(control); /* XXX */

if (addr) {
laddr = inp->inp_laddr;
if (inp->inp_faddr.s_addr != INADDR_ANY) {
error = EISCONN;
goto release;
}
/*
* Must block input while temporarily connected.
*/
s = splnet();
error = in_pcbconnect(inp, addr);
if (error) {
splx(s);
goto release;
}
} else {
if (inp->inp_faddr.s_addr == INADDR_ANY) {
error = ENOTCONN;
goto release;
}
}
/*
* Calculate data length and get a mbuf
* for UDP and IP headers.
*/
M_PREPEND(m, sizeof(struct udpiphdr), M_DONTWAIT);
if (m == 0) {
error = ENOBUFS;
goto release;
}

/*
* Fill in mbuf with extended UDP header
* and addresses and length put into network format.
*/
ui = mtod(m, struct udpiphdr *);
ui->ui_next = ui->ui_prev = 0;
ui->ui_x1 = 0;
ui->ui_pr = IPPROTO_UDP;
ui->ui_len = htons((u_short)len + sizeof (struct udphdr));
ui->ui_src = inp->inp_laddr;
ui->ui_dst = inp->inp_faddr;
ui->ui_sport = inp->inp_lport;
ui->ui_dport = inp->inp_fport;
ui->ui_ulen = ui->ui_len;

/*
* Stuff checksum and output datagram.
*/
ui->ui_sum = 0;
if (udpcksum) {
if ((ui->ui_sum = in_cksum(m, sizeof (struct udpiphdr) + len)) == 0)
ui->ui_sum = 0xffff;
}
((struct ip *)ui)->ip_len = sizeof (struct udpiphdr) + len;
((struct ip *)ui)->ip_ttl = inp->inp_ip.ip_ttl; /* XXX */
((struct ip *)ui)->ip_tos = inp->inp_ip.ip_tos; /* XXX */
udpstat.udps_opackets++;
error = ip_output(m, inp->inp_options, &inp->inp_route,
inp->inp_socket->so_options & (SO_DONTROUTE | SO_BROADCAST),
inp->inp_moptions);

if (addr) {
in_pcbdisconnect(inp);
inp->inp_laddr = laddr;
splx(s);
}
return (error);

release:
m_freem(m);
return (error);
}

UDP输入

输入函数的目标是把UDP数据报放到合适的插口缓存内,然后唤醒该插口上因输入阻塞的所有进程或线程(唤醒需要依靠操作系统的IPC机制)。

分为三个步骤:

1.确认消息并简单处理输入的数据

2.处理目的地址是单播类型的数据报:提交给单个socket即可

3.处理目的地址是广播或多播类型的数据报:需要找到所有需要提交的socket

处理输入的代码如下:

代码主要是验证数据报长度,有两个参数:ip_len 与 uh_ulen,这两个参数都表示数据报长度,正常情况下,它们应该是相等的。

正常情况:

uh_ulen:UDP首部加UDP数据长度

ip_len:数据报内容长度

但我们都知道网络结构是分层的,下一层对上一层来说就是内容,所以有:

1
2
3
4
graph LR
Ba(UDP首部)-->Bb(UDP数据)
Aa(IP首部)--ip_len-->Ab(UDP首部)-->Ac(UDP数据)

ip_len 大于 uh_ulen:代码相信小的那个,也就是uh_ulen,此时调用m_adj丢弃mbuf后面多出来的部分,在校验和检验时会丢弃该数据报。

ip_len 小于 uh_ulen:长度出现严重错误,数据报必须立即被丢弃。

后面就是填写字段并计算校验和,在前文讲过了,就不过多赘述了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
void udp_input(m, iphlen)
register struct mbuf *m;
int iphlen;
{
register struct ip *ip;
register struct udphdr *uh;
register struct inpcb *inp;
struct mbuf *opts = 0;
int len;
struct ip save_ip;

udpstat.udps_ipackets++;
//此时还没有实现备份IP选项,因此需要丢弃
if (iphlen > sizeof (struct ip)) {
ip_stripoptions(m, (struct mbuf *)0);
iphlen = sizeof(struct ip);
}
//如果IP/UDP长度不合理,那么重新安排mbuf链,使第一个mbuf至少有28个字节
ip = mtod(m, struct ip *);
if (m->m_len < iphlen + sizeof(struct udphdr)) {
if ((m = m_pullup(m, iphlen + sizeof(struct udphdr))) == 0) {
udpstat.udps_hdrops++;//状态标志位,这些代码不影响理解
return;
}
ip = mtod(m, struct ip *);
}
uh = (struct udphdr *)((caddr_t)ip + iphlen);

/*
* Make mbuf data length reflect UDP length.
* If not enough data to reflect UDP length, drop.
*/
len = ntohs((u_short)uh->uh_ulen);
if (ip->ip_len != len) {
if (len > ip->ip_len) {
udpstat.udps_badlen++;
goto bad;
}
m_adj(m, len - ip->ip_len);
/* ip->ip_len = len; */
}
/*
* Save a copy of the IP header in case we want restore it
* for sending an ICMP error message in response.
*/
save_ip = *ip;

/*
* Checksum extended UDP header and data.
*/
if (udpcksum && uh->uh_sum) {
((struct ipovly *)ip)->ih_next = 0;
((struct ipovly *)ip)->ih_prev = 0;
((struct ipovly *)ip)->ih_x1 = 0;
((struct ipovly *)ip)->ih_len = uh->uh_ulen;
if (uh->uh_sum = in_cksum(m, len + sizeof (struct ip))) {
udpstat.udps_badsum++;
m_freem(m);
return;
}
}
后面的程序依次是:
分用多播和广播数据报
分用单播数据报
生成ICMP端口不可达差错

分用多播和广播数据报

与单播相比,多播与广播需要提交数据报给所有匹配的socket,其他的操作其实大同小异,读者可以参考下文的单播。但是,其中的难点就在于数据报的提交与回收。

实现逻辑:迭代遍历pcb,如果pcb不匹配,就使用continue关键字开启下一轮迭代,直到找到匹配的pcb或迭代到NULL。

如果pcb匹配,判断上一次是否匹配到pcb,如果是,那么调用sbappendaddr提交数据报到上一次的接收队列,并唤醒因为接收队列而阻塞的进程,之后使用last缓存指针保存pcb。

如果last指针为NULL,说明这是第一次匹配到pcb,那就用last保存该pcb,并进行一次是否结束迭代的判断。

但是在提交之前,我们需要拷贝一个副本,

sbappendaddr函数执行成功后会返回1,也就是说,提交数据报成功后就会唤醒阻塞进程。如果返回的是0,说明提交失败,这是因为缓冲区已满导致的问题,所以需要释放mbuf链。除此之外,该函数还会释放mbuf链,但是考虑到我们是多播或广播,可能不是最后一次提交,所以我们需要使用m_copy备份,并使用变量n保存地址,然后提交给接收队列。

这部分程序的精髓在于提交“上一次”,而非“这一次”。如果检查到匹配的pcb就提交数据报,那么我们不得不考虑最后一次可能出现的问题:我们会多留下一个数据报。但是使用“上一次”,当程序运行到这里时,表示前面的循环结束,可以判断是最后一次,因此无需备份,直接使用原始数据报即可。

(这段程序综合考虑了内存的回收释放,对不同情况处理得非常好)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
if (IN_MULTICAST(ntohl(ip->ip_dst.s_addr)) ||
in_broadcast(ip->ip_dst, m->m_pkthdr.rcvif)) {
struct socket *last;
/*
* Deliver a multicast or broadcast datagram to *all* sockets
* for which the local and remote addresses and ports match
* those of the incoming datagram. This allows more than
* one process to receive multi/broadcasts on the same port.
* (This really ought to be done for unicast datagrams as
* well, but that would cause problems with existing
* applications that open both address-specific sockets and
* a wildcard socket listening to the same port -- they would
* end up receiving duplicates of every unicast datagram.
* Those applications open the multiple sockets to overcome an
* inadequacy of the UDP socket interface, but for backwards
* compatibility we avoid the problem here rather than
* fixing the interface. Maybe 4.5BSD will remedy this?)
*/

/*
* Construct sockaddr format source address.
*/
udp_in.sin_port = uh->uh_sport;
udp_in.sin_addr = ip->ip_src;
m->m_len -= sizeof (struct udpiphdr);
m->m_data += sizeof (struct udpiphdr);
/*
* Locate pcb(s) for datagram.
* (Algorithm copied from raw_intr().)
*/
last = NULL;
for (inp = udb.inp_next; inp != &udb; inp = inp->inp_next) {
if (inp->inp_lport != uh->uh_dport)
continue;
if (inp->inp_laddr.s_addr != INADDR_ANY) {
if (inp->inp_laddr.s_addr !=
ip->ip_dst.s_addr)
continue;
}
if (inp->inp_faddr.s_addr != INADDR_ANY) {
if (inp->inp_faddr.s_addr !=
ip->ip_src.s_addr ||
inp->inp_fport != uh->uh_sport)
continue;
}

if (last != NULL) {
struct mbuf *n;

if ((n = m_copy(m, 0, M_COPYALL)) != NULL) {
if (sbappendaddr(&last->so_rcv,
(struct sockaddr *)&udp_in,
n, (struct mbuf *)0) == 0) {
m_freem(n);
udpstat.udps_fullsock++;
} else
sorwakeup(last);
}
}
last = inp->inp_socket;
/*
* Don't look for additional matches if this one does
* not have either the SO_REUSEPORT or SO_REUSEADDR
* socket options set. This heuristic avoids searching
* through all pcbs in the common case of a non-shared
* port. It * assumes that an application will never
* clear these options after setting them.
*/
if ((last->so_options&(SO_REUSEPORT|SO_REUSEADDR) == 0))
break;
}

if (last == NULL) {
/*
* No matching pcb found; discard datagram.
* (No need to send an ICMP Port Unreachable
* for a broadcast or multicast datgram.)
*/
udpstat.udps_noportbcast++;
goto bad;
}
if (sbappendaddr(&last->so_rcv, (struct sockaddr *)&udp_in,
m, (struct mbuf *)0) == 0) {
udpstat.udps_fullsock++;
goto bad;
}
sorwakeup(last);
return;
}

分用单播数据报

如果程序执行到这里,说明程序并没有执行多播操作,那么大概率是单播。

维护缓存指针

udp_last_inpcb是上一次接收数据报的端口的控制块指针,维护该指针的依据是许多程序往往具有时间局部性,也就是:经常运行的程序下一次往往也还运行,不经常运行的程序下一次大概率不会运行。

迭代全部端口

判断缓存指针指向的是不是现在要找的控制块,如果不是,就调用in_pcblookup函数迭代全部端口。

生成ICMP不可达错误

如果inp为NULL,说明没有找到端口,那么需要生成ICMP错误报告消息。

小结

虽然据TCP/IP卷二中描述,udp_last_inpcb其实没什么用,但笔者看来这里的程序相当巧妙,能从中感受到作者对时间局部性与空间局部性的考虑。inp被复用进行程序流控制,例如inp为NULL时,这说明肯定是进行了寻找操作且没有找到,因为赋值缓存指针必定使inp不为NULL,如果inp匹配缓存指针或者是找到符合的端口,那么生成ICMP错误的代码就直接跳过。

inp这个变量使用register关键字修饰,并被反复使用。可以想象它的值第一次被保存在寄存器后,后面的程序只需要反复操作该寄存器,而不用频繁对其他变量取地址并引用,这对于指令预取和流水线机制都是十分有利的,因为寄存器本身就是速度最快的内存。

(整个UDP协议的代码都写得非常巧妙,精简凝练,对变量的复用不仅能体现全部的控制流,还能体现时间局部性)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
* Locate pcb for datagram.
*/
inp = udp_last_inpcb;
if (inp->inp_lport != uh->uh_dport ||
inp->inp_fport != uh->uh_sport ||
inp->inp_faddr.s_addr != ip->ip_src.s_addr ||
inp->inp_laddr.s_addr != ip->ip_dst.s_addr) {
inp = in_pcblookup(&udb, ip->ip_src, uh->uh_sport,
ip->ip_dst, uh->uh_dport, INPLOOKUP_WILDCARD);
if (inp)
udp_last_inpcb = inp;
udpstat.udpps_pcbcachemiss++;
}
if (inp == 0) {
udpstat.udps_noport++;
if (m->m_flags & (M_BCAST | M_MCAST)) {
udpstat.udps_noportbcast++;
goto bad;
}
*ip = save_ip;
ip->ip_len += iphlen;
icmp_error(m, ICMP_UNREACH, ICMP_UNREACH_PORT, 0, 0);
return;
}

单播提交数据报给对应端口

程序执行到这里都没有返回,说明并不是多播,但肯定是匹配了上一次的控制块或者是找到了控制块,那么是单播。

返回源站IP地址和端口

还记得我们在应用层定义的sockaddr_in变量吗?在这里会把IP地址和端口保存到这个变量中。

返回控制信息

如果定义了控制选项,那么可以使用udp_saveopt分配一个mbuf缓存控制信息(在这里是IP地址)并返回。

提交数据报到socket的接收队列

这一步可以使用sbappendaddr完成,但是在执行之前,需要修正第一个mbuf,忽略UDP首部和IP首部。提交数据报后,需要使用sorwakeup函数唤醒所有阻塞在接收队列的进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

/*
* Construct sockaddr format source address.
* Stuff source address and datagram in user buffer.
*/
udp_in.sin_port = uh->uh_sport;
udp_in.sin_addr = ip->ip_src;
if (inp->inp_flags & INP_CONTROLOPTS) {
struct mbuf **mp = &opts;

if (inp->inp_flags & INP_RECVDSTADDR) {
*mp = udp_saveopt((caddr_t) &ip->ip_dst,
sizeof(struct in_addr), IP_RECVDSTADDR);
if (*mp)
mp = &(*mp)->m_next;
}
下面宏条件编译的代码还没有实现,暂时忽略
#ifdef notyet
/* options were tossed above */
if (inp->inp_flags & INP_RECVOPTS) {
*mp = udp_saveopt((caddr_t) opts_deleted_above,
sizeof(struct in_addr), IP_RECVOPTS);
if (*mp)
mp = &(*mp)->m_next;
}
/* ip_srcroute doesn't do what we want here, need to fix */
if (inp->inp_flags & INP_RECVRETOPTS) {
*mp = udp_saveopt((caddr_t) ip_srcroute(),
sizeof(struct in_addr), IP_RECVRETOPTS);
if (*mp)
mp = &(*mp)->m_next;
}
#endif
}
iphlen += sizeof(struct udphdr);
m->m_len -= iphlen;
m->m_pkthdr.len -= iphlen;
m->m_data += iphlen;
if (sbappendaddr(&inp->inp_socket->so_rcv, (struct sockaddr *)&udp_in,
m, opts) == 0) {
udpstat.udps_fullsock++;
goto bad;
}
sorwakeup(inp->inp_socket);
return;
bad:
m_freem(m);
if (opts)
m_freem(opts);
}