2024年10月3日11:53:29 lwIP-version-1.4.1
INTRODUCTION
lwIP 是一个独立实现的小巧精致的 TCP/IP 协议栈套件。lwIP 致力于协议栈运行时的低 RAM 占用率且兼具完备TCP功能这两点。因此开发出来的 lwIP 适用于嵌入式系统,要求有几十K字节余量的RAM空间,以及将近 40k 字节的ROM空间。lwIP已经成长为一个适用于嵌入式系统且卓越的 TCP/IP 协议栈。
FEATURES
-
IP (Internet Protocol) including packet forwarding over multiple network interfaces
-
ICMP (Internet Control Message Protocol) for network maintenance and debugging
-
IGMP (Internet Group Management Protocol) for multicast traffic management
-
UDP (User Datagram Protocol) including experimental UDP-lite extensions
-
TCP (Transmission Control Protocol) with congestion control, RTT estimation and fast recovery/fast retransmit
-
Specialized raw/native API for enhanced performance
-
Optional Berkeley-like socket API
-
DNS (Domain names resolver)
-
SNMP (Simple Network Management Protocol)
-
DHCP (Dynamic Host Configuration Protocol)
-
AUTOIP (for IPv4, conform with RFC 3927)
-
PPP (Point-to-Point Protocol)
-
ARP (Address Resolution Protocol) for Ethernet
路径 | 路径下的内容 |
---|---|
api/ | 路径下是有些高层级的封装函数。若使用底层回调或原生(RAW)APIs,就不需要用到此目录下的文件 |
core/ | TCP/IP协议栈实现的核心,包含各协议的实现、内存与缓冲区管理、以及底层原生(RAW)APIs |
include/ | lwIP 的 include 文件 |
netif/ | 通用的网卡接口设备驱动、以及 ARP 模块 |
上面是1.4.1版本的,下面是 lwIP-2.1.3 版本的路径文件描述:
路径 | 路径下的内容 |
---|---|
api/ | 高层级的封装函数,不适用于 RAW API |
apps/ | 基于 lwIP 底层的 RAW API 接口编程的应用代码 |
core/ | TCP/IP 协议栈内核实现的代码部分,包含各协议的实现、内存与缓冲区管理、以及底层原生(RAW)APIs |
include/ | lwIP 的 include 文件 |
netif/ | 通用的网卡接口设备驱动 |
若需了解更多关于子目录的信息,请查阅对应目录下的“FILES”文件。
Raw TCP/IP interface for lwIP
lwIP 给程序员提供了三套类型的编程接口APIs函数,用于同 tcp/ip 协议栈代码通信:
-
low-level "core" / "callback" or "raw" API.
-
higher-level "sequential" API. (作者起名,时间逻辑的顺序,简称序列API)
-
BSD-style socket API. (套接字API)
序列API,提供了一种普通的、时间逻辑顺序的lwIP协议栈编程方式。它与 BSD socket API 的编程方式十分相近。它的运行逻辑过程基于 锁定、打开-读取-写入-关闭 的模式。因为 TCP/IP 协议栈天生就是事件驱动型,所以 TCP/IP 代码以及应用程序代码,必须在各自的线程中运行。
套接字 API 是现有应用程序的兼容 API,目前它是基于序列API构建的。套接字API致力于提供能运行在其他平台(unix/windows等)套接字API上所有必要的功能。(个人理解:言下之意,就是为了兼容现有的unix/windows的socket api,才开发了这套API,故而叫 socket API)。但是,由于这套socket API的规格存在局限,移植时,需要对现存程序做小小适配修改。
截至lwIP version 2.2.0 版本,仍然存在如下设计实现方式:BSD-style socket API 是在 sequential API 代码基础上编写的,sequential API 是在 RAW API 代码的基础上编写的。(文档总结)
Threading
最开始,lwIP致力于单线程环境(也就是裸机)。当有多线程环境支持后,没有选择去走tcpip内核线程安全的路线,而是选择了另一种道路:只设计了一个用于运行 lwIP 内核的主线程(也就是 tcpip_thread)。RAW API 只能用于这个线程中。使用序列API或者套接字API的应用程序线程,通过消息传递与这个主线程通信。
简言之,就是随着项目代码设计的演变,协议内核的设计模式仍然没变。这种设计方式就是,所有关于协议内核实现的代码都放在RTOS的一个线程内部;裸机情况放在while(1){协议内核实现{子模块scanning-callbacks}},这时与应用程序共处一个while(1){}内。(此段译者添加20241122)
因此,可供从其他线程或ISR中断中调用的函数数量非常有限!只有那些API头文件中列出的函数,才是线程安全的:
-
api.h
-
netbuf.h
-
netdb.h
-
netifapi.h
-
sockets.h
-
sys.h
另外,在 NO_SYS=0 的情况下,内存的分配和释放函数可能会被线程调用,中断中不允许调用!因为它们被 SYS_LIGHTWEIGHT_PROT 和/或者 信号量保护。
只有从 1.3.0 版本后,若 SYS_LIGHTWEIGHT_PROT = 1 且 LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT = 1时,pbuf_free() 也可以从另一个线程或ISR终端中调用(因为只有这样,才能从中断中调用 mem_free 用于 PBUF_RAM:否则,堆仅受信号量的保护)。
The remainder of "raw" API
使用原始 TCP/IP 接口(RAW API)可以将应用程序更好地与 TCP/IP 内核代码整合。程序运行是基于事件驱动的,实现的方式是, TCP/IP 内核代码内置并向外输出的回调函数。TCP/IP 内核代码与应用程序代码在同一个线程中运行。而序列API的开销要高得多,对资源紧缺型嵌入式系统不是很友好,因为它强制应用程序采用多线程范式。
采用 RAW API 实现的代码,不仅运行速度更快,而且还占用更小的内存开销。缺点是程序开发稍微困难一些,并且应用代码更难理解。即使这样,在Flash/RAM资源紧缺型嵌入式硬件环境中,RAW API 仍然是首选。
这两种 API 都可以被不同的应用程序同时使用。事实上 sequential API 是基于 RAW API 接口代码封装实现的上层代码。
回调函数
程序的执行是由回调函数驱动的。每个回调函数都是一个普通的C函数,只不过这些回调函数都在 TCP/IP 内核代码中被调用。每个回调函数通过参数形式传入当下 TCP 或 UDP 的连接状态。此外,为了能够跟踪应用程序的特定状态,回调函数在被调用的时候,还带有一个不属于 TCP/IP 状态的特殊形参。
用于设置应用程序连接状态的函数是:
-
void tcp_arg(struct tcp_pcb *pcb, void *arg)
此函数明确了应该传入所有回调函数的程序状态。pcb形参是当下的 TCP 连接控制块,arg形参是传入回调函数内的参数。
建立 TCP 连接
RAW API 中用于建立连接的函数与序列API、BSD套接字的类似。使用 tcp_new() 函数,可以创建一个新的 TCP 连接标识符(比如 a protocol control block - PCB)。这个 PCB 可以设定成去监听一个新的即将到来的连接,也可以明确地设定成连接到某个主机。
struct tcp_pcb *tcp_new(void)
创建一个连接标识符(PCB)。若无充足内存来创建,则返回 NULL。
err_t tcp_bind(struct tcp_pcb *pcb, ip_addr_t *ipaddr, u16_t port)
绑定 PCB 连接标识符到本地的 IP:PORT 。可以将 IP 地址指定为 IP_ADDR_ANY,以便将连接绑定到所有本地 IP 地址。若另一个绑定到相同接口编号的连接已经建立,那么返回 ERR_USE(占用中),否者返回 ERR_OK(成功)。
struct tcp_pcb *tcp_listen(struct tcp_pcb *pcb)
此函数,驱使一个pcb去监听到来的连接(请求)。建立连接后,会调用 tcp_accept() 指定的函数。此后,使用 tcp_bind() 绑定一个本地端口(PORT)。 tcp_listen() 函数返回一个新的连接标识符,并且作为参数传递给该函数的连接标识符将被释放。这样做的原因是,监听的连接需要更少的内存,因此 tcp_listen() 将回收原始连接的内存,并为监听连接分配一个新的较小的内存块。 如果没有内存可用于监听连接,tcp_listen() 会返回 NULL。如果这样,那么作为参数传递给 tcp_listen() 的 pcb 关联的内存将不会被释放。
struct tcp_pcb *tcp_listen_with_backlog(struct tcp_pcb *pcb, u8_t backlog)
跟 tcp_listen 实现的功能相同。不同点在于,此函数会限制监听队列连接请求的数量,由 backlog 此参数进行限定。若要使用此函数,需设置 lwipopts.h 文件中的 TCP_LISTEN_BACKLOG=1。
void tcp_accepted(struct tcp_pcb *pcb)
通知 lwIP 已接受传入连接。这通常会从接受回调中调用。这会让 lwIP 内核执行内部任务,例如促使后续传入连接在监听队列中排队。 注意:此函数传入的 PCB 形参必须是监听的PCB,而不是传入接收回调的 pcb.
void tcp_accept(struct tcp_pcb pcb, err_t (*accept)(void *arg, struct tcp_pcb *newpcb, err_t err))
指定当监听到新的连接时,需要调用的回调函数。
err_t tcp_connect(struct tcp_pcb *pcb, ip_addr_t * ipaddr, u16_t port, err_t (* connected)(void *arg, struct tcp_pcb *tpcb, err_t err));
建立本地 PCB 与远程主机的连接,并发用于送打开连接的初始 SYN 段。 tcp_connect() 会立即返回,它并不会等待完全建立好连接。相反,当连接建立好以后,会调用此函数指定的第四位形参所设定的函数(也就是 connected() 函数)。可能因为远程主机拒绝连接或者没有应答,而导致创建连接失败,则注册到 PCB 内的 tcp_err 回调函数,就会被调用。 当用于存储 SYN 段的队列没有内存时,The tcp_connect() 返回 ERR_MEM。若 SYN 排入队列成功,则返回 ERR_OK。
Sending TCP data
TCP 数据的发送方式是调用 tcp_write() 将欲发送的数据排入队列。当数据已成功传给远程主机后,会由指定的回调函数通知给应用程序。
err_t tcp_write(struct tcp_pcb *pcb, const void *dataptr, u16_t len, u8_t apiflags)
此函数将 dataptr 形参指针所指向的数据入队(排入队列)。数据长度由 len 形参指定。apiflags 形参可以指定为:
-
TCP_WRITE_FLAG_COPY:指明是否为即将复制进来的数据分配新内存。如标志位是否定的,那么将不会分配内存,数据只能通过指针方式引用。这种情况下,也意味着 dataptr 指针不允许被修改,直到远程主机传来 ACK。
-
TCP_WRITE_FLAG_MORE:表明还有后续数据传入。若标记位是肯定的,那么将在由此 tcp_write() 创建的最后一个数据段中设置 PSH 标志位。若此标志位是否定的,则不会设置 PSH 标志位。
如果数据长度超出当前发送缓冲区长度,亦或数据输出段队列的长度大于 lwipopts.h 文件中设定的上限,那么 tcp_write() 将会执行失败,并返回 ERR_MEM。tcp_sndbuf() 中可以找到输出队列的可用字节的数。
正确使用 tcp_write() 函数的方式是:以 tcp_sndbuf() 所指明的最大字节数去调用此函数,如果此函数返回 ERR_MEM,则应用程序应该继续等待,直到队列中的一些数据被成功传给远程主机后,再重新尝试。
void tcp_sent(struct tcp_pcb *pcb, err_t ( * sent)(void *arg, struct tcp_pcb *tpcb, u16_t len))
此函数设置了当数据被远程主机成功接收后的回调函数。回调函数中的 len 形参给出了接收到的数据字节量。
Receiving TCP data
TCP 数据的接收过程是基于回调函数实现的,应用程序会指定当数据到来时需要调用的回调函数。当应用程序取走数据后,它必须调用 tcp_recved() 去通知 TCP 可以通知增加接收窗口。
void tcp_recv(struct tcp_pcb *pcb, err_t ( * recv)(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err))
此函数设定当数据到达后需要调用的回调函数。给回调函数传入 NULL 的 pbuf,意味着远程主机已经关闭了连接。如果没有错误且回调函数返回 ERR_OK ,则必须释放 pbuf 。否则,它不允许释放 pbuf 以便 lwIP 内核代码可以存入数据到 pbuf。
void tcp_recved(struct tcp_pcb *pcb, u16_t len)
当应用程序已经接收到数据后,必须调用此函数。len形参表示已接收数据的长度。
Application polling
当某个连接处于空闲期(比如没有数据发送或者接收), lwIP 会调用一个指定的回调函数去轮询应用程序。这种实现机制被用于看门狗定时器去杀死那些已经空闲很久的连接,也可为等待可用内存提供一种实现手段。比如,如果由于内存可用空间不足导致 tcp_write() 执行失败,则当连接已空闲一段时间后,应用程序可以使用这个轮询功能去再次调用 tcp_write() 函数。
void tcp_poll(struct tcp_pcb *pcb, err_t ( * poll)(void *arg, struct tcp_pcb *tpcb), u8_t interval)
此函数用于设定轮询的时间间隔以及用于轮询应用程序的回调函数。间隔以 TCP 粗粒度计时器触发次数指定,通常每秒发生两次。间隔 10 表示每 5 秒轮询一次应用程序。
Closing and aborting connections
err_t tcp_close(struct tcp_pcb *pcb)
此函数执行关闭连接操作。若无可用空间用于关闭连接,则返回 ERR_MEM. 若发生这种情况,则应用程序应该通过使用 告知回调函数 或 轮询功能去等待和重试。 当调用 tcp_close() 后, TCP 代码会释放 pcb .
void tcp_abort(struct tcp_pcb *pcb)
发送一个 RST(reset)段给远程主机,会终止连接。此后 pcb 会被释放,此函数永远不会执行失败。
注意:在 TCP 回调函数中调用此函数时,请确保始终返回 ERR_ABRT(否则永远不会返回 ERR_ABRT),否则有访问已释放内存或内存泄漏的风险! 如果因为发生错误而中止连接,应用程序会通过回调函数的 err 收到错误事件的警报。内存不足可能导致终止连接。使用 tcp_err() 函数设定要调用的回调函数。
void tcp_err(struct tcp_pcb *pcb, void ( * err)(void *arg, err_t err))
错误回调函数不会获取到 pcb 形参,因为 pcb 可能早已被释放。
Lower layer TCP interface
TCP 向底层系统提供了简洁的接口函数APIs。在系统初始化期间,tcp_init() 必须在其他任何 TCP 函数之前调用。在系统运行时,两个定时器功能函数 tcp_fasttmr() 和 tcp_slowtmr() 必须周期性地执行。tcp_fasttmr() 的运行周期定义于 tcp.h 中的 TCP_FAST_INTERVAL ,单位是毫秒;tcp_slowtmr() 的运行周期是 TCP_SLOW_INTERVAL 毫秒。
UDP interface
UDP 接口API函数类似 TCP 的API函数。但是因为 UDP 底层的复杂度较低,所以 UDP 接口APIs 要简单很多。
struct udp_pcb *udp_new(void)
创建一个UDP的pcb,用于 UDP 通信。这个 PCB 只能在绑定到本地地址或连接到远程地址后才被激活。
void udp_remove(struct udp_pcb *pcb)
移除并释放 PCB .
err_t udp_bind(struct udp_pcb *pcb, ip_addr_t *ipaddr, u16_t port)
将 PCB 绑定到本地地址。IP地址参数 “ipaddr” 可以设定为 IP_AD