最近學習Unity想實現網絡通訊,爲了對之後項目作打算,想對網絡通訊方面作些準備以及驗證。對於mmorpg類遊戲這種網絡要求不是很強可使用Tcp,可是對於Moba、FPS使用TCP有點勉爲其難。之前使用 KCP + UDP 驗證了 UDP 雙端數據的完整性且效率比TCP要高的多,可是本身尚未沒有使用 C# 實現,目前先把前端 TCP 弄好,過些日子時間空餘再集成 TCP、KCP + UDP,TCP登陸驗證,分配UDP客戶端登陸識別KEY,以及要不要在Github上面開源整套RPC框架,整套RPC框架只要用過它就會以爲超爽,比GRPC等方便多了,有集成Lua,這套框架是某遊戲公司的並非我擼出來的,可是已經被我重寫了大部分功能,若是開源會不會設計知識產權問題...這些都是後話了。可是我以爲每一個開發人員都參與到開源事業,則中國的技術會有總體的提升,誰沒有用過開源庫?有一點得知道:並非那些不開源的源碼對公司有多大的商業價值,而是這些開源後由於代碼實在是太爛了而致使用戶不敢使用。誰面試的時候不是被問的技術有多深、多牛逼,可是你會發現公司內部的源碼就是小學生寫的。前端
入正題吧:回想在上家的時候網絡通訊基本沒有問題,有一點就是客戶端比較卡,這段時間學Unity的時候順便把之前客戶端的看了些:沒眼看。費盡心思總算把前端的網絡給撿出來了。正常說來網絡這部分無論先後端都會有單獨的線程來處理,然而這裏的客戶端不是這樣,貼代碼吧git
//部分代碼 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結構,再扔給邏輯層。
整個數據流向就理通了,這是主程幹得出來的?github
還有更奇葩的 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 在拆包的時候將邏輯層完整的包扔進一個隊裏裏面,基本上只須要拷貝一次,只有在兩種極端狀況纔會多一次拷貝:網絡
一、尾部在頭部後面,且容量比減去尾部小於 MIN_READ_BUF,當前接收到的總數據不足 MIN_READ_BUF 框架
二、頭部在尾部後面,頭部減去尾部小於 MIN_READ_BUF,就是當前的包比較大,基本是是最大的包大於 Buffer大小。異步
對包大於 Buffer 狀況,要麼邏輯層實現分頁(像Skynet最大的包不能超過64K),要麼加大 Buffer 空間,讀 Buffer 會出現頭在尾部後面自動擴漲的話會出現屢次拷貝得不償失,還有前端不多會發生一個超大的數據包,寫 Buffer 能夠自動擴漲,有時候包比較大,好比:獲取揹包信息