skaiuijing

UDP是一个无连接的协议。

笔者使用的源码为lwip2.20版本,4.4BSD-lite。

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

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

pbuf和mbuf

pbuf和mbuf分别是两种协议栈中携带数据包的实现。

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

IP报头结构

IP报头结构如下:

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

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
26

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;
}

UDP控制块

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

规范

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

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

前四个元素组成UDP头部。

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

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层的报文结构,对于IP层来说,发送给哪个传输层协议需要根据字段中的值进行判断,也就是服务类型,IP层根据这个值把数据报分离到特定的传输协议。

因此不同协议可以使用的端口号是独立的,不同的协议使用相同的端口号是不会冲突的。同样的,对于两个不同的服务器来说,只要它们使用不同的传输协议,那么它们可以使用相同的端口号和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
26
27
28
29
30
31
32
33
34
35

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伪头部

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

UDP伪头部下面就是UDP头部和数据部分了,数据部分后面可能会有一个填充值。

其中填充值的作用是字节对齐,确保UDP伪头部的大小是偶数,也就是12字节。这样方便计算校验和,因为校验和算法只相加16个字,我们都知道字的大小基本由CPU的寻址大小决定(32位的CPU,字的大小往往是4字节,不过也可能由编译器决定),所以校验和必定是偶数个字节。

这个填充字节实际上不会被发送出去的(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发送

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

发送函数如下:

其实就是先检查几遍,再把UDP首部添加到pbuf中,然后填写UDP各个字段,最后输出到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
/** @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 (((pcb->flags & UDP_FLAGS_MULTICAST_LOOP) != 0) && ip_addr_ismulticast(dst_ip)) {
q->flags |= PBUF_FLAG_MCASTLOOP;
}

/* 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
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
/**
* 处理接收到的UDP数据报。
*
* 这个函数找到对应的UDP控制块,并将数据报传递给控制块的接收函数。
* 如果没有找到控制块或者数据报不正确,则释放数据报。
*
* @param p 指向UDP头部的数据报缓冲区
* @param inp 接收到数据报的网络接口
*/
void udp_input(struct pbuf *p, struct netif *inp) {
struct udp_hdr *udphdr;
struct udp_pcb *pcb, *prev;
u16_t src, dest;
u8_t broadcast;


udphdr = (struct udp_hdr *)p->payload;
broadcast = ip_addr_isbroadcast(ip_current_dest_addr(), ip_current_netif());
src = lwip_ntohs(udphdr->src); // 源端口
dest = lwip_ntohs(udphdr->dest); // 目的端口

// 遍历UDP控制块列表,寻找匹配的PCB
for (pcb = udp_pcbs; pcb != NULL; pcb = pcb->next) {
if ((pcb->local_port == dest) &&
(udp_input_local_match(pcb, inp, broadcast) != 0)) {
if ((pcb->remote_port == src) &&
(ip_addr_isany_val(pcb->remote_ip) || ip_addr_eq(&pcb->remote_ip, ip_current_src_addr()))) {
break;
}
prev = pcb;
}
}

// 找到匹配的PCB并处理数据报
if (pcb != NULL) {
#if CHECKSUM_CHECK_UDP
if (udphdr->chksum != 0 && ip_chksum_pseudo(p, IP_PROTO_UDP, p->tot_len, ip_current_src_addr(), ip_current_dest_addr()) != 0) {
pbuf_free(p);
return;
}
#endif
// 移除UDP头部
if (pbuf_remove_header(p, UDP_HLEN)) {
pbuf_free(p);
return;
}

// 调用接收回调函数
if (pcb->recv != NULL) {
pcb->recv(pcb->recv_arg, pcb, p, ip_current_src_addr(), src);
} else {
pbuf_free(p);
}
} else {
pbuf_free(p);
}
}