概述
LWIP协议栈设计与实现笔记
一、进程模型:采用何种方法把系统分成不同的进程。
-
TCP/IP协议族的每一个协议作为一个独立的进程存在。此模型必须符合协议的每一层,同时必须指定协议之间的通讯点。
优势:每一种协议可以随时参与到系统运行中,代码易理解,调试方便。
缺点:该模型不是最好的TCP/IP协议实现方法。
数据跨层传递时将不得不产生进程切换,对于接收一个TCP段来说需要进行三次进程切换,分别是:网络设备驱动层进程到IP进程、IP进程到TCP进程,TCP进程到应用层进程。 -
协议栈驻留在操作系统内核中,应用进程通过系统调用与协议栈通讯。各层协议不必被严格的区分,但可以使用交叉协议分层技术 .
LWIP采取将所有协议驻留在同一个进程的方式,以便独立于操作系统内核之外。应用程序既可以驻留在 LWIP的进程中,也可以使用一个单独的进程。
应用程序与 TCP/IP 协议栈通讯可以采用两种方法:一种是函数调用,这适用于应用程序与 LWIP使用同一个进程的情况;另一种是使用更抽象的 API 。
LWIP在用户空间而不是操作系统内核实现。 LWIP作为一个进程的优点:是可以轻易的移植到不同的操作系统中。 LWIP被设计运行在小系统里,通常它既不支持进程换出,也不支持虚拟内存,因此就不会产生因 LWIP进程的一部分被交换或分页到磁盘上,进程因等待磁盘激活而造成延时的问题。不过在获取一个偶然发生的服务请求之前因任务调度产生的等待延时依然是一个问题,不过在 LWIP的设计中,这并没有妨碍它以后在操作系统内核实现。
二、操作系统模拟层
为了方便 LWIP移植,属于操作系统的函数调用及数据结构并没有在代码中直接使用,而是用操作系统模拟层来代替对这些函数的使用。操作系统模拟层使用统一的接口提供定时器(该定时器由操作系统模拟层提供,是一个时间间隔至少为200ms 的单脉冲定时器, 当时间溢出发生时就会调用一个已注册的函数 )、进程同步及消息传递机制等诸如此类的系统服务。移植 LWIP只需针对目标操作系统修改模拟层实现即可。
进程同步机制仅提供了信号量。即使信号量不被底层的操作系统支持也可以使用其它基本的同步方式来模拟,比如条件变量或者加锁。消息传递通过一个简单机制来实现,它使用一个被称作邮箱的抽象方法。邮箱有两种操作:邮递( post)与提取( fetch),邮递操作不会阻塞进程;相反,投递到邮箱的消息被操作系统模拟层放到队列中直至其它进程将它们取出。即使底层的操作系统本身并不支持邮箱机制,采用信号量的方式也是很容易实现的 。
三、缓冲与内存管理
一个TCP 段可能有几百个字节,而一个 ICMP 回显数据却仅有几个字节。为了避免复制,尽可能的让缓冲区中的数据内容驻留在不能被网络子系统管理的存储区中,比如应用程序存储区或者 ROM。
- pbuf 是 LwIP 信息包的内部表示,为最小限度协议栈的特殊需求而设计。 pbuf 结构即支持动态内存分配保存信息包内容,也支持让信息包数据驻留在静态存储区。 pbufs 可以在一个链表中链接在一起,被称作 pbuf 链,这样一个信息包可以穿越几个 pbufs。
pbufs 有三种类型: PBUF RAM、 PBUF ROM、 PBUF POOL。
PBUF RAM 类型,包数据存储在由 pbuf 子系统管理的存储区中
1、包缓冲区 pbufs
PBUF RAM 类型,包数据存储在由 pbuf 子系统管理的存储区中
PBUF POOL类型,由分配自固定大小的 pbufs 池里的固定大小的 pbufs 组成。一个 pbuf 链可以由pbufs 的不同类型组成
PBUF POOL 主要用于网络设备驱动层,因为分配一个 pbuf 的操作可以快速完成,非常适合用于中断处理
PBUF ROM 类型的 pbufs 用于应用程序要发送的数据放置在应用程序管理的存储区的情况。在 pbuf 已经移交给TCP/IP 协议栈后,这些数据是不能被编辑修改的,因此这种 pbuf 类型主要用于数据被放置在 ROM 中的情况。
PBUF RAM 类型的 pbuf 还用于应用程序发送的数据被动态生成的情况。在这种情况下。pbuf
系统不仅为应用数据分配内存,还要给为这些数据预置的包头分配内存。pbuf系统不可能预先知道为这些数据预置什么样的包头,因而考虑最坏的情况。包头大小在编译时是可配置的。
2、内存管理
负责处理内存连续区域的分配和回收以及收缩已分配内存块的大小。内存管理模块使用系统内存的一部分作为自己的专用区域,这确保了网络系统不会使用系统中所有可用内存,即使网络系统使用了所有自己的内存,也不会扰乱其它程序的操作。
在内部,内存管理模块通过在每一个内存分配块的顶部放置一个比较小的结构体来保存内存分配纪录。这个结构体拥有三个成员变量,两个指针一个标志。 next 与 prev分别指向内存的下一个和上一个分配块,used 标志标示该内存块是否已被分配。
内存管理模块根据所申请分配的大小来搜索所有未被使用的内存分配块,检索到的最先满足条件的内存块将分配给申请者。已经分配的内存块被回收后,使用标志 used 清零。为了防止内存碎片的产生,上一个与下一个分配块的使用标志会被检查,如果他们中的任何一个还未被使用,这个内存块将被合并到一个更大的未使用内存块中。
3、网络接口
strcut netif{
struct netif *next;
char name[2];
int num;
struct ip_addr ipaddr;
struct ip_addr netmask;
struct ip_addr gw;
void (*input)(struct pbuf *p,struct netif *inp);
int (*output)(struct netif *netif,struct pbuf *p,struct ip_addr *ipaddr);
void *state();
};
每一个网络接口都拥有一个名字,保存在name 字段。两个字符的名字标识网络接口使用的设备驱动的种类并且只用于这个接口在运行时由人工操作进行配置的情况。名字由设备驱动来设置并且应该反映通过网络接口表示的硬件的种类。比如蓝牙设备( bluetooth)的网络接口名字可以是 bt。因网络接口的名字不必具有唯一性,因此 num 字段被用来区分相同类别的不同网络接口。
三个 IP 地址 ip_addr, netmask 与 gw 用于 IP 层发送和接收信息包。一个网络接口只能拥有一个 IP 地址,每一个 IP 地址应当创建一个网络接口。
当收到一个信息包时,设备驱动程序调用 input 指针指向的函数。网络接口通过 output 指针连接到设备驱动。这个指针指向设备驱动中一个向物理网络发送信息包的函数,当信息包被发送时由 IP 层调用。这个字段由设备驱动的初始设置函数填充。output 函数的第三个参数 ipaddr 是应该接收实际的链路层帧的主机的 IP 地址。它不 必与 IP 信息包的目的地址相同。特别地,当要发送 IP 信息包到一个并不在本地网络里的主机上时,链路层帧会被发送到网络里的一个路由器上。在这种情况下,给 output 函数的 IP地址将是这个路由器的地址。最后,state 指针指向网络接口的设备驱动特定状态,它由设备驱动设置。
4、IP处理
LWIP仅实现了 IP 层大部分的基本功能,能够发送、接收以及转发信息包,但是不能接收和发送 IP 分片包,也不能处理携带 IP 参数选项的信息包。
接收信息包
- 对到达的 IP 信息包,由网络设备驱动调用 ip_input()函数开始处理。在这里完成对 IP版本字段及包头长度的初始完整性检查,同时还要计算和验证包头校验和。协议栈假定代理会重新组合 IP 分片包为一个完整的包,因此它会把收到的 IP 分片包直接丢掉。同样,信息包携带的 IP 参数选项也被认为已经由代理处理过,这些内容会被删掉。
- 接下来,函数检查目的地址是否与网络接口的 IP 地址相符以确定信息包是否到达预定主机。网络接口在链表中被排序并且采用了线性检索。因为预计接口的数量比较少,所以没有实现比线性检索更好的检索策略。
- 如果一个到达的信息包被发现已经到达了目的主机,则由协议字段来决定信息包应该传送到哪一个上层协议。
发送信息包
- 外发的信息包由 ip_output()函数处理,该函数使用 ip_route()函数查找适当的网络接口来传送信息包。当外发的网络接口确定后,信息包传给以外发网络接口为参数的 ip_output_if()函数。在这里,所有的 IP 包头字段被填充,并且计算 IP 包头校验和。 IP 信息包的源及目标地址作为参数被传递给 ip_output_if()函数。源 IP 地址可以被忽略,不过在这种情况下外发网络接口的 IP 地址会作为 IP 信息包的源 IP 地址被使用。
- ip_route()函数通过线性检索网络接口链表找到适当的网络接口。在检索期间,用网络接口的网络掩码对 IP 信息包的目标地址进行掩码运算。如果经掩码运算的目标地址等与同样经掩码运算的接口 IP 地址( 即网络地址相等,同在一个子网中,译者注),则选择这个接口。如果没有找到匹配的,则使用缺省网络接口。缺省网络接口在启动时或运行时由人工操作进行配置(注意运行期间人工配置需要一个能配置协议栈的应用程序, LWIP不包含这样的程序)。如果缺省网络接口的地址不匹配目的 IP 地址,则网络接口结构体的 gw 字段被选择作为链路层帧的目的 IP 地址(注意,在这种情况下, IP 信息包的目标地址与链路层帧的目标地址是不同的)。路由的基本形式掩盖这样一个事实:一个网络可能拥有很多路由器依附它。不过对于最基本的情况,一个本地网络只拥有一个路由器进行路由工作。
- 因为传输层协议 UDP 与 TCP 在计算传输层校验和的时候需要拥有目标 IP 地址,因此在有些情况下,信息包被传递给 IP 层之前必须确定外发网络接口。这可以让传输层函数直接调用 ip_route()函数做到,因为在信息包到达 IP 层时已经知道了外发网络接口,因此就没有必要再检索网络接口链表,而是让那些协议改为直接调用 ip_output_if()函数。因为这个函数将网络接口作为参数,从而避免了对外发接口的检索。
转发信息包
- 如果没有网络接口的地址与到达的信息包的目标地址相同,信息包应该被转发。由ip_forward()函数完成,TTL 字段值被减少,当减为 0 的时候,将会给 IP 信息包的最初发送者发送 ICMP 错误信息,并抛弃该信息包( IP 包头被改变,因此需要调整 IP 包头校验和)。不必重新计算完整的校验和,可以使用简单的算法调整原始 IP 校验和。最后,信息包被转发到适当的网络接口。查找适当的网络接口的算法与发送信息包使用的算法相同。
ICMP 处理
-
ip_input()函数收到的 ICMP 信息包被移交给 icmp_input()函数对ICMP 包头解码,然后进行适当的动作。某些 ICMP 消息被传递给上层协议,由传输层的特定函数处理。 ICMP 目标不可到达的消息由传输层发送,特别是通过 UDP,使用icmp_dest_unreach()函数完成。
-
用 ICMP ECHO 消息来探测网络被广泛的使用,因而 ICMP 回显处理性能最优。实际处理被放置在icmp_input()函数,由交换到达包的源与目的 IP 地址,改变 ICMP 类型为回显应答以及调整 ICMP 校验和组成。然后,信息包被回传给 IP 层传送。
5、UDP 处理
UDP 是被用来在不同的进程间分解信息包的一个简单协议。 每个UDP会话的状态保存在一个结构体中,如何下代码块所示,UDP PCBs保存在一个链表中,当一个UDP数据包到达时对该链表进行匹配检索。
struct udp_pcb{
struct udp_pcb *next;
struct ip_addr local_ip,dest_ip;
u16_t local_port,dest_port;
u8_t flags;
u16_t chksum_len;
void(* recv)(void *arg,struct udp_pcb *pcb,strcut pbuf *p);
void *recv_arg;
};
UDP PCB 结构体包含一个指向 UDP PCBs 全局链表中下一个 PCB 的指针。一个 UDP 会话由终端 IP 地址和端口号来定义,这些信息保存在 local_ip,dest_ip,local_port 以及dest_port 字段中。flags 字段标识什么样的 UDP 校验和策略应该用于这个会话。或者可以完全关闭 UDP 校验和,或者使用 UDP 简化版(UDP Lite)[LDP99]校验和只覆盖数据包的一部分。如果使用 UDP Lite,chksum_len 字段指出应该进行校验和计算的数据段的长度。 recv 与 recv_arg 是在由 PCB 指定的会话收到一个数据包时使用。 在收到UDP 数据包时调用 recv 指向的函数。由于 UDP 的简单性,输入输出处理比较简单,并且遵循图 8 所示的处理流程。发送数据由程序调用udp_send()函数,然后再由该函数请求调用 udp_output()函数来完成。在此处进行必须的校验和计算以及填充 UDP 包头。因为校验和包括 IP 信息包的源地址,因此在某些情况下会调用 ip_route()函数以查找信息包将被传输到哪一个网络接口。网络接口的 IP 地址将作为信息包的源地址被使用。最后,信息包被移交给 ip_output_if()函数传送。
当一个 UDP 数据包到达,IP 层调用 udp_input()函数。如果校验和在这个会话中应该被使用,则 UDP 校验和被检查并分解数据包。当找到了相应的 UDP PCB,recv 函数被调用。
6、TCP 处理
TCP 属于传输层协议,它为应用层提供了可靠的字节流服务。对它的描述要比对其它协议的描述复杂的多,单从代码量来说,它就占了 LWIP代码总量的 50%。
概览
- 基本的 TCP 处理过程被分割为六个功能函数来实现(如图 9 所示):tcp_input()、tcp_process()及 tcp_receive()函数与 TCP 输入有关, tcp_write()、tcp_enqueue()及tcp_output()则用于 TCP 输出。
- 应用层调用 ip_write()函数, 接着 tcp_write()函数再将控制权交给 tcp_enqueue()函数,这个函数会在必要时将数据分割为适当大小的 TCP 段,然后再把这些 TCP 段放到所属连接的传输队列中。 tcp_output()函数会检查现在是不是能够发送数据,判断接收器窗口是否拥有足够大的空间,阻塞窗口是否也足够大,如果条件满足,就使用 ip_route()及ip_output_if()函数发送数据。
- 接收过程。过程的发起者是网络接口层,网络接口层将数据包传递给 ip_input()函数,该函数验证 IP 头后移交 TCP 段给tcp_input()函数。 tcp_input()函数完成两项工作:
其一,初始完整性检查(也就是校验和验证与 TCP 选项解析);
其二,判定这个 TCP 段属于哪个 TCP 连接。
这个 TCP 段到达tcp_process()函数,实现 TCP 状态机,任何必要的状态转换在这里实现。当该TCP 所属的连接正处于接受网络数据的状态, tcp_receive()函数将被调用。最终, tcp_receive()函数将数据传给上层的应用程序,完成接收过程。如果这个 TCP 段由一个不被承认的 ACK应答数据构成,数据将会从缓冲区移走,它所占用的存储区被收回。同样,如果收到一个ACK 应答确认数据,接收器同意接收更多的数据, tcp_output()函数将会被调用。
由于 LwIP 被设计运行于内存受限的最小限度系统,用于TCP 实现的数据结构应该尽量较小。为了能够让这些数据结构占用较少的内存单元,我们只能选择代码复杂性。 TCP 连接在 LISTEN 和 TIME-WAIT状态相对于其它状态的连接要保留的信息比较少,所以设计了一个较小的数据结构用于这种状态下的连接。这个小数据结构包含在一个完整的 PCB 结构里。
数据结构
struct tcp_pcb{
struct tcp_pcb *next;
enum tcp_state state;
void (* accept)(void *arg,struct tcp_pcb *newpcb);
void *accept_arg;
strcut ip_addr local_ip;
u16_t local_port;
struct ip_addr dest_ip;
u16_t dest_port;
u32 rcv_nxt,rcv_wnd;
u16_t tmr;
u32_t mss;
u8_t flags;
u16_t rttest;
u32_t rtseq;
u32_t sa,sv;
u32_t rto;
u32_t lastack;
u8_t dupacks;
u32_t cwnd,u32_t ssthresh;
u32_t snd_ack,snd_nxt,snd_wnd,snd_wl1,snd_wl2,snd_lbb;
void (* recv)(void *arg,struct tcp_pcb *pcb,struct pubf *p);
void *recv_arg;
struct tcp_seg *unsent,*unacked, **ooseq;
};
- state 变量包含网络连接的当前 TCP 状态、 IP 地址和端口号存储着网络连接信息。
- mss 变量则包含网络连接所允许的段的最大容量。
- rcv_nxt 字段包含从远程终端期望得到的下一个包序号,因而该字段被用于向远程主机发送 ACK 包。
- 接收器窗口由 rcv_wnd 字段保存并且字段值是在将要发送的 TCP 段中获取的。
- tmr 字段被作为一个定时器使用,当指定的计时结束后,连接应该被取消。
- flags 字段包含了连接的附加状态信息
- rttest,rtseq,sa,sv字段被用于RTT( round-trip time:往返时间,指数据包在 TCP 链路上发
送和收到确认的延时时间)评估。rtseq 保存段序号 ,rttest 保存段的发送时间,sa 及 sv分别保存平均往返时间及时间差。 - lastack 与 dupacks 字段用于快速重发及快速恢复的实现。 lastack 字段包含收到的最后一个 ACK 包的序列号, dupacks 字段则对后续收到的与该序列号相等的 ACK 重复包计数。连接的当前阻塞窗口保存在 cwnd 字段,慢速启动阀值保存在 ssthresh 字段。
- snd_ack、snd_nxt、snd_wnd、snd_wl1、snd_wl2、snd_lbb这六个字段用于发送数据。接收器应答的最高顺序编号保存在 snd_ack 字段,下一个要发送的序号保存在snd_nxt字段。接收器公开窗口( advertised window)保存在snd_wnd字段。snd_wl1与 snd_wl2两个字段用于更新snd_wnd字段。 snd_lbb 字段保存传输队列最后一个字节的顺序编号 。
- 函数指针 recv 及 recv_arg 用于向应用层传递收到的数据。
从应用层接收但还未发送的数据被放在unsent队列排队等待发送
已发送但未收到远程主机应答确认的数据保存在unacked队列
接收到序列以外的数据由ooseq缓冲。
struct tcp_seg{
struct tcp_seg *next;
u16_t len;
struct pubf *p;
struct tcp_hdr *tcphdr;
void *data;
u16_t rtime;
};
tcp_seg 结构成员列表是 TCP 报文段的内部表示方法
- 结构的第一个成员是指向其自身的 next 指针,该指针用于将接收到的多个报文段链结在一起以形成一个等待队列。
- len 字段包含 TCP 报文段的长度。对于具备 SYN 或者 FIN 标志的空段来说, len 值为 1。
- pbuf 类型的指针 p 指向TCP 报文段的存储缓冲区。
- tcphdr 及 data 指针分别指向 TCP 头和段中的数据。
- 对于外发报文段, rtime 字段用于报文段的重发超时。而接收报文段是不需要重发的,因此, rtime 字段是不需要的,所以接收报文段该字段不分配内存。
队列与数据传输
tcp_enqueue()函数对要发送的数据按照适当大小进行分割,并对分割后的数据块进行顺序编号。数据被封装进pbufs并附加到tcp_seg结构。在pbuf内,TCP头被创建,并且除应答数量,ackno、广播窗口,wnd以外的其他所有字段都将被填充。tcp_output()函数可以在保温段排队时重新设置这些字段的值,该函数完成实际的报文传输。报文段被创建后放入PCB内的unsent队列的末尾存在一个低于最大容量是段时,函数会使用pbuf链表的功能将新数据附加到该段。在tcp_enqueue()函数格式完成及排队报文段之后,tcp_output()函数被调用。它将检查当前窗口是否还有空间存放更多数据。通过获取阻塞窗口及发布的接收器窗口的最大数量来计算。接着,会填充未被tcp_enqueue()函数填充的TCP报头字段,然后用ip_route()与ip_output_if()函数发送报文段,发送之后,报文段被放入unacked列表,并一直保留至收到相应的ACK应答包。
接收报文段
- 解析
当 TCP 报文段到达 tcp_input()函数,它们会在 TCP PCBs 之间被解析。
解析的关键是源及目的 IP 地址和 TCP 端口号。
解析报文段时,有两种 PCBs 类型必须被区分:相对于开放连接的 PCB 类型与相对于半开放连接的 PCB 类型。半开放连接指的是那些处于监听状态并且只有指定的本地 TCP 端口号及本地 IP 地址为任意值的连接;而开放连接则指拥有两个指定的 IP 地址及两个端口号的连接。大部分的 TCP 连接组成代表性的显示一个大量位置的批量传输,这样就会导致一个高的缓冲区命中率。其它的缓冲方案包括保存两个单一的缓冲入口,一个用作已经被发送的最后一个包的PCB,另一个用作已经收到的最后一个包的 PCB。 通过移动最近用过的 PCB 到链表的前端可以使用二者之中的任何一个方案。两种方案已经表明通要胜过单一入口的方案。 - 接收数据
对到达报文段的处理在 tcp_receive()函数里进行。报文段应答序号与处在连接unacked 队列里的报文段进行比较,如果应答序号比 unacked 队列里的报文段序号高,则这个报文段会被移出队列,并且为其分配的内存也被收回。如果到达段的序号要比 PCB 中的 rcv_nxt 变量高,则该段脱离序列。脱离队列的报文段会被放入 PCB 中的 ooseq 队列。如果到达段的序号等于 rcv_nxt,则通过调用 PCB 中的 recv指向的函数将报文段转交到上层,并且通过到达段的长度来增加 rcv_nxt 值。因为序列中报文段的接收可能意味着先前收到的脱离序列的报文段是被期望接收的下一个段,ooseq 队列被检查。如果它包含一个序号等于 rcv_nxt 值的报文段,则通过调用 recv 指向的函数将该段转交给应用程序,并且 rcv_nxt 值被更新。这个过程会一直持续至 ooseq 队列为空或者ooseq 队列中的下一个报文段脱离序列。
处于监听状态(被动开放)的连接,准备着接受远程主机新的连接请求。为了那些连接,必须建立新的 TCP PCB,并传递给打开初始监听 TCP 连接的应用程序。对于 LWIP,这个过程是通过让应用程序注册一个回调函数来实现的,这个回调函数在新的连接建立时调用。
当处于监听状态的连接收到一个 SYN 标志设置的 TCP 段时, 一个新的连接被建立并且一个携带 SYN 与 ACK 标志的段被发送以响应收到的 SYN 段。连接进入 SYN-RCVD 状态并且等待发送的 SYN 段的应答。当应答到达,连接进入 ESTABLISHED 状态,并且 accept 指向的函数被调用。
接受新的连接
LWIP通过保存最后一个应答 ACK 来实现快速重发与恢复。 这样, 当收到相同序号的 ACK,TCP PCB 结构中的 dupacks 计数会加一。当 dupacks 值为 3,unacked 队列中的第一个报文段被重发且快速恢复被初始化。快速恢复按照描述的过程实现。无论何时收到新数据的应答 ACK,dupacks 计数都将复位为 0。
快速重发
LWIP使用两个周期性定时器,周期分别为 200ms 和 500ms。这两个定时器又被用于实现更复杂的逻辑定时器,比如重发定时器,TIME-WAIT 定时器及延迟 ACK 定时器(delayed-ACK timer)。
定时器
- 细粒度定时器tcp_timer_fine()会遍历每一个 TCP PCB,检查是否存在应该被发送的被延迟的 ACKs,就像 tcp_pcb 结构里 flag 字段所指示的。如果延迟 ACK 标志被设置,一个空的TCP ACK 应答段被发送,并且标志被清除。
- 粗粒度定时器在 tcp_timer_coarse()函数里实现,同样扫描 PCB 列表。对每一个 PCB,将遍历未应答报文段列表(unacked 指针详细信息见图 10 及 11,译者注),并且 rtime 变量值被增加。如果 rtime 值变得比 PCB 中 rto 变量给出的当前重发超时值大,报文段被重发并且重发超时加倍。只有在阻塞窗口与通告的接收器窗口的值允许的情况下报文段才被重发。重发之后,阻塞窗口被设置为最大段尺寸大小,慢启动阀值被设置为有效窗口大小的一半,并且慢启动在连接中被初始化。
- 对于 TIME-WAIT 状态的连接,粗粒度定时器也会增加 PCB 结构中的 tmr 字段值。当定时器到达 2× MSL 阀值,连接被取消。粗粒度定时器还会增加一个全局的 TCP 时钟值, tcp_ticks。这个时钟用于 RTT(往返时间: round-trip time)估算及重发超时( retransmission time-outs)。
往返时间估算
RTT 估算是 TCP 的主要部分,因为估算出的往返时间用于确定适当的重发超时。 每一次往返时间就被测量一次并且使用描述的smoothing 函数计算适当的重发超时。
TCP PCB 中的 rttseq 变量保存着被测量过往返时间的报文段的序号。PCB 中的 rttest变量保存报文段被第一次重发时的 tcp_ticks 值。当收到的 ACK 包的序号等于或者大于rtseq, 往返时间通过从 tcp_ticks 减去 rttest 来测量。 如果重发发生在往返时间测量期间,测量值不被采纳。
阻塞控制
当收到一个新数据的ACK应答,阻塞窗口cwnd值会加上最大段大小或mss2/cwnd的大小,这取决于连接是慢速启动还是阻塞控制。当发送数据时,接收器通告的窗口与阻塞窗口的最小值被用于确定每个窗口能够发送多少数据。
7、协议栈接口
使用 TCP/IP 协议栈提供的服务有两种方法:
- 直接调用 TCP 与 UDP 模块的函数;
- 使用下一节将要介绍的 LwIP API 函数。
TCP 与 UDP 模块提供网络服务的一个基本接口,该接口基于函数回调技术。
接收数据,应用程序会向协议栈注册一个回调函数。该回调函数与特定的连接相关联,当该关联的连接到达一个信息包,该回调函数就会被协议栈调用。此外,与 TCP 和 UDP 模块直接接口的应用程序必须驻留在像 TCP/IP 协议栈这样的进程中(回调函数不能跨进程调用)。
优点:应用程序和 TCP/IP 协议栈驻留在同一个进程中,发送和接收数据不再产生进程切换。
缺点:应用程序不能使自己陷入长期的连续运算中,这样会导致通讯性能下降,TCP/IP 处理与连续运算是不能并行发生。可以通过把应用程序分为两部分来克服,一部分处理通讯,一部分处理运算。通讯部分驻留在 TCP/IP 进程,进行大量运算的部分放在一个单独的进程。将在下一节介绍的 LwIP API 提供了以这样一种方式分割应用程序的构造方法。
8、应用程序接口
由 BSD 提供的高级别的 Socket API不适合用于受限系统的 TCP/IP 实现,特别是 BSD Socket 需要将要发送的数据从应用程序复制(复制数据的原因:应用程序与TCP/IP 协议栈通常驻留在不同的受保护空间)到 TCP/IP 协议栈的内部缓冲区。应用程序是一个用户进程,而TCP/IP 协议栈则驻留在操作系统内核。通过避免额外的复制操作,API 的性能可以大幅度提升。复制数据,系统还需要为此分配额外的内存。
LWIP API 可以充分利用 LwIP 的内部结构以实现其设计目标。LwIP API 操作相对低级。API 不需要在应用程序和协议栈之间复制数据,因为应用程序可以巧妙的直接处理内部缓冲区。
API 参考手册
数据类型
- netbuf,描述网络缓存的数据类型
- netconn,描述网络连接的数据类型
缓冲区函数
struct netbuf_new()
分配一个 netbuf结构,该函数并不会分配实际的缓冲区空间,只创建顶层的结构。netbuf用完后,必须使用 netbuf_delete()回收。
void netbuf_delete(struct netbuf)
回收先前通过调用 netbuf_new()函数创建的 netbuf 结构, 任何通过 netbuf_alloc()函数分
配给 netbuf的缓冲区内存同样也会被回收。
void _ netbuf_alloc(struct netbuf _buf, int size )
为 netbuf buf 分配指定字节(bytes)大小的缓冲区内存。这个函数返回一个指针指向已分配的内存,任何先前已分配给netbuf buf的内存会被回收。刚分配的内存可以在以后使用netbuf_free()函数回收。因为协议头应该要先于数据被发送,所以这个函数即为协议头也为实际的数据分配内存。
int netbuf_free(struct netbuf *buf)
回收与 netbuf buf 相关联的缓冲区。如果还没有为 netbuf 分配缓冲区,这个函数不做任何事情。
int netbuf_ref(struct netbuf _buf, void _data, int size)
使数据指针指向的外部存储区与 netbuf buf 关联起来。外部存储区大小由 size参数给出。任何先前已分配给 netbuf 的存储区会被回收。使用 netbuf_alloc()函数为 netbuf分配存储区与先分配存储区——比如使用 malloc()函数——然后再使用netbuf_ref()函数引用这块存储区相比,不同的是前者还要为协议头分配空间这样会使处理和发送缓冲区速度更快。
int netbuf_len(struct netbuf *buf)
返回 netbuf buf 中的数据长度,即使 netbuf 被分割为数据片断。对数据片断状的 netbuf来说,通过调用这个函数取得的长度值并不等于 netbuf中的第一个数据片断的长度。
int netbuf_data(struct netbuf *buf, void * data, int *len)
这个函数用于获取一个指向 netbuf buf 中的数据的指针,同时还取得数据块的长度。参数data 和len 为结果参数,参数 data 用于接收指向数据的指针值,len 指针接收数据块长度。如果 netbuf中的数据被分割为片断,则函数给出的指针指向 netbuf 中的第一个数据片断。应用程序必须使用片断处理函数netbuf_first()和 netbuf_next()来取得 netbuf 中的完整数据。
int netbuf_next(struct netbuf *buf)
函数修改netbuf中数据片断的指针以便指向netbuf中的下一个数据片断。返回值为0表明netbuf中还有数据片断存在,大于0表明指针现在正指向最后一个数据片断,小于0表明已经到了最后一个数据片断的后面的位置,netbuf中已经没有数据片断了。
int netbuf_first(struct netbuf *buf)
复位netbuf buf中的数据片断指针,使其指向netbuf中的第一个数据片断。
void netbuf_copy(struct netbuf _buf, void _data, int len)
将netbuf buf中的所有数据复制到data指针指向的存储区,即使netbuf buf中的数据被分割
为片断。len参数指定要复制数据的最大值。
void netbuf_chain(struct netbuf _head, struct netbuf _tail)
将两个netbufs的首尾链接在一起,以使首部netbuf的最后一个数据片断成为尾部netbuf的第一个数据片断。函数被调用后,尾部netbuf会被回收,不能再使用。
struct ip_addr _netbuf_fromaddr(struct netbuf _buf)
返回接收到的netbuf
buf的主机IP地址。如果指定的netbuf还没有从网络收到,函数返回一个未定义值。netbuf_fromport()函数用于取得远程主机的端口号。
unsigned short netbuf_fromport(struct netbuf *buf)
返回接收到的netbuf buf的主机端口号。如果指定的netbuf还没有从网络收到,函数返回一个不确定值。netbuf_fromaddr()函数用于取得远程主机的IP地址。
网络连接函数
struct netconn *netconn_new(enum netconn_type type)
建立一个新的连接数据结构,根据是要建立TCP还是UDP连接来选择参数值是NETCONN_TCP还是NETCONN_UCP。调用这个函数并不会建立连接并且没有数据被发送到网络中。
void netconn_delete(struct netconn *conn)
删除连接数据结构conn,如果连接已经打开,调用这个函数将会关闭这个连接。
enum netconn_type netconn_type(struct netconn *conn)
返回指定的连接conn的连接类型。返回的类型值就是前面netconn_new()函数说明中提到的NETCONN_TCP或者NETCONN_UDP。
int netconn_peer(struct netconn _conn, struct ip_addr _addr, unsigned short *port)
这个函数用于获取连接的远程终端的IP地址和端口号。addr和port为结果参数,它们的值由函数设置。如果指定的连接conn并没有连接任何远程主机,则获得的结果值并不确定。
**int netconn_addr(struct netconn *conn, struct ip_addr **addr, unsigned short port)
这个函数用于获取由conn指定的连接的本地IP地址和端口号。
int netconn_bind(struct netconn _conn, struct ip_addr _addr, unsigned short port)
为参数conn指定的连接绑定本地IP地址和TCP或UDP端口号。如果addr参数为NULL则本地IP地址由网络系统确定
int netconn_connect(struct netconn _conn, struct ip_addr _addr, unsigned short port)
对UDP连接,该函数通过addr和port参数设定发送的UDP消息要到达的远程主机的IP地址和端口号。对TCP,netconn_connect()函数打开与指定远程主机的连接
int netconn_listen(struct netconn *conn)
使参数conn指定的连接进入TCP监听(TCP LISTEN)状态。
struct netconn _netconn_accept(struct netconn _conn)
阻塞进程直至从远程主机发出的连接请求到达参数conn指定的连接。 这个连接必须处于监听(LISTEN)状态,因此在调用netconn_accept()函数之前必须调用netconn_listen()函数。与远程主机的连接建立后,函数返回新连接的结构
int main()
{
struct netconn *conn,*newconn;
//建立一个连接结构
conn=netconnn_new(NEWCONN_TCP);
//将连接绑定到一个本地任意IP地址的2000端口
netconn_bind(conn,NULL,2000);
//告诉这个连接监听进入的连接请求
netcoonn_linsten(conn);
//阻塞直至得到一个进入的连接
newconn=netconn_accept(conn);
//处理这个连接
process_connection(newconn);
//删除连接
netconn_delete(newconn);
netconn_delete(conn);
};
struct netbuf _netconn_recv(struct netconn _conn)
阻塞进程, 等待数据到达参数conn指定的连接。 如果连接已经被远程主机关闭, 则返回NULL,其它情况,函数返回一个包含着接收到的数据的netbuf。
int netconn_write(struct netconn _conn, void _data, int len, unsigned int flags)
这个函数只用于TCP连接。它把data指针指向的数据放在属于conn连接的输出队列。Len参数指定数据的长度,这里对数据长度没有任何限制。这个函数不需要应用程序明确的分配缓冲区(buffers),因为这由协议栈来负责。Flags参数有两种可能的状态,如下所示:
#define NETCONN_NOCOPY 0x00
#define NETCONN_COPY 0X01
这个例子显示了 netconn_write()函数的基本用法。这里假定程序里的 data 变量在
后面编辑修改,因此它被复制到内部缓冲区,方法是前文所讲的在调用 netconn_write()
函数时将 flags 参数值设未 NETCONN_COPY。text 变量包含了一个不能被编辑修改的字
符串,因此它采用指针引用的方式以代替复制。
int main()
{
struct netconn *conn;
char data[10];
char text[] = "Static text";
int i;
/* 设置连接conn */
/* [...] */
/* 建立一些任意的数据 */
for(i = 0; i < 10; i++)
data[i] = i;
netconn_write(conn, data, 10, NETCONN_COPY);
netconn_write(conn, text, sizeof(text), NETCONN_NOCOPY);
/* 这些数据可以被修改 */
for(i = 0; i < 10; i++)
data[i] = 10 - i;
/* 关闭连接 */
netconn_close(conn);
}
int netconn_send(struct netconn _conn, struct netbuf _buf)
使用参数conn指定的UDP连接发送参数buf中的数据。netbuf中的数据不能太大,因为没有使用IP分段。数据长度不能大于发送网络接口的最大传输单元值。因为目前还没有获取这个值的方法,这就需要采用其它的途径来避免超过MTU值,所以规定了一个上限,就是netbuf中包含的数据不能大于1000个字节。
函数对要发送的数据大小没有进行校验,无论是非常小还是非常大,因而函数的执行结果是不确定的。
int main()
{
struct netconn *conn;
struct netbuf *buf;
struct ip_addr addr;
char *data;
char text[] = "A static text";
int i;
/* 建立一个新的连接 */
conn = netconn_new(NETCONN_UDP);
/* 设置远程主机的IP地址,执行这个操作后, addr.addr的值为0x0100000a,译者注 */
addr.addr = htonl(0x0a000001);
/* 连接远程主机 */
netconn_connect(conn, &addr, 7000);
/* 建立一个新的netbuf */
buf = netbuf_new();
data = netbuf_alloc(buf, 10);
/* 建立一些任意数据 */
for(i = 0; i < 10; i++)
data[i]= i;
/* 发送任意的数据 */
netconn_send(conn, buf);
/* 引用这个文本给netbuf */
netbuf_ref(buf, text, sizeof(text));
/* 发送文本 */
netconn_send(conn, buf);
/* 删除conn和buf */
netconn_delete(conn);
netconn_delete(buf);
}
13. int netconn_close(struct netconn *conn)<br />关闭参数conn指定的连接。
最后
以上就是体贴曲奇为你收集整理的LWIP协议栈设计与实现笔记:LWIP协议栈设计与实现笔记的全部内容,希望文章能够帮你解决LWIP协议栈设计与实现笔记:LWIP协议栈设计与实现笔记所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复