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_IPV4if (!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_SOCKETint 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,但是对于开发者来说,已经足够了。