UDP协议

目录

1 UDP 协议简介        

UDP 报文封装流程

UDP 报文的数据结构

UDP 首部结构

UDP 控制块

发送 UDP 报文

UDP 报文接收


1 UDP 协议简介        

        UDP 协议是 TCP/IP 协议栈的传输层协议,是一个简单的面向数据报的协议,在传输层中还有另一个重要的协议,那就是 TCP 协议, TCP 协议的知识笔者会在下一章节中讲解。 UDP不提供数据包分组、组装,不能对数据包进行排序,当报文发送出去后无法知道是否安全、完整的到达。 UDP 除了这些缺点外肯定有它自身的优势,由于 UDP 不属于连接型协议,因而消耗资源小,处理速度快,所以通常在音频、视频和普通数据传输时使用 UDP 较多。 UDP 数据报结构如下图所示。

UDP 首部有 8 个字节,由 4 个字段构成,每个字段都是两个字节, 这些字段的作用如下:

  1. 源端口: 源端口号,需要对方回信时选用,不需要时全部置 0。
  2. 目的端口: 目的端口号,在终点交付报文的时候需要用到。
  3. 长度: UDP 的数据报的长度(包括首部和数据)其最小值为 8(只有首部)。
  4. 校验和:检测 UDP 数据报在传输中是否有错,有错则丢弃。

        UDP 协议使用端口号为不同的应用保留各自的数据传输通道, UDP 和 TCP 协议都是采用端口号对同一时刻内多项应用同时发送和接收数据,而数据接收方则通过目标端口接收数据。有的网络应用只能使用预先为其预留或注册的静态端口;而另外一些网络应用则可以使用未被注册的动态端口。因为 UDP 报头使用两个字节存放端口号,所以端口号的有效范围是从 0 到65535。一般来说,大于 49151 的端口号都代表动态端口。

        数据报的长度是指包括报头和数据部分在内的总字节数。因为报头的长度是固定的,所以该数据区域主要被用来计算可变长度的数据部分(又称为数据负载)。数据报的最大长度根据操作环境的不同而各异。从理论上说,包含报头在内的数据报的最大长度为 65535 字节。

        UDP 协议使用报头中的校验和来保证数据的安全。校验和首先在数据发送方通过特殊的算法计算得出,在传递到接收方之后,还需要再重新计算。如果某个数据报在传输过程中被第三方篡改或者由于线路噪音等原因受到损坏,发送和接收方的校验计算和将不会相符,由此UDP 协议可以检测是否出错。

UDP 报文封装流程

        UDP 报文与 TCP 报文一样也是由 UDP/TCP 首部+数据区域组成, UDP 协议是位于传输层,该层是应用层的下一层,当用户发送数据时候,需要选择使用那种协议发送出去,如果使用UDP 协议, 则 UDP 协议就会简单的把数据封装起来, UDP 报文结构如下图所示:

UDP 报文的数据结构

UDP 首部结构

        从上面可知, UDP 首部包含了四个字段,这些字段在 lwIP 内核中由结构体 udp_hdr 描述,该结构体如下所示:

struct udp_hdr {
PACK_STRUCT_FIELD(u16_t src); /* 源端口 */
PACK_STRUCT_FIELD(u16_t dest); /* 目的端口 */
PACK_STRUCT_FIELD(u16_t len); /* 长度 */
PACK_STRUCT_FIELD(u16_t chksum); /* 校验和 */
} PACK_STRUCT_STRUCT;

UDP 控制块

        lwIP 为了更好的管理 UDP 报文, 它定义了一个 UDP 控制块,使用该控制块来记录 UDP的通讯信息,例如源端口、目的端口,源 IP 地址和目的 IP 地址以及收到的数据回调函数等信息, lwIP 把多个 UDP 控制块使用链表形式连接起来,在处理时候遍历列表即可, 该 UDP 控制块结构如以下所示:

#define IP_PCB \
ip_addr_t local_ip; \/* 本地 ip 地址与远端 IP 地址 */
ip_addr_t remote_ip; \
u8_t netif_idx; \ /* 绑定 netif 索引 */
u8_t so_options; \ /* Socket 选项 */
u8_t tos; \ /* 服务类型 */
u8_t ttl \ /* 生存时间 */
IP_PCB_NETIFHINT/* 链路层地址解析提示 */
struct ip_pcb {
IP_PCB;
};
struct udp_pcb {
IP_PCB;
struct udp_pcb *next; /* 指向下一个控制块 */
u8_t flags; /* 控制块状态 */
u16_t local_port, remote_port; /* 本地端口和目标端口 */
udp_recv_fn recv; /* 接收回调函数 */
void *recv_arg; /* 用户为 recv 回调提供的参数 */
}

        对于 RAW 的 API 接口来讲,上图中的 recv 由用户提供这个函数,而 NETCONN 和SOCKET 接口无需用户提供回调函数,因为 lwIP 内核已经注册了该回调函数,所以数据到来时,该函数把数据以邮箱的方式发送至 NETCONN 和 SOCKET 对应的接口。 

发送 UDP 报文

        UDP 报文发送函数是由 udp_sendto_if_src 实现,其实它最终调用 ip_output_if_src 函数把数据报递交给网络层处理, udp_sendto_if_src 函数如下所示:

err_t
udp_sendto_if_src(struct udp_pcb *pcb, /* udp 控制块 */
struct pbuf *p, /* pbuf 网络数据包 */
const ip_addr_t *dst_ip, /* 目的 IP 地址 */
u16_t dst_port, /* 目的端口 */
struct netif *netif, /* 网卡信息 */
const ip_addr_t *src_ip) /* 源 IP 地址 */
{
struct udp_hdr *udphdr;
err_t err;
struct pbuf *q;
u8_t ip_proto;
u8_t ttl;
/* 第一步:判断控制块是否为空和远程 IP 地址是否为空 */
if (!IP_ADDR_PCB_VERSION_MATCH(pcb, src_ip) ||
!IP_ADDR_PCB_VERSION_MATCH(pcb, dst_ip))
{
return ERR_VAL;/* 放回错误 */
}
/* 如果 PCB 还没有绑定到一个端口,那么在这里绑定它 */
if (pcb->local_port == 0)
{
err = udp_bind(pcb, &pcb->local_ip, pcb->local_port);
if (err != ERR_OK)
{
return err;
}
}
/* 判断添加 UDP 首部会不会溢出 */
if ((u16_t)(p->tot_len + UDP_HLEN) < p->tot_len)
{
return ERR_MEM;
}
/* 第二步:没有足够的空间将 UDP 首部添加到给定的 pbuf 中 */
if (pbuf_add_header(p, UDP_HLEN))
{
/* 在单独的新 pbuf 中分配标头 */
q = pbuf_alloc(PBUF_IP, UDP_HLEN, PBUF_RAM);
/* 在单独的新 pbuf 中分配标头 */
if (q == NULL)
{
return ERR_MEM;/* 返回错误 */
}
if (p->tot_len != 0)
{
/* 把首部 pbuf 和数据 pbuf 连接到一个 pbuf 链表上 */
pbuf_chain(q, p);
}
} e
lse /* 如果有足够的空间 */
{
/* 在数据 pbuf 中已经预留 UDP 首部空间 */
/* q 指向 pbuf */
q = p;
}
/* 第三步:设置 UDP 首部信息 */
/* 指向它的 UDP 首部 */
udphdr = (struct udp_hdr *)q->payload;
/* 填写本地 IP 端口 */
udphdr->src = lwip_htons(pcb->local_port);
/* 填写目的端口 */
udphdr->dest = lwip_htons(dst_port);
/* 填写校验和 */
udphdr->chksum = 0x0000;
/* 设置长度 */
udphdr->len = lwip_htons(q->tot_len);
/* 设置协议类型 */
ip_proto = IP_PROTO_UDP;
/* 设置生存时间 */
ttl = pcb->ttl;
/* 第四步:发送到 IP 层 */
NETIF_SET_HWADDRHINT(netif, &(pcb->addr_hint));
err = ip_output_if_src(q, src_ip, dst_ip, ttl, pcb->tos, ip_proto, netif);
NETIF_SET_HWADDRHINT(netif, NULL);
MIB2_STATS_INC(mib2.udpoutdatagrams);
if (q != p)
{
/*释放内存 */
pbuf_free(q);
q = NULL;
}
UDP_STATS_INC(udp.xmit);
return err;
}

        首先判断源 IP 地址和目标 IP 地址是否为空,接着判断本地端口是否为空,判断完成之后添加 UDP 首部,最后调用 ip_output_if_src 函数把数据报递交给网络层处理。

UDP 报文接收

网络层处理数据报完成之后,由 udp_input 函数把数据报递交给传输层,该函数源码所示:

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;
LWIP_UNUSED_ARG(inp);
PERF_START;
UDP_STATS_INC(udp.recv);
/* 第一步:判断数据报长度少于 UDP 首部 */
if (p->len < UDP_HLEN)
{
UDP_STATS_INC(udp.lenerr);
UDP_STATS_INC(udp.drop);
MIB2_STATS_INC(mib2.udpinerrors);
pbuf_free(p); /* 释放内存,掉弃该数据报 */
goto end;
}
/* 指向 UDP 首部 */
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_debug_print(udphdr);
pcb = NULL;
prev = NULL;
uncon_pcb = NULL;
/* 第二步:遍历 UDP pcb 列表以找到匹配的 pcb */
for (pcb = udp_pcbs; pcb != NULL; pcb = pcb->next)
{
/* 第三步:比较 PCB 本地 IP 地址与端口*/
if ((pcb->local_port == dest) &&
(udp_input_local_match(pcb, inp, broadcast) != 0))
{
/* 判断 UDP 控制块的状态 */
if (((pcb->flags & UDP_FLAGS_CONNECTED) == 0) &&
((uncon_pcb == NULL)))
{
/* 如果未找到使用第一个 UDP 控制块 */
uncon_pcb = pcb;
}
/* 判断目的 IP 是否为广播地址 */
else if (broadcast &&
ip4_current_dest_addr()->addr == IPADDR_BROADCAST)
{
/* 全局广播地址(仅对 IPv4 有效;之前检查过匹配)*/
if (!IP_IS_V4_VAL(uncon_pcb->local_ip)
|| !ip4_addr_cmp(ip_2_ip4(&uncon_pcb->local_ip),
netif_ip4_addr(inp)))
{
/* 检查此 pcb , uncon_pcb 与输入 netif 不匹配 */
if (IP_IS_V4_VAL(pcb->local_ip) &&
ip4_addr_cmp(ip_2_ip4(&pcb->local_ip),
netif_ip4_addr(inp)))
{
/* 更好的匹配 */
uncon_pcb = pcb;
}
}
}
/* 比较 PCB 远程地址+端口和 UDP 源地址+端口 */
if ((pcb->remote_port == src) &&
(ip_addr_isany_val(pcb->remote_ip) ||
ip_addr_cmp(&pcb->remote_ip, ip_current_src_addr())))
{
/* 第一个完全匹配的 PCB */
if (prev != NULL)
{
/* 将 pcb 移到 udp_pcbs 前面 */
prev->next = pcb->next;
pcb->next = udp_pcbs;
udp_pcbs = pcb;
}
else
{
UDP_STATS_INC(udp.cachehit);
}
break;
}
}
prev = pcb;
}
/* 第五步:找不到完全匹配的 UDP 控制块
将第一个未使用的 UDP 控制块作为匹配结果 */
if (pcb == NULL)
{
pcb = uncon_pcb;
}
/* 检查校验和是否匹配或是否匹配 */
if (pcb != NULL)
{
for_us = 1;
}
else
{
#if LWIP_IPV4
if (!ip_current_is_v6())
{
for_us = ip4_addr_cmp(netif_ip4_addr(inp), ip4_current_dest_addr());
}
#endif /* LWIP_IPV4 */
}
/* 第六步:如果匹配 */
if (for_us)
{
/* 调整报文的数据区域指针 */
if (pbuf_header(p, -UDP_HLEN))
{
UDP_STATS_INC(udp.drop);
MIB2_STATS_INC(mib2.udpinerrors);
pbuf_free(p);
goto end;
}
/* 如果找到对应的控制块 */
if (pcb != NULL)
{
MIB2_STATS_INC(mib2.udpindatagrams);
/* 回调函数,将数据递交给上层应用 */
if (pcb->recv != NULL)
{
/* 回调函数 recv 需要负责释放 p */
pcb->recv(pcb->recv_arg, pcb, p, ip_current_src_addr(), src);
}
else
{
/* 如果 recv 函数没有注册,直接释放 p */
pbuf_free(p);
goto end;
}
}
else/* 第七步:没有找到匹配的控制块,返回端口不可达 ICMP 报文 */
{
if (!broadcast && !ip_addr_ismulticast(ip_current_dest_addr()))
{
/* 将数据区域指针移回 IP 数据报首部 */
pbuf_header_force(p, (s16_t)(ip_current_header_tot_len() +
UDP_HLEN));
/* 返回一个端口不可达 ICMP 差错控制报文到源主机中 */
icmp_port_unreach(ip_current_is_v6(), p);
}
UDP_STATS_INC(udp.proterr);
UDP_STATS_INC(udp.drop);
MIB2_STATS_INC(mib2.udpnoports);
pbuf_free(p); /* 掉弃该数据包 */
}
}
/* 如果不匹配,则掉弃该数据包 */
else
{
pbuf_free(p);
}
end:
PERF_STOP("udp_input");
return;
}

        此函数根据接收数据包的 UDP 首部信息遍历 UDP 控制块链表,找到对应的控制块之后 lwIP 内核把接收到的数据包递交给 pcb->recv 回调函数处理。

  • 20
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

理想本征半导体

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

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

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

打赏作者

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

抵扣说明:

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

余额充值