背景:主要学习正点原子的LwIP,初步学习UDP协议和LwIP RAW UDP接口的使用,后面对LwIP有更深入的了解之后,再来分析LwIP RAW UDP相关的源码。
11.1 UDP 协议简介
UDP 协议是 TCP/IP 协议栈的传输层协议,是一个简单的面向数据报的协议,在传输层中还有另一个重要的协议,那就是 TCP 协议。UDP不提供数据包分组、组装,不能对数据包进行排序,当报文发送出去后无法知道是否安全、完整的到达。UDP 除了这些缺点外肯定有它自身的优势,由于 UDP 不属于连接型协议,因而消耗资源小,处理速度快,所以通常在音频、视频和普通数据传输时使用 UDP 较多。UDP 数据报结构如下图所示。
UDP 首部有 8 个字节,由 4 个字段构成,每个字段都是两个字节,这些字段的作用如下:
①源端口:源端口号,需要对方回信时选用,不需要时全部置 0。
②目的端口:目的端口号,在终点交付报文的时候需要用到。
③长度:UDP 的数据报的长度(包括首部和数据)其最小值为 8(只有首部)。
④校验和:检测 UDP 数据报在传输中是否有错,有错则丢弃。
UDP 协议使用端口号为不同的应用保留各自的数据传输通道,UDP 和 TCP 协议都是采用端口号对同一时刻内多项应用同时发送和接收数据,而数据接收方则通过目标端口接收数据。有的网络应用只能使用预先为其预留或注册的静态端口;而另外一些网络应用则可以使用未被注册的动态端口。因为 UDP 报头使用两个字节存放端口号,所以端口号的有效范围是从 0 到65535。一般来说,大于 49151 的端口号都代表动态端口。
数据报的长度是指包括报头和数据部分在内的总字节数。因为报头的长度是固定的,所以该数据区域主要被用来计算可变长度的数据部分(又称为数据负载)。数据报的最大长度根据操作环境的不同而各异。从理论上说,包含报头在内的数据报的最大长度为 65535 字节。
UDP 协议使用报头中的校验和来保证数据的安全。校验和首先在数据发送方通过特殊的算法计算得出,在传递到接收方之后,还需要再重新计算。如果某个数据报在传输过程中被第三方篡改或者由于线路噪音等原因受到损坏,发送和接收方的校验计算和将不会相符,由此UDP 协议可以检测是否出错。
UDP 报文封装流程:
UDP 报文与 TCP 报文一样也是由 UDP/TCP 首部+数据区域组成,UDP 协议是位于传输层,该层是应用层的下一层,当用户发送数据时候,需要选择使用那种协议发送出去,如果使用UDP 协议,则 UDP 协议就会简单的把数据封装起来,UDP 报文结构如下图所示。
11.1.1 UDP 报文的数据结构
(1) 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;
可见,这个结构体的成员变量与图 11.1.1 的 UDP 首部字段一一对应。
(2) 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 回调提供的参数 */
};
可以看到,结构体 udp_pcb 包含了指向下一个节点的指针 next,多个 UDP 控制块构建了一个单向链表且各个控制块指向独立的接收回调函数,如下图所示。
对于 RAW 的 API 接口来讲,上图中的 recv 由用户提供这个函数,而 NETCONN 和SOCKET 接口无需用户提供回调函数,因为 lwIP 内核已经注册了该回调函数,所以数据到来时,该函数把数据以邮箱的方式发送至 NETCONN 和 SOCKET 对应的接口。
11.2 RAW 的 UDP 接口简介
下表给出了 UDP 协议的 RAW 的 API 功能函数,我们使用这些函数来完成 UDP 的数据发送和接收功能。
RAW 的 API 函数 描述
udp_new 新建一个 UDP 的 PCB 块
udp_remove 将一个 PCB 控制块从链表中删除,并且释放这个控制块的内存
udp_bind 为 UDP 的 PCB 控制块绑定一个本地 IP 地址和端口号
udp_connect 连接到指定 IP 地址主机的指定端口上
udp_disconnect 断开连接,将控制块设置为非连接状态
udp_send 通过一个 PCB 控制块发送数据
udp_recv 创建的一个回调函数
接下来我们来看一下上述中重要的函数:
(1) udp_new 函数
此函数用来创建一个 UDP 控制块,这个控制块用来描述 IP 地址、端口号和状态等信息
(2) udp_remove 函数
从 PCB 控制块链表中移除一个控制块,并且把移除的控制块释放内存
(3) udp_recv 函数
此函数用来设置接收回调函数及函数参数,若用户使用 RAW 接口实现 UDP,则用户必须调用此函数设置接收回调函数
11.3 RAW 的 UDP demo
#include <stdint.h>
#include <stdio.h>
#include "./BSP/LCD/lcd.h"
#include "./MALLOC/malloc.h"
#include "./BSP/KEY/key.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "lwip/pbuf.h"
#include "lwip/udp.h"
#include "lwip/tcp.h"
#include "lwip_demo.h"
#include "lwip_comm.h"
#include "stdio.h"
#include "string.h"
#define LWIP_DEMO_RX_BUFSIZE 2000 /* 定义udp最大接收数据长度 */
#define LWIP_DEMO_PORT 8080 /* 定义udp连接的本地端口号 */
/* 接收数据缓冲区 */
uint8_t g_lwip_demo_recvbuf[LWIP_DEMO_RX_BUFSIZE];
/* 发送数据内容 */
char *g_lwip_demo_sendbuf = "hello xl,good good study\r\n";
/* 数据发送标志位 */
uint8_t g_lwip_send_flag;
static void lwip_udp_callback(void *arg, struct udp_pcb *upcb, struct pbuf *p, const ip_addr_t *addr, u16_t port);
void lwip_udp_senddata(struct udp_pcb *upcb);
void lwip_udp_connection_close(struct udp_pcb *upcb);
/**
* @brief 设置远端IP地址
* @param 无
* @retval 无
*/
void lwip_udp_set_remoteip(void)
{
char *tbuf;
uint16_t xoff;
uint8_t key;
printf("UDP Test lwip_udp_set_remoteip\r\n");
tbuf = mymalloc(SRAMIN, 100); /* 申请内存 */
if (tbuf == NULL)
return;
sprintf((char *)tbuf, "Remote IP:%d.%d.%d.%d", g_lwipdev.remoteip[0], g_lwipdev.remoteip[1], g_lwipdev.remoteip[2], g_lwipdev.remoteip[3]); /* 远端IP */
printf("%s\r\n", tbuf);
myfree(SRAMIN, tbuf);
}
/**
* @brief UDP服务器回调函数
* @param arg :传入参数
* @param upcb:UDP控制块
* @param p : 网络数据包
* @param addr:IP地址
* @param port:端口号
* @retval 无
*/
static void lwip_udp_callback(void *arg, struct udp_pcb *upcb, struct pbuf *p, const ip_addr_t *addr, u16_t port)
{
uint32_t data_len = 0;
struct pbuf *q;
if (p != NULL) /* 接收到不为空的数据时 */
{
memset(g_lwip_demo_recvbuf, 0, LWIP_DEMO_RX_BUFSIZE); /* 数据接收缓冲区清零 */
for (q = p; q != NULL; q = q->next) /* 遍历完整个pbuf链表 */
{
/* 判断要拷贝到LWIP_DEMO_RX_BUFSIZE中的数据是否大于LWIP_DEMO_RX_BUFSIZE的剩余空间,如果大于 */
/* 的话就只拷贝LWIP_DEMO_RX_BUFSIZE中剩余长度的数据,否则的话就拷贝所有的数据 */
if (q->len > (LWIP_DEMO_RX_BUFSIZE - data_len))
memcpy(g_lwip_demo_recvbuf + data_len, q->payload, (LWIP_DEMO_RX_BUFSIZE - data_len)); /* 拷贝数据 */
else
memcpy(g_lwip_demo_recvbuf + data_len, q->payload, q->len);
data_len += q->len;
if (data_len > LWIP_DEMO_RX_BUFSIZE)
break; /* 超出UDP客户端接收数组,跳出 */
}
upcb->remote_ip = *addr; /* 记录远程主机的IP地址 */
upcb->remote_port = port; /* 记录远程主机的端口号 */
g_lwipdev.remoteip[0] = upcb->remote_ip.addr & 0xff; /* IADDR4 */
g_lwipdev.remoteip[1] = (upcb->remote_ip.addr >> 8) & 0xff; /* IADDR3 */
g_lwipdev.remoteip[2] = (upcb->remote_ip.addr >> 16) & 0xff; /* IADDR2 */
g_lwipdev.remoteip[3] = (upcb->remote_ip.addr >> 24) & 0xff; /* IADDR1 */
g_lwip_send_flag |= 1 << 6; /* 标记接收到数据了 */
pbuf_free(p); /* 释放内存 */
}
else
{
udp_disconnect(upcb);
printf("UDP Test lwip_udp_callback\r\n");
}
}
// void lwip_xl_demo(void)
// {
// struct udp_pcb* udp_pcb;
// ip_addr_t rmtipaddr;
// udp_pcb = udp_new();
// if (udp_pcb)
// {
// IP4_ADDR(&rmtipaddr, g_lwipdev.remoteip[0], g_lwipdev.remoteip[1], g_lwipdev.remoteip[2], g_lwipdev.remoteip[3]);
// err = udp_connect(udppcb, &rmtipaddr, LWIP_DEMO_PORT);
// if (err ==)
// }
// }
void lwip_demo(void)
{
err_t err;
struct udp_pcb *udppcb; /* 定义一个UDP服务器控制块 */
ip_addr_t rmtipaddr; /* 远端ip地址 */
ip_addr_t local_ipaddr; /* 远端ip地址 */
char *tbuf;
uint8_t key;
uint8_t res = 0;
uint8_t t = 0;
printf("UDP Test lwip_demo\r\n");
lwip_udp_set_remoteip();/*显示设置的IP地址信息 */
tbuf = mymalloc(SRAMIN, 200); /* 申请内存 */
if (tbuf == NULL)
return ; /* 内存申请失败了,直接退出 */
sprintf((char *)tbuf, "Local IP:%d.%d.%d.%d", g_lwipdev.ip[0], g_lwipdev.ip[1], g_lwipdev.ip[2], g_lwipdev.ip[3]); /* 服务器IP */
printf("%s\r\n", tbuf);
sprintf((char *)tbuf, "Remote IP:%d.%d.%d.%d", g_lwipdev.remoteip[0], g_lwipdev.remoteip[1], g_lwipdev.remoteip[2], g_lwipdev.remoteip[3]); /* 远端IP */
printf("%s\r\n", tbuf);
sprintf((char *)tbuf, "Remote Port:%d", LWIP_DEMO_PORT); /* 客户端端口号 */
printf("%s\r\n", tbuf);
udppcb = udp_new();
if (udppcb) /* 创建成功 */
{
IP4_ADDR(&local_ipaddr, g_lwipdev.ip[0], g_lwipdev.ip[1], g_lwipdev.ip[2], g_lwipdev.ip[3]);
//err = udp_bind(udppcb, IP_ADDR_ANY, LWIP_DEMO_PORT); /* 绑定本地IP地址与端口号 */
err = udp_bind(udppcb, &local_ipaddr, LWIP_DEMO_PORT); /* 绑定本地IP地址与端口号 */
if (err == ERR_OK)
{
IP4_ADDR(&rmtipaddr, g_lwipdev.remoteip[0], g_lwipdev.remoteip[1], g_lwipdev.remoteip[2], g_lwipdev.remoteip[3]);
err = udp_connect(udppcb, &rmtipaddr, LWIP_DEMO_PORT); /* UDP客户端连接到指定IP地址和端口号的服务器 */
if (err == ERR_OK) /* 绑定完成 */
{
udp_recv(udppcb,lwip_udp_callback, NULL); /* 注册接收回调函数 */
printf("Receive Data:"); /* 提示消息 */
}
else
res = 1;
}
else
res = 1;
}
else
res = 1;
while (res == 0)
{
key = key_scan(0);
if (key == KEY1_PRES)
break;
if (key == KEY0_PRES) /* KEY0按下了,发送数据 */
{
lwip_udp_senddata(udppcb);
}
if (g_lwip_send_flag & 1 << 6) /* 是否收到数据 */
{
/* 显示接收到的数据 */
printf("recv:%s\r\n", (char *)g_lwip_demo_recvbuf, g_point_color);
g_lwip_send_flag &= ~(1 << 6); /* 标记数据已经被处理了 */
}
lwip_periodic_handle(); /* LWIP轮询任务 */
delay_ms(2);
t++;
if (t == 200)
{
t = 0;
LED0_TOGGLE();
}
}
lwip_udp_connection_close(udppcb);
myfree(SRAMIN, tbuf);
}
/**
* @brief UDP服务器发送数据
* @param upcb: UDP控制块
* @retval 无
*/
void lwip_udp_senddata(struct udp_pcb *upcb)
{
struct pbuf *ptr;
ptr = pbuf_alloc(PBUF_TRANSPORT, strlen((char *)g_lwip_demo_sendbuf), PBUF_POOL); /* 申请内存 */
if (ptr)
{
pbuf_take(ptr, (char *)g_lwip_demo_sendbuf, strlen((char *)g_lwip_demo_sendbuf)); /* 将g_lwip_demo_sendbuf中的数据打包进pbuf结构中 */
udp_send(upcb, ptr); /* udp发送数据 */
pbuf_free(ptr); /* 释放内存 */
}
}
/**
* @brief 关闭tcp连接
* @param upcb: UDP控制块
* @retval 无
*/
void lwip_udp_connection_close(struct udp_pcb *upcb)
{
udp_disconnect(upcb);
udp_remove(upcb); /* 断开UDP连接 */
g_lwip_send_flag &= ~(1 << 5); /* 标记连接断开 */
printf("UDP Test lwip_udp_connection_close\r\n");
}
11.4 测试现象:
使用wireshark 抓包,分析UDP协议
11.5 源码路径:
git clone https://gitee.com/xiaoliangliangcong/stm32.git
LWIP/STM32F407/lianxi/RAW/LwIP_RAW_UDP_v2