TJ/T808 终端通讯协议设计与实现(码农本色)
由于公司项目涉及到相关技术,对于平常写WEB的技术人员来说对这人来说比较默生;为了让下面的技术人员更好地对这个协议的实施,所以单独针对这个协议进行了分析和设计,以更于后期更好指导相关开发工作。由于自己对网络这一块比较熟悉,之前也有过想法实现TJ/T808相关协议,只是一直没这个动力去做;恰好碰到这次机会顺更也动动手写下代码。
TJ/T808协议了解
其实看了一下这个协议,在设计上感觉有些不太合理,不过已经是国标的东西也没有什么可异议的;总体来说这个协议还是比较简单,以下是这个协议的基础部分:
为了方便所以截个图就算了,图上已经描述的协议的组成部门和一些主要细节;后面的基本就是一些具体消息体的技术,有需要的朋友可以看协议的详细文档。
设计
协议整体分为两大部分,消息头和消息体;在消息中还有一个相对处理工作比较的多信息,就是消息属性。所以在设计上主要分为以下几大部分:协议封装和解释,消息结构定义,消息体结构定义和消息体属性结构定义;部体结构设计如下:
为了达到更好的通用性,在设计上通过协议封装和解释接口和最终网络通讯环节隔离;这样在集成和开发上都具备比较高的灵活性。
IProtocolBuffer协议
首先我们需要一个规范来定义网络数据封装和解释,并且可以和网络处理层进行一个良好的隔离;这个协议接口的主要功能包括:组包,拆包,相关基础类型的读取和写入。
public interface IProtocolBuffer { void Write(byte[] data); void Write(byte data); byte Read(); byte[] Read(int length); bool Import(byte value); int Import(byte[] data, int offset, int count); void ReadSubBuffer(IProtocolBuffer buffer, int count); void WriteSubBuffer(IProtocolBuffer buffer); void Reset(); int Length { get; } void SetLength(int length); int Postion { get; set; } byte[] Array { get; } void Write(ushort value); void Write(uint value); void WriteBCD(string value); ushort ReadUInt16(); uint ReadUInt(); string ReadBCD(int length); }
在实现上需要注意一些细节,由于协议规定是大端处理,而C#是小端的,所以在处理一些数据上需要进行一些反转处理,以下是针对shot,int,long等基础类型处理代码:
public static short SwapInt16(short v) { return (short)(((v & 0xff) << 8) | ((v >> 8) & 0xff)); } public static ushort SwapUInt16(ushort v) { return (ushort)(((v & 0xff) << 8) | ((v >> 8) & 0xff)); } public static int SwapInt32(int v) { return (int)(((SwapInt16((short)v) & 0xffff) << 0x10) | (SwapInt16((short)(v >> 0x10)) & 0xffff)); } public static uint SwapUInt32(uint v) { return (uint)(((SwapUInt16((ushort)v) & 0xffff) << 0x10) | (SwapUInt16((ushort)(v >> 0x10)) & 0xffff)); } public static long SwapInt64(long v) { return (long)(((SwapInt32((int)v) & 0xffffffffL) << 0x20) | (SwapInt32((int)(v >> 0x20)) & 0xffffffffL)); } public static ulong SwapUInt64(ulong v) { return (ulong)(((SwapUInt32((uint)v) & 0xffffffffL) << 0x20) | (SwapUInt32((uint)(v >> 0x20)) & 0xffffffffL)); }
在这个协议上还有一个需要注意的地方,由于协议采用单字节作为开始和结束标识,对于相关字符需要进行一个转议处理;以下是主要部分的代码封装:
private ProtocolBuffer OnWrite(byte value) { mArray[mPostion] = value; mPostion++; mLength++; return this; } public bool Import(byte value) { if (value == PROTOBUF_TAG) { OnWrite(value); if (!mProtocolStart) { mProtocolStart = true; } else { mPostion = 0; return true; } } else { if (mProtocolStart) { OnWrite(value); } } return false; } public int Import(byte[] data, int offset, int count) { int result = 0; for (int i = offset; i < count; i++) { result++; byte value = data[i]; if (Import(value)) return result; } return -1; } public byte Read() { byte result = mArray[mPostion]; mPostion++; return result; } public byte[] Read(int length) { byte[] result = new byte[length]; for (int i = 0; i < length; i++) { byte value = Read(); if (value == REPLACE_TAG) { value = Read(); if (value == 0x01) { result[i] = REPLACE_TAG; } else if (value == 0x02) { result[i] = PROTOBUF_TAG; } else { //result[i] = value; } } else { result[i] = value; } } return result; } public void Write(byte data) { if (data == PROTOBUF_TAG) { OnWrite(REPLACE_TAG).OnWrite(0x02); } else if (data == REPLACE_TAG) { OnWrite(REPLACE_TAG).OnWrite(0x01); } else { OnWrite(data); } }
消息结构定义
一看到需求进行代码编写的实现代码的习惯并不好,最好在设计的时候通过接口结构来描述具体编写代码总体框架的可行性,这样可以在设计阶段能更好的把控存在问题。根据协议的要求消息的结构定义出接口,交根据实际规划细化接口的组成部分:
public interface IMessage { ushort ID { get; set; } MessageBodyAttributes Property { get; set; } string SIM { get; set; } ushort BussinessNO { get; set; } PacketInfo Packet { get; set; } void Save(IProtocolBuffer buffer); void Load(IProtocolBuffer buffer); IMessageBody Body { get; set; } byte CRC { get; set; } }
由于有两大部分相对比较复杂所以针对消息的消息体属性和消息体单独抽象出来,这样主要降低在协议封装和解释过程在主消息接口处理的复杂度。
接口制定了Save和Load方法用一描述消息包的封装和解释,通过这个规范设计消息的封装和解释完全和具体的数据来源隔离;根据具体消息封装和解释的具体实现如下:
public void Load(IProtocolBuffer buffer) { byte crc = 0; for (int i = 1; i < buffer.Length - 1; i++) crc ^= buffer.Array[i]; //read start buffer.Read(); //read id ID = buffer.ReadUInt16(); //read property Property.Load(buffer); //read sim SIM = buffer.ReadBCD(6); //read no BussinessNO = buffer.ReadUInt16(); //read packet if (Property.IsPacket) { Packet = new PacketInfo(); Packet.Load(buffer); } //read body if (Property.BodyLength > 0) { ProtocolBuffer bodybuffer = new ProtocolBuffer(); IMessageBody body = MessageBodyFactory.Default.GetBody(ID); if (body != null) body.Load(bodybuffer); } //read crc this.CRC = buffer.Read(); if (this.CRC != crc) throw new Exception("message check CRC error!"); //read end buffer.Read(); } public void Save(IProtocolBuffer buffer) { ProtocolBuffer bodybuffer = null; if (Packet != null) Property.IsPacket = true; if (Body != null) { bodybuffer = new ProtocolBuffer(); Body.Save(bodybuffer); if (bodybuffer.Length > MessageBodyAttributes.BODY_LENGTH) throw new Exception("message body to long!"); Property.BodyLength = (ushort)bodybuffer.Length; } //write start buffer.Write(ProtocolBuffer.PROTOBUF_TAG); //write id buffer.Write(ID); //write body property Property.Save(buffer); //write sim buffer.WriteBCD(SIM); //write no buffer.Write(BussinessNO); //write packet if (Packet != null) Packet.Save(buffer); //write body if (bodybuffer != null) buffer.WriteSubBuffer(bodybuffer); //write crc byte crc = 0; for (int i = 1; i < buffer.Length; i++) crc ^= buffer.Array[i]; buffer.Write(crc); //write end buffer.Write(ProtocolBuffer.PROTOBUF_TAG); }
消息体属性描述
由于消息体属性描述是通过解位来处理,所以对于WEB开发的技术人员来这些基础知识相对来说还是比较薄弱了一点。其实大体上就是通过移位,&,|的一些操作来获取相关位的信息,如果对于二进制真的不熟悉其实可以用系统带的计算器开启程序员模式就可以了(这方面的知识对于程序员来说还是有必要补充一下)。
//保留位15 public bool CustomHigh { get; set; } //保留位14 public bool CustomLow { get; set; } //分包位13 public bool IsPacket { get; set; } //加密位12 public bool EncryptHigh { get; set; } //加密位11 public bool EncryptMiddle { get; set; } //加密位10 public bool EncryptLow { get; set; } //消息长度9-0 public ushort BodyLength { get; set; } public void Save(IProtocolBuffer buffer) { ushort value = (ushort)(BodyLength & BODY_LENGTH); if (CustomHigh) value |= CUSTOM_HEIGHT; if (CustomLow) value |= CUSTOM_LOW; if (IsPacket) value |= IS_PACKET; if (EncryptHigh) value |= ENCRYPT_HEIGHT; if (EncryptMiddle) value |= ENCRYPT_MIDDLE; if (EncryptLow) value |= ENCRYPT_LOW; buffer.Write(value); } public void Load(IProtocolBuffer buffer) { ushort value = buffer.ReadUInt16(); CustomHigh = (CUSTOM_HEIGHT & value) > 0; CustomLow = (CUSTOM_LOW & value) > 0; IsPacket = (IS_PACKET & value) > 0; EncryptHigh = (ENCRYPT_HEIGHT & value) > 0; EncryptMiddle = (ENCRYPT_MIDDLE & value) > 0; EncryptLow = (ENCRYPT_LOW & value) > 0; BodyLength = (ushort)(BODY_LENGTH & value); }
消息体描述
在消息设计上通过接口和具体网络处理隔离,在消息体设计也应该采用同样的原则;这样消息体的实现和扩展就不会对上层消息代码有任何的影响。
public interface IMessageBody { void Save(IProtocolBuffer buffer); void Load(IProtocolBuffer buffer); }
只需要很简单的代码即能完成这个工作,所以我们在设计不要为了一些的方便而不去制定抽象行为;其实在抽象的过程就是一个很好的设计方式。有这个规范那在实现基础消息就会方便多了,也不用提心对上层的影响;以下是一个终端设通用响应的实现
class ClientResponse : IMessageBody { public ushort BussinessNO { get; set; } public ushort ResultID { get; set; } public ResultType Result { get; set; } public void Load(IProtocolBuffer buffer) { BussinessNO = buffer.ReadUInt16(); ResultID = buffer.ReadUInt16(); Result = (ResultType)buffer.Read(); } public void Save(IProtocolBuffer buffer) { buffer.Write(BussinessNO); buffer.Write(ResultID); buffer.Write((byte)Result); } }
总结
以上是针对TJ/T808协议实现的一种方式紧供参考!其实在设计上我们还是有些基础准则可以遵守的,在设计根据职责划分抽像规则,把复杂的结构拆成简单独立的个体进行组合应用;接口的抽像定义也是非常重要,其实很多时候沟通过时发现有很多程序员对接口的定性是除了多写代码没有什么作用!其实接口是一个逻辑规划的抽像,通过抽像可以上你在设计阶段的时候更深入的了解功能切割和模块快,通过接口可以更快速有效的审核自己设计的合理性。