LWIP学习笔记1——基础介绍

1.网络协议的分层模型

LWIP学习笔记1——基础介绍_第1张图片
物理层(PHY) 规定了传输信号所需要的物理电平、介质特征。
链路层( MAC) 规定了数据帧能被网卡接收的条件,最常见的方式是利用网卡的 MAC 地址,发送方会在欲发送的数据帧的首部加上接收方网卡的 MAC 地址信息,接收方只有监听到属于自己的MAC 地址信息后,才会去接收并处理该数据。
网络层: 每台网络设备都应该有自己的网络地址,网络层规定了主机的网络地址该如何定义, 以及如何在网络地址和 MAC 地址之间进行映射,即 ARP 协议;
传输层: 网络层实现了数据包在主机之间的传递, 而一台主机内部可能运行着多个网络程序,传输层可以区分数据包是属于哪一个应用程序的, 可以说传输层实现了数据包端到端的传递。另外, 数据包在传输过程中可能会出现丢包、乱序和重复的现象,网络层并没有提供应对这些错误的机制, 而传输层可以解决这些问题,如 TCP 协议;
**应用层:**应用层以下的工作完成了数据的传递工作,应用层则决定了你如何应用和处理这些数据。

1.1协议层报文间的封装与拆封

LWIP学习笔记1——基础介绍_第2张图片

2.LwIP 简介

LwIP 全名: Light weight IP,意思是轻量化的 TCP/IP 协议, 是瑞典计算机科学院(SICS)的 Adam Dunkels 开发的一个小型开源的 TCP/IP 协议栈。 LwIP 的设计初衷是:用少量的资源消耗实现一个较为完整的 TCP/IP 协议栈,其中“完整”主要指的是 TCP 协议的完整性, 实现的重点是在保持 TCP 协议主要功能的基础上减少对 RAM 的占用。此外 LwIP既可以移植到操作系统上运行,也可以在无操作系统的情况下独立运行。

LwIP 具有主要特性:

  1. 支持 ARP 协议(以太网地址解析协议)。
  2. 支持 ICMP 协议(控制报文协议),用于网络的调试与维护。
  3. 支持 IGMP 协议(互联网组管理协议),可以实现多播数据的接收。
  4. 支持 UDP 协议(用户数据报协议)。
  5. 支持 TCP 协议(传输控制协议),包括阻塞控制、 RTT 估算、快速恢复和快速转发。
  6. 支持 PPP 协议(点对点通信协议) ,支持 PPPoE。
  7. 支持 DNS(域名解析)。
  8. 支持 DHCP 协议,动态分配 IP 地址。
  9. 支持 IP 协议,包括 IPv4、 IPv6 协议,支持 IP 分片与重装功能,多网络接口下的
    数据包转发。
  10. 支持 SNMP 协议(简单网络管理协议)。
  11. 支持 AUTOIP,自动 IP 地址配置。
  12. 提供专门的内部回调接口(Raw API),用于提高应用程序性能。
  13. 提供可选择的 Berkeley 接口 API,即 Socket 套接字 (在多线程情况下使用) 。

LwIP 在嵌入式中使用有以下优点:
14. 资源开销低,即轻量化。 LwIP 内核有自己的内存管理策略和数据包管理策略, 使得内核处理数据包的效率很高。另外, LwIP 高度可剪裁,一切不需要的功能都可以通过宏编译选项去掉。 LwIP 的流畅运行需要 40KB 的代码 ROM 和几十 KB 的RAM, 这让它非常适合用在内存资源受限的嵌入式设备中。
15. 所支持的协议较为完整。 几乎支持 TCP/IP 中所有常见的协议,这在嵌入式设备中早已够用。
16. 实现了一些常见的应用程序: DHCP 客户端、 DNS 客户端、 HTTP 服务器、MQTT 客户端、 TFTP 服务器、 SNTP 客户端等等。
17. 同时提供了三种编程接口: RAW API、 NETCONN API(注: NETCONN API 即为 Sequential API, 为了统一,下文均采用 NETCONN API) 和 Socket API。 这三种 API 的执行效率、易用性、可移植性以及时空间的开销各不相同,用户可以根据实际需要,平衡利弊,选择合适的 API 进行网络应用程序的开发。
18. 高度可移植。其源代码全部用 C 实现,用户可以很方便地实现跨处理器、跨编译器的移植。另外,它对内核中会使用到操作系统功能的地方进行了抽象,使用了一套自定义的 API,用户可以通过自己实现这些 API,从而实现跨操作系统的移植工作。
19. 开源、免费,用户可以不用承担任何商业风险地使用它。

2.1 LwIP 源码文件说明

源码下载地址http://download-mirror.savannah.gnu.org/releases/lwip/
打开“lwip-2.1.2”文件夹,如图所示
LWIP学习笔记1——基础介绍_第3张图片
(1) CHANGELOG 文件记录了 LwIP 在版本升级过程中源代码发生的变化。
(2) COPYING 文件记录了 LwIP 这个开源软件的 license。
(3) FILES 文件用于介绍当前目录下的目录信息。
(4) README 文件对 LwIP 进行了一个简单的介绍。
(5) UPGRADING 文件记录了 LwIP 每个大版本的更新,会对用户使用和移植 LwIP造成的影响。 所谓大版本更新指的是: 1.3.x - 1.4.x – 2.0.x – 2.1.x。小版本更新,比如 2.0.1– 2.0.2 – 2.0.3,这个过程只是一些 bug 的修复和性能的改善,不会对用户的使用造成影响。用户只要将原有工程的目录中与 LwIP 相关的旧版本文件替换成新版本的文件,重新编译,就能直接使用。
(6) doc 文件夹里面是关于 LwIP 的一些文档,可以看成是应用和移植 LwIP 的指南。但是这些文档比较零散,不成体系,而且纯文本阅读起来很费劲,阅读意义不是很大。
(7) test 文件夹里面是测试 LwIP 内核性能的源码,将它们和 LwIP 源码加入到工程中一起编译,调用它们提供的函数,可以获得许多与 LwIP 内核性能有关的指标。这种内核性能测试功能,只有非常专业的人士才用的到。
(8) src 文件夹里面就是我们最关心的 LwIP 源码文件。
打开 src 文件夹,如图所示
LWIP学习笔记1——基础介绍_第4张图片
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 目录中。

打开之前下载好的 contrib-2.1.0 文件夹
LWIP学习笔记1——基础介绍_第5张图片
(1) addons 目录。 LwIP 中很多模块的实现,都是可以由用户干预的,比如校验和、TCP 初始序列号。 LwIP 的内核代码, 通过宏编译选项的设置,可以将内核中某些模块的实现方法配置成 LwIP 默认的方法,或者用户自定义的方法。用户自定义的方法通常需要用户在钩子函数中实现。在实际应用中,我们采用内核默认的方法就足够了,只有在非常特定的场合下,为了性能、资源开销等因素的考虑,我们可能会需要自己实现相关的模块,或者说编写相应的钩子函数。那么这时该怎么办呢? addons 目录下的内容就为我们提供了参考。 对于初学者,没必要关心这个目录。
(2) apps 目录里实现了很多应用层协议。 LwIP 源码包中也有 apps 目录,但源码包中apps 目录下的应用程序全部用 RAW/Callback API 实现,属于内核代码的一部分。 而此apps 目录里的应用程序可以是由三种 API 中的任何一种实现的。 读者可以把它看成是内核源码所提供的应用程序的一个补充。
(3) examples 目录里是一些 LwIP 的应用示例。 在使用 LwIP 开发应用程序时会出现的典型问题, 比如如何移植网卡、如何使用 LwIP 的 API、如何使用源码中提供的应用程序,对于这些问题,这个目录为我们提供了参考。
(4) ports 目录里是一些移植文件,它可以帮助我们将 LwIP 移植到某个具体的操作系统中。目前这个目录所提供的移植文件,只支持 FreeRTOS、 UNIX、 Win32。

2.2 LwIP 的三种编程接口

LwIP 提供了三种编程接口,分别为 RAW/Callback API、 NETCONN API、 SOCKET API。
RAW/Callback API 是指内核回调型的 API,在没有操作系统支持的裸奔环境中,只能使用这种 API 进行开发,同时这种 API 也可以用在操作系统环境中。
NETCONN API 是基于操作系统的 IPC 机制(即信号量和邮箱机制) 实现的, 它的设计将 LwIP 内核代码和网络应用程序分离成了独立的线程。如此一来, LwIP 内核线程就只负责数据包的 TCP/IP 封装和拆封,而不用进行数据的应用层处理,大大提高了系统对网络数据包的处理效率。
Socket,即套接字,它对网络连接进行了高级的抽象,使得用户可以像操作文件一样操作网络连接。

3 PHY层和MAC层

在物理层,由 IEEE 802.3 标准规定了以太网使用的传输介质、传输速度、数据编码方式和冲突检测机制,物理层一般是通过一个 PHY 芯片实现其功能的,我们使用的是野火STM32F429 挑战者开发板,板载的 PHY 芯片是 LAN8720A。物理层定义了以太网使用的传输介质、传输速度、数据编码方式和冲突检测机制,PHY 芯片是物理层功能实现的实体,生活中常用水晶头网线+水晶头插座+PHY 组合构成了物理层
MAC 子层是属于数据链路层的下半部分,它主要负责与物理层进行数据交接,如是否可以发送数据,发送的数据是否正确,对数据流进行控制等。它自动对来自上层的数据包加上一些控制信号,交给物理层。接收方得到正常数据时,自动去除 MAC 控制信号,把该数据包交给上层。STM32F42x 系列控制器内部集成了一个以太网外设,它实际是一个通过 DMA 控制器进行介质访问控制(MAC),它的功能就是实现 MAC 层的任务。
LWIP学习笔记1——基础介绍_第6张图片
MAC 数据包由前导字段、帧起始定界符、目标地址、源地址、数据包类型、数据域、填充域、校验和域组成。

  • 前导字段,也称报头,这是一段方波,用于使收发节点的时钟同步。内容为连续7 个字节的 0x55。字段和帧起始定界符在 MAC 收到数据包后会自动过滤掉。
  • 帧起始定界符(SFD): 用于区分前导段与数据段的,内容为 0xD5。
  • MAC 地址: MAC 地址由 48 位数字组成,它是网卡的物理地址,在以太网传输的最底层,就是根据 MAC 地址来收发数据的。部分 MAC 地址用于广播和多播,在同一个网络里不能有两个相同的 MAC 地址。 PC 的网卡在出厂时已经设置好了MAC 地址,但也可以通过一些软件来进行修改,在嵌入式的以太网控制器中可由程序进行配置。数据包中的 DA 是目标地址, SA 是源地址。
  • 数据包类型: 本区域可以用来描述本 MAC 数据包是属于 TCP/IP 协议层的 IP 包、ARP 包还是 SNMP 包,也可以用来描述本 MAC 数据包数据段的长度。 如果该值被设置大于 0x0600,不用于长度描述,而是用于类型描述功能,表示与以太网帧相关的 MAC 客户端协议的种类。
  • 数据段: 数据段是 MAC 包的核心内容,它包含的数据来自 MAC 的上层。其长度可以从 0~1500 字节间变化。
  • 填充域:由于协议要求整个 MAC 数据包的长度至少为 64 字节(接收到的数据包如果少于 64 字节会被认为发生冲突,数据包被自动丢弃),当数据段的字节少于 46 字节时,在填充域会自动填上无效数据,以使数据包符合长度要求。
  • 校验和域: MAC 数据包的尾部是校验和域,它保存了 CRC 校验序列,用于检错。

4 网络接口管理和pbuf数据结构

网络接口管理:
网卡有多种多样,怎么能让 LwIP 使用同样的软件能兼容不同的硬件呢?原来 LwIP 使用一个数据结构——netif 来描述一个网卡。LwIP 提供统一的接口,但是底层的实现需要用户自己去完成,比如网卡的初始化, 网卡的收发数据,当 LwIP 底层得到了网络的数据之后,才会传入内核中去处理;同理, LwIP 内核需要发送一个数据包的时候,也需要调用网卡的发送函数,这样子才能把数据从硬件接口到软件内核无缝连接起来。简单来说, netif 是 LwIP 抽象出来的网卡, LwIP 协议栈可以使用多个不同的接口,而ethernetif.c 文件则提供了 netif 访问各种不同的网卡,每个网卡有不同的实现方式, 用户只需要修改 ethernetif.c 文件即可。
netif数据结构:
LWIP学习笔记1——基础介绍_第7张图片
netif 使用:
首先我们需要根据我们的网卡定义一个 netif 结构体变量 struct netif gnetif, 然后实现这个结构体的各个底层函数,然后我们要把网卡挂载到 netif_list 链表上才能使用,因为 LwIP 是通过链表来管理所有的网卡,所有第一步是通过 netif_add()函数将我们的网卡挂载到 netif_list 链表上。
每个 netif 接口都需要一个底层接口文件提供访问硬件的支持, 而 LwIP 作者将这种支持做成一个框架供我们参考,如 ethernetif.c 文件就是实现为一个框架的形式,我们在移植的时候只需要根据实际的网卡特性完善这里面的函数即可。
与 netif 相关的底层函数:
使用不同的底层框架,所实现的接口也有所不同,但是总归要用户实现的最主要的接口只有三个:
1.网卡初始化函数,比如:static void low_level_init(struct netif *netif),它由netif_add()添加网卡的时候,会调用 netif_add的参数ethernetif_init()回调函数,最终调用的还是low_level_init(struct netif *netif)函数。
它主要完成网卡的复位及参数初始化,根据实际的网卡属性进行配置 netif 中与网卡相关的字段,例如网卡的 MAC 地址、长度,最大发送单元等。
2.网卡的数据接收函数,比如:static struct pbuf * low_level_input(struct netif *netif),无操作系统时它由ethernetif_input()函数调用,再通过ethernet_input函数将数据包递交给上层,在无操作系统的时候 ethernetif_input()就是一个由内核周期性处理的接收函数;而在多线程操作系统的时候,我们一般会将其改写成一个线程的形式,通过 tcpip_input()函数将网卡收到的数据包打包成为一个消息,发送到 tcpip_mbox 邮箱中唤醒tcpip_thread线程,tcpip_thread线程根据消息类型去调用ethernetif_input()函数处理接收到的数据包,并将数据包递交上层。
该函数会接收一个数据包,为了内核易于对数据包的管理,该函数必须将接收的数据封装成 pbuf 的形式。
3.网卡的发送函数,比如static err_t low_level_output(struct netif *netif, struct pbuf *p),当需要在网卡上发送一个数据包时,通常由IP 层的 ethernet_output()调用。
它主要将内核的数据包发送出去,数据包采用 pbuf 数据结构进行描述。
pbuf数据结构:
在标准的 TCP/IP 协议栈中,各层之间都是一个独立的模块,它有着很清晰的层次结构,每一层只负责完成该层的处理,不会越界到其他层次去读写数据。而 LwIP 只是一个轻量级 TCP/IP 协议栈,它只是一个较完整的 TCP/IP 协议,多应用在嵌入式领域中,由于处理器的性能有限, LwIP 并没有采用很明确的分层结构,它假设各层之间的部分数据和结构体和实现原理在其他层是可见的,简单来说就传输层知道 IP 层是如何封装数据、传递数据的,IP 层知道链路层是怎么封装数据的等等。
pbuf 就是一个描述协议栈中数据包的数据结构, LwIP 中在 pbuf.c 和 pubf.h 实现了协议栈数据包管理的所有函数与数据结构。因为网络中的数据包可能很大,而 pbuf 能管理的数据包大小有限,就会采用链表的形式将所有的 pbuf 包连接起来,这样子才能完整描述一个数据包,这些连接起来的 pbuf 包会组成一个链表,我称之为 pbuf 链表。
LWIP学习笔记1——基础介绍_第8张图片
PBUF_RAM 类型的 pbuf 示意图
LWIP学习笔记1——基础介绍_第9张图片

5 LWIP协议解析

应用层调用API发送接收数据流程:
LWIP学习笔记1——基础介绍_第10张图片

5.1网卡接收数据的流程

LWIP学习笔记1——基础介绍_第11张图片

5.2 内核超时处理

LwIP 通过一个 sys_timeo 类型的数据结构管理与超时链表相关的所有超时事件。 LwIP使用这个结构体记录下内核中所有被注册的超时事件, 这些结构体会以链表的形式一个个连接在超时链表中, LwIP定义了一个 sys_timeo 类型的指针 next_timeout,并且将 next_timeout 指向当前内核中链表头部,所有被注册的超时事件都会按照被处理的先后顺序排列在超时链表上。
tcpip_timeouts_mbox_fetch(sys_mbox_t *mbox, void **msg): 这个函数在操作系统的线程中循环调用,主要是等待 tcpip_mbox 消息,是可阻塞的,如果在等待 tcpip_mbox 的过程中发生超时事件,则会同时执行超时事件处理,即调用超时回调函数。 LwIP 是这样子处理的,如果已经发生超时, LwIP 就会内部调用 sys_check_timeouts()函数去检查超时的sys_timeo 结构体并调用其对应的回调函数, 如果没有发生超时,那就一直等待消息,其等待的时间为下一个超时时间的时间, 一举两得。 LwIP 中 tcpip 线程就是靠这种方法,即处理了上层及底层的 tcpip_mbox 消息,同时处理了所有需要超时处理的事件。

5.3 tcpip_thread 线程

LwIP 在操作系统的环境下, LwIP 内核是作为操作系统的一个线程运行的,在协议栈初始化的时候就会创建 tcpip_thread 线程。

tcpip_thread(void *arg)
{
  struct tcpip_msg *msg;
  LWIP_UNUSED_ARG(arg);

  LWIP_MARK_TCPIP_THREAD();

  LOCK_TCPIP_CORE();
  if (tcpip_init_done != NULL) {
    tcpip_init_done(tcpip_init_done_arg);
  }

  while (1) {                          /* MAIN Loop */
    LWIP_TCPIP_THREAD_ALIVE();
    /* wait for a message, timeouts are processed while waiting */
    TCPIP_MBOX_FETCH(&tcpip_mbox, (void **)&msg);
    if (msg == NULL) {
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: invalid message: NULL\n"));
      LWIP_ASSERT("tcpip_thread: invalid message", 0);
      continue;
    }
    tcpip_thread_handle_msg(msg);
  }
}

LwIP 将函数 tcpip_timeouts_mbox_fetch()定义为带参宏TCPIP_MBOX_FETCH,所以在这里就是等待消息并且处理超时事件。如果没有等到消息就继续等待。等待到消息就对消息进行处理。

5.4 LwIP 中的消息

LwIP 中消息是有多种结构的的, 对于不同的消息类型其封装是不一样的, tcpip_thread 线程是通过 tcpip_msg 描述消息的,tcpip_thread 线程接收到消息后,根据消息的类型进行不同的处理。
tcpip_msg_type 枚举类型定义了系统中可能出现的消息的类型:

enum tcpip_msg_type
 {
	 TCPIP_MSG_API,
	 TCPIP_MSG_API_CALL, //API 函数调用
	 TCPIP_MSG_INPKT, //底层数据包输入
	 TCPIP_MSG_TIMEOUT, //注册超时事件
	 TCPIP_MSG_UNTIMEOUT, //删除超时事件
	 TCPIP_MSG_CALLBACK,
	 TCPIP_MSG_CALLBACK_STATIC //执行回调函数
 };

LWIP学习笔记1——基础介绍_第12张图片
LWIP学习笔记1——基础介绍_第13张图片

你可能感兴趣的:(TCP/IP协议,嵌入式,TCP/IP协议)