LwIP TCP/IP

LWIP 架构

LwIP 符合 TCP/IP 模型架构,规定了数据的格式、传输、路由和接收,以实现端到端的通信。
此模型包括四个抽象层,用于根据涉及的网络范围,对所有相关协议排序(参见图 2)。这几层从低到高依次为:
链路层包含了局域网的单网段 (链路)通信技术。
网际层 (IP)将独立的网络连接起来,建立互联。
传输层处理主机端口到主机端口的通信。
应用层在实现多个应用进程相互通信的同时,完成应用所需的服务 (例如:数据处理)
在这里插入图片描述

LwIP API 概述

LwIP 栈提供了三种 API:

Raw API

Raw API 基于原始 LwIP API。它可用于开发基于事件回调机制的应用。当初始化应用时,用户需要为不同内核事件注册所需的回调函数 (例如 TCP_Sent、TCP_error…)。当相应事件发生时, LwIP 会自发地调用相关的回调函数。
TCP RAW API 函数

UDP RAW API 函数

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();
 	  }
}

其中调用了下列函数:

  1. HAL_Init 函数调用的目的是复位所有外设,并初始化 Flash 接口和 Systick 定时器
  2. lwIP_init 函数调用的目的是初始化 LwIP 栈内部结构体,并开始栈操作。
  3. Netif_config 函数调用的目的是配置网络接口 (netif)。
  4. tcp_echoserver_init 函数调用的目的是初始化 TCP 回响服务器应用。
  5. 在无限 while 循环中的 ethernetif_input 函数轮询包的接收。当收到包时,将包传给栈处
  6. 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;
 ...
}

其中调用了下列函数:

  1. 通过 newpcb 参数,将新的 TCP 连接传给 tcp_echoserver_accept 回调函数。
  2. es 结构体被用来存储应用状态。通过调用 tcp_arg LwIP API,将它作为一个参数传给
    TCP PCB “newpcb” 连接。
  3. 通过调用 LwIP API tcp_recv,为 TCP 接收回调函数 tcp_echoserver_recv 赋值。此回
    调处理远程客户端的所有数据流。
  4. 通过调用 LwIP API tcp_err,为 TCP 错误回调函数 tcp_echoserver_error 赋值。此回调
    处理 TCP 错误。
  5. 通过调用 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);
	 }
 }
}

其中执行了下述序列:

  1. 调用了 Netconn_new API 函数,参数 NETCONN_TCP 将创建一个新 TCP 连接。
  2. 之后,将新创建的连接绑定到端口 7 (回响协议),方法是调用 Netconn_bind API 函
    数。
  3. 绑定连接之后,通过调用 Netconn_listen API 函数,应用开始监听连接。
  4. 在无限 while(1) 循环中,通过调用 API 函数 Netconn_accept,应用等待一个新连接。
    当没有传入的连接时,进程被阻塞。
  5. 当有传入的连接时,通过调用 netconn_recv API 函数,应用可开始接收数据。传入的
    数据接收在 netbuf 中。
  6. 应用可通过调用 netbuf_data netbuf API 函数得到接收的数据。
  7. 通过调用 Netconn_write API 函数,将接收的数据发送回 (回响)远程 TCP 客户端。
  8. 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,但是对于开发者来说,已经足够了。

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-04-02 08:48:01       15 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-02 08:48:01       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-02 08:48:01       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-02 08:48:01       18 阅读

热门阅读

  1. 32-5 XXE漏洞 - xml数据格式

    2024-04-02 08:48:01       12 阅读
  2. 机器视觉系统-分辨率、信噪比、动态范围

    2024-04-02 08:48:01       13 阅读
  3. 6-88 Print the right subtree of X in BST

    2024-04-02 08:48:01       12 阅读
  4. 如何重置woocommerce,如何批量删除woocommerce产品

    2024-04-02 08:48:01       12 阅读
  5. StarRocks部署

    2024-04-02 08:48:01       11 阅读
  6. WPF —— 动画

    2024-04-02 08:48:01       10 阅读
  7. react 父子组件的渲染机制 | 优化手段

    2024-04-02 08:48:01       13 阅读