如何自定义一个通信协议

借鉴简单的OSI和TCP/IP通信模型来讨论如何自定义一个适应自己的通信协议

1.前言

在物联网的通信中,很多地方需要自定义协议。但考虑到平时工作中接触到的自定义协议,都或多或少存在一些问题和缺陷。所以想借鉴之前看过的书上的知识以及一些国际标准的协议,来简单谈谈如何设计一个自定义通信协议,并且通信方都遵守这个协议,并如何谈谈根据自己的需求对协议进行简化。
除此之外,考虑到后面可能不太会接触这一块,所以可能是一次集中性的大清理,后面可能会整理一下底层(单片机)上一般如何处理数据接受发送的问题,大概会以lwip为例。
收集的资料会有些不全,如果有什么比较特别的协议可以在评论指出。下面也是按我自己对这个协议理解的角度来讲。为了节省篇幅着重点,中间会省去一些基本的概念。

这里会省去一些通信的基本概念,着重分析协议,分析每层的目的和协议组成,不分析原理。

这里补充一点,也是突然上次有人和我说我才想起来,除了我们用相互规定的数据流来表示内容外,还有一种协议的表示方法:可以用字符串解析,这种可以参考HTTP没什么太多需要解释的。只不过字符串的内容和方式通信方自定义。不过个人认为这种方式对嵌入式来说没太多必要,太浪费有限的带宽,同时解析速度也很慢。不过就算是HTTP的下面层也是TCP,IP数据链路之类的,依然是要通过数据流的层次上进行包装,只是对应用层的解析上比较直观罢了。

2.经典的OSI七层模型

这里首先以经典的OSI七层模型介绍整个通信从物理层到应用层的的层次分层。分层有助于分离出不同层的处理任务和职责,不同层之间通过接口由下一层向上一层提供服务。此外,分层设计的也会过分的模块化,使处理变得繁重。
下面是模型的图示:

这里写图片描述

(盗了一张图,各层次的简单功能都写在旁边了。附上出处地址:http://blog.sina.com.cn/s/blog_4770ef020101i1wd.html)

这个模型分的很细,中间增加了很多保证速度,数据可靠性,安全等的机制。在我们应用的时候,根据需求不同,有着不同的侧重点。有的时候需要的只是一些简单的数据传输功能,有的时候我们借助了硬件模块(模块自身已经实现了协议),或者需要实现协议栈,每种还需要根据结合情况,制定的协议有所不同。

从很多通信模型中都不难看出,其实基本都逃不出这个模型,他们很多都是根据自己的需要在模型的基础上做些删减和简化。

2.1.TCP/IP模型解析

2.1.1.整体介绍

TCP/IP是将OSI模型进行部分的简化,将7层模型压缩为4层模型。不过两者的侧重点不太一样。OSI是列出了通信的基本协议,以及具体如何分层,划分职责。TCP/IP则侧重具体协议的实现。

对应分层如下图所示。图片出处:http://www.lxway.com/925941824.htm。

TCP/IP协议整体的感觉大概就是下面这个样子。每一层会在数据的前面加一个首部,中间包含了那一层对应的控制信息。(这里补充一下:数据可以用包,帧,数据包,段,消息来描述。)

这里写图片描述

这里写图片描述

2.2.2.数据链路层

首先是把物理层和数据链路层合并,定义了通信媒介互联设备间的传输规范。这一层往往对应的是设备驱动程序和网络接口。物理层实现数据0,1转化,数据链路层把数据集合成帧,同时需要保证电子线路上的可靠性。这层还是比较注重数据的传输方式,网络的拓扑结构等物理方面的处理。
除去线路上的物理连接外,还使用了MAC地址来识别不同的连接对象。因此这层的数据首部中会加入MAC地址信息。通常MAC地址是有设备制造商决定的,并且是唯一的(虚拟机的那种??除外)。

以太网是数据链路中最著名的一种。这里以以太的数据链路层的协议规定示范,其他的不同协议的数据链路层规范也都不一样,如下图。

这里写图片描述

可以看出这层协议的首部主要有MAC地址和控制以太类型。在以太帧格式前面有一段前导码。由0,1交替组成,用来做前导码(8字节),以一个SFD的域(11)作为前导码的结尾。
以太帧的头部一共有14字节,目的MAC和源MAC,再加2字节的上层协议类型(决定下一层网络层接受的数据类型)。帧尾的FCS为帧校验序列,用来排除硬件噪声的干扰导致的错误

2.2.3.网络层

网络层单独对应一层,和OSI的网络层相同。这层主要负责将数据送到正确的目的地址。这里的地址是IP地址,每个连接入网络的设备都需要有个IP地址,IP地址由网络号和主机号组成。这样看上去就可以按地区分配(这里是有ISP和区域网分配出来的),并且比较固定和集中,而不会像MAC地址一样无规律分布。这样加快了数据传输双方寻址的速度(对于很大的大网络更加明显)。这里有个关系,IP寻址是靠路由控制表,同时IP和MAC地址由地址转发表控制。

这层可以不管底层是用那种数据链路进行通信的(可以跨越不同的数据链路,比如WIFI和网线的混连),只负责路由实现节点间的路由通信,同时也不会涉及重发机制,这部分是传输层需要做的工作。

对于下一层的数据链路层来说,网络层同时可以屏蔽不同数据链路的传输问题,这里使用了IP分片处理方式。IP包会根据不同链路进行重组。这应该都是路由器的工作,从而使上层忽略掉数据链路层的影响。而对于上层来说,IP提供的是无连接的服务,这样虽然会有很多冗余,但会提高速度。如果要保证可靠性,可以靠上的传输层来保证。

这层的核心是IP协议,它是基于地址转发分包数据的,跟在数据链路层首部的后面,协议格式具体如图所示:此外还有些辅助性的测试协议比如ICMP,地址解析协议ARP等。
这里写图片描述

前面有4bit为版本号,标记着这个是什么数据,比如说是IPV4还是IPV6。接着4bit表示首部长度用来表示首部大小,以4字节为基本单位,默认为5,即20字节。区分服务用一个字节来表示,说明服务质量。总长度表示IP首部与数据部分结合起来的总字数。标识表(2字节),标志(3bit)片偏移,用于分片重组;生存时间表示以秒为单位当前包在网络中都应该的生存期限。协议表示IP首部的下一个首部隶属于哪个协议。首部校验和用来保证首部的正确性。源地址和目的地址为通信双方的ip地址。

2.2.4.传输层

传输层也是单独对应,负责建立连接断开,保证传输的可靠性。这层包括2类很著名的传输层协议:TCP和UDP。
这里以TCP为例,TCP是一种面向连接的通信协议,它比UDP复杂。这里TCP主要实现数据传输时的控制功能,确认应答,重发机制,连接管理等策略来保证通信的可靠性的。
分析它的格式,具体如下:

这里写图片描述

源/目的端口号就是我们平时TCP/IP所说的端口号。序列号是用来确认发送位置的,每发送一次自增1。确认序列号指下一次应该收到数据的序列号,发送端收到这个序列号以后可以认为前面的数据都被正常接收。数据偏移表示首部的长度。控制位有8为,每一位都有特殊的控制标志位。如下图:

窗口大小是用来表示确认应答号所示位置开始能接受数据的大小。通常不允许超过测出的大小。校验和来表示数据正确性(注意这里的校验和,是用于防止数据链路层以上的干扰而做的)。紧急指针在应用中处理,一般在暂停中断通行或者中断通信情况下使用。选项用于提高TCP的传输能力。

这里写图片描述

SYN建立连接;FIN断开连接;RST连接出现异常必须强制断开;ACK为应答;ECE通知网络拥塞,CWR通知缩小拥塞窗口。

2.2.5.应用层

应用层包括了原来的应用层,表示层,会话层,中间有很多是为了实现某种特定的应用而制定的协议,所以对于具体的应用有不同的处理。这里以http举例。
HTTP属于TCP/IP协议族中的常见的应用服务。HTTP主要应用在Web中。Web中有三个重要的概念:URI,HTML,HTTP。

URI用于识别资源的具体位置。就是我们打开在浏览器地址栏输入的地址。URI可以和HTTP或者HTTPS组合来访问Web,就像输入网页一样。

HTTP是在接入要访问的网页是开始工作的,它的传输层协议采用TCP连接,端口号为80,然后再在这个连接上进行请求应答发送数据报文。HTTP提供了一些命令,像常见的有GET,HEAD,POST等。

HTML是WWW通用的数据表现协议,是一种标记语言,类似于OSI的表示层。可以通过标签的方式将浏览器中的内容显示出阿里,或者设置等。

3.不同类型的通信方式的总结

3.1.从上述模型/协议中获得的经验

TCP/IP模型采用分层设计,从OSI模型中演变过来的。但是TCP/IP相对来说比较复杂,毕竟我们上个网用的就是TCP/IP。但是有一说一,讲讲每个层的特点。
先说数据链路层吧。数据链路这层是为了规范各种不同的数据链路。一个是拓扑结构,一个是物理连接的不同。物理连接不同也会造成数据速度,单次传输长度间的差异。从协议的格式上也可以看出这层实现了解决了。
1.硬件地址的表示MAC,解决多机通信的问题。
2.消除了数据链路的电磁干扰,保证数据正确性。
3.标记了当前数据链路的类型,使上层方便处理。
4.有段前导码来识别帧的起始。

接着是网络层,这层主要的任务是数据路由,另外对上层隔离(消除)了数据链路层差别的影响。通过IP地址来辅助数据发送或接受。如果没有那么多的节点,其实这里也不需要,如果节点较少用简单的地址也能解决。下面是协议中的针对改层的一些重点:
1.标记IP协议相关信息,在这层可以进行不同的处理。
2.对不同的数据链路的数据进行分组操作。
3.转化成IP地址
4.记录了数据的长度。

传输层主要保证连接,确保传输的可靠性。但也根据不同的需要,实现有了不同的策略,像分出TCP,UDP等。但这层总体的目的还是控制数据传输为主。协议中包括了一些:
1.处理端口号。
2.控制不同的传输层协议。
3.连接,断开,应答,重发等控制。
3.流量控制策略。

应用层其实就是我们最后需要给我们应用量身制定的协议,中间可以按自己的需要给加一些特定的内容,甚至数据加密等。

3.2.几种的情况的通信协议

3.2.1.简单的数据传输功能

这种方式一般是指数据量较少,而且大多数为一对一,不进行组网的方式。而且选择的是一些简单的通信方式,像是USB,串口,485,I2C总线之类的。这个时候协议相当于直接和硬件接触了。这时候保证数据不受电磁干扰就非常必要了,所以可以在数据的最后加入数据正确性校验。

3.2.2.借助了硬件模块/协议栈的通信

这种方式类似于,比如我们用了硬件集成协议栈的模块,我们只需要对模块进行配置,剩下就通过借助上一种方式中提到的总线,传输数据给模块或者从模块读取数据,模块会自动帮你发送和接收。但因为和模块的通信还是有硬件连接的过程,所以还是需要保证硬件的干扰,但节点和节点间的一些问题有的模块已经忙你处理了。

3.2.3.直接使用socket通信

直接使用socket通信其实和上面的差不多,只不过不需要通过硬件模块连接这部分,所以协议的内容上也可以功能加自由。但是如果其他端有涉及到硬件的,一些硬件保护措施还是必不可少,除非在集中通信的地方做次协议转换。

3.3.其他补充

上述只是说要注意写什么,并不是绝对的,大多数时候还是要根据自己的应用需求来定协议,如果处理速度,数据量跟得上,可以加入足够的预留,保证除真实数据意外的部分不变,这样方便后续拓展协议的兼容。预留部分可以层次分开来,但不处理,以后需要拓展的时候再加进来,而不影响整体。但是如果是一次性的开发,或者保证不会出现那么多的,可以适当裁剪来减少负担。

另外TCP/IP中经常出现一些协议的版本的分辨标记,如果不是专门去做协议栈,个人认为这部分可以适当忽略。因为我们的通信数据并没有那么复杂,而且大多数时候我们都会用socket,TCP/IP的方式进行通信了,实际上已经少了很多工作,所以也就是在其一些高级平台上开发并不需要你太了解底层。这样就开发的就相当于只用开发应用层协议,定义一个自己开发功能相关的协议,而可以忽略了一些TCP/IP那些协议帮你做过的工作。如果是纯底层开始那考虑的东西就比较多了。

3.3.1.参考协议

借鉴前面的一些协议,不过这里的协议其实是应用层的协议。前面也说了,所以中间省略了一些特定策略的,没必要做TCP/IP协议族里面那些那么复杂,但是真如果有需要还是按目的添加一些参考的协议,增加对应层的策略来解决,但还是按照上下两层提供接口的原则。

大概简单画了一下参考的组成,主要还是针对总线的单片机通信。其实中间有很多也可以是不需要的,具体见自己的应用需求,进行删减和增加。

这里写图片描述

前导码可以按借鉴一下以太帧的前导码,主要的是那个01交替,可以容易区分受到的电磁干扰。同时后面可以加校验,验证整段数据。

地址这里仿数据链路层和网络层的地址以及传输层的端口,为了确定接受发送双方。但如果是一对一可以省略,同时如果没特殊需要,源地址可以省略。同时把地址放前面,如果不是自己对应的地址数据不处理也可以加快速度。

数据长度参照网络层,来标识数据长度,也可以用来验证数据是否有丢失。

序号和确认应答,这里主要参照网络层,防止有重复发送或者数据丢失的问题,数据丢失好理解,数据重复发送,有时候在有些超时重发或者接受错误重发很常见。

标识可以区分数据的不同类型,如果没有那么多控制功能,可以把功能和应答号都混在一起。

数据包分块的问题主要针对的是数据长度过大,单帧发送不能够完全放下的问题。现在是把数据包分块的放在了后面。但其实如果按照通信的原则这个还是属于数据链路层的应该解决的问题(不同的通信方式对应的MTU不一致),应该放在前面,这样可以让应用层不要关注底层的事情。单如果在一些嵌入式的简单应用中,往往收发双方的数据链路是一致(如果不一致,估计由模块里的协议栈已经帮你统一成一致了),但有的时候数据长度还是大于一次发送的长度(或者那个协议栈没做之类的处理),这时候就需要将发送的数据拆包和合包了。这时候放在这个地方,相当于让应用层来处理这些事情。结合标识,可以得到完整的数据。其实也是为了防止有的标准一次发送数据太小的问题。

数据校验的话可以看自己的需求,如果觉得数据传输很稳定可以不用加= =,不过这里的数据校验也是包括几个层次的。比如(硬件干扰/路由器自身的问题之类)的错误,此外还有一些恶意篡改(如果数据重要),可以在应用HASH校验验证数据的完整性等。同时也可以使用一些加密手段防止数据内容被截取。

注:如果考虑可拓展性,省略的部分可以用保留来填充,增加灵活性。

3.3.2.透明传输

另外补充一下透明传输的知识。我之前忘记在那本书上看到了,我只记得自己截了图,下面放上图。

这里写图片描述

透明传输可以把关键部分从你的数据中排除,这样更加清晰了数据的界限,这样也可以减轻程序的负担。之前记得有人问过我,数据校验应该是部分数据还是全部数据。如果用了透明传输那么结果就很明显了:肯定是先校验全部数据,在进行处理数据。

3.3.3.程序处理

协议处理在程序中的处理表现也是分层次的。以3.3.1中的自定义协议为例,像前导码,地址,长度可以归为一层,这一层可以保证数据内容的正确性和接受地址的正确性;确认序列号,确认应答则控制数据传输过程中的可靠性。后面的标识,数据分包和数据内容则可以归为应用的内容。

3.3.4.协议的内容

协议的内容是由自己的应用需要传输的数据自定义的。定义的自由度比较高,只要通信双方按相同的规定装数据和取数据就可以了。

好的,我来回答你的问题。首先,我们需要明确一下自定义用户串口通信协议的基本要求和实现方式。 1. 基本要求 自定义用户串口通信协议需要满足以下基本要求: - 双方之间进行数据交互 - 传输的数据需要有一定的格式规范 - 可以支持复杂的数据交互操作 2. 实现方式 实现自定义用户串口通信协议的方式可以有多种,这里提供一种基本的实现方式: - 定义数据交互的格式规范,如数据包头、数据长度、数据类型、数据校验等。 - 建立数据交互的状态机,根据接收到的数据包进行数据解析,并根据解析结果进行相应的操作。 - 编写对应的数据交互函数,如发送数据包、接收数据包等。 - 定义协议的错误处理机制,如超时、校验失败等错误的处理方式。 下面是一个基本的自定义用户串口通信协议示例: ```c // 数据包头 #define PACKET_HEADER 0xAA // 数据交互状态 typedef enum { STATE_IDLE, // 空闲状态 STATE_HEADER, // 等待数据包头 STATE_LENGTH, // 等待数据包长度 STATE_PAYLOAD, // 等待数据包内容 STATE_CHECKSUM // 等待数据包校验和 } state_t; // 数据包结构体 typedef struct { uint8_t header; // 数据包头 uint8_t length; // 数据包长度 uint8_t *payload; // 数据包内容 uint8_t checksum; // 数据包校验和 } packet_t; // 发送数据包 void send_packet(packet_t *packet) { // 将数据包内容通过串口发送出去 // ... } // 接收数据包 void receive_packet(packet_t *packet) { static state_t state = STATE_IDLE; static uint8_t payload_index = 0; static uint8_t checksum = 0; uint8_t data = uart_receive(); // 从串口接收一个字节数据 switch (state) { case STATE_IDLE: if (data == PACKET_HEADER) { // 数据包头正确,进入下一个状态 state = STATE_LENGTH; checksum = data; } break; case STATE_LENGTH: packet->length = data; packet->payload = (uint8_t *)malloc(packet->length); // 分配数据包内容的内存空间 payload_index = 0; state = STATE_PAYLOAD; checksum += data; break; case STATE_PAYLOAD: packet->payload[payload_index++] = data; checksum += data; if (payload_index == packet->length) { state = STATE_CHECKSUM; } break; case STATE_CHECKSUM: if (data == checksum) { // 数据包校验和正确,接收成功 packet->header = PACKET_HEADER; packet->checksum = checksum; state = STATE_IDLE; } else { // 数据包校验和错误,接收失败 free(packet->payload); packet->length = 0; packet->payload = NULL; state = STATE_IDLE; } break; default: break; } } // 发送数据 void send_data(uint8_t *data, uint8_t length) { packet_t packet; packet.header = PACKET_HEADER; packet.length = length; packet.payload = data; packet.checksum = PACKET_HEADER + length; for (int i = 0; i < length; i++) { packet.checksum += data[i]; } send_packet(&packet); } // 接收数据 void receive_data(uint8_t *data, uint8_t *length) { packet_t packet; receive_packet(&packet); if (packet.length > 0) { memcpy(data, packet.payload, packet.length); *length = packet.length; free(packet.payload); } else { *length = 0; } } ``` 以上代码仅供参考,具体实现方式可以根据具体需求进行优化和修改。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值