lwIP 开发指南

目录

lwIP 初探

本章,先介绍计算机网络相关知识,然后对lwIP软件库进行概述,接着介绍MAC 内核的基本知识,最后探讨LAN8720A 和YT8512C 以太网PHY 层芯片。

TCP/IP 协议栈是什么

TCP/IP 协议栈是一系列网络协议的总和,是构成网络通信的核心骨架,它定义了电子设备如何连入因特网,以及数据如何在它们之间进行传输。

TCP/IP 协议采用4层结构,分别是应用层、传输层、网络层和网络接口层,每一层都呼叫它的下一层所提供的协议来完成自己的需求。由于我们大部分时间都工作在应用层,下层的事情不用我们操心;其次网络协议体系本身就很复杂庞大,入门门槛高,因此很难搞清楚TCP/IP 的工作原理。如果读者想深入了解TCP/IP 协议栈的工作原理,可阅读《计算机网络书籍》。

TCP/IP 协议栈架构

网络协议有很多,如MQTT、TCP、UDP、IP 等协议,这些协议组成了TCP/IP 协议栈,同时,这些协议具有层次性,它们分布在应用层,传输层和网络层。

TCP/IP 协议栈的分层结构和网络协议得对应关系如下图所示:

在这里插入图片描述

由于OSI 模型和协议比较复杂,所以并没有得到广泛的应用。而TCP/IP 模型因其开放性和易用性在实践中得到了广泛的应用,它也成为互联网的主流协议。

注意:网络技术的发展并不是遵循严格的OSI分层概念。实际上现在的互联网使用的是TCP/IP 体系结构有时已经演变成为图1.1.1.2所示那样,即某些应用程序可以直接使用IP 层,或甚至直接使用最下面的网络接口层。

在这里插入图片描述

无论哪种表示方法,TCP/IP 模型各个层次都分别对应于不同的协议。TCP/IP 协议栈负责确保网络设备之间能够通信。它是一组规则,规定了信息如何在网络中传输。

这些协议都分布在应用层,传输层和网络层,网络接口层是由硬件来实现。

如Windows 操作系统包含了CBISC 协议栈,该协议栈就是实现了TCP/IP 协议栈的应用层,传输层和网络层的功能;网络接口层由网卡实现,所以CBISC 协议栈和网卡构建了网络通信的核心骨架。因此,无论哪一款以太网产品,都必须符合TCP/IP 体系结构,才能实现网络通信。

注意:路由器和交换机等相关网络设备只实现网络层和网络接口层的功能。

TCP/IP 协议栈的封包和拆包

TCP/IP 协议栈的封包和拆包也是一个非常重要的知识,如以太网设备发送数据和接收数据的处理流程是怎么样的?这个问题涉及到TCP/IP 协议栈对数据处理的流程,该流程称之为“封包”和“拆包”。“封包”是对发送数据处理的流程,而“拆包”是对接收数据处理的流程,如下图所示。

在这里插入图片描述

上图中,发送端发送的数据自顶向下依次传递。各层协议依次在数据前添加本层的首部且设置本层首部的信息,最终将处理后的MAC帧递交给物理层转成光电模拟信号发送至网络,这个流程称之为封包流程。

在这里插入图片描述

上图中,当帧数据到达目的主机时,将沿着协议栈自底向上依次传递。各层协议依次根据帧中本层负责的头部信息以获取所需数据,最终将处理后的帧交给应用层,这个流程称之为拆包的过程。

lwIP 简介

lwIP 是Light Weight(轻型)IP 协议,有无操作系统的支持都可以运行。lwIP 实现的重点是在保持TCP/IP 协议主要功能的基础上减少对RAM 的占用,它只需十几 KB 的 RAM 和 40K左右的ROM 就可以运行,这使lwIP 协议栈适合在低端的嵌入式系统中使用。lwIP 的设计理念下,既可以无操作系统使用,也可以带操作系统使用;既可以支持多线程,也可以无线程。它可以运行在8 位以及32 位的微处理器上,同时支持大端、小端系统。

lwIP 特性参数

lwIP 的各项特性,如下表所示:

在这里插入图片描述

lwIP 与TCP/IP 体系结构的对应关系

在这里插入图片描述

从上图可以看出,lwIP 软件库只实现了TCP/IP 体系结构的应用层、传输层和网络层的功能,但网络接口层不能使用软件的方式实现,因为网络接口层是把数据包转成光电模拟信号,并转发至网络,所以网络接口层只能由硬件来实现

lwIP 源码下载

lwIP 的开发托管在Savannah 上,Savannah 是软件开发、维护和分发。每个人都可以通过使用Savannah 的界面、Git 和邮件列表下载lwIP 源码包。lwIP 的项目主页:http://savannah.nongnu.org/projects/lwip/。在这个主页上,读者需要关注“project homepage”和“download area”这两个链接地址。

打开lwIP 项目主页之后,往下找到“Quick Overview”选项,如下图所示:

在这里插入图片描述

点击上图中Project Homepage 链接地址,读者可以看到官方对于lwIP 的说明文档,包括lwIP 更新日记、常见误解、已发现的BUG、多线程、优化提示和相关文件中的函数描述等内容。

点击上图中的Domnload Area 链接地址,读者可以看到lwIP 源码和contrib 包的下载网页,如下图所示那样。由于lwIP 版本居多,因此本教程选择目前最新的lwIP 版本(2.1.3)。下图中的contrib 包是提供用户lwIP 移植文件和lwIP 相关demo 例程。注:contrib 包不属于lwIP内核的一部分,它只是为我们提供移植文件和学习实例。

在这里插入图片描述

点击上图中的lwip-2.1.3.zip 和contrib-2.1.0.zip 链接,下载完成之后在本地上可以看到这两个压缩包。

lwIP 文件说明

根据上一个小节的操作,我们已经下载了lwip-2.1.3.zip 和contrib-2.1.0.zip 这两个压缩包。

接下来,笔者带大家认识一下lwip-2.1.3 和contrib-2.1.0 文件夹内的文件。

➢ lwIP 源码包文件说明

打开lwip-2.1.3 文件夹,如下图所示:

在这里插入图片描述

上图中,这个文件夹包含的文件和文件夹非常多,这些文件与文件夹描述如下表所示。

在这里插入图片描述

上表中,src 文件夹是lwIP 源码包中最重要的,它是lwIP 的内核文件,也是我们移植到工程中的重要文件。接下来,笔者重点讲解src 文件夹下的文件与文件夹,如下表所示。

在这里插入图片描述

  • api 文件夹下的文件是实现应用层与传输层递交数据的接口实现;
  • apps 文件夹下的文件实现了多种应用层协议;
  • core 文件夹下的文件是构建lwIP 内核的源文件,对应了TCP/IP 体系架构的传输层、网络层;include 文件夹包含了lwIP 软件库的全部头文件;
  • netif 文件夹下的文件实现了网络层与数据链路层交互接口,以及管理不同类型的网卡。

打开core 文件夹,我们会发现,lwIP 是由一系列的模块组合而成,这些模块包括:
TCP/IP 协议栈的各种协议、内存管理、数据包管理、网卡管理、网卡接口、基础功能和API接口模块等,每一个模块是由几个源文件和一个头文件集合,这些头文件全部放在include 文件夹下,而源文件都是放在core 文件夹下。这些模块描述如下:

在这里插入图片描述
在这里插入图片描述

➢ lwIP 的contrib 包文件说明

contrib 包提供了lwIP 移植文件和lwIP 相关demo(应用实例),如下图所示:

在这里插入图片描述

上图中,ports 文件夹提供了lwIP 基于FreeRTOS 操作系统的移植文件;examples 和apps文件夹提供读者学习lwIP 的应用实例。至此,lwIP 源码库和contrib 包介绍完毕。

MAC 内核简介

STM32 内置了一个MAC 内核,它实现了TCP/IP 体系架构的数据链路层功能。STM32 内置以太网架构如下所示:

在这里插入图片描述

上图,绿色框框的RX FIFO 和TX FIFO 都是2KB 的物理存储器,它们分别存储网络层递交的以太网数据和接收的以太网数据。以太网DMA 是网络层和数据链路层的中间桥梁,是利用存储器到存储器方式传输;

红色框框的内容可分为两个部分讲解,RMII 与MII是 MAC内核(数据链路层)与 PHY 芯片(物理层)的数据交互通道,用来传输以太网数据。

MDC 和MDIO 是MAC 内核对PHY 芯片的管理和配置,是站管理接口(SMI)所需的通信引脚。站管理接口(SMI)允许应用程序通过2 条线:时钟(MDC)和数据线(MDIO)访问任意PHY 寄存器。该接口支持访问多达32 个PHY。应用程序可以从32 个PHY 中选择一个PHY,然后从任意PHY 包含的32 个寄存器中选择一个寄存器,发送控制数据或接收状态信息。

任意给定时间内只能对一个PHY 中的一个寄存器进行寻址。在MAC 对PHY 进行读写操作的时候,应用程序不能修改MII 的地址寄存器和MII 的数据寄存器。在此期间对MII 地址寄存器或MII 数据寄存器执行的写操作将会被忽略。例如关于SMI 接口的详细介绍大家可以参考STM32F4xx 中文参考手册的824 页。

➢ 介质独立接口:MII

MII 用于MAC 层与PHY 层进行数据传输。MCU 通过MII 与PHY 层芯片的连接图如下。

图1.3.2 MCU 与PHY 层芯片连接

从图中可以看出,MII 介质接口使用的引脚数量是非常多的,这也反映出引脚紧缺的MCU 不适合使用 MII 介质接口来实现以太网数据传输,MII 接口引脚的作用如下所示。

MII_TX_CLK:连续时钟信号。该信号提供进行TX 数据传输时的参考时序。标称频率为:速率为10 Mbit/s 时为2.5 MHz;速率为100 Mbit/s 时为25 MHz。
MII_RX_CLK:连续时钟信号。该信号提供进行RX 数据传输时的参考时序。标称频率为:速率为10 Mbit/s 时为2.5 MHz;速率为100 Mbit/s 时为25 MHz。
MII_TX_EN:发送使能信号。
MII_TXD[3:0]:数据发送信号。该信号是4 个一组的数据信号。
MII_CRS:载波侦听信号。
MII_COL:冲突检测信号。
MII_RXD[3:0]:数据接收信号。该信号是4 个一组的数据信号
MII_RX_DV:接收数据有效信号。
MII_RX_ER:接收错误信号。该信号必须保持一个或多个周期(MII_RX_CLK),从而向MAC 子层指示在帧的某处检测到错误。

➢ 精简介质独立接口:RMII

精简介质独立接口(RMII)规范降低10/100Mbit/s 下微控制器以太网外设与外部PHY 间的引脚数。根据IEEE 802.3u 标准,MII 包括16 个数据和控制信号的引脚,而RMII 规范将引脚数减少为7 个。

MCU 通过RMII 接口与PHY 层芯片的连接图如下图所示。因为RMII 相比MII,其发送和接收都少了两条线。因此要达到10Mbit/s 的速度,其时钟频率应为5MHZ,同理要达到100Mbit/s 的速度其时钟频率应为50MHz。正点原子开发板就是采用此接口连接PHY 芯片。

在这里插入图片描述

可以看出,REF_CLK 引脚需要提供50MHz 时钟频率,它分别提供MAC 内核和PHY 芯片,确保它们时钟同步。

PHY 芯片介绍

PHY 芯片在TCP/IP 体系架构中扮演着物理层的角色,它把数据转换成光电模拟信号传输至网络当中。本小节为读者介绍正点原子常用的PHY 芯片,它们分别为LAN8720A 和YT8512C,这两款PHY 芯片都是支持10/100BASE-T 百兆以太网传输速率,为此笔者分两个小节来讲解这两款以太网芯片的知识。

YT8512C 简介

YT8512C 是低功耗单端口10/100Mbps 以太网PHY 芯片。它通过两条标准双绞线电缆收发器发送和接收数据所需的所有物理层功能。另外,YT8512C 通过标准MII 和RMII 接口连接到MAC 层。YT8512C 功能结构图如下图所示:

在这里插入图片描述
上图是YT8512C 芯片的内部总架构示意图,从图中我们大概可以看出,它通过LED0\LED1 引脚的电平来设置PHY 地址,由XTAL,Clock 引脚提供PHY 内部时钟,同时TXP\TXN\RXP\RXN 引脚连接到RJ45(网口)。

➢ PHY 地址设置

MAC 层通过SMI 总线对PHY 芯片进行读写操作,SMI 可以控制32 个PHY 芯片,通过PHY 地址的不同来配置对应的PHY 芯片。YT8512C 芯片的PHY 地址设置如下表所示:

在这里插入图片描述

上表中,我们可通过YT8512C 芯片的LED0/PHYADD0 和LED1/PHYADD1 引脚电平来设置PHY 地址。由于正点原子板载的PHY 芯片是把这两个引脚拉低,所以它的PHY 地址为0x00。打开HAL 配置文件或者打开PHY 配置文件,我们在此文件下配置PHY 地址,这些文
件如下表所示:

在这里插入图片描述

可以看到,探索者和DMF407 开发板的PHY 地址在stm32f4xx_hal_conf.h 文件下设置的,而阿波罗和北极星开发板的PHY 地址在ethernet_chip.h 文件下设置的。因为探索者与DMF407 使用的HAL 库版本比阿波罗与北极星开发板所使用的HAL 库版本旧,所以它们的移植流程存在巨大的差异。这里笔者暂且不讲解这部分的内容。

➢ YT8521C 的RMII 接口介绍

YT8521C 的RMII 接口提供了两种RMII 模式,这两种模式分别为:
RMII1 模式:这个模式下YT8521C 的TXC 引脚不会输出50MHz 时钟。该模式的连接示意图如下图1.4.1.2 所示。
RMII2 模式:这个模式下YT8521C 的TXC 引脚会输出50MHz 时钟。该模式的连接示意图如下图1.4.1.3 所示。

在这里插入图片描述

对于RMII 接口而言,外部必须提供50MHz 的时钟驱动PHY 与MAC 内核,该时钟为了使PHY 芯片与MAC 内核保持时钟同步操作,它可以来自PHY 芯片、有源晶振或者STM32的MCO 引脚。如果我们的电路采用RMII1 模式的话,那么PHY 芯片由25MHz 晶振经过内部PLL 倍频达到50MHz,但是MAC 内核没有被提供50MHz 与PHY 芯片保持时钟同步,所以我们必须在此基础上使用MCO 或外部接入50MHz 晶振提供时钟给MAC 内核,以保持时钟同步。

在这里插入图片描述

如果电路使用上图模式连接的话,那么PHY 芯片经过外接晶振25MHz 和内部PLL 倍频操作,最终PHY 芯片内部的时钟为50MHz。接着PHY 芯片外围引脚TXC 会输出50MHz 时钟频率,该时钟频率可输入到MAC 内核保持时钟同步,这样我们无需外接晶振或者MCO 提供MAC 内核时钟。

注:RMII1 模式和RMII2 模式的选择是由YT8521C 的RX_DV(8)和RXD3(12)引脚决定,具体如何选择,请读者参考“YT8512C.PDF”手册的17 到18 页的内容。

➢ YT8521C 的寄存器介绍

PHY 是由IEEE 802.3 定义的,一般通过SMI 对PHY 进行管理和控制,也就是读写PHY内部寄存器。PHY 寄存器的地址空间为5 位,可以定义0 ~ 31 共32 个寄存器,但是,随着PHY 芯片功能的增加,很多PHY 芯片采用分页技术来扩展地址空间,定义更多的寄存器,在
这里笔者不讨论这种情况,IEEE 802.3 定义了0~15 这16 个寄存器的功能,而16~31 寄存器由芯片制造商自由定义的。

在YT8521C 中有很多寄存器,这里笔者只介绍几个用到的寄存器(包括寄存器地址,此
处使用十进制表示):BCR(0),BSR(1),PHY 特殊功能寄存器(17)这三个寄存器。首先我们来看一下BCR(0)寄存器,BCR 寄存器各位介绍如下表所示。

在这里插入图片描述
在这里插入图片描述

我们设置以太网速率和双工,其实就是配置PHY 芯片的BCR 寄存器。在HAL 配置文件
或者ethernet_chip.h 文件定义了BCR 和BSR 寄存器,代码如下:
探索者、DMF407 开发板(HAL 配置文件下):

#define PHY_BCR ((uint16_t)0x0000)
#define PHY_BSR ((uint16_t)0x0001)

阿波罗、北极星开发板(PHY 配置文件下):

#define ETH_CHIP_BCR ((uint16_t)0x0000U)
#define ETH_CHIP_BSR ((uint16_t)0x0001U)

由于探索者及DMF407 开发板的例程是使用V1.26 版本的HAL 库,所以这两个寄存器并不需要读者来操作,原因就是我们调用HAL_ETH_Init 函数以后系统就会根据我们输入的参数配置YT8521C 的相应寄存器。但是,阿波罗及北极星开发板的例程使用目前最新的HAL 版本,它要求读者手动操作BCR 寄存器,例如自动协商、软复位等操作。
BSR 寄存器各个位介绍如下表所示:

在这里插入图片描述
在这里插入图片描述

BSR 寄存器为YT8521C 的状态寄存器,通过读取该寄存器的值我们可以得到当前的连接
速度、双工状态和连接状态等信息。

接下来,笔者介绍的是YT8521C 特殊功能寄存器,此寄存器的各位如下表所示:

在这里插入图片描述
在这里插入图片描述

在特殊功能寄存器中我们关心的是bit13~bit15 这三位,因为系统通过读取这3 位的值来设置BCR 寄存器的bit8 和bit13。由于特殊功能寄存器不属于IEEE802.3 规定的前16 个寄存器,所以每个厂家的可能不同,这个需要用户根据自己实际使用的PHY 芯片去修改。
ST 提供的以太网驱动文件有三个配置项值得读者注意的,它们分别为PHY_SR、PHY_SPEED_STATUS 和PHY_DUPLEX_STATUS 配置项,这些配置项用来描述PHY 特殊功能寄存器,根据该寄存器的值设置BCR 寄存器的第8 位和第13 位,即双工和网速。
探索者、DMF407 开发板:

/* 网卡PHY地址设置*/
#define ETHERNET_PHY_ADDRESS 0x00
/* 选择PHY芯片*/
#define LAN8720 0
#define SR8201F 1
#define YT8512C 2
#define RTL8201 3
#define PHY_TYPE YT8512C
#if(PHY_TYPE == LAN8720)
#define PHY_SR ((uint16_t)0x1F) /* PHY状态寄存器地址*/
#define PHY_SPEED_STATUS ((uint16_t)0x0004) /* PHY速度状态*/
#define PHY_DUPLEX_STATUS ((uint16_t)0x0010) /* PHY双工状态*/
#elif(PHY_TYPE == SR8201F)
#define PHY_SR ((uint16_t)0x00) /* PHY状态寄存器地址*/
#define PHY_SPEED_STATUS ((uint16_t)0x2020) /* PHY速度状态*/
#define PHY_DUPLEX_STATUS ((uint16_t)0x0100) /* PHY双工状态*/
#elif(PHY_TYPE == YT8512C)
#define PHY_SR ((uint16_t)0x11) /* PHY状态寄存器地址*/
#define PHY_SPEED_STATUS ((uint16_t)0x4010) /* PHY速度状态*/
#define PHY_DUPLEX_STATUS ((uint16_t)0x2000) /* PHY双工状态*/
#elif(PHY_TYPE == RTL8201)
#define PHY_SR ((uint16_t)0x10) /* PHY状态寄存器地址*/
#define PHY_SPEED_STATUS ((uint16_t)0x0022) /* PHY速度状态*/
#define PHY_DUPLEX_STATUS ((uint16_t)0x0004) /* PHY双工状态*/

阿波罗、北极星开发板:

/* PHY地址*/
#define ETH_CHIP_ADDR ((uint16_t)0x0000U)
/* 选择PHY芯片*/
#define LAN8720 0
#define SR8201F 1
#define YT8512C 2
#define RTL8201 3
#define PHY_TYPE YT8512C
#if(PHY_TYPE == LAN8720)
#define ETH_CHIP_PHYSCSR ((uint16_t)0x1F)
#define ETH_CHIP_SPEED_STATUS ((uint16_t)0x0004)
#define ETH_CHIP_DUPLEX_STATUS ((uint16_t)0x0010)
#elif(PHY_TYPE == SR8201F)
#define ETH_CHIP_PHYSCSR ((uint16_t)0x00)
#define ETH_CHIP_SPEED_STATUS ((uint16_t)0x2020)
#define ETH_CHIP_DUPLEX_STATUS ((uint16_t)0x0100)
#elif(PHY_TYPE == YT8512C)
#define ETH_CHIP_PHYSCSR ((uint16_t)0x11)
#define ETH_CHIP_SPEED_STATUS ((uint16_t)0x4010)
#define ETH_CHIP_DUPLEX_STATUS ((uint16_t)0x2000)
#elif(PHY_TYPE == RTL8201)
#define ETH_CHIP_PHYSCSR ((uint16_t)0x10)
#define ETH_CHIP_SPEED_STATUS ((uint16_t)0x0022)
#define ETH_CHIP_DUPLEX_STATUS ((uint16_t)0x0004)
#endif /* PHY_TYPE */

笔者已经适配了多款PHY 芯片,根据PHY_TYPE 配置项来选择PHY_SR、PHY_SPEED_
STATUS 和PHY_DUPLEX_STATUS 配置项的数值。

LAN8720A 简介

LAN8720A 是一款低功耗的10/100M 以太网PHY 层芯片,它通过RMII/MII 介质接口与
以太网MAC 层通信,内置10-BASE-T/100BASE-TX 全双工传输模块,支持10Mbps 和
100Mbps。LAN8720A 主要特点如下:

高性能的10/100M 以太网传输模块。
支持RMII 接口以减少引脚数。
支持全双工和半双工模式。
两个状态LED 输出。
可以使用25M 晶振以降低成本。
支持自协商模式。
支持HP Auto-MDIX 自动翻转功能。
支持SMI 串行管理接口。
支持MAC 接口。

LAN8720A 功能框图如下图所示:

在这里插入图片描述

➢ LAN8720A 中断管理
LAN8720A 的器件管理接口支持非IEEE 802.3 规范的中断功能。当一个中断事件发生并且
相应事件的中断位使能,LAN8720A 就会在nINT(14 脚)产生一个低电平有效的中断信号。LAN8720A 的中断系统提供两种中断模式:主中断模式和复用中断模式。主中断模式是默认中断模式,LAN8720A 上电或复位后就工作在主中断模式,当模式控制/状态寄存器(十进制地址为17)的ALTINT 为0 是LAN8720 工作在主模式,当ALTINT 为1 时工作在复用中断模式。正点原子的STM32 系列开发板并未用到中断功能,关于中断的具体用法可以参考LAN8720A 数据手册的29,30 页。

➢ PHY 地址设置
MAC 层通过SMI 总线对PHY 进行读写操作,SMI 可以控制32 个PHY 芯片,通过不同
的PHY 芯片地址来对不同的PHY 操作。LAN8720A 通过设置RXER/PHYAD0 引脚来设置其PHY 地址,默认情况下为0,其地址设置如下表所示。正点原子的STM32 系列开发板使用的是默认地址,也就是0X00。

在这里插入图片描述

➢ nINT/REFCLKO 配置

nINTSEL 引脚(2 号引脚)用于设置nINT/REFCLKO 引脚(14 号引脚)的功能。
nINTSEL 配置如下表所示。

在这里插入图片描述

当工作在REF_CLK In 模式时,50MHz 的外部时钟信号应接到LAN8720 的XTAL1/CKIN
引脚(5 号引脚)和STM32 的RMII_REF_CLK(PA1)引脚上,如下图所示。

在这里插入图片描述

为了降低成本,LAN8720A 可以从外部的25MHz 的晶振中产生REF_CLK 时钟。到要使
用此功能时应工作在REF_CLK Out 模式。当工作在REF_CLO Out 模式时REF_CLK 的时钟源如下图所示。

在这里插入图片描述

➢ LAN8720A 内部寄存器
PHY 是由IEEE 802.3 定义的,一般通过SMI 对PHY 进行管理和控制,也就是读写PHY 内
部寄存器。PHY 寄存器的地址空间为5 位,可以定义0~ 31 共32 个寄存器,但是随之PHY 芯片功能的增加,很多PHY 芯片采用分页技术来扩展地址空间,定义更多的寄存器,在这里我们不讨论这种情况。IEEE 802.3 定义了0~ 15 这16 个寄存器的功能,16~31 寄存器由芯片制造商自由定义。在LAN8720A 有很多寄存器,笔者重点讲解BCR(0),BSR(1),PHY 特殊功能寄存器(31)这三个寄存器,前面两个寄存器笔者已经在1.6.1 小节讲解了,这里笔者无需重复讲解。接下来介绍的是LAN8720A 特殊功能寄存器,此寄存器的各个位如下表所示:

在这里插入图片描述

在特殊功能寄存器中我们关心的是bit2~bit4 这三位,因为系统通过读取这3 位的值来设
置BCR 寄存器的bit8 和bit13。

以太网接入MCU 方案

以太网接入方案一般分为两种,它们分别为全硬件TCP/IP 协议栈软件TCP/IP 协议栈
其中,软件TCP/IP 协议栈用途非常广泛,如电脑、交换机等网络设备,而全硬件TCP/IP 协议栈是近年来比较新型的以太网接入方案。下面笔者分别来讲解这两种接入方案的差异和优缺点。

软件TCP/IP 协议栈以太网接入方案

这种方案由lwIP + MAC 内核 + PHY 层芯片实现以太网物理连接,如正点原子的探索者、
阿波罗、北极星以及电机开发板都是采用这类型的以太网接入方案,该方案的连接示意图如下图所示:

在这里插入图片描述

上图中,MCU 要求内置 MAC 内核,该内核相当TCP/IP 体系结构的数据链路层,而lwIP软件库用来实现TCP/IP 体系结构的应用层、传输层和网络层,同时,板载PHY 层芯片用来实现TCP/IP 体系结构的物理层。因此,lwIP、MAC 内核和PHY 层芯片构建了网络通信核心骨架。

优点:
移植性:可在不同平台、不同编译环境的程序代码经过修改转移到自己的系统中运行。
可造性:可在TCP/IP协议栈的基础上添加和删除相关功能。
可扩展性:可扩展到其他领域的应用及开发。

缺点:
内存方面分析:传统的TCP/IP 方案是移植一个lwIP 的TCP/IP 协议(RAM 50K+,
ROM 80K+),造成主控可用内存减小。
从代码量分析:移植lwIP可能需要的代码量超过40KB,对于有些主控芯片内存匮乏
来说无疑是一个严重的问题。
从运行性能方面分析:由于软件TCP/IP协议栈方案在通信时候是不断地访问中断机
制,造成线程无法运行,如果多线程运行,会使MCU的工作效率大大降低。
从安全性方面分析:软件协议栈会很容易遭受网络攻击,造成单片机瘫痪。

硬件TCP/IP 协议栈以太网接入方案

所谓全硬件TCP/IP 协议栈是将传统的软件协议TCP/IP 协议栈用硬件化的逻辑门电路来实
现。芯片内部完成TCP、UDP、ICMP 等多种网络协议,并且实现了物理层以太网控制
(MAC+PHY)、内存管理等功能,完成了一整套硬件化的以太网解决方案。
该方案的连接示意图如下图所示:

在这里插入图片描述

上图中,MCU 通过串口或者SPI 进行网络通讯,无需移植协议库,极大地减少程序的代
码量,甚至弥补了网络协议安全性不足的短板。硬件TCP/IP 协议栈的优缺点,如下所示:

优点:
从代码量方面来看:相比于传统的接入已经大大减少了代码量。
从运行方面来看:极大的减少了中断次数,让单片机更好的完成其他线程的工作。
从安全性方面来看:硬件化的逻辑门电路来处理TCP/IP协议是不可被攻击的,也就
是说网络攻击和病毒对它无效,这也充分弥补了网络协议安全性不足的短板。

缺点:
从可扩展性来看:虽然该芯片内部使用逻辑门电路来实现应用层和物理层协议,但是
它具有功能局限性,例如给TCP/IP协议栈添加一个协议,这样它无法快速添加了。
从收发速率来看:全硬件TCP/IP协议栈芯片都是采用并口、SPI以及IIC等通讯接
口来收发数据,这些数据会受通信接口的速率而影响。
总的来说:全硬件TCP / IP 协议栈简化传统的软件TCP / IP 协议栈,卸载了MCU 用于处
理TCP / IP 这部分的线程,节约MCU 内部ROM 等硬件资源,工程师只需进行简单的套接字
编程和少量的寄存器操作即可方便地进行嵌入式以太网上层应用开发,减少产品开发周期,降低开发成本。

lwIP 无操作系统移植

lwIP 带操作系统移植

ARP 协议

ARP 协议的简介

ARP 全称为Address Resolution Protocol(地址解析协议),是根据IP 地址获取物理地址的一个TCP/IP 协议。

主机发送信息时将包含目标IP 地址的ARP 请求广播到局域网络上的所有主机,并接收返回消息,以此确定目标的物理地址;收到返回消息后将该IP 地址和物理地址存入本机ARP 缓存中并保留一定时间,下次请求时直接查询ARP 缓存以节约资源。地址解析协议是建立在网络中各个主机互相信任的基础上的,局域网络上的主机可以自主发送ARP 应答消息,其他主机收到应答报文时不会检测该报文的真实性就会将其记入本机ARP 缓存;总的来说,ARP 协议是透过目标设备的IP 地址,查询目标设备的MAC 地址,以保证通信的顺利进行。

ARP 协议的工作流程

假设由两台主机,分别为主机A(192.168.0.10)与主机B(192.168.0.11),它们两个都是
同一网段的,如果主机A 向主机B 发送信息或者数据,ARP 的地址解析过程有以下几个步骤:

  • ①主机A 首先查自己的ARP 表是否有包含主机B 的信息,例如主机B 的MAC 地址,如
    果主机A 的ARP 表包含主机B 的MAC 地址,则主机A 直接利用ARP 表的主机B 的MAC 地址对IP 数据包进行封装并把数据包发给主机B。

  • ②如果主机A 的ARP 表没有包含主机B 的MAC 地址或者没有找到主机B 的MAC 地址,
    则主机A 就把数据包缓存起来,然后以广播的方式发送一个ARP 包的请求报文,该ARP 包的内容包含主机A 的IP 地址、MAC 地址、主机B 的IP 地址和主机B 的全0 的MAC 地址,由于主机A 发送ARP 包是使用广播形式,那么同一网段的主机都可以收到该ARP 包,主机B接收到这个ARP 包会进行处理。

  • ③主机B 接收到主机A 的ARP 包之后,主机B 会对这个ARP 解析并比较自己的IP 地址
    和ARP 包的目的IP 地址是否相同,如果相同,则主机B 将ARP 请求报文中的发送端(即主机A)的IP 地址和MAC 地址存入自己的ARP 表中。之后以单播方式发送ARP 响应报文给主机A,其中包含了自己的MAC 地址。

  • ④当主机A 收到了主机B 的ARP 包也是同样的处理,首先比较ARP 包的IP 地址是否和
    自己的IP 地址相同,如果IP 地址相同,则把ARP 包的信息存入自己的ARP 表中,最后对IP数据包进行封装并把数据包发给主机B。

从上述步骤的内容,可得到ARP 包的流程图,如下图所示:

在这里插入图片描述

可以看到,主机A 发送数据之前先判断主机A 的ARP 缓存表是否包含主机B 的MAC 地
址,若主机A 的ARP 缓存表没有主机B 的MAC 地址,则主机A 把要发送的数据挂起并发送
一个ARP 请求包,发送完成之后等待主机B 应答,直到收到主机B 的应答包之后才把挂起的
数据包添加以太网首部发送至主机B 当中。

lwIP 描述ARP 缓存表和ARP 相关处理函数由etharp.c/h 文件定义,下面笔者重点讲解
ARP 缓存表的表项信息和挂起流程。ARP 缓存表结构如下所示:

struct etharp_entry {
    
    
#if ARP_QUEUEING
	/* 数据包缓存队列指针*/
	struct etharp_q_entry *q;
#else /* ARP_QUEUEING */
	/* 指向此ARP表项上的单个挂起数据包的指针*/
struct pbuf *q;
#endif /* ARP_QUEUEING */
	ip4_addr_t ipaddr; /* 目标IP 地址*/
	struct netif *netif; /* 对应网卡信息*/
	struct eth_addr ethaddr; /* 对应的MAC 地址*/
	u16_t ctime; /* 生存时间信息*/
	u8_t state; /* 表项的状态*/
};

static struct etharp_entry arp_table[ARP_TABLE_SIZE];

可以看出,ARP 缓存表(arp_table)最大存放10 个表项,每一个表项描述符了IP 地址映
射MAC 地址的信息和表项生存时间与状态。这个ARP 缓存表很小,lwIP 根据传入的目标IP
地址对ARP 缓存表直接采用遍历方式查找对应的MAC 地址。

注:每一个表项都有一个生存时间,若超出了自身生存时间,则lwIP 内核会把这个表项
删除,这里用到了超时处理机制。

每一个表项从创建、请求等都设置了一个状态,不同状态的表项都需要特殊的处理,这些
状态如下所示:

enum etharp_state {
    
    
	ETHARP_STATE_EMPTY = 0,
	ETHARP_STATE_PENDING,
	ETHARP_STATE_STABLE,
	ETHARP_STATE_STABLE_REREQUESTING_1,
	ETHARP_STATE_STABLE_REREQUESTING_2
};

下面笔者讲解一下每一个表项的作用及任务。

  • (1) ETHARP_STATE_EMPTY 状态
    这个状态表示ARP 缓存表处于初始化的状态,所有表项初始化之后才可以被使用,如果需要添加表项,lwIP 内核就会遍历ARP 缓存表并找到合适的表项进行添加。

  • (2) ETHARP_STATE_PENDING 状态
    该状态表示该表项处于不稳定状态,此时该表项只记录到了IP 地址,但是还未记录到对
    应的MAC 地址。很可能的情况是:lwIP 内核已经发出一个关于该IP 地址的ARP 请求到数据链路上且lwIP 内核还未收到ARP 应答,此时ETHARP_STATE_PENDING 状态下会设定超时时间(5 秒),当计数超时后,对应的表项将被删除,超时时间需要宏定义
    ARP_MAXPENDING 来指定,默认为5 秒,如果在5 秒之前收到应答数据包,那么系统会更新缓存表的信息,记录目标IP 地址与目标MAC 地址的映射关系并且开始记录表项的生存时间,同时该表项的状态会变成ETHARP_STATE_STABLE 状态。

  • (3) ETHARP_STATE_STABLE 状态
    当收到应答之前,这些数据包会暂时挂载到表项的数据包缓冲队列上,收到应答之后,系统已经更新ARP 缓存表,那么系统发送数据就会进入该状态

  • (4)ETHARP_STATE_STABLE_REREQUESTING_1&&
    ETHARP_STATE_STABLE_REREQUESTING_2 状态
    如果系统再一次发送ARP 请求数据包,则表项状态会暂时被设置为ETHARP_STATE_ST
    ABLE_REREQUESTING_1,之后设置为ETHARP_STATE_STABLE_REREQUESTING_2 状态,其实这两个状态为过渡状态,如果5 秒之前收到ARP 应答后,表项又会被设置为ETHARP_S
    TATE_STABLE 状态,这样子能保持表项的有效性。
    这些状态也是和超时处理相关,在ARP 超时事件中,需要定时遍历ARP 缓存表各个表项的状态和检测各个表项的生存时间。稍后笔者也会讲解ARP 超时事件的作用。
    表项挂起数据包

之前讲解过,lwIP 发送数据包时需要检测ARP 缓存表是否包含对方主机的MAC 地址,
若ARP 缓存表没有包含对方主机的MAC 地址,则lwIP 内核在ARP 缓存表上创建一个表项并
且构建一个ARP 请求包,发送完成之后lwIP 内核把要发送的数据包挂载到新创建的表项当中。在表项中包含了etharp_q_entry 结构体和pbuf 结构体指针,这两个都是用来挂载数据包的,一般来说,lwIP 内核不使用etharp_q_entry 结构体挂载数据包,而是直接使用指针指向pbuf 数据包,下面笔者使用一张图来描述上面的内容。

在这里插入图片描述

ARP 缓存表的超时处理

上一个小节写了这么多,无非就是为了ARP 表项的ctime(生存时间)这个参数准备的,
其实这个参数笔者在上面也有所涉及,因为系统以周期的形式调用函数etharp_trm。例如,5秒之前收到ARP 的应答包就会更新ARP 缓存表,这个函数的作用就是使每个ARP 缓存表项ctime 字段加1 处理,如果某个表项的生存时间计数值大于系统规定的某个值,系统就会删除该表项。etharp_trm 函数如下所示:

void etharp_tmr(void) {
    
    
    u8_t i;
    /* 第一步:ARP缓存表遍历,ARP_TABLE_SIZE = 10 */
    for (i = 0; i < ARP_TABLE_SIZE; ++i) {
    
    
        /* 获取表项的状态*/
        u8_t state = arp_table[i].state;
        /* 第二步:判断该状态不等于空(初始化的状态)*/
        if (state != ETHARP_STATE_EMPTY) {
    
    
            /* ARP缓存表项的生存时间+1 */
            arp_table[i].ctime++;
            /* 第三步:发送ARP请求数据包并判断ctime是否大于5秒*/
            if ((arp_table[i].ctime >= ARP_MAXAGE) ||
                ((arp_table[i].state == ETHARP_STATE_PENDING) &&
                    (arp_table[i].ctime >= ARP_MAXPENDING))) {
    
    
                /* 从ARP缓存表中删除该表项*/
                etharp_free_entry(i);
            } else if (arp_table[i].state == ETHARP_STATE_STABLE_REREQUESTING_1) {
    
    
                /* 这是一个过度形式*/
                arp_table[i].state = ETHARP_STATE_STABLE_REREQUESTING_2;
            } else if (arp_table[i].state == ETHARP_STATE_STABLE_REREQUESTING_2) {
    
    
                /* 将状态重置为稳定状态,使下一个传输的数据包将
                重新发送一个ARP请求*/
                arp_table[i].state = ETHARP_STATE_STABLE;
            } else if (arp_table[i].state == ETHARP_STATE_PENDING) {
    
    
                /* 仍然挂起,重新发送一个ARP查询*/
                etharp_request(arp_table[i].netif, & arp_table[i].ipaddr);
            }
        }
    }
}

此函数非常简单,这里笔者使用一个流程图来讲解这个函数的实现流程,如下图所示:

在这里插入图片描述

从上图可以看出,这些ARP 缓存表的表项都会定期检测,如果这些表项超时最大生存时
间,那么lwIP 内核会把这些表项统一删除。

APR 报文的报文结构

典型的ARP 报文结构,该结构如下图所示:

在这里插入图片描述

左边的是以太网首部,数据发送时必须添加以太网首部,添加完成之后才能把数据发往到
网络当中(这里解答了为什么需要对方主机的MAC 地址),而右边是ARP 报文结构,它一共定义了5 个字段,它们分别为:

  • 硬件类型:如果这个类型设置为1 表示以太网MAC 地址。
  • 协议类型:表示要映射的协议地址类型,0x0800–映射为IP 地址。
  • 硬件地址长度和协议地址长度:以太网ARP 请求和应答分别设置为6 和4,它们代表M
    AC 地址长度和IP 地址长度。在ARP 协议包中留出硬件地址长度字段和协议地址长度字段可以使得ARP 协议在任何网络中被使用,而不仅仅只在以太网中。
  • op:ARP 数据包的类型,ARP 请求设置为1,ARP 应答设置为2。
  • 剩下的字段就是填入本地IP 地址与本地MAC 地址和目标IP 地址与目标MAC 地址。

关于ARP 报文结构可在ethernet.h 找到一些数据结构和宏描述,如下所示:

/**********************************ethernet.h********************************/ #
define ETH_HWADDR_LEN 6 /* 以太网地址长度*/
struct eth_addr {
    
     /* 一个以太网MAC地址*/
    PACK_STRUCT_FLD_8(u8_t addr[ETH_HWADDR_LEN]);
}
PACK_STRUCT_STRUCT;
struct eth_hdr {
    
     /* 以太网首部*/ #
    if ETH_PAD_SIZE
    PACK_STRUCT_FLD_8(u8_t padding[ETH_PAD_SIZE]);#
    endif
    PACK_STRUCT_FLD_S(struct eth_addr dest); /* 以太网目标地址(6字节) */
    PACK_STRUCT_FLD_S(struct eth_addr src); /* 以太网源MAC 地址(6字节) */
    PACK_STRUCT_FIELD(u16_t type); /* 帧类型(2字节) */
}
PACK_STRUCT_STRUCT;
/***********************************etharp.h**********************************/
struct etharp_hdr {
    
     /* ARP 报文*/
    /* ARP 报文首部*/
    PACK_STRUCT_FIELD(u16_t hwtype); /* 硬件类型(2字节) */
    PACK_STRUCT_FIELD(u16_t proto); /* 协议类型(2字节) */
    PACK_STRUCT_FLD_8(u8_t hwlen); /* 硬件地址长度(1字节) */
    PACK_STRUCT_FLD_8(u8_t protolen); /* 协议地址长度(2字节) */
    PACK_STRUCT_FIELD(u16_t opcode); /* op 字段(2字节) */
    PACK_STRUCT_FLD_S(struct eth_addr shwaddr); /* 源MAC 地址(6字节) */
    PACK_STRUCT_FLD_S(struct ip4_addr2 sipaddr); /* 源ip 地址(4字节) */
    PACK_STRUCT_FLD_S(struct eth_addr dhwaddr); /* 目标MAC 地址(6字节) */
    PACK_STRUCT_FLD_S(struct ip4_addr2 dipaddr); /* 目标ip 地址(4字节) */
}
PACK_STRUCT_STRUCT;
/* op 字段操作*/
enum etharp_opcode {
    
    
    ARP_REQUEST = 1, /* 请求包*/
    ARP_REPLY = 2 /* 应答包*/
};

前面的eth_hdr 结构体就是定义了以太网首部字段,而etharp_hdr 结构体定义了ARP 首部
的字段信息。下面笔者使用wireshark 网络抓包工具形象地讲解报文格式和内容,如下图所示:

在这里插入图片描述
在这里插入图片描述

从这两张图可以看出,图一的ARP 数据包是以广播的方式发送,它的OP 字段类型为1
表示ARP 数据包为ARP 请求包。图二的ARP 数据包为ARP 应答包,因为它的OP 字段为2,所以该包不是以广播的方式发送。

ARP 协议层的接收与发送原理解析

发送ARP 请求数据包

构建ARP 请求包函数是在etharp_raw 函数下实现,该函数如下所示:

static err_t
etharp_raw(struct netif * netif, /* 发送ARP 数据包的lwip 网络接口*/
        	const struct eth_addr * ethsrc_addr, /* 以太网源MAC 地址*/
            const struct eth_addr * ethdst_addr, /* 以太网目标MAC 地址*/
            const struct eth_addr * hwsrc_addr, /* ARP 协议源MAC 地址*/
            const ip4_addr_t * ipsrc_addr, /* ARP 协议源IP 地址*/
            const struct eth_addr * hwdst_addr, /* ARP 协议目标MAC 地址*/
            const ip4_addr_t * ipdst_addr, /* ARP 协议目标IP 地址*/
            const u16_t opcode) /* ARP 数据包的类型:1为请求包类型、2为应答包类型*/ {
    
    
        struct pbuf * p;
        err_t result = ERR_OK;
        struct etharp_hdr * hdr;
        /* 申请ARP 报文的内存池空间*/
        p = pbuf_alloc(PBUF_LINK, SIZEOF_ETHARP_HDR, PBUF_RAM);
        /* 申请内存池是否成功*/
        if (p == NULL) {
    
    
            ETHARP_STATS_INC(etharp.memerr);
            return ERR_MEM;
        }
        /* ARP 报文的数据区域,并且强制将起始地址转化成ARP 报文首部*/
        hdr = (struct etharp_hdr * ) p - > payload;
        /* ARP 数据包的op 字段*/
        hdr - > opcode = lwip_htons(opcode);
        /* 源MAC地址*/
        SMEMCPY( & hdr - > shwaddr, hwsrc_addr, ETH_HWADDR_LEN);
        /* 目的MAC地址*/
        SMEMCPY( & hdr - > dhwaddr, hwdst_addr, ETH_HWADDR_LEN);
        /* 源IP地址*/
        IPADDR_WORDALIGNED_COPY_FROM_IP4_ADDR_T( & hdr - > sipaddr, ipsrc_addr);
        /* 目的IP地址*/
        IPADDR_WORDALIGNED_COPY_FROM_IP4_ADDR_T( & hdr - > dipaddr, ipdst_addr);
        /* 硬件类型*/
        hdr - > hwtype = PP_HTONS(HWTYPE_ETHERNET);
        /* 协议类型*/
        hdr - > proto = PP_HTONS(ETHTYPE_IP);
        /* 硬件地址长度*/
        hdr - > hwlen = ETH_HWADDR_LEN;
        /* 协议地址长度*/
        hdr - > protolen = sizeof(ip4_addr_t);#
        if LWIP_AUTOIP
        if (ip4_addr_islinklocal(ipsrc_addr)) {
    
    
            ethernet_output(netif, p, ethsrc_addr, & ethbroadcast, ETHTYPE_ARP);
        } else# endif /* LWIP_AUTOIP */ {
    
    
            /* 调用底层发送函数将以太网数据帧发送出去*/
            ethernet_output(netif, p, ethsrc_addr, ethdst_addr, ETHTYPE_ARP);
        }
        ETHARP_STATS_INC(etharp.xmit);
        /* 发送完成释放内存*/
        pbuf_free(p);
        p = NULL;
        /* 发送完成返回结果*/
        return result;
    }
    /* 定义以太网广播地址*/
const struct eth_addr ethbroadcast = {
    
    
    {
    
    
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff
    }
};
/* 填写ARP请求包的接收方MAC字段*/
const struct eth_addr ethzero = {
    
    
    {
    
    
        0, 0, 0, 0, 0, 0
    }
};
static err_t
etharp_request_dst(struct netif * netif,
        const ip4_addr_t * ipaddr,
            const struct eth_addr * hw_dst_addr) {
    
    
        return etharp_raw(netif, (struct eth_addr * ) netif - > hwaddr, hw_dst_addr, (struct eth_addr * ) netif - > hwaddr,
            netif_ip4_addr(netif), & ethzero,
            ipaddr, ARP_REQUEST);
    }
    /* 发送一个要求ipaddr的ARP请求包*/
err_t
etharp_request(struct netif * netif,
    const ip4_addr_t * ipaddr) {
    
    
    return etharp_request_dst(netif, ipaddr, & ethbroadcast);
}

发送ARP 请求报文之前先申请pbuf 内存,接着由pbuf 的payload 指针指向的地址添加
ARP 首部,添加完成之后设置ARP 首部字段的信息,最后由ethernet_output 函数为pbuf 添加以太网首部和发送,发送完成之后把要发送的数据挂载到ARP 缓存表项当中。

接收ARP 应答数据包

虽然ARP 和IP 协议同属于网络层的协议,但是从分层的结构来看,ARP 处于网络层的最
底层,而IP 处于网络层的顶层。总的来说,ARP 最接近网卡驱动文件,发送的数据经过ARP检测和操作发送至网卡驱动文件处理,由网卡驱动文件调用ETH 外设把数据发送至PHY 设备当中。

下面笔者来讲解网卡驱动文件的函数如何把接收的数据发送至ARP 或者IP 处理,这个函数为ethernet_input,如下所示:

err_t
ethernet_input(struct pbuf * p, struct netif * netif) {
    
    
    struct eth_hdr * ethhdr;
    u16_t type;#
    if LWIP_ARP || ETHARP_SUPPORT_VLAN || LWIP_IPV6
    s16_t ip_hdr_offset = SIZEOF_ETH_HDR;#
    endif /* LWIP_ARP || ETHARP_SUPPORT_VLAN */
    /* 第一步:判断数据包是否小于等于以太网头部的大小
    如果是,则释放内存,直接返回*/
    if (p - > len <= SIZEOF_ETH_HDR) {
    
    
        ETHARP_STATS_INC(etharp.proterr);
        ETHARP_STATS_INC(etharp.drop);
        MIB2_STATS_NETIF_INC(netif, ifinerrors);
        goto free_and_return;
    }
    if (p - > if_idx == NETIF_NO_INDEX) {
    
    
        p - > if_idx = netif_get_index(netif);
    }
    /* 第二步:p->payload表示指向缓冲区中实际数据的指针
    相当于指向以太网的头部*/
    ethhdr = (struct eth_hdr * ) p - > payload;
    /* 第三步:获取数据包的类型*/
    type = ethhdr - > type;#
    if LWIP_ARP_FILTER_NETIF
    netif = LWIP_ARP_FILTER_NETIF_FN(p, netif, lwip_htons(type));#
    endif /* LWIP_ARP_FILTER_NETIF*/
    /* 第四步:判断数据包是以怎么样的类型发来的*/
    if (ethhdr - > dest.addr[0] & 1) {
    
    
        /* 这可能是一个多播或广播包*/
        if (ethhdr - > dest.addr[0] == LL_IP4_MULTICAST_ADDR_0) {
    
    #
            if LWIP_IPV4
            if ((ethhdr - > dest.addr[1] == LL_IP4_MULTICAST_ADDR_1) &&
                (ethhdr - > dest.addr[2] == LL_IP4_MULTICAST_ADDR_2)) {
    
    
                /* 将pbuf标记为链路层多播*/
                p - > flags |= PBUF_FLAG_LLMCAST;
            }#
            endif /* LWIP_IPV4 */
        } else if (eth_addr_cmp( & ethhdr - > dest, & ethbroadcast)) {
    
    
            /* 将pbuf标记为链路层广播*/
            p - > flags |= PBUF_FLAG_LLBCAST;
        }
    }
    /* 第五步:判断数据包的类型*/
    switch (type) {
    
    #
        if LWIP_IPV4 && LWIP_ARP
            /* IP数据包*/
        case PP_HTONS(ETHTYPE_IP):
            if (!(netif - > flags & NETIF_FLAG_ETHARP)) {
    
    
                goto free_and_return;
            }
            /* 去除以太网报头*/
            if ((p - > len < ip_hdr_offset) ||
                pbuf_header(p, (s16_t) - ip_hdr_offset)) {
    
     /* 去除以太网首部失败,则直接返回*/
                goto free_and_return;
            } else {
    
    
                /* 传递到IP 协议去处理*/
                ip4_input(p, netif);
            }
            break;
            /* 对于是ARP 包*/
        case PP_HTONS(ETHTYPE_ARP):
            if (!(netif - > flags & NETIF_FLAG_ETHARP)) {
    
    
                goto free_and_return;
            }
            /* 去除以太网首部*/
            if ((p - > len < ip_hdr_offset) ||
                pbuf_header(p, (s16_t) - ip_hdr_offset)) {
    
     /* 去除以太网首部失败,则直接返回*/
                ETHARP_STATS_INC(etharp.lenerr);
                ETHARP_STATS_INC(etharp.drop);
                goto free_and_return;
            } else {
    
    
                /* 传递到ARP 协议处理*/
                etharp_input(p, netif);
            }
            break;#
            endif /* LWIP_IPV4 && LWIP_ARP */
        default:
            #ifdef LWIP_HOOK_UNKNOWN_ETH_PROTOCOL
            if (LWIP_HOOK_UNKNOWN_ETH_PROTOCOL(p, netif) == ERR_OK) {
    
    
                break;
            }#
            endif
            ETHARP_STATS_INC(etharp.proterr);
            ETHARP_STATS_INC(etharp.drop);
            MIB2_STATS_NETIF_INC(netif, ifinunknownprotos);
            goto free_and_return;
    }
    return ERR_OK;
    free_and_return:
        pbuf_free(p);
    return ERR_OK;
}

为了理解整个以太网的数据帧在ARP 层处理,笔者就以图形展示整个数据包递交流程,
如下图所示

在这里插入图片描述

可以看出,数据包在ethernet_input 中需要判断该数据包的类型,若该数据包的类型为IP
数据包,则lwIP 内核把该数据包递交给ip4_input 函数处理。若该数据包的类型为ARP 数据
包,则lwIP 内核把该数据包递交给etharp_input 函数处理,递交完成之后该函数需要判断
ARP 数据包的类型,如果它是ARP 请求包,则lwIP 内核调用etharp_raw 函数构建ARP 应答包并且更新ARP 缓存表;如果它是ARP 应答包,则lwip 内核更新ARP 缓存表并且把表项挂载的数据包以ethernet_output 函数发送。

IP 协议

IP 指网际互连协议,Internet Protocol 的缩写,是TCP/IP 体系中的网络层协议。设计IP 的
目的是提高网络的可扩展性:一是解决互联网问题,实现大规模、异构网络的互联互通;二是分割顶层网络应用和底层网络技术之间的耦合关系,以利于两者的独立发展。根据端到端的设计原则,IP 只为主机提供一种无连接、不可靠的、尽力而为的数据包传输服务。

IP 协议的简介

IP 协议是整个TCP/IP 协议族的核心,也是构成互联网的基础。IP 位于TCP/IP 模型的网
络层(相当于OSI 模型的网络层),它可以向传输层提供各种协议的信息,例如TCP、UDP 等;

对下可将IP 信息包放到链路层,通过以太网、令牌环网络等各种技术来传送。为了能适应异
构网络,IP 强调适应性、简洁性和可操作性,并在可靠性做了一定的牺牲。这里我们不过多
深入了解IP 协议了,本章笔者重点讲解IP 数据报的分片与重组原理。

IP 数据报

IP 层数据报也叫做IP 数据报或者IP 分组,IP 数据报组装在以太网帧中发送的,它通常由
两个部分组成,即IP 首部与数据区域,其中IP 的首部是20 字节大小,数据区域理论上可以
多达65535 个字节,由于以太网网络接口的最大传输单元为1500,所以一个完整的数据包不
能超出1500 字节大小。IP 数据报结构如以下图所示:

在这里插入图片描述

  • (1) 版本:占4 位指IP 协议的版本。通信双方使用的IP 协议版本必须一致。广泛使用的
    IP 协议版本号为4(即IPv4)。

  • (2) 首部长度:占4 位可表示的最大十进制数值是15。请注意,这个字段所表示数的单位是32 位字长(1 个32 位字长是4 字节),因此,当IP 的首部长度为1111 时(即十进制的15),首部长度就达到60 字节。当IP 分组的首部长度不是4 字节的整数倍时,必须利用最后的填充字段加以填充。因此数据部分永远在4 字节的整数倍开始,这样在实现IP 协议时较为方便。
    首部长度限制为60 字节的缺点是有时可能不够用。但这样做是希望用户尽量减少开销。最常用的首部长度就是20 字节(即首部长度为0101),这时不使用任何选项。

  • (3) 区分服务:占8 位,用来获得更好的服务。这个字段在旧标准中叫做服务类型,但实际上一直没有被使用过。

  • (4) 总长度:总长度指首部和数据之和的长度,单位为字节。总长度字段为16 位,因此数据报的最大长度为2^16-1=65534 字节。
    在IP 层下面的每一种数据链路层都有自己的帧格式,其中包括帧格式中的数据字段的最
    大长度,这称为最大传送单元MTU。当一个数据报封装成链路层的帧时,此数据报的总长度(即首部加上数据部分)一定不能超过下面的数据链路层的MTU 值。

  • (5) 标识(identification):占16 位IP 软件在存储器中维持一个计数器,每产生一个数据报,计数器就加1,并将此值赋给标识字段。但这个“标识”并不是序号,因为IP 是无连接服务,数据报不存在按序接收的问题。当数据报由于长度超过网络的MTU 而必须分片时,这个标识字段的值就被复制到所有的数据报的标识字段中。相同的标识字段的值使分片后的各数据报片最后能正确地重装成为原来的数据报。

  • (6) 标志(flag):占3 位但只有2 位有意义的。

  1. 标志字段中的最低位记为MF(More Fragment)。MF=1 即表示后面“还有分片”的数
    据报。MF=0 表示这已是若干数据报片中的最后一个。
  2. 标志字段中间的一位记为DF(Don’t Fragment),意思是“不能分片”。只有当DF=0
    时才允许分片。
  • (7) 片偏移:占13 位片偏移指出:较长的分组在分片后,某片在原分组中的相对位置。也
    就是说,相对用户数据字段的起点,该片从何处开始。片偏移以8 个字节为偏移单位。这就是说,除了最后一个分片,每个分片的长度一定是8 字节(64 位)的整数倍。
  • (8) 生存时间:占8 位生存时间字段常用的的英文缩写是TTL(Time To Live),表明是数据
    报在网络中的寿命。由发出数据报的源点设置这个字段。其目的是防止无法交付的数据报无限制地在因特网中兜圈子,因而白白消耗网络资源。最初的设计是以秒作为TTL 的单位。每经过一个路由器时,就把TTL 减去数据报在路由器消耗掉的一段时间。若数据报在路由器消耗的时间小于1 秒,就把TTL 值减1。当TTL 值为0 时,就丢弃这个数据报。后来把TTL 字段的功能改为“跳数限制”(但名称不变)。路由器在转发数据报之前就把TTL 值减1.若TTL 值减少到零,就丢弃这个数据报,不再转发。因此,TTL 的单位不再是秒,而是跳数。TTL 的意义是指明数据报在网络中至多可经过多少个路由器。显然,数据报在网络上经过的路由器的最大数值是255。若把TTL 的初始值设为1,就表示这个数据报只能在本局域网中传送。
  • (9) 协议:占8 位协议字段指出此数据报携带的数据是使用何种协议,以便使目的主机的IP 层知道应将数据部分上交给哪个处理过程。
  • (10) 首部检验和:占16 位这个字段只检验数据报的首部,但不包括数据部分。这是因为数据报每经过一个路由器,路由器都要重新计算一下首部检验和(一些字段,如生存时间、标志、片偏移等都可能发生变化)。不检验数据部分可减少计算的工作量。
  • (11) 源地址:占32 位。
  • (12) 目的地址:占32 位。
  • (13) 数据区域:这是IP 数据报的最后的一个字段,也是最重要的内容,lwIP 发送数据报是把该层的首部封装到数据包里面,在IP 层也是把IP 首部封装在其中,因为有数据区域才会有数据报首部的存在,在大多数情况下,IP 数据报中的数据字段包含要交付给目标IP 地址的运输层(TCP 协议或UDP 协议),当然数据区域也可承载其他类型的报文,如ICMP 报文等。

IP 数据报结构

在lwIP 中,为了描述IP 报文结构,它在ip4.h 文件中定义了一个ip_hdr 结构体来描述IP
数据报的内容,该结构体如下所示:

struct ip_hdr {
    
    
    /* 版本号+首部长度+服务类型*/
    PACK_STRUCT_FLD_8(u8_t _v_hl);
    /* 服务类型*/
    PACK_STRUCT_FLD_8(u8_t _tos);
    /* 总长度(IP首部+数据区) */
    PACK_STRUCT_FIELD(u16_t _len);
    /* 数据包标识(编号) */
    PACK_STRUCT_FIELD(u16_t _id);
    /* 标志+片偏移*/
    PACK_STRUCT_FIELD(u16_t _offset);
    /* IP首部标志定义*/
    #define IP_RF 0x8000 U /* 保留*/ # define IP_DF 0x4000 U /* 是否允许分片*/ # define IP_MF 0x2000 U /* 后续是否还有更多分片*/ # define IP_OFFMASK 0x1fff U /* 片偏移域掩码*/
    /* 生存时间(最大转发次数)+协议类型(IGMP:1、UDP:17、TCP:6) */
    PACK_STRUCT_FLD_8(u8_t _ttl);
    /* 协议*/
    PACK_STRUCT_FLD_8(u8_t _proto);
    /* 校验和(IP首部) */
    PACK_STRUCT_FIELD(u16_t _chksum);
    /* 源IP地址/目的IP地址*/
    PACK_STRUCT_FLD_S(ip4_addr_p_t src);
    PACK_STRUCT_FLD_S(ip4_addr_p_t dest);
}
PACK_STRUCT_STRUCT;
PACK_STRUCT_END

可以看出,此结构体的成员变量和上图9.2.1 的字段一一对应。

IP 数据报的分片解析

TCP/IP 协议栈为什么具备分片的概念,因为应用程序处理的数据是不确定的,可能超出
网络接口最大传输单元,为此TCP/IP 协议栈引入了分片概念,它是以MTU 为界限对这个大
型的数据切割成多个小型的数据包。这些小型的数据叫做IP 的分组和分片,它们在接收方进
行重组处理,这样,接收方的应用程序接收到这个大型的数据了。总的来讲,IP 数据报的分
片概念是为了解决IP 数据报数据过大的问题而诞生。注:以太网最大传输单元MTU 为1500

现在笔者举个示例,让大家更好的理解IP 分片的原理:

假设IP 数据报整体的大小为4000 字节,IP 首部默认为20 字节,而数据区域为3980。由
于以太网最大传输单元为1500,所以lwIP 内核会把这个数据报进行分片处理。

  1. 第一个IP 分片:
    分片数据大小:20(IP 首部)+ 1480(数据区域)。
    标识:888。
    标志:IP_MF = 1 后续还有分片。
    片偏移量:片偏移量是0,单位是8 字节,本片偏移量相当于0 字节。
  2. 第二片IP 数据报:
    分片数据大小:20(IP 首部)+ 1480(数据区域)。
    标识:888。
    标志:IP_MF = 1 后续还有分片。
    片偏移量:片偏移量是185(1480/8),单位是8 字节,本片偏移量相当于1480 字节。
  3. 第三片IP 数据报:
    分片数据大小:20(IP 首部)+ 1020(数据区域)。
    标识:888。
    标志:IP_MF = 0,后续没有分片。
    片偏移量:片偏移量是370(185+185),单位是8 字节,本片偏移量相当于2960 字节。

注:这些分片的标识都是一致的,而IP_MF 表示后续有没有分片,若IP_MF 为0,则这
个分片为最后一个分片。

在这里插入图片描述

从上图可以看出,一个大型的IP 数据包经过网络层处理,它会被分成两个或者两个以上的IP 分片,这些分片的数据组合起来就是应用程序发送的数据与传输层的首部。
至此,我们已经明白了IP 分片的原理,下面笔者讲解lwIP 内核如何实现这个原理,它的
实现函数为ip4_frag,该函数如下所示:

/**
* 如果IP数据报对netif来说太大,则将其分片,
将数据报切成MTU大小的块,然后按顺序发送通过将pbuf_ref指向p
* @param p:要发送的IP数据包
* @param netif:发送的netif
* @param dest:目的IP地址
* @return ERR_OK:发送成功, err_t:其他
*/
err_t
ip4_frag(struct pbuf * p, struct netif * netif,
    const ip4_addr_t * dest) {
    
    
    struct pbuf * rambuf;#
    if !LWIP_NETIF_TX_SINGLE_PBUF
    struct pbuf * newpbuf;
    u16_t newpbuflen = 0;
    u16_t left_to_copy;#
    endif
    struct ip_hdr * original_iphdr;
    struct ip_hdr * iphdr;
    /* (1500 - 20)/8 = 偏移185 */
    const u16_t nfb = (u16_t)((netif - > mtu - IP_HLEN) / 8);
    u16_t left, fragsize;
    u16_t ofo;
    int last;
    u16_t poff = IP_HLEN; /* IP头部长度*/
    u16_t tmp;
    int mf_set;
    original_iphdr = (struct ip_hdr * ) p - > payload; /* 指向数据报*/
    iphdr = original_iphdr;
    /* 判断IP头部是否为20 */
    if (IPH_HL_BYTES(iphdr) != IP_HLEN) {
    
    
        return ERR_VAL;
    }
    /* tmp变量获取标志和片偏移数值*/
    tmp = lwip_ntohs(IPH_OFFSET(iphdr));
    /* ofo = 片偏移*/
    ofo = tmp & IP_OFFMASK;
    /* mf_set = 分片标志*/
    mf_set = tmp & IP_MF;
    /* left = 总长度减去IP头部等于有效数据长度,4000 - 20 = 3980 */
    left = (u16_t)(p - > tot_len - IP_HLEN);
    /* 判断left是否为有效数据*/
    while (left) {
    
    
        /* 判断有效数据和偏移数据大小,fragsize = 1480 (3980 < 1480 ? 3980 : 1480) */
        fragsize = LWIP_MIN(left, (u16_t)(nfb * 8));
        /* rambuf申请20字节大小的内存块*/
        rambuf = pbuf_alloc(PBUF_LINK, IP_HLEN, PBUF_RAM);
        if (rambuf == NULL) {
    
    
            goto memerr;
        }
        /* 这个rambuf有效数据指针指向original_iphdr数据报*/
        SMEMCPY(rambuf - > payload, original_iphdr, IP_HLEN);
        /* iphdr指向有效区域地址rambuf->payload */
        iphdr = (struct ip_hdr * ) rambuf - > payload;
        /* left_to_copy = 偏移数据大小(1480) */
        left_to_copy = fragsize;
        while (left_to_copy) {
    
    
            struct pbuf_custom_ref * pcr;
            /* 当前pbuf中数据的长度,plen = 3980 - 20 = 3960 */
            u16_t plen = (u16_t)(p - > len - poff);
            /* newpbuflen = 1480 (1480 < 3960 ? 1480 : 3960) */
            newpbuflen = LWIP_MIN(left_to_copy, plen);
            if (!newpbuflen) {
    
    
                poff = 0;
                p = p - > next;
                continue;
            }
            /* pcr申请内存*/
            pcr = ip_frag_alloc_pbuf_custom_ref();
            if (pcr == NULL) {
    
    
                pbuf_free(rambuf);
                goto memerr;
            }
            /* newpbuf申请内存1480字节,
            保存了这个数据区域偏移poff字节的数据(p->payload + poff) */
            newpbuf = pbuf_alloced_custom(PBUF_RAW, newpbuflen, PBUF_REF, & pcr - > pc, (u8_t * ) p - > payload + poff, newpbuflen);
            if (newpbuf == NULL) {
    
    
                /* 释放内存*/
                ip_frag_free_pbuf_custom_ref(pcr);
                pbuf_free(rambuf);
                goto memerr;
            }
            /* 增加pbuf的引用计数*/
            pbuf_ref(p);
            pcr - > original = p;
            pcr - > pc.custom_free_function = ipfrag_free_pbuf_custom;
            /* 将它添加到rambuf的链的末尾*/
            pbuf_cat(rambuf, newpbuf);
            /* left_to_copy = 0 (1480 - 1480) */
            left_to_copy = (u16_t)(left_to_copy - newpbuflen);
            if (left_to_copy) {
    
    
                poff = 0;
                p = p - > next;
            }
        }
        /* poff = 1500 (20 + 1480) */
        poff = (u16_t)(poff + newpbuflen);
        /* last = 0 (3980 <= (1500 - 20)) */
        last = (left <= netif - > mtu - IP_HLEN);
        /* 设置新的偏移量和MF标志*/
        tmp = (IP_OFFMASK & (ofo));
        /* 判断是否是最后一个分片*/
        if (!last || mf_set) {
    
    
            /* 最后一个片段设置了MF为0 */
            tmp = tmp | IP_MF;
        }
        /* 分段偏移与标志字段*/
        IPH_OFFSET_SET(iphdr, lwip_htons(tmp));
        /* 设置数据报总长度= 1500 (1480 + 20) */
        IPH_LEN_SET(iphdr, lwip_htons((u16_t)(fragsize + IP_HLEN)));
        /* 校验为0 */
        IPH_CHKSUM_SET(iphdr, 0);
        /* 发送IP数据报*/
        netif - > output(netif, rambuf, dest);
        IPFRAG_STATS_INC(ip_frag.xmit);
        /* rambuf释放内存*/
        pbuf_free(rambuf);
        /* left = 2500 (3980 - 1480) */
        left = (u16_t)(left - fragsize);
        /* 片偏移ofo = 185(0 + 185) */
        ofo = (u16_t)(ofo + nfb);
    }
    MIB2_STATS_INC(mib2.ipfragoks);
    return ERR_OK;
    memerr:
        MIB2_STATS_INC(mib2.ipfragfails);
    return ERR_MEM;
}
MIB2_STATS_INC(mib2.ipfragoks);
return ERR_OK;
memerr:
    MIB2_STATS_INC(mib2.ipfragfails);
return ERR_MEM;
}

此函数非常简单,首先判断这个大型数据包的有效区域总长度,系统根据这个总长度划分
数据区域,接着申请20+sizeof(struct pbuf)字节的rampbuf 来存储IP 首部,然后根据poff 数值让被分片数据包的payload 指针偏移poff 大小,它所指向的地址由newpbuf 数据包的payload指针指向,最后调用netif->output 函数发送该分片,其他分片一样操作。

在这里插入图片描述

上图中,newpbuf 的payload 指针指向的地址由左边的payload 指针经过偏移得来的。

IP 数据报的分片重装

由于IP 分组在网络传输过程中到达目的地点的时间是不确定的,所以后面的分组可能比
前面的分组先达到目的地点。为此,lwIP 内核需要将接收到的分组暂存起来,等所有的分组
都接收完成之后,再将数据传递给上层。

在lwIP 中,有专门的结构体负责缓存这些分组,这个结构体为ip_reassdata 重装数据链表,
该结构体在ip4_frag.h 文件中定义,如下所示:

/* 重装数据结构体*/
struct ip_reassdata {
    
    
	struct ip_reassdata *next; /* 指向下一个重装节点*/
	struct pbuf *p; /* 指向分组的pbuf */
	struct ip_hdr iphdr; /* IP数据报的首部*/
	u16_t datagram_len; /* 已收到数据的长度*/
	u8_t flags; /* 标志是否最后一个分组*/
	u8_t timer; /* 超时间隔*/
};

这个结构体描述了同类型的IP 分组信息,同类型的IP 分组会挂载到该重装节点上,如下
图所示:

在这里插入图片描述

可以看到,这些分片挂载到同一个重装节点上,它们挂载之前,是把IP 首部的前8 字节
强制转换成三个字段,其中next_pbuf 指针用来链接这些IP 分组,形成了单向链表,而start
和end 字段用来描述分组的顺序,lwIP 系统根据这些数值对分组进行排序。

lwIP 内核的IP 重组功能由ip4_reass 函数实现,该函数的代码量比较长,这里笔者不深入
讲解了,我们会在视频当中讲解IP 重装流程。

IP 数据报的输出

无论是UDP 还是TCP,它们的数据段递交至网络层的接口是一致的,这个接口函数如下
所示:

err_t
ip4_output_if_src(struct pbuf * p,
    const ip4_addr_t * src,
        const ip4_addr_t * dest,
            u8_t ttl, u8_t tos,
            u8_t proto, struct netif * netif) {
    
    
    struct ip_hdr * iphdr;
    ip4_addr_t dest_addr;
    if (dest != LWIP_IP_HDRINCL) {
    
    
        u16_t ip_hlen = IP_HLEN;
        /* 第一步:生成IP报头*/
        if (pbuf_header(p, IP_HLEN)) {
    
    
            return ERR_BUF;
        }
        /* 第二步:iphdr 指向IP头部指针*/
        iphdr = (struct ip_hdr * ) p - > payload;
        /* 设置生存时间(最大转发次数) */
        IPH_TTL_SET(iphdr, ttl);
        /* 设置协议类型(IGMP:1、UDP:17、TCP:6) */
        IPH_PROTO_SET(iphdr, proto);
        /* 设置目的IP地址*/
        ip4_addr_copy(iphdr - > dest, * dest);
        /* 设置版本号+设置首部长度*/
        IPH_VHL_SET(iphdr, 4, ip_hlen / 4);
        /* 服务类型*/
        IPH_TOS_SET(iphdr, tos);
        /* 设置总长度(IP首部+数据区) */
        IPH_LEN_SET(iphdr, lwip_htons(p - > tot_len));
        /* 设置标志+片偏移*/
        IPH_OFFSET_SET(iphdr, 0);
        /* 设置数据包标识(编号) */
        IPH_ID_SET(iphdr, lwip_htons(ip_id));
        /* 每发送一个数据包,编号加一*/
        ++ip_id;
        /* 没有指定源IP地址*/
        if (src == NULL) {
    
    
            /* 将当前网络接口IP地址设置为源IP地址*/
            ip4_addr_copy(iphdr - > src, * IP4_ADDR_ANY4);
        } else {
    
    
            /* 复制源IP地址*/
            ip4_addr_copy(iphdr - > src, * src);
        }
    } else {
    
    
        /* IP头部已经包含在pbuf中*/
        iphdr = (struct ip_hdr * ) p - > payload;
        ip4_addr_copy(dest_addr, iphdr - > dest);
        dest = & dest_addr;
    }
    IP_STATS_INC(ip.xmit);
    ip4_debug_print(p);
    /* 如果数据包总长度大于MTU,则分片发送*/
    if (netif - > mtu && (p - > tot_len > netif - > mtu)) {
    
    
        return ip4_frag(p, netif, dest);
    }
    /* 如果数据包总长度不大于MTU,则直接发送*/
    return netif - > output(netif, p, dest);
}

此函数非常简单,这里笔者使用一个流程图来描述该函数的实现原理,如下图所示:

在这里插入图片描述

此函数首先判断目标IP 地址是否为NULL,若目标IP 地址不为空,则偏移payload 指针
添加IP 首部,偏移完成之后设置IP 首部字段信息,接着判断该数据包的总长度是否大于以太网传输单元,若大于,则调用ip4_frag 函数对这个数据包分组并且逐一发送,否则直接调用ethrap_output 函数把数据包递交给ARP 层处理。

IP 数据报的输入

数据包提交给网络层之前,系统需要判断接收到的数据包是IP 数据包还是ARP 数据包,
若接收到的是IP 数据包,则lwIP 内核调用ip4_input 函数处理这个数据包,该函数如下所示:

err_t
ip4_input(struct pbuf * p, struct netif * inp) {
    
    
        struct ip_hdr * iphdr;
        struct netif * netif;
        u16_t iphdr_hlen;
        u16_t iphdr_len;#
        if IP_ACCEPT_LINK_LAYER_ADDRESSING || LWIP_IGMP
        int check_ip_src = 1;#
        endif /* IP_ACCEPT_LINK_LAYER_ADDRESSING || LWIP_IGMP */
        IP_STATS_INC(ip.recv);
        MIB2_STATS_INC(mib2.ipinreceives);
        /* 识别IP报头*/
        iphdr = (struct ip_hdr * ) p - > payload;
        /* 第一步:判断版本是否为IPv4 */
        if (IPH_V(iphdr) != 4) {
    
    
            ip4_debug_print(p);
            pbuf_free(p); /* 释放空间*/
            IP_STATS_INC(ip.err);
            IP_STATS_INC(ip.drop);
            MIB2_STATS_INC(mib2.ipinhdrerrors);
            return ERR_OK;
        }
        /* 以4字节(32位)字段获得IP头的长度*/
        iphdr_hlen = IPH_HL(iphdr);
        /* 以字节计算IP报头长度*/
        iphdr_hlen *= 4;
        /* 以字节为单位获取ip长度*/
        iphdr_len = lwip_ntohs(IPH_LEN(iphdr));
        /* 修剪pbuf。这对于< 60字节的数据包尤其需要。*/
        if (iphdr_len < p - > tot_len) {
    
    
            pbuf_realloc(p, iphdr_len);
        }
        /* 第二步:标头长度超过第一个pbuf 长度,或者ip 长度超过总pbuf 长度*/
        if ((iphdr_hlen > p - > len) || (iphdr_len > p - > tot_len) || (iphdr_hlen < IP_HLEN)) {
    
    
            if (iphdr_hlen < IP_HLEN) {
    
    }
            if (iphdr_hlen > p - > len) {
    
    }
            if (iphdr_len > p - > tot_len) {
    
    }
            /* 释放空间*/
            pbuf_free(p);
            IP_STATS_INC(ip.lenerr);
            IP_STATS_INC(ip.drop);
            MIB2_STATS_INC(mib2.ipindiscards);
            return ERR_OK;
        }
        /* 第三步:验证校验和*/
        #if CHECKSUM_CHECK_IP
        /* 省略代码*/
        #endif
        /* 将源IP 地址与目标IP 地址复制到对齐的ip_data.current_iphdr_src和
        ip_data.current_iphdr_dest */
        ip_addr_copy_from_ip4(ip_data.current_iphdr_dest, iphdr - > dest);
        ip_addr_copy_from_ip4(ip_data.current_iphdr_src, iphdr - > src);
        /* 第四步:匹配数据包和接口,即这个数据包是否发给本地*/
        if (ip4_addr_ismulticast(ip4_current_dest_addr())) {
    
    #
            if LWIP_IGMP
            /* 省略代码*/
            #else /* LWIP_IGMP */
            /* 如果网卡已经挂载了和IP 地址有效*/
            if ((netif_is_up(inp)) && (!ip4_addr_isany_val( * netif_ip4_addr(inp)))) {
    
    
                netif = inp;
            } else {
    
    
                netif = NULL;
            }
            #endif /* LWIP_IGMP */
        }
        /* 如果数据报不是发给本地*/
        else {
    
    
            int first = 1;
            netif = inp;
            do {
    
    
                /* 接口已启动并配置? */
                if ((netif_is_up(netif)) &&
                    (!ip4_addr_isany_val( * netif_ip4_addr(netif)))) {
    
    
                    /* 单播到此接口地址? */
                    if (ip4_addr_cmp(ip4_current_dest_addr(),
                            netif_ip4_addr(netif)) ||
                        /* 或广播在此接口网络地址? */
                        ip4_addr_isbroadcast(ip4_current_dest_addr(), netif)# if LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF || (ip4_addr_get_u32(ip4_current_dest_addr()) ==
                            PP_HTONL(IPADDR_LOOPBACK))# endif /* LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF */
                    ) {
    
    
                        break;
                    }
                    #if LWIP_AUTOIP
                    if (autoip_accept_packet(netif, ip4_current_dest_addr())) {
    
    
                        /* 跳出if循环*/
                        break;
                    }
                    #endif /* LWIP_AUTOIP */
                }
                if (first) {
    
    
                	#if !LWIP_NETIF_LOOPBACK || LWIP_HAVE_LOOPIF
                        /* 检查一下目标IP 地址是否是环回地址*/
                    if (ip4_addr_isloopback(ip4_current_dest_addr())) {
    
    
                        netif = NULL;
                        break;
                    }
                    #endif /* !LWIP_NETIF_LOOPBACK || LWIP_HAVE_LOOPIF */
                    first = 0;
                    netif = netif_list;
                } else {
    
    
                    netif = netif - > next;
                }
                if (netif == inp) {
    
    
                    netif = netif - > next;
                }
            } while (netif != NULL);
        }
        #if IP_ACCEPT_LINK_LAYER_ADDRESSING
        if (netif == NULL) {
    
    
            /* 远程端口是DHCP服务器? */
            if (IPH_PROTO(iphdr) == IP_PROTO_UDP) {
    
    
                struct udp_hdr * udphdr = (struct udp_hdr * )
                    ((u8_t * ) iphdr + iphdr_hlen);
                if (IP_ACCEPT_LINK_LAYER_ADDRESSED_PORT(udphdr - > dest)) {
    
    
                    netif = inp;
                    check_ip_src = 0;
                }
            }
        }
        #endif /* IP_ACCEPT_LINK_LAYER_ADDRESSING */ 
        #if LWIP_IGMP || IP_ACCEPT_LINK_LAYER_ADDRESSING
        if (check_ip_src#
            if IP_ACCEPT_LINK_LAYER_ADDRESSING && !ip4_addr_isany_val( * ip4_current_src_addr())# endif /* IP_ACCEPT_LINK_LAYER_ADDRESSING */
        )
        # endif /* LWIP_IGMP || IP_ACCEPT_LINK_LAYER_ADDRESSING */ {
      
      
                /* 第五步:IP 地址,源IP 地址不能是多播或者广播地址*/
                if ((ip4_addr_isbroadcast(ip4_current_src_addr(), inp)) ||
                    (ip4_addr_ismulticast(ip4_current_src_addr()))) {
    
    
                    /* 释放空间*/
                    pbuf_free(p);
                    IP_STATS_INC(ip.drop);
                    MIB2_STATS_INC(mib2.ipinaddrerrors);
                    MIB2_STATS_INC(mib2.ipindiscards);
                    return ERR_OK;
                }
            }
            /* 第六步:如果还没找到对应的网卡,数据包不是给我们的*/
        if (netif == NULL) {
    
    
            /* 路由转发或者丢弃。如果IP_FORWARD 宏定义被使能,则进行转发*/
            #if IP_FORWARD
            /* 非广播包?*/
            if (!ip4_addr_isbroadcast(ip4_current_dest_addr(), inp)) {
    
    
                /* 尝试在(其他)网卡上转发IP 数据包*/
                ip4_forward(p, iphdr, inp);
            } else# endif /* IP_FORWARD */ {
    
    
                    IP_STATS_INC(ip.drop);
                    MIB2_STATS_INC(mib2.ipinaddrerrors);
                    MIB2_STATS_INC(mib2.ipindiscards);
                }
                /* 释放空间*/
            pbuf_free(p);
            return ERR_OK;
        }
        /* 第七步:如果数据报由多个片段组成(分片处理)?*/
        if ((IPH_OFFSET(iphdr) & PP_HTONS(IP_OFFMASK | IP_MF)) != 0) {
    
    
            /* 重装数据报*/
            p = ip4_reass(p);
            /* 如果重装没有完成*/
            if (p == NULL) {
    
    
                return ERR_OK;
            }
            /* 分片重装完成,将数据报首部强制转换为ip_hdr 类型*/
            iphdr = (struct ip_hdr * ) p - > payload;
        }#
        if IP_OPTIONS_ALLOWED == 0#
        if LWIP_IGMP
        if ((iphdr_hlen > IP_HLEN) && (IPH_PROTO(iphdr) != IP_PROTO_IGMP)) {
    
    #
            else
            /* 第八步:如果IP 数据报首部长度大于20 字节,就表示错误*/
            if (iphdr_hlen > IP_HLEN) {
    
    #
                endif /* LWIP_IGMP */
                /* 释放空间*/
                pbuf_free(p);
                IP_STATS_INC(ip.opterr);
                IP_STATS_INC(ip.drop);
                /* u不受支持的协议特性*/
                MIB2_STATS_INC(mib2.ipinunknownprotos);
                return ERR_OK;
            }#
            endif /* IP_OPTIONS_ALLOWED == 0 */
            /* 第九步:发送到上层协议*/
            ip4_debug_print(p);
            ip_data.current_netif = netif;
            ip_data.current_input_netif = inp;
            ip_data.current_ip4_header = iphdr;
            ip_data.current_ip_header_tot_len = IPH_HL(iphdr) * 4;#
            if LWIP_RAW
            /* RAW API 输入*/
            if (raw_input(p, inp) == 0)# endif /* LWIP_RAW */ {
    
    
                    /* 转移到有效载荷(数据区域),不需要检查*/
                    pbuf_header(p, -(s16_t) iphdr_hlen);
                    /* 根据IP 数据报首部的协议的类型处理*/
                    switch (IPH_PROTO(iphdr)) {
    
    #
                        if LWIP_UDP
                        /* UDP协议*/
                        case IP_PROTO_UDP:
                            #if LWIP_UDPLITE
                        case IP_PROTO_UDPLITE:
                            #endif /* LWIP_UDPLITE */
                            MIB2_STATS_INC(mib2.ipindelivers);
                            /* IP层递交给网络层的函数*/
                            udp_input(p, inp);
                            break;#
                            endif /* LWIP_UDP */ #
                            if LWIP_TCP
                            /* TCP协议*/
                        case IP_PROTO_TCP:
                            MIB2_STATS_INC(mib2.ipindelivers);
                            /* IP层递交给网络层的函数*/
                            tcp_input(p, inp);
                            break;#
                            endif /* LWIP_TCP */
                            pbuf_free(p); /* 释放空间*/
                            IP_STATS_INC(ip.proterr);
                            IP_STATS_INC(ip.drop);
                            MIB2_STATS_INC(mib2.ipinunknownprotos);
                    }
                }
                /* 全局变量清零*/
            ip_data.current_netif = NULL;
            ip_data.current_input_netif = NULL;
            ip_data.current_ip4_header = NULL;
            ip_data.current_ip_header_tot_len = 0;
            ip4_addr_set_any(ip4_current_src_addr());
            ip4_addr_set_any(ip4_current_dest_addr());
            return ERR_OK;
        }

上述的源码篇幅很长,也不容易理解,下面笔者把上述的源码分成十步来讲解:
第一步:判断IP 数据报的版本是否是IPv4,如果不是,那么lwIP 会掉弃该数据报。
第二步:判断标头长度超过第一个pbuf 长度,或者ip 长度超过总pbuf 长度,如果是,那
么lwIP 会丢弃该数据报。
第三步:验证校验和,如果不正确,那么lwIP 会掉弃该数据报。
第四步:匹配数据包和接口,这个数据包是否发给本地。
第五步:判断IP 数据报是否是广播或者多播,如果是,那么lwIP 会丢弃该数据报。
第六步:如果到了这一步,没有发现网络接口,那么lwIP 会丢弃该数据报。
第七步:如果如IP 数据报不能分片处理,那么lwIP 会丢弃该数据报。
第八步:如果IP 数据报的IP 首部大于20 字节,那么lwIP 会丢弃该数据报。
第九步:把数据包递交给上层。
第十步:判断该数据报的协议为TCP/UDP/ICMP/IGMP,如果不是这四个协议,则丢弃该
数据报。

ICMP 协议

ICMP(Internet Control Message Protocol)Internet 控制报文协议。它是TCP/IP 协议簇的
一个子协议,用于在IP 主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是
否可达、路由是否可用等网络本身的消息,这些控制消息虽然并不传输到用户数据,但是对于用户数据的传递起着重要的作用。

ICMP 协议简介

IP 协议虽然是TCP/IP 协议中的核心部分,但是它是一种无连接的不可靠数据报交付,这
个协议本身没有任何错误检验和恢复机制,为了弥补IP 协议中的缺陷,ICMP 协议登场了,
ICMP 协议是一种面向无连接的协议,用于传输出错报告控制信息。它是一个非常重要的协议,它对于网络安全具有极其重要的意义。

它属于网络层协议,主要用于在主机与路由器之间传递控制信息,包括报告错误、交换受限控制和状态信息等。当遇到IP 数据无法访问目标、IP 路由器无法按当前的传输速率转发数据包等情况时,会自动发送ICMP 消息。

ICMP 协议用于IP 主机、路由器之间递交控制消息,在网络中,控制消息分为很多种,例
如数据报错信息、网络状况信息和主句状况信息等,虽然这些信息不会递交给用户数据,但对于用户来说数据报有效性得到提高。

ICMP 应用场景

IP 协议本身不提供差错报告和差错控制机制来保证数据报递交的有效性,如果在路由器
无法递交一个数据报或者数据报生存时间为0 时,那么路由器会直接掉弃这个数据报,虽然
IP 层这样处理是合理的,但是对于源主机来说,比较希望得到数据报递交过程中出现异常相
关信息,以便重新递交数据报或者其他处理。

IP 协议不能进行主机管理与查询机制,简单来说:不知道对方主机或者路由器的活跃,
对于不活跃的主机和路由器就没有必要发送数据报,所以对于主机管理员来说:更希望得到对方主机和路由器的信息,这样可以根据相关的信息对自身配置、数据报发送控制。

为了解决上述的两个问题,TCP/IP 设计人员在协议上引入了特殊用途报文,这个报文为
网际报文控制协议简称ICMP,从TCP/IP 的协议结构来看,它是和IP’协议一样,都是处于网
络层,但是ICMP 协议有自己一套报文结构,这样数据报就变成了IP 首部+ICMP 首部+数据
区域,ICMP 协议不为任何的应用程序服务,它的目的是目的主机的网络层处理软件。

ICMP 报文类型

在没有引入ICMP 报文之前,IP 数据报一般分为IP 首部+IP 数据区域,现在添加了ICMP
协议,则IP 数据报分为IP 首部+ICMP 首部+数据区域。ICMP 报文分为两类:一类是ICMP 差错报告报文,另一类是ICMP 查询报文,这两类报文分别解决上小节的两个问题。

①ICMP 差错报告报文主要用来向IP 数据报源主机返回一个差错报告信息,这个信息就
是判断路由器和主机对当前的数据报进行正常处理,例如无法将数据报递交给上层处理,或者数据报因为生存时间而被删除。

②ICMP 查询报文用于一台主机向另一台主机查询特定的信息,这个类型的报文是成对出
现的,例如源主机发送查询报文,当目标主机收到该报文之后,它会根据查询报文的约定的格式为源主机放回应答报文。

ICMP 差错报告报文和ICMP 查询报文常见类型如下表所示:

在这里插入图片描述
注:lwIP 只实现差错报文的类型3 和11,而查询报文只处理回显请求。

ICMP 报文结构

ICMP 报文有8 字节首部和可变长度的数据部分组成,因为ICMP 有两种类型的报文,其
中不同的报文其首部的格式也会有点差异,当然也有通用的地方,例如首部的前4 个字节是通用的,ICMP 报文结构如下图所示:

在这里插入图片描述

类型字段:表示使用ICMP 的两类类型中的哪一个。
代码字段:产生ICMP 报文的具体原因。
校验和字段:用于记录包括ICMP 报文数据部分在内的整个ICMP 数据报的校验和。
首部剩余的4 字节在每种类型的报文有特殊的定义,总的看来说:不同类型的报文,数据
部分长度和含义存在差异,例如差错报文会引起差错的据报的信息,而查询报文携带查询请求和查询结果数据。

  1. ICMP 差错报文
    (1) 目的站不可到达
    当路由器发送的数据报不能发送到指定目的地时,或者说当路由器不能够给数据报找到路由或主机不能够交付数据报时,就丢弃这个数据报,然后向发送数据报的源主机设备发回一个终点不可达数据报文。如下图所示:

在这里插入图片描述

举个例子:主机A 给主机B 发送一个数据报,在网络中传输时中间可能要经过很多台路
由器,主机A 先把这个数据报发送给路由器,路由器收到这个数据报后,此时路由R1 发生了故障,它不知道这个数据报下一步该发给哪个路由设备或者那台主机设备,也就是说这个数据报不能发送到目的地主机B,这时路由器会把这个数据报丢弃并向主机A 发回一个终点不可达的数据报文。

ICMP 目的不可达差错报告报文产生差错的原因有很多,如网络不可达、主机不可达、协
议不可达、端口不可达等,引起差错的原因会在ICMP 报文中的代码字段(Code)记录。对
于不同的差错代码字段的值是不一样的,但是lwIP 实现的只有前6 种,如下图所示:

在这里插入图片描述

当然ICMP 目的不可达报文首部剩下的4 字节是未使用,而ICMP 报文数据区装载了IP
数据报首部及IP 数据报的数据区域前8 字节,为什么需要装载IP 数据报的数据区域中前8 个
字节的数据呢?因为IP 数据报的数据区域前8 个字节刚好覆盖了传输层协议中的端口号字段,而IP 数据报首部就拥有目标IP 地址与源IP 地址,当源主机收到这样子的ICMP 报文后,它能根据ICMP 报文的数据区域判断出是哪个数据包出现问题,并且IP 层能够根据端口号将报文传递给对应的上层协议处理,差错报文结构如下图所示:

在这里插入图片描述

可以看出:首部剩下的4 个字节是未使用的,而数据区域保存的是引起差错IP 首部和引
起差错数据包的数据区域前8 字节数据。准确来说,就是把引起差错IP 数据包的IP 部和数据
区域的前8 字节数据拷贝到差错报文的数据区域。

(2) 源站抑制
由于IP 协议是面向无连接的,没有流量控制机制,数据在传输过程中是非常容易造成拥
塞的现象。而ICMP 源点抑制报文就是给IP 协议提供一种流量监控的机制,因为ICMP 源点
抑制机制并不能控制流量的大小,但是能根据流量的使用情况,给源主机提供一些建议。这个报文的作用就是通知数据报在拥塞时被丢弃了,另外还会警告源主机流量出现了拥塞的情况,然后源主机根据反馈的ICMP 源点抑制报文信息作出处理,至于源主机怎么就不关它的事了。

如下图所示:

在这里插入图片描述

(3) 端口不可达
当目标系统收到一个IP 数据报的某个服务请求时,如果本地没有此服务,则本地会向源
头返回ICMP 端口不可达信息。常见的端口不可达有:主机A 向主机B 发起一个ftp 的传输请
求,从主机B 传输一个文件到主机A,由于主机B 设备没有开启ftp 服务的69 端口,因此主
机A 在请求主机B 时,会收到主机B 回复的一个ICMP 端口不可达的差错报文。

(4) 超时
ICMP 差错报告报文主要在以下几种情况中,会发送ICMP 超时报文:

  1. 当路由器接收到的数据报的TTL 生命周期字段值为0 时,路由器会把该数据报丢弃掉,
    并向源主机发回一个ICMP 超时报文。
  2. 另外,当目标主机在规定时间内没有收到所有的数据分片时,会把已经收到的所有数据
    分片丢弃,并向源主机发回一个ICMP 超时报文。在超时报文中,代码0 只能给路由器使用,表示生存周期字段值为0,代码1 只能给目的主机使用,它表示在规定的时间内,目的主机没有收到所有的数据分片。

(5) 参数错误
当数据报在因特网上传送时,在其首部中出现的任何二义性或者首部字段值被修改都可能
会产生非常严重的问题。如果路由器或目的主机发现了这种二义性,或在数据报的某个字段中缺少某个值,就丢弃这个数据报,并回送参数问题报文。

  1. ICMP 查询报文
    ping 程序利用ICMP 回显请求报文和回显应答报文(而不用经过传输层)来测试目标主机是否可达。它是一个检查系统连接性的基本诊断工具。
    ICMP 回显请求和ICMP 回显应答报文是配合工作的。当源主机向目标主机发送了ICMP
    回显请求数据包后,它期待着目标主机的回答。目标主机在收到一个ICMP 回显请求数据包后,它会交换源、目的主机的地址,然后将收到的ICMP 回显请求数据包中的数据部分原封不动地封装在自己的ICMP 回显应答数据包中,然后发回给发送ICMP 回显请求的一方。如果校验正确,发送者便认为目标主机的回显服务正常,也即物理连接畅通。查询报文结构如下图所示:

在这里插入图片描述

类型字段是指请求报文(8)和回答报文(0),代码段在ICMP 查询报文没有特殊取值,
其值为0,首部中的标识符和序号在ICMP 中没有正式定义该值的范围,所以发送方可以自由定义这两个字段,可以用来记录源主机发送出去的请求报文编号。数据可选区域标识回送请求报文包含数据和长度是可选的,发送放应该选择适合的长度和填充数据。在接收方它可以根据这个回送请求产生一个回送回答报文,回送报文的数据与回送请求报文的数据是相同的。

ICMP 的实现

我们可以总结一下ICMP 协议的作用,ICMP 协议是IP 协议的辅助协议,为什么ICMP 协
议是IP 协议的辅助协议呢?由于IP 协议本身不提供差错报告和差错控制机制来保证数据报递交的有效性和进行主机管理与查询机制,简单来说:ICMP 协议为了解决IP 协议的缺陷而诞生的,ICMP 报文分为差错报文和查询报文,这两个报文分别解决IP 协议的两大缺陷,本小节主要讲解lwIP 是怎么样实现ICMP 协议发送及处理的。

ICMP 数据结构体

在讲述IP 协议时,它是有自己的数据结构,同样ICMP 也有它自己的数据结构icmp_echo
_hdr,该数据结构在lwIP 的icmp.h 文件中定义,该结构体如下源码所示:

PACK_STRUCT_BEGIN
struct icmp_echo_hdr {
    
    
	PACK_STRUCT_FLD_8(u8_t type); /* ICMP类型*/
	PACK_STRUCT_FLD_8(u8_t code); /* ICMP代码号*/
	PACK_STRUCT_FIELD(u16_t chksum); /* ICMP校验和*/
	PACK_STRUCT_FIELD(u16_t id); /* ICMP的标识符*/
	PACK_STRUCT_FIELD(u16_t seqno); /* 序号*/
} PACK_STRUCT_STRUCT;
PACK_STRUCT_END

此外lwIP 还定义了很多宏与枚举类型的变量对ICMP 的类型及代码字段进行描述,如下
源码所示:

#define ICMP_ER 0 /* 回送应答*/ # define ICMP_DUR 3 /* 目标不可达*/ # define ICMP_SQ 4 /* 源站抑制*/ # define ICMP_RD 5 /* 重定向*/ # define ICMP_ECHO 8 /* 回送*/ # define ICMP_TE 11 /* 超时*/ # define ICMP_PP 12 /* 参数问题*/ # define ICMP_TS 13 /* 时间戳*/ # define ICMP_TSR 14 /* 时间戳应答*/ # define ICMP_IRQ 15 /* 信息请求*/ # define ICMP_IR 16 /* 信息应答*/ # define ICMP_AM 17 /* 地址掩码请求*/ # define ICMP_AMR 18 /* 地址掩码应答*/
/* ICMP目标不可到达的代码*/
enum icmp_dur_type {
    
    
    /* 网络不可到达*/
    ICMP_DUR_NET = 0,
        /* 主机不可达*/
        ICMP_DUR_HOST = 1,
        /* 协议不可到达*/
        ICMP_DUR_PROTO = 2,
        /* 端口不可达*/
        ICMP_DUR_PORT = 3,
        /* 需要进行分片但设置不分片比特*/
        ICMP_DUR_FRAG = 4,
        /* 源路由失败*/
        ICMP_DUR_SR = 5
};
/* ICMP时间超时代码*/
enum icmp_te_type {
    
    
    /* 在运输过程中超出了生存时间*/
    ICMP_TE_TTL = 0,
        /* 分片重组时间超时*/
        ICMP_TE_FRAG = 1
};

可以看出,这些宏定义描述了ICMP 数据报文的类型字段,下面的icmp_dur_type 和
icmp_te_type 枚举用来描述ICMP 数据报文的代码字段,它们分别为目的不可到达和超时差错报文。

lwIP 的作者为了快速读取和填写ICMP 报文首部,在icmp.h 文件还定义了ICMP 报文首
部的宏定义,如下源码所示:

#define ICMPH_TYPE(hdr) ((hdr)->type) /* 读取类型字段*/
#define ICMPH_CODE(hdr) ((hdr)->code) /* 读取代码字段*/
#define ICMPH_TYPE_SET(hdr, t) ((hdr)->type = (t)) /* 填写类型字段*/
#define ICMPH_CODE_SET(hdr, c) ((hdr)->code = (c)) /* 填写代码字段*/

使用这些宏定义能快速设置ICMP 各个字段的数值。

发送ICMP 差错报文

lwIP 只实现目的不可到达和超时差错报文,它们的实现函数分别为icmp_dest_unreach 和i
cmp_time_exceeded,这两个函数转入的参数与icmp_dur_type 和icmp_te_type 枚举相关。如目的不可到达报文的代码字段由icmp_dur_type 枚举描述,而超时报文的代码字段由icmp_te_type 枚举描述。

打开icmp.c 文件查看icmp_dest_unreach 和icmp_time_exceeded 这两
个函数,如下所示:

/* 发送目标不可达报文,该函数实际调用函数
icmp_send_response来发送ICMP差错报文
ICMP_DUR 为目的不可到达*/
void
icmp_dest_unreach(struct pbuf * p, enum icmp_dur_type t) {
    
    
        MIB2_STATS_INC(mib2.icmpoutdestunreachs);
        icmp_send_response(p, ICMP_DUR, t);
    }
    /* 发送超时报文,该函数实际调用函数
    icmp_send_response来发送ICMP差错报文
    ICMP_TE 为超时*/
void
icmp_time_exceeded(struct p buf * p, enum icmp_te_type t) {
    
    
    MIB2_STATS_INC(mib2.icmpouttimeexcds);
    icmp_send_response(p, ICMP_TE, t);
}

从上述源码可以看出,差错报文的类型已经固定为目的不可到达或者超时,它们唯一不同
的是差错报文的代码值,这个代码值就是由icmp_dur_type 和icmp_te_type 枚举定义的,最后调用相同的icmp_send_response 函数发送差错报文,这个发送函数如下所示:

static void
icmp_send_response(struct pbuf * p, u8_t type, u8_t code) {
    
    
    struct pbuf * q;
    struct ip_hdr * iphdr;
    struct icmp_echo_hdr * icmphdr;
    ip4_addr_t iphdr_src;
    struct netif * netif;
    MIB2_STATS_INC(mib2.icmpoutmsgs);
    /* 为差错报文申请pbuf,pbuf预留以太网首部和ip首部,
    申请数据长度为icmp首部长度+icmp数据长度(ip首部长度+8) */
    q = pbuf_alloc(PBUF_IP, sizeof(struct icmp_echo_hdr) + IP_HLEN +
        ICMP_DEST_UNREACH_DATASIZE, PBUF_RAM);
    if (q == NULL) {
    
    
        MIB2_STATS_INC(mib2.icmpouterrors);
        return;
    }
    /* 指向IP 数据报首部*/
    iphdr = (struct ip_hdr * ) p - > payload;
    /* 指向带填写的icmp首部*/
    icmphdr = (struct icmp_echo_hdr * ) q - > payload;
    /* 填写类型字段*/
    icmphdr - > type = type;
    /* 填写代码字段*/
    icmphdr - > code = code;
    icmphdr - > id = 0;
    icmphdr - > seqno = 0;
    /* 从原始数据包中复制字段,IP 数据报首部+8 字节的数据区域*/
    SMEMCPY((u8_t * ) q - > payload + sizeof(struct icmp_echo_hdr), (u8_t * ) p - > payload,
        IP_HLEN + ICMP_DEST_UNREACH_DATASIZE);
    /* 得到源IP 地址*/
    ip4_addr_copy(iphdr_src, iphdr - > src);
    /* 判断是否同一网段*/
    netif = ip4_route( & iphdr_src);
    if (netif != NULL) {
    
    
        /* 计算校验和*/
        icmphdr - > chksum = 0;
        ICMP_STATS_INC(icmp.xmit);
        /* 发送ICMP差错报文*/
        ip4_output_if(q, NULL, & iphdr_src, ICMP_TTL, 0, IP_PROTO_ICMP, netif);
    }
    /* 释放icmp pbuf */
    pbuf_free(q);
}

可以看到,此函数申请了一个pbuf 内存,它的数据区域存储了ICMP 首部,接着对这个
首部各个字段设置数值,然后在ICMP 首部后面添加引起差错数据包的IP 首部和引起差错的
前8 字节数据区域,这样lwIP 内核构建差错报文完成,最后调用ip4_output_if 函数发送该差
错报文。

ICMP 报文处理

IP 层把数据报递交至传输层之前,lwIP 内核会判断IP 首部的上层协议字段,若这个上层
协议字段不为TCP 和UDP,则该数据报不会递交给传输层处理;若上层协议字段为ICMP,
则该数据报递交给icmp_input 函数处理,该函数如下所示:

void
icmp_input(struct pbuf * p, struct netif * inp) {
    
    
    u8_t type;
    struct icmp_echo_hdr * iecho;
    const struct ip_hdr * iphdr_in;
    u16_t hlen;
    const ip4_addr_t * src;
    ICMP_STATS_INC(icmp.recv);
    MIB2_STATS_INC(mib2.icmpinmsgs);
    iphdr_in = ip4_current_header();
    hlen = IPH_HL_BYTES(iphdr_in);
    /* 判断IP首部的大小*/
    if (hlen < IP_HLEN) {
    
    
        goto lenerr;
    }
    /* 判断pbud的大小*/
    if (p - > len < sizeof(u16_t) * 2) {
    
    
        goto lenerr;
    }
    /* 获取ICMP的类型字段*/
    type = * ((u8_t * ) p - > payload);
    switch (type) {
    
    
        case ICMP_ER:
            /* 回送应答*/
            MIB2_STATS_INC(mib2.icmpinechoreps);
            break;
        case ICMP_ECHO:
            /* 回送*/
            MIB2_STATS_INC(mib2.icmpinechos);
            src = ip4_current_dest_addr();
            /* 判断是否为多播*/
            if (ip4_addr_ismulticast(ip4_current_dest_addr())) {
    
    
                goto icmperr;
            }
            /* 判断是否为广播*/
            if (ip4_addr_isbroadcast(ip4_current_dest_addr(),
                    ip_current_netif())) {
    
    
                goto icmperr;
            }
            if (p - > tot_len < sizeof(struct icmp_echo_hdr)) {
    
    
                goto lenerr;
            }
            if (pbuf_header(p, (s16_t)(hlen + PBUF_LINK_HLEN +
                    PBUF_LINK_ENCAPSULATION_HLEN))) {
    
    
                struct pbuf * r;
                r = pbuf_alloc(PBUF_LINK, p - > tot_len + hlen, PBUF_RAM);
                if (r == NULL) {
    
    
                    goto icmperr;
                }
                if (r - > len < hlen + sizeof(struct icmp_echo_hdr)) {
    
    
                    pbuf_free(r);
                    goto icmperr;
                }
                MEMCPY(r - > payload, iphdr_in, hlen);
                if (pbuf_header(r, (s16_t) - hlen)) {
    
    
                    pbuf_free(r);
                    goto icmperr;
                }
                if (pbuf_copy(r, p) != ERR_OK) {
    
    
                    pbuf_free(r);
                    goto icmperr;
                }
                pbuf_free(p);
                p = r;
            } else {
    
    
                if (pbuf_header(p, -(s16_t)(hlen + PBUF_LINK_HLEN +
                        PBUF_LINK_ENCAPSULATION_HLEN))) {
    
    
                    goto icmperr;
                }
            }
            /* 强制将数据区域转换为ICMP 报文首部*/
            iecho = (struct icmp_echo_hdr * ) p - > payload;
            if (pbuf_header(p, (s16_t) hlen)) {
    
    } else {
    
    
                err_t ret;
                struct ip_hdr * iphdr = (struct ip_hdr * ) p - > payload;
                /* 拷贝源IP 地址*/
                ip4_addr_copy(iphdr - > src, * src);
                /* 拷贝目标IP 地址*/
                ip4_addr_copy(iphdr - > dest, * ip4_current_src_addr());
                /* 填写报文类型*/
                ICMPH_TYPE_SET(iecho, ICMP_ER);
                iecho - > chksum = 0;
                /* 设置正确的TTL并重新计算头校验和。*/
                IPH_TTL_SET(iphdr, ICMP_TTL);
                IPH_CHKSUM_SET(iphdr, 0);
                ICMP_STATS_INC(icmp.xmit);
                MIB2_STATS_INC(mib2.icmpoutmsgs);
                MIB2_STATS_INC(mib2.icmpoutechoreps);
                /* 发送一个应答ICMP数据包*/
                ret = ip4_output_if(p, src, LWIP_IP_HDRINCL,
                    ICMP_TTL, 0, IP_PROTO_ICMP, inp);
                if (ret != ERR_OK) {
    
    }
            }
            break;
        default:
            /* 对于其他类型的报文,直接丢掉*/
            if (type == ICMP_DUR) {
    
    
                MIB2_STATS_INC(mib2.icmpindestunreachs);
            } else if (type == ICMP_TE) {
    
    
                MIB2_STATS_INC(mib2.icmpintimeexcds);
            } else if (type == ICMP_PP) {
    
    
                MIB2_STATS_INC(mib2.icmpinparmprobs);
            } else if (type == ICMP_SQ) {
    
    
                MIB2_STATS_INC(mib2.icmpinsrcquenchs);
            } else if (type == ICMP_RD) {
    
    
                MIB2_STATS_INC(mib2.icmpinredirects);
            } else if (type == ICMP_TS) {
    
    
                MIB2_STATS_INC(mib2.icmpintimestamps);
            } else if (type == ICMP_TSR) {
    
    
                MIB2_STATS_INC(mib2.icmpintimestampreps);
            } else if (type == ICMP_AM) {
    
    
                MIB2_STATS_INC(mib2.icmpinaddrmasks);
            } else if (type == ICMP_AMR) {
    
    
                MIB2_STATS_INC(mib2.icmpinaddrmaskreps);
            }
            ICMP_STATS_INC(icmp.proterr);
            ICMP_STATS_INC(icmp.drop);
    }
    pbuf_free(p);
    return;
    lenerr:
        pbuf_free(p);
    ICMP_STATS_INC(icmp.lenerr);
    MIB2_STATS_INC(mib2.icmpinerrors);
    return;
    icmperr:
        pbuf_free(p);
    ICMP_STATS_INC(icmp.err);
    MIB2_STATS_INC(mib2.icmpinerrors);
    return;
}

可以看出,lwIP 接收到回显请求报文时,系统会把这个回显请求报文的ICMP 类型字段修
改为0(回显应答类型),接着偏移payload 指针添加IP 首部并设置IP 首部的各个字段,最后调用ip4_output_if 函数发送这个回显应答报文。注:lwIP 只处理回显请求报文,而其他类型的请求报文一律不处理。

RAW 编程接口TCP 客户端实验

本章,我们学习传输层的另一个协议,它是TCP 协议,TCP 协议对于UDP 协议来说,可
能有点晦涩难懂,读者可以参考相关网络书籍,来学习TCP 协议。

TCP 协议

TCP 协议简介

TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字
节流的传输层通信协议。

TCP 为了保证数据包传输的可靠行,会给每个包一个序号,同时此序号也保证了发送到
接收端主机能够按序接收。然后接收端主机对成功接收到的数据包发回一个相应的确认字符
(ACK,Acknowledgement),如果发送端主机在合理的往返时延(RTT)内未收到确认字符
ACK,那么对应的数据包就被认为丢失并将被重传。TCP 协议,它是基于连接的一种传输层
协议,在发送数据之前要求系统需要在不可靠的信道上建立可靠连接,我们称之为“三次握
手”。建立连接完成之后客户端与服务器才能互发数据,不需要发送数据时,可以可以断开连
接,这里我们称之为“四次挥手”。下面笔者带大家了解一下TCP 协议建立连接的过程和断开
连接的过程,即三次握手和四次挥手的过程。

TCP 的建立连接

握手之前主动打开连接的客户端结束CLOSED 阶段,被动打开的服务器端也结束
CLOSED 阶段,并进入LISTEN 阶段。随后开始“三次握手”:
①TCP 服务器进程先创建传输控制块TCB,时刻准备接受客户进程的连接请求,此时服
务器就进入了LISTEN(监听)状态。
②TCP 客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报
文首部中的同部位SYN=1,同时选择一个初始序列号seq=x ,此时,TCP 客户端进程进入了
SYN-SENT(同步已发送状态)状态。TCP 规定,SYN 报文段(SYN=1 的报文段)不能携带
数据,但需要消耗掉一个序号。
③TCP 服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该
ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号seq=y,此时,TCP
服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消
耗一个序号。
④TCP 客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,
自己的序列号seq=x+1,此时,TCP 连接建立,客户端进入ESTABLISHED(已建立连接)状
态。TCP 规定,ACK 报文段可以携带数据,但是如果不携带数据则不消耗序号。
当服务器收到客户端的确认后也进入ESTABLISHED 状态,此后双方就可以开始通信了。
这就是“三次握手”的过程,如下图所示。

在这里插入图片描述

TCP 终止连接

建立一个连接需要三次握手而终止一个连接需要四次挥手,终止连接有以下过程。
(1) 第一次挥手:客户端发送释放报文,并停止发送数据。释放数据报文首部,FIN=1,其
序列号为seq=u,此时,客户端进入FIN-WAIT1(等待服务器应答FIN 报文)。
(2) 第二次挥手:服务器收到客户端的FIN 报文后,发出确认报文ACK=1、ack=u+1,并
携带自己的序列号seq=v。此时,服务器进入CLOSE-WAIT(关闭等待)状态。客户端收到服
务端确认请求,此时,客户端进入FIN-WAIT2(终止等待2)状态,等待服务器发送连接释放
报文。
(3) 第三次挥手:服务器向客户端发送连接释放报文FIN=1、ack=u+1,此时,服务器进入
了LAST-ACK(最后确认)等待客户端的确认。客户端接收到服务器的连接释放报文后,必
须发送确认ack=1、ack=w+1,客户端的序列号为seq=u+1,此时,客户端进入TIME-WAIT(时
间等待)。
(4) 第四次挥手:服务器接收到客户端的确认报文,立刻进入CLOSED 状态。
这四次挥手就是终止TCP 协议连接,如下图所示:

在这里插入图片描述

上图的终止连接由客户端发起,当然服务器也可以发起终止连接。

TCP 报文结构

在传输层中,TCP 的数据包称为数据段,TCP 报文段与UDP 报文段一样都是封装在IP 数
据报中发送。TCP 首部包含建立与断开、数据确认、窗口大小通告、数据发送相关的所有标
志和控制信息,TCP 报文结构如下图所示:

在这里插入图片描述

(1) 源、目标端口号字段:占16 比特。TCP 协议通过使用”端口”来标识源端和目标端的
应用进程。端口号可以使用0 到65535 之间的任何数字。在收到服务请求时,操作系统动态地
为客户端的应用程序分配端口号。在服务器端,每种服务在”众所周知的端口”(Well-Know
Port)为用户提供服务。
(2) 序列号字段:占32 比特。用来标识从TCP 源端向TCP 目标端发送的数据字节流,它
表示在这个报文段中的第一个数据字节。
(3) 确认号字段:占32 比特。只有ACK 标志为1 时,确认号字段才有效。它包含目标端
所期望收到源端的下一个数据字节。
(4) 头部长度字段:占4 比特。给出头部占32 比特的数目。没有任何选项字段的TCP 头
部长度为20 字节;最多可以有60 字节的TCP 头部。
(5) 标志位字段(U、A、P、R、S、F):占6 比特。各比特的含义如下:
①URG:紧急指针有效。
②ACK:为1 时,确认序号有效。
③PSH:为1 时,接收方应该尽快将这个报文段交给应用层。
④RST:为1 时,重建连接。
⑤SYN:为1 时,同步程序,发起一个连接。
⑥FIN:为1 时,发送端完成任务,释放一个连接。
(6) 窗口大小字段:占16 比特。此字段用来进行流量控制。单位为字节数,这个值是本机
期望一次接收的字节数。
(7) TCP 校验和字段:占16 比特。对整个TCP 报文段,即TCP 头部和TCP 数据进行校验
和计算,并由目标端进行验证。
(8) 紧急指针字段:占16 比特。它是一个偏移量,和序号字段中的值相加表示紧急数据最
后一个字节的序号。
(9) 选项字段:占32 比特。可能包括”窗口扩大因子”、”时间戳”等选项。
上述的内容讲解的是TCP 首部信息,这些信息被封装在一个IP 数据报中,该数据报结构
如下图所示。

在这里插入图片描述

lwIP 的TCP 报文首部数据结构

实现TCP 协议的文件有tcp.h、tcp.c、tcp_in.c 和tcp_out.c,这四个文件实现了TCP 协议
全部数据结构和函数,其中tcp.c 文件包含了与TCP 编程、TCP 定时器相关的函数,而
tcp_in.c 文件包含了TCP 报文段输入处理函数,而tcp_out.c 文件包含了TCP 报文输出处理函
数,当然tcp.h 定义了宏和结构体。首先我们看一下TCP 首部结构,这个结构为tcp_hdr,如
下源码所示:

struct tcp_hdr {
    
    
	PACK_STRUCT_FIELD(u16_t src); /* 源端口*/
	PACK_STRUCT_FIELD(u16_t dest); /* 目的端口*/
	PACK_STRUCT_FIELD(u32_t seqno); /* 序号*/
	PACK_STRUCT_FIELD(u32_t ackno); /* 确认序号*/
	PACK_STRUCT_FIELD(u16_t _hdrlen_rsvd_flags); /* 首部长度+保留位+标志位*/
	PACK_STRUCT_FIELD(u16_t wnd); /* 窗口大小*/
	PACK_STRUCT_FIELD(u16_t chksum); /* 校验位*/
	PACK_STRUCT_FIELD(u16_t urgp); /* 紧急指针*/
} PACK_STRUCT_STRUCT;

可见,lwIP 使用tcp_hdr 结构体描述TCP 首部各个字段,值得注意的是,该结构体的
_hdrlen_rsvd_flags 变量用来描述下图黄色部分的内容。

在这里插入图片描述

lwIP 的TCP 连接状态图

根据图12.1.2.1 和12.1.2.2 所示,发送端与接收端发送的指令会进入不同的状态,因此,
lwIP 在tcpbase.h 文件中定义了枚举类型tcp_state,它是用来描述TCP 的状态,该枚举
tcp_state 如下源码所示:

enum tcp_state {
    
    
	CLOSED = 0, /* 关闭状态*/
	LISTEN = 1, /* 监听状态*/
	SYN_SENT = 2, /* 发送请求连接*/
	SYN_RCVD = 3, /* 接收请求连接*/
	ESTABLISHED = 4, /* 连接状态已建立*/
	FIN_WAIT_1 = 5, /* 程序已关闭该连接*/
	FIN_WAIT_2 = 6, /* 另一端已关闭连接*/
	CLOSE_WAIT = 7, /* 等待程序关闭连接*/
	CLOSING = 8, /* 两端同时收到对方的关闭请求*/
	LAST_ACK = 9, /* 服务器等待对方接收关闭操作*/
	TIME_WAIT = 10 /* 关闭成功*/
};

下面笔者使用TCP 状态转换图来描述连接可能在各个状态之间的转换关系,如下图所示:

在这里插入图片描述

如果TCP 需要建立连接,则系统需要三次握手;如果TCP 中断连接,则系统需要四次挥
手,现在笔者以上图12.1.4.2 的TCP 状态变迁图来绘制三次握手与四次挥手的状态图,不得不
说图片让我们更直观了解TCP 连接和关闭,如下图所示:

在这里插入图片描述

lwIP 的TCP 控制块

到目前为此,笔者已经讲解了太多TCP 协议理论的知识,这一小节我们正式踏入lwIP 的
TCP 协议大门。在此之前我们先了解一下TCP 控制块,这个控制块定义了TCP 协议运作过程
中的参数,例如发送窗口、数据缓冲区等,如下源码所示:

/** TCP协议控制块*/
struct tcp_pcb {
    
    
    /** common PCB members */
    IP_PCB;
    TCP_PCB_COMMON(struct tcp_pcb);
    /* 远端端口号*/
    u16_t remote_port;
    /*附加状态信息,如连接是快速恢复、一个被延迟的ACK 是否被发送等*/
    tcpflags_t flags;#
    define TF_ACK_DELAY 0x01 U /* 延迟发送ACK. */ # define TF_ACK_NOW 0x02 U /* 延迟发送ACK. */ # define TF_INFR 0x04 U /* 在快速恢复. */ # define TF_CLOSEPEND 0x08 U /* 关闭挂起*/ # define TF_RXCLOSED 0x10 U /* rx 由tcp_shutdown 关闭*/ # define TF_FIN 0x20 U /* 连接在本地关闭(FIN段入队) */ # define TF_NODELAY 0x40 U /* 纳格尔禁用算法*/ # define TF_NAGLEMEMERR 0x80 U /* nagle启用,本地缓冲区溢出*/
    /* Timers */
    u8_t polltmr, pollinterval;
    /* 控制块被最后一次处理的时间*/
    u8_t last_timer;
    /* 该字段记录该PCB 被创建的时刻*/
    u32_t tmr;
    /* 接收变量*/
    u32_t rcv_nxt; /* 下一个期望收到的序号*/
    tcpwnd_size_t rcv_wnd; /* 当前接收窗口的大小,会随着数据的接收与递交动态变化*/
    tcpwnd_size_t rcv_ann_wnd; /* 将向对方通告的窗口大小,随着数据的接收与递交动态变化*/
    u32_t rcv_ann_right_edge; /* 上一次窗口通告时窗口的右边界值*/
    /* 重传定时器,该值随时间递增,当大于rto 的值时重传报文*/
    s16_t rtime;
    u16_t mss; /* 对方可接收的最大报文段大小*/
    /* RTT(往返时间)估计变量*/
    u32_t rttest; /* RTT估计每秒500毫秒*/
    u32_t rtseq; /* 序列号定时*/
    s16_t sa, sv; /* RTT 估计得到的平均值与时间差*/
    s16_t rto; /* 重新传输超时(以TCP_SLOW_INTERVAL为单位) */
    u8_t nrtx; /* 重新发送的*/
    /* 快速重新传输/恢复*/
    u8_t dupacks; /* 上述最大确认号被重复收到的次数*/
    u32_t lastack; /* 接收到的最大确认序号*/
    /* 拥塞避免/控制变量*/
    tcpwnd_size_t cwnd; /* 连接当前的窗口大小*/
    tcpwnd_size_t ssthresh; /* 拥塞避免算法启动的阈值*/
    /* 第一个字节后面最后一个rto字节*/
    u32_t rto_end;
    /* 发送变量*/
    u32_t snd_nxt; /* 下一个要发送的序号*/
    u32_t snd_wl1, snd_wl2; /* 上一次收到的序号和确认号*/
    u32_t snd_lbb; /* 要缓冲的下一个字节的序列号*/
    tcpwnd_size_t snd_wnd; /* 发送窗口*/
    tcpwnd_size_t snd_wnd_max; /* 对方的最大发送方窗口*/
    /* 可用的缓冲区空间*/
    tcpwnd_size_t snd_buf;#
    define TCP_SNDQUEUELEN_OVERFLOW(0xffff U - 3)
    u16_t snd_queuelen; /* 可用的发送包数*/ #
    if TCP_OVERSIZE
    /* Extra bytes available at the end of the last pbuf in unsent. */
    u16_t unsent_oversize;#
    endif /* TCP_OVERSIZE */
    tcpwnd_size_t bytes_acked;
    /* These are ordered by sequence number: */
    struct tcp_seg * unsent; /* 未发送的报文段*/
    struct tcp_seg * unacked; /* 已发送但未收到确认的报文段. */
    struct pbuf * refused_data; /* 以前收到但上层尚未取得的数据*/ #
    if LWIP_CALLBACK_API || TCP_LISTEN_BACKLOG
    struct tcp_pcb_listen * listener;#
    endif /* LWIP_CALLBACK_API || TCP_LISTEN_BACKLOG */
    /* TCP 协议相关的回调函数*/
    #
    if LWIP_CALLBACK_API
    /* 当数据发送成功后被调用. */
    tcp_sent_fn sent;
    /* 接收数据完成后被调用*/
    tcp_recv_fn recv;
    /* 建立连接后被调用. */
    tcp_connected_fn connected;
    /* 该函数被内核周期调用. */
    tcp_poll_fn poll;
    /* 发送错误时候被调用. */
    tcp_err_fn errf;#
    endif /* LWIP_CALLBACK_API */
    /* 保持活性*/
    u32_t keep_idle;
    /* 坚持计时器计数器值*/
    u8_t persist_cnt;
    /* 坚持计时器关闭*/
    u8_t persist_backoff;
    /* 持续探测数*/
    u8_t persist_probe;
    /* 保持活性报文发送次数*/
    u8_t keep_cnt_sent;
};

TCP 协议控制块的成员变量有点多,由于TCP 协议在lwIP 源码中占了50%之多,所以深
入的去了解TCP 协议可能会花很多精力和时间,这里笔者讲解重要的知识即可。首先我们先
讲解一下接收数据相关的字段rcv_nxt,rcv_wnd,rcv_ann_wnd 和数据发送的相关字段
snd_nxt,snd_max,snd_wnd,acked,这些字段和TCP 中滑动窗口协议有密切关系的。
声明:下面的内容参考自《嵌入式网络那些事LWIP 协议深度剖析与实战演练》,作者朱
升林!。

  1. TCP 控制块接收窗口
    在TCP 控制块中,关于接收窗口有四个变量来描述,如下图所示:
    ①rcv_nxt:是自己期望收到的下一个数据字节编号。
    ②rcv_wnd:表示接收窗口的大小。
    ③rcv_ann_wnd:表示将向对方通告的窗口大小值,这个值在报文发送时会被填在首部中
    的窗口大小字段。
    ④rcv_ann_right_edge:记录了上一次窗口通告时窗口右边界取值,该字段在窗口滑动过
    程中经常被用到。

在这里插入图片描述

在上图中绿色框是窗口大小(rcv_wnd = 9 ),也就是说可以发送9 个数据,而
rcv_ann_wnd = 9 就是通知对方窗口大小的值,而rcv_ann_right_edge 记录了上一次窗口通告时
窗口右边界取值(14),当然下一次发送时,这四个变量就不一定是上述图中的值了,它们会
随着数据的发送与接收动态改变。当接收到数据后,数据会被放在接收窗口中等待上层调用,
rcv_nxt 字段会指向下一个期望接收的编号,同时窗口值rcv_wnd 值会减少,当上层取走相关
的数据后,窗口的值会增加;rcv_ann_wnd 在整个过程中都是动态计算的,当rcv_wnd 值改变
时,内核会计算一个合理的窗口值rcv_ann_wnd(并不一定与rcv_wnd 相等),在下一次报文
发送时,通告窗口的值(rcv_ann_wnd )会被填入报文的首部,同时右边界值
rcv_ann_right_edge 也在报文发送后更新数值。
2. TCP 控制块发送窗口
在lwIP 源码描述TCP 的发送窗口涉及4 个变量,它们之间的关系如下图所示:
①lastack:字段记录了被接收方确认的最高序列号。
②snd_nxt:表示自己将要发送的下一个数据的起始编号。
③snd_wnd:记录了当前的发送窗口大小,它常被设置为接收方通告的接收窗口值。
④snd_lbb:记录了下一个将被应用程序缓存的数据的起始编号。

在这里插入图片描述

可以看出,左边部分是已经发送并确认的数据,绿色框是已经发送但未确认的数据(需要
等待对方确认),红色框可以发送的数据,最右边的是不能发送的。上面这四个字段的值也是
动态变化的,每当收到接收方的一个有效ACK 后,lastack 的值就做相应的增加,指向下一个
待确认数据的编号,当发送一个报文后,snd_nxt 的值就做相应的增加,指向下一个待发送数
据。snd_nxt 和lastack 之间的差值不能超过sndwnd 的大小。由于实际数据发送时是按照报文
段的形式组织的,因此可能存在这样的情况:即使发送窗口允许,但并不是窗口内的所有数据
都能被发送以填满窗口,如上图中编号为11~13 的数据,可能因为它们太小不能组织成一个有
效的报文段,因此不会被发送。发送方会等到新的确认到来,从而使发送窗口向右滑动,使得
更多的数据被包含在窗口中,这样再启动下一个报文段的发送。
3. 监听控制块
lwIP 除了定义结构体tcp_pcb,它还定义了结构体tcp_pcb_listen,前者我们知道有这个就
行,后者结构体tcp_pcb_listen 主要描述LISTEN 状态的连接,一般用于描述处于监听状态的
连接,在处于LISTEN 状态的连接只记录本地端口的信息,不记录任何远程端口的信息,当然
处于该状态不会进行数据发送、连接握手之类的服务,主要是分配完整的TCP 控制块是比较
消耗内存资源的,在TCP 协议在连接之初,是无法进行数据交互,那么在监听的时候只需要
把对方主机的相关信息得到,然后无缝切换到完整的TCP 控制块中,这样子就能节省不少资
源,tcp_pcb_listen 的庐山真面目,如下源码所示:

/** 用于监听pcb的TCP协议控制块*/
struct tcp_pcb_listen {
    
    
    /** 该宏包含源IP 地址、目的IP 地址两个重要字段*/
    IP_PCB;
    /** 两种控制块都具有的字段*/
    TCP_PCB_COMMON(struct tcp_pcb_listen);#
    if LWIP_CALLBACK_API
    /* 函数在连接侦听器时调用*/
    tcp_accept_fn accept;#
    endif /* LWIP_CALLBACK_API */
};
  1. 控制块链表:
    为了描述TCP 控制块,lwIP 内核定义了四条链表来链接处于不同状态下的控制块,TCP
    操作一般对于链表上的控制块进行查找,这四个控制块链表在tcp.c 文件中,如下源码所示:
/*连接所有进行了端口号绑定,但是还没有发起连接(主动连接)或进入侦听状态(被动连接)的控制块*/
struct tcp_pcb *tcp_bound_pcbs;
/* 连接所有进入侦听状态(被动连接)的控制块*/
union tcp_listen_pcbs_t tcp_listen_pcbs;
/* 连接所有处于其他状态的控制块. */
struct tcp_pcb *tcp_active_pcbs;
/* 连接所有处于TIME-WAIT 状态的控制块*/
struct tcp_pcb *tcp_tw_pcbs;
  1. TCP 报文段缓冲
    在内核中,所有待发送的数据或者已经接收的数据都会以报文的形式保存,一般都是保存
    在pbuf 中,为了很好的管理报文段的pbuf,内核引用了一个tcp_seg 的结构体,该结构体的作
    用就是把所有报文段连接起来,当然这些报文段可以是无发送、已发送并未确认的或者是以收
    到的报文,它们都保存在TCP 控制块缓冲区中,该结构体如下源码所示:
/* 定义组织TCP 报文段的结构*/
struct tcp_seg {
    
    
	struct tcp_seg *next; /* 该指针用于将报文段组织为队列的形式*/
	struct pbuf *p; /* 指向装载报文段的pbuf */
	u16_t len; /* 报文段中的数据长度*/
	u8_t flags;
#define TF_SEG_OPTS_MSS (u8_t)0x01U /* 包含了最大报文段大小选项*/
#define TF_SEG_OPTS_TS (u8_t)0x02U /* 包含了时间戳选项*/
#define TF_SEG_DATA_CHECKSUMMED (u8_t)0x04U /* 所有数据(不是header)都是校验和为*/
#define TF_SEG_OPTS_ WND_SCALE (u8_t)0x08U /* 包括WND规模选项(仅用于SYN段) */
#define TF_SEG_OPTS_SACK_PERM (u8_t)0x10U/*包括SACK允许选项(仅在SYN段中使用)*/
	/* 指向报文段中的TCP 首部*/
	struct tcp_hdr *tcphdr; /* TCP报头*/
};

每个控制块中都维护了三个缓冲队列,unsent、unacked、ooseq 三个字段(这三个字段已
经在TCP 控制块时候讲解了)。unsent 用于连接还未被发送出去的报文段、unacked 用于连接
已经发送出去但是还未被确认的报文段、ooseq 用于连接接收到的无序报文段,如下图所示:

在这里插入图片描述

lwIP 的TCP 编程

(2) TCP 报文段的接收
报文段的接收函数是tcp_input,该函数位于tcp_inc.c 文件中,如下源码所示:

void
tcp_input(struct pbuf * p, struct netif * inp) {
    
    
    struct tcp_pcb * pcb, * prev;
    struct tcp_pcb_listen * lpcb;
    u8_t hdrlen_bytes;
    err_t err;
    /* 指向TCP首部*/
    tcphdr = (struct tcp_hdr * ) p - > payload;
    /* 第一步:检查TCP报头是否少于20 */
    if (p - > len < TCP_HLEN) {
    
    
        /* 释放空间掉弃报文段*/
        goto dropped;
    }
    /* 第二步:判断是否是广播与多播类型*/
    if (ip_addr_isbroadcast(ip_current_dest_addr(), ip_current_netif()) ||
        ip_addr_ismulticast(ip_current_dest_addr())) {
    
    
        /* 释放空间掉弃报文段*/
        goto dropped;
    }
    /* 获取tcphdr首部字节*/
    hdrlen_bytes = TCPH_HDRLEN_BYTES(tcphdr);
    /* 第三步:检测TCP报文长度*/
    if ((hdrlen_bytes < TCP_HLEN) || (hdrlen_bytes > p - > tot_len)) {
    
    
        /* 释放空间掉弃报文段*/
        goto dropped;
    }
    /* 移动pbuf中的有效负载指针,使其指向TCP数据*/
    /* tcphdr_optlen = TCP报头选项长度(TCP报头总长度- TCP标准报头20字节) */
    tcphdr_optlen = (u16_t)(hdrlen_bytes - TCP_HLEN);
    tcphdr_opt2 = NULL; /* tcphdr_opt2 指向NULL */
    /* 判断TCP报头是否在一个pbuf中*/
    if (p - > len >= hdrlen_bytes) {
    
    
        /* 若TCP报头在第一个pbuf中*/
        tcphdr_opt1len = tcphdr_optlen; /* tcphdr_opt1len = TCP报头选项长度*/
        pbuf_remove_header(p, hdrlen_bytes); /* 将指针移动到pbuf数据中*/
    } else {
    
    
        u16_t opt2len;
        /* 删除TCP首部*/
        pbuf_remove_header(p, TCP_HLEN);
        /* 确定选项的第一部分和第二部分长度*/
        tcphdr_opt1len = p - > len;
        opt2len = (u16_t)(tcphdr_optlen - tcphdr_opt1len);
        /* 移除tcphdr_opt1len选项*/
        pbuf_remove_header(p, tcphdr_opt1len);
        /* 检查TCP报头选项部分是否在第二个pbuf中*/
        if (opt2len > p - > next - > len) {
    
    
            /* 丢弃过短的报文*/
            goto dropped;
        }
        /* 记住指向TCP报头选项的第二部分的指针
        (有部分选项在第二个pbuf中,记录TCP报头选项的开始部分) */
        tcphdr_opt2 = (u8_t * ) p - > next - > payload;
        /* 将第二个pbuf的指针指向pbuf 的数据部分*/
        pbuf_remove_header(p - > next, opt2len);
        p - > tot_len = (u16_t)(p - > tot_len - opt2len);
    }
    /* 提取源端口*/
    tcphdr - > src = lwip_ntohs(tcphdr - > src);
    /* 提取目标端口*/
    tcphdr - > dest = lwip_ntohs(tcphdr - > dest);
    /* 提取序号*/
    seqno = tcphdr - > seqno = lwip_ntohl(tcphdr - > seqno);
    /* 提取确认号*/
    ackno = tcphdr - > ackno = lwip_ntohl(tcphdr - > ackno);
    /* 提取窗口*/
    tcphdr - > wnd = lwip_ntohs(tcphdr - > wnd);
    /* 6位标志位*/
    flags = TCPH_FLAGS(tcphdr);
    /* TCP数据包中数据的总长度,对于有FIN或SYN标志的数据包,该长度要加1 */
    tcplen = p - > tot_len;
    if (flags & (TCP_FIN | TCP_SYN)) {
    
    
        tcplen++;
        if (tcplen < p - > tot_len) {
    
    
            /* 释放空间掉弃报文段*/
            goto dropped;
        }
    }
    /* ****************************省略代码********************************* */
    /* 如果pcb在回调中被中止(通过调用tcp_abort()),则跳转目标。*/
    aborted:
        tcp_input_pcb = NULL;
    recv_data = NULL;
    if (inseg.p != NULL) {
    
    
        pbuf_free(inseg.p);
        inseg.p = NULL;
    }
} else {
    
    
    /*如果在3张链表里都未找到匹配的pcb,则调用tcp_rst向源主机发送一个TCP复位数据包*/
    if (!(TCPH_FLAGS(tcphdr) & TCP_RST)) {
    
    
        TCP_STATS_INC(tcp.proterr);
        TCP_STATS_INC(tcp.drop);
        tcp_rst(NULL, ackno, seqno + tcplen, ip_current_dest_addr(),
            ip_current_src_addr(), tcphdr - > dest, tcphdr - > src);
    }
    pbuf_free(p);
}
return;
dropped:
    pbuf_free(p);
}

上述的源码大概400 多行,该函数可以分为上部分与下部分,上部分主要讲述了对IP 层
递交传输层的数据报检验,例如检验数据报是否正常操作、是否包含数据、该数据报是否为广
播或者多播,如果以上检验成立,则系统把该数据报掉弃处理,并释放pbuf。下部分主要对
tcp_active_pcbs 链表寻找对应的TCP 控制块,如果找到了TCP 控制块,则调用tcp_process 函
数处理;如果找不到TCP 控制块,则内核转换到tcp_tw_pcbs 链表中查找;如果在
tcp_tw_pcbs 链表中找到TCP 控制块,则内核调用tcp_timewait_input 函数处理它;如果这两个
链表没有找到TCP 控制块,则系统会进入tcp_listen_pcbs 链表中查找;如果找到了就调用
tcp_listen_input 函数处理;如果三个链表都找不到的话,则系统就释放pbuf 内存。
(3) TCP 报文段的发送
传输层与网络层的交互函数为tcp_output,它在tcp_output.c 文件中定义,如下源码所示:

/* 发送控制块缓冲队列中的报文段*/
err_t
tcp_output(struct tcp_pcb * pcb) {
    
    
    struct tcp_seg * seg, * useg;
    u32_t wnd, snd_nxt;
    err_t err;
    struct netif * netif;
    /* 如果控制块当前正有数据被处理,这里不做任何输出,直接返回*/
    if (tcp_input_pcb == pcb) /* 在控制块的数据处理完成后,内核会再次调用*/ {
    
    
        return ERR_OK; /* 调用tcp_output 发送数据,见函数tcp_input */
    }
    /* 从发送窗口和阻塞窗口取小者得到有效发送窗口,拥塞避免会讲解到这个原理*/
    wnd = LWIP_MIN(pcb - > snd_wnd, pcb - > cwnd);
    /* 未发送队列*/
    seg = pcb - > unsent;
    if (seg == NULL) {
    
    
        /* 若要求立即确认,但该ACK 又不能被捎带出去,则只发送一个纯ACK 的报文段*/
        if (pcb - > flags & TF_ACK_NOW) {
    
    
            return tcp_send_empty_ack(pcb); /* 发送只带ACK 的报文段*/
        }
        /* 没什么可送的*/
        goto output_done;
    } else {
    
    }
    /* 判断本地IP地址与远程IP地址是否同一网段*/
    netif = tcp_route(pcb, & pcb - > local_ip, & pcb - > remote_ip);
    if (netif == NULL) {
    
    
        return ERR_RTE;
    }
    /* 如果没有本地IP地址,从netif获得一个*/
    if (ip_addr_isany( & pcb - > local_ip)) {
    
    
        const ip_addr_t * local_ip = ip_netif_get_local_ip(netif, & pcb - > remote_ip);
        if (local_ip == NULL) {
    
    
            return ERR_RTE;
        }
        ip_addr_copy(pcb - > local_ip, * local_ip);
    }
    /* 处理窗口中不匹配的当前段*/
    if (lwip_ntohl(seg - > tcphdr - > seqno) - pcb - > lastack + seg - > len > wnd) {
    
    
        /* 开始持续定时器*/
        if (wnd == pcb - > snd_wnd && pcb - > unacked == NULL && \
            pcb - > persist_backoff == 0) {
    
    
            pcb - > persist_cnt = 0;
            pcb - > persist_backoff = 1;
            pcb - > persist_probe = 0;
        }
        /* 我们需要一个ACK,但是现在不能发送数据,所以发送一个空ACK */
        if (pcb - > flags & TF_ACK_NOW) {
    
    
            return tcp_send_empty_ack(pcb);
        }
        goto output_done;
    }
    /* 停止持续计时器,如果以上条件不满足*/
    pcb - > persist_backoff = 0;
    /* useg应该指向未处理队列的最后一个tcp_seg 结构*/
    useg = pcb - > unacked;
    if (useg != NULL) {
    
    
        for (; useg - > next != NULL; useg = useg - > next);
    }
    /* 可用数据和窗口允许它发送报文段,直到把数据全部发送出去或者填满发送窗口*/
    while (seg != NULL &&
        lwip_ntohl(seg - > tcphdr - > seqno) - pcb - > lastack + seg - > len <= wnd) {
    
    
        /* 如果nagle算法可以阻止发送,就停止发送*/
        if ((tcp_do_output_nagle(pcb) == 0) &&
            ((pcb - > flags & (TF_NAGLEMEMERR | TF_FIN)) == 0)) {
    
    
            break;
        }
        if (pcb - > state != SYN_SENT) /* 当前不为SYN_SENT 状态*/ {
    
    
            TCPH_SET_FLAG(seg - > tcphdr, TCP_ACK); /* 填写首部中的ACK 标志*/
        }
        /* 调用函数发送报文段*/
        err = tcp_output_segment(seg, pcb, netif);
        if (err != ERR_OK) {
    
    
            /* segment could not be sent, for whatever reason */
            tcp_set_flags(pcb, TF_NAGLEMEMERR);
            return err;
        }
        /* 得到下一个未发送的tcp_seg */
        pcb - > unsent = seg - > next;
        if (pcb - > state != SYN_SENT) {
    
    
            tcp_clear_flags(pcb, TF_ACK_DELAY | TF_ACK_NOW);
        }
        /* 计算snd_nxt 的值*/
        snd_nxt = lwip_ntohl(seg - > tcphdr - > seqno) + TCP_TCPLEN(seg);
        /* 更新下一个要发送的数据编号*/
        if (TCP_SEQ_LT(pcb - > snd_nxt, snd_nxt)) {
    
    
            pcb - > snd_nxt = snd_nxt;
        }
        /* 如果发送出去的报文段数据长度不为0,或者带有SYN、FIN 标志,则将该报
        文段加入到未确认队列中以便超时后重传*/
        if (TCP_TCPLEN(seg) > 0) {
    
    
            seg - > next = NULL; /* 空报文段next 字段*/
            /* 若未确认队列为空,则直接挂接*/
            if (pcb - > unacked == NULL) {
    
    
                pcb - > unacked = seg;
                useg = seg; /* 变量useg 指向未确认队列尾部*/
            } else {
    
    
                /* 如果未确认队列不为空,则需要把当前报文按照顺序组织在队列中*/
                if (TCP_SEQ_LT(lwip_ntohl(seg - > tcphdr - > seqno),
                        lwip_ntohl(useg - > tcphdr - > seqno))) {
    
    
                    /* 如果当前报文的序列号比队列尾部报文的序列号低,则从队列首部开始
                    查找合适的位置,插入报文段*/
                    struct tcp_seg * * cur_seg = & (pcb - > unacked);
                    while ( * cur_seg &&
                        TCP_SEQ_LT(lwip_ntohl(( * cur_seg) - > tcphdr - > seqno),
                            lwip_ntohl(seg - > tcphdr - > seqno))) {
    
    
                        cur_seg = & (( * cur_seg) - > next);
                    } /* 找到插入位置,将报文段插入到队列中*/
                    seg - > next = ( * cur_seg);
                    ( * cur_seg) = seg;
                } else {
    
    
                    /* 报文段序号最高,则放在未确认队列尾部*/
                    useg - > next = seg;
                    useg = useg - > next;
                }
            }
        } else /* 报文段长度为0,不需要重传,直接删除*/ {
    
    
            tcp_seg_free(seg);
        }
        seg = pcb - > unsent; /* 发送下一个报文段*/
    }#
    if TCP_OVERSIZE
    if (pcb - > unsent == NULL) {
    
    
        /* 清0 已发送的窗口探测包数目*/
        pcb - > unsent_oversize = 0;
    }#
    endif /* TCP_OVERSIZE */
    output_done:
        tcp_clear_flags(pcb, TF_NAGLEMEMERR);
    return ERR_OK;
}

从整体来看,此函数首先检测报文是否满足发送要求,接着判断控制块的flags 字段是否
被设置为TF_ACK_NOW 状态,如果是,则发送一个纯粹ACK 报文段,因此,此时unsent 队
列中无数据发送或者发送窗口不允许发送数据。如果内核能发送数据,则就将ACK 应答捎带
发送出去,同时在发送的时候先找到未发送链表,然后调用tcp_output_segment()-> ip_output_if()函数进行发送,直到把未发送链表的数据完全发送出去或者直到填满发送窗口,
并且更新发送窗口相关字段,当然也要将这些已发送但是未确认的数据存储在未确认链表中,
以防丢失数据进行重发操作,放入未确认链表的时候是按序号升序进行排序的。

lwIP 的TCP 建立与关闭连接原理

下面笔者来讲解一下lwIP 如何实现TCP 客户端以及服务器连接,这里我们可以根据TCP
连接示意图来讲解lwIP 源码是如何实现TCP 连接的。在讲解之前,我们先了解TCP 客户端的
配置流程,如下所示:

  1. TCP 客户端建立连接原理:
    ①创建TCP 控制块
    调用函数tcp_new 创建TCP 控制块。
    ②连接指定的IP 地址和端口号
    调用函数tcp_connect 连接到目的地址的指定端口上,注意:当连接成功后进入回调
    tcp_client_connected 函数。
    ③接收数据
    调用函数tcp_recved 接收数据。
    ④发送数据
    调用函数tcp_write 发送数据。
    从上述步骤可知,我们主要调用函数tcp_connect 连接远程服务器,这个函数和TCP 连接
    图存在某种联系,下面笔者简单的讲解这个函数到底如何连接服务器,该函数如下所示:
err_t
tcp_connect(struct tcp_pcb * pcb,
    const ip_addr_t * ipaddr, u16_t port,
        tcp_connected_fn connected) {
    
    
    /*.....................前面省略大部分代码......................*/
    /* 发送SYN与MSS选项一起发送*/
    ret = tcp_enqueue_flags(pcb, TCP_SYN);
    (1)
    if (ret == ERR_OK) {
    
    
        /* 设置当前TCP控制块为SYN_SENT状态*/
        pcb - > state = SYN_SENT;
        (2)
        if (old_local_port != 0) {
    
    
            TCP_RMV( & tcp_bound_pcbs, pcb);
        }
        TCP_REG_ACTIVE(pcb);
        MIB2_STATS_INC(mib2.tcpactiveopens);
        tcp_output(pcb);
        (3)
    }
    return ret;
}

可见,上述的(1)表示程序调用函数tcp_enqueue_flags 构建连接请求报文(TCP_SYN);上
述的(2)表示当前TCP 控制块设置为SYN_SENT 状态;上述的(3)表示程序调用函数tcp_output
向服务器发送连接请求报文。下面笔者使用一个示意图来描述上述的内容,如下图所示:

在这里插入图片描述

上图中红色框框的是tcp_connect 函数实现流程,这里可以称之为TCP 第一次握手,此时
客户端等待服务器的连接应答报文(TCP_ACK)。当客户端接收服务器应答报文(TCP_ACK)
时,系统会在tcp_input 这个函数处理该应答报文。这个函数在上小节也讲解过,这里我们无
需重复讲解了,该连接应答报文会在tcp_input–>tcp_process 函数下处理,注意:tcp_input 函
数中flags 的全局变量是获取接收数据报的首部标志位(TCP_ACK+ TCP_SYN),这个过程请
看tcp_in.c 文件234 行的代码,如下源码所示:

static err_t
tcp_process(struct tcp_pcb * pcb) {
    
    
    /*..................此处省略了很多代码..................... */
    switch (pcb - > state) {
    
    
        case SYN_SENT:
            /* 收到SYN ACK与预期的序列号? */
            if ((flags & TCP_ACK) && (flags & TCP_SYN)(1) && (ackno == pcb - > lastack + 1)) {
    
    
                pcb - > rcv_nxt = seqno + 1;
                pcb - > rcv_ann_right_edge = pcb - > rcv_nxt;
                pcb - > lastack = ackno;
                pcb - > snd_wnd = tcphdr - > wnd;
                pcb - > snd_wnd_max = pcb - > snd_wnd;
                pcb - > snd_wl1 = seqno - 1;
                pcb - > state = ESTABLISHED;
                (2)
            }
            /*..................此处省略了很多代码..................... */
    }
    /*..................此处省略了很多代码..................... */
}

上述的的(1)就是为了判断服务器应答报文的标志位是否包含TCP_ACK 和TCP_SYN,如
果该应答报文包含这些标志位,则系统执行上述(2)的代码设置TCP 控制块为ESTABLISHED
状态。这里笔者也是使用一个示意图来描述上述的内容,如下图所示:

在这里插入图片描述

上图的红色框框就是上述内容实现的过程,这里可以称之为TCP 第二次握手,此时客户端必须发送TCP_ACK 应答报文给服务器才能实现第三次握手。上面的函数tcp_process 执行
完成之后返回到tcp_input 函数,该函数的553 行代码调用了tcp_output 函数发送应答报文,该
函数如下所示:

err_t
tcp_output(struct tcp_pcb * pcb) {
    
    
    /*..................此处省略了很多代码..................... */
    if (pcb - > state != SYN_SENT) {
    
    
        TCPH_SET_FLAG(seg - > tcphdr, TCP_ACK);
    }
    /* 发送应答包*/
    err = tcp_output_segment(seg, pcb, netif);
    /*..................此处省略了很多代码..................... */
}

因为TCP 控制块已经是ESTABLISHED 状态了,所以这个if 语句判断为真且执行if 语句
内的代码,这个代码主要添加该数据报的首部标志位TCP_ACK ,接着系统调用
tcp_output_segmen 发送该应答包,这里就完成了三次握手的动作。下面笔者使用一个示意图
来讲解这个过程,如下图所示:

在这里插入图片描述

  1. TCP 服务器建立连接原理
    TCP 服务器的配置流程,如下步骤所示:
    ①创建TCP 控制块
    调用函数tcp_new 创建TCP 控制块。
    ②绑定本地IP 地址和端口号
    调用函数tcp_bind 绑定本地IP 地址和端口号。
    ③连接请求
    调用函数tcp_accept 等待连接。注意:有连接时,会调用函数lwip_tcp_server_accept 处理
    ④接收数据
    调用函数tcp_recved 接收数据。
    ⑤发送数据
    调用函数tcp_write 发送数据。
    首先我们调用tcp_listen 函数让服务器进去监听状态,简单来说,TCP 服务器控制块从
    CLOSER 转换成LISTEN 状态,如下源码所示:
#
define tcp_listen(pcb) tcp_listen_with_backlog(pcb, TCP_DEFAULT_LISTEN_BACKLOG)
struct tcp_pcb *
    tcp_listen_with_backlog(struct tcp_pcb * pcb, u8_t backlog) {
    
    
        LWIP_ASSERT_CORE_LOCKED();
        return tcp_listen_with_backlog_and_err(pcb, backlog, NULL);
    }
struct tcp_pcb *
    tcp_listen_with_backlog_and_err(struct tcp_pcb * pcb, u8_t backlog, err_t * err) {
    
    
        /* ..............省略代码.............. */
        lpcb - > callback_arg = pcb - > callback_arg;
        lpcb - > local_port = pcb - > local_port;
        lpcb - > state = LISTEN;
        (1)
        lpcb - > prio = pcb - > prio;
        lpcb - > so_options = pcb - > so_options;
        lpcb - > netif_idx = pcb - > netif_idx;
        lpcb - > ttl = pcb - > ttl;
        lpcb - > tos = pcb - > tos;
        /* ..............省略代码.............. */
    }

上述的(1)就是让TCP 服务器控制块从CLOSER 状态转换成LISTEN 状态,下面笔者使用
一个图来描述上述的内容,如下图所示:

在这里插入图片描述

上图的红色框框就是由tcp_listen 函数实现的,下面开始讲解TCP 第一次握手流程,对于服务器而言,它是先接收客户端发来的连接请求包并判断该请求报文的首部标志位是否包含T
CP_SYN,这个请求报文的处理是由tcp_input→tcp_listen_input 函数处理的,该函数与客户端
请求包相关的源码如下所示:

#
define tcp_listen(pcb) tcp_listen_with_backlog(pcb, TCP_DEFAULT_LISTEN_BACKLOG)
struct tcp_pcb *
    tcp_listen_with_backlog(struct tcp_pcb * pcb, u8_t backlog) {
    
    
        LWIP_ASSERT_CORE_LOCKED();
        return tcp_listen_with_backlog_and_err(pcb, backlog, NULL);
    }
struct tcp_pcb *
    tcp_listen_with_backlog_and_err(struct tcp_pcb * pcb, u8_t backlog, err_t * err) {
    
    
        /* ..............省略代码.............. */
        lpcb - > callback_arg = pcb - > callback_arg;
        lpcb - > local_port = pcb - > local_port;
        lpcb - > state = LISTEN;
        (1)
        lpcb - > prio = pcb - > prio;
        lpcb - > so_options = pcb - > so_options;
        lpcb - > netif_idx = pcb - > netif_idx;
        lpcb - > ttl = pcb - > ttl;
        lpcb - > tos = pcb - > tos;
        /* ..............省略代码.............. */
    }

可见,lwIP 内核首先判断连接请求报文的首部标志位是否包含TCP_SYN,显然这个符合
第一次TCP 握手,然后系统把服务器控制块的状态从LISTEN 转换成SYN-RCVD,这个过程
请看上述的(1),其次系统构建连接应答TCP_ACK| TCP_SYN 报文(上述源码中的(2)),最后
系统调用函数tcp_output 发送该连接应答TCP_ACK| TCP_SYN 报文到客户端当中(上述源码
中的(3))。至此我们已经实现了TCP 第二次握手了,下面笔者使用一个示意图来讲解上述的内
容,如下图所示:

在这里插入图片描述

上图的红色框框就是服务器接收客户端的连接请求报文之后发送连接应答报文,到了这里
服务器必须接收客户端的确认连接应答TCP_ACK 报文才能实现TCP 第三次握手,下面笔者
带大家讲解一下最后一次握手,它是在tcp_input→ tcp_process 函数下处理的,该函数如下所
示:

static err_t
tcp_process(struct tcp_pcb * pcb) {
    
    
    /* ...........此处省略多行代码....... */
    case SYN_RCVD:
        if (flags & TCP_ACK) {
    
    
            if (TCP_SEQ_BETWEEN(ackno, pcb - > lastack + 1, pcb - > snd_nxt)) {
    
    
                pcb - > state = ESTABLISHED;
                /* ...........此处省略多行代码....... */
            } else {
    
    
                /* ...........此处省略多行代码....... */
            }
        } else if ((flags & TCP_SYN) && (seqno == pcb - > rcv_nxt - 1)) {
    
    
            /* ...........此处省略多行代码....... */
        }
        break;
        /* ...........此处省略多行代码....... */
}

服务器接收到客户端的应答ACK 报文之后会把自身的状态SYN-RCVD 转换成
ESTABLISHED,至此客户端和服务器可以相互发送数据了。TCP 客户端和服务器握手流程已
经很详细讲解了,如有疑问,请大家联系笔者,我们可以一起讨论研究。
3. TCP 关闭连接原理:
(1) 客户端发送FIN 报文
程序关闭TCP 连接是调用tcp_close 函数实现的,在调用这个函数之前,我们必须把
tcp_pcb 的recv 回调函数指针设置为NULL(应用层不再接收数据,所有数据直接被丢弃,协
议层的处理仍按正常流程走,认为应用层已经接收到数据),tcp_close 函数主要作用是发送
FIN 报文,进入FIN_WAIT_1 状态(第一次挥手),下面我们来看一下第一次挥手的源码,注
意:以下源码的路径:tcp_close→tcp_close_shutdown→tcp_close_shutdown_fin 函数下,该函数
如下所示:

static err_t
tcp_close_shutdown_fin(struct tcp_pcb * pcb) {
    
    
    err_t err;
    /* 省略多余的代码行*/
    switch (pcb - > state) {
    
    
        case SYN_RCVD:
            err = tcp_send_fin(pcb);
            if (err == ERR_OK) {
    
    
                tcp_backlog_accepted(pcb);
                MIB2_STATS_INC(mib2.tcpattemptfails);
                pcb - > state = FIN_WAIT_1;
            }
            break;
        case ESTABLISHED:
            err = tcp_send_fin(pcb);
            if (err == ERR_OK) {
    
    
                MIB2_STATS_INC(mib2.tcpestabresets);
                /* 设置TCP控制块的状态为FIN_WAIT_1 */
                pcb - > state = FIN_WAIT_1;
            }
            break;
        case CLOSE_WAIT:
            err = tcp_send_fin(pcb);
            if (err == ERR_OK) {
    
    
                MIB2_STATS_INC(mib2.tcpestabresets);
                pcb - > state = LAST_ACK;
            }
            break;
        default:
            return ERR_OK;
    }
    /* 发送关闭连接请求包*/
    if (err == ERR_OK) {
    
    
        tcp_output(pcb);
    } else if (err == ERR_MEM) {
    
    
        tcp_set_flags(pcb, TF_CLOSEPEND);
        return ERR_OK;
    }
    return err;
}

大家请看上述有注释的代码,这些代码是客户端发送关闭连接请求报文过程,该包的首部
包含FIN 标志位并调用函数tcp_output 发送到服务器当中。由此可见,客户端从
ESTABLISHED 状态转换成FIN-WAIT-1 状态,下面笔者使用一个示意图来描述上述的内容,
如下图所示:

在这里插入图片描述

上图红色框框表示tcp_close 函数处理过程,这里也可以称之为TCP 第一次挥手的动作。
(2) 服务器接收到FIN 报文并发送ACK 报文
当服务器接收到客户端的FIN 报文时,它会进入到CLOSE_WAIT 状态,这个FIN 报文交
由tcp_process 函数处理,当然它接收到的数据可以发送给应用层,但是它递交一个空的EOF
数据给应用层(应用层知道接收数据已经完成,不需要再从协议栈读数据),最后系统发送客
户端ACK 报文给客户端(第二次挥手),进入CLOSE_WAIT 状态,如下源码所示:

static err_t
tcp_process(struct tcp_pcb * pcb) {
    
    
    /* ...........省略多行代码........... */
    switch (pcb - > state) {
    
    
        /* ...........省略多行代码........... */
        case ESTABLISHED:
            tcp_receive(pcb);
            if (recv_flags & TF_GOT_FIN) {
    
     /* 收到FIN被动关闭*/
                tcp_ack_now(pcb); /* 构建ACK报文*/
                pcb - > state = CLOSE_WAIT; /* 进入CLOSE_WAIT状态*/
            }
            break;
    }
    /* ...........省略多行代码........... */
}

上述源码是服务器接收到客户端的FIN 报文时,它构建了一个ACK 报文发送到客户端当
中,然后它的状态从ESTABLISHED 转换成CLOSE-WAIT,下面笔者也是使用一个示意图来
描述上述的内容,如下图所示:

在这里插入图片描述

上图红色框框就是上述源码运行的流程,为了理解,笔者没有把全部的代码列举出来。
(3) 客户端接收到ACK 报文并转换成FIN-WAIT-2 状态
当FIN_WAIT_1 状态的客户端收到服务器的ACK 报文时,它的状态从FIN-WAIT-1 转换
成FIN-WAIT-2 状态,这个过程的源码如下所示:

static err_t
tcp_process(struct tcp_pcb * pcb) {
    
    
    /* ...........省略多行代码........... */
    switch (pcb - > state) {
    
    
        /* ...........省略多行代码........... */
        case FIN_WAIT_1:
            /* 接收数据*/
            tcp_receive(pcb);
            /* 服务器还没有确认FIN报文*/
            if (recv_flags & TF_GOT_FIN) {
    
    
                /* 非同时关闭*/
                if ((flags & TCP_ACK) && (ackno == pcb - > snd_nxt)) {
    
    
                    /* ...........省略多行代码........... */
                    /* 发送ACK应答对端的FIN报文*/
                    tcp_ack_now(pcb);
                    TCP_RMV( & tcp_active_pcbs, pcb); /* 从tcp_active_pcbs删除tcp_pcb */
                    /* tcp_timewait_input处理,所有数据都丢弃,不发送给应用层,
                    直接确认当前收到的报文,rcv_nxt设置为当前报文的下一个字节*/
                    pcb - > state = TIME_WAIT;
                    TCP_REG( & tcp_tw_pcbs, pcb); /* 添加tcp_pcb到tcp_tw_pcbs */
                } else {
    
     /* (客户端、服务器同时调用tcp_close,都在FIN_WAIT_1状态收到对方的FIN报文)*/
                    tcp_ack_now(pcb); /* 发送FIN报文的ACK */
                    pcb - > state = CLOSING; /* 进入CLOSING状态*/
                }
            } /* 服务器确认了FIN报文*/
            else if ((flags & TCP_ACK) && (ackno == pcb - > snd_nxt)) {
    
    
                pcb - > state = FIN_WAIT_2; /* 进入FIN_WAIT_2状态*/
            }
    }
    /* ...........省略多行代码........... */
}

上述源码可分为两个部分讲解,第一部分:处于FIN_WAIT_1 客户端会判断服务器有没
有确认FIN 报文,如果它没有发送ACK 报文,则系统进入if 语句执行,该if 语句的代码主要
为了判断服务器和客户端是否同时调用tcp_close 函数关闭连接,如果不同时,则将TCP 控制
块从tcp_active_pcbs 队列移除并设置该控制块的状态为TIME_WAIT。最后把该控制块挂在
tcp_tw_pcbs 队列当中;如果客户端和服务器同时关闭连接,则系统发送一个ACK 报文到服务
器当中并设置TCP 控制块的状态为CLOSING;第二部分:服务器发送ACK 报文给客户端了,
显然它直接设置TCP 控制块的状态为FIN_WAIT_2,下面我们使用一个示意图来描述上述的
内容,如下图所示:

在这里插入图片描述

从上图可知:服务器的状态从FIN-WAIT-1 转换成FIN-WAIT-2 状态,FIN-WAIT-2 状态的
客户端需要等待服务器发送FIN 报文。
(4) CLOSE-WAIT 状态的服务器发送FIN 报文
CLOSE-WAIT 状态的服务器发送FIN 报文如下源码所示:

static err_t
tcp_close_shutdown_fin(struct tcp_pcb * pcb) {
    
    
    err_t err;
    /* 省略多余的代码行*/
    switch (pcb - > state) {
    
    
        case SYN_RCVD:
            err = tcp_send_fin(pcb);
            if (err == ERR_OK) {
    
    
                tcp_backlog_accepted(pcb);
                MIB2_STATS_INC(mib2.tcpattemptfails);
                pcb - > state = FIN_WAIT_1;
            }
            break;
        case ESTABLISHED:
            err = tcp_send_fin(pcb);
            if (err == ERR_OK) {
    
    
                MIB2_STATS_INC(mib2.tcpestabresets);
                pcb - > state = FIN_WAIT_1;
            }
            break;
        case CLOSE_WAIT:
            /* 发送FIN报文*/
            err = tcp_send_fin(pcb);
            if (err == ERR_OK) {
    
    
                MIB2_STATS_INC(mib2.tcpestabresets);
                /* 设置状态为LAST_ACK */
                pcb - > state = LAST_ACK;
            }
            break;
        default:
            return ERR_OK;
    }
    /* 发送关闭连接请求包*/
    if (err == ERR_OK) {
    
    
        tcp_output(pcb);
    } else if (err == ERR_MEM) {
    
    
        tcp_set_flags(pcb, TF_CLOSEPEND);
        return ERR_OK;
    }
    return err;
}

此函数很简单,主要发送FIN 报文以及设置CLOSE_WAIT 状态的服务器为LAST_ACK
状态。下面笔者也是使用一个示意图来描述上述的内容,如下图所示:

在这里插入图片描述

这里称之为TCP 第三次挥手,最后就是FIN-WAIT-2 状态的客户端接收服务器的FIN 报文
并发送ACK 报文确认关闭。
(5) FIN-WAIT-2 状态的客户端接收FIN 报文并发送ACK 报文确认
这个过程是在tcp_input→tcp_ process 函数下处理,该函数如下所示:

static err_t
tcp_process(struct tcp_pcb * pcb) {
    
    
    /* ...........省略多行代码........... */
    switch (pcb - > state) {
    
    
        /* ...........省略多行代码........... */
        case FIN_WAIT_2:
            /* 接收报文*/
            tcp_receive(pcb);
            if (recv_flags & TF_GOT_FIN) {
    
    
                /* 构建ACK报文*/
                tcp_ack_now(pcb);
                tcp_pcb_purge(pcb);
                TCP_RMV_ACTIVE(pcb);
                /* 设置状态为TIME_WAIT */
                pcb - > state = TIME_WAIT;
                TCP_REG( & tcp_tw_pcbs, pcb);
            }
            break;
    }
    /* ...........省略多行代码........... */
}

此函数主要判断FIN_WAIT_2 状态的客户端是否接收到FIN 报文,如果系统接收的报文
是FIN 报文,则系统发送ACK 报文给服务器并设置客户端的状态为TIME_WAIT,这里就是
TCP 第四次挥手。

lwIP 中RAW API 编程接口中与TCP 相关的函数

tcp.c、tcp.h、tcp_in.c 和tcp_out.c 是lwIP 中关于TCP 协议的文件,TCP 层中函数的关系
如下图所示。

在这里插入图片描述

lwIP 提供了很多关于TCP 协议的的RAW 编程API 函数,我们可以使用这些函数来完成
有关TCP 的实验,我们在下表列出了一部分函数。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

RAW 接口的TCP 实验

硬件设计

  1. 例程功能
    本章实验的目标是PC 端和开发板通过TCP 协议连接起来,开发板做TCP 客户端,PC 端
    的网络调试助手配置成服务器。开发板接收服务器发送的数据在LCD 上显示,我们也可以通
    过开发板上的按键发送数据给PC。
    该实验的实验工程,请参考《lwIP 例程3 lwIP_RAW_TCPClient 实验》。

软件设计

12.2.2.1 TCP 客户端配置步骤

  1. 创建TCP 控制块
    调用函数tcp_new 创建TCP 控制块。
  2. 连接指定的IP 地址和端口号
    调用函数tcp_connect 连接到目的地址的指定端口上。
  3. 接收数据
    调用函数tcp_recved 接收数据。
  4. 发送数据
    调用函数tcp_write 发送数据。
    12.2.2.2 程序流程图
    本实验的程序流程图,如下图所示:

在这里插入图片描述

12.2.2.3 程序解析
本章实验只讲解lwip_demo.c 文件,该文件定义了9 个函数,这些函数的作用如下所示:

在这里插入图片描述

程序首先执行lwip_demo 函数,此函数为lwip_demo.c 文件的入口处,如下源码所示:

/**
 * @brief lwip_demo程序入口
 * @param 无
 * @retval 无
 */
void lwip_demo(void) {
    
    
    struct tcp_pcb * tcppcb; /* 定义一个TCP服务器控制块*/
    ip_addr_t rmtipaddr; /* 远端ip地址*/
    char * tbuf;
    uint8_t key;
    uint8_t res = 0;
    uint8_t t = 0;
    uint8_t connflag = 0; /* 连接标记*/
    lwip_tcp_client_set_remoteip(); /* 先选择IP */
    lcd_clear(BLACK); /* 清屏*/
    g_point_color = WHITE;
    lcd_show_string(30, 30, 200, 16, 16, "STM32", g_point_color);
    lcd_show_string(30, 50, 200, 16, 16, "TCP Client Test", g_point_color);
    lcd_show_string(30, 70, 200, 16, 16, "ATOM@正点原子", g_point_color);
    lcd_show_string(30, 90, 200, 16, 16, "KEY0:Send data", g_point_color);
    lcd_show_string(30, 110, 200, 16, 16, "KEY1:Quit", g_point_color);
    tbuf = mymalloc(SRAMIN, 200); /* 申请内存*/
    if (tbuf == NULL) return; /* 内存申请失败了,直接退出*/
    sprintf((char * ) tbuf, "Local IP:%d.%d.%d.%d", lwipdev.ip[0],
        lwipdev.ip[1],
        lwipdev.ip[2],
        lwipdev.ip[3]); /* 服务器IP */
    lcd_show_string(30, 130, 210, 16, 16, tbuf, g_point_color);
    /* 远端IP */
    sprintf((char * ) tbuf, "Remote IP:%d.%d.%d.%d", lwipdev.remoteip[0],
        lwipdev.remoteip[1],
        lwipdev.remoteip[2],
        lwipdev.remoteip[3]);
    lcd_show_string(30, 150, 210, 16, 16, tbuf, g_point_color);
    sprintf((char * ) tbuf, "Remote Port:%d", TCP_CLIENT_PORT); /* 客户端端口号*/
    lcd_show_string(30, 170, 210, 16, 16, tbuf, g_point_color);
    g_point_color = BLUE;
    lcd_show_string(30, 190, 210, 16, 16, "STATUS:Disconnected", g_point_color);
    tcppcb = tcp_new(); /* 创建一个新的pcb */
    if (tcppcb) /* 创建成功*/ {
    
    
        IP4_ADDR( & rmtipaddr, lwipdev.remoteip[0], lwipdev.remoteip[1],
            lwipdev.remoteip[2], lwipdev.remoteip[3]);
        /* 连接到目的地址的指定端口上,当连接成功后回调tcp_client_connected()函数*/
        tcp_connect(tcppcb, & rmtipaddr, TCP_CLIENT_PORT,
            lwip_tcp_client_connected);
    } else res = 1;
    while (res == 0) {
    
    
        key = key_scan(0);
        if (key == KEY1_PRES) break;
        if (key == KEY0_PRES) /* KEY0按下了,发送数据*/ {
    
    
            lwip_tcp_client_usersent(tcppcb); /* 发送数据*/
        }
        if (lwip_client_flag & 1 << 6) /* 是否收到数据*/ {
    
    
            /* 清上一次数据*/
            lcd_fill(30, 230, lcddev.width - 1, lcddev.height - 1, BLACK);
            /* 显示接收到的数据*/
            lcd_show_string(30, 230, lcddev.width - 30, lcddev.height - 230, 16,
                lwip_client_recvbuf, g_point_color);
            lwip_client_flag &= ~(1 << 6); /* 标记数据已经被处理了*/
        }
        if (lwip_client_flag & 1 << 5) /* 是否连接上*/ {
    
    
            if (connflag == 0) {
    
    
                lcd_show_string(30, 190, lcddev.width - 30,
                    lcddev.height - 190, 16,
                    "STATUS:Connected ",
                    g_point_color); /* 提示消息*/
                g_point_color = WHITE;
                lcd_show_string(30, 210, lcddev.width - 30,
                    lcddev.height - 190, 16,
                    "Receive Data:", g_point_color); /* 提示消息*/
                g_point_color = BLUE;
                connflag = 1; /* 标记连接了*/
            }
        } else if (connflag) {
    
    
            lcd_show_string(30, 190, 190, 16, 16, "STATUS:Disconnected",
                g_point_color);
            lcd_fill(30, 210, lcddev.width - 1,
                lcddev.height - 1, BLACK); /* 清屏*/
            connflag = 0; /* 标记连接断开了*/
        }
        lwip_periodic_handle();
        delay_ms(2);
        t++;
        if (t == 200) {
    
    
            /* 未连接上,则尝试重连*/
            if (connflag == 0 && (tcp_client_flag & 1 << 5) == 0) {
    
    
                lwip_tcp_client_connection_close(tcppcb, 0); /* 关闭连接*/
                tcppcb = tcp_new(); /* 创建一个新的pcb */
                if (tcppcb) /* 创建成功*/ {
    
    
                    /* 连接到目的地址的指定端口上,
                    当连接成功后回调tcp_client_connected()函数*/
                    tcp_connect(tcppcb, & rmtipaddr, TCP_CLIENT_PORT,
                        tcp_client_connected);
                }
            }
            t = 0;
            LED0_TOGGLE();
        }
    }
    lwip_tcp_client_connection_close(tcppcb, 0); /* 关闭TCP Client连接*/
    myfree(SRAMIN, tbuf);
}

可见,此函数和UDP 实验一样,根据开发板上的KEY0 和KEY1 设置远程IP 地址,接着
调用RAW 接口函数配置TCP 客户端,配置完成之后连接服务器。
设置远程IP 地址的函数lwip_tcp_client_set_remoteip,如下源码所示:

/**
 * @brief 设置远端IP地址
 * @param 无
 * @retval 无
 */
void lwip_tcp_client_set_remoteip(void) {
    
    
    char * tbuf;
    uint16_t xoff;
    uint8_t key;
    lcd_clear(BLACK);
    g_point_color = WHITE;
    lcd_show_string(30, 30, 200, 16, 16, "STM32", g_point_color);
    lcd_show_string(30, 50, 200, 16, 16, "TCP Client Test", g_point_color);
    lcd_show_string(30, 70, 200, 16, 16, "Remote IP Set", g_point_color);
    lcd_show_string(30, 90, 200, 16, 16, "KEY0:+ KEY2:-", g_point_color);
    lcd_show_string(30, 110, 200, 16, 16, "KEY_UP:OK", g_point_color);
    tbuf = mymalloc(SRAMIN, 100); /* 申请内存*/
    if (tbuf == NULL) return;
    /* 前三个IP保持和DHCP得到的IP一致*/
    lwipdev.remoteip[0] = lwipdev.ip[0];
    lwipdev.remoteip[1] = lwipdev.ip[1];
    lwipdev.remoteip[2] = lwipdev.ip[2];
    /* 远端IP */
    sprintf((char * ) tbuf, "Remote IP:%d.%d.%d.", lwipdev.remoteip[0],
        lwipdev.remoteip[1],
        lwipdev.remoteip[2]);
    lcd_show_string(30, 150, 210, 16, 16, tbuf, g_point_color);
    g_point_color = BLUE;
    xoff = strlen((char * ) tbuf) * 8 + 30;
    lcd_show_xnum(xoff, 150, lwipdev.remoteip[3], 3, 16, 0, g_point_color);
    while (1) {
    
    
        key = key_scan(0);
        if (key == KEY1_PRES) break;
        else if (key) {
    
    
            if (key == KEY0_PRES) lwipdev.remoteip[3] ++; /* IP增加*/
            if (key == KEY2_PRES) lwipdev.remoteip[3] --; /* IP减少*/
            /* 显示新IP */
            lcd_show_xnum(xoff, 150, lwipdev.remoteip[3], 3, 16, 0X80,
                g_point_color);
        }
    }
    myfree(SRAMIN, tbuf);
}

此函数根据开发板上的按键设置远程IP 地址,设置完成之后按下KEY1 退出设置。
TCP 连接建立后的回调函数lwip_tcp_client_connected,如下源码所示:

/**
 * @brief lwIP TCP连接建立后回调函数
 * @param arg : 回调函数传入的参数
 * @param tpcb : TCP控制块
 * @param err : 错误码
 * @retval 返回错误码
 */
err_t lwip_tcp_client_connected(void * arg, struct tcp_pcb * tpcb, err_t err) {
    
    
    struct tcp_client_struct * es = NULL;
    if (err == ERR_OK) {
    
    
        es = (struct tcp_client_struct * ) mem_malloc(sizeof(struct tcp_client_struct)); /* 申请内存*/
        if (es) /* 内存申请成功*/ {
    
    
            es - > state = ES_TCPCLIENT_CONNECTED; /* 状态为连接成功*/
            es - > pcb = tpcb;
            es - > p = NULL;
            tcp_arg(tpcb, es); /* 使用es更新tpcb的callback_arg */
            /* 初始化LwIP的tcp_recv回调功能*/
            tcp_recv(tpcb, lwip_tcp_client_recv);
            tcp_err(tpcb, lwip_tcp_client_error); /* 初始化tcp_err()回调函数*/
            /* 初始化LwIP的tcp_sent回调功能*/
            tcp_sent(tpcb, lwip_tcp_client_sent);
            /* 初始化LwIP的tcp_poll回调功能*/
            tcp_poll(tpcb, lwip_tcp_client_poll, 1);
            tcp_client_flag |= 1 << 5; /* 标记连接到服务器了*/
            err = ERR_OK;
        } else {
    
    
            lwip_tcp_client_connection_close(tpcb, es); /* 关闭连接*/
            err = ERR_MEM; /* 返回内存分配错误*/
        }
    } else {
    
    
        lwip_tcp_client_connection_close(tpcb, 0); /* 关闭连接*/
    }
    return err;
}

这个回调函数由用户编写,由tcp_connect 函数注册此函数。简单来讲,就是让TCP 控制
块内的函数指针指向该函数。
lwip_tcp_client_recv 函数是当接收到数据时的回调函数,在这个函数中我们根据不同的状
态有不同的处理,这里最重要的就是当处于连接状态并且接收到数据时的处理,这个时候我们
将遍历完接收数据的pbuf 链表,将链表中的所有数据拷贝到lwip_tcp_client_recvbuf 中,这个
过程和UDP 的接收处理过程相似。数据接收成功以后我们将lwip_client_flag 的bit5 置1,表
示接收到数据,lwip_tcp_client_recv 函数代码如下。

/**
 * @brief lwIP tcp_recv()函数的回调函数
 * @param arg : 回调函数传入的参数
 * @param tpcb : TCP控制块
 * @param p : 网络数据包
 * @param err : 错误码
 * @retval 返回错误码
 */
err_t lwip_tcp_client_recv(void * arg, struct tcp_pcb * tpcb,
    struct pbuf * p, err_t err) {
    
    
    uint32_t data_len = 0;
    struct pbuf * q;
    struct tcp_client_struct * es;
    err_t ret_err;
    LWIP_ASSERT("arg != NULL", arg != NULL);
    es = (struct tcp_client_struct * ) arg;
    if (p == NULL) /* 如果从服务器接收到空的数据帧就关闭连接*/ {
    
    
        es - > state = ES_TCPCLIENT_CLOSING; /* 需要关闭TCP 连接了*/
        es - > p = p;
        ret_err = ERR_OK;
    } else if (err != ERR_OK) /* 当接收到一个非空的数据帧,但是err!=ERR_OK */ {
    
    
        if (p) pbuf_free(p); /* 释放接收pbuf */
        ret_err = err;
    } else if (es - > state == ES_TCPCLIENT_CONNECTED) /* 当处于连接状态时*/ {
    
    
        if (p != NULL) /* 当处于连接状态并且接收到的数据不为空时*/ {
    
    
            /* 数据接收缓冲区清零*/
            memset(lwip_client_recvbuf, 0, TCP_CLIENT_RX_BUFSIZE);
            for (q = p; q != NULL; q = q - > next) /* 遍历完整个pbuf链表*/ {
    
    
                /* 判断要拷贝到TCP_CLIENT_RX_BUFSIZE中的数据是否大于
                TCP_CLIENT_RX_BUFSIZE的剩余空间,如果大于*/
                /* 的话就只拷贝TCP_CLIENT_RX_BUFSIZE中剩余长度的数据,
                否则的话就拷贝所有的数据*/
                if (q - > len > (LWIP_DEMO_RX_BUFSIZE - data_len)) memcpy(lwip_client_recvbuf + data_len, q - > payload, (LWIP_DEMO_RX_BUFSIZE - data_len)); /* 拷贝数据*/
                else memcpy(lwip_client_recvbuf + data_len, q - > payload, q - > len);
                data_len += q - > len;
                /* 超出TCP客户端接收数组,跳出*/
                if (data_len > LWIP_DEMO_RX_BUFSIZE) break;
            }
            tcp_client_flag |= 1 << 6; /* 标记接收到数据了*/
            /*用于获取接收数据,通知LWIP可以获取更多数据*/
            tcp_recved(tpcb, p - > tot_len);
            pbuf_free(p); /* 释放内存*/
            ret_err = ERR_OK;
        }
    } else /* 接收到数据但是连接已经关闭*/ {
    
    
        /* 用于获取接收数据,通知LWIP可以获取更多数据*/
        tcp_recved(tpcb, p - > tot_len);
        es - > p = NULL;
        pbuf_free(p); /* 释放内存*/
        ret_err = ERR_OK;
    }
    return ret_err;
}

lwip_tcp_client_error 函数是控制块中errf 字段的回调函数,当出现知名错误的时候就会被
调用,这里我们没有实现这个函数,用户可以根据自己的实际情况来实现这个函数。
lwip_tcp_client_poll 函数为控制块中poll 字段的回调函数,这个函数会被周期调用,因此
在这个函数中我们可以将要发送的数据发送出去。通过lwip_client_flag 的bit7 来判断是否有
数据要发送,因为lwIP 中处理数据用的是pbuf 结构体组成的链表,因此如果有数据要发送的
话就将发送缓冲区lwip_tcp_client_sendbuf 中的待发送数据放进pbuf 链表中,这个我们使用
pbuf_take 来实现这个过程,然后我们调用lwip_tcp_client_senddata 函数将数据发送出去,发送
完成以后记得将lwip_client_flag 的bit7 清零,如下源码所示:

/**
 * @brief lwIP tcp_poll的回调函数
 * @param arg : 回调函数传入的参数
 * @param tpcb: TCP控制块
 * @retval ERR_OK
 */
err_t lwip_tcp_client_poll(void * arg, struct tcp_pcb * tpcb) {
    
    
    err_t ret_err;
    struct tcp_client_struct * es;
    es = (struct tcp_client_struct * ) arg;
    if (es - > state == ES_TCPCLIENT_CLOSING) /* 连接断开*/ {
    
    
        lwip_tcp_client_connection_close(tpcb, es); /* 关闭TCP连接*/
    }
    ret_err = ERR_OK;
    return ret_err;
}

lwip_tcp_client_sent 函数为控制块中的sent 字段的回调函数,这个函数中主要调用了我们
下面要讲的lwip_tcp_client_senddata 这个函数,lwip_tcp_client_sent 函数源码如下。

/**
 * @brief lwIP tcp_sent的回调函数(当从远端主机接收到ACK信号后发送数据)
 * @param arg : 回调函数传入的参数
 * @param tpcb: TCP控制块
 * @param len : 长度
 * @retval ERR_OK
 */
err_t lwip_tcp_client_sent(void * arg, struct tcp_pcb * tpcb, u16_t len) {
    
    
    struct tcp_client_struct * es;
    es = (struct tcp_client_struct * ) arg;
    if (es - > p) lwip_tcp_client_senddata(tpcb, es); /* 发送数据*/
    return ERR_OK;
}

lwip_tcp_client_senddata 函数用来发送数据,在这个函数中我们使用tcp_write 函数将要发
送的数据加入到发送缓冲队列中,最后调用tcp_output 函数将发送缓冲队列中的数据发送出去,
这个函数的代码如下。

/**
 * @brief 用来发送数据
 * @param tpcb: TCP控制块
 * @param es : LWIP回调函数使用的结构体
 * @retval 无
 */
void lwip_tcp_client_senddata(struct tcp_pcb * tpcb,
    struct tcp_client_struct * es) {
    
    
    struct pbuf * ptr;
    err_t wr_err = ERR_OK;
    /* 将要发送的数据加入到发送缓冲队列中*/
    while ((wr_err == ERR_OK) && es - > p && (es - > p - > len <= tcp_sndbuf(tpcb))) {
    
    
        ptr = es - > p;
        wr_err = tcp_write(tpcb, ptr - > payload, ptr - > len, 1);
        if (wr_err == ERR_OK) {
    
    
            es - > p = ptr - > next; /* 指向下一个pbuf */
            if (es - > p) pbuf_ref(es - > p); /* pbuf的ref加一*/
            pbuf_free(ptr); /* 释放ptr */
        } else if (wr_err == ERR_MEM) es - > p = ptr;
        tcp_output(tpcb); /* 将发送缓冲队列中的数据立即发送出去*/
    }
}

lwip_tcp_client_connection_close 函数的功能是关闭与服务器的连接,通过调用tcp_abort
函数来关闭与服务器的连接,然后注销掉控制块中的回调函数,将lwip_client_flag 的bit5 置1,
标记连接断开,lwip_tcp_client_connection_close 函数源码如下。

/**
 * @brief 关闭与服务器的连接
 * @param tpcb: TCP控制块
 * @param es : LWIP回调函数使用的结构体
 * @retval 无
 */
void lwip_tcp_client_connection_close(struct tcp_pcb * tpcb,
    struct tcp_client_struct * es) {
    
    
    /* 移除回调*/
    tcp_abort(tpcb); /* 终止连接,删除pcb控制块*/
    tcp_arg(tpcb, NULL);
    tcp_recv(tpcb, NULL);
    tcp_sent(tpcb, NULL);
    tcp_err(tpcb, NULL);
    tcp_poll(tpcb, NULL, 0);
    if (es) mem_free(es);
    tcp_client_flag &= ~(1 << 5); /* 标记连接断开了*/
    lcd_show_string(30, 30, 200, 16, 16, "STM32", g_point_color);
    lcd_show_string(30, 50, 200, 16, 16, "TCPclient Test", g_point_color);
    lcd_show_string(30, 70, 200, 16, 16, "ATOM@正点原子", g_point_color);
    lcd_show_string(30, 90, 200, 16, 16, "KEY1:Connect", g_point_color);
    lcd_show_string(30, 190, 210, 16, 16, "STATUS:Disconnected", g_point_color);
}

至此,lwip_demo.c 文件就讲完了,接下来就是编写main 函数,main 函数基本和UDP 实
验的相同。

下载验证

代码编译成功之后下载代码到开发板中。打开网络调试助手软件设置为如下图的信息。
在这里插入图片描述

开发板上电,等待出现12.3.3.2 所示画面,我们设置远端IP 地址为电脑的IP 地址,也就
是图12.3.1 中的本地IP 地址,设置好以后按KEY_UP 键确认,确认后进入图12.2.3.3 所示界
面,当STATUS 为Connected 的时候就可以和网络调试助手互相发送数据了。

在这里插入图片描述
图12.2.3.2 设置服务器IP 地址

在这里插入图片描述

图12.2.3.3 连接到服务器
我们通过网络调试助手向开发板发送:http://www.openedv.com,此时开发板LCD 上显示
接收到的数据如图12.2.3.4 所示,按下KEY0 键向网络调试助手发送数据。
在这里插入图片描述

NETCONN 编程接口TCP 客户端实验

本章实验中开发板做TCP 客户端,网络调试助手为TCP 服务器。开发板连接到TCP 服务
器(网络调试助手),网络调试助手给开发板发送数据,开发板接收数据并通过串口将接收到的
数据发送到串口调试助手上,也可以通过按键从开发板向网络调试助手发送数据。

NETCONN 实现TCP 客户端连接步骤

NETCONN 实现TCP 客户端连接有以下几步:
①调用函数netconn_new 创建TCP 控制块。
②调用函数netconn_connect 连接服务器。
③设置接收超时时间tcp_clientconn->recv_timeout。
④调用函数netconn_getaddr 获取远端IP 地址和端口号。
⑤调用函数netconn_write 和netconn_recv 收发数据。
至于TCP 协议的知识,请读者擦看第十二章的内容。

NETCONN 接口的TCPClient 实验

硬件设计

  1. 例程功能
    本实验使用NETCONN 编程接口实现TCPClient 连接,我们可通过按下KEY0 按键发送数
    据至网络调试助手,还可以接收网络调试助手发送的数据,并在LCD 显示屏上显示。
    该实验的实验工程,请参考《lwIP 例程8 lwIP_NETCONN_TCPClient 实验》。

软件设计

17.2.2.1 netconn 的TCPClient 连接步骤

  1. 创建TCP 控制块
    调用函数netconn_new 创建TCP 控制块。
  2. 绑定远程IP 地址与端口号
    调用函数netconn_connect 绑定远程IP 地址和远程端口号。
  3. 接收数据
    netconn_recv 接收数据。
  4. 发送数据
    调用函数netconn_write 发送数据。
    17.2.2.2 程序流程图
    本实验的程序流程图,如下图所示:

在这里插入图片描述

17.2.2.3 程序解析
打开我们的例程,找到lwip_demo.c 和lwip_demo.h 两个文件,这两个文件就是我本章实
验的源码,在lwip_demo.c 中我们实现了一个函数lwip_demo,同上一章一样,都有操作系统
的支持下,如下源码所示:

void lwip_demo(void) {
    
    
    uint32_t data_len = 0;
    struct pbuf * q;
    err_t err, recv_err;
    ip4_addr_t server_ipaddr, loca_ipaddr;
    static uint16_t server_port, loca_port;
    char * tbuf;
    server_port = LWIP_DEMO_PORT;
    IP4_ADDR( & server_ipaddr, DEST_IP_ADDR0, DEST_IP_ADDR1,
        DEST_IP_ADDR2, DEST_IP_ADDR3); /* 构造目的IP地址*/
    tbuf = mymalloc(SRAMIN, 200); /* 申请内存*/
    sprintf((char * ) tbuf, "Port:%d", LWIP_DEMO_PORT); /* 客户端端口号*/
    lcd_show_string(5, 150, 200, 16, 16, tbuf, BLUE);
    myfree(SRAMIN, tbuf);
    while (1) {
    
    
        tcp_clientconn = netconn_new(NETCONN_TCP); /*创建一个TCP链接*/
        /*连接服务器*/
        err = netconn_connect(tcp_clientconn, & server_ipaddr, server_port);
        if (err != ERR_OK) {
    
    
            printf("接连失败\r\n");
            /*返回值不等于ERR_OK,删除tcp_clientconn连接*/
            netconn_delete(tcp_clientconn);
        } else if (err == ERR_OK) /*处理新连接的数据*/ {
    
    
            struct netbuf * recvbuf;
            tcp_clientconn - > recv_timeout = 10;
            /*获取本地IP主机IP地址和端口号*/
            netconn_getaddr(tcp_clientconn, & loca_ipaddr, & loca_port, 1);
            printf("连接上服务器%d.%d.%d.%d,本机端口号为:%d\r\n",
                DEST_IP_ADDR0,
                DEST_IP_ADDR1,
                DEST_IP_ADDR2,
                DEST_IP_ADDR3, loca_port);
            while (1) {
    
    
                /*有数据要发送*/
                if ((tcp_client_flag & LWIP_SEND_DATA) == LWIP_SEND_DATA) {
    
    
                    /* 发送tcp_server_sentbuf中的数据*/
                    err = netconn_write(tcp_clientconn, tcp_client_sendbuf,
                        strlen((char * ) tcp_client_sendbuf), NETCONN_COPY);
                    if (err != ERR_OK) {
    
    
                        printf("发送失败\r\n");
                    }
                    tcp_client_flag &= ~LWIP_SEND_DATA;
                }
                /*接收到数据*/
                if ((recv_err = netconn_recv(tcp_clientconn, & recvbuf)) == ERR_OK) {
    
    
                    taskENTER_CRITICAL(); /*进入临界区*/
                    /*数据接收缓冲区清零*/
                    memset(lwip_demo_recvbuf, 0, LWIP_DEMO_RX_BUFSIZE);
                    for (q = recvbuf - > p; q != NULL; q = q - > next) /*遍历完整个pbuf链表*/ {
    
    
                        if (q - > len > (LWIP_DEMO_RX_BUFSIZE - data_len)) {
    
    
                            memcpy(lwip_demo_recvbuf + data_len, q - > payload, (LWIP_DEMO_RX_BUFSIZE - data_len)); /* 拷贝数据*/
                        } else {
    
    
                            memcpy(lwip_demo_recvbuf + data_len, q - > payload,
                                q - > len);
                        }
                        data_len += q - > len;
                        if (data_len > TCP_CLIENT_RX_BUFSIZE) {
    
    
                            break; /*超出TCP客户端接收数组,跳出*/
                        }
                    }
                    taskEXIT_CRITICAL(); /*退出临界区*/
                    data_len = 0; /*复制完成后data_len要清零*/
                    printf("%s\r\n", lwip_demo_recvbuf);
                    netbuf_delete(recvbuf);
                } else if (recv_err == ERR_CLSD) /*关闭连接*/ {
    
    
                    netconn_close(tcp_clientconn);
                    netconn_delete(tcp_clientconn);
                    printf("服务器%d.%d.%d.%d断开连接\r\n", DEST_IP_ADDR0,
                        DEST_IP_ADDR1, DEST_IP_ADDR2, DEST_IP_ADDR3);
                    lcd_fill(5, 89, lcddev.width, 110, WHITE);
                    break;
                }
            }
        }
    }
}

上述的源码结构和上一章节的UDP 实验非常相似,它们唯一不同的是连接步骤以及发送函数不同,注意:上述函数做了一个判断服务器与客户端的连接状态,如果这个连接状态是断
开状态,则系统不断的调用函数netconn_connect 连接服务器,直到连接成功才进入第二个
while 循环执行发送接收工作。

下载验证

代码编译完成后下载到开发板中,初始化完成之后我们来看一下LCD 显示的内容,如下
图所示。

在这里插入图片描述

我们在来看一下串口调试助手如图17.2.3.2 所示,在串口调试助手上也输出了我们开发板
的IP 地址,子网掩码、默认网关等信息。

在这里插入图片描述

我们通过网络调试助手发送数据到开发板当中,结果如图17.2.3.3 所示,当然我们可以通
过开发板上的KEY0 发送数据到网络调式助手当中,如图17.2.3.4 所示:

在这里插入图片描述

图17.2.3.3 LCD 显示

在这里插入图片描述
图17.2.3.4 网络调试助手接收数据

Socket 编程接口TCP 客户端实验

关于TCP 协议的相关知识,请参考第12 章的内容。本章,笔者重点讲解lwIP 的Socket
接口如何配置TCP 客户端,并在此基础上实现收发功能。本章分为如下几个部分:
21.1 Socket 编程TCP 客户端流程
21.2 Socket 接口的TCPClient 实验

Socket 编程TCP 客户端流程

实现TCP 客户端之前,用户必须先配置结构体sockaddr_in 的成员变量才能实现
TCPClient 连接,该配置步骤如下所示:
①sin_family 设置为AF_INET 表示IPv4 网络协议。
②sin_port 为设置端口号。
③sin_addr.s_addr 设置远程IP 地址。
④调用函数Socket 创建Socket 连接,注意:该函数的第二个参数SOCK_STREAM 表
示TCP 连接,SOCK_DGRAM 表示UDP 连接。
⑤调用函数connect 连接远程IP 地址。
⑥调用收发函数实现远程通讯。

Socket 接口的TCPClient 实验

21.2.1 硬件设计

  1. 例程功能
    本实验使用Socket 编程接口实现TCPClient 客户端,并可通过按键向所连接的TCP 服务
    器发送数据,也能够接收来自TCP 服务器的数据,并实时显示至LCD 屏幕上。
    该实验的实验工程,请参考《lwIP 例程11 lwIP_SOCKET_TCPClient 实验》。
    21.2.2 软件设计
    21.2.2.1 程序流程图
    本实验的程序流程图,如下图所示:

在这里插入图片描述

1.2.2.2 程序解析
本实验,我们着重讲解lwip_demo.c 文件,该文件实现了三个函数,它们分别为
lwip_data_send、lwip_demo 和lwip_send_thread 函数,下面笔者分别地讲解它们的实现功能。

/**
 * @brief 发送数据线程
 * @param 无
 * @retval 无
 */
void lwip_data_send(void) {
    
    
    sys_thread_new("lwip_send_thread", lwip_send_thread, NULL,
        512, LWIP_SEND_THREAD_PRIO);
}

此函数调用sys_thread_new 函数创建发送数据线程,它的线程函数为lwip_send_thread,
稍后我们重点会讲解。

/**
 * @brief lwip_demo实验入口
 * @param 无
 * @retval 无
 */
void lwip_demo(void) {
    
    
    struct sockaddr_in atk_client_addr;
    err_t err;
    int recv_data_len;
    BaseType_t lwip_err;
    char * tbuf;
    lwip_data_send(); /* 创建发送数据线程*/
    while (1) {
    
    
        sock_start: lwip_connect_state = 0;
        atk_client_addr.sin_family = AF_INET; /* 表示IPv4网络协议*/
        atk_client_addr.sin_port = htons(LWIP_DEMO_PORT); /* 端口号*/
        atk_client_addr.sin_addr.s_addr = inet_addr(IP_ADDR); /* 远程IP地址*/
        sock = Socket(AF_INET, SOCK_STREAM, 0); /* 可靠数据流交付服务既是TCP协议*/
        memset( & (atk_client_addr.sin_zero), 0,
            sizeof(atk_client_addr.sin_zero));
        tbuf = mymalloc(SRAMIN, 200); /* 申请内存*/
        sprintf((char * ) tbuf, "Port:%d", LWIP_DEMO_PORT); /* 客户端端口号*/
        lcd_show_string(5, 150, 200, 16, 16, tbuf, BLUE);
        /* 连接远程IP地址*/
        err = connect(sock, (struct sockaddr * ) & atk_client_addr,
            sizeof(struct sockaddr));
        if (err == -1) {
    
    
            printf("连接失败\r\n");
            sock = -1;
            closeSocket(sock);
            myfree(SRAMIN, tbuf);
            vTaskDelay(10);
            goto sock_start;
        }
        printf("连接成功\r\n");
        lwip_connect_state = 1;
        while (1) {
    
    
            recv_data_len = recv(sock, lwip_demo_recvbuf,
                LWIP_DEMO_RX_BUFSIZE, 0);
            if (recv_data_len <= 0) {
    
    
                closeSocket(sock);
                sock = -1;
                lcd_fill(5, 89, lcddev.width, 110, WHITE);
                lcd_show_string(5, 90, 200, 16, 16, "State:Disconnect", BLUE);
                myfree(SRAMIN, tbuf);
                goto sock_start;
            }
            /* 接收的数据*/
            lwip_err = xQueueSend(Display_Queue, & lwip_demo_recvbuf, 0);
            if (lwip_err == errQUEUE_FULL) {
    
    
                printf("队列Key_Queue已满,数据发送失败!\r\n");
            }
            vTaskDelay(10);
        }
    }
}

根据21.1 小节的流程配置server_addr 结构体的字段,配置完成之后调用connect 连接远程
服务器,接着调用recv 函数接收客户端的数据,并且把数据以消息的方式发送至其他线程当
中。

/**
 * @brief 发送数据线程函数
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void lwip_send_thread(void * pvParameters) {
    
    
    pvParameters = pvParameters;
    err_t err;
    while (1) {
    
    
        while (1) {
    
    
            if (((lwip_send_flag & LWIP_SEND_DATA) == LWIP_SEND_DATA) && (lwip_connect_state == 1)) /* 有数据要发送*/ {
    
    
                err = write(sock, lwip_demo_sendbuf, sizeof(lwip_demo_sendbuf));
                if (err < 0) {
    
    
                    break;
                }
                lwip_send_flag &= ~LWIP_SEND_DATA;
            }
            vTaskDelay(10);
        }
        closeSocket(sock);
    }
}

此线程函数非常简单,它主要判断lwip_send_flag 变量的状态,若该变量的状态为发送状
态,则程序调用write 函数发送数据,并且清除lwip_send_flag 变量的状态。

21.2.3 下载验证
初始化完成之后LCD 显示以下信息,如下图所示:

在这里插入图片描述

我们通过网络调试助手发送数据至开发板,开发板接收完成之后LCD 在指定位置显示接
收的数据,如下图所示:

在这里插入图片描述
当然,读者可通过KEY0 按键发送数据至网络调试助手。

基于MQTT 协议连接阿里云服务器

本章主要学习lwIP 提供的MQTT 协议文件使用,通过MQTT 协议将设备连接到阿里云服
务器,实现远程互通。由于MQTT 协议是基于TCP 的协议实现的,所以我们只需要在单片机端实现TCP 客户端程序并使用lwIP 提供的MQTT 文件来连接阿里云服务器。

MQTT 协议简介

(1) MQTT 是什么?
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(Publish/Subscribe)模式的轻量级通讯协议,该协议构建于TCP/IP 协议上,由IBM 在1999 年发布,目前最新版本为v3.1.1。MQTT 最大的优点在于可以以极少的代码和有限的带宽,为远程设备提供实时可靠的消息服务。做为一种低开销、低带宽占用的即时通讯协议,MQTT在物联网、小型设备、移动应用等方面有广泛的应用,MQTT 协议属于应用层。
(2) MQTT 协议特点
MQTT 是一个基于客户端与服务器的消息发布/订阅传输协议。MQTT 协议是轻量、简单
开放和易于实现的,这些特点使它适用范围非常广泛。在很多情况下,包括受限境中,如:机器与机器(M2M)通信和物联网(IoT)。其在,通过卫星链路通信传感器、医疗设备、智能家居、及一些小型化设备中已广泛使用。
(3) MQTT 协议原理及实现方式
实现MQTT 协议需要:客户端和服务器端MQTT 协议中有三种身份:发布者(Publish)、
代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者,如下图所示。

在这里插入图片描述

MQTT 传输的消息分为:主题(Topic)和消息的内容(payload)两部分。
Topic:可以理解为消息的类型,订阅者订阅(Subscribe)后,就会收到该主题的消息内
容(payload)。
Payload:可以理解为消息的内容,是指订阅者具体要使用的内容。

MQTT 协议实现原理

  1. 要在客户端与代理服务端建立一个TCP 连接,建立连接的过程是由客户端主动发起的,
    代理服务一直是处于指定端口的监听状态,当监听到有客户端要接入的时候,就会立刻去处理。
    客户端在发起连接请求时,携带客户端ID、账号、密码(无账号密码使用除外,正式项目不会允许这样)、心跳间隔时间等数据。代理服务收到后检查自己的连接权限配置中是否允许该账号密码连接,如果允许则建立会话标识并保存,绑定客户端ID 与会话,并记录心跳间隔时间(判断是否掉线和启动遗嘱时用)和遗嘱消息等,然后回发连接成功确认消息给客户端,客户端收到连接成功的确认消息后,进入下一步(通常是开始订阅主题,如果不需要订阅则跳过)。如下图所示:

在这里插入图片描述

  1. 客户端将需要订阅的主题经过SUBSCRIBE 报文发送给代理服务,代理服务则将这个主
    题记录到该客户端ID 下(以后有这个主题发布就会发送给该客户端),然后回复确认消息SUBACK 报文,客户端接到SUBACK 报文后知道已经订阅成功,则处于等待监听代理服务推送的消息,也可以继续订阅其他主题或发布主题,如下图所示:

在这里插入图片描述

  1. 当某一客户端发布一个主题到代理服务后,代理服务先回复该客户端收到主题的确认消
    息,该客户端收到确认后就可以继续自己的逻辑了。但这时主题消息还没有发给订阅了这个主题的客户端,代理要根据质量级别(QoS)来决定怎样处理这个主题。所以这里充分体现了是MQTT 协议是异步通信模式,不是立即端到端反应的,如下图所示:

在这里插入图片描述

如果发布和订阅时的质量级别QoS 都是至多一次,那代理服务则检查当前订阅这个主题
的客户端是否在线,在线则转发一次,收到与否不再做任何处理。这种质量对系统压力最小。

如果发布和订阅时的质量级别QoS 都是至少一次,那要保证代理服务和订阅的客户端都
有成功收到才可以,否则会尝试补充发送(具体机制后面讨论)。这也可能会出现同一主题多次重复发送的情况。这种质量对系统压力较大。

如果发布和订阅时的质量级别QoS 都是只有一次,那要保证代理服务和订阅的客户端都
有成功收到,并只收到一次不会重复发送(具体机制后面讨论)。这种质量对系统压力最大。

移植MQTT 协议

其实移植lwIP 的MQTT 文件是非常简单的,只将lwip\src\apps\mqt 路径下的mqtt.c 文件
添加到工程当中,这里我们在工程中添加一个名为Middlewares/lwip/src/apps 分组,该分组用来添加lwIP 应用层的文件,如下图所示所示:

在这里插入图片描述

mqtt.c 文件是lwIP 根据MQTT 协议规则编写而来的,如果用户不使用这个文件,请自行
移植MQTT 协议包。

在Middlewares/lwip/lwip_app 分组添加hmac_sha1 和sha1 文件,这些文件用来计算核心密
钥,这两个文件可在阿里云官方下载。

配置远程服务器

配置阿里云服务器步骤
第一步:注册阿里云平台,打开产品分类/物联网Iot/物联网应用开发,如下图所示。

在这里插入图片描述

点击上图中的“立刻使用”按键进去物联网应用开发页面。

第二步:在物联网应用开发页面下点击项目管理/新建项目/新建空白项目,在此界面下填
写项目名称等相关信息,如下图所示:

在这里插入图片描述

创建项目完成之后在项目管理页面下点击项目进去子项目管理界面,如下图所示:

在这里插入图片描述

第三步:在上图中点击产品,如下图所示:

在这里插入图片描述

注:上图中的节点类型、连网方式、数据格式以及认证模式的选择,其他产品参数根据用
户爱好设置。

第三步:创建产品之后点击图26.1.3.3 中的设备选项添加设备,如下图所示。

在这里插入图片描述

第五步:在设备页面下找到我们刚刚创建的设备,如下图所示。

在这里插入图片描述

这三个参数非常重要!!!!!!!!!!,在本章实验中会用到。
第六步:打开“产品/查看/功能定义”路径,在该路径下添加功能定义,如下图所示。
在这里插入图片描述

第七步:打开自定义功能并发布上线,这里我们添加了两个CurrentTemperature 和
RelativeHumidity 标签。

阿里云MQTT 协议实验

硬件设计

  1. 例程功能
    本章的目标是,lwIP 连接阿里云实现数据上存。
    该实验的实验工程,请参考《lwIP 例程17 lwIP_Aliyun_MQTT 实验》。
    26.2.2 软件设计
    26.2.2.1 MQTT 配置步骤
  1. 配置MCU 为TCP 客户端模式
    配置为TCP 客户端等步骤,请参考第21 章。
  2. DNS 解析阿里云网页转成IP 地址
    调用函数gethostbyname 获取DNS 解析的IP 地址。
  3. MQTT 连接
    调用函数mqtt_client_connect 连接服务器。
  4. 连接状态
    对服务器发布和订阅操作。
  5. 循环发布数据到服务器当中
    在lwip_demo 函数的while()语句中定时1s 调用函数mqtt_publish 发布数据至服务器。
    26.2.2.2 程序流程图
    本实验的程序流程图,如下图所示。

在这里插入图片描述

程序解析
我们打开lwip_deom.h 文件,在这个文件中我们定义了阿里云服务器创建设备的配置项,
另外还声明了lwip_demo 函数,关于阿里云服务器的MQTT 主题请大家查看阿里云相关手册。

重点关注的是lwip_deom.c 这个文件,在这个文件定义了8 个函数,如下表所示。

在这里插入图片描述
我们首先看一下lwip_demo 函数,该函数的代码如下。

/**
 * @brief lwip_demo进程
 * @param 无
 * @retval 无
 */
void lwip_demo(void) {
    
    
    struct hostent * server;
    static struct mqtt_connect_client_info_t mqtt_client_info;
    server = gethostbyname((char * ) HOST_NAME); /* 对oneNET服务器地址解析*/
    /* 把解析好的地址存放在mqtt_ip变量当中*/
    memcpy( & mqtt_ip, server - > h_addr, server - > h_length);
    char * PASSWORD;
    PASSWORD = mymalloc(SRAMIN, 300); /* 为密码申请内存*/
    /* 通过hmac_sha1算法得到password */
    lwip_ali_get_password(DEVICE_SECRET, CONTENT, PASSWORD);
    /* 设置一个空的客户端信息结构*/
    memset( & mqtt_client_info, 0, sizeof(mqtt_client_info));
    /* 设置客户端的信息量*/
    mqtt_client_info.client_id = (char * ) CLIENT_ID; /* 设备名称*/
    mqtt_client_info.client_user = (char * ) USER_NAME; /* 产品ID */
    mqtt_client_info.client_pass = (char * ) PASSWORD; /* 计算出来的密码*/
    mqtt_client_info.keep_alive = 100; /* 保活时间*/
    mqtt_client_info.will_msg = NULL;
    mqtt_client_info.will_qos = NULL;
    mqtt_client_info.will_retain = 0;
    mqtt_client_info.will_topic = 0;
    myfree(SRAMIN, PASSWORD); /* 释放内存*/
    /* 创建MQTT客户端控制块*/
    mqtt_client = mqtt_client_new();
    /* 连接服务器*/
    mqtt_client_connect(mqtt_client, /* 服务器控制块*/ & mqtt_ip, MQTT_PORT, /* 服务器IP与端口号*/
        mqtt_connection_cb,
        /* 设置服务器连接回调函数*/
        LWIP_CONST_CAST(void * , & mqtt_client_info), & mqtt_client_info); /* MQTT连接信息*/
    while (1) {
    
    
        if (publish_flag == 1) {
    
    
            temp = 30 + rand() % 10 + 1; /* 温度的数据*/
            humid = 54.8 + rand() % 10 + 1; /* 湿度的数据*/
            sprintf((char * ) payload_out,
                "{\"params\":{\"CurrentTemperature\":+ % 0.1 f, \"RelativeHumidity\":%0.1f},\"method\":\
                "thing.event.property.post\"}", temp, humid);
            payload_out_len = strlen((char * ) payload_out);
            mqtt_publish(mqtt_client, DEVICE_PUBLISH, payload_out,
                payload_out_len, 1, 0, mqtt_publish_request_cb, NULL);
        }
        vTaskDelay(1000);
    }
}

此函数非常简单,首先我们调用gethostbyname 函数解析阿里云的域名,根据这个域名来
连接阿里云服务器,其次使用一个结构体配置MQTT 客户端的信息并调用mqtt_client_new 函数创建MQTT 服务器控制块,接着我们调用mqtt_client_connect 函数连接阿里云服务器并添加mqtt_connection_cb 连接回调函数,最后在while()语句中判断是否订阅操作成功,如果系统订阅成功,则构建MQTT 消息,并调用mqtt_publish 函数发布。

接下来我们来讲解一下mqtt_client_connect 函数的作用,如下源码所示:

/**
 * @brief mqtt连接回调函数
 * @param client:客户端控制块
 * @param arg:传入的参数
 * @param status:连接状态
 * @retval 无
 */
static void mqtt_connection_cb(mqtt_client_t * client, void * arg,
    mqtt_connection_status_t status) {
    
    
    err_t err;
    const struct mqtt_connect_client_info_t * client_info =
        (const struct mqtt_connect_client_info_t * ) arg;
    LWIP_UNUSED_ARG(client);
    printf("\r\nMQTT client \"%s\" connection cb: status %d\r\n",
        client_info - > client_id, (int) status);
    /* 判断是否连接*/
    if (status == MQTT_CONNECT_ACCEPTED) {
    
    
        /* 判断是否连接*/
        if (mqtt_client_is_connected(client)) {
    
    
            /* 设置传入发布请求的回调*/
            mqtt_set_inpub_callback(mqtt_client,
                mqtt_incoming_publish_cb,
                mqtt_incoming_data_cb,
                NULL);
            /* 订阅操作,并设置订阅响应会回调函数mqtt_sub_request_cb */
            err = mqtt_subscribe(client, DEVICE_SUBSCRIBE, 1,
                mqtt_request_cb, arg);
            if (err == ERR_OK) {
    
    
                printf("mqtt_subscribe return: %d\n", err);
                lcd_show_string(5, 170, 210, 16, 16,
                    "mqtt_subscribe succeed", BLUE);
            }
        }
    } else /* 连接失败*/ {
    
    
        printf("mqtt_connection_cb: Disconnected, reason: %d\n", status);
    }
}

此函数也是非常简单,它主要调用函数mqtt_client_is_connected 判断是否已经连接服务器,如果连接成功,则程序调用函数mqtt_set_inpub_callback 添加mqtt_incoming_publish_cb 和mqtt_incoming_data_cb 回调函数,这些回调函数需要根据客户端以及服务器的发布操作才能进去该回调函数,最后我们调用函数mqtt_subscribe 对服务器进行订阅操作并且添加mqtt_request_cb 订阅响应回调函数。

下载验证

下载完代码后,在浏览器上打开阿里云平台,并在指定的网页查看上存数据,如下图所示。

在这里插入图片描述

基于MQTT 协议连接OneNET 服务器

本章主要介绍lwIP 如何通过MQTT 协议将设备连接到OneNET 平台,并通过MQTT 协议
远程互通。关于MQTT 协议的知识,请参考第二十六章节的内容。

配置OneNET 平台

配置OneNET 服务器步骤:
第一步:首先打开OneNET 服务器并注册账号,注册之后在主界面下打开产品服务页面下
的MQTT 物联网套件,如下图所示:

在这里插入图片描述

第二步:在上图中点击“立刻使用”选项,页面跳转完成之后点击“添加产品”选项,此
时该页面会弹出产品信息小界面,这里我们根据自己的项目填写相关的信息,如下图所示:

在这里插入图片描述

上图中,我们重点添加的选项有联网方式和设备接入协议,这里笔者选择移动蜂窝网络以
及MQTT 协议接入,至于其他选项根据爱好选择。创建MQTT 产品之后用户可以得到该产品
的信息,如下图所示:

在这里插入图片描述

本实验会用到上述的产品信息,例如产品ID(366007)、“access_key”产品密钥以及产品
名称(MQTT_TSET)等。
第三步:在产品页面下点击设备列表添加设备,如下图所示:

在这里插入图片描述

第四步:在上图创建的设备中,点击右边的详情标签进入标签的链接页面,在这个页面下
我们得到以下设备信息,如下图所示:

在这里插入图片描述

本实验会用到上图中的设备ID(617747917)、设备名称MQTT 以及“key”设备的密钥。
下面我们打开OneNET 在线开发指南,在这个指南中找到服务器地址,这些服务器地址就
是MQTT 服务器地址,如下图所示:

在这里插入图片描述

上图中,OneNTE 的MQTT 服务器具有两个连接方式,一种是加密接口连接,而另一种
是非加密接口连接,本章实验使用的是非加密接口连接MQTT 服务器。
注:MQTT 物联网套件采用安全鉴权策略进行访问认证,即通过核心密钥计算的token 进
行访问认证,简单来讲,用户想连接OneNET 的MQTT 服务器必须计算核心密钥,这个密钥
是根据我们前面创建的产品和设备相关的信息计算得来的,密钥的计算方法可以使用OneNET
提供的token 生成工具计算,该软件可在这个网址下载:https://open.iot.10086.cn/doc/v5/develo
p/detail/242。
下面笔者简单讲解一下token 生成工具的使用,如图27.1.1.7 所示:

在这里插入图片描述

res:输入格式为“products/{pid}/devices/{device_name}”,这个输入格式中的“pid”就是
我们MQTT 产品ID,而“device_name”就是设备的名称。根据前面创建的产品和设备来填写
res 选项的参数,如下图所示:

在这里插入图片描述

et:访问过期时间(expirationTime,unix)时间,这里笔者选择参考文档中的数值
(1672735919),如下图所示:

在这里插入图片描述
key:指选择设备的key 密钥,如下图所示:

在这里插入图片描述

最后按下上图中的“Generate”按键生成核心密钥,如下图所示。

在这里插入图片描述

这个核心密钥会在MQTT 客户端的结构体client_pass 成员变量保存。

工程配置

小上节我们使用token 生成工具根据产品信息以及设备信息来计算核心密钥,这样的方式
导致每次创建一个设备都必须根据这个设备信息再一次计算核心密钥才能连接,这种方式会大
大地降低我们的开发效率,为了解决这个问题,笔者使用另一个方法,那就是使用代码的方式
计算核心密钥,它和上一章节中的方式不一样,因为阿里云和OneNET 计算的方式不同,所
以不能使用阿里云的那两个文件来计算OneNET 的密钥。OneOS 源码中有几个文件是用来计
算MQTT 协议连接OneNET 平台的核心密钥,这些文件在oneos2.0\components\cloud\onenet\m
qtt-kit\authorization 路径下,大家先下载OneOS 源码并在该路径下复制这些文件到工程当中。
打开工程并在Middlewares/lwip/lwip_app 分组下添加以下文件,如下图所示:

在这里插入图片描述

这些文件都在oneos2.0\components\cloud\onenet\mqtt-kit\authorization 路径下获取。

基于OneNET 平台MQTT 实验

硬件设计

  1. 例程功能
    本章目标是开发板使用MQTT 协议连接OneNET 服务器,并实现数据上存更新。
    该实验的实验工程,请参考《lwIP 例程18 lwIP_OneNET_MQTT 实验》。
    27.3.2 软件设计
    27.3.2.1 程序流程图
    本实验的程序流程图,如下图所示。

在这里插入图片描述
程序解析

我们打开lwip_deom.h 文件,在这个文件中我们定义了OneNET 服务器创建设备的配置项,
另外还声明了lwip_demo 函数,关于OneNET 服务器的MQTT 主题请大家查看OneNET 相关
手册,该手册地址为https://open.iot.10086.cn/doc/v5/develop/detail/251,这个地址里面已经说明

了OneNET 的MQTT 服务器相关主题信息。至于lwip_deom.c 文件前面我们已经讲解过了,它
们唯一不同的是计算核心密钥方式。

下载验证

我们编译代码,并把下载到开发板上运行,打开OneNET 的MQTT 服务器查看数据流展
示,如下图所示。

在这里插入图片描述

HTTP 客户端实验

HTTP 客户端用于实现平台与应用服务器之间的单向数据通信。平台作为客户端,通过
HTTP/HTTPS 请求方式,将项目下应用数据、设备数据推送给用户指定服务器。本章主要介
绍lwIP 如何通过HTTP 协议将设备连接到OneNET 平台,并实现远程互通。

OneNTE 的HTTP 配置

关于OneNET 平台HTTP 接入方式可参考该官方的文档手册,该文档手册地址为https://op
en.iot.10086.cn/,本实验主要参考官方文档的多协议接入/HTTP/上传数据点的内容。
OneNTE 的HTTP 服务器流程
第一步:注册OneNTE 服务器账号,注册完成之后打开右上角的控制台/ 全部产品服务/多
协议接入,如下图所示。

在这里插入图片描述

第二步:选择HTTP 协议/添加产品。
第三步:填写产品信息,如下图所示。

在这里插入图片描述

上图中的几个技术参数非常重要,剩下的技术参数根据用户的爱好填写。

第四步:双击创建的产品并点击设备列表且在设备列表中添加设备,如下图所示。

在这里插入图片描述

这些参数用户可以随便填写。
第五步:打开数据流,如下图所示。

在这里插入图片描述

第六步:打开数据流管理/添加数据流模板,如下图所示。

在这里插入图片描述

注意:上图的数据名称必须与程序发送数据的标志一样。
第七步:打开设备列表/设备详情查看设备信息,如下图所示。

在这里插入图片描述

上图中的设备ID 和APIKey 是我们需要的信息。

HTTP 客户端实验

硬件设计

  1. 例程功能
    本章目标是开发板使用HTTP 协议连接OneNET 服务器,并实现温湿度上报。
    该实验的实验工程,请参考《lwIP 例程19 lwIP_OneNET_HTTP 实验》。

软件设计

28.3.2.1 HTTP 配置步骤

  1. 配置MCU 为TCP 客户端模式
    配置为TCP 客户端等步骤,请参考第21 章。
  2. 数据合并操作
    调用函数lwip_onehttp_postpkt 把OneNET 产品的设备ID 和OneNET 设备的设备api 参数
    合拼成一个字符串。
  3. 发送数据
    调用函数netconn_write 把上述的postpkt 发送到OneNET 服务器平台。
    28.3.2.2 程序流程图
    本实验的程序流程图,如下图所示。

在这里插入图片描述

程序解析
本章实验中我们重点讲解lwip_demo.c 和lwip_demo.h。lwip_demo.h 文件很简单,主要声
明OneNET 平台的设备ID 和设备密钥,而lwip_demo.c 文件定义了2 个函数,这些函数的作
用如下表所示。

在这里插入图片描述

  1. lwip_onehttp_postpkt 函数
    把数值封装至HTTP 数据包中,如下源码所示:
uint32_t lwip_onehttp_postpkt(char * pkt, /* 保存的数据*/
    char * key, /* 连接onenet的apikey */
    char * devid, /* 连接onenet的onenet_id */
    char * dsid, /* onenet的显示字段*/
    char * val) /* 该字段的值*/ {
    
    
    char dataBuf[100] = {
    
    
        0
    };
    char lenBuf[10] = {
    
    
        0
    }; * pkt = 0;
    sprintf(dataBuf, ",;%s,%s", dsid, val); /* 采用分割字符串格式:type = 5 */
    sprintf(lenBuf, "%d", strlen(dataBuf));
    strcat(pkt, "POST /devices/");
    strcat(pkt, devid);
    strcat(pkt, "/datapoints?type=5 HTTP/1.1\r\n");
    strcat(pkt, "api-key:");
    strcat(pkt, key);
    strcat(pkt, "\r\n");
    strcat(pkt, "Host:api.heclouds.com\r\n");
    strcat(pkt, "Content-Length:");
    strcat(pkt, lenBuf);
    strcat(pkt, "\r\n\r\n");
    strcat(pkt, dataBuf);
    return strlen(pkt);
}

上述源码主要采用典型的C 语言基础,调用函数strcat 把两个字符串拼接成一个字符串,
如果我们使用网络调试助手接收该数据包,那么我们发现该数据与OneNET 平台HTTP 协议
接入文档描述是一致,该数据如下所示:

POST /devices/655766336/datapoints?type=5 HTTP/1.1
api-key:rw2p2Fq=VW4fhhhkj4CwpVcqJq8=
Host:api.heclouds.com
Content-Length:13
,;humidity,00
POST /devices/655766336/datapoints?type=5 HTTP/1.1
api-key:rw2p2Fq=VW4fhhhkj4CwpVcqJq8=
Host:api.heclouds.com
Content-Length:16
,;temperature,00
  1. lwip_demo 函数
    此函数非常简单,它用来配置网络环境即以TCP 协议连接OneNET 服务器。连接完成之后发送HTTP 数据包至服务器当中。
/**
 * @brief lwip_demo程序入口
 * @param 无
 * @retval 无
 */
void lwip_demo(void) {
    
    
    uint32_t data_len = 0;
    struct pbuf * q;
    err_t err;
    ip4_addr_t server_ipaddr, loca_ipaddr;
    static uint16_t server_port, loca_port;
    server_port = TCP_DEMO_PORT;
    netconn_gethostbyname(DEST_MANE, & server_ipaddr);
    while (1) {
    
    
        atk_start: tcp_clientconn = netconn_new(NETCONN_TCP); /* 创建一个TCP链接*/
        /* 连接服务器*/
        err = netconn_connect(tcp_clientconn, & server_ipaddr, server_port);
        if (err != ERR_OK) {
    
    
            printf("接连失败\r\n");
            /* 返回值不等于ERR_OK,删除tcp_clientconn连接*/
            netconn_delete(tcp_clientconn);
        } else if (err == ERR_OK) /* 处理新连接的数据*/ {
    
    
            struct netbuf * recvbuf;
            tcp_clientconn - > recv_timeout = 10;
            /* 获取本地IP主机IP地址和端口号*/
            netconn_getaddr(tcp_clientconn, & loca_ipaddr, & loca_port, 1);
            lcd_show_string(5, 170, 200, 16, 16, "link succeed", BLUE);
            while (1) {
    
    
                temp_rh[0] = 30 + rand() % 10 + 1; /* 温度的数据*/
                temp_rh[1] = 54.8 + rand() % 10 + 1; /* 湿度的数据*/
                tempStr[0] = temp_rh[0] / 10 + 0x30; /* 上传温度*/
                tempStr[1] = temp_rh[0] % 10 + 0x30;;
                humiStr[0] = temp_rh[1] / 10 + 0x30; /* 上传湿度*/
                humiStr[1] = temp_rh[1] % 10 + 0x30;
                len = lwip_onehttp_postpkt(buffer, apikey,
                    onenet_id, "temperature", tempStr);
                /* 发送tcp_server_sentbuf中的数据*/
                netconn_write(tcp_clientconn, buffer, len, NETCONN_COPY);
                len = lwip_onehttp_postpkt(buffer, apikey,
                    onenet_id, "humidity", humiStr);
                /* 发送tcp_server_sentbuf中的数据*/
                netconn_write(tcp_clientconn, buffer, len, NETCONN_COPY);
                vTaskDelay(1000);
                /* 接收到数据*/
                if (netconn_recv(tcp_clientconn, & recvbuf) == ERR_OK) {
    
    
                    taskENTER_CRITICAL(); /* 进入临界区*/
                    /* 数据接收缓冲区清零*/
                    memset(tcp_client_recvbuf, 0, TCP_CLIENT_RX_BUFSIZE);
                    /*遍历完整个pbuf链表*/
                    for (q = recvbuf - > p; q != NULL; q = q - > next) {
    
    
                        if (q - > len > (TCP_CLIENT_RX_BUFSIZE - data_len)) {
    
    
                            memcpy(tcp_client_recvbuf + data_len, q - > payload, (TCP_CLIENT_RX_BUFSIZE - data_len));
                        } else {
    
    
                            memcpy(tcp_client_recvbuf + data_len,
                                q - > payload, q - > len);
                        }
                        data_len += q - > len;
                        if (data_len > TCP_CLIENT_RX_BUFSIZE) {
    
    
                            break; /* 超出TCP客户端接收数组,跳出*/
                        }
                    }
                    taskEXIT_CRITICAL(); /* 退出临界区*/
                    data_len = 0; /* 复制完成后data_len要清零*/
                    printf("%s\r\n", tcp_client_recvbuf);
                    netbuf_delete(recvbuf);
                } else /*关闭连接*/ {
    
    
                    netconn_close(tcp_clientconn);
                    netconn_delete(tcp_clientconn);
                    goto atk_start;
                }
            }
        }
    }
}

下载验证

我们编译代码下载到开发板并运行,打开数据流展示,如下图所示。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/zhuguanlin121/article/details/130914561