TCP/IP协议栈
LwIP架构
进程模型
进程模型是指TCP/IP协议栈的各协议入IP协议、TCP协议、ICMP协议等是如何实现的。
- TCP/IP协议栈的每个协议都通过一个不同的进程实现。在该模型下,每个进程都严格地与一个协议相对应。这种进程模型的优点是网络协议的每一层都很清晰,每一层都可以随时参与系统运行。该模型的缺点是进程间的上下文切换比较频繁,系统将为频繁的上下文切换付出较大的代价。
- TCP/IP协议栈驻留在操作系统的内核中,应用程序通过系统调用与TCP/IP协议栈通信。该模型下,各协议栈并非严格地与一个进程相对应。
- TCP/IP协议栈驻留在同一个进程中,独立于操作系统内核空间。LwIP采用正是这种方式,LwIP作为一个独立的进程,运行在用户空间内,其优点是可以方便地移植到不同的操作系统中运行。
内存管理
LwIP的动态内存管理机制大致上可以分成三种:标准C运行库自带的内存分配策略、LwIP的动态内存堆分配策略、LwIP的动态内存池分配策略。
- 将MEM_LIBC_MALLOC设置为1,表明使用标准C库自带的内存分配策略
- 将MEMP_MEM_MALLOC设置为1,表明使用LwIP自己的动态内存堆分配策略
- LwIP还支持内存池,不过在ESP-IDF中并没有被使能。相较于内存堆的动态分配,内存池效率更高,碎片少,但是会消耗更多的内存
缓冲管理
LwIP的缓冲管理机制的功能是尽量避免内存拷贝,尽量较少对内存和空间的需求,提高程序的执行效率。LwIP使用数据结构pbuf来描述内存的缓冲数据包。
- 由于实际发送或接收的数据包长度不一,而每个pbuf只能管理一部分数据,因此对于大容量的数据包,就必须使用多个pbuf才能完整地描述它
- type表明了该pbuf的类型,目前LwIP定义了四种类型的pbuf,分别是:
PBUF_RAM
,PBUF_ROM
,PBUF_REF
,PBUF_POOL
- PBUF_RAM类型的pbuf是通过内存堆分配得到的,LwIP协议栈和应用程序要传递的数据一般都使用该类型的pbuf。
- PBUF_POOL类型的pbuf是通过内存池分配得到的,由于分配此类型的pbuf可以快速完成,适合中断处理,因此它更多地应用在网络设备驱动层。
- PBUF_REF和PBUF_ROM类型的pbuf基本相同,他们都是从内存池中申请分配pbuf结构首部空间,而不申请数据区的空间。两者的区别在于,前者指向RAM空间内的某段数据,后者指向ROM空间内的某段数据。
pbuf管理API
当使用Netconn API时,则使用netbuf(网络缓冲)发送/接收数据,netbuf只是pbuf结构的封装,它可容纳分配的或引用的数据。
网络接口层
在LwIP中,物理网络硬件的设备驱动通过网络接口结构体netif来描述一个硬件网络接口,并通过
netif_add
函数向全局变量netif链表结构增加一个硬件网络接口。
1 | /** Generic data structure used for all lwIP network interfaces. |
- ip_addr,netmask,gw分别表示了IP地址、子网掩码、网关,建议这样子设定:
IP4_ADDR(&ipaddr,192,168,1,100)
- mtu表明最大的网络传输个数,以字节为单位
- hwaddr存放了硬件接口的地址,对于以太网而言,就是MAC地址
- flags是硬件接口状态信息标志位,如是否建立连接状态,是否允许广播功能等
- name用来表示硬件接口使用的驱动类型,缩写,2个字节,比如蓝牙设备为“bl”,wifi设备为”wl”
- num用来表示硬件接口的编号,当两个硬件接口的name字段相同时,该字段可以用来区分是哪一个硬件接口
- input是一个函数指针,它指向的函数用于将网络硬件接口接收到的数据包传递给上层TCP/IP协议栈
- output是一个函数指针,它所指向的函数用于将IP层的数据包发送到网络硬件接口上
- linkoutput是一个函数指针,在ARP模块中调用,output指向的函数也是通过调用linkoutput指向的函数实现数据报发送的
ARP处理
ARP协议是TCP/IP协议的基础,本质是实现IP地址与底层物理地址的相互转换。ARP协议的核心是ARP缓存表,而ARP协议的实质就是对缓存表的建立、更新、查询等操作。ARP缓存表是由若干缓存表项组成,在LwIP中,描述缓存表项的数据结构叫etharp_entry。
1 | struct etharp_entry { |
- ipaddr存放IP地址,ethaddr存放物理地址,state表示缓存项的状态(例如是否为空,是否稳定),ctime记录ARP缓存项处于某个状态的时间,当某表项的ctime值大于规定的表项最大生存值时,LwIP内核会删除该表项。因此使用ARP功能时,必须设置一个ARP超时事件,该超时事件的基本功能就是对每个表项的ctime字段值加1,然后删除那些生存时间大于最大生存值的表项
- 函数ethernet_input根据报文首部的帧类型字段判断接收到的报文类型,如果是IP包,则将该包传递给etharp_ip_input,如果是ARP包,则将该包递交给etharp_arp_input
- 函数etharp_ip_input调用函数update_arp_entry,它是将报文首部的MAC地址和IP地址更新到ARP缓存中
- 函数etharp_arp_input首先判断接收到的ARP数据包的类型,如果是ARP请求包,那么首先判断这个包是否是给自己的,如果是给自己的,就在原有包的基础上重组一个ARP应答包发送出去;如果不是给自己的,则直接忽略而如果接收到的数据包是ARP应答包,那么就调用update_arp_entry更新ARP缓存表
IP处理
LwIP软件大致框架
- ip_input会做各项检查,包括协议版本号,IP首部的校验值,源IP地址是否有效等,然后检测IP数据包中的目的IP地址是否与本节点的IP地址相符,如果是本节点的IP地址,则根据该IP数据包首部的协议字段判断该数据包应该被递交到哪个上层协议,并调用相应的函数。如果是UDP协议,则调用udp_input函数;如果是TCP协议,则调用tcp_input函数;如果是ICMP协议,则调用icmp_input函数;如果是IGMP协议,则调用igmp_input函数;如果都不是,则调用函数icmp_dest_unreach返回一个协议不可达ICMP数据包给源主机。如果不是本节点的IP地址,则通过调用函数ip_forward对数据包进行转发。需要注意,由于一个节点可能含有多个IP地址,因此ip_input函数会遍历网络接口链表netif_list上的netif结构变量,来查找与IP数据包中相匹配的IP地址。
- ip_output使用ip_route函数查找目标网络接口netif来发送IP数据包。当网络接口netif确定后,IP数据包通过函数ip_output_if发送出去。若ip_route没有找到合适的网络接口,则丢弃该报文,终止本次发送。函数ip_route通过遍历网络接口链表netif_list,查找与目的IIP地址在同一个子网中的网络接口,并将该网络接口返回给变量netif。
ICMP处理
- icmp_input在ip_input中被调用,它处理接收到的ICMP数据包,并根据包类型做相应的处理。在LwIP协议栈中,它只处理ICMP回显请求包,对其他类型的ICMP包不作响应。icmp_input在处理ICMP回显请求时,首先判断该数据包是否为广播或者组播包,如果是,则直接返回,不再继续处理;如果不是,则继续判断该数据包长度是否小于ICMP回显请求头部长度,如果是则丢弃数据包;如果不是则将该ICMP报文类型字段变为0,重新计算校验和,并将IP报文首部的源IP地址和目的IP地址交换位置,并通过调用函数ip_output_if将数据包发送出去。
- 函数icmp_dest_unreach在ip_input、udp_input中被调用,它的功能是通过调用函数icmp_send_response发送一个“目的不可到达”类型的icmp报文。在函数ip_input中,当所接收的IP报文协议字段不可识别时,icmp_dest_unreach就被调用。而在UDP处理器中,若不能找到与接收的报文相对应的端口号,则icmp_dest_unreach也将被调用。
- 函数icmp_time_exceeded在ip_forward中被调用,它的功能是通过调用函数icmp_send_response发送一个“超时”类型的ICMP报文。在函数ip_forward中,当TTL减小为0时,调用该函数。
UDP处理
- 函数udp_input将检查报文的UDP校验,最终调用函数recv,将收到的报文传递给应用层程序
- 当应用层程序要通过UDP协议向外发送IP报文时,将通过调用函数udp_send实现,函数udp_send通过调用IP层的函数ip_output_if实现报文的发送
- LwIP使用链表结构体udp_pcb来保存每一个UDP会话的状态
1 | struct udp_pcb { |
TCP处理
- TCP的滑动窗口协议是用于实现流量控制的
- TCP的超时和重传机制提高了数据传输的可靠性
- 拥塞控制是通过慢启动算法和拥塞避免算法来实现的
- LwIP中含有两个定时器函数:tcp_fasttmr和tcp_slowtmr,tcp_fasttmr每250ms调用一次,tcp_slowtmr每500ms调用一次。快速定时器主要做两个方面的事情:向上层递交上层一直未接收的数据,二是发送该连接上的延迟ACK请求数据段。慢速定时器参与了较多功能,如超时与重传、拥塞控制等。
常用API接口
LwIP提供了3种应用程序接口:
- 直接调用协议栈各模块的函数,它是基于回调函数的API接口,也成为RAW API接口,回调函数直接被协议栈代码调用,因此应用程序代码和TCP/IP协议栈运行在同一个进程里,无需使用操作系统,两者之间这种良好的结合可以使得程序的执行效率更高,而且在运行中它占用更少的内存资源
- 使用LwIP提供的专用API接口,也称为Sequential API接口,程序的执行过程基于open-read-write-close模型,需要操作系统的支持,另外需要在文件lwipopts.h中把宏定义
NO_SYS
定义为0。Sequential API被分成两部分实现,一部分驻留在应用程序进程中,另一部分在TCP/IP协议栈进程内实现。这两部分API之间采用由操作系统模拟层提供的进程间通信机制进行通信。在LwIP中,操作系统模拟层是LwIP协议栈的一部分,它存在的目的是方面LwIP的移植,它在底层操作系统和LwIP协议栈之间提供了一个接口,当用户移植LwIP到一个新的目标系统的时候,只需要修改这个接口内的函数即可。驻留在应用程序进程中的API接口与TCP/IP协议栈进程中的API之间通过共享内存传递数据,对该共享内存区的描述是采用netbuf结构体- BSD Socket兼容的Socket函数接口,但是BSD套接字需要将发送的数据从应用程序复制到TCP/IP协议栈的内部缓冲区,将会消耗系统有限的资源
TCP RAW API
函数 | 说明 |
---|---|
struct tcp_pcb* tcp_new() | 新建tcp协议控制块 |
ert_t tcp_bind(struct tcp_pcb pcb,struct ip_addr ipaddr,u16_t port) | 绑定本地IP地址和端口号,如果ipaddr为IP_ADDR_ANY,则将连接绑定到所有的本地IP地址上 |
struct tcp_pcb tcp_listen(struct tcp_pcb pcb) | 使指定的连接开始进入监听状态,如果监听成功,就返回一个新的连接控制块pcb |
void tcp_accepted(struct tcp_pcb* pcb) | 通知LwIP一个新来的连接已经被接收,这个函数通常在由tcp_accept指定的回调函数中被调用 |
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,struct ip_addr* ipaddr,u16_t port,err_t (* connected)(void* arg,struct tcp_pcb* tpcb,err_t err)) | 请求连接到执行的远程主机 |
err_t tcp_write(struct tcp_pcb* pcb,void* dataptr,u16_t len,u8_t copy) | 发送TCP数据,将要发送的数据放入发送队列中,由协议栈内核发送,copy为0则不会为发送的数据分配新的内存空间 |
void tcp_sent(struct tcp_pcb* pcb,err_t (*sent)(void* arg,struct tcp_pcb* tpcb,u16_t len)) | 指定当远程主机成功接收数据后,应用程序调用的回调函数 |
void tcp_recv(struct tcp_pcb* pcb,err_t (* recv)(void* arg,struct tcp_pcb* tpcb,struct pbuf* p,err_t err)) | 指定接收数据时调用的回调函数 |
void tcp_recved(struct tcp_pcb* pcb,u16_t len) | 用于获取接收到的数据的长度,必须在tcp_recv指定的回调函数中被调用 |
err_t tcp_close(struct tcp_pcb* pcb) | 关闭一个指定的TCP连接,调用该函数后将会释放pcb控制块所占用的内存空间 |
void tcp_abort(struct tcp_pcb* pcb) | 终止一个指定的连接,调用该函数后,pcb控制块所占用的内存空间将被释放 |
void tcp_err(struct tcp_pcb* pcb,void (*err)(void* arg,err_t err)) | 指定处理错误的回调函数 |
TCP RAW API
Netconn API
- 数据结构netconn描述了应用程序要使用API函数机那里一个连接的各种属性,包括了连接的类型、最近的故障代码、回调函数等。
1 | /** A netconn descriptor */ |
函数 | 说明 |
---|---|
struct netconn* netconn_new_with_proto_and_callback(enum netconn_type t,u8_t proto,netconn_callback callback) | 建立一个新的netconn连接 |
err_t netconn_delete(struct netconn* conn) | 删除netconn所指向的连接 |
err_t netconn_getaddr(struct netconn* conn,struct ip_addr* addr,u16_t* port,u8_t local) | 获取conn连接的主机IP地址和端口号 |
err_t netconn_bind(struct netconn* conn,struct ip_addr* addr,u16_t port) | 将一个IP地址及端口号与conn指向的而连接绑定 |
err_t netconn_connect(struct netconn* conn,struct ip_addr* addr,u16_t port) | 将服务器端的IP地址和端口号与conn指向的连接绑定 |
err_t netconn_disconnect(struct netconn* conn) | 断开conn指向的连接 |
err_t netconn_listen_with_backlog(struct netconn* conn,u8_t backlog) | 将conn指向的连接设定为监听状态 |
struct netconn* netconn_accept(struct netconn* conn) | 接收客户端的连接,该函数会阻塞在acceptmbox邮箱上 |
struct netbuf* netconn_recv(struct netconn* conn) | 接收数据,接收到的数据被封装为netbuf结构 |
err_t netconn_sendto(struct netconn* conn,struct netbuf* buf,struct ip_addr* addr,u16_t port) | 向一个指定的IP地址和端口号发送数据,这个函数只能用在conn类型为UDP或者RAW的连接中 |
err_t netconn_write(struct netconn* conn,const void* dataptr,size_t size,u8_t apiflag) | 向相应的TCP连接上发送数据,这个函数只能用于发送TCP的报文 |
err_t nnetconn_close(struct netconn* conn) | 关闭conn指向的连接 |
- netconn_new_with_proto_and_callback首先调用netconn_alloc函数分配并初始化一个netconn结构,接下来该函数会构建一个api_msg消息,该消息要求内核执行函数do_newconn,最后调用函数tcpip_apimsg来将消息包装成tcpip_msg结构并发送出去。tcpip_thread函数解析该消息并调用函数do_newconn,do_newconn根据参数的类型调用函数tcp_new创建一个TCP控制块
- tcpip_thread是处理TCP/IP的内核协议栈进程,它只接收tcpip_msg结构封装的消息,并根据消息的类型来判定该消息来自物理网卡或应用层程序。如果接收到网卡的IP报文,则将该报文递交给ip_input函数;如果是应用层程序发送的消息,则通过调用消息指定的内核处理函数来完成相应的功能
Socket API
LwIP提供了标准BSD套接字API,它也是有序API,在内存构建于Netconn API之上。所谓“有序”是指其执行模型基于典型的阻塞式打开-读-写-关闭机制。
LwIP移植
- 以太网接口任务用于接收来自物理网卡的数据报文,同时将收到的报文通过FreeRTOS提供的邮箱传递给TCP/IP协议栈任务。以太网接口任务平时处于挂起状态,当硬件收到报文时,将产生接收报文中断,该终端以信号量的方式将以太网接口任务激活
- 应用程序使用TCP/IP协议栈提供的Sequential API接口访问LwIP,同时这两个独立的任务需要使用FreeRTOS提供的邮箱机制实现彼此之间信息的交互。Sequential API接口函数在FreeRTOS操作系统运行环境下是“阻塞”函数,也就是说应用程序任务在调用Sequential API接口函数时,将会被阻塞,直到收到来自TCP/IP协议栈返回的消息应答
- 基于LwIP的TCP/IP协议栈与应用程序运行在两个独立的任务中
- 以太网接口文件ethernetif.c的移植,主要包含
ethernet_low_level_init
,ethernet_low_level_output
,ethernetif_input
,ethernetif_init
这几个函数的功能ethernetif_input
函数用于从底层物理网卡读取报文,并将该报文向上传递给LwIP协议栈函数ethernet_input进行处理ethernetif_init
函数指定了网络接口netif对应的主机名及网卡描述,并指定了该网卡的MAC地址,同事还指定了netif的发送数据报文函数
- 操作系统模拟层文件sys_arch.c的移植,总的来时操作系统模拟层主要完成了与信号量、消息邮箱机制、线程相关的功能
- 在sys_arch.h文件中对信号量、邮箱、线程对象进行重定义
- sys_mbox_new函数,使用FreeRTOS提供的消息队列机制创建一个空的消息队列
- sys_mbox_free函数,删除一个队列,当该队列中还有未被取出的消息时,该函数应当报错,并通知应用程序
- sys_mbox_post函数,将消息发送到消息队列中,该函数是一个阻塞函数,当消息被发送至队列后,该函数才会退出阻塞状态
- sys_mbox_trypost函数,用于尝试将某个消息发送至消息队列中,当消息被成功投递后,则返回成功,否则返回失败
- sys_arch_mbox_fetch函数,用于从消息队列中取出一条消息,该函数是一个阻塞函数,调用该函数的线程若未取到消息,则在形参timeout所指定的时间内,该线程被阻塞。当超过timeout所指定的时间后,该线程恢复至就绪状态。若timeout为0,则调用该函数的线程一直被阻塞,直到收到消息
- sys_arch_mbox_tryfetch函数尝试从消息队列中取出消息,它是一个非阻塞函数,当取到消息时,则返回成功,否则立即退出,返回队列空
- sys_sem_new函数创建一个信号量,并根据形参的值指定好当前信号量的状态
- sys_arch_sem_wait函数在形参timeout指定的时间被阻塞,若timeout为0,则调用该函数的线程将一直被阻塞,直到等待的信号量被释放。但该函数取到信号量时,它将返回取到的该信号量所占的时间
- sys_sem_signal函数用于释放一个信号量
- sys_sem_free函数用于删除一个信号量
- sys_thread_new函数用于创建一个新的线程
- sys_init函数是操作系统模拟层的初始化函数,主要对定时器管理数组进行了初始化
- sys_zrch_timeouts函数用于返回当前任务的定时器管理链表首地址
- sys_arch_protect函数和sys_arch_unprotect函数在访问临界区资源时成对使用
- ethernet_input函数的实现在独立模式和RTOS模式时是不同的:
- 在独立应用中,此函数必须被插入到应用的主循环中,以便轮询任何收到的包
- 在RTOS应用中,此函数为一个阻塞线程,只有当得到所等待的信号量时才处理接收到的数据包。当以太网外设收到数据并产生中断时,会在中断处理函数中释放此信号量
LwIP配置
LwIP提供了名为lwipopts.h的文件,它允许用户充分配置栈及其所有模块。用户不需要定义所有LwIP选项:如果未定义某选项,则使用opt.h文件中定义的默认值
- 内存配置