由於公司項目涉及到相關技術,對於平常寫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協議實現的一種方式緊供參考!其實在設計上我們還是有些基礎准則可以遵守的,在設計根據職責划分抽像規則,把復雜的結構拆成簡單獨立的個體進行組合應用;接口的抽像定義也是非常重要,其實很多時候溝通過時發現有很多程序員對接口的定性是除了多寫代碼沒有什么作用!其實接口是一個邏輯規划的抽像,通過抽像可以上你在設計階段的時候更深入的了解功能切割和模塊快,通過接口可以更快速有效的審核自己設計的合理性。