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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/800987.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ctfshow web入门 php特性 web123--web139

web123 必须传CTF_SHOW&#xff0c;CTF_SHOW.COM 不能有fl0g 在php中变量名字是由数字字母和下划线组成的&#xff0c;所以不论用post还是get传入变量名的时候都将空格、、点、[转换为下划线&#xff0c;但是用一个特性是可以绕过的&#xff0c;就是当[提前出现后&#xff0c;…

机器学习模型——GBDT和Xgboost

GBDT基本概念&#xff1a; GBDT&#xff08;Gradient Boosting Decision Tree&#xff0c;简称GBDT&#xff09;梯度提升决策树&#xff0c;是Gradient Boost 框架下使用较多的一种模型&#xff0c;且在GBDT中&#xff0c;其基学习器是分类回归树也就是CART&#xff0c;且使用…

【第二十六篇】Burpsuite实现请求方式修改+请求体文件选取

有时我们想将请求包的请求方法或请求体进行修改,这些操作可以由burpsuite完成,以节省时间。 文章目录 修改请求方法请求体文件选取修改请求方法 例如,某请求包的请求方法为GET: 如果我们想将其修改为POST且传递POST参数、上传文件,可以按以下步骤: 1、修改请求方法 2…

vue2中的局部组件和全局组件

注&#xff1a;vue2中使用组件远没有vue3中简单&#xff0c;具体可以看阿耿老师的lingshi小程序 如图所示&#xff1a;

【拓扑的基】示例及详解

集合X的某拓扑的一个基是X的子集的一个族(其成员称为基元素)&#xff0c;满足条件&#xff1a; 1. 2. 由基生成拓扑 由生成的拓扑(满足以上两个条件&#xff09; 等价描述&#xff1a; 由所有可表示为的某些成员的井的那些集合组成 例1: 证明&#xff1a;由生成的族确实是拓扑…

springboot-admin使用及原理剖析

服务端 依赖 <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>de.codecentric</groupId><art…

提高网站安全性,漏洞扫描能带来什么帮助

随着互联网的蓬勃发展&#xff0c;网站已经成为人们获取信息、交流思想、开展业务的重要平台。然而&#xff0c;与之伴随的是日益严重的网络安全问题&#xff0c;包括恶意攻击、数据泄露、隐私侵犯等。 为了保障网站的安全性&#xff0c;提前做好网站的安全检测非常有必要&…

2024年 CS2最佳游戏启动项

引言&#xff1a; Counter-Strike 2&#xff08;CS 2&#xff09;是一款备受瞩目的游戏&#xff0c;而启动选项则是影响游戏性能和体验的关键因素之一。然而&#xff0c;有关所有选项都应该强制使用的说法并不正确。事实上&#xff0c;大多数选项可能对某些计算机并不适用&…

Cali Linux上的PoshC2安装和使用

一、安装PoshC2 curl -sSL https://raw.githubusercontent.com/nettitude/PoshC2/master/Install-for-Docker.sh | sudo bash二、创建工程 posh-project -n test三、修改配置文件 posh-config将图中的baidu.com改为自己要攻击的域名或者IP地址 四、执行 posh-server 显示没…

YOLOV8注意力改进方法: Dual-ViT(Dual Vision Transformer) (附改进代码)

原论文地址&#xff1a;原论文下载网址 论文相关内容介绍 将自注意力过程分解为区域和局部特征提取过程&#xff0c;每个过程产生的计算复杂度要小得多。然而&#xff0c;区域信息通常仅以由于下采样而丢失的不希望的信息为代价。在本文中&#xff0c;作者提出了一种旨在缓解…

基于Leaflet.js的Marker闪烁特效的实现-模拟预警

目录 前言 一、闪烁组件 1、关于leaflet-icon-pulse 2、 使用leaflet-icon-pulse 3、方法及参数简介 二、闪烁实例开发 1、创建网页 2、Marker闪烁设置 3、实际效果 三、总结 前言 在一些地质灾害或者应急情况当中&#xff0c;或者热门预测当中。我们需要基于时空位置来…

C++练级之路——类和对象(上)

1、类的定义 class 类名{//成员函数 //成员变量}; class为定义的关键字&#xff0c;{ }内是类的主体&#xff0c;注意后面的 ; 不要忘了 类体中的内容成为类的成员&#xff0c;类中的变量为成员变量或类的属性&#xff0c;类中的函数为成员函数或类的方法&#xff0c; 类的两种…

通过Golang获取公网IP地址

在Go语言中&#xff0c;获取当前的外网&#xff08;公网&#xff09;IP地址可以通过多种方法实现。其中一种常见的方法是通过访问外部服务来获取。这些服务可以返回访问者的公网IP地址&#xff0c;例如 httpbin.org/ip 或 ipify.org。下面是一个简单的例子&#xff0c;展示了如…

免费云服务器汇总,最长永久免费使用

随着云计算技术的快速发展&#xff0c;越来越多的企业和个人开始将业务迁移到云端。云服务器作为云计算的重要组成部分&#xff0c;以其灵活、高效、可扩展等特点受到广泛关注。然而&#xff0c;许多人在初次接触云服务器时&#xff0c;可能会对高昂的价格望而却步。为了帮助大…

GEE:绘制和对比不同地物的光谱曲线

作者:CSDN @ _养乐多_ 光谱曲线是指在不同波长范围内物体或地表特征对电磁辐射的反射、吸收或发射的表现。这些曲线展示了物体或地表在可见光、红外线、微波等电磁波段上的光谱特征。光谱曲线的形状和特征能够提供关于物体或地表的信息,可以利用光谱曲线来识别和分类不同的地…

Java设计模式—策略模式(商场打折)

策略这个词应该怎么理解&#xff0c;打个比方说&#xff0c;我们出门的时候会选择不同的出行方式&#xff0c;比如骑自行车、坐公交、坐火车、坐飞机、坐火箭等等&#xff0c;这些出行方式&#xff0c;每一种都是一个策略。 再比如我们去逛商场&#xff0c;商场现在正在搞活动&…

Python技能树学习-函数

题目一&#xff1a;递归调用 函数的参数&#xff1a; def dump(index, default0, *args, **kw): print(打印函数参数) print(---) print(index:, index) print(default:, default) for i, arg in enumerate(args): print(farg[{i}]:, arg) for…

Vue 样式技巧总结与整理[中级局]

SFC&#xff08;单文件组件&#xff09;由 3 个不同的实体组成&#xff1a;模板、脚本和样式。三者都很重要&#xff0c;但后者往往被忽视&#xff0c;即使它可能变得复杂&#xff0c;且经常导致挫折和 bug。 更好的理解可以改善代码审查并减少调试时间。 这里有 7 个奇技淫巧…

[StartingPoint][Tier2]Archetype

Task 1 Which TCP port is hosting a database server? (哪个端口开放了数据库服务) $ nmap 10.129.95.187 -sC --min-rate 1000 1433 Task 2 What is the name of the non-Administrative share available over SMB? (哪个非管理共享提供了SMB?) $ smbclient -N -L 1…

Rsync——远程同步命令

目录 一、关于Rsync 1.定义 2.Rsync同步方式 3.备份的方式 4.Rsync命令 5.配置源的两种表达方法 二、配置服务端与客户端的实验——下载 1.准备工作 2.服务端配置 3.客户端配置同步 4.免交互数据同步 5.源服务器删除数据是否会同步 6.可以定期执行数据同步 三、关…