项目代码:在这
第三章
客户端:定义了NetManager。负责Socket连接,发送,接收。具体接收信息操作将会根据MsgName委托给Main类中的函数操作
服务端:同样MainClass负责Socket的功能,并将消息具体操作给MsgHandler执行。
客户端,在发送List(获取玩家信息时)。客户端加个Enter和List一块发送,导致粘包。书中没有处理,这里可以延迟一点时间发送List消息
这里只实现了进入Enter、Move和Leave。书中攻击、伤害和死亡协议未实现
第四章
- 操作系统层面Socket有发送缓冲区和接收缓冲区、用户的Send和Receive只是从缓冲区中存取,由于缓冲区的情况,所以很容易出现粘包和缺包的情况。
粘包
- 解决方案:
- 长度信息法、发送每一个消息前加上消息的字节长度、这里长度一般可以使用16bit(2个字节)和32bit(4个字节)的整数来表示
- 固定长度、每次发送消息均为固定长度
- 结束符、发送消息后缀均为特殊自定义符号
- 文中利用了长度信息法来解决粘包的情况
- 服务器会将发送的消息体完全的广播给客户端,并且选用Int16作为消息长度信息放在消息头部
- 客户端首先定义recvBuffCount标识当前recvBuff有多少字节数据,在接收到数据时,将会把此次数据长度加到recvBuffCount上。然后调用OnReceiveByteData处理数据。
- 首先判断 recvBuffCount是否小于2,如果小于2说明recvBuff没有完整的消息长度信息,直接返回。如果有利用BitConverter.ToInt16(recvBuff, 0);取出消息长度信息bodyLength。
- 然后判断recvBuff中是否大于等于2+bodyLength。没有说明这条消息不完整。否则从index=2,取出bodyLength长度。
- 最后更新recvBuff和recvBuffCount ,也就是recvBuffCount -=2+bodyLength。然后将recvBuff前2+bodyLength个字节数据去除。这里使用了Array.Copy进行了前移。这里可以想到ET中的CircularBuffer。CircularBuffer使用了一个环形的循环。就不用像这里进行数组拷贝了。
- 这里客户端在RecvCallback使用了System.Threading.Thread.Sleep(10000);阻塞了子线程,来模拟客户端粘包的情况。下图可以看到服务端接收了3条消息。客户端会先显示123,然后等待10s后开始接收并解析剩余的消息。
大小端
个人理解:大端:高尾端,小端:低尾端。顺序是低地址->高地址
解决方案: 统一使用小端
- 在进行BitConverter.GetBytes将会判断是否为小端,如果不是小端!BitConverter.IsLittleEndian。利用Reverse反转
- 取出消息前两位之前用了BitConverter.ToInt16函数。这个函数内部做了一些大小端的处理。但是我们可以仿照这个函数手动来处理。
Int16 bodyLength = (short)((recvBuff[1] << 8) | (recvBuff[0]));
- 解释一下上面的,(星pbyte)指向缓冲区第一个字节,星(pbyte+1)指向缓冲区第二个字节。以小端为例,星(pbyte+1)<<8,左移8位也就是乘2的8次幂。然后 | (星pbyte)也就是相加(自己理解0 | 1 = 1是不是相加)
如何发送完整的数据
- 在发送数据时,定义ByteArray这个类型包括,此次发送的起始索引readIdx=0,和数据长度等然后入队列writeQue。然后判断writeQue队列中是否只有一个只有一个那么将直接发送。否则不发送,此处调用的方法时BeginSend
- 然后在SendCallback回调中,从队列取出第一个firstBa,根据实际发送字节长度更新firstBa的readIdx。如果剩余字节数firstBa.Length == 0,那么说明当前这条信息完整发送。将会这个ByteArray推出,然后取出下一个ByteArray。然后判断此时的firstBa是否为null(此时的firstBa可能是第二个ByteArray,也可能是第一个没有发送完整的),不为null将会继续发送
- 这里因为BeginSend,SendCallback执行在不同的线程中,加入猛点Send。这是可能SendCallback正在执行,又开了一个新线程执行SendCallback。这时将会同时操作writeQue的入队列或者出队列造成资源的竞争。所以这里采用lock进行了加锁。可以看到加锁光写就很不方便。所以ET采用了多进程单线程。
- ByteArray这个结构包含了扩容功能,并且每次需要翻倍时候,将会扩容至原来的2倍,例如1、2、4、8…,还包含对数据的读写,Write操作将会自动判断写入时剩余空间是否足够,不够将会自动扩容,Read操作时,将会根据条件判断是否满足数据前移动操作(也就是Array.Copy)。文中是数据小于8将会进行前移操作。这里复杂度为O(8)。并且提供了读取Int16和Int32长度信息的操作。