SEO基础

SEO基础

Products

当前位置:首页 > SEO基础 >

如何为四川奎屯地区建设一个专业的网站?

96SEO 2026-02-20 05:13 15


TCP/IP

本章先介绍计算机网络相关知识然后对lwIP软件库进行概述接着介绍MAC

如何为四川奎屯地区建设一个专业的网站?

和YT8512C

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

TCP/IP

协议采用4层结构分别是应用层、传输层、网络层和网络接口层每一层都呼叫它的下一层所提供的协议来完成自己的需求。

由于我们大部分时间都工作在应用层下层的事情不用我们操心其次网络协议体系本身就很复杂庞大入门门槛高因此很难搞清楚TCP/IP

TCP/IP

模型因其开放性和易用性在实践中得到了广泛的应用它也成为互联网的主流协议。

注意网络技术的发展并不是遵循严格的OSI分层概念。

实际上现在的互联网使用的是TCP/IP

体系结构有时已经演变成为图1.1.1.2所示那样即某些应用程序可以直接使用IP

无论哪种表示方法TCP/IP

协议栈负责确保网络设备之间能够通信。

它是一组规则规定了信息如何在网络中传输。

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

如Windows

协议栈的应用层传输层和网络层的功能网络接口层由网卡实现所以CBISC

协议栈和网卡构建了网络通信的核心骨架。

因此无论哪一款以太网产品都必须符合TCP/IP

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

TCP/IP

协议栈的封包和拆包也是一个非常重要的知识如以太网设备发送数据和接收数据的处理流程是怎么样的这个问题涉及到TCP/IP

协议栈对数据处理的流程该流程称之为“封包”和“拆包”。

“封包”是对发送数据处理的流程而“拆包”是对接收数据处理的流程如下图所示。

上图中发送端发送的数据自顶向下依次传递。

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

上图中当帧数据到达目的主机时将沿着协议栈自底向上依次传递。

各层协议依次根据帧中本层负责的头部信息以获取所需数据最终将处理后的帧交给应用层这个流程称之为拆包的过程。

lwIP

的设计理念下既可以无操作系统使用也可以带操作系统使用既可以支持多线程也可以无线程。

它可以运行在8

lwIP

体系结构的应用层、传输层和网络层的功能但网络接口层不能使用软件的方式实现因为网络接口层是把数据包转成光电模拟信号并转发至网络所以网络接口层只能由硬件来实现。

lwIP

的项目主页http://savannah.nongnu.org/projects/lwip/。

在这个主页上读者需要关注“project

homepage”和“download

更新日记、常见误解、已发现的BUG、多线程、优化提示和相关文件中的函数描述等内容。

Area

包不属于lwIP内核的一部分它只是为我们提供移植文件和学习实例。

和contrib-2.1.0.zip

根据上一个小节的操作我们已经下载了lwip-2.1.3.zip

和contrib-2.1.0.zip

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

上表中src

的内核文件也是我们移植到工程中的重要文件。

接下来笔者重点讲解src

api

文件夹下的文件实现了网络层与数据链路层交互接口以及管理不同类型的网卡。

打开core

协议栈的各种协议、内存管理、数据包管理、网卡管理、网卡接口、基础功能和API接口模块等每一个模块是由几个源文件和一个头文件集合这些头文件全部放在include

lwIP

的物理存储器它们分别存储网络层递交的以太网数据和接收的以太网数据。

以太网DMA

MAC内核数据链路层与

芯片的管理和配置是站管理接口SMI所需的通信引脚。

站管理接口SMI允许应用程序通过2

个PHY

MII_RX_ER接收错误信号。

该信号必须保持一个或多个周期(MII_RX_CLK)从而向MAC

精简介质独立接口RMII

相比MII其发送和接收都少了两条线。

因此要达到10Mbit/s

的速度其时钟频率应为50MHz。

正点原子开发板就是采用此接口连接PHY

引脚需要提供50MHz

体系架构中扮演着物理层的角色它把数据转换成光电模拟信号传输至网络当中。

本小节为读者介绍正点原子常用的PHY

芯片它们分别为LAN8720A

百兆以太网传输速率为此笔者分两个小节来讲解这两款以太网芯片的知识。

YT8512C

芯片。

它通过两条标准双绞线电缆收发器发送和接收数据所需的所有物理层功能。

另外YT8512C

和RMII

芯片的内部总架构示意图从图中我们大概可以看出它通过LED0\LED1

地址由XTAL,Clock

库版本旧所以它们的移植流程存在巨大的差异。

这里笔者暂且不讲解这部分的内容。

YT8521C

的RX_DV8和RXD312引脚决定具体如何选择请读者参考“YT8512C.PDF”手册的17

到18

特殊功能寄存器17这三个寄存器。

首先我们来看一下BCR0寄存器BCR

配置文件

((uint16_t)0x0001)阿波罗、北极星开发板PHY

配置文件下

((uint16_t)0x0001U)由于探索者及DMF407

库所以这两个寄存器并不需要读者来操作原因就是我们调用HAL_ETH_Init

的相应寄存器。

但是阿波罗及北极星开发板的例程使用目前最新的HAL

版本它要求读者手动操作BCR

个寄存器所以每个厂家的可能不同这个需要用户根据自己实际使用的PHY

提供的以太网驱动文件有三个配置项值得读者注意的它们分别为PHY_SR、PHY_SPEED_STATUS

和PHY_DUPLEX_STATUS

的中断系统提供两种中断模式主中断模式和复用中断模式。

主中断模式是默认中断模式LAN8720A

上电或复位后就工作在主中断模式当模式控制/状态寄存器(十进制地址为17)的ALTINT

工作在主模式当ALTINT

系列开发板并未用到中断功能关于中断的具体用法可以参考LAN8720A

PHY

芯片采用分页技术来扩展地址空间定义更多的寄存器在这里我们不讨论这种情况。

IEEE

802.3

特殊功能寄存器31这三个寄存器前面两个寄存器笔者已经在1.6.1

小节讲解了这里笔者无需重复讲解。

接下来介绍的是LAN8720A

置BCR

协议栈用途非常广泛如电脑、交换机等网络设备而全硬件TCP/IP

协议栈是近年来比较新型的以太网接入方案。

下面笔者分别来讲解这两种接入方案的差异和优缺点。

软件TCP/IP

阿波罗、北极星以及电机开发板都是采用这类型的以太网接入方案该方案的连接示意图如下图所示

上图中MCU

移植性可在不同平台、不同编译环境的程序代码经过修改转移到自己的系统中运行。

协议RAM

从代码量分析移植lwIP可能需要的代码量超过40KB对于有些主控芯片内存匮乏

从运行性能方面分析由于软件TCP/IP协议栈方案在通信时候是不断地访问中断机

制造成线程无法运行如果多线程运行会使MCU的工作效率大大降低。

从安全性方面分析软件协议栈会很容易遭受网络攻击造成单片机瘫痪。

硬件TCP/IP

MACPHY、内存管理等功能完成了一整套硬件化的以太网解决方案。

上图中MCU

从运行方面来看极大的减少了中断次数让单片机更好的完成其他线程的工作。

从安全性方面来看硬件化的逻辑门电路来处理TCP/IP协议是不可被攻击的也就

是说网络攻击和病毒对它无效这也充分弥补了网络协议安全性不足的短板。

从可扩展性来看虽然该芯片内部使用逻辑门电路来实现应用层和物理层协议但是

它具有功能局限性例如给TCP/IP协议栈添加一个协议这样它无法快速添加了。

从收发速率来看全硬件TCP/IP协议栈芯片都是采用并口、SPI以及IIC等通讯接

用于处

编程和少量的寄存器操作即可方便地进行嵌入式以太网上层应用开发减少产品开发周期降低开发成本。

lwIP

请求广播到局域网络上的所有主机并接收返回消息以此确定目标的物理地址收到返回消息后将该IP

地址和物理地址存入本机ARP

缓存以节约资源。

地址解析协议是建立在网络中各个主机互相信任的基础上的局域网络上的主机可以自主发送ARP

应答消息其他主机收到应答报文时不会检测该报文的真实性就会将其记入本机ARP

ARP

假设由两台主机分别为主机A192.168.0.10与主机B192.168.0.11它们两个都是

向主机B

arp_table[ARP_TABLE_SIZE];可以看出ARP

缓存表arp_table最大存放10

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

状态如下所示

0,ETHARP_STATE_PENDING,ETHARP_STATE_STABLE,ETHARP_STATE_STABLE_REREQUESTING_1,ETHARP_STATE_STABLE_REREQUESTING_2

(1)

缓存表处于初始化的状态所有表项初始化之后才可以被使用如果需要添加表项lwIP

内核就会遍历ARP

地址的映射关系并且开始记录表项的生存时间同时该表项的状态会变成ETHARP_STATE_STABLE

(3)

当收到应答之前这些数据包会暂时挂载到表项的数据包缓冲队列上收到应答之后系统已经更新ARP

缓存表那么系统发送数据就会进入该状态

(4)ETHARP_STATE_STABLE_REREQUESTING_1

ETHARP_STATE_STABLE_REREQUESTING_2

如果系统再一次发送ARP

请求数据包则表项状态会暂时被设置为ETHARP_STATE_ST

ABLE_REREQUESTING_1之后设置为ETHARP_STATE_STABLE_REREQUESTING_2

秒之前收到ARP

缓存表各个表项的状态和检测各个表项的生存时间。

稍后笔者也会讲解ARP

表项挂起数据包

内核把要发送的数据包挂载到新创建的表项当中。

在表项中包含了etharp_q_entry

结构体和pbuf

其实这个参数笔者在上面也有所涉及因为系统以周期的形式调用函数etharp_trm。

例如5秒之前收到ARP

缓存表项ctime

处理如果某个表项的生存时间计数值大于系统规定的某个值系统就会删除该表项。

etharp_trm

函数如下所示

第三步发送ARP请求数据包并判断ctime是否大于5秒*/if

((arp_table[i].ctime

从ARP缓存表中删除该表项*/etharp_free_entry(i);}

else

ETHARP_STATE_STABLE_REREQUESTING_1)

{/*

ETHARP_STATE_STABLE_REREQUESTING_2;}

else

ETHARP_STATE_STABLE_REREQUESTING_2)

{/*

将状态重置为稳定状态使下一个传输的数据包将重新发送一个ARP请求*/arp_table[i].state

else

仍然挂起重新发送一个ARP查询*/etharp_request(arp_table[i].netif,

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

间那么lwIP

左边的是以太网首部数据发送时必须添加以太网首部添加完成之后才能把数据发往到

个字段它们分别为

地址。

协议类型表示要映射的协议地址类型0x0800–映射为IP

请求和应答分别设置为6

/**********************************e***rnet.h********************************/

define

一个以太网MAC地址*/PACK_STRUCT_FLD_8(u8_t

PACK_STRUCT_STRUCT;

ETH_PAD_SIZEPACK_STRUCT_FLD_8(u8_t

padding[ETH_PAD_SIZE]);#endifPACK_STRUCT_FLD_S(struct

eth_addr

/***********************************etharp.h**********************************/

struct

{ETHARP_STATS_INC(etharp.memerr);return

ERR_MEM;}/*

源IP地址*/IPADDR_WORDALIGNED_COPY_FROM_IP4_ADDR_T(

hdr

目的IP地址*/IPADDR_WORDALIGNED_COPY_FROM_IP4_ADDR_T(

hdr

(ip4_addr_islinklocal(ipsrc_addr))

ethsrc_addr,

调用底层发送函数将以太网数据帧发送出去*/e***rnet_output(netif,

ethsrc_addr,

ETHTYPE_ARP);}ETHARP_STATS_INC(etharp.xmit);/*

NULL;/*

最接近网卡驱动文件发送的数据经过ARP检测和操作发送至网卡驱动文件处理由网卡驱动文件调用ETH

外设把数据发送至PHY

第一步判断数据包是否小于等于以太网头部的大小如果是则释放内存直接返回*/if

len

{ETHARP_STATS_INC(etharp.proterr);ETHARP_STATS_INC(etharp.drop);MIB2_STATS_NETIF_INC(netif,

ifinerrors);goto

第二步p-payload表示指向缓冲区中实际数据的指针相当于指向以太网的头部*/ethhdr

(struct

去除以太网首部失败则直接返回*/ETHARP_STATS_INC(etharp.lenerr);ETHARP_STATS_INC(etharp.drop);goto

free_and_return;}

LWIP_HOOK_UNKNOWN_ETH_PROTOCOLif

(LWIP_HOOK_UNKNOWN_ETH_PROTOCOL(p,

netif)

{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

ERR_OK;

目的是提高网络的可扩展性一是解决互联网问题实现大规模、异构网络的互联互通二是分割顶层网络应用和底层网络技术之间的耦合关系以利于两者的独立发展。

根据端到端的设计原则IP

位于TCP/IP

模型的网络层它可以向传输层提供各种协议的信息例如TCP、UDP

信息包放到链路层通过以太网、令牌环网络等各种技术来传送。

为了能适应异

构网络IP

强调适应性、简洁性和可操作性并在可靠性做了一定的牺牲。

这里我们不过多

数据报

个字节由于以太网网络接口的最大传输单元为1500所以一个完整的数据包不

能超出1500

位可表示的最大十进制数值是15。

请注意这个字段所表示数的单位是32

位字长1

字节的整数倍时必须利用最后的填充字段加以填充。

因此数据部分永远在4

首部长度限制为60

字节的缺点是有时可能不够用。

但这样做是希望用户尽量减少开销。

最常用的首部长度就是20

(3)

位用来获得更好的服务。

这个字段在旧标准中叫做服务类型但实际上一直没有被使用过。

(4)

总长度总长度指首部和数据之和的长度单位为字节。

总长度字段为16

在IP

层下面的每一种数据链路层都有自己的帧格式其中包括帧格式中的数据字段的最

大长度这称为最大传送单元MTU。

当一个数据报封装成链路层的帧时此数据报的总长度即首部加上数据部分一定不能超过下面的数据链路层的MTU

(5)

软件在存储器中维持一个计数器每产生一个数据报计数器就加1并将此值赋给标识字段。

但这个“标识”并不是序号因为IP

是无连接服务数据报不存在按序接收的问题。

当数据报由于长度超过网络的MTU

而必须分片时这个标识字段的值就被复制到所有的数据报的标识字段中。

相同的标识字段的值使分片后的各数据报片最后能正确地重装成为原来的数据报。

(6)

表示这已是若干数据报片中的最后一个。

标志字段中间的一位记为DFDon’t

(7)

个字节为偏移单位。

这就是说除了最后一个分片每个分片的长度一定是8

字节64

报在网络中的寿命。

由发出数据报的源点设置这个字段。

其目的是防止无法交付的数据报无限制地在因特网中兜圈子因而白白消耗网络资源。

最初的设计是以秒作为TTL

减去数据报在路由器消耗掉的一段时间。

若数据报在路由器消耗的时间小于1

秒就把TTL

字段的功能改为“跳数限制”但名称不变。

路由器在转发数据报之前就把TTL

值减1.若TTL

的意义是指明数据报在网络中至多可经过多少个路由器。

显然数据报在网络上经过的路由器的最大数值是255。

若把TTL

协议占8

位协议字段指出此数据报携带的数据是使用何种协议以便使目的主机的IP

首部检验和占16

位这个字段只检验数据报的首部但不包括数据部分。

这是因为数据报每经过一个路由器路由器都要重新计算一下首部检验和一些字段如生存时间、标志、片偏移等都可能发生变化。

不检验数据部分可减少计算的工作量。

(11)

源地址占32

首部封装在其中因为有数据区域才会有数据报首部的存在在大多数情况下IP

协议或UDP

版本号首部长度服务类型*/PACK_STRUCT_FLD_8(u8_t

_v_hl);/*

生存时间(最大转发次数)协议类型(IGMP:1、UDP:17、TCP:6)

_ttl);/*

源IP地址/目的IP地址*/PACK_STRUCT_FLD_S(ip4_addr_p_t

src);PACK_STRUCT_FLD_S(ip4_addr_p_t

dest);

PACK_STRUCT_END可以看出此结构体的成员变量和上图9.2.1

TCP/IP

协议栈为什么具备分片的概念因为应用程序处理的数据是不确定的可能超出

网络接口最大传输单元为此TCP/IP

行重组处理这样接收方的应用程序接收到这个大型的数据了。

总的来讲IP

假设IP

分片这些分片的数据组合起来就是应用程序发送的数据与传输层的首部。

实现函数为ip4_frag该函数如下所示

将数据报切成MTU大小的块然后按顺序发送通过将pbuf_ref指向p

param

!LWIP_NETIF_TX_SINGLE_PBUFstruct

pbuf

lwip_ntohs(IPH_OFFSET(iphdr));/*

ofo

这个rambuf有效数据指针指向original_iphdr数据报*/SMEMCPY(rambuf

payload,

ip_frag_alloc_pbuf_custom_ref();if

(pcr

newpbuf申请内存1480字节保存了这个数据区域偏移poff字节的数据(p-payload

poff)

释放内存*/ip_frag_free_pbuf_custom_ref(pcr);pbuf_free(rambuf);goto

memerr;}/*

将它添加到rambuf的链的末尾*/pbuf_cat(rambuf,

newpbuf);/*

分段偏移与标志字段*/IPH_OFFSET_SET(iphdr,

lwip_htons(tmp));/*

dest);IPFRAG_STATS_INC(ip_frag.xmit);/*

rambuf释放内存*/pbuf_free(rambuf);/*

left

nfb);}MIB2_STATS_INC(mib2.ipfragoks);return

ERR_OK;memerr:MIB2_STATS_INC(mib2.ipfragfails);return

ERR_MEM;

MIB2_STATS_INC(mib2.ipfragoks);

return

memerr:MIB2_STATS_INC(mib2.ipfragfails);

return

}此函数非常简单首先判断这个大型数据包的有效区域总长度系统根据这个总长度划分

来存储IP

数据包的payload指针指向最后调用netif-output

上图中newpbuf

分组在网络传输过程中到达目的地点的时间是不确定的所以后面的分组可能比

在lwIP

中有专门的结构体负责缓存这些分组这个结构体为ip_reassdata

重装数据链表

还是TCP它们的数据段递交至网络层的接口是一致的这个接口函数如下

err_t

设置版本号设置首部长度*/IPH_VHL_SET(iphdr,

ip_hlen

将当前网络接口IP地址设置为源IP地址*/ip4_addr_copy(iphdr

src,

payload;ip4_addr_copy(dest_addr,

iphdr

dest_addr;}IP_STATS_INC(ip.xmit);ip4_debug_print(p);/*

(netif

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

此函数首先判断目标IP

首部字段信息接着判断该数据包的总长度是否大于以太网传输单元若大于则调用ip4_frag

函数对这个数据包分组并且逐一发送否则直接调用ethrap_output

函数把数据包递交给ARP

IP_ACCEPT_LINK_LAYER_ADDRESSING

LWIP_IGMPint

IP_ACCEPT_LINK_LAYER_ADDRESSING

LWIP_IGMP

*/IP_STATS_INC(ip.recv);MIB2_STATS_INC(mib2.ipinreceives);/*

识别IP报头*/iphdr

{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;}/*

释放空间*/pbuf_free(p);IP_STATS_INC(ip.lenerr);IP_STATS_INC(ip.drop);MIB2_STATS_INC(mib2.ipindiscards);return

ERR_OK;}/*

地址复制到对齐的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

(ip4_addr_ismulticast(ip4_current_dest_addr()))

{#if

(ip4_addr_cmp(ip4_current_dest_addr(),netif_ip4_addr(netif))

||/*

*/ip4_addr_isbroadcast(ip4_current_dest_addr(),

netif)#

(ip4_addr_get_u32(ip4_current_dest_addr())

endif

(ip4_addr_isloopback(ip4_current_dest_addr()))

{netif

IP_ACCEPT_LINK_LAYER_ADDRESSINGif

(netif

(IP_ACCEPT_LINK_LAYER_ADDRESSED_PORT(udphdr

dest))

IP_ACCEPT_LINK_LAYER_ADDRESSING

#if

IP_ACCEPT_LINK_LAYER_ADDRESSINGif

(check_ip_src#if

IP_ACCEPT_LINK_LAYER_ADDRESSING

!ip4_addr_isany_val(

IP_ACCEPT_LINK_LAYER_ADDRESSING

*/)#

IP_ACCEPT_LINK_LAYER_ADDRESSING

{/*

((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;}}/*

(!ip4_addr_isbroadcast(ip4_current_dest_addr(),

inp))

{IP_STATS_INC(ip.drop);MIB2_STATS_INC(mib2.ipinaddrerrors);MIB2_STATS_INC(mib2.ipindiscards);}/*

ERR_OK;}/*

释放空间*/pbuf_free(p);IP_STATS_INC(ip.opterr);IP_STATS_INC(ip.drop);/*

u不受支持的协议特性*/MIB2_STATS_INC(mib2.ipinunknownprotos);return

ERR_OK;}#endif

第九步发送到上层协议*/ip4_debug_print(p);ip_data.current_netif

netif;ip_data.current_input_netif

iphdr;ip_data.current_ip_header_tot_len

IPH_HL(iphdr)

转移到有效载荷数据区域不需要检查*/pbuf_header(p,

-(s16_t)

*/MIB2_STATS_INC(mib2.ipindelivers);/*

inp);break;#endif

IP_PROTO_TCP:MIB2_STATS_INC(mib2.ipindelivers);/*

inp);break;#endif

释放空间*/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;}上述的源码篇幅很长也不容易理解下面笔者把上述的源码分成十步来讲解

第一步判断IP

第十步判断该数据报的协议为TCP/UDP/ICMP/IGMP如果不是这四个协议则丢弃该

ICMP

否可达、路由是否可用等网络本身的消息这些控制消息虽然并不传输到用户数据但是对于用户数据的传递起着重要的作用。

ICMP

协议是一种面向无连接的协议用于传输出错报告控制信息。

它是一个非常重要的协议它对于网络安全具有极其重要的意义。

它属于网络层协议主要用于在主机与路由器之间传递控制信息包括报告错误、交换受限控制和状态信息等。

当遇到IP

数据无法访问目标、IP

路由器无法按当前的传输速率转发数据包等情况时会自动发送ICMP

ICMP

如数据报错信息、网络状况信息和主句状况信息等虽然这些信息不会递交给用户数据但对于用户来说数据报有效性得到提高。

ICMP

协议本身不提供差错报告和差错控制机制来保证数据报递交的有效性如果在路由器

无法递交一个数据报或者数据报生存时间为0

层这样处理是合理的但是对于源主机来说比较希望得到数据报递交过程中出现异常相

协议不能进行主机管理与查询机制简单来说不知道对方主机或者路由器的活跃

对于不活跃的主机和路由器就没有必要发送数据报所以对于主机管理员来说更希望得到对方主机和路由器的信息这样可以根据相关的信息对自身配置、数据报发送控制。

首部ICMP

协议不为任何的应用程序服务它的目的是目的主机的网络层处理软件。

ICMP

是判断路由器和主机对当前的数据报进行正常处理例如无法将数据报递交给上层处理或者数据报因为生存时间而被删除。

②ICMP

查询报文用于一台主机向另一台主机查询特定的信息这个类型的报文是成对出

现的例如源主机发送查询报文当目标主机收到该报文之后它会根据查询报文的约定的格式为源主机放回应答报文。

ICMP

中不同的报文其首部的格式也会有点差异当然也有通用的地方例如首部的前4

报文结构如下图所示

部分长度和含义存在差异例如差错报文会引起差错的据报的信息而查询报文携带查询请求和查询结果数据。

ICMP

当路由器发送的数据报不能发送到指定目的地时或者说当路由器不能够给数据报找到路由或主机不能够交付数据报时就丢弃这个数据报然后向发送数据报的源主机设备发回一个终点不可达数据报文。

如下图所示

举个例子主机A

发生了故障它不知道这个数据报下一步该发给哪个路由设备或者那台主机设备也就是说这个数据报不能发送到目的地主机B这时路由器会把这个数据报丢弃并向主机A

ICMP

目的不可达差错报告报文产生差错的原因有很多如网络不可达、主机不可达、协

种如下图所示

层能够根据端口号将报文传递给对应的上层协议处理差错报文结构如下图所示

首部和引

协议是面向无连接的没有流量控制机制数据在传输过程中是非常容易造成拥

抑制机制并不能控制流量的大小但是能根据流量的使用情况给源主机提供一些建议。

这个报文的作用就是通知数据报在拥塞时被丢弃了另外还会警告源主机流量出现了拥塞的情况然后源主机根据反馈的ICMP

如下图所示

超时报文。

另外当目标主机在规定时间内没有收到所有的数据分片时会把已经收到的所有数据

分片丢弃并向源主机发回一个ICMP

只能给目的主机使用它表示在规定的时间内目的主机没有收到所有的数据分片。

(5)

当数据报在因特网上传送时在其首部中出现的任何二义性或者首部字段值被修改都可能

会产生非常严重的问题。

如果路由器或目的主机发现了这种二义性或在数据报的某个字段中缺少某个值就丢弃这个数据报并回送参数问题报文。

ICMP

回显请求报文和回显应答报文而不用经过传输层来测试目标主机是否可达。

它是一个检查系统连接性的基本诊断工具。

ICMP

回显请求数据包后它期待着目标主机的回答。

目标主机在收到一个ICMP

回显请求数据包后它会交换源、目的主机的地址然后将收到的ICMP

回显应答数据包中然后发回给发送ICMP

回显请求的一方。

如果校验正确发送者便认为目标主机的回显服务正常也即物理连接畅通。

查询报文结构如下图所示

查询报文没有特殊取值

中没有正式定义该值的范围所以发送方可以自由定义这两个字段可以用来记录源主机发送出去的请求报文编号。

数据可选区域标识回送请求报文包含数据和长度是可选的发送放应该选择适合的长度和填充数据。

在接收方它可以根据这个回送请求产生一个回送回答报文回送报文的数据与回送请求报文的数据是相同的。

ICMP

协议本身不提供差错报告和差错控制机制来保证数据报递交的有效性和进行主机管理与查询机制简单来说ICMP

ICMP

ICMP代码号*/PACK_STRUCT_FIELD(u16_t

chksum);

ICMP校验和*/PACK_STRUCT_FIELD(u16_t

id);

ICMP的标识符*/PACK_STRUCT_FIELD(u16_t

seqno);

只实现目的不可到达和超时差错报文它们的实现函数分别为icmp_dest_unreach

cmp_time_exceeded这两个函数转入的参数与icmp_dur_type

和icmp_te_type

枚举相关。

如目的不可到达报文的代码字段由icmp_dur_type

打开icmp.c

{MIB2_STATS_INC(mib2.icmpoutdestunreachs);icmp_send_response(p,

ICMP_DUR,

发送超时报文该函数实际调用函数icmp_send_response来发送ICMP差错报文ICMP_TE

为超时*/

{MIB2_STATS_INC(mib2.icmpouttimeexcds);icmp_send_response(p,

ICMP_TE,

}从上述源码可以看出差错报文的类型已经固定为目的不可到达或者超时它们唯一不同

的是差错报文的代码值这个代码值就是由icmp_dur_type

和icmp_te_type

netif;MIB2_STATS_INC(mib2.icmpoutmsgs);/*

为差错报文申请pbufpbuf预留以太网首部和ip首部申请数据长度为icmp首部长度icmp数据长度(ip首部长度8)

*/q

{MIB2_STATS_INC(mib2.icmpouterrors);return;}/*

指向IP

和UDP则该数据报不会递交给传输层处理若上层协议字段为ICMP

则该数据报递交给icmp_input

src;ICMP_STATS_INC(icmp.recv);MIB2_STATS_INC(mib2.icmpinmsgs);iphdr_in

(hlen

回送应答*/MIB2_STATS_INC(mib2.icmpinechoreps);break;case

ICMP_ECHO:/*

回送*/MIB2_STATS_INC(mib2.icmpinechos);src

(ip4_addr_ismulticast(ip4_current_dest_addr()))

{goto

(ip4_addr_isbroadcast(ip4_current_dest_addr(),ip_current_netif()))

{goto

PBUF_LINK_ENCAPSULATION_HLEN)))

{struct

PBUF_LINK_ENCAPSULATION_HLEN)))

{goto

设置正确的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

{MIB2_STATS_INC(mib2.icmpindestunreachs);}

else

{MIB2_STATS_INC(mib2.icmpintimeexcds);}

else

{MIB2_STATS_INC(mib2.icmpinparmprobs);}

else

{MIB2_STATS_INC(mib2.icmpinsrcquenchs);}

else

{MIB2_STATS_INC(mib2.icmpinredirects);}

else

{MIB2_STATS_INC(mib2.icmpintimestamps);}

else

{MIB2_STATS_INC(mib2.icmpintimestampreps);}

else

{MIB2_STATS_INC(mib2.icmpinaddrmasks);}

else

{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内未收到确认字符

协议在发送数据之前要求系统需要在不可靠的信道上建立可靠连接我们称之为“三次握

手”。

建立连接完成之后客户端与服务器才能互发数据不需要发送数据时可以可以断开连

TCP

服务器进程先创建传输控制块TCB时刻准备接受客户进程的连接请求此时服

②TCP

客户进程也是先创建传输控制块TCB然后向服务器发出连接请求报文这是报

此时TCP

服务器收到请求报文后如果同意连接则发出确认报文。

确认报文中应该

ACK1SYN1确认号是ackx1同时也要为自己初始化一个序列号seqy此时TCP

服务器进程进入了SYN-RCVD同步收到状态。

这个报文也不能携带数据但是同样要消

④TCP

客户进程收到确认后还要向服务器给出确认。

确认报文的ACK1acky1

规定ACK

建立一个连接需要三次握手而终止一个连接需要四次挥手终止连接有以下过程。

(1)

第一次挥手客户端发送释放报文并停止发送数据。

释放数据报文首部FIN1,其

序列号为sequ,此时客户端进入FIN-WAIT1等待服务器应答FIN

(2)

携带自己的序列号seqv。

此时服务器进入CLOSE-WAIT关闭等待状态。

客户端收到服

务端确认请求此时客户端进入FIN-WAIT2终止等待2状态等待服务器发送连接释放

(3)

第三次挥手服务器向客户端发送连接释放报文FIN1、acku1,此时服务器进入

了LAST-ACK最后确认等待客户端的确认。

客户端接收到服务器的连接释放报文后必

须发送确认ack1、ackw1,客户端的序列号为sequ1此时客户端进入TIME-WAIT时

(4)

首部包含建立与断开、数据确认、窗口大小通告、数据发送相关的所有标

志和控制信息TCP

为客户端的应用程序分配端口号。

在服务器端每种服务在”众所周知的端口”Well-Know

(2)

首部长度保留位标志位*/PACK_STRUCT_FIELD(u16_t

wnd);

remote_port;/*附加状态信息如连接是快速恢复、一个被延迟的ACK

flags;#define

当前接收窗口的大小会随着数据的接收与递交动态变化*/tcpwnd_size_t

rcv_ann_wnd;

将向对方通告的窗口大小随着数据的接收与递交动态变化*/u32_t

rcv_ann_right_edge;

TCP_SNDQUEUELEN_OVERFLOW(0xffff

3)u16_t

协议可能会花很多精力和时间这里笔者讲解重要的知识即可。

首先我们先

讲解一下接收数据相关的字段rcv_nxtrcv_wndrcv_ann_wnd

snd_nxtsnd_maxsnd_wndacked这些字段和TCP

协议深度剖析与实战演练》作者朱

③rcv_ann_wnd表示将向对方通告的窗口大小值这个值在报文发送时会被填在首部中

④rcv_ann_right_edge记录了上一次窗口通告时窗口右边界取值该字段在窗口滑动过

个数据而

就是通知对方窗口大小的值而rcv_ann_right_edge

窗口右边界取值14当然下一次发送时这四个变量就不一定是上述图中的值了它们会

随着数据的发送与接收动态改变。

当接收到数据后数据会被放在接收窗口中等待上层调用

rcv_nxt

时内核会计算一个合理的窗口值rcv_ann_wnd并不一定与rcv_wnd

相等在下一次报文

③snd_wnd记录了当前的发送窗口大小它常被设置为接收方通告的接收窗口值。

④snd_lbb记录了下一个将被应用程序缓存的数据的起始编号。

可以看出左边部分是已经发送并确认的数据绿色框是已经发送但未确认的数据需要

等待对方确认红色框可以发送的数据最右边的是不能发送的。

上面这四个字段的值也是

后lastack

段的形式组织的因此可能存在这样的情况即使发送窗口允许但并不是窗口内的所有数据

效的报文段因此不会被发送。

发送方会等到新的确认到来从而使发送窗口向右滑动使得

监听控制块

除了定义结构体tcp_pcb它还定义了结构体tcp_pcb_listen前者我们知道有这个就

行后者结构体tcp_pcb_listen

处于该状态不会进行数据发送、连接握手之类的服务主要是分配完整的TCP

控制块中这样子就能节省不少资

两种控制块都具有的字段*/TCP_PCB_COMMON(struct

tcp_pcb_listen);#if

操作一般对于链表上的控制块进行查找这四个控制块链表在tcp.c

文件中如下源码所示

/*连接所有进行了端口号绑定但是还没有发起连接主动连接或进入侦听状态被动连接的控制块*/

struct

在内核中所有待发送的数据或者已经接收的数据都会以报文的形式保存一般都是保存

在pbuf

用就是把所有报文段连接起来当然这些报文段可以是无发送、已发送并未确认的或者是以收

控制块缓冲区中该结构体如下源码所示

(u8_t)0x10U/*包括SACK允许选项(仅在SYN段中使用)*//*

首部*/struct

};每个控制块中都维护了三个缓冲队列unsent、unacked、ooseq

三个字段这三个字段已

报文段的接收函数是tcp_input该函数位于tcp_inc.c

文件中如下源码所示

(ip_addr_isbroadcast(ip_current_dest_addr(),

ip_current_netif())

||ip_addr_ismulticast(ip_current_dest_addr()))

{/*

若TCP报头在第一个pbuf中*/tcphdr_opt1len

tcphdr_optlen;

TCP报头选项长度*/pbuf_remove_header(p,

hdrlen_bytes);

确定选项的第一部分和第二部分长度*/tcphdr_opt1len

len;opt2len

移除tcphdr_opt1len选项*/pbuf_remove_header(p,

tcphdr_opt1len);/*

记住指向TCP报头选项的第二部分的指针(有部分选项在第二个pbuf中记录TCP报头选项的开始部分)

*/tcphdr_opt2

TCP数据包中数据的总长度对于有FIN或SYN标志的数据包该长度要加1

*/tcplen

****************************省略代码*********************************

*//*

如果pcb在回调中被中止(通过调用tcp_abort())则跳转目标。

*/aborted:tcp_input_pcb

NULL;recv_data

{/*如果在3张链表里都未找到匹配的pcb则调用tcp_rst向源主机发送一个TCP复位数据包*/if

TCP_RST))

{TCP_STATS_INC(tcp.proterr);TCP_STATS_INC(tcp.drop);tcp_rst(NULL,

ackno,

ip_current_dest_addr(),ip_current_src_addr(),

tcphdr

递交传输层的数据报检验例如检验数据报是否正常操作、是否包含数据、该数据报是否为广

播或者多播如果以上检验成立则系统把该数据报掉弃处理并释放pbuf。

下部分主要对

tcp_active_pcbs

传输层与网络层的交互函数为tcp_output它在tcp_output.c

文件中定义如下源码所示

从发送窗口和阻塞窗口取小者得到有效发送窗口拥塞避免会讲解到这个原理*/wnd

LWIP_MIN(pcb

可用数据和窗口允许它发送报文段直到把数据全部发送出去或者填满发送窗口*/while

(seg

如果未确认队列不为空则需要把当前报文按照顺序组织在队列中*/if

tcphdr

如果当前报文的序列号比队列尾部报文的序列号低则从队列首部开始查找合适的位置插入报文段*/struct

tcp_seg

*/output_done:tcp_clear_flags(pcb,

ERR_OK;

}从整体来看此函数首先检测报文是否满足发送要求接着判断控制块的flags

被设置为TF_ACK_NOW

列中无数据发送或者发送窗口不允许发送数据。

如果内核能发送数据则就将ACK

应答捎带

发送出去同时在发送的时候先找到未发送链表然后调用tcp_output_segment()-

ip_output_if()函数进行发送直到把未发送链表的数据完全发送出去或者直到填满发送窗口

并且更新发送窗口相关字段当然也要将这些已发送但是未确认的数据存储在未确认链表中

以防丢失数据进行重发操作放入未确认链表的时候是按序号升序进行排序的。

lwIP

图存在某种联系下面笔者简单的讲解这个函数到底如何连接服务器该函数如下所示

err_t

{/*.....................前面省略大部分代码......................*//*

发送SYN与MSS选项一起发送*/ret

pcb);}TCP_REG_ACTIVE(pcb);MIB2_STATS_INC(mib2.tcpactiveopens);tcp_output(pcb);(3)}return

ret;

}可见上述的(1)表示程序调用函数tcp_enqueue_flags

构建连接请求报文TCP_SYN上

向服务器发送连接请求报文。

下面笔者使用一个示意图来描述上述的内容如下图所示

第一次握手此时

客户端等待服务器的连接应答报文TCP_ACK。

当客户端接收服务器应答报文TCP_ACK

时系统会在tcp_input

需重复讲解了该连接应答报文会在tcp_input–tcp_process

函数下处理注意tcp_input

{/*..................此处省略了很多代码.....................

*/switch

ESTABLISHED;(2)}/*..................此处省略了很多代码.....................

*/}/*..................此处省略了很多代码.....................

}上述的的(1)就是为了判断服务器应答报文的标志位是否包含TCP_ACK

和TCP_SYN如

果该应答报文包含这些标志位则系统执行上述(2)的代码设置TCP

控制块为ESTABLISHED

应答报文给服务器才能实现第三次握手。

上面的函数tcp_process

完成之后返回到tcp_input

{/*..................此处省略了很多代码.....................

*/if

netif);/*..................此处省略了很多代码.....................

因为TCP

发送该应答包这里就完成了三次握手的动作。

下面笔者使用一个示意图

TCP

等待连接。

注意有连接时会调用函数lwip_tcp_server_accept

④接收数据

*tcp_listen_with_backlog(struct

tcp_pcb

{LWIP_ASSERT_CORE_LOCKED();return

tcp_listen_with_backlog_and_err(pcb,

backlog,

*tcp_listen_with_backlog_and_err(struct

tcp_pcb

..............省略代码..............

*/lpcb

..............省略代码..............

服务器控制块从CLOSER

第一次握手流程对于服务器而言它是先接收客户端发来的连接请求包并判断该请求报文的首部标志位是否包含T

CP_SYN这个请求报文的处理是由tcp_input→tcp_listen_input

define

*tcp_listen_with_backlog(struct

tcp_pcb

{LWIP_ASSERT_CORE_LOCKED();return

tcp_listen_with_backlog_and_err(pcb,

backlog,

*tcp_listen_with_backlog_and_err(struct

tcp_pcb

..............省略代码..............

*/lpcb

..............省略代码..............

内核首先判断连接请求报文的首部标志位是否包含TCP_SYN显然这个符合

第一次TCP

上图的红色框框就是服务器接收客户端的连接请求报文之后发送连接应答报文到了这里

报文才能实现TCP

ESTABLISHED至此客户端和服务器可以相互发送数据了。

TCP

客户端和服务器握手流程已

回调函数指针设置为NULL应用层不再接收数据所有数据直接被丢弃协

议层的处理仍按正常流程走认为应用层已经接收到数据tcp_close

FIN

意以下源码的路径tcp_close→tcp_close_shutdown→tcp_close_shutdown_fin

函数下该函数

{tcp_backlog_accepted(pcb);MIB2_STATS_INC(mib2.tcpattemptfails);pcb

state

{MIB2_STATS_INC(mib2.tcpestabresets);/*

*/pcb

{MIB2_STATS_INC(mib2.tcpestabresets);pcb

state

}大家请看上述有注释的代码这些代码是客户端发送关闭连接请求报文过程该包的首部

包含FIN

函数处理当然它接收到的数据可以发送给应用层但是它递交一个空的EOF

数据给应用层应用层知道接收数据已经完成不需要再从协议栈读数据最后系统发送客

户端ACK

ESTABLISHED:tcp_receive(pcb);if

(recv_flags

上图红色框框就是上述源码运行的流程为了理解笔者没有把全部的代码列举出来。

(3)

发送ACK应答对端的FIN报文*/tcp_ack_now(pcb);TCP_RMV(

tcp_active_pcbs,

tcp_timewait_input处理所有数据都丢弃不发送给应用层直接确认当前收到的报文rcv_nxt设置为当前报文的下一个字节*/pcb

state

(客户端、服务器同时调用tcp_close都在FIN_WAIT_1状态收到对方的FIN报文)*/tcp_ack_now(pcb);

*/pcb

队列移除并设置该控制块的状态为TIME_WAIT。

最后把该控制块挂在

tcp_tw_pcbs

控制块的状态为FIN_WAIT_2下面我们使用一个示意图来描述上述的

内容如下图所示

{tcp_backlog_accepted(pcb);MIB2_STATS_INC(mib2.tcpattemptfails);pcb

state

{MIB2_STATS_INC(mib2.tcpestabresets);pcb

state

{MIB2_STATS_INC(mib2.tcpestabresets);/*

设置状态为LAST_ACK

构建ACK报文*/tcp_ack_now(pcb);tcp_pcb_purge(pcb);TCP_RMV_ACTIVE(pcb);/*

设置状态为TIME_WAIT

的网络调试助手配置成服务器。

开发板接收服务器发送的数据在LCD

例程3

连接标记*/lwip_tcp_client_set_remoteip();

先选择IP

g_point_color);lcd_show_string(30,

50,

g_point_color);lcd_show_string(30,

70,

g_point_color);lcd_show_string(30,

90,

g_point_color);lcd_show_string(30,

110,

lwipdev.ip[0],lwipdev.ip[1],lwipdev.ip[2],lwipdev.ip[3]);

服务器IP

lwipdev.remoteip[0],lwipdev.remoteip[1],lwipdev.remoteip[2],lwipdev.remoteip[3]);lcd_show_string(30,

150,

lwipdev.remoteip[1],lwipdev.remoteip[2],

连接到目的地址的指定端口上,当连接成功后回调tcp_client_connected()函数*/tcp_connect(tcppcb,

rmtipaddr,

TCP_CLIENT_PORT,lwip_tcp_client_connected);}

else

{lwip_tcp_client_usersent(tcppcb);

发送数据*/}if

g_point_color);lwip_client_flag

~(1

STATUS:Disconnected,g_point_color);lcd_fill(30,

210,

标记连接断开了*/}lwip_periodic_handle();delay_ms(2);t;if

200)

{lwip_tcp_client_connection_close(tcppcb,

0);

连接到目的地址的指定端口上,当连接成功后回调tcp_client_connected()函数*/tcp_connect(tcppcb,

rmtipaddr,

TCP_CLIENT_PORT,tcp_client_connected);}}t

0;LED0_TOGGLE();}}lwip_tcp_client_connection_close(tcppcb,

0);

地址的函数lwip_tcp_client_set_remoteip如下源码所示

/***

lwip_tcp_client_set_remoteip(void)

{char

key;lcd_clear(BLACK);g_point_color

30,

g_point_color);lcd_show_string(30,

50,

g_point_color);lcd_show_string(30,

70,

g_point_color);lcd_show_string(30,

90,

g_point_color);lcd_show_string(30,

110,

前三个IP保持和DHCP得到的IP一致*/lwipdev.remoteip[0]

lwipdev.ip[0];lwipdev.remoteip[1]

lwipdev.ip[1];lwipdev.remoteip[2]

lwipdev.ip[2];/*

lwipdev.remoteip[0],lwipdev.remoteip[1],lwipdev.remoteip[2]);lcd_show_string(30,

150,

0X80,g_point_color);}}myfree(SRAMIN,

tbuf);

连接建立后的回调函数lwip_tcp_client_connected如下源码所示

/***

初始化LwIP的tcp_recv回调功能*/tcp_recv(tpcb,

lwip_tcp_client_recv);tcp_err(tpcb,

初始化tcp_err()回调函数*//*

初始化LwIP的tcp_sent回调功能*/tcp_sent(tpcb,

初始化LwIP的tcp_poll回调功能*/tcp_poll(tpcb,

1);tcp_client_flag

{lwip_tcp_client_connection_close(tpcb,

es);

{lwip_tcp_client_connection_close(tpcb,

0);

态有不同的处理这里最重要的就是当处于连接状态并且接收到数据时的处理这个时候我们

链表将链表中的所有数据拷贝到lwip_tcp_client_recvbuf

中这个

的接收处理过程相似。

数据接收成功以后我们将lwip_client_flag

置1表

数据接收缓冲区清零*/memset(lwip_client_recvbuf,

NULL;

判断要拷贝到TCP_CLIENT_RX_BUFSIZE中的数据是否大于TCP_CLIENT_RX_BUFSIZE的剩余空间如果大于*//*

的话就只拷贝TCP_CLIENT_RX_BUFSIZE中剩余长度的数据否则的话就拷贝所有的数据*/if

len

标记接收到数据了*//*用于获取接收数据,通知LWIP可以获取更多数据*/tcp_recved(tpcb,

释放内存*/ret_err

用于获取接收数据,通知LWIP可以获取更多数据*/tcp_recved(tpcb,

tot_len);es

调用这里我们没有实现这个函数用户可以根据自己的实际情况来实现这个函数。

lwip_tcp_client_poll

在这个函数中我们可以将要发送的数据发送出去。

通过lwip_client_flag

数据要发送因为lwIP

话就将发送缓冲区lwip_tcp_client_sendbuf

链表中这个我们使用

来实现这个过程然后我们调用lwip_tcp_client_senddata

函数将数据发送出去发送

{lwip_tcp_client_connection_close(tpcb,

es);

tcp_sent的回调函数(当从远端主机接收到ACK信号后发送数据)*

param

lwip_tcp_client_senddata(struct

tcp_pcb

}lwip_tcp_client_connection_close

函数来关闭与服务器的连接然后注销掉控制块中的回调函数将lwip_client_flag

标记连接断开lwip_tcp_client_connection_close

/***

lwip_tcp_client_connection_close(struct

tcp_pcb

g_point_color);lcd_show_string(30,

50,

g_point_color);lcd_show_string(30,

70,

g_point_color);lcd_show_string(30,

90,

g_point_color);lcd_show_string(30,

190,

代码编译成功之后下载代码到开发板中。

打开网络调试助手软件设置为如下图的信息。

开发板上电等待出现12.3.3.2

我们通过网络调试助手向开发板发送http://www.openedv.com此时开发板LCD

上显示

器(网络调试助手)网络调试助手给开发板发送数据开发板接收数据并通过串口将接收到的

数据发送到串口调试助手上也可以通过按键从开发板向网络调试助手发送数据。

NETCONN

③设置接收超时时间tcp_clientconn-recv_timeout。

④调用函数netconn_getaddr

中我们实现了一个函数lwip_demo同上一章一样都有操作系统

void

netconn_connect(tcp_clientconn,

server_ipaddr,

{printf(接连失败\r\n);/*返回值不等于ERR_OK,删除tcp_clientconn连接*/netconn_delete(tcp_clientconn);}

else

10;/*获取本地IP主机IP地址和端口号*/netconn_getaddr(tcp_clientconn,

loca_ipaddr,

1);printf(连接上服务器%d.%d.%d.%d,本机端口号为:%d\r\n,DEST_IP_ADDR0,DEST_IP_ADDR1,DEST_IP_ADDR2,DEST_IP_ADDR3,

loca_port);while

tcp_client_sendbuf,strlen((char

tcp_client_sendbuf),

{printf(发送失败\r\n);}tcp_client_flag

((recv_err

/*进入临界区*//*数据接收缓冲区清零*/memset(lwip_demo_recvbuf,

recvbuf

/*超出TCP客户端接收数组,跳出*/}}taskEXIT_CRITICAL();

/*退出临界区*/data_len

/*复制完成后data_len要清零*/printf(%s\r\n,

lwip_demo_recvbuf);netbuf_delete(recvbuf);}

else

{netconn_close(tcp_clientconn);netconn_delete(tcp_clientconn);printf(服务器%d.%d.%d.%d断开连接\r\n,

DEST_IP_ADDR2,

实验非常相似它们唯一不同的是连接步骤以及发送函数不同注意上述函数做了一个判断服务器与客户端的连接状态如果这个连接状态是断

连接服务器直到连接成功才进入第二个

我们通过网络调试助手发送数据到开发板当中结果如图17.2.3.3

图17.2.3.3

{sys_thread_new(lwip_send_thread,

lwip_send_thread,

函数创建发送数据线程它的线程函数为lwip_send_thread

/***

表示IPv4网络协议*/atk_client_addr.sin_port

端口号*/atk_client_addr.sin_addr.s_addr

inet_addr(IP_ADDR);

0,sizeof(atk_client_addr.sin_zero));tbuf

mymalloc(SRAMIN,

-1;closeSocket(sock);myfree(SRAMIN,

sock_start;}printf(连接成功\r\n);lwip_connect_state

1;while

lwip_demo_recvbuf,LWIP_DEMO_RX_BUFSIZE,

0);if

{printf(队列Key_Queue已满数据发送失败!\r\n);}vTaskDelay(10);}}

}根据21.1

~LWIP_SEND_DATA;}vTaskDelay(10);}closeSocket(sock);}

态则程序调用write

Transport消息队列遥测传输协议是一种基于发布/订阅Publish/Subscribe模式的轻量级通讯协议该协议构建于TCP/IP

协议上由IBM

最大的优点在于可以以极少的代码和有限的带宽为远程设备提供实时可靠的消息服务。

做为一种低开销、低带宽占用的即时通讯协议MQTT在物联网、小型设备、移动应用等方面有广泛的应用MQTT

(2)

开放和易于实现的这些特点使它适用范围非常广泛。

在很多情况下包括受限境中如机器与机器M2M通信和物联网IoT。

其在通过卫星链路通信传感器、医疗设备、智能家居、及一些小型化设备中已广泛使用。

(3)

代理Broker服务器、订阅者Subscribe。

其中消息的发布者和订阅者都是客户端消息代理是服务器消息发布者可以同时是订阅者如下图所示。

MQTT

传输的消息分为主题Topic和消息的内容payload两部分。

Topic可以理解为消息的类型订阅者订阅Subscribe后就会收到该主题的消息内

Payload可以理解为消息的内容是指订阅者具体要使用的内容。

MQTT

代理服务一直是处于指定端口的监听状态当监听到有客户端要接入的时候就会立刻去处理。

客户端在发起连接请求时携带客户端ID、账号、密码无账号密码使用除外正式项目不会允许这样、心跳间隔时间等数据。

代理服务收到后检查自己的连接权限配置中是否允许该账号密码连接如果允许则建立会话标识并保存绑定客户端ID

与会话并记录心跳间隔时间判断是否掉线和启动遗嘱时用和遗嘱消息等然后回发连接成功确认消息给客户端客户端收到连接成功的确认消息后进入下一步通常是开始订阅主题如果不需要订阅则跳过。

如下图所示

报文发送给代理服务代理服务则将这个主

下以后有这个主题发布就会发送给该客户端然后回复确认消息SUBACK

报文客户端接到SUBACK

报文后知道已经订阅成功则处于等待监听代理服务推送的消息也可以继续订阅其他主题或发布主题如下图所示

当某一客户端发布一个主题到代理服务后代理服务先回复该客户端收到主题的确认消

息该客户端收到确认后就可以继续自己的逻辑了。

但这时主题消息还没有发给订阅了这个主题的客户端代理要根据质量级别QoS来决定怎样处理这个主题。

所以这里充分体现了是MQTT

的客户端是否在线在线则转发一次收到与否不再做任何处理。

这种质量对系统压力最小。

有成功收到才可以否则会尝试补充发送具体机制后面讨论。

这也可能会出现同一主题多次重复发送的情况。

这种质量对系统压力较大。

有成功收到并只收到一次不会重复发送具体机制后面讨论。

这种质量对系统压力最大。

移植MQTT

添加到工程当中这里我们在工程中添加一个名为Middlewares/lwip/src/apps

分组该分组用来添加lwIP

第一步注册阿里云平台打开产品分类/物联网Iot/物联网应用开发如下图所示。

第二步在物联网应用开发页面下点击项目管理/新建项目/新建空白项目在此界面下填

写项目名称等相关信息如下图所示

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

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

注上图中的节点类型、连网方式、数据格式以及认证模式的选择其他产品参数根据用

第三步创建产品之后点击图26.1.3.3

第六步打开“产品/查看/功能定义”路径在该路径下添加功能定义如下图所示。

第七步打开自定义功能并发布上线这里我们添加了两个CurrentTemperature

RelativeHumidity

*/lwip_ali_get_password(DEVICE_SECRET,

CONTENT,

设置客户端的信息量*/mqtt_client_info.client_id

(char

设备名称*/mqtt_client_info.client_user

(char

计算出来的密码*/mqtt_client_info.keep_alive

100;

保活时间*/mqtt_client_info.will_msg

NULL;mqtt_client_info.will_retain

0;myfree(SRAMIN,

连接服务器*/mqtt_client_connect(mqtt_client,

服务器控制块*/

服务器IP与端口号*/mqtt_connection_cb,/*

设置服务器连接回调函数*/LWIP_CONST_CAST(void

mqtt_client_info),

payload_out,{\params\:{\CurrentTemperature\:

0.1

\RelativeHumidity\:%0.1f},\method\:\thing.event.property.post\},

temp,

payload_out);mqtt_publish(mqtt_client,

DEVICE_PUBLISH,

服务器控制块接着我们调用mqtt_client_connect

函数连接阿里云服务器并添加mqtt_connection_cb

连接回调函数最后在while()语句中判断是否订阅操作成功如果系统订阅成功则构建MQTT

消息并调用mqtt_publish

mqtt_connection_cb(mqtt_client_t

client,

arg;LWIP_UNUSED_ARG(client);printf(\r\nMQTT

client

(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_client_is_connected

判断是否已经连接服务器如果连接成功则程序调用函数mqtt_set_inpub_callback

回调函数这些回调函数需要根据客户端以及服务器的发布操作才能进去该回调函数最后我们调用函数mqtt_subscribe

下载验证

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

基于MQTT

第二步在上图中点击“立刻使用”选项页面跳转完成之后点击“添加产品”选项此

时该页面会弹出产品信息小界面这里我们根据自己的项目填写相关的信息如下图所示

上图中我们重点添加的选项有联网方式和设备接入协议这里笔者选择移动蜂窝网络以

及MQTT

本实验会用到上述的产品信息例如产品ID366007、“access_key”产品密钥以及产品

第四步在上图创建的设备中点击右边的详情标签进入标签的链接页面在这个页面下

本实验会用到上图中的设备ID617747917、设备名称MQTT

下面我们打开OneNET

物联网套件采用安全鉴权策略进行访问认证即通过核心密钥计算的token

行访问认证简单来讲用户想连接OneNET

是根据我们前面创建的产品和设备相关的信息计算得来的密钥的计算方法可以使用OneNET

生成工具计算该软件可在这个网址下载https://open.iot.10086.cn/doc/v5/develo

下面笔者简单讲解一下token

res输入格式为“products/{pid}/devices/{device_name}”这个输入格式中的“pid”就是

我们MQTT

产品ID而“device_name”就是设备的名称。

根据前面创建的产品和设备来填写

res

et访问过期时间expirationTimeunix时间这里笔者选择参考文档中的数值

1672735919如下图所示

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

这个核心密钥会在MQTT

导致每次创建一个设备都必须根据这个设备信息再一次计算核心密钥才能连接这种方式会大

大地降低我们的开发效率为了解决这个问题笔者使用另一个方法那就是使用代码的方式

计算核心密钥它和上一章节中的方式不一样因为阿里云和OneNET

算MQTT

平台的核心密钥这些文件在oneos2.0\components\cloud\onenet\m

路径下大家先下载OneOS

打开工程并在Middlewares/lwip/lwip_app

这些文件都在oneos2.0\components\cloud\onenet\mqtt-kit\authorization

基于OneNET

手册该手册地址为https://open.iot.10086.cn/doc/v5/develop/detail/251这个地址里面已经说明

下载验证

客户端用于实现平台与应用服务器之间的单向数据通信。

平台作为客户端通过

HTTP/HTTPS

请求方式将项目下应用数据、设备数据推送给用户指定服务器。

本章主要介

绍lwIP

接入方式可参考该官方的文档手册该文档手册地址为https://op

en.iot.10086.cn/本实验主要参考官方文档的多协议接入/HTTP/上传数据点的内容。

OneNTE

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

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

和APIKey

Host:api.heclouds.com\r\n);strcat(pkt,

lenBuf);strcat(pkt,

如果我们使用网络调试助手接收该数据包那么我们发现该数据与OneNET

平台HTTP

/devices/655766336/datapoints?type5

HTTP/1.1

api-key:rw2p2FqVW4fhhhkj4CwpVcqJq8

Content-Length:13

/devices/655766336/datapoints?type5

HTTP/1.1

api-key:rw2p2FqVW4fhhhkj4CwpVcqJq8

Content-Length:16

TCP_DEMO_PORT;netconn_gethostbyname(DEST_MANE,

server_ipaddr);while

netconn_connect(tcp_clientconn,

server_ipaddr,

返回值不等于ERR_OK,删除tcp_clientconn连接*/netconn_delete(tcp_clientconn);}

else

获取本地IP主机IP地址和端口号*/netconn_getaddr(tcp_clientconn,

loca_ipaddr,

发送tcp_server_sentbuf中的数据*/netconn_write(tcp_clientconn,

buffer,

发送tcp_server_sentbuf中的数据*/netconn_write(tcp_clientconn,

buffer,

NETCONN_COPY);vTaskDelay(1000);/*

接收到数据*/if

数据接收缓冲区清零*/memset(tcp_client_recvbuf,

TCP_CLIENT_RX_BUFSIZE);/*遍历完整个pbuf链表*/for

recvbuf

超出TCP客户端接收数组,跳出*/}}taskEXIT_CRITICAL();

退出临界区*/data_len

复制完成后data_len要清零*/printf(%s\r\n,

tcp_client_recvbuf);netbuf_delete(recvbuf);}

else

{netconn_close(tcp_clientconn);netconn_delete(tcp_clientconn);goto

atk_start;}}}}



SEO优化服务概述

作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。

百度官方合作伙伴 白帽SEO技术 数据驱动优化 效果长期稳定

SEO优化核心服务

网站技术SEO

  • 网站结构优化 - 提升网站爬虫可访问性
  • 页面速度优化 - 缩短加载时间,提高用户体验
  • 移动端适配 - 确保移动设备友好性
  • HTTPS安全协议 - 提升网站安全性与信任度
  • 结构化数据标记 - 增强搜索结果显示效果

内容优化服务

  • 关键词研究与布局 - 精准定位目标关键词
  • 高质量内容创作 - 原创、专业、有价值的内容
  • Meta标签优化 - 提升点击率和相关性
  • 内容更新策略 - 保持网站内容新鲜度
  • 多媒体内容优化 - 图片、视频SEO优化

外链建设策略

  • 高质量外链获取 - 权威网站链接建设
  • 品牌提及监控 - 追踪品牌在线曝光
  • 行业目录提交 - 提升网站基础权威
  • 社交媒体整合 - 增强内容传播力
  • 链接质量分析 - 避免低质量链接风险

SEO服务方案对比

服务项目 基础套餐 标准套餐 高级定制
关键词优化数量 10-20个核心词 30-50个核心词+长尾词 80-150个全方位覆盖
内容优化 基础页面优化 全站内容优化+每月5篇原创 个性化内容策略+每月15篇原创
技术SEO 基本技术检查 全面技术优化+移动适配 深度技术重构+性能优化
外链建设 每月5-10条 每月20-30条高质量外链 每月50+条多渠道外链
数据报告 月度基础报告 双周详细报告+分析 每周深度报告+策略调整
效果保障 3-6个月见效 2-4个月见效 1-3个月快速见效

SEO优化实施流程

我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:

1

网站诊断分析

全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。

2

关键词策略制定

基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。

3

技术优化实施

解决网站技术问题,优化网站结构,提升页面速度和移动端体验。

4

内容优化建设

创作高质量原创内容,优化现有页面,建立内容更新机制。

5

外链建设推广

获取高质量外部链接,建立品牌在线影响力,提升网站权威度。

6

数据监控调整

持续监控排名、流量和转化数据,根据效果调整优化策略。

SEO优化常见问题

SEO优化一般需要多长时间才能看到效果?
SEO是一个渐进的过程,通常需要3-6个月才能看到明显效果。具体时间取决于网站现状、竞争程度和优化强度。我们的标准套餐一般在2-4个月内开始显现效果,高级定制方案可能在1-3个月内就能看到初步成果。
你们使用白帽SEO技术还是黑帽技术?
我们始终坚持使用白帽SEO技术,遵循搜索引擎的官方指南。我们的优化策略注重长期效果和可持续性,绝不使用任何可能导致网站被惩罚的违规手段。作为百度官方合作伙伴,我们承诺提供安全、合规的SEO服务。
SEO优化后效果能持续多久?
通过我们的白帽SEO策略获得的排名和流量具有长期稳定性。一旦网站达到理想排名,只需适当的维护和更新,效果可以持续数年。我们提供优化后维护服务,确保您的网站长期保持竞争优势。
你们提供SEO优化效果保障吗?
我们提供基于数据的SEO效果承诺。根据服务套餐不同,我们承诺在约定时间内将核心关键词优化到指定排名位置,或实现约定的自然流量增长目标。所有承诺都会在服务合同中明确约定,并提供详细的KPI衡量标准。

SEO优化效果数据

基于我们服务的客户数据统计,平均优化效果如下:

+85%
自然搜索流量提升
+120%
关键词排名数量
+60%
网站转化率提升
3-6月
平均见效周期

行业案例 - 制造业

  • 优化前:日均自然流量120,核心词无排名
  • 优化6个月后:日均自然流量950,15个核心词首页排名
  • 效果提升:流量增长692%,询盘量增加320%

行业案例 - 电商

  • 优化前:月均自然订单50单,转化率1.2%
  • 优化4个月后:月均自然订单210单,转化率2.8%
  • 效果提升:订单增长320%,转化率提升133%

行业案例 - 教育

  • 优化前:月均咨询量35个,主要依赖付费广告
  • 优化5个月后:月均咨询量180个,自然流量占比65%
  • 效果提升:咨询量增长414%,营销成本降低57%

为什么选择我们的SEO服务

专业团队

  • 10年以上SEO经验专家带队
  • 百度、Google认证工程师
  • 内容创作、技术开发、数据分析多领域团队
  • 持续培训保持技术领先

数据驱动

  • 自主研发SEO分析工具
  • 实时排名监控系统
  • 竞争对手深度分析
  • 效果可视化报告

透明合作

  • 清晰的服务内容和价格
  • 定期进展汇报和沟通
  • 效果数据实时可查
  • 灵活的合同条款

我们的SEO服务理念

我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。

提交需求或反馈

Demand feedback