LwIP从入门到放弃之(七)—用户数据协议UDP

IP协议提供了在各个主机之间传送数据报的功能,但是各个主机并不是数据的最终目的地,数据的最终目的地应该是主机上某个特定的应用程序。而来执行这个操作的便是我们的传输层协议,典型的传输层协议包括UDP和TCP。这章主要介绍UDP协议,它有着很高的传输速率,在局域网环境或者视频播放领域有着广泛的作用。

1. 背景知识

1.1 UDP协议

UDP称为用户数据报协议,是一种无连接的、不可靠的传输协议,它只在低级程度上实现了上述的传输层功能。UDP只是简单地完成数据从一个进程到另一个进程的交付,它没有提供任何流量控制机制。

UDP协议的可靠性如此差,那为何还要使用它呢?首先,这里的不可靠定义还是要根据具体使用环境来的,在现在的高可靠性、低时延的局域网环境下,使用UDP协议出现传输错误的可能性很小,但使用UDP却可以带来数据递交效率和处理速度的提升,因为它省去了连接建立、数据确认、流量控制等一系列过程。从代码的实现角度讲,UDP协议的代码量非常小,对于小型嵌入式设备来说,在局域网中使用UDP来实现通信还是很合适的。除此之外,UDP也经常在那些对轻微数据差错不敏感的应用中被使用到,例如实时视频传输、网络电话等。

1.2 端口号

UDP报文的最终目的站是用户的某个进程,UDP协议使用端口号来识别主机上的不同进程。这里简单介绍一下端口号:每台主机都包含一组成为协议端口的抽象目的点,每个协议口用一个正整数来标志。在TCP/IP协议族中,端口号范围为0~65535,进程可以绑定到某一个端口上,UDP报文需要在其内部指出该报文需递交的内部端口号。

1.3 UDP报文的交付

用户进程使用UDP来传送数据时,UDP协议会在数据前加上首部组成UDP报文,并交给IP协议来发送,而IP层将报文封装在IP数据报中并交给底层发送,在底层,IP数据报会被封装在物理数据帧中。因此,一份用户数据在被发送时,经历了三次封装过程,如图所示。
在这里插入图片描述

在接收端,物理网络先接收到数据帧,然后逐层地将数据递交给上层协议,每一层都在向上层递交前去除掉一个首部。在UDP层,它将从IP层得到UDP报文,UDP协议会根据该报文首部中的目的端口字段将报文递交给用户进程,绑定到这个目的端口的进程将得到报文中的数据。从两台对等的机器上看,目的主机上IP层传递给UDP层的报文等同于源主机上UDP层传递给IP层的报文,目的主机上UDP层传递给用户进程的数据正好是源主机上用户进程传递给UDP层的数据。

1.4 UDP报文格式

UDP报文格式很简单,它由4个16位字段组成,分别指出了该用户数据从哪个端口来、到哪个端口去、总长度和校验和。
在这里插入图片描述

源端口号和目的端口号都是16位的,这样端口号的取值范围在0到65535之间。源端源端口号是主机上发送该用户数据报的进程所绑定的本地端口号,而目的端口是该报文要送达的目的主机上应用进程所绑定的端口号。在用户数据报的发起端(通常作为客户机),通常上图报文结构会将目的端口号填写为服务器上某个熟知的端口,对源端口号字段的填写则是可选的,如果客户端期望服务器为自己返回数据,则必须填写源端口号字段,服务器会在收到的报文中提取到这个源端口号,并在返回数据时使用到。当然,客户端进程也可以不填写源端口字段,此时该字段置0,但若选用,源端口字段往往是一个随机分配的短暂端口号。
16位的总长度字段定义了用户数据报的总长度,包括首部长度和数据区长度,以字节为单位。显然,该字段的最小值为8,它恰好为一个UDP首部大小,此时的报文只有首部没有数据。UDP数据区的数据最多只能有65507字节(65535-8-20),因为我们在讲解IP数据报首部时,IP首部中的总长度字段也为16位,UDP要使用IP层来传送数据报,所以其数据长度也必须满足IP首部中的长度要求。

2. UDP数据结构和控制块

LwIP中描述一个UDP的数据报首部,用结构体udp_hdr表示,代码如下:

#define UDP_HLEN 8//定义UDP数据报首部长度
PACK_STRUCT_BEGIN
struct udp_hdr {
PACK_STRUCT_FIELD(ul6_t src);//源端口号
PACK_STRUCT_FIELD(u16_t dest);//目的端口号
PACK_STRUCT_FIELD(u16_tlen);//总长度
PACK_STRUCT_FIELD(ul6_t chksum);//校验和
} PACK_STRUCT_STRUCT;
PACK STRUCT_END

UDP控制块是整个UDP协议中最重要的东西,控制块里面描述了一个UDP连接的所有信息,包括源端口号、目的端口号、源IP地址、目的IP地址等。用户的UDP编程,本质上都是对UDP的控制块进行操作。
系统为每一个连接分配一个UDP控制块,并将其组织在一个链表上,当UDP层收到IP层的报文时,会去遍历整个链表,找出与报文部首匹配的控制块,并调用控制块中注册的函数来完成报文的处理。

//下面定义宏IP_PCB,它是与IP层相关的字段
#define IP_PCB\
struct ip_addr local_ip;\        //本地IP地址
struct ip_addr remote_ip;\    //远端IP地址
u16_t so_options;\                 //socket选项
u8_t tos;\                                 //服务类型
u8_t ttl                                     //生存时间(TTL)
//定义IP控制块
struct ip_pcb{
IP_PCB; //宏IP_PCB相关的字段
};
//定义两个宏,用于控制块的flags字段,标识控制块的状态信息
#define UDP_FLAGS_NOCHKSUM 0x01U//不进行校验和的计算
#define UDP_FLAGS_CONNECTED 0x04U//控制块已和远端建立连接
//定义UDP控制块结构体
struct udp_pcb{
IP_PCB;//宏IPPCB中的各个字段
struct udp_pcb *next;//用于将控制块组织成链表的指针
u8_t flags; //控制块状态字段
u16_t local_port, remote_port; //保存本地端口号和远端端口号,使用主机字节序
void (* recv)(void *arg, struct udp_pcb *pcb, struct pbuf* p,//处理数据时的回调函数
struct ip_addr *addr, ul6_t port);
void *recv _ang;  //当调用回调函数时,将传递给函数的用户定义数据信息
};

next字段是一个UDP控制块类型的指针,系统会将所有控制块组织在一个链表上,链表头指针为udp_pcbs,next字段就是用来构成链表的。在后面会看到,UDP协议实现的本质是对链表udp_pcbs上各个UDP控制块的操作。
flags字段用于标识UDP控制块的状态信息,当前有两个状态信息比较重要,正如上面的宏定义所示,第一个标志该控制块是否进行校验和的计算,当flags的无校验位(位0)为1时,表示在发送报文时不计算首部中的校验和字段。第二个标志该控制块是否处于连接状态(位2),当某个控制块处于连接状态时,表示它内部已经完整地记录了关于通信双方的IP地址和端口号信息。
接下来的两个字段local_port和remote_port是描述通信双方的重要字段,它们分别标识本地端口号和远端端口号,当UDP接收到一个报文时,会遍历链表udp_pcbs上的所有控制块,检查其中的本地端口号与报文首部中的目的端口号是否匹配,并将报文递交给匹配成功的控制块处理。
匹配成功的控制块怎么来处理报文中的数据呢,这就是通过下面这个字段一函数指针recv来完成的了。用户程序在初始化一个控制块时,需要在该字段注册自定义的报文处理函数,在内核接收到报文并匹配到某个控制块后,通过函数指针recv来回调用户自定义的处理函数,这样就最终完成了报文向用户程序的递交。

3. 控制块操作函数

3.1 新建控制块

任何想使用UDP服务的应用程序都必须拥有一个控制块,并把控制块绑定到相应的端口号上。新建一个控制块调用的接口是udp_new(void)。
在接收报文时,端口号将作为报文终点选择的唯一依据。新建控制块函数很简单,就是在内存池中为UDP控制块申请一个MEMP_UDP_PCB类型的内存池空间,并初始化相关字段。

3.2 绑定控制块

当作为服务器程序时,必须手动为控制块绑定一个熟知的端口号,客户端程序能够使用一个熟知的端口号和服务器进行通信;当作为客户端程序时,手动绑定端口号不是必须的,在和服务器通信前,UDP会自动为控制块绑定一个短暂端口号。端口号绑定的本质就是设置控制块中的local_ip和local_port字段。绑定一个本地IP地址和端口号的接口是

//函数功能:为UDP控制块绑定一个本地IP地址和端口号
//参数pcb:指向要操作的控制块指针
//参数ipaddr:本地IP地址,若为IP_ADDR_ANY(0),表示任意网络接口结构的IP地址
//参数port:本地端口号,若为0,则函数将自动为控制块分配一个有效的短暂端口号
udp_bind(struct udp_pcb *pcb, struct ip_addr *ipaddr, u16_t port);

3.3 连接控制块

与绑定控制块函数相对应,连接控制块函数完成控制块中remote_ip和remote_port的设置。只有绑定了本地IP地址和端口号,以及远端IP地址和端口号的控制块才会处于连接状态。

//函数功能,为UDP控制块绑定一个远端IP地址和端口号
//参数pcb:指向要操作的控制块
//参数ipaddr:远端IP地址
//参数port:远端端口号
err_t udp_connect(struct udp_pcb*pcb, struct ip_addr *ipaddr, ul6_t port);

4. 报文处理函数

4.1 报文的发送

UDP报文的发送依靠IP层提供的服务,用户程序可以按照如下过程使用UDP发送数据:首先应该为数据开辟一个pbuf,将用户数据填装到pbuf的数据区域,最好pbuf数据区前面已经为UDP、IP和以太网首部预留了足够的空间;然后,用户程序将该数据pbuf作为参数,调用UDP提供的数据发送函数udp_sendudp_sendto,当UDP层发送数据pbuf时,它会在该pbuf中填入UDP首部区域形成一个完整的UDP报文,最后调用IP层的函数来发送报文。

//函数功能:使用一个处于连接状态的UDP控制块发送用户数据pbuf
//参数pcb:UDP控制块
//参数p:存储持发送数据的pbuf
err_t udp_send(struct udp_pcb *pcb, struct pbuf*p)
//函数功能:将用户数据pbuf发送到指定的远端P地址和端口号上
//参数pcb:发送数据的UDP控制块
//参数p存储特发送数据的pbuf
//参数dst_ip和dst_port:远端IP地址和端口号
err_t udp_sendto(struct udp pcb *pcb, struct pbuf *p, stuct ip_addr *dst_ip,ul6_t dst_port)

4.2 报文的接受与递交

在IP层,当收到一个包含UDP报文的数据报时,函数udp_input会被调用,以处理报文。该函数接口如下,其处理的过程也相对繁琐,首先是进行一些报文合法性的一些检验,然后根据报文中的端口信息查找匹配的UDP控制块,并把报文数据递交给控制块中注册的用户自定义函数处理。

//函数功能:UDP报文处理函数
//参数pbuf:IP层接收到的包含UDP报文的数据报pbuf, payload指针指向IP首部
//参数inp:接收到IP数据报的网络接口结构
void udp_input(struct pbuf *p, struct netif *inp)

下图显示了本章讲到的几个与UDP报文处理密切相关的函数以及他们之间的关系:
在这里插入图片描述

注:LwIP协议栈源代码我已上传,需要的小伙伴欢迎下载:
https://download.csdn.net/download/rgxiwei/15724471


版权声明:本文为rgxiwei原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>