lwIP 介绍_3 协议

协议

我的博客
本文原文


lwIP 是一模块化的框架,支持很多的协议,大部分代码可以为了精简代码删除。

链路与网络协议

ARP: 地址解析协议

地址解析协议 ARP: Address Resolution Protocol 是链路层协议,用来转换本机硬件地址 (即 MAC 地址) 与 IP 地址。

支持 ARP 的网络接口会令 etharp_output 处理所有外发的包,并将会在它的 netif 结构体中设置 flag 标识来使能免费 ARP (gratuitous ARP)。并将维护一个最近的 IP 地址以及相关的硬件地址表,如果外发的包不能匹配任何已知的硬件地址,代码将会执行一个 ARP 请求来解析正确的地址。

一个应用程序员不应该直接调用这个协议,因为如果一切工作正常,那么这个过程是透明的。有几个标识能够确定 ARP 表的大小、包是如何在挂起的 ARP 请求后排队的,以及 ARP 代码是否需要从到来的包中学习 MAC-IP 地址绑定关系。这些选项可以在 lwipopts.h 头文件中配置。

IPv4

现今使用的主流的网络层协议。

IPv4 (Internet Protocol version 4) 是现今广为使用的网络协议。它具有如下特性:

  • 尽可能发送,网络设备尽可能将要发送的包发送到目标点,然而不能确保百分百完成,所有的中间媒介都会尽可能完成这个任务
  • 不能保证发送或重传质量,目标点不能通知主机它接受到了数据包
  • 接受到包的顺序可能与发送包的顺序不同,一旦包发送到网络上,它们可能会经过完全不同的路由。因此包到达目标点的顺序是不能保证的
  • 主机可能会收到重复的包

针对应用向的 IPv4

操作地址

一个 IPv4 地址由 32 比特位长的字串组成,可以通过 . 划分成格式 127.0.0.1。在 lwIP 中,IP 地址由 struct ip_addr 结构体持有,它持有无符号的 32 比特位长的子。因此,设置一个 IP 地址,可以使用如下的代码:

#include <lwip/ip_addr.h>
struct ip_addr local;
IP4_ADDR(&local, 127.0.0.1); // 设置回环地址为 127.0.0.1

其他会实用到 ip_addr 结构体的如下:

  • IP_ADDR_ANY 任意地址,比如,如果你想要监听一个 TCP 端口,但是不希望绑定到一个指定的地址
  • ip_addr_set(dest, src) 从一个结构体复制地址到另一个结构体
  • ip_addr_cmp(addr1, addr2) 比较两个地址是否相同
  • ip4_addr1(ipaddr) IP 地址的第一个字节,比如 192.168.133.144 中的 192
  • ip4_addr2(ipaddr) IP 地址的第二个字节,比如 192.168.133.144 中的 168
  • ip4_addr3(ipaddr) IP 地址的第三个字节,比如 192.168.133.144 中的 133
  • ip4_addr4(ipaddr) IP 地址的第四个字节,比如 192.168.133.144 中的 144
主机与网络字节顺序

因为大端、小端的架构不同,必须确定结构体中的值是按照主机中的字序排布的还是网络字节序排布的。

网络字节序是以大端排布的。你的主机平台可能也是大端,这样你的代码无需考虑这些技术细节。但是,如果你为一台小端处理器 (比如 IntelAMDARM 等) 重新编译了你的程序,那么你的代码可能会出现一些难以确定的问题。

因此,下面的一些主机到网络的字节转换函数可能会有帮助,这些函数如下:

  • netval = htonl(hostval)32 比特位主机字序转换为网络字序
  • netval = htons(hostval)16 比特位主机字序转换为网络字序
  • hostval = ntohl(netval)32 比特位网络字序转换为主机字序
  • hostval = ntohs(netval)16 比特位网络字序转换为主机字序

lwIP 在大部分情况下,会自动为你注意所有的内部网络协议结构体。在你手动设置或比较 IP 地址时,需要注意这一点;或者,如果你将结构体串起到包中,并发送到另一个系统上时需要注意这一点。如果你的目标的字序与你主机的字序不同,那么你必须按照既定的标准,保证你这边的字序与对方的字序一致(你这边的16位与对方的16位一致)。

注意一点ip_addr->addr 是网络字序排布的,u32_t someip 是主机字序排布的。

  • IN_CLASSA 宏检查一个 IP 地址是否是一个 A 类地址,它操作 32 比特值,因此是主机字节序
  • IP_ADDR_ANY 宏是 struct ip_addr 结构体,它的 IP 地址为网络字节序
  • ip_addr_cmp 宏比较两个保存为 struct ip_addr 中的 IP 地址,因此它是网络字节序

下面一些实例:

struct ip_addr ip, otherip;

ip.addr = 0x7f000001;       /* BAD,大端下为 127.0.0.1,小端下为 1.0.0.127 */
IP4_ADDR(&ip, 127.0.0.1);   /* GOOD */
ip_addr_set(&otherip, &ip); /* GOOD,网络字序分配给网络字序 */
otherip.addr = ip.addr;     /* BAD,可以工作,但是最好实用宏 */

if(ip.addr == 0x7f000001);  /* BAD,在大端上 true,小端上 false */
if(ip.addr == 0x0100007f);  /* BAD,在小端上 true,大端上 false */

if(ntohl(ip.addr) == 0x7f000001);   /* GOOD */
if(ip_addr_cmp(&ip, &otherip));     /* GOOD */
为接口分配地址

一个网络接口只能具有一个 IP 地址。在 lwIP 中,支持三种方法为接口分配 IP 地址:

  • 静态 IP: netifIP 地址可以通过 netif_addnetif_set_addrnetif_set_ipaddr 进行初始化
  • DHCP 动态获取: DHCP 是一个可选协议,可从 DHCP 服务器上获取动态 IP
  • AUTOIP: AUTOIP 是一个可选协议,可以从本地子网中挑选一个 IP 地址,而不需要通过服务器为其分配地址

IPv6

IPv4 的后继,将 IP 地址扩展为 128 比特。

应用角度的 IPv6

IPv6 的支持现在已经添加到 lwIP 中。在 v1.4.x 以上版本的 lwIP 可以在 IPv4IPv6 之间进行选择,但不是两者同时使用。双协议栈的使用是当前开发版本中的一个特性,可能会在 v1.5.0 版本发布。

ICMP

IP 的控制协议。

ICMP 支持

ICMP 应用支持三个协议:

  • Echo Replyping,客户端响应 ping 请求,响应 IP 包中的数据
  • 目的地不可达,指示设备不能转发 IP 数据包。比如,如果一个包寻址到的设备需求一个本设备不支持的协议,就会报这个错误
  • 超时,指示设备因为包的生命周期 Time To Live TTL 到达 0,而丢弃了数据包

应用角度的 ICMP

ICMP 信息由 lwIP 协议栈自身处理或生成。因此一个应用不应该与 ICMP 的代码进行交互。如果用户希望生成自己的 ping,那么可以组织一个包作为 IP 包,通过 IP 模块进行发送。

IGMP

IP 进行的多播协议。

传输层协议

UDP

没有可靠握手机制的套接字协议。

TCP

具有握手,可以进行流控的协议。

其他

DHCP

在带有服务器时的 IP 地址获取方法。

应用角度的 DHCP

为了使能 DHCP,你必须确保编译、链接到 DHCP。你可以通过在 lwipopts.h 头文件中将 LWIP_DHCP 值设置为 1 来使能这一功能,这会添加指向 dhcp 结构体的字段到 netif 中。dhcp 结构体会在 dhcp_start() 中进行分配。另外,LWIP_UDP 必不能设置为 0,因为 DHCP 是运行在 UDP 上的协议。

在一个简单的设置中,在初始化接口之后,可以简单的调用:

dhcp_start(&mynetif);

之后,为了正确处理动态 IP 使用时限,DHCP 需要调用一系列的计时函数。在 1.4.0 版本之后,你只需要调用一个单独的函数,来处理协议栈中的所有的定时器,因此添加下面的函数到你的 main 循环中是等效的:

sys_check_timeouts();

之后,你需要检查你的接口 ->dhcp->state == DHCP_BOUND,就可以了。

对于 lwIP 2.0,你需要调用:

dhcp_supplied_address(const struct netif *netif)

如果需要处理比较复杂的网络变化环境,比如,一个移动设备可能会不停的更换链接到的网络,那么你需要告知 DHCP 函数这一情况。这通常可以通过调用 dhcp_network_changed() 函数实现。因为协议栈中的 AUTOIP 以及 IGMP 协议也需要关注这个情况,所以正确的调用方法是:

netif_set_link_up(&mynetif);
netif_set_link_down(&mynetif);

这两个函数需要与接口的 link 状态变化绑定。

下面是详细信息,为了在一个接口上使用 DHCP,简单实用如下函数:

  • dhcp_start() 为一个接口启动 DHCP 配置
  • dhcp_renew() 强制刷新之前的时限
  • dhcp_release() 释放 DHCP 时限,通常在 dhcp_stop() 之前调用
  • dhcp_stop() 为一个接口停止 DHCP 配置
  • dhcp_inform() 告知服务器我们的 IP 地址

注意: 这些函数是 lwIP 的内核函数。它并不会受到并发访问的保护。在多线程环境中,它们可能只在核心线程调用 (即,tcp-ip 线程)。在其他线程调用时,使用在 netifapi.c 中定义的对应版本的 netifapi_dhcp_*() 函数。

一个选择是利用 PHY 自协商的特点。大部分 PHY 在连接状态 link 发生改变时生成中断。对 PHY 连接状态发生改变的中断处理传递给 tcp-ip 线程。之后在 HandlePhyInterrupt,如果 linkup 态,进行任何必要的硬件寄存器调整,来适配子协商的速度,之后调用 dhcp_start。如果 linkdown 态,首先调用 netif_set_down(),前面提到的其他的 dhcp 函数可能就不需要了。

AUTOIP

没有服务器时的 IP 地址选择方法。

应用角度的 AUTOIP

为了使能 AUTOIP,需要配置 lwipopts.h 头文件中的 LWIP_AUTOIP

在一个接口上使用 AUTOIP,简单实用如下命令:

  • autoip_start() 使用一个新的 IP 另接口 up
  • autoip_stop() 使接口 down

注意: 这些函数是 lwIP 的内核函数。不会受到并发访问的保护。在多线程的环境下,工作类似 DHCP

SNMP

用来监控网络条件。

PPP

用来在两个节点之间直接创建链接。

应用角度的 PPP

有两种使用 PPP 的方式,一种是通过 PPPoE (PPP over Ethernet),一种是 PPP over seriallwIP 支持在线程环境下运行,这个情况下 PPP 是一个单独的任务,与 lwIP 主线程相分离。lwIP 也支持在 main 循环中运行,在 main 函数中调用 lwIP 的函数。

串行下的 PPP

为了设置 PPP 通过串行链接,你需要提供串行 IO 功能。

无论是线程环境还是 main 循环函数环境,都需要应用 sio_write()

/**
 * Writes to the serial device.
 * 
 * @param fd serial device handle
 * @param data pointer to data to send
 * @param len length (in bytes) of data to send
 * @return number of bytes actually sent
 * 
 * @note This function will block until all data can be sent.
 */
u32_t sio_write(sio_fd_t fd, u8_t *data, u32_t len);
任务支持

除了 sio_write(),还需要应用 sio_read()sio_read_abort() 以及 linkStatusCB。前两个函数是静态链接的,最后一个函数通过函数指针定义 (因此名称可以不同,也可以动态创建)。

函数 sio_read()pppInputThread 中重复调用。它会阻塞等待,直到超时或请求缓冲区被填充,会从串行设备上读取数据。如果调用了 sio_read_abort() 那么必须废弃掉 sio_read()。在 sio_readsio_read_abort 之间这样的软连接关系必须被开发者自己实现,可以通过全局变量实现,可以通过 RTOS 的事件实现等。超时时长需要较小,大小只需要数据足够有 1 字节宽度满足 PPP 协议栈响应即可。2 毫秒的时长就已经足够了。

/**
 * Reads from the serial device.
 * 
 * @param fd serial device handle
 * @param data pointer to data buffer for receiving
 * @param len maximum length (in bytes) of data to receive
 * @return number of bytes actually received - may be 0 if aborted by sio_read_abort
 * 
 */
u32_t sio_read(sio_fd_t fd, u8_t *data, u32_t len);

sio_read_abort() 函数必须要让 sio_read 马上退出。这是在 tcpip_thread 中实现的,大部分情况是在结束一个 PPP 会话时执行 (终止、链接断开或显式关闭)。

/**
 * Reads from the serial device.
 * 
 * @param fd serial device handle
 * @param data pointer to data buffer for receiving
 * @param len maximum length (in bytes) of data to receive
 * @return number of bytes actually received - may be 0 if aborted by sio_read_abort
 * 
 */
u32_t sio_read(sio_fd_t fd, u8_t *data, u32_t len);
回调函数

下面的回调函数:

void (*linkStatusCB)(void *ctx, int errCode, void *arg)

会在下面事件下调用:

  • 链接终止,errCode0arg 为空
  • 接口 uperrCodePPPERR_NONEarg 是指向 ppp_addrs 结构体,这个结构体中包含 IP 地址
  • 接口 downerrCodePPPERR_CONNECTarg 为空

errCode 可以为下面的情况:

#define PPPERR_NONE      0 /* 无错误 */
#define PPPERR_PARAM    -1 /* 无效参数 */
#define PPPERR_OPEN     -2 /* 不能开启 PPP 会话 */
#define PPPERR_DEVICE   -3 /* 无效的 IO 设备 */
#define PPPERR_ALLOC    -4 /* 不能分配资源 */
#define PPPERR_USER     -5 /* 用户打断 */
#define PPPERR_CONNECT  -6 /* 链接丢失 */
#define PPPERR_AUTHFAIL -7 /* 认证失败 */
#define PPPERR_PROTOCOL -8 /* 协议不匹配 */

ctx 指针是用户可选定义的指针,是调用 pppOverSerialOpen 函数的参数,可以指向用户定义的数据。

无任务支持

你需要在你的主线程中接收串行数据,之后你的主线程调用:

pppos_input(int pd, u_char *data, int len);
posted @ 2022-01-12 13:06  ArvinDu  阅读(1502)  评论(0编辑  收藏  举报