经过上一篇的手把手教你手撸通讯协议(一) S7协议解析 中,大家有没有发现缺了很大一部分篇幅,而且也只讲到了UDP的包头;由于UDP是让大家简单的看到以太网的工作方式,接下去我们通过开源的LwIP协议栈来好好了解以太网的真实工作方式,我将会在这一期结束的时候,给大家实现一个基于STM32的modbusTCP 主站的小demo。 第一节:协议层简介 首先我们还是根据标准的TCP/IP协议栈来分析传输层和链路层的网络是怎样打包的; 首先我们先了解几个基础协议及网络分层: 网络接口层:定义数据帧(对电信号0/1进行的特定分组)、确认主机的物理地址(MAC地址),通过传输介质在网络上传输数据帧。网络接口有不同的实现方式,比如可以通过有线或无线的方式收发数据帧,不同的实现方式意味着不同的帧结构、传输速率等。 网络层:定义网络地址(IP地址)、区分网段、对于子网内的数据包进行MAC寻址、对于不同子网的数据包进行路由,实现网络中主机到主机的通信。主要有PPP协议、SLIP协议、ARP、ipv4等基础协议。 传输层:定义端口(Port)、标识应用程序身份、实现端口到端口的通信,TCP协议可以保证数据传输的可靠性。 应用层:定义数据格式并按照对应的格式解读数据(下层传送过来的是字节流,不能很好的被程序识别)。应用层定义了各种各样的协议来规范数据格式,常见的有 HTTP、FTP、SMTP 等。 第二节:数据包及内存存储结构 由上面的基础知识、我们根据理论知识,我们根据LwIP来进行学习。其实其实网络层级来说:TCP和UDP类似,但TCP需要实现可靠连接,网卡接收的数据包,有可能是成千上万字节,也有可能是几个字节,所以我们需要对其数据进行打包处理。 由于内存分配问题可以谈的很深、涉及到编译原理、字节对齐这些,篇幅有限,不展开。反正主要是两种方式、一种是链表、一种是内存池方式,各种系统中也都会讲到,我们主要从数据包开始说明: struct pbuf { struct pbuf *next; void *payload; u16_t tot_len; u16_t len; u8_t type; u8_t flags; u16_t ref;};
这两个看上去是不是很熟悉,就是一个链表节点。分配完成后就是这样: 组成链表后的形式大概是这样的: 这个就是数据包在内存中存储的方式了。 第三节:网络接口 在 LWIP 中,是通过一个叫做 netif 的网络结构体来描述一个硬件网络接口的。 struct netif { struct netif *next; // 指向下一个 netif 结构的指针 struct ip_addr ip_addr; // IP 地址相关配置 struct ip_addr netmask; struct ip_addr gw; err_t (* input)(struct pbuf *p, struct netif *inp); //调用这个函数可以从网卡上取得一个 // 数据包 err_t (* output)(struct netif *netif, struct pbuf *p, // IP 层调用这个函数可以向网卡发送 struct ip_addr *ipaddr); // 一个数据包 err_t (* linkoutput)(struct netif *netif, struct pbuf *p); // ARP 模块调用这个函数向网 // 卡发送一个数据包 void *state; // 用户可以独立发挥该指针,用于指向用户关心的网卡信息 u8_t hwaddr_len; // 硬件地址长度,对于以太网就是 MAC 地址长度,为 6 各字节 u8_t hwaddr[NETIF_MAX_HWADDR_LEN]; //MAC 地址 u16_t mtu; // 一次可以传送的最大字节数,对于以太网一般设为 1500 u8_t flags; // 网卡状态信息标志位 char name[2]; // 网络接口使用的设备驱动类型的种类 u8_t num; // 用来标示使用同种驱动类型的不同网络接口 }; 举个例子来实现一张网卡的初始化:
这里我们完成了初始化一张名为enc28j60 的网卡。 接下去是网卡的接收和发送主要通过low_level_input 和 low_level_output这两个函数来实现。然后在操作系统中直接调用这两个函数就行了。 以UC/OSII的网卡数据接收为例: 第一步创建线程: OSTaskCreate(ethernetif_input,(void *)&enc28j60, &T_ETHERNETIF_INPUT_STK[T_ETHERNETIF_INPUT_STKSIZE-1] ETH_IF_TASK_PRIO); 第二步:数据包接收
至此,数据包的接收可算大功告成 。 第四节:网络层 接下去我们要进行网络层协议的讲解了: (1) ARP:全称 Address Resolution Protocol,译作地址解析协议,是位于TCP/IP协议栈底层的协议。 ARP的协议包格式: struct etharp_hdr { PACK_STRUCT_FIELD(struct eth_hdr ethhdr); // 14 字节的以太网数据报头 PACK_STRUCT_FIELD(u16_t hwtype); // 2 字节的硬件类型 PACK_STRUCT_FIELD(u16_t proto); // 2 字节的协议类型 PACK_STRUCT_FIELD(u16_t _hwlen_protolen); // 两个 1 字节的长度字段 PACK_STRUCT_FIELD(u16_t opcode); // 2 字节的操作字段 op PACK_STRUCT_FIELD(struct eth_addr shwaddr); // 6 字节源 MAC 地址 PACK_STRUCT_FIELD(struct ip_addr2 sipaddr); // 4 字节源 IP 地址 PACK_STRUCT_FIELD(struct eth_addr dhwaddr); // 6 字节目的 MAC 地址 PACK_STRUCT_FIELD(struct ip_addr2 dipaddr); // 4 字节目的 IP 地址 } PACK_STRUCT_STRUCT; 我们可以把他转化成这个结构体。 接下去这个图我们可以看到ARP的工作流程: 说白了就是两个功能:通过ARP协议实现IP地址和MAC地址的映射,或者广播获取目标MAC地址。 嘿嘿嘿,这里有人知道为啥拨号叫PPPOE么?或者说校园网、闪讯这些校园网络是怎么进行独立收费的么。理解了这一层的协议,可以做很多事情噢。 (2)IP协议 IP层其实在上一章节也有讲到过。 最重要的是在网络标识那一标识位置确认了使用了什么协议。8位协议字段用来描述该IP数据包是来自于上层的哪个协议,如该值为1表示为ICMP协议,该值为2表示IGMP协议,该值为6表示TCP协议,该值为17表UDP协议。 前面我说们说到TCP包需要分包,接下去一个图可以很清晰的解释LwIP是怎么进行分包的: 这一层的东西太多了,不展开。IP层的讲解主要了解这些就够了。 再说个大家感兴趣东西:ping和tracert ,其实在连接过程中,就是用ICMP协议实现的,主要用来测试路径和时间。其实第一次接触hack也是从ICMP攻击开始的。中美黑客大战,我也是拿了脚本,贡献了一份力(ORZ)。 第五节:传输层 接下去我们说说传输层:这一层的东西很多很多,偷懒了。 这里先补充一点:在PLC还没分配IP地址时,我们是怎么找到设备并分配IP的?没有IP地址是怎么发现PLC或模块的地址的? 先以Rockwell的EtherNet/IP举个例子,由于Rockwell的CIP协议大多数功能都是基于标准以太网协议实现,所以可以很贴合现在这个系列。AB模块可以通过一个叫BOOTP的工具进行模块的发现和IP地址分配,很好理解,AB的PLC是使用BOOTP协议进行PLC或模块的发现的。为什么我们现在挺少听说BOOTP了呢?因为现在大家都在用DHCP的方式了。Bootp其实是基于UDP协议进行设备发现的。其实我们上下位机的通讯基本靠TCP协议,而下位机之间的通讯基本是基于UDP进行通讯,UDP协议的本身协议特点可以实现模块之间的高速通讯,更适合用于现场网络,本系列主要以与上位机通讯为主,所以减少UDP这一块的解释。 西门子的协议对这一层的协议进行了一些修改,以下图为例(来自西门子官网)。这个下次再聊。 下级预告:本系列知识点的重点了:TCP的建立和断开 TCP的全称大家自行百度:主要功能是为上层提供一个可靠连接(虽然容易出线粘包问题)。 这里先给大家看一张图(别的地方截取过来的):这是TCP连接的状态转化。 对这一期就先到这边,TCP的内容留在下一期。 结尾 总结一下 Summary 1、LwIP协议栈主要用于嵌入式系统的以太网协开发。该协议栈为很轻量级的以太网协议栈,通过该协议栈的学习,可以很好的理解以太网是怎么工作的,采用该协议栈,我在很多项目中实现了MQTT、S7协议、ModbusTCP协议等工业协议的开发,还有一些私有协议的开发,很好的用于网络中间件的开发。 2、讲解了物理接口层、链路层、网络层、传输层的部分协议实现和打包方式。讲的比较简单,也是给大家一个可以参考的方向。 留两个问题 问题1:IP数据包失序后怎么处理? 问题2:TCP发生粘包问题如何处理(或者说S7协议、CIP协议等是怎么处理粘包问题)? 2022年2月 |
|
来自: 新用户0935snDB > 《待分类》