在Socket应用开发中,还有一个话题是讨论的比较多的,那就是数据接收后如何处理的问题。这也是一个令刚接触Socket开发的人很头疼的问题。
因为Socket的TCP通讯中有一个“粘包”的现象,既:大多数时候发送端多次发送的小数据包会被连在一起被接收端同时接收到,多个小包被组成一个大包被接收。有时候一个大数据包又会被拆成多个小数据包发送。这样就存在一个将数据包拆分和重新组合的问题。那么如何去处理这个问题呢?这就是我今天要讲的通讯协议。
所谓的协议就是通讯双方协商并制定好要传送的数据的结构与格式。并按制定好的格式去组合与分析数据。从而使数据得以被准确的理解和处理。
那么我们如何去制定通讯协议呢?很简单,就是指定数据中各个字节所代表的意义。比如说:第一位代表封包头,第二位代表封类型,第三、四位代表封包的数据长度。然后后面是实际的数据内容。
如下面这个例子:
01
01
06 00
01 0f ef 87 56 34
协议类别
协议代码
数据长度
实际数据
前面三部分称之为封包头,它的长度是固定的,第四部分是封包数据,它的长度是不固定的,由第三部分标识其长度。因为我们的协议将用在TCP中,所以我没有加入校验位。原因是TCP可以保证数据的完整性。校验位是没有必要存在的。
接下来我们要为这个数据封包声明一个类来封装它:
1 public class Message 2 { 3 private byte _class; 4 private byte _flag; 5 private int _size; 6 private byte[] _content; 7 8 public byte[] Content 9 { 10 get { return _content; } 11 set { _content = value; } 12 } 13 14 public int Size 15 { 16 get { return _size; } 17 set { _size = value; } 18 } 19 20 public byte Flag 21 { 22 get { return _flag; } 23 set { _flag = value; } 24 } 25 26 public byte Class 27 { 28 get { return _class; } 29 set { _class = value; } 30 } 31 32 public Message() 33 { 34 35 } 36 37 public Message(byte @class, byte flag, byte[] content) 38 { 39 _class = @class; 40 _flag = flag; 41 _size = content.Length; 42 _content = content; 43 } 44 45 public byte[] ToBytes() 46 { 47 byte[] _byte; 48 using (MemoryStream mem = new MemoryStream()) 49 { 50 BinaryWriter writer = new BinaryWriter(mem); 51 writer.Write(_class); 52 writer.Write(_flag); 53 writer.Write(_size); 54 if (_size > 0) 55 { 56 writer.Write(_content); 57 } 58 _byte = mem.ToArray(); 59 writer.Close(); 60 } 61 return _byte; 62 } 63 64 public static Message FromBytes(byte[] Buffer) 65 { 66 Message message = new Message(); 67 using (MemoryStream mem = new MemoryStream(Buffer)) 68 { 69 BinaryReader reader = new BinaryReader(mem); 70 message._class = reader.ReadByte(); 71 message._flag = reader.ReadByte(); 72 message._size = reader.ReadInt32(); 73 if (message._size > 0) 74 { 75 message._content = reader.ReadBytes(message._size); 76 } 77 reader.Close(); 78 } 79 return message; 80 } 81 82 }
我们可以用Tobytes()和FromBytes()将封包转换成二进制数组和从二进制数组转换回来。
事情看起来已经解决了,但……真的是这样子吗?不然,我们知道,TCP数据是以流的形式被传送的,我们并不知道一个数据包是否被传送完毕,也不知道我们接收回来的数据包中是否有多个数据包,如果直接使用FromBytes()来转换的话,很可能会因为数据不完整而出现异常,也有可能会因为数据中含有多个数据包而导致数据丢失(因为你并不知道这些数据中含有多少个数据包)。那我们该怎么办?这也不难,我们先把接收回来的数据写入一个流中。然后分析其中是否有完整的数据包,如果有,将其从流中取出,并将这部分数据从流中清除。直到流中没有完整的数据为止,以后接收回来的数据就将其写入流的结尾处,并从头继续分析。直到结束。
让我们来看看这部分的代码:
1 public class MessageStream 2 { 3 private byte[] _buffer; 4 private int _position; 5 private int _length; 6 private int _capacity; 7 8 public MessageStream() 9 { 10 _buffer = new byte[0]; 11 _position = 0; 12 _length = 0; 13 _capacity = 0; 14 } 15 16 private byte ReadByte() 17 { 18 if (this._position >= this._length) 19 { 20 return 0; 21 } 22 return this._buffer[this._position++]; 23 } 24 25 private int ReadInt() 26 { 27 int num = this._position += 4; 28 if (num > this._length) 29 { 30 this._position = this._length; 31 return -1; 32 } 33 return (((this._buffer[num - 4] | (this._buffer[num - 3] << 8)) | (this._buffer[num - 2] << 0x10)) | (this._buffer[num - 1] << 0x18)); 34 } 35 36 private byte[] ReadBytes(int count) 37 { 38 int num = this._length - this._position; 39 if (num > count) 40 { 41 num = count; 42 } 43 if (num <= 0) 44 { 45 return null; 46 } 47 byte[] buffer = new byte[num]; 48 if (num <= 8) 49 { 50 int num2 = num; 51 while (--num2 >= 0) 52 { 53 buffer[num2] = this._buffer[this._position + num2]; 54 } 55 } 56 else 57 { 58 Buffer.BlockCopy(this._buffer, this._position, buffer, 0, num); 59 } 60 this._position += num; 61 return buffer; 62 } 63 64 public bool Read(out Message message) 65 { 66 message = null; 67 _position = 0; 68 if (_length > 6) 69 { 70 message = new Message(); 71 message.Class = ReadByte(); 72 message.Flag = ReadByte(); 73 message.Size = ReadInt(); 74 if (message.Size <= 0 || message.Size <= _length - _position) 75 { 76 if (message.Size > 0) 77 { 78 message.Content = ReadBytes(message.Size); 79 } 80 Remove(message.Size + 6); 81 return true; 82 } 83 else 84 { 85 message = null; 86 return false; 87 } 88 } 89 else 90 { 91 return false; 92 } 93 } 94 95 private void EnsureCapacity(int value) 96 { 97 if (value <= this._capacity) 98 return; 99 int num1 = value; 100 if (num1 < 0x100) 101 num1 = 0x100; 102 if (num1 < (this._capacity * 2)) 103 num1 = this._capacity * 2; 104 byte[] buffer1 = new byte[num1]; 105 if (this._length > 0) 106 Buffer.BlockCopy(this._buffer, 0, buffer1, 0, this._length); 107 this._buffer = buffer1; 108 this._capacity = num1; 109 } 110 111 public void Write(byte[] buffer, int offset, int count) 112 { 113 if (buffer.Length - offset < count) 114 { 115 count = buffer.Length - offset; 116 } 117 EnsureCapacity(buffer.Length + count); 118 Array.Clear(_buffer, _length, _capacity - _length); 119 Buffer.BlockCopy(buffer, offset, _buffer, _length, count); 120 _length += count; 121 } 122 123 private void Remove(int count) 124 { 125 if (_length >= count) 126 { 127 Buffer.BlockCopy(_buffer, count, _buffer, 0, _length - count); 128 _length -= count; 129 Array.Clear(_buffer, _length, _capacity - _length); 130 } 131 else 132 { 133 _length = 0; 134 Array.Clear(_buffer, 0, _capacity); 135 } 136 } 137 }
这个类的使用非常简单,你只要用Write(byte[] buffer, int offset, int count)将接收到的数据写入数据流中,并用bool Read(out Message message)将数据中的第一个数据包取出,如果函数返回True,就说明取回一个封包成功,如果返回False,则说明流中已经没有完整的封包,你需要继续接收后面的数据以组成一个完整的封包。
这们我们的数据分析就会变得非常简单。我们可以在ReceiveCallBack回调函数中将接收到的数据写入到流中并通知线程池中的工作者线程分析数据流并处理数据。我在前面的关于Socket异步操作的文章中的Analyzer函数就是用这两个类来分析处理数据的。这样的好处理就是,Socket工作线程只需要负责数据的接收,并将其写入流,其它的事情由其它的线程这处理,就不会因为处理的时间过长而导致接收操作被阻塞。从而影响Socket的性能。
本文所述方法只是协议处理的多种方法中的其中一种,而且可能并不是很优秀的方法,如果谁有更好的方法,还希望您能和我多多交流。好了,今天就到这里了,关于Socket的文章到这里可能就告一段落了,我现在在研究VS2008里面的新东西,如果有什么必得的话,我会继续写出来的。谢谢大家的支持。