LwIP TCP/IP

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 驱动之间的接口


c 复制代码
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 文件中分别需进 行以下定义:

c 复制代码
/* 禁用 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 文件。

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 函数代码如下:
c 复制代码
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 连接。

以下内容节选自该函数。

c 复制代码
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 文件。

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( ;; )
 {
 }
}
c 复制代码
void tcpecho_init(void)
{
 sys_thread_new("tcpecho_thread", tcpecho_thread, NULL, 
DEFAULT_THREAD_STACKSIZE, TCPECHO_THREAD_PRIO);
}

tcpecho_thread 函数说明

TCP 回响服务器线程有如下代码:

c 复制代码
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 首部结构

c 复制代码
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 控制块结构如以下所示:

c 复制代码
#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 函数如下所示:

c 复制代码
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 函数把数据报递交给传输层,该函数源码所示:

c 复制代码
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 结构体,代码如下。

c 复制代码
/* 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 文件中还定义了连接状态和连接类型,这两个都是枚举类型。

c 复制代码
/* 枚举类型,用于描述连接类型 */
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 时表示本地地址,此函数原型如下。

c 复制代码
err_t netconn_getaddr(struct netconn*conn,ip_addr_t*addr,u16_t*port,u8_t local);

netconn_bind 函数将一个连接结构与本地 IP 地址 addr 和端口号 port 进行绑定,服务器端

程序必须执行这一步,服务器必须与指定的端口号绑定才能结接受客户端的连接请求,该函数

原型如下

c 复制代码
err_t netconn_bind(struct netconn *conn, const ip_addr_t *addr, u16_t port);

netconn_connect 函数的功能是连接服务器,它将指定的连接结构与目的 IP 地址 addr 和目

的端口号 port 进行绑定,当作为 TCP 客户端程序时,调用此函数会产生握手过程,该函数原

型如下。

c 复制代码
err_t netconn_connect(struct netconn *conn, const ip_addr_t *addr, u16_t port);

netconn_disconnect 函数只能使用在 UDP 连接中,功能是断开与服务器的连接。对于 UDP

连接来说就是将 UDP 控制块中的 remote_ip 和 remote_port 字段值清零,函数原型如下。

c 复制代码
err_t netconn_disconnect (struct netconn *conn);

netconn_listen 函数只有在 TCP 服务器程序中使用,将一个连接结构 netconn 设置为侦听状

态,既将 TCP 控制块的状态设置为 LISTEN 状态,该函数原型如下:

c 复制代码
#define netconn_listen(conn) \
netconn_listen_with_backlog(conn, TCP_DEFAULT_LISTEN_BACKLOG)

netconn_accept 函数也只用于 TCP 服务器程序,服务器调用此函数可以从 acceptmbox 邮箱

中获取一个新建立的连接,若邮箱为空,则函数会一直阻塞,直到新连接的到来。服务器端调

用此函数前必须先调用 netconn_listen 函数将连接设置为侦听状态,函数原型如下

c 复制代码
err_t netconn_accept(struct netconn *conn, struct netconn **new_conn);

netconn_recv 函数是从连接的 recvmbox 邮箱中接收数据包,可用于 TCP 连接,也可用于

UDP 连接,函数会一直阻塞,直到从邮箱中获得数据消息,数据被封装在 netbuf 中。如果从

邮箱中接收到一条空消息,表示对方已经关闭当前的连接,应用程序也应该关闭这个无效的连

接,函数原型如下。

c 复制代码
err_t netconn_recv(struct netconn *conn, struct netbuf **new_buf);

netconn_send 函数用于在 UDP 连接上发送数据,参数 conn 指出了要操作的连接,参数

buf 为要发送的数据,数据被封装在 netbuf 中。如果 IP 层分片功能未使能,则 netbuf 中的数据

不能太长,不能超过 MTU 的值,最好不要超过 1000 字节。如果 IP 层分片功能使能的情况下

就可以忽略此细节,函数原型如下。

c 复制代码
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 函数来删除连接结构,否则会造成内存泄漏,函数原型如下。

c 复制代码
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,但是对于开发者来说,已经足够了。

相关推荐
速盾cdn18 分钟前
速盾:vue的cdn是干嘛的?
服务器·前端·网络
叫我龙翔35 分钟前
【计网】实现reactor反应堆模型 --- 框架搭建
linux·运维·网络
lucy1530275107936 分钟前
【青牛科技】GC5931:工业风扇驱动芯片的卓越替代者
人工智能·科技·单片机·嵌入式硬件·算法·机器学习
怀旧6661 小时前
spring boot 项目配置https服务
java·spring boot·后端·学习·个人开发·1024程序员节
不爱学习的YY酱2 小时前
【计网不挂科】计算机网络期末考试——【选择题&填空题&判断题&简述题】试卷(4)
网络·计算机网络
装睡的小5郎2 小时前
家庭宽带如何开启公网ipv4和ipv6
网络
yfs10242 小时前
压缩Minio桶中的文件为ZIP,并通过 HTTP 响应输出
网络·网络协议·http
有谁看见我的剑了?2 小时前
Ubuntu 22.04.5 配置vlan子接口和网桥
服务器·网络·ubuntu
hgdlip2 小时前
有什么办法换网络ip动态
网络·tcp/ip·智能路由器
超栈2 小时前
HCIP(11)-期中综合实验(BGP、Peer、OSPF、VLAN、IP、Route-Policy)
运维·网络·网络协议·计算机网络·web安全·网络安全·信息与通信