当前位置: 首页 > news >正文

奎屯网站建设网站响应样式

奎屯网站建设,网站响应样式,wordpress4.94中文版,自动收录目录 lwIP 初探TCP/IP 协议栈是什么TCP/IP 协议栈架构TCP/IP 协议栈的封包和拆包 lwIP 简介lwIP 源码下载lwIP 文件说明 MAC 内核简介PHY 芯片介绍YT8512C 简介LAN8720A 简介 以太网接入MCU 方案软件TCP/IP 协议栈以太网接入方案硬件TCP/IP 协议栈以太网接入方案 lwIP 无操作系… 目录 lwIP 初探TCP/IP 协议栈是什么TCP/IP 协议栈架构TCP/IP 协议栈的封包和拆包 lwIP 简介lwIP 源码下载lwIP 文件说明 MAC 内核简介PHY 芯片介绍YT8512C 简介LAN8720A 简介 以太网接入MCU 方案软件TCP/IP 协议栈以太网接入方案硬件TCP/IP 协议栈以太网接入方案 lwIP 无操作系统移植lwIP 带操作系统移植ARP 协议ARP 协议的简介ARP 协议的工作流程ARP 缓存表的超时处理 APR 报文的报文结构ARP 协议层的接收与发送原理解析发送ARP 请求数据包接收ARP 应答数据包 IP 协议IP 协议的简介IP 数据报IP 数据报结构IP 数据报的分片解析IP 数据报的分片重装 IP 数据报的输出IP 数据报的输入 ICMP 协议ICMP 协议简介ICMP 报文类型ICMP 报文结构 ICMP 的实现ICMP 数据结构体发送ICMP 差错报文ICMP 报文处理 RAW 编程接口TCP 客户端实验TCP 协议TCP 协议简介TCP 的建立连接TCP 终止连接TCP 报文结构lwIP 的TCP 报文首部数据结构lwIP 的TCP 连接状态图lwIP 的TCP 控制块lwIP 的TCP 编程lwIP 的TCP 建立与关闭连接原理lwIP 中RAW API 编程接口中与TCP 相关的函数 RAW 接口的TCP 实验硬件设计软件设计下载验证 NETCONN 编程接口TCP 客户端实验NETCONN 实现TCP 客户端连接步骤NETCONN 接口的TCPClient 实验硬件设计软件设计下载验证 Socket 编程接口TCP 客户端实验Socket 编程TCP 客户端流程Socket 接口的TCPClient 实验 基于MQTT 协议连接阿里云服务器MQTT 协议简介MQTT 协议实现原理移植MQTT 协议配置远程服务器 阿里云MQTT 协议实验硬件设计下载验证 基于MQTT 协议连接OneNET 服务器配置OneNET 平台工程配置基于OneNET 平台MQTT 实验硬件设计下载验证 HTTP 客户端实验OneNTE 的HTTP 配置HTTP 客户端实验硬件设计软件设计下载验证 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 层芯片的连接图如下。 从图中可以看出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_DV8和RXD312引脚决定具体如何选择请读者参考“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 中有很多寄存器这里笔者只介绍几个用到的寄存器包括寄存器地址此 处使用十进制表示BCR0BSR1PHY 特殊功能寄存器17这三个寄存器。首先我们来看一下BCR0寄存器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 数据手册的2930 页。 ➢ 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_CLKPA1引脚上如下图所示。 为了降低成本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 有很多寄存器笔者重点讲解BCR0BSR1PHY 特殊功能寄存器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 等多种网络协议并且实现了物理层以太网控制 MACPHY、内存管理等功能完成了一整套硬件化的以太网解决方案。 该方案的连接示意图如下图所示 上图中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 协议的工作流程 假设由两台主机分别为主机A192.168.0.10与主机B192.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 协议在任何网络中被使用而不仅仅只在以太网中。opARP 数据包的类型ARP 请求设置为1ARP 应答设置为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_SIZEPACK_STRUCT_FLD_8(u8_t padding[ETH_PAD_SIZE]);#endifPACK_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_AUTOIPif (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_IPV6s16_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_NETIFnetif 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_IPV4if ((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_PROTOCOLif (LWIP_HOOK_UNKNOWN_ETH_PROTOCOL(p, netif) ERR_OK) {break;}#endifETHARP_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-165534 字节。 在IP 层下面的每一种数据链路层都有自己的帧格式其中包括帧格式中的数据字段的最 大长度这称为最大传送单元MTU。当一个数据报封装成链路层的帧时此数据报的总长度即首部加上数据部分一定不能超过下面的数据链路层的MTU 值。 (5) 标识identification占16 位IP 软件在存储器中维持一个计数器每产生一个数据报计数器就加1并将此值赋给标识字段。但这个“标识”并不是序号因为IP 是无连接服务数据报不存在按序接收的问题。当数据报由于长度超过网络的MTU 而必须分片时这个标识字段的值就被复制到所有的数据报的标识字段中。相同的标识字段的值使分片后的各数据报片最后能正确地重装成为原来的数据报。 (6) 标志flag占3 位但只有2 位有意义的。 标志字段中的最低位记为MFMore Fragment。MF1 即表示后面“还有分片”的数 据报。MF0 表示这已是若干数据报片中的最后一个。标志字段中间的一位记为DFDon’t Fragment意思是“不能分片”。只有当DF0 时才允许分片。 (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 内核会把这个数据报进行分片处理。 第一个IP 分片 分片数据大小20IP 首部 1480数据区域。 标识888。 标志IP_MF 1 后续还有分片。 片偏移量片偏移量是0单位是8 字节本片偏移量相当于0 字节。第二片IP 数据报 分片数据大小20IP 首部 1480数据区域。 标识888。 标志IP_MF 1 后续还有分片。 片偏移量片偏移量是1851480/8单位是8 字节本片偏移量相当于1480 字节。第三片IP 数据报 分片数据大小20IP 首部 1020数据区域。 标识888。 标志IP_MF 0后续没有分片。 片偏移量片偏移量是370185185单位是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_PBUFstruct pbuf * newpbuf;u16_t newpbuflen 0;u16_t left_to_copy;#endifstruct 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; }此函数非常简单首先判断这个大型数据包的有效区域总长度系统根据这个总长度划分 数据区域接着申请20sizeof(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_IGMPint 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_AUTOIPif (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_ADDRESSINGif (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_ADDRESSINGif (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_IGMPif ((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_UDPLITEcase 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 协议 ICMPInternet Control Message ProtocolInternet 控制报文协议。它是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 字节在每种类型的报文有特殊的定义总的看来说不同类型的报文数据 部分长度和含义存在差异例如差错报文会引起差错的据报的信息而查询报文携带查询请求和查询结果数据。 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 超时报文 当路由器接收到的数据报的TTL 生命周期字段值为0 时路由器会把该数据报丢弃掉 并向源主机发回一个ICMP 超时报文。另外当目标主机在规定时间内没有收到所有的数据分片时会把已经收到的所有数据 分片丢弃并向源主机发回一个ICMP 超时报文。在超时报文中代码0 只能给路由器使用表示生存周期字段值为0代码1 只能给目的主机使用它表示在规定的时间内目的主机没有收到所有的数据分片。 (5) 参数错误 当数据报在因特网上传送时在其首部中出现的任何二义性或者首部字段值被修改都可能 会产生非常严重的问题。如果路由器或目的主机发现了这种二义性或在数据报的某个字段中缺少某个值就丢弃这个数据报并回送参数问题报文。 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);/* 为差错报文申请pbufpbuf预留以太网首部和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 协议简介 TCPTransmission Control Protocol 传输控制协议是一种面向连接的、可靠的、基于字 节流的传输层通信协议。 TCP 为了保证数据包传输的可靠行会给每个包一个序号同时此序号也保证了发送到 接收端主机能够按序接收。然后接收端主机对成功接收到的数据包发回一个相应的确认字符 ACKAcknowledgement如果发送端主机在合理的往返时延RTT内未收到确认字符 ACK那么对应的数据包就被认为丢失并将被重传。TCP 协议它是基于连接的一种传输层 协议在发送数据之前要求系统需要在不可靠的信道上建立可靠连接我们称之为“三次握 手”。建立连接完成之后客户端与服务器才能互发数据不需要发送数据时可以可以断开连 接这里我们称之为“四次挥手”。下面笔者带大家了解一下TCP 协议建立连接的过程和断开 连接的过程即三次握手和四次挥手的过程。 TCP 的建立连接 握手之前主动打开连接的客户端结束CLOSED 阶段被动打开的服务器端也结束 CLOSED 阶段并进入LISTEN 阶段。随后开始“三次握手” ①TCP 服务器进程先创建传输控制块TCB时刻准备接受客户进程的连接请求此时服 务器就进入了LISTEN监听状态。 ②TCP 客户进程也是先创建传输控制块TCB然后向服务器发出连接请求报文这是报 文首部中的同部位SYN1同时选择一个初始序列号seqx 此时TCP 客户端进程进入了 SYN-SENT同步已发送状态状态。TCP 规定SYN 报文段SYN1 的报文段不能携带 数据但需要消耗掉一个序号。 ③TCP 服务器收到请求报文后如果同意连接则发出确认报文。确认报文中应该 ACK1SYN1确认号是ackx1同时也要为自己初始化一个序列号seqy此时TCP 服务器进程进入了SYN-RCVD同步收到状态。这个报文也不能携带数据但是同样要消 耗一个序号。 ④TCP 客户进程收到确认后还要向服务器给出确认。确认报文的ACK1acky1 自己的序列号seqx1此时TCP 连接建立客户端进入ESTABLISHED已建立连接状 态。TCP 规定ACK 报文段可以携带数据但是如果不携带数据则不消耗序号。 当服务器收到客户端的确认后也进入ESTABLISHED 状态此后双方就可以开始通信了。 这就是“三次握手”的过程如下图所示。 TCP 终止连接 建立一个连接需要三次握手而终止一个连接需要四次挥手终止连接有以下过程。 (1) 第一次挥手客户端发送释放报文并停止发送数据。释放数据报文首部FIN1,其 序列号为sequ,此时客户端进入FIN-WAIT1等待服务器应答FIN 报文。 (2) 第二次挥手服务器收到客户端的FIN 报文后发出确认报文ACK1、acku1并 携带自己的序列号seqv。此时服务器进入CLOSE-WAIT关闭等待状态。客户端收到服 务端确认请求此时客户端进入FIN-WAIT2终止等待2状态等待服务器发送连接释放 报文。 (3) 第三次挥手服务器向客户端发送连接释放报文FIN1、acku1,此时服务器进入 了LAST-ACK最后确认等待客户端的确认。客户端接收到服务器的连接释放报文后必 须发送确认ack1、ackw1,客户端的序列号为sequ1此时客户端进入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_BACKLOGstruct 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_nxtrcv_wndrcv_ann_wnd 和数据发送的相关字段 snd_nxtsnd_maxsnd_wndacked这些字段和TCP 中滑动窗口协议有密切关系的。 声明下面的内容参考自《嵌入式网络那些事LWIP 协议深度剖析与实战演练》作者朱 升林。 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 */ };控制块链表 为了描述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;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_OVERSIZEif (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 客户端的 配置流程如下所示 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 发送该应答包这里就完成了三次握手的动作。下面笔者使用一个示意图 来讲解这个过程如下图所示 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 实验 硬件设计 例程功能 本章实验的目标是PC 端和开发板通过TCP 协议连接起来开发板做TCP 客户端PC 端 的网络调试助手配置成服务器。开发板接收服务器发送的数据在LCD 上显示我们也可以通 过开发板上的按键发送数据给PC。 该实验的实验工程请参考《lwIP 例程3 lwIP_RAW_TCPClient 实验》。 软件设计 12.2.2.1 TCP 客户端配置步骤 创建TCP 控制块 调用函数tcp_new 创建TCP 控制块。连接指定的IP 地址和端口号 调用函数tcp_connect 连接到目的地址的指定端口上。接收数据 调用函数tcp_recved 接收数据。发送数据 调用函数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 实验 硬件设计 例程功能 本实验使用NETCONN 编程接口实现TCPClient 连接我们可通过按下KEY0 按键发送数 据至网络调试助手还可以接收网络调试助手发送的数据并在LCD 显示屏上显示。 该实验的实验工程请参考《lwIP 例程8 lwIP_NETCONN_TCPClient 实验》。 软件设计 17.2.2.1 netconn 的TCPClient 连接步骤 创建TCP 控制块 调用函数netconn_new 创建TCP 控制块。绑定远程IP 地址与端口号 调用函数netconn_connect 绑定远程IP 地址和远程端口号。接收数据 netconn_recv 接收数据。发送数据 调用函数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 硬件设计 例程功能 本实验使用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 是什么 MQTTMessage 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 协议实现原理 要在客户端与代理服务端建立一个TCP 连接建立连接的过程是由客户端主动发起的 代理服务一直是处于指定端口的监听状态当监听到有客户端要接入的时候就会立刻去处理。 客户端在发起连接请求时携带客户端ID、账号、密码无账号密码使用除外正式项目不会允许这样、心跳间隔时间等数据。代理服务收到后检查自己的连接权限配置中是否允许该账号密码连接如果允许则建立会话标识并保存绑定客户端ID 与会话并记录心跳间隔时间判断是否掉线和启动遗嘱时用和遗嘱消息等然后回发连接成功确认消息给客户端客户端收到连接成功的确认消息后进入下一步通常是开始订阅主题如果不需要订阅则跳过。如下图所示 客户端将需要订阅的主题经过SUBSCRIBE 报文发送给代理服务代理服务则将这个主 题记录到该客户端ID 下以后有这个主题发布就会发送给该客户端然后回复确认消息SUBACK 报文客户端接到SUBACK 报文后知道已经订阅成功则处于等待监听代理服务推送的消息也可以继续订阅其他主题或发布主题如下图所示 当某一客户端发布一个主题到代理服务后代理服务先回复该客户端收到主题的确认消 息该客户端收到确认后就可以继续自己的逻辑了。但这时主题消息还没有发给订阅了这个主题的客户端代理要根据质量级别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 协议实验 硬件设计 例程功能 本章的目标是lwIP 连接阿里云实现数据上存。 该实验的实验工程请参考《lwIP 例程17 lwIP_Aliyun_MQTT 实验》。 26.2.2 软件设计 26.2.2.1 MQTT 配置步骤 配置MCU 为TCP 客户端模式 配置为TCP 客户端等步骤请参考第21 章。DNS 解析阿里云网页转成IP 地址 调用函数gethostbyname 获取DNS 解析的IP 地址。MQTT 连接 调用函数mqtt_client_connect 连接服务器。连接状态 对服务器发布和订阅操作。循环发布数据到服务器当中 在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 产品之后用户可以得到该产品 的信息如下图所示 本实验会用到上述的产品信息例如产品ID366007、“access_key”产品密钥以及产品 名称MQTT_TSET等。 第三步在产品页面下点击设备列表添加设备如下图所示 第四步在上图创建的设备中点击右边的详情标签进入标签的链接页面在这个页面下 我们得到以下设备信息如下图所示 本实验会用到上图中的设备ID617747917、设备名称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访问过期时间expirationTimeunix时间这里笔者选择参考文档中的数值 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 实验 硬件设计 例程功能 本章目标是开发板使用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 客户端实验 硬件设计 例程功能 本章目标是开发板使用HTTP 协议连接OneNET 服务器并实现温湿度上报。 该实验的实验工程请参考《lwIP 例程19 lwIP_OneNET_HTTP 实验》。 软件设计 28.3.2.1 HTTP 配置步骤 配置MCU 为TCP 客户端模式 配置为TCP 客户端等步骤请参考第21 章。数据合并操作 调用函数lwip_onehttp_postpkt 把OneNET 产品的设备ID 和OneNET 设备的设备api 参数 合拼成一个字符串。发送数据 调用函数netconn_write 把上述的postpkt 发送到OneNET 服务器平台。 28.3.2.2 程序流程图 本实验的程序流程图如下图所示。 程序解析 本章实验中我们重点讲解lwip_demo.c 和lwip_demo.h。lwip_demo.h 文件很简单主要声 明OneNET 平台的设备ID 和设备密钥而lwip_demo.c 文件定义了2 个函数这些函数的作 用如下表所示。 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?type5 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?type5 HTTP/1.1 api-key:rw2p2FqVW4fhhhkj4CwpVcqJq8 Host:api.heclouds.com Content-Length:13 ,;humidity,00 POST /devices/655766336/datapoints?type5 HTTP/1.1 api-key:rw2p2FqVW4fhhhkj4CwpVcqJq8 Host:api.heclouds.com Content-Length:16 ,;temperature,00lwip_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;}}}} }下载验证 我们编译代码下载到开发板并运行打开数据流展示如下图所示。
http://www.dnsts.com.cn/news/38497.html

相关文章:

  • 建站工具 风铃吴博 wordpress
  • 做基金哪个网站好微网站背景图片
  • 自己的网站怎么创建有模板怎么做网站
  • 江苏城乡建设职业学院网站网站建设好的乡镇
  • 有哪些网站做的比较好看的网站建设业务开展方案
  • 网站建设与准备django 和 wordpress
  • 国外科技感强的网站wordpress 主题导出
  • wordpress修改器seo建网站
  • 网站建设先做后网站建设要多久豆瓣
  • 一站式企业服务wordpress密码错误
  • 哪里有个人卖房网站防网站模板
  • 烟台网站制作企业什么软件可以做动漫视频网站
  • 陵水网站建设费用wordpress主题页面丢失
  • 定制网站开发系统桂林象鼻山在哪
  • 网站页眉设计wordpress php7.2
  • 模板网站判定3d打印 东莞网站建设
  • 南昌品牌网站建设新冠北京最新消息
  • 公司做网站自己可以做类似wordpress
  • 网站婚庆模板免费制作永久个人网站
  • 怎样下载建设部网站商城网站后台模板
  • 浙江网站建设与维护书版面设计网站
  • 合肥市重点工程建设管理局网站长春哪家网站做的好
  • 网站品牌建设建议学做网站后台开发
  • 哪些网站可以做代理做网站诱导网站
  • 商丘网站制作推广网络营销推广的步骤是什么
  • 烟台快速建站有哪些公司江门市住房城乡建设局网站
  • 重庆开网站韩国的小游戏网站
  • 上海网站建设升级5g空间大吗企业网站
  • 海盐网站设计常熟建设网站
  • 做外贸哪个网站最容易上手网站开发嘉比格网络