UDP理论
每一个UDP连接都对应一个UDP控制块,UDP协议的实现就是对这些控制块结构成员进行操作。为什么需要控制块链表?为了让协议栈可以实现多个连接,可以多个网络进程同时进行。最后这些控制块通过链表连接在一起。其中链接属性为外部的udp_pcbs是一个全局变量,指向控制块变量首地址。即这是一个指针变量,其数值是链表首地址。
extern struct udp_pcb *udp_pcbs;//链表首地址,声明为全局变量
//标识控制块的状态信息
#define UDP_FLAGS_NOCHKSUM 0x01U
//不进行校验和计算
#define UDP_FLAGS_UDPLITE 0x02U
#define UDP_FLAGS_CONNECTED 0x04U
//已经连接(UDP不需要握手协议所以连接成功仅仅是内部已经完整记录了双方IP地址和端口)
#define UDP_FLAGS_MULTICAST_LOOP 0x08U
/** Function prototype for udp pcb receive callback functions
* addr and port are in same byte order as in the pcb
* The callback is responsible for freeing the pbuf
* if it's not used any more.
*
* ATTENTION: Be aware that 'addr' points into the pbuf 'p' so freeing this pbuf
* makes 'addr' invalid, too.
*
* @param arg user supplied argument (udp_pcb.recv_arg)
* @param pcb the udp_pcb which received data
* @param p the packet buffer that was received
* @param addr the remote IP address from which the packet was received
* @param port the remote port from which the packet was received
*/
typedef void (*udp_recv_fn)(void *arg, struct udp_pcb *pcb, struct pbuf *p,
ip_addr_t *addr, u16_t port);//定义回调函数类型,typedef就这样使用,不然定义一个函数指针会很复杂。
// Processing Control Block 编程就是操作控制块
struct udp_pcb {
/* Common members of all PCB types */
IP_PCB; //宏定义IP控制块字段,使用到里面的本地IP地址和远端IP地址
/* Protocol specific PCB members */
struct udp_pcb *next;//将控制块构成链表
u8_t flags;//状态信息
/** ports are in host byte order */
u16_t local_port, remote_port;
/*
本地和远端端口,UDP收到报文,会遍历链表,检查本地端口号和报文中目的的端口,匹配成 功,那么调用对应控制块里面的函数指针处理报文。这样完成报文最终递交给用户程序。
*/
#if LWIP_IGMP
/** outgoing network interface for multicast packets */
ip_addr_t multicast_ip;
#endif /* LWIP_IGMP */
#if LWIP_UDPLITE
/** used for UDP_LITE only */
u16_t chksum_len_rx, chksum_len_tx;
#endif /* LWIP_UDPLITE */
/** receive callback function */
udp_recv_fn recv;//回调函数,这个变量类型通过typedef定义了。匹配成功进行报文处理函数,最终报文向应用程序提交了数据
/** user-supplied argument for the recv callback */
void *recv_arg; //回调函数中,用户可以用过第一个参数传递一些信息,最后保存在结构体这里,这个用户参数有什么作用?
}
当有多个udp用户进程时候就会产生多个udp控制块。用户进程发送数据包,根据目标IP和端口通过IP层发送出去;用户进程接收数据包,通过数据包里面的目标IP和端口选择udp控制块里面对应的回调函数。
总体简单操作就是遍历控制块链表。
其中用户通过回调函数的方式被协议栈调用,这个就叫做raw/callback API。这个大型开源项目里面使用很多,框架将底层接口留给用户实现,上层牛逼程序员将框架给你搭建起来。用得最多的就是结构体里面放入函数指针。
/*
从内存池申请一个控制块存储区域,返回动态分配内存首地址。
在建立链接之前必须动态分配控制块存储
*/
struct udp_pcb *
udp_new(void)//内存池申请一个控制块,存储接下来这个连接需要存储的信息。
{
struct udp_pcb *pcb;
pcb = (struct udp_pcb *)memp_malloc(MEMP_UDP_PCB);//初始化指针变量
/* could allocate UDP PCB? */
if (pcb != NULL) {
/* UDP Lite: by initializing to all zeroes, chksum_len is set to 0
* which means checksum is generated over the whole datagram per default
* (recommended as default by RFC 3828). */
/* initialize PCB to all zeroes */
memset(pcb, 0, sizeof(struct udp_pcb));//将结构体全部清0
pcb->ttl = UDP_TTL;//设置udp数据报有效时间
}
return pcb;
}
/**
* 绑定本地IP和端口号。也就是初始化控制块结构里面相应的成员,参数说明已经很清楚了。
* ipaddr=IP_ADDR_ANY 指定任意网卡本地IP
* port=0表示自动分配一个端口号
*
* @param pcb: UDP PCB to be bound with a local address ipaddr and port.
* @param ipaddr: local IP address to bind with. Use IP_ADDR_ANY to
* bind to all local interfaces.
* @param port: local UDP port to bind with. Use 0 to automatically bind
* to a random port between UDP_LOCAL_PORT_RANGE_START and
* UDP_LOCAL_PORT_RANGE_END.
*
* ipaddr & port are expected to be in the same byte order as in the pcb.
*
* @return: lwIP error code.
* - ERR_OK. Successful. No error occured.
* - ERR_USE. The specified ipaddr and port are already bound to by
* another UDP PCB.
*
*/
err_t
udp_bind(struct udp_pcb *pcb, ip_addr_t *ipaddr, u16_t port)//给UDP控制块绑定本地IP和端口,可以给定,也可以自动分配
{
u8_t rebind; //表明控制块是否已经在链表中
struct udp_pcb *ipcb;//缓存变量,缓存UDP控制块首地址
LWIP_DEBUGF(UDP_DEBUG | LWIP_DBG_TRACE, ("udp_bind(ipaddr = "));
ip_addr_debug_print(UDP_DEBUG, ipaddr);
LWIP_DEBUGF(UDP_DEBUG | LWIP_DBG_TRACE, (", port = %"U16_F")\n", port));
rebind = 0;
/* Check for double bind and rebind of the same pcb */
for (ipcb = udp_pcbs; ipcb != NULL; ipcb = ipcb->next) {
/* is this UDP PCB already on active list? */
if (pcb == ipcb) {
/* pcb may occur at most once in active list */
LWIP_ASSERT("rebind == 0", rebind == 0);
/*
断定rebind == 0是否成立,成立则返回,不成立,则打印调试信息,这种做法写库函数时候有用。
*/
/* pcb already in list, just rebind */
rebind = 1;//表示pcb已经绑定过。这里只需要重新设置IP和端口即可。
}
/* By default, we don't allow to bind to a port that any other udp
PCB is alread bound to, unless *all* PCBs with that port have tha
REUSEADDR flag set. */
#if SO_REUSE
else if (!ip_get_option(pcb, SOF_REUSEADDR) &&
!ip_get_option(ipcb, SOF_REUSEADDR)) {
#else /* SO_REUSE */
/* port matches that of PCB in list and REUSEADDR not set -> reject */
else {
#endif /* SO_REUSE */
if ((ipcb->local_port == port) &&
/* IP address matches, or one is IP_ADDR_ANY? */
(ip_addr_isany(&(ipcb->local_ip)) ||
ip_addr_isany(ipaddr) ||
ip_addr_cmp(&(ipcb->local_ip), ipaddr))) {
/* other PCB already binds to this local IP and port */
LWIP_DEBUGF(UDP_DEBUG,
("udp_bind: local port %"U16_F" already bound by another pcb\n", port));
return ERR_USE;
}
}
}
ip_addr_set(&pcb->local_ip, ipaddr);//设置控制块IP
/* no port specified? */
if (port == 0) {
port = udp_new_port();//0,自动分配一个临时port
if (port == 0) {
/* no more ports available in local range */
LWIP_DEBUGF(UDP_DEBUG, ("udp_bind: out of free UDP ports\n"));
return ERR_USE;
}
}
pcb->local_port = port;//绑定port
snmp_insert_udpidx_tree(pcb);
/* pcb not active yet? */
if (rebind == 0) {//控制块不在链表中,将其加入链表首部
/* place the PCB on the active list if not already there */
pcb->next = udp_pcbs;
udp_pcbs = pcb;//链表首地址
}
LWIP_DEBUGF(UDP_DEBUG | LWIP_DBG_TRACE | LWIP_DBG_STATE,
("udp_bind: bound to %"U16_F".%"U16_F".%"U16_F".%"U16_F", port %"U16_F"\n",
ip4_addr1_16(&pcb->local_ip), ip4_addr2_16(&pcb->local_ip),
ip4_addr3_16(&pcb->local_ip), ip4_addr4_16(&pcb->local_ip),
pcb->local_port));
return ERR_OK;
}
/**
* 为UDP绑定一个远端IP和端口号,两个都绑定,UDP才处于链接状态。
*
* This will associate the UDP PCB with the remote address.
*
* @param pcb: UDP PCB to be connected with remote address ipaddr and port.
* @param ipaddr: remote IP address to connect with.
* @param port: remote UDP port to connect with.
*
* @return :lwIP error code
*
* ipaddr & port are expected to be in the same byte order as in the pcb.
*
* The udp pcb is bound to a random local port if not already bound.
*
* @see udp_disconnect()
*/
err_t//为UDP绑定一个远端IP和端口号,两个都绑定,UDP才处于链接状态,数据才可以发送出去
//要操作的控制块,远端IP和端口号
udp_connect(struct udp_pcb *pcb, ip_addr_t *ipaddr, u16_t port)
{
struct udp_pcb *ipcb;
if (pcb->local_port == 0) {//如何本地IP没绑定,则调用绑定本地IP
err_t err = udp_bind(pcb, &pcb->local_ip, pcb->local_port);
if (err != ERR_OK) {
return err;
}
}
ip_addr_set(&pcb->remote_ip, ipaddr);//设置远端IP
pcb->remote_port = port;//设置远端端口好
pcb->flags |= UDP_FLAGS_CONNECTED;//标记连接成功
/** TODO: this functionality belongs in upper layers */
#ifdef LWIP_UDP_TODO
/* Nail down local IP for netconn_addr()/getsockname() */
if (ip_addr_isany(&pcb->local_ip) && !ip_addr_isany(&pcb->remote_ip)) {
struct netif *netif;
if ((netif = ip_route(&(pcb->remote_ip))) == NULL) {
LWIP_DEBUGF(UDP_DEBUG, ("udp_connect: No route to 0x%lx\n", pcb->remote_ip.addr));
UDP_STATS_INC(udp.rterr);
return ERR_RTE;
}
/** TODO: this will bind the udp pcb locally, to the interface which
is used to route output packets to the remote address. However, we
might want to accept incoming packets on any interface! */
pcb->local_ip = netif->ip_addr;
} else if (ip_addr_isany(&pcb->remote_ip)) {
pcb->local_ip.addr = 0;
}
#endif
LWIP_DEBUGF(UDP_DEBUG | LWIP_DBG_TRACE | LWIP_DBG_STATE,
("udp_connect: connected to %"U16_F".%"U16_F".%"U16_F".%"U16_F",port %"U16_F"\n",
ip4_addr1_16(&pcb->local_ip), ip4_addr2_16(&pcb->local_ip),
ip4_addr3_16(&pcb->local_ip), ip4_addr4_16(&pcb->local_ip),
pcb->local_port));
/* Insert UDP PCB into the list of active UDP PCBs. */
for (ipcb = udp_pcbs; ipcb != NULL; ipcb = ipcb->next) {//遍历链表,查找是否是控制块
if (pcb == ipcb) {
/* already on the list, just return */
return ERR_OK;//返回OK
}
}
/* PCB not yet on the list, add PCB now */
pcb->next = udp_pcbs;//不是再加入链表,所以单独申请内存块,然后直接绑定,可以自动填充后加入链表
udp_pcbs = pcb;
return ERR_OK;//返回正确
}
/**
* Disconnect a UDP PCB也就是清除远端IP和端口。要使用这个,那么就需要重新connect一次绑定一个远端IP和地址。
*
* @param pcb the udp pcb to disconnect.
*/
void//清除远端IP和端口,并设置为非连接
udp_disconnect(struct udp_pcb *pcb)
{
/* reset remote address association */
ip_addr_set_any(&pcb->remote_ip);//清IP
pcb->remote_port = 0;//清端口
/* mark PCB as unconnected */
pcb->flags &= ~UDP_FLAGS_CONNECTED;//设置为非连接状态
}
void//从链表中删除一个UDP控制块,也就是清楚对应的内存池空间
udp_remove(struct udp_pcb *pcb)
{
struct udp_pcb *pcb2;
snmp_delete_udpidx_tree(pcb);
/* pcb to be removed is first in list? */
if (udp_pcbs == pcb) {
/* make list start at 2nd pcb */
udp_pcbs = udp_pcbs->next;
/* pcb not 1st in list */
} else {
for (pcb2 = udp_pcbs; pcb2 != NULL; pcb2 = pcb2->next) {
/* find pcb in udp_pcbs list */
if (pcb2->next != NULL && pcb2->next == pcb) {
/* remove pcb from list */
pcb2->next = pcb->next;
}
}
}
memp_free(MEMP_UDP_PCB, pcb);
}
/**
* Set a receive callback for a UDP PCB,设置UDP接收数据回调函数。
*
* This callback will be called when receiving a datagram for the pcb.
*
* @param pcb the pcb for wich to set the recv callback
* @param recv function pointer of the callback function
* @param recv_arg additional argument to pass to the callback function
*/
void//注册回调函数,最重要的地方
udp_recv(struct udp_pcb *pcb, udp_recv_fn recv, void *recv_arg)
{
/* remember recv() callback and user data */
pcb->recv = recv;//用户自定义数据报处理函数
pcb->recv_arg = recv_arg;//用户自定义数据
}
err_t//发送报文,通过控制块,发送对应的报文
udp_send(struct udp_pcb *pcb, struct pbuf *p)
{
/* send to the packet using remote ip and port stored in the pcb */
return udp_sendto(pcb, p, &pcb->remote_ip, pcb->remote_port);//用户数据填充pbuf,然后udp取出数据区域填充,然后送给ip层
}
void
udp_input(struct pbuf *p, struct netif *inp);
//udp报文接收函数,这函数是被IP层调用,用于处理报文并根据端口号交给对应的用户进程(这里指的对应的回调函数)。
/*
与报文中目的端口号和源端口号都匹配且处于连接状态的控制块将被视作最佳匹配的控制块,若没有这样的控制块,则与目的端口号相匹配的未连接控制块将被视作查找结构。使用UDP传输、处理数据的关键在于定义用户自定义的数据处理函数,当UDP匹配到某个控制块时,将回调用户注册的处理函数,用户程序应该负责报文pbuf的删除工作。如果找不到匹配的端口号,而该数据报确实是发送给本地的,则一个端口不可达报文会被返回给源主机(这点是为了告诉发送主机,这点完全可以利用wireshark捕捉到)。
*/
协议栈已经实现了,其实自己写好了网络驱动,以及调用上述函数,非常简单。
void udp_demo_init(void)
{
err_t err;
struct udp_pcb *udppcb;//
struct ip_addr rmtipaddr;
udppcb = udp_new();//初始化指针变量并指向新分配的控制块内存
if(udppcb){//
IP4_ADDR(&rmtipaddr,10,13,3,214);//将点分10进制IP转为4字节变量,因为IP控制块里面通过32位存储IP地址。
err=udp_connect(udppcb,&rmtipaddr,UDP_REMOTE_PORT);//设置远端IP和端口
err=udp_bind(udppcb,IP_ADDR_ANY,UDP_LOCAL_PORT);//设置本地IP和端口,其中IP_ADDR_ANY表明使用DHCP网卡地址,在发送函数里会判断这个变量,选择网卡地址
udp_recv(udppcb , udp_demo_recv , NULL);//注册控制块回调函数,端口收到数据,回调处理函数将被调用。
}
}
//这里进行简单处理,也就是将接受到的数据发送出去。
void udp_demo_recv(void *arg,struct udp_pcb *upcb,struct pbuf *p,struct ip_addr *addr,u16_t port)
{
struct ip_addr my_ipaddr;
unsigned char *temp = (unsigned char *)addr;//保存远端IP。
IP4_ADDR(&my_ipaddr , temp[0] , temp[1] , temp[2] , temp[3]);//
udp_send(upcb , p );//通过控制块发送数据,已经绑定了远端IP。
pbuf_free(p);//发送出去,用户空间清除接收数据帧pbuf。
}
void udp_demo_senddata(struct udp_pcb *upcb)
{
struct pbuf *ptr;
ptr=pbuf_alloc(PBUF_TRANSPORT,strlen((char*)tcp_demo_sendbuf),PBUF_POOL); //
if(ptr)
{
ptr->payload=(void*)tcp_demo_sendbuf;
udp_send(upcb,ptr); //
pbuf_free(ptr);//
}
}
//
void udp_demo_connection_close(struct udp_pcb *upcb)
{
udp_disconnect(upcb);
udp_remove(upcb); //清理内存块空间
}
//上述在STM32上面实现,进行回环测试。通过pc网口调试助手发送数据,STM32相应对应数据。
至此UDP协议讲解完毕。UDP协议是一个比较简单的传输层协议,不涉及复杂的握手已经确认机制,所以实现起来也比较简单。