5-消息缓冲

错误Sequence contains no elements

请使用.FirstOrDefault()而不是.First()

否则 ==队列为空时== 会报错 Sequence contains no elements

解决线程冲突

由异步的机制可以知道,BeginSend和回调函数往往执行于不同的线程,如果多个线程同时操作writeQueue,有可能引发些问题。

在图4-36所示的流程中,玩家连续点击两次发送按钮,假如运气特别差,第二次发送时,第一次发送的回调函数刚好被调用。

如果线程1的Send刚好走到writeQueue.Enqueue(ba)这一行(t2时刻),按理说writeQueue.Count应为2,不应该进入if(writeQueue.Count==1)的真分支去发送数据(因为此时writeQueue.Count==2)。

但假如在条件判断之前,回调线程刚好执行了writeQueue.Dequeue()(t3时刻),由于writeQueue里只有1个元素,在t4时刻主线程判断if(writeQueue.Count==1)时,条件成立,会发送数据。

SendCallbackba=writeQueue.First()也会获取到队列的第一条数据,也会把它发送出去。第二次发送的数据将会被发送两次,显然不是我们需要的。

为了避免线程竞争,可以通过加锁(lock)的方式处理。当两个线程争夺一个锁的时候,一个线程等待,被阻止的那个锁变为可用。关于锁的介绍,读者可以去网上搜寻更多资料。加锁后,4.5.3节的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public void Send(BaseMessage info)
{
if (!Connected)
{
Program.socket.CloseClientSocket(this);
return;
}
try
{
//拼接字节,省略组装sendBytes的代码
byte[] sendBytes = info.GetBytes();
ByteArray ba = new ByteArray(sendBytes);
writeQueue.Enqueue(ba);
//send
if (writeQueue.Count == 1)
{
this.socket.BeginSend(ba.bytes, ba.readIdx, ba.length,
0, SendCallback, socket);
}
}
catch (Exception e)
{
Console.WriteLine($"发送消息出错: {e.Message}");
Program.socket.CloseClientSocket(this);
}
}

//解决发送不完整问题
private void SendCallback(IAsyncResult result)
{
try
{
//获取state、EndSend的处理
int count = socket.EndSend(result);
//判断是否发送完整
ByteArray ba = writeQueue.FirstOrDefault();
ba.readIdx += count;
if (ba.length == 0)
{ //发送完整
lock (writeQueue)
{
writeQueue.Dequeue();
ba = writeQueue.FirstOrDefault();
}
}
if (ba != null)
{
//发送不完整,或发送完整且存在第二条数据
socket.BeginSend(ba.bytes, ba.readIdx, ba.length,
0, SendCallback, socket);
}
}
catch (SocketException e)
{
Console.WriteLine($"发送消息出错 {e.SocketErrorCode}:{e.Message}");
}
}

高效的接收数据

1.Copy操作

要做到极致,那就极致到底。回顾4.3.4节中接收数据的代码(OnReceiveData),每次成功接收一条完整的数据后,程序会调用Array.Copy,将缓冲区的数据往前移动。但Array.Copy是个时间复杂度为 ==$O(n)$== 的操作,假如缓冲区中的数据很多,那移动全部数据将会花费较长的时间。