第一节:协议层简介
首先我们还是根据标准的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:
一个机械设计起身的自动化工程师,励志为自动化行业贴砖加瓦。