96SEO 2026-02-20 05:13 15
本章先介绍计算机网络相关知识然后对lwIP软件库进行概述接着介绍MAC

协议栈是一系列网络协议的总和是构成网络通信的核心骨架它定义了电子设备如何连入因特网以及数据如何在它们之间进行传输。
协议采用4层结构分别是应用层、传输层、网络层和网络接口层每一层都呼叫它的下一层所提供的协议来完成自己的需求。
由于我们大部分时间都工作在应用层下层的事情不用我们操心其次网络协议体系本身就很复杂庞大入门门槛高因此很难搞清楚TCP/IP
模型因其开放性和易用性在实践中得到了广泛的应用它也成为互联网的主流协议。
注意网络技术的发展并不是遵循严格的OSI分层概念。
实际上现在的互联网使用的是TCP/IP
体系结构有时已经演变成为图1.1.1.2所示那样即某些应用程序可以直接使用IP
协议栈负责确保网络设备之间能够通信。
它是一组规则规定了信息如何在网络中传输。
这些协议都分布在应用层传输层和网络层网络接口层是由硬件来实现。
协议栈的应用层传输层和网络层的功能网络接口层由网卡实现所以CBISC
协议栈和网卡构建了网络通信的核心骨架。
因此无论哪一款以太网产品都必须符合TCP/IP
注意路由器和交换机等相关网络设备只实现网络层和网络接口层的功能。
协议栈的封包和拆包也是一个非常重要的知识如以太网设备发送数据和接收数据的处理流程是怎么样的这个问题涉及到TCP/IP
协议栈对数据处理的流程该流程称之为“封包”和“拆包”。
“封包”是对发送数据处理的流程而“拆包”是对接收数据处理的流程如下图所示。
上图中发送端发送的数据自顶向下依次传递。
各层协议依次在数据前添加本层的首部且设置本层首部的信息最终将处理后的MAC帧递交给物理层转成光电模拟信号发送至网络这个流程称之为封包流程。
上图中当帧数据到达目的主机时将沿着协议栈自底向上依次传递。
各层协议依次根据帧中本层负责的头部信息以获取所需数据最终将处理后的帧交给应用层这个流程称之为拆包的过程。
的设计理念下既可以无操作系统使用也可以带操作系统使用既可以支持多线程也可以无线程。
它可以运行在8
体系结构的应用层、传输层和网络层的功能但网络接口层不能使用软件的方式实现因为网络接口层是把数据包转成光电模拟信号并转发至网络所以网络接口层只能由硬件来实现。
的项目主页http://savannah.nongnu.org/projects/lwip/。
在这个主页上读者需要关注“project
更新日记、常见误解、已发现的BUG、多线程、优化提示和相关文件中的函数描述等内容。
包不属于lwIP内核的一部分它只是为我们提供移植文件和学习实例。
根据上一个小节的操作我们已经下载了lwip-2.1.3.zip
上图中这个文件夹包含的文件和文件夹非常多这些文件与文件夹描述如下表所示。
的内核文件也是我们移植到工程中的重要文件。
接下来笔者重点讲解src
文件夹下的文件实现了网络层与数据链路层交互接口以及管理不同类型的网卡。
协议栈的各种协议、内存管理、数据包管理、网卡管理、网卡接口、基础功能和API接口模块等每一个模块是由几个源文件和一个头文件集合这些头文件全部放在include
的物理存储器它们分别存储网络层递交的以太网数据和接收的以太网数据。
以太网DMA
芯片的管理和配置是站管理接口SMI所需的通信引脚。
站管理接口SMI允许应用程序通过2
MII_RX_ER接收错误信号。
该信号必须保持一个或多个周期(MII_RX_CLK)从而向MAC
相比MII其发送和接收都少了两条线。
因此要达到10Mbit/s
的速度其时钟频率应为50MHz。
正点原子开发板就是采用此接口连接PHY
体系架构中扮演着物理层的角色它把数据转换成光电模拟信号传输至网络当中。
本小节为读者介绍正点原子常用的PHY
百兆以太网传输速率为此笔者分两个小节来讲解这两款以太网芯片的知识。
芯片。
它通过两条标准双绞线电缆收发器发送和接收数据所需的所有物理层功能。
另外YT8512C
芯片的内部总架构示意图从图中我们大概可以看出它通过LED0\LED1
库版本旧所以它们的移植流程存在巨大的差异。
这里笔者暂且不讲解这部分的内容。
的RX_DV8和RXD312引脚决定具体如何选择请读者参考“YT8512C.PDF”手册的17
特殊功能寄存器17这三个寄存器。
首先我们来看一下BCR0寄存器BCR
((uint16_t)0x0001)阿波罗、北极星开发板PHY
((uint16_t)0x0001U)由于探索者及DMF407
库所以这两个寄存器并不需要读者来操作原因就是我们调用HAL_ETH_Init
的相应寄存器。
但是阿波罗及北极星开发板的例程使用目前最新的HAL
个寄存器所以每个厂家的可能不同这个需要用户根据自己实际使用的PHY
提供的以太网驱动文件有三个配置项值得读者注意的它们分别为PHY_SR、PHY_SPEED_STATUS
的中断系统提供两种中断模式主中断模式和复用中断模式。
主中断模式是默认中断模式LAN8720A
上电或复位后就工作在主中断模式当模式控制/状态寄存器(十进制地址为17)的ALTINT
系列开发板并未用到中断功能关于中断的具体用法可以参考LAN8720A
芯片采用分页技术来扩展地址空间定义更多的寄存器在这里我们不讨论这种情况。
IEEE
特殊功能寄存器31这三个寄存器前面两个寄存器笔者已经在1.6.1
小节讲解了这里笔者无需重复讲解。
接下来介绍的是LAN8720A
协议栈用途非常广泛如电脑、交换机等网络设备而全硬件TCP/IP
协议栈是近年来比较新型的以太网接入方案。
下面笔者分别来讲解这两种接入方案的差异和优缺点。
阿波罗、北极星以及电机开发板都是采用这类型的以太网接入方案该方案的连接示意图如下图所示
移植性可在不同平台、不同编译环境的程序代码经过修改转移到自己的系统中运行。
从代码量分析移植lwIP可能需要的代码量超过40KB对于有些主控芯片内存匮乏
从运行性能方面分析由于软件TCP/IP协议栈方案在通信时候是不断地访问中断机
制造成线程无法运行如果多线程运行会使MCU的工作效率大大降低。
从安全性方面分析软件协议栈会很容易遭受网络攻击造成单片机瘫痪。
MACPHY、内存管理等功能完成了一整套硬件化的以太网解决方案。
从运行方面来看极大的减少了中断次数让单片机更好的完成其他线程的工作。
从安全性方面来看硬件化的逻辑门电路来处理TCP/IP协议是不可被攻击的也就
是说网络攻击和病毒对它无效这也充分弥补了网络协议安全性不足的短板。
从可扩展性来看虽然该芯片内部使用逻辑门电路来实现应用层和物理层协议但是
它具有功能局限性例如给TCP/IP协议栈添加一个协议这样它无法快速添加了。
从收发速率来看全硬件TCP/IP协议栈芯片都是采用并口、SPI以及IIC等通讯接
编程和少量的寄存器操作即可方便地进行嵌入式以太网上层应用开发减少产品开发周期降低开发成本。
请求广播到局域网络上的所有主机并接收返回消息以此确定目标的物理地址收到返回消息后将该IP
缓存以节约资源。
地址解析协议是建立在网络中各个主机互相信任的基础上的局域网络上的主机可以自主发送ARP
应答消息其他主机收到应答报文时不会检测该报文的真实性就会将其记入本机ARP
假设由两台主机分别为主机A192.168.0.10与主机B192.168.0.11它们两个都是
arp_table[ARP_TABLE_SIZE];可以看出ARP
每一个表项从创建、请求等都设置了一个状态不同状态的表项都需要特殊的处理这些
0,ETHARP_STATE_PENDING,ETHARP_STATE_STABLE,ETHARP_STATE_STABLE_REREQUESTING_1,ETHARP_STATE_STABLE_REREQUESTING_2
缓存表处于初始化的状态所有表项初始化之后才可以被使用如果需要添加表项lwIP
地址的映射关系并且开始记录表项的生存时间同时该表项的状态会变成ETHARP_STATE_STABLE
当收到应答之前这些数据包会暂时挂载到表项的数据包缓冲队列上收到应答之后系统已经更新ARP
(4)ETHARP_STATE_STABLE_REREQUESTING_1
ETHARP_STATE_STABLE_REREQUESTING_2
请求数据包则表项状态会暂时被设置为ETHARP_STATE_ST
ABLE_REREQUESTING_1之后设置为ETHARP_STATE_STABLE_REREQUESTING_2
缓存表各个表项的状态和检测各个表项的生存时间。
稍后笔者也会讲解ARP
内核把要发送的数据包挂载到新创建的表项当中。
在表项中包含了etharp_q_entry
其实这个参数笔者在上面也有所涉及因为系统以周期的形式调用函数etharp_trm。
例如5秒之前收到ARP
处理如果某个表项的生存时间计数值大于系统规定的某个值系统就会删除该表项。
etharp_trm
第三步发送ARP请求数据包并判断ctime是否大于5秒*/if
从ARP缓存表中删除该表项*/etharp_free_entry(i);}
ETHARP_STATE_STABLE_REREQUESTING_1)
ETHARP_STATE_STABLE_REREQUESTING_2;}
ETHARP_STATE_STABLE_REREQUESTING_2)
将状态重置为稳定状态使下一个传输的数据包将重新发送一个ARP请求*/arp_table[i].state
仍然挂起重新发送一个ARP查询*/etharp_request(arp_table[i].netif,
}此函数非常简单这里笔者使用一个流程图来讲解这个函数的实现流程如下图所示
左边的是以太网首部数据发送时必须添加以太网首部添加完成之后才能把数据发往到
地址。
协议类型表示要映射的协议地址类型0x0800–映射为IP
/**********************************e***rnet.h********************************/
一个以太网MAC地址*/PACK_STRUCT_FLD_8(u8_t
ETH_PAD_SIZEPACK_STRUCT_FLD_8(u8_t
padding[ETH_PAD_SIZE]);#endifPACK_STRUCT_FLD_S(struct
/***********************************etharp.h**********************************/
{ETHARP_STATS_INC(etharp.memerr);return
源IP地址*/IPADDR_WORDALIGNED_COPY_FROM_IP4_ADDR_T(
目的IP地址*/IPADDR_WORDALIGNED_COPY_FROM_IP4_ADDR_T(
(ip4_addr_islinklocal(ipsrc_addr))
调用底层发送函数将以太网数据帧发送出去*/e***rnet_output(netif,
ETHTYPE_ARP);}ETHARP_STATS_INC(etharp.xmit);/*
最接近网卡驱动文件发送的数据经过ARP检测和操作发送至网卡驱动文件处理由网卡驱动文件调用ETH
第一步判断数据包是否小于等于以太网头部的大小如果是则释放内存直接返回*/if
{ETHARP_STATS_INC(etharp.proterr);ETHARP_STATS_INC(etharp.drop);MIB2_STATS_NETIF_INC(netif,
第二步p-payload表示指向缓冲区中实际数据的指针相当于指向以太网的头部*/ethhdr
去除以太网首部失败则直接返回*/ETHARP_STATS_INC(etharp.lenerr);ETHARP_STATS_INC(etharp.drop);goto
LWIP_HOOK_UNKNOWN_ETH_PROTOCOLif
(LWIP_HOOK_UNKNOWN_ETH_PROTOCOL(p,
{break;}#endifETHARP_STATS_INC(etharp.proterr);ETHARP_STATS_INC(etharp.drop);MIB2_STATS_NETIF_INC(netif,
ERR_OK;free_and_return:pbuf_free(p);return
目的是提高网络的可扩展性一是解决互联网问题实现大规模、异构网络的互联互通二是分割顶层网络应用和底层网络技术之间的耦合关系以利于两者的独立发展。
根据端到端的设计原则IP
模型的网络层它可以向传输层提供各种协议的信息例如TCP、UDP
信息包放到链路层通过以太网、令牌环网络等各种技术来传送。
为了能适应异
强调适应性、简洁性和可操作性并在可靠性做了一定的牺牲。
这里我们不过多
个字节由于以太网网络接口的最大传输单元为1500所以一个完整的数据包不
位可表示的最大十进制数值是15。
请注意这个字段所表示数的单位是32
字节的整数倍时必须利用最后的填充字段加以填充。
因此数据部分永远在4
字节的缺点是有时可能不够用。
但这样做是希望用户尽量减少开销。
最常用的首部长度就是20
位用来获得更好的服务。
这个字段在旧标准中叫做服务类型但实际上一直没有被使用过。
总长度总长度指首部和数据之和的长度单位为字节。
总长度字段为16
层下面的每一种数据链路层都有自己的帧格式其中包括帧格式中的数据字段的最
大长度这称为最大传送单元MTU。
当一个数据报封装成链路层的帧时此数据报的总长度即首部加上数据部分一定不能超过下面的数据链路层的MTU
软件在存储器中维持一个计数器每产生一个数据报计数器就加1并将此值赋给标识字段。
但这个“标识”并不是序号因为IP
是无连接服务数据报不存在按序接收的问题。
当数据报由于长度超过网络的MTU
而必须分片时这个标识字段的值就被复制到所有的数据报的标识字段中。
相同的标识字段的值使分片后的各数据报片最后能正确地重装成为原来的数据报。
表示这已是若干数据报片中的最后一个。
标志字段中间的一位记为DFDon’t
个字节为偏移单位。
这就是说除了最后一个分片每个分片的长度一定是8
报在网络中的寿命。
由发出数据报的源点设置这个字段。
其目的是防止无法交付的数据报无限制地在因特网中兜圈子因而白白消耗网络资源。
最初的设计是以秒作为TTL
减去数据报在路由器消耗掉的一段时间。
若数据报在路由器消耗的时间小于1
字段的功能改为“跳数限制”但名称不变。
路由器在转发数据报之前就把TTL
的意义是指明数据报在网络中至多可经过多少个路由器。
显然数据报在网络上经过的路由器的最大数值是255。
若把TTL
位协议字段指出此数据报携带的数据是使用何种协议以便使目的主机的IP
位这个字段只检验数据报的首部但不包括数据部分。
这是因为数据报每经过一个路由器路由器都要重新计算一下首部检验和一些字段如生存时间、标志、片偏移等都可能发生变化。
不检验数据部分可减少计算的工作量。
(11)
首部封装在其中因为有数据区域才会有数据报首部的存在在大多数情况下IP
版本号首部长度服务类型*/PACK_STRUCT_FLD_8(u8_t
生存时间(最大转发次数)协议类型(IGMP:1、UDP:17、TCP:6)
源IP地址/目的IP地址*/PACK_STRUCT_FLD_S(ip4_addr_p_t
src);PACK_STRUCT_FLD_S(ip4_addr_p_t
PACK_STRUCT_END可以看出此结构体的成员变量和上图9.2.1
协议栈为什么具备分片的概念因为应用程序处理的数据是不确定的可能超出
行重组处理这样接收方的应用程序接收到这个大型的数据了。
总的来讲IP
分片这些分片的数据组合起来就是应用程序发送的数据与传输层的首部。
将数据报切成MTU大小的块然后按顺序发送通过将pbuf_ref指向p
!LWIP_NETIF_TX_SINGLE_PBUFstruct
lwip_ntohs(IPH_OFFSET(iphdr));/*
这个rambuf有效数据指针指向original_iphdr数据报*/SMEMCPY(rambuf
ip_frag_alloc_pbuf_custom_ref();if
newpbuf申请内存1480字节保存了这个数据区域偏移poff字节的数据(p-payload
释放内存*/ip_frag_free_pbuf_custom_ref(pcr);pbuf_free(rambuf);goto
将它添加到rambuf的链的末尾*/pbuf_cat(rambuf,
分段偏移与标志字段*/IPH_OFFSET_SET(iphdr,
dest);IPFRAG_STATS_INC(ip_frag.xmit);/*
rambuf释放内存*/pbuf_free(rambuf);/*
nfb);}MIB2_STATS_INC(mib2.ipfragoks);return
ERR_OK;memerr:MIB2_STATS_INC(mib2.ipfragfails);return
MIB2_STATS_INC(mib2.ipfragoks);
memerr:MIB2_STATS_INC(mib2.ipfragfails);
}此函数非常简单首先判断这个大型数据包的有效区域总长度系统根据这个总长度划分
数据包的payload指针指向最后调用netif-output
分组在网络传输过程中到达目的地点的时间是不确定的所以后面的分组可能比
中有专门的结构体负责缓存这些分组这个结构体为ip_reassdata
还是TCP它们的数据段递交至网络层的接口是一致的这个接口函数如下
设置版本号设置首部长度*/IPH_VHL_SET(iphdr,
将当前网络接口IP地址设置为源IP地址*/ip4_addr_copy(iphdr
payload;ip4_addr_copy(dest_addr,
dest_addr;}IP_STATS_INC(ip.xmit);ip4_debug_print(p);/*
}此函数非常简单这里笔者使用一个流程图来描述该函数的实现原理如下图所示
首部字段信息接着判断该数据包的总长度是否大于以太网传输单元若大于则调用ip4_frag
函数对这个数据包分组并且逐一发送否则直接调用ethrap_output
IP_ACCEPT_LINK_LAYER_ADDRESSING
IP_ACCEPT_LINK_LAYER_ADDRESSING
*/IP_STATS_INC(ip.recv);MIB2_STATS_INC(mib2.ipinreceives);/*
{ip4_debug_print(p);pbuf_free(p);
释放空间*/IP_STATS_INC(ip.err);IP_STATS_INC(ip.drop);MIB2_STATS_INC(mib2.ipinhdrerrors);return
释放空间*/pbuf_free(p);IP_STATS_INC(ip.lenerr);IP_STATS_INC(ip.drop);MIB2_STATS_INC(mib2.ipindiscards);return
地址复制到对齐的ip_data.current_iphdr_src和ip_data.current_iphdr_dest
*/ip_addr_copy_from_ip4(ip_data.current_iphdr_dest,
dest);ip_addr_copy_from_ip4(ip_data.current_iphdr_src,
(ip4_addr_ismulticast(ip4_current_dest_addr()))
(ip4_addr_cmp(ip4_current_dest_addr(),netif_ip4_addr(netif))
*/ip4_addr_isbroadcast(ip4_current_dest_addr(),
(ip4_addr_get_u32(ip4_current_dest_addr())
(ip4_addr_isloopback(ip4_current_dest_addr()))
IP_ACCEPT_LINK_LAYER_ADDRESSINGif
(IP_ACCEPT_LINK_LAYER_ADDRESSED_PORT(udphdr
IP_ACCEPT_LINK_LAYER_ADDRESSING
IP_ACCEPT_LINK_LAYER_ADDRESSINGif
IP_ACCEPT_LINK_LAYER_ADDRESSING
IP_ACCEPT_LINK_LAYER_ADDRESSING
IP_ACCEPT_LINK_LAYER_ADDRESSING
((ip4_addr_isbroadcast(ip4_current_src_addr(),
||(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
(!ip4_addr_isbroadcast(ip4_current_dest_addr(),
{IP_STATS_INC(ip.drop);MIB2_STATS_INC(mib2.ipinaddrerrors);MIB2_STATS_INC(mib2.ipindiscards);}/*
释放空间*/pbuf_free(p);IP_STATS_INC(ip.opterr);IP_STATS_INC(ip.drop);/*
u不受支持的协议特性*/MIB2_STATS_INC(mib2.ipinunknownprotos);return
第九步发送到上层协议*/ip4_debug_print(p);ip_data.current_netif
netif;ip_data.current_input_netif
iphdr;ip_data.current_ip_header_tot_len
转移到有效载荷数据区域不需要检查*/pbuf_header(p,
*/MIB2_STATS_INC(mib2.ipindelivers);/*
IP_PROTO_TCP:MIB2_STATS_INC(mib2.ipindelivers);/*
释放空间*/IP_STATS_INC(ip.proterr);IP_STATS_INC(ip.drop);MIB2_STATS_INC(mib2.ipinunknownprotos);}}/*
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;}上述的源码篇幅很长也不容易理解下面笔者把上述的源码分成十步来讲解
第十步判断该数据报的协议为TCP/UDP/ICMP/IGMP如果不是这四个协议则丢弃该
否可达、路由是否可用等网络本身的消息这些控制消息虽然并不传输到用户数据但是对于用户数据的传递起着重要的作用。
协议是一种面向无连接的协议用于传输出错报告控制信息。
它是一个非常重要的协议它对于网络安全具有极其重要的意义。
它属于网络层协议主要用于在主机与路由器之间传递控制信息包括报告错误、交换受限控制和状态信息等。
当遇到IP
路由器无法按当前的传输速率转发数据包等情况时会自动发送ICMP
如数据报错信息、网络状况信息和主句状况信息等虽然这些信息不会递交给用户数据但对于用户来说数据报有效性得到提高。
协议本身不提供差错报告和差错控制机制来保证数据报递交的有效性如果在路由器
层这样处理是合理的但是对于源主机来说比较希望得到数据报递交过程中出现异常相
协议不能进行主机管理与查询机制简单来说不知道对方主机或者路由器的活跃
对于不活跃的主机和路由器就没有必要发送数据报所以对于主机管理员来说更希望得到对方主机和路由器的信息这样可以根据相关的信息对自身配置、数据报发送控制。
协议不为任何的应用程序服务它的目的是目的主机的网络层处理软件。
是判断路由器和主机对当前的数据报进行正常处理例如无法将数据报递交给上层处理或者数据报因为生存时间而被删除。
查询报文用于一台主机向另一台主机查询特定的信息这个类型的报文是成对出
现的例如源主机发送查询报文当目标主机收到该报文之后它会根据查询报文的约定的格式为源主机放回应答报文。
中不同的报文其首部的格式也会有点差异当然也有通用的地方例如首部的前4
部分长度和含义存在差异例如差错报文会引起差错的据报的信息而查询报文携带查询请求和查询结果数据。
当路由器发送的数据报不能发送到指定目的地时或者说当路由器不能够给数据报找到路由或主机不能够交付数据报时就丢弃这个数据报然后向发送数据报的源主机设备发回一个终点不可达数据报文。
如下图所示
发生了故障它不知道这个数据报下一步该发给哪个路由设备或者那台主机设备也就是说这个数据报不能发送到目的地主机B这时路由器会把这个数据报丢弃并向主机A
目的不可达差错报告报文产生差错的原因有很多如网络不可达、主机不可达、协
层能够根据端口号将报文传递给对应的上层协议处理差错报文结构如下图所示
协议是面向无连接的没有流量控制机制数据在传输过程中是非常容易造成拥
抑制机制并不能控制流量的大小但是能根据流量的使用情况给源主机提供一些建议。
这个报文的作用就是通知数据报在拥塞时被丢弃了另外还会警告源主机流量出现了拥塞的情况然后源主机根据反馈的ICMP
超时报文。
另外当目标主机在规定时间内没有收到所有的数据分片时会把已经收到的所有数据
只能给目的主机使用它表示在规定的时间内目的主机没有收到所有的数据分片。
当数据报在因特网上传送时在其首部中出现的任何二义性或者首部字段值被修改都可能
会产生非常严重的问题。
如果路由器或目的主机发现了这种二义性或在数据报的某个字段中缺少某个值就丢弃这个数据报并回送参数问题报文。
回显请求报文和回显应答报文而不用经过传输层来测试目标主机是否可达。
它是一个检查系统连接性的基本诊断工具。
回显请求数据包后它期待着目标主机的回答。
目标主机在收到一个ICMP
回显请求数据包后它会交换源、目的主机的地址然后将收到的ICMP
回显请求的一方。
如果校验正确发送者便认为目标主机的回显服务正常也即物理连接畅通。
查询报文结构如下图所示
中没有正式定义该值的范围所以发送方可以自由定义这两个字段可以用来记录源主机发送出去的请求报文编号。
数据可选区域标识回送请求报文包含数据和长度是可选的发送放应该选择适合的长度和填充数据。
在接收方它可以根据这个回送请求产生一个回送回答报文回送报文的数据与回送请求报文的数据是相同的。
协议本身不提供差错报告和差错控制机制来保证数据报递交的有效性和进行主机管理与查询机制简单来说ICMP
ICMP代码号*/PACK_STRUCT_FIELD(u16_t
ICMP校验和*/PACK_STRUCT_FIELD(u16_t
ICMP的标识符*/PACK_STRUCT_FIELD(u16_t
只实现目的不可到达和超时差错报文它们的实现函数分别为icmp_dest_unreach
cmp_time_exceeded这两个函数转入的参数与icmp_dur_type
枚举相关。
如目的不可到达报文的代码字段由icmp_dur_type
{MIB2_STATS_INC(mib2.icmpoutdestunreachs);icmp_send_response(p,
发送超时报文该函数实际调用函数icmp_send_response来发送ICMP差错报文ICMP_TE
{MIB2_STATS_INC(mib2.icmpouttimeexcds);icmp_send_response(p,
}从上述源码可以看出差错报文的类型已经固定为目的不可到达或者超时它们唯一不同
的是差错报文的代码值这个代码值就是由icmp_dur_type
netif;MIB2_STATS_INC(mib2.icmpoutmsgs);/*
为差错报文申请pbufpbuf预留以太网首部和ip首部申请数据长度为icmp首部长度icmp数据长度(ip首部长度8)
{MIB2_STATS_INC(mib2.icmpouterrors);return;}/*
和UDP则该数据报不会递交给传输层处理若上层协议字段为ICMP
src;ICMP_STATS_INC(icmp.recv);MIB2_STATS_INC(mib2.icmpinmsgs);iphdr_in
回送应答*/MIB2_STATS_INC(mib2.icmpinechoreps);break;case
回送*/MIB2_STATS_INC(mib2.icmpinechos);src
(ip4_addr_ismulticast(ip4_current_dest_addr()))
(ip4_addr_isbroadcast(ip4_current_dest_addr(),ip_current_netif()))
PBUF_LINK_ENCAPSULATION_HLEN)))
PBUF_LINK_ENCAPSULATION_HLEN)))
设置正确的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);/*
{MIB2_STATS_INC(mib2.icmpindestunreachs);}
{MIB2_STATS_INC(mib2.icmpintimeexcds);}
{MIB2_STATS_INC(mib2.icmpinparmprobs);}
{MIB2_STATS_INC(mib2.icmpinsrcquenchs);}
{MIB2_STATS_INC(mib2.icmpinredirects);}
{MIB2_STATS_INC(mib2.icmpintimestamps);}
{MIB2_STATS_INC(mib2.icmpintimestampreps);}
{MIB2_STATS_INC(mib2.icmpinaddrmasks);}
{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;
为了保证数据包传输的可靠行会给每个包一个序号同时此序号也保证了发送到
接收端主机能够按序接收。
然后接收端主机对成功接收到的数据包发回一个相应的确认字符
ACKAcknowledgement如果发送端主机在合理的往返时延RTT内未收到确认字符
协议在发送数据之前要求系统需要在不可靠的信道上建立可靠连接我们称之为“三次握
手”。
建立连接完成之后客户端与服务器才能互发数据不需要发送数据时可以可以断开连
服务器进程先创建传输控制块TCB时刻准备接受客户进程的连接请求此时服
客户进程也是先创建传输控制块TCB然后向服务器发出连接请求报文这是报
服务器收到请求报文后如果同意连接则发出确认报文。
确认报文中应该
ACK1SYN1确认号是ackx1同时也要为自己初始化一个序列号seqy此时TCP
服务器进程进入了SYN-RCVD同步收到状态。
这个报文也不能携带数据但是同样要消
客户进程收到确认后还要向服务器给出确认。
确认报文的ACK1acky1
建立一个连接需要三次握手而终止一个连接需要四次挥手终止连接有以下过程。
第一次挥手客户端发送释放报文并停止发送数据。
释放数据报文首部FIN1,其
序列号为sequ,此时客户端进入FIN-WAIT1等待服务器应答FIN
携带自己的序列号seqv。
此时服务器进入CLOSE-WAIT关闭等待状态。
客户端收到服
务端确认请求此时客户端进入FIN-WAIT2终止等待2状态等待服务器发送连接释放
第三次挥手服务器向客户端发送连接释放报文FIN1、acku1,此时服务器进入
了LAST-ACK最后确认等待客户端的确认。
客户端接收到服务器的连接释放报文后必
须发送确认ack1、ackw1,客户端的序列号为sequ1此时客户端进入TIME-WAIT时
首部包含建立与断开、数据确认、窗口大小通告、数据发送相关的所有标
为客户端的应用程序分配端口号。
在服务器端每种服务在”众所周知的端口”Well-Know
首部长度保留位标志位*/PACK_STRUCT_FIELD(u16_t
remote_port;/*附加状态信息如连接是快速恢复、一个被延迟的ACK
当前接收窗口的大小会随着数据的接收与递交动态变化*/tcpwnd_size_t
将向对方通告的窗口大小随着数据的接收与递交动态变化*/u32_t
TCP_SNDQUEUELEN_OVERFLOW(0xffff
协议可能会花很多精力和时间这里笔者讲解重要的知识即可。
首先我们先
讲解一下接收数据相关的字段rcv_nxtrcv_wndrcv_ann_wnd
snd_nxtsnd_maxsnd_wndacked这些字段和TCP
③rcv_ann_wnd表示将向对方通告的窗口大小值这个值在报文发送时会被填在首部中
④rcv_ann_right_edge记录了上一次窗口通告时窗口右边界取值该字段在窗口滑动过
就是通知对方窗口大小的值而rcv_ann_right_edge
窗口右边界取值14当然下一次发送时这四个变量就不一定是上述图中的值了它们会
随着数据的发送与接收动态改变。
当接收到数据后数据会被放在接收窗口中等待上层调用
时内核会计算一个合理的窗口值rcv_ann_wnd并不一定与rcv_wnd
③snd_wnd记录了当前的发送窗口大小它常被设置为接收方通告的接收窗口值。
④snd_lbb记录了下一个将被应用程序缓存的数据的起始编号。
可以看出左边部分是已经发送并确认的数据绿色框是已经发送但未确认的数据需要
等待对方确认红色框可以发送的数据最右边的是不能发送的。
上面这四个字段的值也是
段的形式组织的因此可能存在这样的情况即使发送窗口允许但并不是窗口内的所有数据
效的报文段因此不会被发送。
发送方会等到新的确认到来从而使发送窗口向右滑动使得
除了定义结构体tcp_pcb它还定义了结构体tcp_pcb_listen前者我们知道有这个就
处于该状态不会进行数据发送、连接握手之类的服务主要是分配完整的TCP
两种控制块都具有的字段*/TCP_PCB_COMMON(struct
操作一般对于链表上的控制块进行查找这四个控制块链表在tcp.c
/*连接所有进行了端口号绑定但是还没有发起连接主动连接或进入侦听状态被动连接的控制块*/
在内核中所有待发送的数据或者已经接收的数据都会以报文的形式保存一般都是保存
用就是把所有报文段连接起来当然这些报文段可以是无发送、已发送并未确认的或者是以收
(u8_t)0x10U/*包括SACK允许选项(仅在SYN段中使用)*//*
};每个控制块中都维护了三个缓冲队列unsent、unacked、ooseq
报文段的接收函数是tcp_input该函数位于tcp_inc.c
(ip_addr_isbroadcast(ip_current_dest_addr(),
||ip_addr_ismulticast(ip_current_dest_addr()))
若TCP报头在第一个pbuf中*/tcphdr_opt1len
TCP报头选项长度*/pbuf_remove_header(p,
确定选项的第一部分和第二部分长度*/tcphdr_opt1len
移除tcphdr_opt1len选项*/pbuf_remove_header(p,
记住指向TCP报头选项的第二部分的指针(有部分选项在第二个pbuf中记录TCP报头选项的开始部分)
TCP数据包中数据的总长度对于有FIN或SYN标志的数据包该长度要加1
****************************省略代码*********************************
如果pcb在回调中被中止(通过调用tcp_abort())则跳转目标。
*/aborted:tcp_input_pcb
{/*如果在3张链表里都未找到匹配的pcb则调用tcp_rst向源主机发送一个TCP复位数据包*/if
{TCP_STATS_INC(tcp.proterr);TCP_STATS_INC(tcp.drop);tcp_rst(NULL,
ip_current_dest_addr(),ip_current_src_addr(),
递交传输层的数据报检验例如检验数据报是否正常操作、是否包含数据、该数据报是否为广
播或者多播如果以上检验成立则系统把该数据报掉弃处理并释放pbuf。
下部分主要对
传输层与网络层的交互函数为tcp_output它在tcp_output.c
从发送窗口和阻塞窗口取小者得到有效发送窗口拥塞避免会讲解到这个原理*/wnd
可用数据和窗口允许它发送报文段直到把数据全部发送出去或者填满发送窗口*/while
如果未确认队列不为空则需要把当前报文按照顺序组织在队列中*/if
如果当前报文的序列号比队列尾部报文的序列号低则从队列首部开始查找合适的位置插入报文段*/struct
*/output_done:tcp_clear_flags(pcb,
}从整体来看此函数首先检测报文是否满足发送要求接着判断控制块的flags
列中无数据发送或者发送窗口不允许发送数据。
如果内核能发送数据则就将ACK
发送出去同时在发送的时候先找到未发送链表然后调用tcp_output_segment()-
ip_output_if()函数进行发送直到把未发送链表的数据完全发送出去或者直到填满发送窗口
并且更新发送窗口相关字段当然也要将这些已发送但是未确认的数据存储在未确认链表中
以防丢失数据进行重发操作放入未确认链表的时候是按序号升序进行排序的。
图存在某种联系下面笔者简单的讲解这个函数到底如何连接服务器该函数如下所示
{/*.....................前面省略大部分代码......................*//*
pcb);}TCP_REG_ACTIVE(pcb);MIB2_STATS_INC(mib2.tcpactiveopens);tcp_output(pcb);(3)}return
}可见上述的(1)表示程序调用函数tcp_enqueue_flags
向服务器发送连接请求报文。
下面笔者使用一个示意图来描述上述的内容如下图所示
客户端等待服务器的连接应答报文TCP_ACK。
当客户端接收服务器应答报文TCP_ACK
需重复讲解了该连接应答报文会在tcp_input–tcp_process
{/*..................此处省略了很多代码.....................
ESTABLISHED;(2)}/*..................此处省略了很多代码.....................
*/}/*..................此处省略了很多代码.....................
}上述的的(1)就是为了判断服务器应答报文的标志位是否包含TCP_ACK
果该应答报文包含这些标志位则系统执行上述(2)的代码设置TCP
应答报文给服务器才能实现第三次握手。
上面的函数tcp_process
{/*..................此处省略了很多代码.....................
netif);/*..................此处省略了很多代码.....................
发送该应答包这里就完成了三次握手的动作。
下面笔者使用一个示意图
等待连接。
注意有连接时会调用函数lwip_tcp_server_accept
*tcp_listen_with_backlog(struct
{LWIP_ASSERT_CORE_LOCKED();return
tcp_listen_with_backlog_and_err(pcb,
*tcp_listen_with_backlog_and_err(struct
..............省略代码..............
..............省略代码..............
第一次握手流程对于服务器而言它是先接收客户端发来的连接请求包并判断该请求报文的首部标志位是否包含T
CP_SYN这个请求报文的处理是由tcp_input→tcp_listen_input
*tcp_listen_with_backlog(struct
{LWIP_ASSERT_CORE_LOCKED();return
tcp_listen_with_backlog_and_err(pcb,
*tcp_listen_with_backlog_and_err(struct
..............省略代码..............
..............省略代码..............
内核首先判断连接请求报文的首部标志位是否包含TCP_SYN显然这个符合
上图的红色框框就是服务器接收客户端的连接请求报文之后发送连接应答报文到了这里
ESTABLISHED至此客户端和服务器可以相互发送数据了。
TCP
回调函数指针设置为NULL应用层不再接收数据所有数据直接被丢弃协
议层的处理仍按正常流程走认为应用层已经接收到数据tcp_close
意以下源码的路径tcp_close→tcp_close_shutdown→tcp_close_shutdown_fin
{tcp_backlog_accepted(pcb);MIB2_STATS_INC(mib2.tcpattemptfails);pcb
{MIB2_STATS_INC(mib2.tcpestabresets);/*
{MIB2_STATS_INC(mib2.tcpestabresets);pcb
}大家请看上述有注释的代码这些代码是客户端发送关闭连接请求报文过程该包的首部
函数处理当然它接收到的数据可以发送给应用层但是它递交一个空的EOF
数据给应用层应用层知道接收数据已经完成不需要再从协议栈读数据最后系统发送客
ESTABLISHED:tcp_receive(pcb);if
上图红色框框就是上述源码运行的流程为了理解笔者没有把全部的代码列举出来。
发送ACK应答对端的FIN报文*/tcp_ack_now(pcb);TCP_RMV(
tcp_timewait_input处理所有数据都丢弃不发送给应用层直接确认当前收到的报文rcv_nxt设置为当前报文的下一个字节*/pcb
(客户端、服务器同时调用tcp_close都在FIN_WAIT_1状态收到对方的FIN报文)*/tcp_ack_now(pcb);
队列移除并设置该控制块的状态为TIME_WAIT。
最后把该控制块挂在
控制块的状态为FIN_WAIT_2下面我们使用一个示意图来描述上述的
{tcp_backlog_accepted(pcb);MIB2_STATS_INC(mib2.tcpattemptfails);pcb
{MIB2_STATS_INC(mib2.tcpestabresets);pcb
{MIB2_STATS_INC(mib2.tcpestabresets);/*
构建ACK报文*/tcp_ack_now(pcb);tcp_pcb_purge(pcb);TCP_RMV_ACTIVE(pcb);/*
的网络调试助手配置成服务器。
开发板接收服务器发送的数据在LCD
连接标记*/lwip_tcp_client_set_remoteip();
g_point_color);lcd_show_string(30,
g_point_color);lcd_show_string(30,
g_point_color);lcd_show_string(30,
g_point_color);lcd_show_string(30,
lwipdev.ip[0],lwipdev.ip[1],lwipdev.ip[2],lwipdev.ip[3]);
lwipdev.remoteip[0],lwipdev.remoteip[1],lwipdev.remoteip[2],lwipdev.remoteip[3]);lcd_show_string(30,
lwipdev.remoteip[1],lwipdev.remoteip[2],
连接到目的地址的指定端口上,当连接成功后回调tcp_client_connected()函数*/tcp_connect(tcppcb,
TCP_CLIENT_PORT,lwip_tcp_client_connected);}
{lwip_tcp_client_usersent(tcppcb);
g_point_color);lwip_client_flag
STATUS:Disconnected,g_point_color);lcd_fill(30,
标记连接断开了*/}lwip_periodic_handle();delay_ms(2);t;if
{lwip_tcp_client_connection_close(tcppcb,
连接到目的地址的指定端口上,当连接成功后回调tcp_client_connected()函数*/tcp_connect(tcppcb,
TCP_CLIENT_PORT,tcp_client_connected);}}t
0;LED0_TOGGLE();}}lwip_tcp_client_connection_close(tcppcb,
地址的函数lwip_tcp_client_set_remoteip如下源码所示
lwip_tcp_client_set_remoteip(void)
key;lcd_clear(BLACK);g_point_color
g_point_color);lcd_show_string(30,
g_point_color);lcd_show_string(30,
g_point_color);lcd_show_string(30,
g_point_color);lcd_show_string(30,
前三个IP保持和DHCP得到的IP一致*/lwipdev.remoteip[0]
lwipdev.ip[0];lwipdev.remoteip[1]
lwipdev.ip[1];lwipdev.remoteip[2]
lwipdev.remoteip[0],lwipdev.remoteip[1],lwipdev.remoteip[2]);lcd_show_string(30,
0X80,g_point_color);}}myfree(SRAMIN,
连接建立后的回调函数lwip_tcp_client_connected如下源码所示
初始化LwIP的tcp_recv回调功能*/tcp_recv(tpcb,
lwip_tcp_client_recv);tcp_err(tpcb,
初始化LwIP的tcp_sent回调功能*/tcp_sent(tpcb,
初始化LwIP的tcp_poll回调功能*/tcp_poll(tpcb,
{lwip_tcp_client_connection_close(tpcb,
{lwip_tcp_client_connection_close(tpcb,
态有不同的处理这里最重要的就是当处于连接状态并且接收到数据时的处理这个时候我们
链表将链表中的所有数据拷贝到lwip_tcp_client_recvbuf
的接收处理过程相似。
数据接收成功以后我们将lwip_client_flag
数据接收缓冲区清零*/memset(lwip_client_recvbuf,
判断要拷贝到TCP_CLIENT_RX_BUFSIZE中的数据是否大于TCP_CLIENT_RX_BUFSIZE的剩余空间如果大于*//*
的话就只拷贝TCP_CLIENT_RX_BUFSIZE中剩余长度的数据否则的话就拷贝所有的数据*/if
标记接收到数据了*//*用于获取接收数据,通知LWIP可以获取更多数据*/tcp_recved(tpcb,
用于获取接收数据,通知LWIP可以获取更多数据*/tcp_recved(tpcb,
调用这里我们没有实现这个函数用户可以根据自己的实际情况来实现这个函数。
在这个函数中我们可以将要发送的数据发送出去。
通过lwip_client_flag
话就将发送缓冲区lwip_tcp_client_sendbuf
来实现这个过程然后我们调用lwip_tcp_client_senddata
{lwip_tcp_client_connection_close(tpcb,
tcp_sent的回调函数(当从远端主机接收到ACK信号后发送数据)*
lwip_tcp_client_senddata(struct
}lwip_tcp_client_connection_close
函数来关闭与服务器的连接然后注销掉控制块中的回调函数将lwip_client_flag
标记连接断开lwip_tcp_client_connection_close
lwip_tcp_client_connection_close(struct
g_point_color);lcd_show_string(30,
g_point_color);lcd_show_string(30,
g_point_color);lcd_show_string(30,
g_point_color);lcd_show_string(30,
代码编译成功之后下载代码到开发板中。
打开网络调试助手软件设置为如下图的信息。
我们通过网络调试助手向开发板发送http://www.openedv.com此时开发板LCD
器(网络调试助手)网络调试助手给开发板发送数据开发板接收数据并通过串口将接收到的
数据发送到串口调试助手上也可以通过按键从开发板向网络调试助手发送数据。
③设置接收超时时间tcp_clientconn-recv_timeout。
中我们实现了一个函数lwip_demo同上一章一样都有操作系统
netconn_connect(tcp_clientconn,
{printf(接连失败\r\n);/*返回值不等于ERR_OK,删除tcp_clientconn连接*/netconn_delete(tcp_clientconn);}
10;/*获取本地IP主机IP地址和端口号*/netconn_getaddr(tcp_clientconn,
1);printf(连接上服务器%d.%d.%d.%d,本机端口号为:%d\r\n,DEST_IP_ADDR0,DEST_IP_ADDR1,DEST_IP_ADDR2,DEST_IP_ADDR3,
tcp_client_sendbuf,strlen((char
{printf(发送失败\r\n);}tcp_client_flag
/*进入临界区*//*数据接收缓冲区清零*/memset(lwip_demo_recvbuf,
/*超出TCP客户端接收数组,跳出*/}}taskEXIT_CRITICAL();
/*复制完成后data_len要清零*/printf(%s\r\n,
lwip_demo_recvbuf);netbuf_delete(recvbuf);}
{netconn_close(tcp_clientconn);netconn_delete(tcp_clientconn);printf(服务器%d.%d.%d.%d断开连接\r\n,
实验非常相似它们唯一不同的是连接步骤以及发送函数不同注意上述函数做了一个判断服务器与客户端的连接状态如果这个连接状态是断
我们通过网络调试助手发送数据到开发板当中结果如图17.2.3.3
{sys_thread_new(lwip_send_thread,
函数创建发送数据线程它的线程函数为lwip_send_thread
表示IPv4网络协议*/atk_client_addr.sin_port
端口号*/atk_client_addr.sin_addr.s_addr
0,sizeof(atk_client_addr.sin_zero));tbuf
-1;closeSocket(sock);myfree(SRAMIN,
sock_start;}printf(连接成功\r\n);lwip_connect_state
lwip_demo_recvbuf,LWIP_DEMO_RX_BUFSIZE,
{printf(队列Key_Queue已满数据发送失败!\r\n);}vTaskDelay(10);}}
~LWIP_SEND_DATA;}vTaskDelay(10);}closeSocket(sock);}
Transport消息队列遥测传输协议是一种基于发布/订阅Publish/Subscribe模式的轻量级通讯协议该协议构建于TCP/IP
最大的优点在于可以以极少的代码和有限的带宽为远程设备提供实时可靠的消息服务。
做为一种低开销、低带宽占用的即时通讯协议MQTT在物联网、小型设备、移动应用等方面有广泛的应用MQTT
开放和易于实现的这些特点使它适用范围非常广泛。
在很多情况下包括受限境中如机器与机器M2M通信和物联网IoT。
其在通过卫星链路通信传感器、医疗设备、智能家居、及一些小型化设备中已广泛使用。
代理Broker服务器、订阅者Subscribe。
其中消息的发布者和订阅者都是客户端消息代理是服务器消息发布者可以同时是订阅者如下图所示。
传输的消息分为主题Topic和消息的内容payload两部分。
Topic可以理解为消息的类型订阅者订阅Subscribe后就会收到该主题的消息内
Payload可以理解为消息的内容是指订阅者具体要使用的内容。
代理服务一直是处于指定端口的监听状态当监听到有客户端要接入的时候就会立刻去处理。
客户端在发起连接请求时携带客户端ID、账号、密码无账号密码使用除外正式项目不会允许这样、心跳间隔时间等数据。
代理服务收到后检查自己的连接权限配置中是否允许该账号密码连接如果允许则建立会话标识并保存绑定客户端ID
与会话并记录心跳间隔时间判断是否掉线和启动遗嘱时用和遗嘱消息等然后回发连接成功确认消息给客户端客户端收到连接成功的确认消息后进入下一步通常是开始订阅主题如果不需要订阅则跳过。
如下图所示
下以后有这个主题发布就会发送给该客户端然后回复确认消息SUBACK
报文后知道已经订阅成功则处于等待监听代理服务推送的消息也可以继续订阅其他主题或发布主题如下图所示
当某一客户端发布一个主题到代理服务后代理服务先回复该客户端收到主题的确认消
息该客户端收到确认后就可以继续自己的逻辑了。
但这时主题消息还没有发给订阅了这个主题的客户端代理要根据质量级别QoS来决定怎样处理这个主题。
所以这里充分体现了是MQTT
的客户端是否在线在线则转发一次收到与否不再做任何处理。
这种质量对系统压力最小。
有成功收到才可以否则会尝试补充发送具体机制后面讨论。
这也可能会出现同一主题多次重复发送的情况。
这种质量对系统压力较大。
有成功收到并只收到一次不会重复发送具体机制后面讨论。
这种质量对系统压力最大。
添加到工程当中这里我们在工程中添加一个名为Middlewares/lwip/src/apps
第一步注册阿里云平台打开产品分类/物联网Iot/物联网应用开发如下图所示。
第二步在物联网应用开发页面下点击项目管理/新建项目/新建空白项目在此界面下填
创建项目完成之后在项目管理页面下点击项目进去子项目管理界面如下图所示
注上图中的节点类型、连网方式、数据格式以及认证模式的选择其他产品参数根据用
第六步打开“产品/查看/功能定义”路径在该路径下添加功能定义如下图所示。
第七步打开自定义功能并发布上线这里我们添加了两个CurrentTemperature
*/lwip_ali_get_password(DEVICE_SECRET,
设置客户端的信息量*/mqtt_client_info.client_id
设备名称*/mqtt_client_info.client_user
计算出来的密码*/mqtt_client_info.keep_alive
保活时间*/mqtt_client_info.will_msg
NULL;mqtt_client_info.will_retain
连接服务器*/mqtt_client_connect(mqtt_client,
服务器IP与端口号*/mqtt_connection_cb,/*
设置服务器连接回调函数*/LWIP_CONST_CAST(void
payload_out,{\params\:{\CurrentTemperature\:
\RelativeHumidity\:%0.1f},\method\:\thing.event.property.post\},
payload_out);mqtt_publish(mqtt_client,
服务器控制块接着我们调用mqtt_client_connect
函数连接阿里云服务器并添加mqtt_connection_cb
连接回调函数最后在while()语句中判断是否订阅操作成功如果系统订阅成功则构建MQTT
mqtt_connection_cb(mqtt_client_t
arg;LWIP_UNUSED_ARG(client);printf(\r\nMQTT
(mqtt_client_is_connected(client))
设置传入发布请求的回调*/mqtt_set_inpub_callback(mqtt_client,mqtt_incoming_publish_cb,mqtt_incoming_data_cb,NULL);/*
订阅操作并设置订阅响应会回调函数mqtt_sub_request_cb
}此函数也是非常简单它主要调用函数mqtt_client_is_connected
判断是否已经连接服务器如果连接成功则程序调用函数mqtt_set_inpub_callback
回调函数这些回调函数需要根据客户端以及服务器的发布操作才能进去该回调函数最后我们调用函数mqtt_subscribe
下载完代码后在浏览器上打开阿里云平台并在指定的网页查看上存数据如下图所示。
第二步在上图中点击“立刻使用”选项页面跳转完成之后点击“添加产品”选项此
时该页面会弹出产品信息小界面这里我们根据自己的项目填写相关的信息如下图所示
上图中我们重点添加的选项有联网方式和设备接入协议这里笔者选择移动蜂窝网络以
本实验会用到上述的产品信息例如产品ID366007、“access_key”产品密钥以及产品
第四步在上图创建的设备中点击右边的详情标签进入标签的链接页面在这个页面下
本实验会用到上图中的设备ID617747917、设备名称MQTT
物联网套件采用安全鉴权策略进行访问认证即通过核心密钥计算的token
是根据我们前面创建的产品和设备相关的信息计算得来的密钥的计算方法可以使用OneNET
生成工具计算该软件可在这个网址下载https://open.iot.10086.cn/doc/v5/develo
res输入格式为“products/{pid}/devices/{device_name}”这个输入格式中的“pid”就是
产品ID而“device_name”就是设备的名称。
根据前面创建的产品和设备来填写
et访问过期时间expirationTimeunix时间这里笔者选择参考文档中的数值
最后按下上图中的“Generate”按键生成核心密钥如下图所示。
导致每次创建一个设备都必须根据这个设备信息再一次计算核心密钥才能连接这种方式会大
大地降低我们的开发效率为了解决这个问题笔者使用另一个方法那就是使用代码的方式
计算核心密钥它和上一章节中的方式不一样因为阿里云和OneNET
平台的核心密钥这些文件在oneos2.0\components\cloud\onenet\m
打开工程并在Middlewares/lwip/lwip_app
这些文件都在oneos2.0\components\cloud\onenet\mqtt-kit\authorization
手册该手册地址为https://open.iot.10086.cn/doc/v5/develop/detail/251这个地址里面已经说明
客户端用于实现平台与应用服务器之间的单向数据通信。
平台作为客户端通过
请求方式将项目下应用数据、设备数据推送给用户指定服务器。
本章主要介
接入方式可参考该官方的文档手册该文档手册地址为https://op
en.iot.10086.cn/本实验主要参考官方文档的多协议接入/HTTP/上传数据点的内容。
上图中的几个技术参数非常重要剩下的技术参数根据用户的爱好填写。
第四步双击创建的产品并点击设备列表且在设备列表中添加设备如下图所示。
Host:api.heclouds.com\r\n);strcat(pkt,
如果我们使用网络调试助手接收该数据包那么我们发现该数据与OneNET
/devices/655766336/datapoints?type5
api-key:rw2p2FqVW4fhhhkj4CwpVcqJq8
/devices/655766336/datapoints?type5
api-key:rw2p2FqVW4fhhhkj4CwpVcqJq8
TCP_DEMO_PORT;netconn_gethostbyname(DEST_MANE,
netconn_connect(tcp_clientconn,
返回值不等于ERR_OK,删除tcp_clientconn连接*/netconn_delete(tcp_clientconn);}
获取本地IP主机IP地址和端口号*/netconn_getaddr(tcp_clientconn,
发送tcp_server_sentbuf中的数据*/netconn_write(tcp_clientconn,
发送tcp_server_sentbuf中的数据*/netconn_write(tcp_clientconn,
NETCONN_COPY);vTaskDelay(1000);/*
数据接收缓冲区清零*/memset(tcp_client_recvbuf,
TCP_CLIENT_RX_BUFSIZE);/*遍历完整个pbuf链表*/for
超出TCP客户端接收数组,跳出*/}}taskEXIT_CRITICAL();
复制完成后data_len要清零*/printf(%s\r\n,
tcp_client_recvbuf);netbuf_delete(recvbuf);}
{netconn_close(tcp_clientconn);netconn_delete(tcp_clientconn);goto
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback