在Socket應用開發中,還有一個話題是讨論的比較多的,那就是資料接收後如何處理的問題。這也是一個令剛接觸Socket開發的人很頭疼的問題。
因為Socket的TCP通訊中有一個“粘包”的現象,既:大多數時候發送端多次發送的小資料包會被連在一起被接收端同時接收到,多個小包被組成一個大包被接收。有時候一個大資料包又會被拆成多個小資料包發送。這樣就存在一個将資料包拆分和重新組合的問題。那麼如何去處理這個問題呢?這就是我今天要講的通訊協定。
所謂的協定就是通訊雙方協商并制定好要傳送的資料的結構與格式。并按制定好的格式去組合與分析資料。進而使資料得以被準确的了解和處理。
那麼我們如何去制定通訊協定呢?很簡單,就是指定資料中各個位元組所代表的意義。比如說:第一位代表封標頭,第二位代表封類型,第三、四位代表封包的資料長度。然後後面是實際的資料内容。
如下面這個例子:
<col>
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
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
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
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裡面的新東西,如果有什麼必得的話,我會繼續寫出來的。謝謝大家的支援。