设计一个可变长度的数据通信协议编码和解码代码。
要有本机ID字段,远端设备ID字段,指令类型字段,数据体字段,校验字段。其中一个要求是,每次固定收发八个字节,单个数据帧超过八个字节需要分包收发。对接收的数据帧要先存入环形缓存区,解码函数需要对环形缓存区中的协议数据持续解码,直到没有数据。解析出的数据最后逐个列出来,验证对错。对于存在的丢包问题,要求有重发机制。
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace edcode_s
{
/*
* Packet 类表示一个数据包,
* 包含本地ID、远程ID、命令类型、数据长度和数据内容等字段。
*/
class Packet
{
public byte LocalID;
public byte RemoteID;
public byte CommandType;
public byte DataLength;
public byte[] Data = new byte[255];
public ushort CRC;
}
/*
* RingBuffer 类实现了一个环形缓冲区,
* 用于存储发送和接收的数据。
* 它有一个固定大小的缓冲区,
* 通过添加和读取操作来管理数据。
*/
class RingBuffer
{
private const int BufferSize = 1024;
private byte[] buffer = new byte[BufferSize];
private int start = 0;
private int end = 0;
/*
* Add方法
* 将给定的字节数组data中的数据按顺序添加到循环缓冲区中,
* 直到填满整个缓冲区或达到指定的length长度。
*/
public void Add(byte[] data, int length)
{
for (int i = 0; i < length; i++)
{
buffer[end] = data[i];
end = (end + 1) % BufferSize;
}
}
/*
* Read方法
* 从循环缓冲区中读取指定长度的数据,
* 并将其存储到给定的字节数组中,
* 从指定的偏移量开始存储。
* 如果没有读取到任何数据(即缓冲区为空),
* 则返回0;否则返回1表示成功读取了数据。
*/
public int Read(byte[] data, int offset, int length)
{
if (IsEmpty())
return 0;
for (int i = 0; i < length; i++)
{
data[offset + i] = buffer[start];
start = (start + 1) % BufferSize;
}
return 1;
}
public bool IsEmpty()
{
return start == end;
}
}
/*
* Protocol 类是通信协议的核心部分,
* 负责发送和接收数据包。
* 它使用 RingBuffer 来存储发送和接收的数据。
*/
class Protocol
{
private const int PacketSize = 8;
private RingBuffer ringBuffer = new RingBuffer();
private Random rand = new Random();
/*
* SendPacket 方法将一个 Packet 对象转换为字节数组,并计算CRC校验码,
* 然后将数据分块发送到缓冲区中。
* 如果模拟了数据包丢失的情况,则会重新发送数据包。
*
*/
public void SendPacket(Packet packet)
{
int totalLength = 4 + packet.DataLength + 2;
byte[] buffer = new byte[totalLength];
buffer[0] = packet.LocalID;
buffer[1] = packet.RemoteID;
buffer[2] = packet.CommandType;
buffer[3] = packet.DataLength;
Array.Copy(packet.Data, 0, buffer, 4, packet.DataLength);
ushort crc = ComputeCRC(buffer, totalLength - 2);
//变量crc转换为字节数组,并将该字节数组复制到buffer字节数组中。
//totalLength - 2 表示从 buffer 数组的索引位置 totalLength - 2 开始粘贴数据
BitConverter.GetBytes(crc).CopyTo(buffer, totalLength - 2);
for (int i = 0; i < totalLength; i += PacketSize)
{
int chunkSize = Math.Min(PacketSize, totalLength - i);
byte[] chunk = new byte[chunkSize];
Array.Copy(buffer, i, chunk, 0, chunkSize);
if (!SimulatePacketLoss(0.1f)) // 10% packet loss rate
{
ringBuffer.Add(chunk, chunkSize);
}
else
{
Console.WriteLine("Packet loss occurred, resending...");
ResendPacket(chunk, chunkSize);
}
}
}
/*
* DecodePacket 方法从缓冲区中读取数据,
* 解析出数据包的各个字段,
* 并进行CRC校验。如果校验成功,则返回 true,否则返回 false。
*/
public bool DecodePacket(out Packet packet)
{
packet = new Packet();
if (ringBuffer.IsEmpty())
return false;
byte[] header = new byte[4];
ringBuffer.Read(header, 0, 4);
packet.LocalID = header[0];
packet.RemoteID = header[1];
packet.CommandType = header[2];
packet.DataLength = header[3];
packet.Data = new byte[packet.DataLength];
ringBuffer.Read(packet.Data, 0, packet.DataLength);
ushort receivedCRC;
byte[] crcBytes = new byte[2];
ringBuffer.Read(crcBytes, 0, 2);
receivedCRC = BitConverter.ToUInt16(crcBytes, 0);
byte[] fullPacket = new byte[4 + packet.DataLength];
Array.Copy(header, 0, fullPacket, 0, 4);
//将一个数组packet.Data的从0位置开始的长度为packet.DataLength的内容复制到另一个数组fullPacket的4位置。
Array.Copy(packet.Data, 0, fullPacket, 4, packet.DataLength);
ushort computedCRC = ComputeCRC(fullPacket, fullPacket.Length);
if (receivedCRC != computedCRC)
{
Console.WriteLine("CRC error");
return false;
}
return true;
}
/*
* ResendPacket 方法将数据包重新添加到缓冲区中,以便重新发送。
*/
private void ResendPacket(byte[] data, int length)
{
ringBuffer.Add(data, length);
}
/*
* SimulatePacketLoss 方法根据给定的丢包率随机决定是否模拟数据包丢失。
*/
private bool SimulatePacketLoss(float lossRate)
{
return rand.NextDouble() < lossRate;//生成一个0到1之间的随机小数,并将其与lossRate进行比较。
}
/*
* ComputeCRC 方法计算给定数据的CRC校验码(CRC-16)。
*/
private ushort ComputeCRC(byte[] data, int length)
{
/*
* 变量crc初始化为0xFFFF。然后使用两个嵌套的循环来遍历数据并计算CRC值。
* 外层循环遍历整个数据数组,内层循环处理每个字节的每一位。
* 在内层循环中,首先检查crc的最低位是否为1。
* 如果是1,则将crc右移一位并与0xA001进行异或操作;
* 否则,直接将crc右移一位。
*/
//CRC寄存器被初始化0xFFFF。
ushort crc = 0xFFFF;
for (int i = 0; i < length; i++)
{
//将数据的第一个字节(8位)与CRC寄存器的当前值进行异或运算,并将结果存回CRC寄存器。
crc ^= data[i];
//根据最低有效位(LSB)是否为0来决定下一步操作。
//如果LSB为0,则直接将CRC寄存器的内容右移一位;
//如果LSB为1,则在右移一位后,
//还需要与一个特定的16位多项式(如0xA001)进行异或运算。
for (int j = 0; j < 8; j++)
{
if ((crc & 1) != 0)
crc = (ushort)((crc >> 1) ^ 0xA001);
else
crc >>= 1;
}
}
return crc;
}
}
/*
* Program 类是程序的入口点,创建一个 Protocol 对象,
* 构造一个 Packet 对象,然后发送该数据包,
* 并尝试解码接收到的数据包。
*/
class Program
{
static void Main(string[] args)
{
Protocol protocol = new Protocol();
Packet packet = new Packet();
packet.LocalID = 1;
packet.RemoteID = 2;
packet.CommandType = 0x01;
packet.Data = new byte[] { 0x11, 0x22, 0x33, 0x44, 0x55,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0xaa,0xbb,0xff };
packet.DataLength = byte.Parse(packet.Data.Length.ToString());
protocol.SendPacket(packet);
if (protocol.DecodePacket(out Packet received))
{
Console.WriteLine("Packet received successfully:");
Console.WriteLine($"Local ID: {received.LocalID}");
Console.WriteLine($"Remote ID: {received.RemoteID}");
Console.WriteLine($"Command Type: {received.CommandType:X2}");
Console.WriteLine($"Data Length: {received.DataLength}");
Console.Write("Data: ");
for (int i = 0; i < received.DataLength; i++)
{
Console.Write($"{received.Data[i]:X2} ");
}
Console.WriteLine();
}
else
{
Console.WriteLine("Failed to decode packet or CRC error occurred.");
}
Console.ReadKey();
}
}
}