最近学习Unity想实现网络通信,为了对以后项目做打算,想对网络通信方面做些准备以及验证。对于mmorpg类游戏这种网络要求不是很强可以使用Tcp,但是对于Moba、FPS使用TCP有点勉为其难。以前使用 KCP + UDP 验证了 UDP 双端数据的完整性且效率比TCP要高的多,但是自己还没有没有使用 C# 实现,目前先把前端 TCP 弄好,过些日子时间空余再集成 TCP、KCP + UDP,TCP登录验证,分配UDP客户端登录识别KEY,以及要不要在Github上面开源整套RPC框架,整套RPC框架只要用过它就会觉得超爽,比GRPC等方便多了,有集成Lua,这套框架是某游戏公司的并不是我撸出来的,但是已经被我重写了大部分功能,如果开源会不会设计知识产权问题...这些都是后话了。但是我觉得每个开发人员都参与到开源事业,则中国的技术会有整体的提高,谁没有用过开源库?有一点得知道:并不是那些不开源的源码对公司有多大的商业价值,而是这些开源后因为代码实在是太烂了而导致用户不敢使用。谁面试的时候不是被问的技术有多深、多牛逼,但是你会发现公司内部的源码就是小学生写的。
入正题吧:回想在上家的时候网络通信基本没有问题,有一点就是客户端比较卡,这段时间学Unity的时候顺便把以前客户端的看了些:没眼看。费尽心思总算把前端的网络给捡出来了。正常说来网络这部分不管前后端都会有单独的线程来处理,然而这里的客户端不是这样,贴代码吧
//部分代码 class GameLoader : MonoBehaviour { private void FixedUpdate() { IConnection main = _net.getMainConnection(); main.onBagTimer(); } } private void ReceiveSorket() { ... byte[] bytes = new byte[4096]; int len = socket.Receive(bytes, 4096, SocketFlags.None); .... } virtual public void onBagTimer() { ReceiveSorket(); byte[] ba; for (int i = 0; i < bagMax; i++) { if (bagArray.Count == 0) { break; } ba = bagArray[0] as byte[]; bagArray.RemoveAt(0); handler(ba); } }
FixedUpdate 固定帧会被执行的,那就是说onBagTimer固定帧数被执行
ReceiveSorket 中将Buff数据按照协议将数据拆解,再组装成逻辑层需要用到的二进制流,最终在 handler 回调里面将数据解析成protobuf结构,再扔给逻辑层。
整个数据流向就理通了,这是主程干得出来的?
还有更奇葩的 buffer 处理
private void ReceiveSorket() { try { //Receive方法中会一直等待服务端回发消息 //如果没有回发会一直在这里等着。 if ((socket.Connected == false || socket.Available <= 0)) { // Thread.Sleep(133); return; } //接受数据保存至bytes当中 byte[] bytes = new byte[4096]; int len = socket.Receive(bytes, 4096, SocketFlags.None); if (len <= 0) { socket.Close(); return; } byte[] new_bytes = new byte[len]; Array.Copy(bytes, 0, new_bytes, 0, len); buffer.pushByteArray(new_bytes); List<byte[]> temp = buffer.split(); if (temp == null) { return; } bagArray.AddRange(temp); } }
public void pushByteArray(byte[] ba) { if (buffer == null) { readLength(ba, 0); buffer = ba; } else { byte[] temp = new byte[buffer.Length + ba.Length]; buffer.CopyTo(temp,0); ba.CopyTo(temp,buffer.Length); buffer = temp; readLength(buffer, afterLength); } }
public List<byte[]> split() { try { //判断当前缓存包长度是否够读取 if (buffer == null || length == 0 || (buffer != null && (buffer.Length - afterLength) < length)) { return null; } bag = new List<byte[]>(); //截取数据包 while (true) { tempBag = new byte[length]; Array.Copy(buffer,afterLength,tempBag,0,length); afterLength += length; length = 0; bag.Add(tempBag); if (!readLength(buffer, afterLength) || buffer.Length - afterLength == 0) { //检查是否还有下一组消息数据 if (buffer.Length - afterLength == 0) { //当前缓存区如果木有数据则清空 buffer = null; afterLength = 0; } break; } } } catch (Exception ex) { } return bag; }
每次最多接收4096个字节到临时 bytes 中,在new一个实际接收长度的 new_bytes 将 bytes 拷贝到 new_bytes 中, pushByteArray 中将新 buffer 和 上一次接收的数据一起再来一次数据拷贝(缓存起来), split 又一次拷贝(将缓存数据按照包长拆解程逻辑层用的数据包),这 Buffer 拷贝次数太多了吧,谁家游戏网络卡顿的时候不是在怼后端?
重点来了:对前端 Buffer 处理优化(单独的网络线程 + 循环数组)
buffer 基类 public class BufferLoop { protected const int CHUNK_SIZE = 1024 * 2; protected byte[] _buff; protected int _head = 0; protected int _tail = 0; protected int _capacity = 0; public BufferLoop(int bufsize) { int c = (bufsize + CHUNK_SIZE - 1) / CHUNK_SIZE; _capacity = c * CHUNK_SIZE; _buff = new byte[_capacity]; _head = 0; _tail = 0; } public int Capacity() { return _capacity; } public int Size() { if (_head < _tail) return _tail - _head; else if (_head > _tail) return _capacity - _head + _tail; return 0; } public void OffsetHead(int off) { _head = (_head + off) % _capacity; } public void OffsetTail(int off) { _tail = (_tail + off) % _capacity; } public byte[] GetBuffer() { return _buff; } public int GetHead() { return _head; } public int GetTail() { return _tail; } public int GetMaxBufferSize() { return System.Convert.ToInt32(CHUNK_SIZE * 0.9); } }
Buffer_loop_r.cs 读 buffer public class Buffer_loop_r : BufferLoop { const int MIN_READ_BUF = 10; public Buffer_loop_r(int bufsize) : base(bufsize) { } public int Read(ref byte[] buf, int len, bool offset = true) { if (len <= 0) return 0; else if (len > Size()) return 0; if (_head < _tail) { Array.Copy(_buff, _head, buf, 0, len); } else { int rLen = _capacity - _head; if (len <= rLen) { Array.Copy(_buff, _head, buf, 0, len); } else { Array.Copy(_buff, _head, buf, 0, rLen); Array.Copy(_buff, 0, buf, rLen, len - rLen); } } if (offset) OffsetHead(len); return len; } //可用来接收的空间 如果不足 MIN_READ_BUF 则将 数据 public int GetSpaceR() { if (GetSpaceRead() <= MIN_READ_BUF) ReplaceR(); return GetSpaceRead(); } // protected int GetSpaceRead() { if (_head <= _tail) return _capacity - _tail; else return _head - _tail; } protected void ReplaceR() { if (_head <= _tail) { int s = Size(); if (s > 0) { //不需要处理局部重叠 Array.Copy(_buff, _head, _buff, 0, s); } _head = 0; _tail = s % _capacity; } } }
Buffer_loop_w.cs 写 Buffer public class Buffer_loop_w : BufferLoop { public Buffer_loop_w(int bufsize) : base(bufsize) { } public int Write(byte[] buf, int len) { if (_head <= _tail) { int rLen = _capacity - _tail; if (len <= rLen) { Array.Copy(buf, 0, _buff, _tail, len); } else { Array.Copy(buf, 0, _buff, _tail, rLen); Array.Copy(buf, rLen, _buff, 0, len - rLen); } } else { Array.Copy(buf, 0, _buff, _tail, len); } OffsetTail(len); return len; } public int GetSizeS() { if (_head <= _tail) { return _tail - _head; } else if (_head > _tail) { return _capacity - _head; } return 0; } public int GetSpaceW() { return Capacity() - Size(); } public void Replace(ref Buffer_loop_w buffW) { int h = buffW._head; int t = buffW._tail; if (h < t) { int len = t - h; Array.Copy(buffW._buff, h, _buff, 0, len); _head = 0; _tail = len; } else if (h > t) { int len = buffW._capacity - h; Array.Copy(buffW._buff, h, _buff, 0, len); if (t > 0) Array.Copy(buffW._buff, 0, _buff, len, t); _head = 0; _tail = len + t; } } }
使用方式
var bytesRead = _client.Receive(_inBuffer.GetBuffer(), _inBuffer.GetTail(), len, SocketFlags.None); _inBuffer.OffsetTail(bytesRead); 这里我用的是同步,也有使用BeginReceive实现的,但是网络线程没有别的事,使用异步的的话那大部分时间在sleep
var bytesSent = _client.Send(_outBuffer.GetBuffer(), _outBuffer.GetHead(), len, SocketFlags.None);
_outBuffer.OffsetHead(bytesSent);
对接收 Buffer 在拆包的时候将逻辑层完整的包扔进一个队里里面,基本上只需要拷贝一次,只有在两种极端情况才会多一次拷贝:
1、尾部在头部后面,且容量比减去尾部小于 MIN_READ_BUF,当前接收到的总数据不足 MIN_READ_BUF
2、头部在尾部后面,头部减去尾部小于 MIN_READ_BUF,就是当前的包比较大,基本是是最大的包大于 Buffer大小。
对包大于 Buffer 情况,要么逻辑层实现分页(像Skynet最大的包不能超过64K),要么加大 Buffer 空间,读 Buffer 会出现头在尾部后面自动扩涨的话会出现多次拷贝得不偿失,还有前端很少会发生一个超大的数据包,写 Buffer 可以自动扩涨,有时候包比较大,比如:获取背包信息