本专栏使用的是LwIP 2.1.2版本 ,官方下载链接:http://savannah.nongnu.org/projects/lwip/。
本专栏以LwIP 2. 1.2 为主要对象进行讲解,后续中出现的LwIP 如果没有特殊声明,均指2.1.2 版本。此时的LwIP 2. 1.2 为最新版本,可能当这本书写完的时候,LwIP 又被更新了,对于学习而言,大家其实不必纠结于是否必须用最新的版本,因为2.1.2 版本和它后面的版本在移植和应用方法上并没有什么区别。
LwIP 全名:Light weight IP,意思是轻量化的TCP/IP 协议,是瑞典计算机科学院(SICS) 的AdamDunkels 开发的一个小型开源的TCP/IP 协议栈。LwIP 的设计初衷是:用少量的资源消耗实现一个较为完整的TCP/IP 协议栈,其中“完整”主要指的是TCP 协议的完整性,实现的重点是在保持TCP 协议主要功能的基础上减少对RAM 的占用。此外LwIP 既可以移植到操作系统上运行,也可以在无操作系统的情况下独立运行。
LwIP 具有主要特性:
LwIP 在嵌入式中使用有以下优点:
LwIP 尽管有如此多的优点,但它毕竟是为嵌入式而生,所以并没有很完整地实现TCP/IP 协议栈。相比于Linux 和Windows 系统自带的TCP/IP 协议栈,LwIP 的功能不算完整和强大。但对于大多数物联网领域的网络应用程序,LwIP 已经足够了。
LwIP 的代码已经交给Savannah 托管,LwIP 的项目主页是:http://savannah.nongnu.org/projects/lwip/。这个主页简单地介绍了一下LwIP,然后给出了许多链接,你可以通过这些链接去挖掘更多关于LwIP 的信息。在这里,我们只关注两个地方,如图中的方框所示。
点击“Project Homepage”,会得到一个网页,如图2_2 所示。这个网页可以看成是LwIP 的官方说明文档。我们可以通过这个网页获得关于LwIP 的很多信息,包括LwIP 的使用注意、数据的拷贝、系统初始化流程、多线程中要注意的问题、优化方法、内核模块的分类介绍、内核数据结构、内核重要全局变量、内核源码文件等。这些内容专业性比较强,不建议初学时在它上面花费精力,并且里面的很多内容在我们专栏的后续章节中中会有所讲解。在这里,我们只要知道有这么个东西就行了。
点击“Download Area”,会得到一个网页,如图2_3 所示。通过这个网页,我们可以下载到LwIP所有版本的源代码包和contrib 包。你每点击一个红色字体的资源链接,浏览器就会开启一个ftp连接,帮助你下载想要的文件到电脑中。但是这个页面提供的下载链接,在国内一般是没有响应的。这个网页最下方的黑字内容推荐我们使用另外一个下载页面:http://download-mirror.savannah.gnu.org/releases/。在这个页面下,用户可以下载到所有在Savannah 托管的开源软件,但我们只关心LwIP。利用浏览器的搜索功能,快捷键Ctrl+F,可以快速找到lwip 目录。在这里为了方便,我们直接给出最终的下载链接:http://download-mirror.savannah.gnu.org/releases/lwip/。
可能有人会问,什么是contrib 包,它与源代码包有什么不同?源代码包里面装的主要是LwIP内核的源码文件,而contrib 包里面装的是移植和应用LwIP 的一些demo,即应用示例。contrib包不属于LwIP 内核的一部分,里面的很多内容来自开源社区的贡献,因此contrib 包的版本管理不像内核源码那样严格和规范,但也是很有参考价值的。按理说,LwIP 源码面世越久,开源社区对它的贡献就越大,所以越高版本的contrib 包,提供的应用示例就越丰富,越有参考价值。在大版本区别不大的情况下,建议大家下载最新的contrib 包。后续我们会对contrib 包里面提供的应用示例进行讲解。另外,还有些“.sig”后缀的文件,这是数字签名,大家忽略就好。
按照上一小节的介绍,我们下载两个包:lwip-2.1.2.zip(源码包)和contrib-2.1.0.zip(contrib 包)。解压以后会得到两个文件夹,如图所示。
我们先打开“lwip-2.1.2”文件夹,如图所示。
该目录的内容为:
api 文件夹里面装的是NETCONN API 和Socket API 相关的源文件,只有在操作系统的环境中,才能被编译。
apps 文件夹里面装的是应用程序的源文件,包括常见的应用程序,如httpd、mqtt、tftp、sntp、snmp等。
core 文件夹里面是LwIP 的内核源文件,后续会详细讲解。
include 文件夹里面是LwIP 所有模块对应的头文件。
netif 文件夹里面是与网卡移植有关的文件,这些文件为我们移植网卡提供了模板,我们可以直接使用。
LwIP 内核是由一系列模块组合而成的,这些模块包括:TCP/IP 协议栈的各种协议、内存管理模块、数据包管理模块、网卡管理模块、网卡接口模块、基础功能类模块、API 模块。每个模块是由相关的几个源文件和头文件组成的,通过头文件对外声明一些函数、宏、数据类型,使得其它模块可以方便地调用此模块的功能。而构成每个模块的头文件都被组织在了include 目录中,而源文件则根据类型被分散地组织在api、apps、core、netif 目录中。
接下来,我们介绍一下core 文件夹,如图 所示。
我们逐一介绍一下这些源文件的功能。
ipv4 文件夹里面是与IPv4 模块相关的源文件,它们实现了IPv4 协议规定的对数据包的各种操作。ipv4 文件夹中还包括一些并非属于IP 协议,但会受IP 协议影响的协议源文件,包括DHCP、ARP、ICMP、IGMP。
ipv6 文件夹里面是与IPv6 模块相关的源文件,它们实现了IPv6 协议规定的对数据包的各种操作。ipv6 文件夹中还包括一些并非属于IP 协议,但会受IP 协议影响的协议源文件,包括DHCP、ARP、ICMP、IGMP。
altcp.c、altcp_alloc.c、altcp_tcp.c 等文件是应用程序分层TCP 连接API,从TCPIP 线程使用,是一个抽象层,可以模拟应用程序的tcp 回调API,同时防止直接链接,这样,应用程序可以使用其他应用程序层协议在TCP 之上而不知道细节(例如TLS,代理连接),此类接口我们并没有怎么使用,或者如果选择使用安全的加密传输的话,可以配合mbed TLS 使用。
def.c 文件定义了一些基础类函数,比如主机序和网络序的转换、字符串的查找和比较、整数转换成字符串等,这些函数会被LwIP 内核的很多模块所调用。在include 目录里面的def.h 文件对外声明了def.c 所实现的函数,同时定义了许多宏,能实现一些基础操作,比如取最大值、取最小值、计算数组长度等,这些宏同样也被内核的许多模块所调用。我们经常可以看到某个内核的源文件在开始的地方#include “def.h”。
dns.c 文件实现了域名解析的功能,有了它,用户就可以在知道服务器域名的情况下,获得该服务器的IP 地址。很多时候我们只记得服务器域名而不记得服务器IP 地址,例如“www.baidu.com”就是一个域名,通过dns 功能,我们就可以得到与服务器域名对应的IP 地址,这给用户使用带来很大的方便。
inet_chksum.c 文件提供了LwIP 所需的校验和功能,在IP、UDP、TCP 协议的实现中,需要计算校验和。
init.c 文件对LwIP 的用户宏配置进行了检查,会将配置错误和不合理的地方,通过编译器的#error和#warning 功能表示出来。另外,init.c 定义了lwip_init 初始化函数,这个函数会依次对LwIP 的各个模块进行初始化。
ip.c 文件实现了IP 协议相关的函数,但只是封装了ipv4 和ipv6 文件夹中的函数。
mem.c 文件实现了动态内存池管理机制,使得LwIP 内核的各个模块可以灵活地申请和释放内存。
memp.c 文件实现了静态内存堆管理机制,使得LwIP 内核的各个模块可以快速地申请和释放内存。
netif.c 文件实现了网卡的操作,比如注册/删除网卡、使能/禁能网卡、设置网卡IP 地址等等。netif.c与include 目录中的netif.h 文件共同构成了LwIP 的netif 模块,它对网卡进行了抽象,使得LwIP内核可以方便地管理多个特性各异的物理网卡。
pbuf.c 文件实现了LwIP 对网络数据包的各种操作。网络数据包在LwIP 内核中以pbuf 结构体的形式存在,这提高了LwIP 内核对数据包处理效率,以及提高了数据包在各层之间递交的效率。pbuf 结构体也是我们使用RAW/Callback API 进行网络应用程序开发的关键,后续我们会详细讲解。
raw.c 文件实现了一个传输层协议的框架,我们可以在它的基础上修改和添加代码,实现自定义的传输层协议,与UDP/TCP 一样,它可以与IP 层直接进行交互。这类似RAW Socket。在实际的应用中,我们常用UDP 和TCP 作为传输层协议。但有时,底层网络开发人员会嫌UDP 的可靠性太差,或者TCP 虽然可靠性强,但是很耗费时间和内存,他们需要根据实际需求,平衡利弊,定义自己的传输层协议。LwIP 的raw 模块可以满足他们的需求。
stat.c 文件实现了LwIP 内核的统计功能,使用户可以实时地查看LwIP 内核对网络数据包的处理情况。
sys.c 文件和sys.h 文件构成了LwIP 的sys 模块,它提供了与临界区相关的操作。
tcp.c、tcp_in.c 和tcp_out.c 文件实现了TCP 协议,包括对TCP 连接的操作、对TCP 数据包的输入输出操作和TCP 定时器,它们和include 目录中名称带tcp 的头文件共同构成了LwIP 的TCP 模块。TCP 模块的实现是LwIP 的最大特点,它以很小的资源开销几乎实现了TCP 协议中规定的全部内容。TCP 协议是非常复杂的协议,这几个与TCP 模块相关的文件占据了LwIP 内核的绝大部分。
timeouts.c 定义了LwIP 内核的超时处理机制。LwIP 内核中多个模块的实现需要借助超时处理机制,包括ARP 表项的时间统计、IP 分片报文的重装、TCP 的各种定时器、实现各种应用层协议需要的超时处理。
udp.c 文件实现了UDP 协议,包括对UDP 连接的操作和UDP 数据包的操作。
关于LwIP 的官方说明文档:http://www.nongnu.org/lwip/2_1_x/index.html,我就简单带大家浏览一下。打开连接,我们可以看到LwIP 的Overview(概述),这里就简单看看即可,我们可以点击左侧的“Common pitfalls”,查看一下LwIP 常见的陷阱,可能在使用中会遇到,到时候注意一下即可,在前面的章节中,我们也提到过,LwIP 可以工作在无操作系统环境也可以工作在有操作系统的环境中,Common pitfalls 中提到Mainloop Mode(主函数轮询模式)与OS Mode(操作系统模式)需要注意的一些事情,具体见图。
此外,我们还可以点击左侧的“Modules”,查看一些模块相关的说明,以及例子,比如有无操作系统相关的,如,还有基础配置,如LwIP 的内存管理模块,数据包缓冲区等会是在“Modules –>Infrastructure”页面中,具体见图。
当然,还有很重要的一些用户常用的API 函数,也是在“Modules”中可以找到,例如Raw API,NETCONN API 和Socket API 等,具体见图。
此外还有一些“Applications”应用层相关的说明,如HTTP、MQTT、TFTP 等,具体见图。
还有一些数据结构相关的说明,当我们在程序中看到哪个数据结构不懂的,都可以在这里找到对应的说明,也是比较重要的,LwIP 本质就是对数据的处理,其中也使用了大量的数据结构,有空可以多研究研究它,具体见图。
当然,我们也能通过函数名字的首字母来查找函数的作用,具体见图。
LwIP 的源码很庞大,我们使用微软的开源软件——vs code 查看源码,并且快速找到源码的函数与定义,首先我们先安装vs code,我们可以在https://code.visualstudio.com/download 中下载时候自己电脑的vs code 版本,然后安装即可。
然后打开我们的源码文件夹,右键,选择Open with Code,这样子就能直接在vs code 打开我们整个文件夹的源码了,具体见图。
在vs code 中,就显示了我们打开的源码,LwIP 那么多文件,我们怎么去快速找到源码文件中的某个函数呢?很简单,比如我们知道某个函数的名字的话,可以直接搜索的,这点就不必我多说,但是有时候,我们不记得某个函数的名字,只知道它在哪个文件,或者只知道在好几个文件中的某一个,那么我们就需要一个个去查找这个函数了,vs code 提供很强大的功能,就是可以快速查文件中的符号列表和函数列表,我们首先打开一个源码文件,比如tcp.c,然后我们通过快捷键“Shift+Ctrl+O”即可打开对应源码文件的符号列表和函数列表,通过查看这些列表,就能知道该源码文件中是否有我们需要的函数或者宏定义等,具体见图。
vs code 看源码是非常方便的,比如,我们可以通过F12 按键进行跳转到定义,通过“Alt+F12”速览定义,或者通过快捷键“Ctrl+F12”进行Go to Declaration,这些操作还是很方便的,当然啦,我们也能通过鼠标右键,进行选择,具体见图。如果在查看函数之后,想返回跳转前的位置,只需要通过快捷键“Alt+ 键盘的←(前后左右的左按键)”跳回即可。
(后面LwIP 的基础例程主要直接使用或参考源码里的example 即可)
我们打开之前下载好的contrib-2.1.0 文件夹,如图所示。
我们先讲解一下这个目录:
addons 目录。LwIP 中很多模块的实现,都是可以由用户干预的,比如校验和、TCP 初始序列号。LwIP 的内核代码,通过宏编译选项的设置,可以将内核中某些模块的实现方法配置成LwIP默认的方法,或者用户自定义的方法。用户自定义的方法通常需要用户在钩子函数中实现。在实际应用中,我们采用内核默认的方法就足够了,只有在非常特定的场合下,为了性能、资源开销等因素的考虑,我们可能会需要自己实现相关的模块,或者说编写相应的钩子函数。那么这时该怎么办呢?addons 目录下的内容就为我们提供了参考。对于初学者,没必要关心这个目录。
apps 目录里实现了很多应用层协议。LwIP 源码包中也有apps 目录,但源码包中apps 目录下的应用程序全部用RAW/Callback API 实现,属于内核代码的一部分。而此apps 目录里的应用程序可以是由三种API 中的任何一种实现的。读者可以把它看成是内核源码所提供的应用程序的一个补充。
examples 目录里是一些LwIP 的应用示例。在使用LwIP 开发应用程序时会出现的典型问题,比如如何移植网卡、如何使用LwIP 的API、如何使用源码中提供的应用程序,对于这些问题,这个目录为我们提供了参考。我们在后续的章节中,会使用这个目录中的例子来讲解LwIP 的应用程序。
ports 目录里是一些移植文件,它可以帮助我们将LwIP 移植到某个具体的操作系统中。目前这个目录所提供的移植文件,只支持FreeRTOS、UNIX、Win32。我们会在后续的章节中讲解如何移植LwIP。
LwIP 提供了三种编程接口,分别为RAW/Callback API、NETCONN API、SOCKET API。它们的易用性从左到右依次提高,而执行效率从左到右依次降低,用户可以根据实际情况,平衡利弊,选择合适的API 进行网络应用程序的开发。以下内容将分别介绍这三种API。
LwIP 提供了三种编程接口,分别为RAW/Callback API、NETCONN API、SOCKET API。它们的易用性从左到右依次提高,而执行效率从左到右依次降低,用户可以根据实际情况,平衡利弊,选择合适的API 进行网络应用程序的开发。以下内容将分别介绍这三种API。
RAW/Callback API 是指内核回调型的API,这在许多通信协议的C 语言实现中都有所应用。对于从来没有接触过回调式编程的人来说,可能理解起来会比较困难,我们在后面的章节中会详细介绍它。
RAW/Callback API 是LwIP 的一大特色,在没有操作系统支持的裸机环境中,只能使用这种API进行开发,同时这种API 也可以用在操作系统环境中。这里先简要说明一下“回调”的概念。你新建了一个TCP 或者UDP 的连接,你想等它接收到数据以后去处理它们,这时你需要把处理该数据的操作封装成一个函数,然后将这个函数的指针注册到LwIP 内核中。LwIP 内核会在需要的时候去检测该连接是否收到数据,如果收到了数据,内核会在第一时间调用注册的函数,这个过程被称为“回调”,这个注册函数被称为“回调函数”。这个回调函数中装着你想要的业务逻辑,在这个函数中,你可以自由地处理接收到的数据,也可以发送任何数据,也就是说,这个回调函数就是你的应用程序。到这里,我们可以发现,在回调编程中,LwIP 内核把数据交给应用程序的过程就只是一次简单的函数调用,这是非常节省时间和空间资源的。每一个回调函数实际上只是一个普通的C 函数,这个函数在TCP/IP 内核中被调用。每一个回调函数都作为一个参数传递给当前TCP 或UDP 连接。而且,为了能够保存程序的特定状态,可以向回调函数传递一个指定的状态,并且这个指定的状态是独立于TCP/IP 协议栈的。
在有操作系统的环境中,如果使用RAW/Callback API,用户的应用程序就以回调函数的形式成为了内核代码的一部分,用户应用程序和内核程序会处于同一个线程之中,这就省去了任务间通信和切换任务的开销了。
简单来说,RAW/Callback API 的优点有两个:
RAW/Callback API 的优点是显著的,但缺点也是显著的:
在操作系统环境中,可以使用NETCONN API 或者Socket API 进行网络应用程序的开发。NETCONNAPI 是基于操作系统的IPC 机制(即信号量和邮箱机制)实现的,它的设计将LwIP 内核代码和网络应用程序分离成了独立的线程。如此一来,LwIP 内核线程就只负责数据包的TCP/IP封装和拆封,而不用进行数据的应用层处理,大大提高了系统对网络数据包的处理效率。
前面提到,使用RAW/Callback API 会造成内核程序和网络应用程序、不同网络应用程序之间的相互制约,如果使用NETCONN API 或者Socket API,这种制约将不复存在。
在操作系统环境中,LwIP 内核会被实现为一个独立的线程,名为tcpip_thread,使用NETCONNAPI 或者Socket API 的应用程序处在不同的线程中,我们可以根据任务的重要性,分配不同的优先级给这些线程,从而保证重要任务的时效性,分配优先级的原则具体见表格。
NETCONN API 使用了操作系统的IPC 机制,对网络连接进行了抽象,用户可以像操作文件一样操作网络连接(打开/关闭、读/写数据)。但是NETCONN API 并不如操作文件的API 那样简单易用。举个例子,调用f_read 函数读文件时,读到的数据会被放在一个用户指定的数组中,用户操作起来很方便,而NETCONN API 的读数据API,就没有那么人性化了。用户获得的不是一个数组,而是一个特殊的数据结构netbuf,用户如果想使用好它,就需要对内核的pbuf 和netbuf 结构体有所了解,我们会在后续的章节中对它们进行讲解。NETCONN API 之所以采取这种不人性的设计,是为了避免数据包在内核程序和应用程序之间发生拷贝,从而降低程序运行效率。当然,用户如果不在意数据递交时的效率问题,也可以把netbuf 中的数据取出来拷贝到一个数组中,然后去处理这个数组。
简单来说,NETCONN API 的优缺点是:
相较于RAW/Callback API,NETCONN API 简化了编程工作,使用户可以按照操作文件的方式来操作网络连接。但是,内核程序和网络应用程序之间的数据包传递,需要依靠操作系统的信号量和邮箱机制完成,这需要耗费更多的时间和内存,另外还要加上任务切换的时间开销,效率较低。
相较于Socket API,NETCONN API 避免了内核程序和网络应用程序之间的数据拷贝,提高了数据递交的效率。但是,NETCONN API 的易用性不如Socket API 好,它需要用户对LwIP 内核所使用数据结构有一定的了解。
Socket,即套接字,它对网络连接进行了高级的抽象,使得用户可以像操作文件一样操作网络连接。它十分易用,许多网络开发人员最早接触的就是Socket 编程,Socket 已经成为了网络编程的标准。在不同的系统中,运行着不同的TCP/IP 协议,但是只要它实现了Socket 的接口,那么用Socket 编写的网络应用程序就能在其中运行。可见用Socket 编写的网络应用程序具有很好的可移植性。
不同的系统有自己的一套Socket 接口。Windows 系统中支持的是WinSock,UNIX/Linux 系统中支持的是BSD Socket,它们虽然风格不一致,但大同小异。LwIP 中的Socket API 是BSD Socket。但是LwIP 并没有也没办法实现全部的BSD Socket,如果开发人员想要移植UNIX/Linux 系统中的网络应用程序到使用LwIP 的系统中,就要注意这一点。
相较于NETCONN API,Socket API 具有更好的易用性。使用Socket API 编写的程序可读性好,便于维护,也便于移植到其它的系统中。Socket API 在内核程序和应用程序之间存在数据的拷贝,这会降低数据递交的效率。另外,LwIP 的Socket API 是基于NETCONN API 实现的,所以效率上相较前者要打个折扣。
参考资料:LwIP 应用开发实战指南—基于野火STM32