手把手教你手撸通讯协议(二)-网络的基础

2022-03-11 17:42:22 浏览数 (1)

经过上一篇的手把手教你手撸通讯协议(一)

第一节:协议层简介

首先我们还是根据标准的TCP/IP协议栈来分析传输层和链路层的网络是怎样打包的;

首先我们先了解几个基础协议及网络分层:

网络接口层:定义数据帧(对电信号0/1进行的特定分组)、确认主机的物理地址(MAC地址),通过传输介质在网络上传输数据帧。网络接口有不同的实现方式,比如可以通过有线或无线的方式收发数据帧,不同的实现方式意味着不同的帧结构、传输速率等。

网络层:定义网络地址(IP地址)、区分网段、对于子网内的数据包进行MAC寻址、对于不同子网的数据包进行路由,实现网络中主机到主机的通信。主要有PPP协议、SLIP协议、ARP、ipv4等基础协议。

传输层:定义端口(Port)、标识应用程序身份、实现端口到端口的通信,TCP协议可以保证数据传输的可靠性。

应用层:定义数据格式并按照对应的格式解读数据(下层传送过来的是字节流,不能很好的被程序识别)。应用层定义了各种各样的协议来规范数据格式,常见的有 HTTP、FTP、SMTP 等。

第二节:数据包及内存存储结构

由上面的基础知识、我们根据理论知识,我们根据LwIP来进行学习。其实其实网络层级来说:TCP和UDP类似,但TCP需要实现可靠连接,网卡接收的数据包,有可能是成千上万字节,也有可能是几个字节,所以我们需要对其数据进行打包处理。

由于内存分配问题可以谈的很深、涉及到编译原理、字节对齐这些,篇幅有限,不展开。反正主要是两种方式、一种是链表、一种是内存池方式,各种系统中也都会讲到,我们主要从数据包开始说明:

代码语言:javascript复制
struct pbuf {
  struct pbuf *next;
  void *payload;
  u16_t tot_len;
  u16_t len;
  u8_t  type;
  u8_t flags;
  u16_t ref;
};
代码语言:javascript复制
typedef enum {
  PBUF_RAM, /* pbuf data is stored in RAM */
  PBUF_ROM, /* pbuf data is stored in ROM */
  PBUF_REF, /* pbuf comes from the pbuf pool */
  PBUF_POOL /* pbuf payload refers to RAM */
} pbuf_type;

这两个看上去是不是很熟悉,就是一个链表节点。分配完成后就是这样:

组成链表后的形式大概是这样的:

这个就是数据包在内存中存储的方式了。

第三节:网络接口

在 LWIP 中,是通过一个叫做 netif 的网络结构体来描述一个硬件网络接口的。

代码语言:javascript复制
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; // 用来标示使用同种驱动类型的不同网络接口 
}; 

举个例子来实现一张网卡的初始化:

代码语言:javascript复制
static struct netif enc28j60;//声明了一个 netif 结构的变量 enc28j60 
struct ip_addr ipaddr, netmask, gw; //声明了三个分别用于暂存 IP 地址、子网掩码和网关地址的变量
IP4_ADDR(&gw, 192,168,0,1); 
IP4_ADDR(&ipaddr, 192,168,0,60); 
IP4_ADDR(&netmask, 255,255,255,0); 
netif_init(); 
netif_add(&enc28j60, &ipaddr, &netmask, &gw, NULL, ethernetif_init, tcpip_input); 
netif_set_default(&enc28j60); 
netif_set_up(&enc28j60); 

err_t ethernetif_init(struct netif *netif) 
{ 
netif->name[0] = IFNAME0; //初始化变量 enc28j60 的 name 字段 
netif->name[1] = IFNAME1; // IFNAME 在文件外定义的,这里不必关心它的具体值  
netif->output = etharp_output; //IP 层发送数据包函数 
netif->linkoutput = low_level_output; // //ARP 模块发送数据包函数 
low_level_init(netif); //底层硬件初始化函数 
return ERR_OK; 
} 
static void low_level_init(struct netif *netif) 
{ 
netif->hwaddr_len = ETHARP_HWADDR_LEN; //设置变量 enc28j60 的 hwaddr_len 字段 
netif->hwaddr[0] = 'F'; //初始化变量 enc28j60 的 MAC 地址 
netif->hwaddr[1] = 'O'; //设什么地址用户自由发挥吧,但是不要与其他 
netif->hwaddr[2] = 'R'; //网络设备的 MAC 地址重复。 
netif->hwaddr[3] = 'E'; 
netif->hwaddr[4] = 'S'; 
netif->hwaddr[5] = 'T'; 
netif->mtu = 1500; //最大允许传输单元 
//允许该网卡广播和 ARP 功能,并且该网卡允许有硬件链路连接 
netif->flags = NETIF_FLAG_BROADCAST |  
NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP; 
enc28j60_init(netif->hwaddr); //与底层驱动硬件驱动程序密切相关的硬件初始化函数 
} 

这里我们完成了初始化一张名为enc28j60 的网卡。

接下去是网卡的接收和发送主要通过low_level_input 和 low_level_output这两个函数来实现。然后在操作系统中直接调用这两个函数就行了。

以UC/OSII的网卡数据接收为例:

第一步创建线程:

代码语言:javascript复制
OSTaskCreate(ethernetif_input,(void *)&enc28j60, 
&T_ETHERNETIF_INPUT_STK[T_ETHERNETIF_INPUT_STKSIZE-1] 
ETH_IF_TASK_PRIO);  

第二步:数据包接收

代码语言:javascript复制
void ethernetif_input(void *arg) //创建该进程时,要将某个网络接口结构的 netif 结构指 
{ //针作为参数传入 
struct eth_hdr *ethhdr; 
struct pbuf *p; 
struct netif *netif = (struct netif *)arg; 
while (1) 
{ 
p = low_level_input (netif); // 接收一个数据包 
if (p == NULL) // 如果数据包为空, 
continue; // 则循环结束,启动下次接收过程 
ethhdr = p->payload; // 取得数据包内数据 
switch (htons(ethhdr->type)) // 判断数据包类型 
{ // 只对 IP 数据包和 ARP 数据包进行处理 
case ETHTYPE_IP: // IP 数据包 
case ETHTYPE_ARP: // ARP 数据包 
if (netif->input(p, netif)!=ERR_OK) // 将数据包发送到上层应用函数 
{ 
pbuf_free(p); 
p = NULL; 
} 
break; 
default: 
pbuf_free(p); 
p = NULL; 
break; 
} //switch 
} //while 
} //main 函数 

至此,数据包的接收可算大功告成 。

第四节:网络层

接下去我们要进行网络层协议的讲解了:

(1) ARP:全称 Address Resolution Protocol,译作地址解析协议,是位于TCP/IP协议栈底层的协议。

ARP的协议包格式:

代码语言:javascript复制
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月

作者简介

Borje Zhou:

一个机械设计起身的自动化工程师,励志为自动化行业贴砖加瓦。

0 人点赞