LWIP 架构
LwIP 符合 TCP/IP 模型架构,规定了数据的格式、传输、路由和接收,以实现端到端的通信。
此模型包括四个抽象层,用于根据涉及的网络范围,对所有相关协议排序(参见图 2)。这几层从低到高依次为:
链路层包含了局域网的单网段 (链路)通信技术。
网际层 (IP)将独立的网络连接起来,建立互联。
传输层处理主机端口到主机端口的通信。
应用层在实现多个应用进程相互通信的同时,完成应用所需的服务 (例如:数据处理)
LwIP API 概述
LwIP 栈提供了三种 API:
Raw API
Raw API 基于原始 LwIP API。它可用于开发基于事件回调机制的应用。当初始化应用时,用户需要为不同内核事件注册所需的回调函数 (例如 TCP_Sent、TCP_error…)。当相应事件发生时, LwIP 会自发地调用相关的回调函数。
Netconn API
Netconn API 为高层有序 API,其执行模型基于典型的阻塞式打开 - 读 - 写 - 关闭机制。
若要正常工作,此 API 必须处于多线程工作模式,该模式需为 LwIP TCP/IP 栈实现专用线程, 或为应用实现多个线程。
Socket API
LwIP 提供了标准 BSD 套接字 API。它是有序 API,在内部构建于 Netconn API 之上。
LwIP 缓冲管理
包缓冲结构
LwIP 使用名为 pbuf 的数据结构管理包缓冲。 pbuf 结构可以通过动态内存申请 / 释放。
pbuf 为链表结构,因此数据包可以由多个 pbuf 组成 (链表)。
其中
next 包含了指向 pbuf 链中下一个 pbuf 的指针
payload 包含了指向包数据载荷的指针
len 为 pbuf 数据内容长度
tot_len 为 pbuf 长度与链中后面 pbuf 的所有 len 字段之和
ref 为 4 位参考数,表示指向 pbuf 的指针数。只有 pbuf 的参考数为 0 时,才能将其从
内存中释放。
flags (4 位)表示 pbuf 的类型。
LwIP 根据分配类型,定义了三种 pbuf:
PBUF_POOL
pbuf 动态分配 (内存池算法)。
• PBUF_RAM
pbuf 动态分配 (内存堆算法)。
• PBUF_ROM
不需为用户载荷分配内存空间:pbuf 载荷指针指向 ROM 内存中的数据,仅能用于发送
常量数据。
对于包的接收,适合的 pbuf 类型为 PBUF_POOL,它允许从 pbuf 池中为收到的包快速分配内存。取决于所收包的大小,会分配一个或多个链接的 pbuf。PBUF_RAM 不适合包接收,因为此分配算法会造成延时。也可能导致内存碎片。
对于包的发送,用户可根据要发送的数据选择最适合的 pbuf 类型。
pbuf 管理 API
LwIP 有专门的 API 可与 pbuf 共同使用。该 API 实现于 pbuf.c 内核文件中。
“pbuf” 可为单个 pbuf 或 pbuf 链。当使用 Netconn API 时,则使用 netbuf (网络缓冲)发送 / 接收数据。netbuf 只是 pbuf 结构的封装。它可容纳分配的或引用的数据。提供了专用 API (在文件 netbuf.c 中实现)以管理 netbuf (分配、释放、链接、解压数据…)
LwIP 与 STM32Cube 以太网 HAL 驱动之间的接口
static void low_level_init(struct netif *netif)
{
uint8_t macaddress[6]= {MAC_ADDR0, MAC_ADDR1, MAC_ADDR2, MAC_ADDR3, MAC_ADDR4, MAC_ADDR5};
EthHandle.Instance = ETH;
EthHandle.Init.MACAddr = macaddress;
EthHandle.Init.AutoNegotiation = ETH_AUTONEGOTIATION_ENABLE;
EthHandle.Init.Speed = ETH_SPEED_100M;
EthHandle.Init.DuplexMode = ETH_MODE_FULLDUPLEX;
EthHandle.Init.MediaInterface = ETH_MEDIA_INTERFACE_MII;
EthHandle.Init.RxMode = ETH_RXINTERRUPT_MODE;
EthHandle.Init.ChecksumMode = ETH_CHECKSUM_BY_HARDWARE;
EthHandle.Init.PhyAddress = DP83848_PHY_ADDRESS;
/* 配置以太网外设 (GPIO、时钟、 MAC、 DMA) */
HAL_ETH_Init(&EthHandle) ;
/* 初始化 Tx 描述符列表:链接模式 */
HAL_ETH_DMATxDescListInit(&EthHandle, DMATxDscrTab, &Tx_Buff[0][0], ETH_TXBUFNB);
/* 初始化 Rx 描述符列表:链接模式 */
HAL_ETH_DMARxDescListInit(&EthHandle, DMARxDscrTab, &Rx_Buff[0][0],ETH_RXBUFNB);
/* 使能 MAC 和 DMA 发送和接收 */
HAL_ETH_Start(&EthHandle);
}
ethernet_input() 函数的实现在独立模式和 RTOS 模式时是不同的:
• 在独立应用中,此函数必须被插入到应用的主循环中,以便轮询任何收到的包。
• 在 RTOS 应用中,此函数为一个阻塞线程,当得到所等待的信号量时才处理收到的数据
包。当以太网外设收到数据并生成中断时,给出此信号量。
ethernetif.c 文件还为底层初始化(GPIO、CLK …)实现了以太网外设 MSP(MCU Support Package)程序和中断回调函数。
对于 RTOS 实现,还需使用其它文件(sys_arch.c)。此文件为 RTOS 服务实现了仿真层(共享内存的访问,信号量,邮箱)。此文件应根据所使用的 RTOS 调整,对于本软件包来说为FreeRTOS。
LWIP配置
LwIP 提供了名为 lwipopts.h 的文件,它允许用户充分配置栈及其所有模块。用户不需要定义所有 LwIP 选项:如果未定义某选项,则使用 opt.h 文件中定义的默认值。因此,lwipopts.h提供了覆盖许多 lwIP 行为的方法。
模块支持
用户可为其应用选择他所需的模块,通过仅编译选定的特性优化了代码长度。
例如,若需要禁用 UDP 或者启用 DHCP (基于 UDP 实现),在 lwipopts.h 文件中分别需进 行以下定义:
/* 禁用 UDP */
#define LWIP_UDP 0
/* 启用 DHCP */
#define LWIP_DHCP 1
内存配置
LwIP 提供了一种灵活的方法管理内存池的大小和组织。
它在数据段中保留了一个固定大小的静态内存区。它细分为不同的池,而 lwIP 将其用于不同的数据结构。例如,有一个 tcp_pcb 结构体的池,还有一个 udp_pcb 结构体的池。每个池都可配置为容纳固定数目的数据结构。该数目可在 lwipopts.h 文件中更改。例如,
MEMP_NUM_TCP_PCB 和 MEMP_NUM_UDP_PCB 定义了在某一时间系统中可激活的
tcp_pcb 和 udb_pcb 结构的最大数目。用户选项可在 lwipopts.h 中更改,如下图为主要的RAM内存选项。
使用LWIP栈开发应用
使用Raw API在独立模式中开发
工作模型
在独立模式中,工作模型基于轮询模式不停地检查是否收到了数据包。
当收到包时,首先将数据包从以太网接收缓冲区拷贝到LwIP缓冲区,为了更快的完成数据的拷贝,应该从缓冲池(PBUF_POOL)分配(pbuf)。
拷贝完成后,lwip会对数据包进行处理。栈根据所收到的包确定是否通知应用层。
lwip使用事件回调机制与应用层通信。因此,应在通信之前,为相关事件注册回调函数。
对于 TCP 应用,必须注册以下回调函数:
• TCP 连接建立时触发,通过 TCP_accept API 注册
• 接收到 TCP 数据包时触发,通过 TCP_recev API 注册
• 数据成功发送后触发,通过 TCP_sent API 注册
• TCP 出错时触发 (在 TCP 中止事件之后),通过 TCP_err API 注册
• 周期性触发 (1s 2 次),用于轮询应用,通过 TCP_poll API 注册
TCP 回响服务器演示举例
TCP 回响服务器示例在目录 \LwIP\LwIP_TCP_Echo_Server 中,它是一个 TCP 服务器的简 单应用,可对从远程客户端收到的任何 TCP 数据包做出回响。
下面的例子提供了固件结构的说明。以下内容节选自 main.c 文件。
int main(void)
{
/* 复位所有外设,初始化 Flash 接口和 Systick。 */
HAL_Init();
...
/* 初始化 LwIP 栈 */
lwIP_init();
/* 网络接口配置 */
Netif_Config();
...
/* tcp 回响服务器初始化 */
tcp_echoserver_init();
/* 无限循环 */
while (1)
{
/* 从以太网缓冲区中读取数据包,交给LwIP 处理 */
ethernetif_input(&gnetif);
/* 处理 LwIP 超时 */
sys_check_timeouts();
}
}
其中调用了下列函数:
- HAL_Init 函数调用的目的是复位所有外设,并初始化 Flash 接口和 Systick 定时器
- lwIP_init 函数调用的目的是初始化 LwIP 栈内部结构体,并开始栈操作。
- Netif_config 函数调用的目的是配置网络接口 (netif)。
- tcp_echoserver_init 函数调用的目的是初始化 TCP 回响服务器应用。
- 在无限 while 循环中的 ethernetif_input 函数轮询包的接收。当收到包时,将包传给栈处
理 - sys_check_timeouts LwIP 函数调用的目的是处理某些 LwIP 内部周期性任务 (协议定
时器、 TCP 包的重传 …)。
tcp_echoserver_init 函数描述
tcp_echoserver_init 函数代码如下:
void tcp_echoserver_init(void)
{
/* 创建新的 tcp pcb */
tcp_echoserver_pcb = tcp_new();
if (tcp_echoserver_pcb != NULL)
{
err_t err;
/* 将 echo_pcb 绑定到端口 7 (ECHO 协议) */
err = tcp_bind(tcp_echoserver_pcb, IP_ADDR_ANY, 7);
if (err == ERR_OK)
{
/* echo_pcb 开始 tcp 监听 */
tcp_echoserver_pcb = tcp_listen(tcp_echoserver_pcb);
/* 注册 LwIP tcp_accept 回调函数 */
tcp_accept(tcp_echoserver_pcb, tcp_echoserver_accept);
}else
{
/* 释放 echo_pcb */
memp_free(MEMP_TCP_PCB, tcp_echoserver_pcb);
}
}
}
LwIP API 调用 tcp_new 来分配一个新的 TCP 协议控制块(PCB)(tcp_echoserver_pcb)。
使用 tcp_bind 函数,将分配的 TCP PCB 绑定到本地 IP 地址和端口,绑定 TCP PCB 之后,会调用 tcp_listen 函数以在 TCP PCB 上开始 TCP 监听进程。最后,应给 tcp_echoserver_accept 回调函数赋值,以处理 TCP PCB 上传入的 TCP 连接, 这通过使用 tcp_accept LwIP API 函数完成。从这点开始, TCP 服务器已经准备好接收任何来自远程客户端的连接。
tcp_echoserver_accept 函数描述
下面的例子展示了怎样使用 tcp_echoserver_accept 用户回调函数,处理传入的 TCP 连接。
以下内容节选自该函数。
static err_t tcp_echoserver_accept(void *arg, struct tcp_pcb *newpcb, err_t
err)
{
...
/* 分配结构体 es 以保存 tcp 连接信息 */
es = (struct tcp_echoserver_struct *)mem_malloc(sizeof(struct
tcp_echoserver_struct));
if (es != NULL)
{
es->state = ES_ACCEPTED;
es->pcb = newpcb;
es->p = NULL;
/* 将新分配的 es 结构体作为参数传给 newpcb */
tcp_arg(newpcb, es);
/* 为 newpcb 注册 lwIP tcp_recv 回调函数 */
tcp_recv(newpcb, tcp_echoserver_recv);
/* 为 newpcb 注册 lwIP tcp_err 回调函数 */
tcp_err(newpcb, tcp_echoserver_error);
/* 为 newpcb 注册 lwIP tcp_poll 回调函数 */
tcp_poll(newpcb, tcp_echoserver_poll, 1);
ret_err = ERR_OK;
...
}
其中调用了下列函数:
- 通过 newpcb 参数,将新的 TCP 连接传给 tcp_echoserver_accept 回调函数。
- es 结构体被用来存储应用状态。通过调用 tcp_arg LwIP API,将它作为一个参数传给
TCP PCB “newpcb” 连接。 - 通过调用 LwIP API tcp_recv,为 TCP 接收回调函数 tcp_echoserver_recv 赋值。此回
调处理远程客户端的所有数据流。 - 通过调用 LwIP API tcp_err,为 TCP 错误回调函数 tcp_echoserver_error 赋值。此回调
处理 TCP 错误。 - 通过调用 LwIP API tcp_poll,为 TCP 轮询回调函数 tcp_echoserver_poll 赋值,以处理
周期性的应用任务 (例如检查是否还有应用数据要发送)。
使用 Netconn 或 Socket API 基于 RTOS 开发
工作模型
使用RTOS的工作模型有如下特点:
TCP/IP栈和应用运行在不同的线程中。
应用通过有序 API 调用与栈通信,它使用 RTOS 邮箱机制进行进程间通信。 API 调用为阻塞调用。这意味着在从栈收到响应之前,应用线程阻塞。
使用另外一个线程 —— 网络接口线程 —— 用于将驱动缓冲区收到的数据包拷贝至 LwIP 协议栈缓冲区。此进程由以太网接收中断所释放的信号量唤醒。
使用 Netconn API 的 TCP 回响服务器演示举例
从应用的角度来看,Netconn API 提供了一种比 raw API 更简单的方法来开发 TCP/IP 应用,这是因为它有一个更加直观的有序 API。
下面的例子显示了使用 Netconn API 开发的 TCP 回响服务器应用。以下内容节选自 main.c 文件。
int main(void)
{
...
/* 创建并开始线程 */
osThreadDef(Start, StartThread, osPriorityNormal, 0,
configMINIMAL_STACK_SIZE * 2);
osThreadCreate (osThread(Start), NULL);
/* 开始调度器 */
osKernelStart (NULL, NULL);
/* 程序不应该运行到这里,因为现在调度器在控制 */
for( ;; );
}
开始线程有如下代码:
static void StartThread(void const * argument)
{
...
/* 创建 tcp_ip 栈线程 */
tcpip_init( NULL, NULL );
/* 网络接口配置 */
Netif_Config();
/* 初始化 tcp 回响服务器 */
tcpecho_init();
for( ;; )
{
}
}
void tcpecho_init(void)
{
sys_thread_new("tcpecho_thread", tcpecho_thread, NULL,
DEFAULT_THREAD_STACKSIZE, TCPECHO_THREAD_PRIO);
}
tcpecho_thread 函数说明
TCP 回响服务器线程有如下代码:
static void tcpecho_thread(void *arg)
{
/* 创建一个新连接标识符。 */
conn = netconn_new(NETCONN_TCP);
if (conn!=NULL)
{
/* 将连接绑定至已知的端口号 7。 */
err = netconn_bind(conn, NULL, 7);
if (err == ERR_OK)
{
/* 告知连接进入监听模式。 */
netconn_listen(conn);
while (1)
{
/* 抓取新连接。 */
accept_err = netconn_accept(conn, &newconn);
/* 处理新连接。 */
if (accept_err == ERR_OK)
{
while (( recv_err = netconn_recv(newconn, &buf)) == ERR_OK)
{
do
{
netbuf_data(buf, &data, &len);
netconn_write(newconn, data, len, NETCONN_COPY);
}
while (netbuf_next(buf) >= 0);
netbuf_delete(buf);
}
/* 关闭连接,丢弃连接标识符。 */
netconn_close(newconn);
netconn_delete(newconn);
}
}
}
else
{
netconn_delete(newconn);
}
}
}
其中执行了下述序列:
- 调用了 Netconn_new API 函数,参数 NETCONN_TCP 将创建一个新 TCP 连接。
- 之后,将新创建的连接绑定到端口 7 (回响协议),方法是调用 Netconn_bind API 函
数。 - 绑定连接之后,通过调用 Netconn_listen API 函数,应用开始监听连接。
- 在无限 while(1) 循环中,通过调用 API 函数 Netconn_accept,应用等待一个新连接。
当没有传入的连接时,进程被阻塞。 - 当有传入的连接时,通过调用 netconn_recv API 函数,应用可开始接收数据。传入的
数据接收在 netbuf 中。 - 应用可通过调用 netbuf_data netbuf API 函数得到接收的数据。
- 通过调用 Netconn_write API 函数,将接收的数据发送回 (回响)远程 TCP 客户端。
- Netconn_close 和 Netconn_delete 分别用于关闭和删除 Netconn 连接。
RAW 编程接口 UDP 实验
RAW 编程接口 UDP 实验
UDP 协议是 TCP/IP 协议栈的传输层协议,是一个简单的面向数据报的协议,在传输层中
还有另一个重要的协议,那就是 TCP 协议,TCP 协议的知识笔者会在下一章节中讲解。UDP不提供数据包分组、组装,不能对数据包进行排序,当报文发送出去后无法知道是否安全、完整的到达。UDP 除了这些缺点外肯定有它自身的优势,由于 UDP 不属于连接型协议,因而消耗资源小,处理速度快,所以通常在音频、视频和普通数据传输时使用 UDP 较多。UDP 数据报结构如下图所示。
UDP 首部有 8 个字节,由 4 个字段构成,每个字段都是两个字节,这些字段的作用如下:
① 源端口:源端口号,需要对方回信时选用,不需要时全部置 0。
② 目的端口:目的端口号,在终点交付报文的时候需要用到。
③ 长度:UDP 的数据报的长度(包括首部和数据)其最小值为 8(只有首部)。
① 校验和:检测 UDP 数据报在传输中是否有错,有错则丢弃。
UDP 报文封装流程
UDP 报文与 TCP 报文一样也是由 UDP/TCP 首部+数据区域组成,UDP 协议是位于传输层,该层是应用层的下一层,当用户发送数据时候,需要选择使用那种协议发送出去,如果使用UDP 协议,则 UDP 协议就会简单的把数据封装起来,UDP 报文结构如下图所示:
UDP 报文的数据结构
UDP 首部结构
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 回调提供的参数 */
};
可以看到,结构体 udp_pcb 包含了指向下一个节点的指针 next,多个 UDP 控制块构建了
一个单向链表且各个控制块指向独立的接收回调函数,如下图所示:
对于 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);
}
}else /* 如果有足够的空间 */
{
/* 在数据 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;
}
NETCONN 编程接口
netconn 连接结构
我们前面在使用 RAW 编程接口的时候,对于 UDP 和 TCP 连接使用的是两种不同的编程
函数:udp_xxx 和 tcp_xxx。NETCONN 对于这两种连接提供了统一的编程接口,用于使用同
一的连接结构和编程函数,在 api.h 中定了 netcon 结构体,代码如下。
/* netconn 描述符 */
struct netconn {
/* 连接类型,TCP UDP 或者 RAW */
enum netconn_type type;
/* 当前连接状态 */
enum netconn_state state;
/* 内核中与连接相关的控制块指针 */
union {
struct ip_pcb *ip; /* IP 控制块 */
struct tcp_pcb *tcp; /* TCP 控制块 */
struct udp_pcb *udp; /* UDP 控制块 */
struct raw_pcb *raw; /* RAW 控制块 */
} pcb;
/* 这个 netconn 最后一个异步未报告的错误 */
err_t pending_err;
#if !LWIP_NETCONN_SEM_PER_THREAD
/* 用于两部分 API 同步的信号量 */
sys_sem_t op_completed;
#endif
/* 接收数据的邮箱 */
sys_mbox_t recvmbox;
#if LWIP_TCP
/* 用于 TCP 服务器端,连接请求的缓冲队列*/
sys_mbox_t acceptmbox;
#endif /* LWIP_TCP */
/* Socket 描述符,用于 Socket API */
#if LWIP_SOCKET
int Socket;
#endif /* LWIP_SOCKET */
#if LWIP_SO_RCVTIMEO
/* 接收数据时的超时时间*/
u32_t recv_timeout;
#endif /* LWIP_SO_RCVTIMEO */
/* 标识符 */
u8_t flags;
#if LWIP_TCP
/* TCP:当传递到 netconn_write 的数据不适合发送缓冲区时,
这将临时存储消息。
也用于连接和关闭。 */
struct api_msg *current_msg;
#endif /* LWIP_TCP */
/* 连接相关回调函数,实现 Socket API 时使用 */
netconn_callback callback;
};
在 api.h 文件中还定义了连接状态和连接类型,这两个都是枚举类型。
/* 枚举类型,用于描述连接类型 */
enum netconn_type {
NETCONN_INVALID = 0, /* 无效类型 */
NETCONN_TCP = 0x10, /* TCP */
NETCONN_UDP = 0x20, /* UDP */
NETCONN_UDPLITE = 0x21, /* UDPLite */
NETCONN_UDPNOCHKSUM = 0x22, /* 无校验 UDP */
NETCONN_RAW = 0x40 /* 原始链接 */
};
/* 枚举类型,用于描述连接状态,主要用于 TCP 连接中 */
enum netconn_state
{
NETCONN_NONE, /* 不处于任何状态 */
NETCONN_WRITE, /* 正在发送数据 */
NETCONN_LISTEN, /* 侦听状态 */
NETCONN_CONNECT, /* 连接状态 */
NETCONN_CLOSE /* 关闭状态 */
};
netconn 编程 API 函数
netconn_getaddr 函数是用来获取一个 netconn 连接结构的源 IP 地址和源端口号或者目的 IP
地址和目的端口号,IP 地址保存在 addr 当中,而端口信息保存在 port 当中,参数 local 表示是
获取源地址还是目的地址,当 local 为 1 时表示本地地址,此函数原型如下。
err_t netconn_getaddr(struct netconn*conn,ip_addr_t*addr,u16_t*port,u8_t local);
netconn_bind 函数将一个连接结构与本地 IP 地址 addr 和端口号 port 进行绑定,服务器端
程序必须执行这一步,服务器必须与指定的端口号绑定才能结接受客户端的连接请求,该函数
原型如下
err_t netconn_bind(struct netconn *conn, const ip_addr_t *addr, u16_t port);
netconn_connect 函数的功能是连接服务器,它将指定的连接结构与目的 IP 地址 addr 和目
的端口号 port 进行绑定,当作为 TCP 客户端程序时,调用此函数会产生握手过程,该函数原
型如下。
err_t netconn_connect(struct netconn *conn, const ip_addr_t *addr, u16_t port);
netconn_disconnect 函数只能使用在 UDP 连接中,功能是断开与服务器的连接。对于 UDP
连接来说就是将 UDP 控制块中的 remote_ip 和 remote_port 字段值清零,函数原型如下。
err_t netconn_disconnect (struct netconn *conn);
netconn_listen 函数只有在 TCP 服务器程序中使用,将一个连接结构 netconn 设置为侦听状
态,既将 TCP 控制块的状态设置为 LISTEN 状态,该函数原型如下:
#define netconn_listen(conn) \
netconn_listen_with_backlog(conn, TCP_DEFAULT_LISTEN_BACKLOG)
netconn_accept 函数也只用于 TCP 服务器程序,服务器调用此函数可以从 acceptmbox 邮箱
中获取一个新建立的连接,若邮箱为空,则函数会一直阻塞,直到新连接的到来。服务器端调
用此函数前必须先调用 netconn_listen 函数将连接设置为侦听状态,函数原型如下
err_t netconn_accept(struct netconn *conn, struct netconn **new_conn);
netconn_recv 函数是从连接的 recvmbox 邮箱中接收数据包,可用于 TCP 连接,也可用于
UDP 连接,函数会一直阻塞,直到从邮箱中获得数据消息,数据被封装在 netbuf 中。如果从
邮箱中接收到一条空消息,表示对方已经关闭当前的连接,应用程序也应该关闭这个无效的连
接,函数原型如下。
err_t netconn_recv(struct netconn *conn, struct netbuf **new_buf);
netconn_send 函数用于在 UDP 连接上发送数据,参数 conn 指出了要操作的连接,参数
buf 为要发送的数据,数据被封装在 netbuf 中。如果 IP 层分片功能未使能,则 netbuf 中的数据
不能太长,不能超过 MTU 的值,最好不要超过 1000 字节。如果 IP 层分片功能使能的情况下
就可以忽略此细节,函数原型如下。
err_t netconn_send(struct netconn *conn, struct netbuf *buf);
netconn_write 函数用于在稳定的 TCP 连接上发送数据,参数 dataptr 和 size 分别指出了待
发送数据的起始地址和长度,函数并不要求用户将数据封装在 netbuf 中,对于数据长度也没
有限制,内核会直接处理这些数据,将他们封装在 pbuf 中,并挂接到 TCP 的发送队列中。
netconn_close 函数用来关闭一个 TCP 连接,该函数会产生一个 FIN 握手包的发送,成功
后函数便返回,而后剩余的断开握手操作由内核自动完成,用户程序不用关心,该函数只是断
开一个连接,但不会删除连接结构 netconn,用户需要调用 netconn_delete 函数来删除连接结构,否则会造成内存泄漏,函数原型如下。
err_t netconn_close(struct netconn *conn);
NETCONN 编程接口 UDP 示例
程序流程图
NETCONN 编程接口 TCP 示例
TCP CLIENT
TCP SERVER
Socket 编程接口
Socket 编程接口简介
说到 Socket,我们不得不提起 BSD Socket,BSD Socket 是由加州伯克利大学为 Unix 系统
开发出来的,所以被称为伯克利套接字(Internet Berkeley Sockets),BSD Socket 是采用 C 语言进程间通信库的应用程序接口(API),允许不同主机或者同一个计算机上的不同进程之间
的通信,支持多种 I/O 设备和驱动,具体的实现是依赖操作系统的。这种接口对于 TCP/IP 是
必不可少的,所以是互联网的基础技术之一,所以 LWIP 也是引入该程序编程接口,虽然不能
完全实现 BSD Socket,但是对于开发者来说,已经足够了。