談一款MOBA類遊戲《碼神聯盟》的服務端架構設計與實現(更新優化思路)

  注:本文僅用於在博客園學習分享,還在隨着項目不斷更新和完善中,多有不足,暫謝絕各平臺或我的的轉載和推廣,感謝支持。python

  • 1、前言

  《碼神聯盟》是一款爲技術人作的開源情懷遊戲,每一種編程語言都是一位英雄。客戶端和服務端均使用C#開發,客戶端使用Unity3D引擎,數據庫使用MySQL。這個MOBA類遊戲是筆者在學習時期和客戶端美術策劃的小夥伴一塊兒作的遊戲,筆者主要負責遊戲服務端開發,客戶端也參與了一部分,同時也是這個項目的發起和負責人。此次主要分享這款遊戲的服務端相關的設計與實現,從總體的架構設計,到服務器網絡通訊底層的搭建,通訊協議、模型定製,再到遊戲邏輯的分層架構實現。同時這篇博客也沉澱了筆者在遊戲公司實踐五個月後對遊戲架構與設計的從新審視與思考。git

  這款遊戲自去年完成後筆者曾屢次想寫篇博客來分享,也曾屢次停筆,只因總以爲靈感還不夠積澱還不夠思考還不夠,如今終於能夠跨過這一步和你們分享,但願能夠帶來的是乾貨與誠意滿滿。因爲目前關於遊戲服務端相關的介紹文章少之又少,而爲數很少的幾篇也都是站在遊戲服務端發展歷史和架構的角度上進行分享,不多涉及具體的實現,這篇文章我將嘗試多從實現的層面上加以介紹,所附的代碼均有詳盡註釋,篇幅較長,能夠關注收藏後再看。學習時期作的項目可能沒法達到工業級,參考了github上開源的C#網絡框架,筆者在和小夥伴作這款遊戲時農藥尚未如今這般火。  : ) github

  • 2、服務器架構

  上圖爲這款遊戲的服務器架構和主要邏輯流程圖,筆者將遊戲的代碼實現分爲三個主要模塊:Protocol通訊協議、NetFrame服務器網絡通訊底層的搭建以及LOLServer遊戲的具體邏輯分層架構實現,下面將針對每一個模塊進行分別介紹。sql

  • 3、通訊協議

  

  先從最簡單也最基本的通訊協議部分提及,咱們能夠看到這部分代碼主要分爲xxxProtocol、xxxDTO和xxxModel、以及xxxData四種類型,讓咱們來對它們的做用一探究竟。docker

  • 1.Protocol協議

 
 

 LOLServer\Protocol\Protocol.cs數據庫

using System;
using System.Collections.Generic;
using System.Text;


namespace GameProtocol
{
   public class Protocol
    {
       public const byte TYPE_LOGIN = 0;//登陸模塊
       public const byte TYPE_USER = 1;//用戶模塊
       public const byte TYPE_MATCH = 2;//戰鬥匹配模塊
       public const byte TYPE_SELECT = 3;//戰鬥選人模塊
       public const byte TYPE_FIGHT = 4;//戰鬥模塊
    }
}

  從上述的代碼舉例能夠看到,在Protocol協議部分,咱們主要是定義了一些常量用於模塊通訊,在這個部分分別定義了用戶協議、登陸協議、戰鬥匹配協議、戰鬥選人協議以及戰鬥協議。編程

  • 2.DTO數據傳輸對象

  DTO即數據傳輸對象,表現層與應用層之間是經過數據傳輸對象(DTO)進行交互的,須要瞭解的是,數據傳輸對象DTO自己並非業務對象。數據傳輸對象是根據UI的需求進行設計的,而不是根據領域對象進行設計的。好比,User領域對象可能會包含一些諸如name, level, exp, email等信息。但若是UI上不打算顯示email的信息,那麼UserDTO中也無需包含這個email的數據。c#

  簡單來講Model面向業務,咱們是經過業務來定義Model的。而DTO是面向界面UI,是經過UI的需求來定義的。經過DTO咱們實現了表現層與Model之間的解耦,表現層不引用Model,若是開發過程當中咱們的模型改變了,而界面沒變,咱們就只須要改Model而不須要去改表現層中的東西。windows

using System;
using System.Collections.Generic;
using System.Text;

namespace GameProtocol.dto
{
    [Serializable]
   public class UserDTO
   {
       public int id;//玩家ID 惟一主鍵
       public string name;//玩家暱稱
       public int level;//玩家等級
       public int exp;//玩家經驗
       public int winCount;//勝利場次
       public int loseCount;//失敗場次
       public int ranCount;//逃跑場次
       public int[] heroList;//玩家擁有的英雄列表
       public UserDTO() { }
       public UserDTO(string name, int id, int level, int win, int lose, int ran,int[] heroList)
       {
           this.id = id;
           this.name = name;
           this.winCount = win;
           this.loseCount = lose;
           this.ranCount = ran;
           this.level = level;
           this.heroList = heroList;
       }

    }
}
  • 3.Data屬性配置表

  這部分的實現主要是爲了將程序功能與屬性配置分離,後面能夠由策劃來配置這部份內容,由導表工具自動生成配表,從而減輕程序的開發工做量,擴展遊戲的功能。數組

using System;
using System.Collections.Generic;
using System.Text;

namespace GameProtocol.constans
{
    /// <summary>
    /// 英雄屬性配置表
    /// </summary>
   public class HeroData
    {

       public static readonly Dictionary<int, HeroDataModel> heroMap = new Dictionary<int, HeroDataModel>();
       /// <summary>
       /// 靜態構造 初次訪問的時候自動調用
       /// </summary>
       static HeroData() {
           create(1, "西嘉迦[C++]", 100, 20, 500, 300, 5, 2, 30, 10, 1, 0.5f, 200,200, 1, 2, 3, 4);
           create(2, "派森[Python]", 100, 20, 500, 300, 5, 2, 30, 10, 1, 0.5f, 200, 200, 1, 2, 3, 4);
           create(3, "扎瓦[Java]", 100, 20, 500, 300, 5, 2, 30, 10, 1, 0.5f, 200, 200, 6, 2, 3, 4);
           create(4, "琵欸赤貔[PHP]", 100, 20, 500, 300, 5, 2, 30, 10, 1, 0.5f, 200, 200, 3, 2, 3, 4);
       }
       /// <summary>
       /// 建立模型並添加進字典
       /// </summary>
       /// <param name="code"></param>
       /// <param name="name"></param>
       /// <param name="atkBase"></param>
       /// <param name="defBase"></param>
       /// <param name="hpBase"></param>
       /// <param name="mpBase"></param>
       /// <param name="atkArr"></param>
       /// <param name="defArr"></param>
       /// <param name="hpArr"></param>
       /// <param name="mpArr"></param>
       /// <param name="speed"></param>
       /// <param name="aSpeed"></param>
       /// <param name="range"></param>
       /// <param name="eyeRange"></param>
       /// <param name="skills"></param>
       private static void create(int code,
           string name,
           int  atkBase,
           int  defBase,
           int  hpBase,
           int  mpBase,
           int  atkArr,
           int  defArr,
           int  hpArr,
           int  mpArr,
           float speed,
           float aSpeed,
           float range,
           float eyeRange,
           params int[] skills) {
               HeroDataModel model = new HeroDataModel();
               model.code = code;
               model.name = name;
               model.atkBase = atkBase;
               model.defBase = defBase;
               model.hpBase = hpBase;
               model.mpBase = mpBase;
               model.atkArr = atkArr;
               model.defArr = defArr;
               model.hpArr = hpArr;
               model.mpArr = mpArr;
               model.speed = speed;
               model.aSpeed = aSpeed;
               model.range = range;
               model.eyeRange = eyeRange;
               model.skills = skills;
               heroMap.Add(code, model);
       }
    }

       public partial class HeroDataModel
       {
           public int code;//策劃定義的惟一編號
           public string name;//英雄名稱
           public int atkBase;//初始(基礎)攻擊力
           public int defBase;//初始防護
           public int hpBase;//初始血量
           public int mpBase;//初始藍
           public int atkArr;//攻擊成長
           public int defArr;//防護成長
           public int hpArr;//血量成長
           public int mpArr;//藍成長
           public float speed;//移動速度
           public float aSpeed;//攻擊速度
           public float range;//攻擊距離
           public float eyeRange;//視野範圍
           public int[] skills;//擁有技能
       }
    
}
  • 4、服務器通訊底層搭建

  這部分爲服務器的網絡通訊底層實現,也是遊戲服務器的核心內容,下面將結合具體的代碼以及代碼註釋一一介紹底層的實現,可能會涉及到一些C#的網絡編程知識,對C#語言不熟悉不要緊,筆者對C#的運用也僅僅停留在使用階段,只需經過C#這門簡單易懂的語言來窺探整個服務器通訊底層搭建起來的過程,來到咱們的NetFrame網絡通訊框架,這部分乾貨不少,我將用完整的代碼和詳盡的註釋來闡明其意。

  

  • 1.四層Socket模型

 

  將SocketModel分爲了四個層級,分別爲:

  (1)type:一級協議 用於區分所屬模塊,如用戶模塊

  (2)area:二級協議 用於區分模塊下的所屬子模塊,如用戶模塊的子模塊爲道具模塊一、裝備模塊二、技能模塊3等

  (3)command:三級協議  用於區分當前處理邏輯功能,如道具模塊的邏輯功能有「使用(申請/結果),丟棄,得到」等,技能模塊的邏輯功能有「學習,升級,遺忘」等;

  (4)message:消息體 當前須要處理的主體數據,如技能書

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NetFrame.auto
{
   public class SocketModel
    {
       /// <summary>
       /// 一級協議 用於區分所屬模塊
       /// </summary>
       public byte type {get;set;}
       /// <summary>
       /// 二級協議 用於區分 模塊下所屬子模塊
       /// </summary>
       public int area { get; set; }
       /// <summary>
       /// 三級協議  用於區分當前處理邏輯功能
       /// </summary>
       public int command { get; set; }
       /// <summary>
       /// 消息體 當前須要處理的主體數據
       /// </summary>
       public object message { get; set; }

       public SocketModel() { }
       public SocketModel(byte t,int a,int c,object o) {
           this.type = t;
           this.area = a;
           this.command = c;
           this.message = o;
       }

       public T GetMessage<T>() {
           return (T)message;
       }
    }
}

   同時封裝了一個消息封裝的方法,收到消息的處理流程如圖所示:

  • 2.對象序列化與反序列化爲對象  

  序列化: 將數據結構或對象轉換成二進制串的過程。

  反序列化:將在序列化過程當中所生成的二進制串轉換成數據結構或者對象的過程。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading.Tasks;

namespace NetFrame
{
   public class SerializeUtil
    {
       /// <summary>
       /// 對象序列化
       /// </summary>
       /// <param name="value"></param>
       /// <returns></returns>
       public static byte[] encode(object value) {
           MemoryStream ms = new MemoryStream();//建立編碼解碼的內存流對象
           BinaryFormatter bw = new BinaryFormatter();//二進制流序列化對象
           //將obj對象序列化成二進制數據 寫入到 內存流
           bw.Serialize(ms, value);
           byte[] result=new byte[ms.Length];
           //將流數據 拷貝到結果數組
           Buffer.BlockCopy(ms.GetBuffer(), 0, result, 0, (int)ms.Length);
           ms.Close();
           return result;
       }
       /// <summary>
       /// 反序列化爲對象
       /// </summary>
       /// <param name="value"></param>
       /// <returns></returns>
       public static object decode(byte[] value) {
           MemoryStream ms = new MemoryStream(value);//建立編碼解碼的內存流對象 並將須要反序列化的數據寫入其中
           BinaryFormatter bw = new BinaryFormatter();//二進制流序列化對象
           //將流數據反序列化爲obj對象
           object result= bw.Deserialize(ms);
           ms.Close();
           return result;
       }
    }
}
  • 3.消息體序列化與反序列化

  相應的,咱們利用上面寫好的序列化和反序列化方法將咱們再Socket模型中定義的message消息體進行序列化與反序列化

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NetFrame.auto
{
   public class MessageEncoding
    {
       /// <summary>
       /// 消息體序列化
       /// </summary>
       /// <param name="value"></param>
       /// <returns></returns>
       public static byte[] encode(object value) {
           SocketModel model = value as SocketModel;
           ByteArray ba = new ByteArray();
           ba.write(model.type);
           ba.write(model.area);
           ba.write(model.command);
           //判斷消息體是否爲空  不爲空則序列化後寫入
           if (model.message != null)
           {
               ba.write(SerializeUtil.encode(model.message));
           }
           byte[] result = ba.getBuff();
           ba.Close();
           return result;
       }
       /// <summary>
       /// 消息體反序列化
       /// </summary>
       /// <param name="value"></param>
       /// <returns></returns>
       public static object decode(byte[] value)
       {
           ByteArray ba = new ByteArray(value);
           SocketModel model = new SocketModel();
           byte type;
           int area;
           int command;
           //從數據中讀取 三層協議  讀取數據順序必須和寫入順序保持一致
           ba.read(out type);
           ba.read(out area);
           ba.read(out command);
           model.type = type;
           model.area = area;
           model.command = command;
           //判斷讀取完協議後 是否還有數據須要讀取 是則說明有消息體 進行消息體讀取
           if (ba.Readnable) {
               byte[] message;
               //將剩餘數據所有讀取出來
               ba.read(out message, ba.Length - ba.Position);
               //反序列化剩餘數據爲消息體
               model.message = SerializeUtil.decode(message);
           }
           ba.Close();
           return model;
       }
    }
}
  • 4.將數據寫入成二進制

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace NetFrame
{
    /// <summary>
    /// 將數據寫入成二進制
    /// </summary>
   public class ByteArray
    {
       MemoryStream ms = new MemoryStream();

       BinaryWriter bw;
       BinaryReader br;
       public void Close() {
           bw.Close();
           br.Close();
           ms.Close();
       }

       /// <summary>
       /// 支持傳入初始數據的構造
       /// </summary>
       /// <param name="buff"></param>
       public ByteArray(byte[] buff) {
           ms = new MemoryStream(buff);
           bw = new BinaryWriter(ms);
           br = new BinaryReader(ms);
       }

       /// <summary>
       /// 獲取當前數據 讀取到的下標位置
       /// </summary>
       public int Position {
           get { return (int)ms.Position; }
       }

       /// <summary>
       /// 獲取當前數據長度
       /// </summary>
       public int Length
       {
           get { return (int)ms.Length; }
       }
       /// <summary>
       /// 當前是否還有數據能夠讀取
       /// </summary>
       public bool Readnable{
           get { return ms.Length > ms.Position; }
       }

       /// <summary>
       /// 默認構造
       /// </summary>
      public ByteArray() {
           bw = new BinaryWriter(ms);
           br = new BinaryReader(ms);
       }

      public void write(int value) {
          bw.Write(value);
      }
      public void write(byte value)
      {
          bw.Write(value);
      }
      public void write(bool value)
      {
          bw.Write(value);
      }
      public void write(string value)
      {
          bw.Write(value);
      }
      public void write(byte[] value)
      {
          bw.Write(value);
      }

      public void write(double value)
      {
          bw.Write(value);
      }
      public void write(float value)
      {
          bw.Write(value);
      }
      public void write(long value)
      {
          bw.Write(value);
      }


      public void read(out int value)
      {
          value= br.ReadInt32();
      }
      public void read(out byte value)
      {
          value = br.ReadByte();
      }
      public void read(out bool value)
      {
          value = br.ReadBoolean();
      }
      public void read(out string value)
      {
          value = br.ReadString();
      }
      public void read(out byte[] value,int length)
      {
          value = br.ReadBytes(length);
      }

      public void read(out double value)
      {
          value = br.ReadDouble();
      }
      public void read(out float value)
      {
          value = br.ReadSingle();
      }
      public void read(out long value)
      {
          value = br.ReadInt64();
      }

      public void reposition() {
          ms.Position = 0;
      }

       /// <summary>
       /// 獲取數據
       /// </summary>
       /// <returns></returns>
      public byte[] getBuff()
      {
          byte[] result = new byte[ms.Length];
          Buffer.BlockCopy(ms.GetBuffer(), 0, result, 0, (int)ms.Length);
          return result;
      }
    }
}
  • 5.粘包長度編碼與解碼

   粘包出現緣由:在流傳輸中出現(UDP不會出現粘包,由於它有消息邊界)
   1 發送端須要等緩衝區滿才發送出去,形成粘包
   2 接收方不及時接收緩衝區的包,形成多個包接收

   因此這裏咱們須要對粘包長度進行編碼與解碼,具體的代碼以下:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NetFrame.auto
{
   public class LengthEncoding
    {
       /// <summary>
       /// 粘包長度編碼
       /// </summary>
       /// <param name="buff"></param>
       /// <returns></returns>
       public static byte[] encode(byte[] buff) {
           MemoryStream ms = new MemoryStream();//建立內存流對象
           BinaryWriter sw = new BinaryWriter(ms);//寫入二進制對象流
           //寫入消息長度
           sw.Write(buff.Length);
           //寫入消息體
           sw.Write(buff);
           byte[] result = new byte[ms.Length];
           Buffer.BlockCopy(ms.GetBuffer(), 0, result, 0, (int)ms.Length);
           sw.Close();
           ms.Close();
           return result;

       }
       /// <summary>
       /// 粘包長度解碼
       /// </summary>
       /// <param name="cache"></param>
       /// <returns></returns>
       public static byte[] decode(ref List<byte> cache) {
           if (cache.Count < 4) return null;

           MemoryStream ms = new MemoryStream(cache.ToArray());//建立內存流對象,並將緩存數據寫入進去
           BinaryReader br = new BinaryReader(ms);//二進制讀取流
           int length = br.ReadInt32();//從緩存中讀取int型消息體長度
           //若是消息體長度 大於緩存中數據長度 說明消息沒有讀取完 等待下次消息到達後再次處理
           if (length > ms.Length - ms.Position) {
               return null;
           }
           //讀取正確長度的數據
           byte[] result = br.ReadBytes(length);
           //清空緩存
           cache.Clear();
           //將讀取後的剩餘數據寫入緩存
           cache.AddRange(br.ReadBytes((int)(ms.Length - ms.Position)));
           br.Close();
           ms.Close();
           return result;
       }
    }
}
  • 6.delegate委託聲明

   delegate 是表示對具備特定參數列表和返回類型的方法的引用的類型。 在實例化委託時,能夠將其實例與任何具備兼容簽名和返回類型的方法相關聯。經過委託實例調用方法。委託至關於將方法做爲參數傳遞給其餘方法,相似於 C++ 函數指針,但它們是類型安全的。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NetFrame
{
    public delegate byte[] LengthEncode(byte[] value);
    public delegate byte[] LengthDecode(ref List<byte> value);

    public delegate byte[] encode(object value);
    public delegate object decode(byte[] value);
}
  • 7.用戶鏈接對象UserToken

  • SocketAsyncEventArgs介紹

  SocketAsyncEventArgs是微軟提供的高性能異步Socket實現類,主要爲高性能網絡服務器應用程序而設計,主要是爲了不在在異步套接字 I/O 量很是大時發生重複的對象分配和同步。使用此類執行異步套接字操做的模式包含如下步驟:
  (1)
分配一個新的 SocketAsyncEventArgs 上下文對象,或者從應用程序池中獲取一個空閒的此類對象。
  (2)
將該上下文對象的屬性設置爲要執行的操做(例如,完成回調方法、數據緩衝區、緩衝區偏移量以及要傳輸的最大數據量)。
  (3)
調用適當的套接字方法 (xxxAsync) 以啓動異步操做。
  (4)
若是異步套接字方法 (xxxAsync) 返回 true,則在回調中查詢上下文屬性來獲取完成狀態。
  (5)
若是異步套接字方法 (xxxAsync) 返回 false,則說明操做是同步完成的。能夠查詢上下文屬性來獲取操做結果。
  (6)
將該上下文重用於另外一個操做,將它放回到應用程序池中,或者將它丟棄。

  • SocketAsyncEventArgs.UserToken 屬性

  獲取或設置與此異步套接字操做關聯的用戶或應用程序對象。

  命名空間:   System.Net.Sockets

 public object UserToken { get; set; }

  備註:

  此屬性能夠由應用程序相關聯的應用程序狀態對象與 SocketAsyncEventArgs 對象。 首先,此屬性是一種將狀態傳遞到應用程序的事件處理程序(例如,異步操做完成方法)的應用程序的方法。

此屬性用於全部異步套接字 (xxxAsync) 方法。

 

  UserToken類的完整實現代碼以下,能夠結合代碼註釋加以理解:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace NetFrame
{
    /// <summary>
    /// 用戶鏈接信息對象
    /// </summary>
   public class UserToken
    {
       /// <summary>
       /// 用戶鏈接
       /// </summary>
       public Socket conn;
       //用戶異步接收網絡數據對象
       public SocketAsyncEventArgs receiveSAEA;
       //用戶異步發送網絡數據對象
       public SocketAsyncEventArgs sendSAEA;

       public LengthEncode LE;
       public LengthDecode LD;
       public encode encode;
       public decode decode;


       public delegate void SendProcess(SocketAsyncEventArgs e);

       public SendProcess sendProcess;

       public delegate void CloseProcess(UserToken token, string error);

       public CloseProcess closeProcess;

       public AbsHandlerCenter center;

       List<byte> cache = new List<byte>();

       private bool isReading = false;
       private bool isWriting = false;
       Queue<byte[]> writeQueue = new Queue<byte[]>();

       public UserToken() {
           receiveSAEA = new SocketAsyncEventArgs();
           sendSAEA = new SocketAsyncEventArgs();
           receiveSAEA.UserToken = this;
           sendSAEA.UserToken = this;
           //設置接收對象的緩衝區大小
           receiveSAEA.SetBuffer(new byte[1024], 0, 1024);
       }
       //網絡消息到達
       public void receive(byte[] buff) {
           //將消息寫入緩存
           cache.AddRange(buff);
           if (!isReading)
           {
               isReading = true;
               onData();
           }
       }
       //緩存中有數據處理
       void onData() {
           //解碼消息存儲對象
           byte[] buff = null;
           //當粘包解碼器存在的時候 進行粘包處理
           if (LD != null)
           {
               buff = LD(ref cache);
               //消息未接收全 退出數據處理 等待下次消息到達
               if (buff == null) { isReading = false; return; }
           }
           else {
               //緩存區中沒有數據 直接跳出數據處理 等待下次消息到達
               if (cache.Count == 0) { isReading = false; return; }
               buff = cache.ToArray();
               cache.Clear();
           }
           //反序列化方法是否存在
           if (decode == null) { throw new Exception("message decode process is null"); }
           //進行消息反序列化
           object message = decode(buff);
           //TODO 通知應用層 有消息到達
           center.MessageReceive(this, message);
           //尾遞歸 防止在消息處理過程當中 有其餘消息到達而沒有通過處理
           onData();
       }

       public void write(byte[] value) {
           if (conn == null) {
               //此鏈接已經斷開了
               closeProcess(this, "調用已經斷開的鏈接");
               return;
           }
           writeQueue.Enqueue(value);
           if (!isWriting) {
               isWriting = true;
               onWrite();
           }
       }

       public void onWrite() {
           //判斷髮送消息隊列是否有消息
           if (writeQueue.Count == 0) { isWriting = false; return; }
           //取出第一條待發消息
           byte[] buff = writeQueue.Dequeue();
           //設置消息發送異步對象的發送數據緩衝區數據
           sendSAEA.SetBuffer(buff, 0, buff.Length);
           //開啓異步發送
           bool result = conn.SendAsync(sendSAEA);
           //是否掛起
           if (!result) {
               sendProcess(sendSAEA);
           }
       }

       public void writed() {
           //與onData尾遞歸同理
           onWrite();
       }
       public void Close() {
           try
           {
               writeQueue.Clear();
               cache.Clear();
               isReading = false;
               isWriting = false;
               conn.Shutdown(SocketShutdown.Both);
               conn.Close();
               conn = null;
           }
           catch (Exception e) {
               Console.WriteLine(e.Message);
           }
       }
    }
}
  • 8.鏈接池UserTokenPool

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NetFrame
{
   public class UserTokenPool
    {
       private Stack<UserToken> pool;

       public UserTokenPool(int max) {
           pool = new Stack<UserToken>(max);
       }
       /// <summary>
       /// 取出一個鏈接對象 --建立鏈接
       /// </summary>
       public UserToken pop() {

           return pool.Pop();
       }
       //插入一個鏈接對象---釋放鏈接
       public void push(UserToken token) {
           if (token != null)
               pool.Push(token);
       }
       public int Size {
           get { return pool.Count; } 
       }
    }
}
  • 9.抽象處理中心AbsHandlerCenter

  在這裏咱們定義了客戶端鏈接、收到客戶端消息和客戶端斷開鏈接的抽象類,標記爲抽象或包含在抽象類中的成員必須經過從抽象類派生的類來實現。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NetFrame
{
   public abstract class AbsHandlerCenter
    {
       /// <summary>
       /// 客戶端鏈接
       /// </summary>
       /// <param name="token">鏈接的客戶端對象</param>
       public abstract void ClientConnect(UserToken token);
       /// <summary>
       /// 收到客戶端消息
       /// </summary>
       /// <param name="token">發送消息的客戶端對象</param>
       /// <param name="message">消息內容</param>
       public abstract void MessageReceive(UserToken token, object message);
       /// <summary>
       /// 客戶端斷開鏈接
       /// </summary>
       /// <param name="token">斷開的客戶端對象</param>
       /// <param name="error">斷開的錯誤信息</param>
       public abstract void ClientClose(UserToken token, string error);
    }
}
  • 10.HandlerCenter實現類

   接下來具體實現客戶端鏈接、斷開鏈接以及收到消息後的協議分發到具體的邏輯處理模塊,代碼以下:

using GameProtocol;
using LOLServer.logic;
using LOLServer.logic.fight;
using LOLServer.logic.login;
using LOLServer.logic.match;
using LOLServer.logic.select;
using LOLServer.logic.user;
using NetFrame;
using NetFrame.auto;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LOLServer
{
   public class HandlerCenter:AbsHandlerCenter
    {
       HandlerInterface login;
       HandlerInterface user;
       HandlerInterface match;
       HandlerInterface select;
       HandlerInterface fight;
       
       public HandlerCenter() {
           login = new LoginHandler();
           user = new UserHandler();
           match = new MatchHandler();
           select = new SelectHandler();
           fight = new FightHandler();
       }

        public override void ClientClose(UserToken token, string error)
        {
            Console.WriteLine("有客戶端斷開鏈接了");

            select.ClientClose(token, error);
            match.ClientClose(token, error);
            fight.ClientClose(token, error);
            //user的鏈接關閉方法 必定要放在邏輯處理單元后面
            //其餘邏輯單元須要經過user綁定數據來進行內存清理 
            //若是先清除了綁定關係 其餘模塊沒法獲取角色數據會致使沒法清理
            user.ClientClose(token, error);
            login.ClientClose(token, error);
        }

        public override void ClientConnect(UserToken token)
        {
            Console.WriteLine("有客戶端鏈接了");
        }

        public override void MessageReceive(UserToken token, object message)
        {
            SocketModel model = message as SocketModel;
            switch (model.type) { 
                case Protocol.TYPE_LOGIN:
                    login.MessageReceive(token, model);
                    break;
                case Protocol.TYPE_USER:
                    user.MessageReceive(token, model);
                    break;
                case Protocol.TYPE_MATCH:
                    match.MessageReceive(token, model);
                    break;
                case Protocol.TYPE_SELECT:
                    select.MessageReceive(token, model);
                    break;
                case Protocol.TYPE_FIGHT:
                    fight.MessageReceive(token, model);
                    break;
                default:
                    //未知模塊  多是客戶端做弊了 無視
                    break;
            }
        }
    }
}
  • 11.啓動服務器

   寫到這裏,服務器終於能夠啓來了,無論你激不激動,反正坐在這裏寫寫畫畫了一天我是激動了,總算要大功告成了。 : )  

   啓動服務器->監聽IP(可選)->監聽端口,服務器處理流程以下圖:

 

  讓咱們來具體看看代碼實現,均給了詳細的註釋:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace NetFrame
{
   public class ServerStart
    {
       Socket server;//服務器socket監聽對象
       int maxClient;//最大客戶端鏈接數
       Semaphore acceptClients;
       UserTokenPool pool;

       public LengthEncode LE;
       public LengthDecode LD;
       public encode encode;
       public decode decode;

       /// <summary>
       /// 消息處理中心,由外部應用傳入
       /// </summary>
       public AbsHandlerCenter center;
       /// <summary>
       /// 初始化通訊監聽
       /// </summary>
       /// <param name="port">監聽端口</param>
       public ServerStart(int max) {
           //實例化監聽對象
           server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
           //設定服務器最大鏈接人數
           maxClient = max;
           
       }

       public void Start(int port) {
           //建立鏈接池
           pool = new UserTokenPool(maxClient);
           //鏈接信號量
           acceptClients = new Semaphore(maxClient, maxClient);
           for (int i = 0; i < maxClient; i++)
           {
               UserToken token = new UserToken();
               //初始化token信息               
               token.receiveSAEA.Completed += new EventHandler<SocketAsyncEventArgs>(IO_Comleted);
               token.sendSAEA.Completed += new EventHandler<SocketAsyncEventArgs>(IO_Comleted);
               token.LD = LD;
               token.LE = LE;
               token.encode = encode;
               token.decode = decode;
               token.sendProcess = ProcessSend;
               token.closeProcess = ClientClose;
               token.center = center;
               pool.push(token);
           }
           //監聽當前服務器網卡全部可用IP地址的port端口
           // 外網IP  內網IP192.168.x.x 本機IP一個127.0.0.1
           try
           {
               server.Bind(new IPEndPoint(IPAddress.Any, port));
               //置於監聽狀態
               server.Listen(10);
               StartAccept(null);
           }
           catch (Exception e)
           {
               Console.WriteLine(e.Message);
           }
       }
       /// <summary>
       /// 開始客戶端鏈接監聽
       /// </summary>
       public void StartAccept(SocketAsyncEventArgs e) {
           //若是當前傳入爲空  說明調用新的客戶端鏈接監聽事件 不然的話 移除當前客戶端鏈接
           if (e == null)
           {
               e = new SocketAsyncEventArgs();
               e.Completed += new EventHandler<SocketAsyncEventArgs>(Accept_Comleted);
           }
           else {
               e.AcceptSocket = null;
           }
           //信號量-1
           acceptClients.WaitOne();
           bool result= server.AcceptAsync(e);
           //判斷異步事件是否掛起  沒掛起說明馬上執行完成  直接處理事件 不然會在處理完成後觸發Accept_Comleted事件
           if (!result) {
               ProcessAccept(e);
           }
       }

       public void ProcessAccept(SocketAsyncEventArgs e) {
           //從鏈接對象池取出鏈接對象 供新用戶使用
           UserToken token = pool.pop();
           token.conn = e.AcceptSocket;
           //TODO 通知應用層 有客戶端鏈接
           center.ClientConnect(token);
           //開啓消息到達監聽
           StartReceive(token);
           //釋放當前異步對象
           StartAccept(e);
       }

       public void Accept_Comleted(object sender, SocketAsyncEventArgs e) {
           ProcessAccept(e);
       }

       public void StartReceive(UserToken token) {
           try
           {
               //用戶鏈接對象 開啓異步數據接收
               bool result = token.conn.ReceiveAsync(token.receiveSAEA);
               //異步事件是否掛起
               if (!result)
               {
                   ProcessReceive(token.receiveSAEA);
               }
           }
           catch (Exception e) {
               Console.WriteLine(e.Message);
           }
       }

       public void IO_Comleted(object sender, SocketAsyncEventArgs e)
       {
           if (e.LastOperation == SocketAsyncOperation.Receive)
           {
               ProcessReceive(e);
           }
           else {
               ProcessSend(e);
           }
       }

       public void ProcessReceive(SocketAsyncEventArgs e) {
           UserToken token= e.UserToken as UserToken;
           //判斷網絡消息接收是否成功
           if (token.receiveSAEA.BytesTransferred > 0 && token.receiveSAEA.SocketError == SocketError.Success)
           {
               byte[] message = new byte[token.receiveSAEA.BytesTransferred];
               //將網絡消息拷貝到自定義數組
               Buffer.BlockCopy(token.receiveSAEA.Buffer, 0, message, 0, token.receiveSAEA.BytesTransferred);
               //處理接收到的消息
               token.receive(message);
               StartReceive(token);
           }
           else {
               if (token.receiveSAEA.SocketError != SocketError.Success)
               {
                   ClientClose(token, token.receiveSAEA.SocketError.ToString());
               }
               else {
                   ClientClose(token, "客戶端主動斷開鏈接");
               }
           }
       }
       public void ProcessSend(SocketAsyncEventArgs e) {
           UserToken token = e.UserToken as UserToken;
           if (e.SocketError != SocketError.Success)
           {
               ClientClose(token, e.SocketError.ToString());
           }
           else { 
            //消息發送成功,回調成功
               token.writed();
           }
       }

       /// <summary>
       /// 客戶端斷開鏈接
       /// </summary>
       /// <param name="token"> 斷開鏈接的用戶對象</param>
       /// <param name="error">斷開鏈接的錯誤編碼</param>
       public void ClientClose(UserToken token,string error) {
           if (token.conn != null) {
               lock (token) { 
                //通知應用層面 客戶端斷開鏈接了
                   center.ClientClose(token, error);
                   token.Close();
                   //加回一個信號量,供其它用戶使用
                   pool.push(token);
                   acceptClients.Release();                   
               }
           }
       }
    }
}

  至此,服務器的通訊底層已經搭建完畢,能夠進一步進行具體的遊戲邏輯玩法開發了。

  • 5、遊戲服務端邏輯分層實現

 

  邏輯處理主要分層架構以下:

 

  (1)logic邏輯層:邏輯處理模塊,異步的邏輯處理,登陸、用戶處理、匹配、選人、戰鬥的主要邏輯都在這裏,Moba類遊戲是典型的房間服務器架構,AbsOnceHandler用於單體消息發送的處理,AbsMulitHandler用於羣發;

  AbsOnceHandler代碼以下:

using LOLServer.biz;
using LOLServer.dao.model;
using NetFrame;
using NetFrame.auto;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LOLServer.logic
{
   public class AbsOnceHandler
    {
      public IUserBiz userBiz = BizFactory.userBiz;

       private byte type;
       private int area;

       public void SetArea(int area) {
           this.area = area;
       }

       public virtual int GetArea() {
           return area;
       }

       public void SetType(byte type)
       {
           this.type = type;
       }

       public new virtual byte GetType()
       {
           return type;
       }

       /// <summary>
       /// 經過鏈接對象獲取用戶
       /// </summary>
       /// <param name="token"></param>
       /// <returns></returns>
       public USER getUser(UserToken token)
       {
           return userBiz.get(token);
       }

       /// <summary>
       /// 經過ID獲取用戶
       /// </summary>
       /// <param name="token"></param>
       /// <returns></returns>
       public USER getUser(int id)
       {
           return userBiz.get(id);
       }

       /// <summary>
       /// 經過鏈接對象 獲取用戶ID
       /// </summary>
       /// <param name="token"></param>
       /// <returns></returns>
       public int getUserId(UserToken token){
           USER user = getUser(token);
           if(user==null)return -1;
           return user.id;
       }
       /// <summary>
       /// 經過用戶ID獲取鏈接
       /// </summary>
       /// <param name="id"></param>
       /// <returns></returns>
       public UserToken getToken(int id) {
           return userBiz.getToken(id);
       }


       #region 經過鏈接對象發送
       public void write(UserToken token,int command) {
           write(token, command, null);
       }
       public void write(UserToken token, int command,object message)
       {
           write(token,GetArea(), command, message);
       }
       public void write(UserToken token,int area, int command, object message)
       {
           write(token,GetType(), GetArea(), command, message);
       }
       public void write(UserToken token,byte type, int area, int command, object message)
       {
           byte[] value = MessageEncoding.encode(CreateSocketModel(type,area,command,message));
           value = LengthEncoding.encode(value);
           token.write(value);
       }
       #endregion

       #region 經過ID發送
       public void write(int id, int command)
       {
           write(id, command, null);
       }
       public void write(int id, int command, object message)
       {
           write(id, GetArea(), command, message);
       }
       public void write(int id, int area, int command, object message)
       {
           write(id, GetType(), area, command, message);
       }
       public void write(int id, byte type, int area, int command, object message)
       {
           UserToken token= getToken(id);
           if(token==null)return;
           write(token, type, area, command, message);
       }

       public void writeToUsers(int[] users, byte type, int area, int command, object message) {
           byte[] value = MessageEncoding.encode(CreateSocketModel(type, area, command, message));
           value = LengthEncoding.encode(value);
           foreach (int item in users)
           {
               UserToken token = userBiz.getToken(item);
               if (token == null) continue;
                   byte[] bs = new byte[value.Length];
                   Array.Copy(value, 0, bs, 0, value.Length);
                   token.write(bs);
               
           }
       }


       #endregion





       public SocketModel CreateSocketModel(byte type, int area, int command, object message)
       {
           return new SocketModel(type, area, command, message);
       }
    }
}

  AbsMulitHandler繼承自AbsOnceHandler,實現代碼以下:

using NetFrame;
using NetFrame.auto;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LOLServer.logic
{
   public class AbsMulitHandler:AbsOnceHandler
    {
       public List<UserToken> list = new List<UserToken>();
       /// <summary>
       /// 用戶進入當前子模塊
       /// </summary>
       /// <param name="token"></param>
       /// <returns></returns>
       public bool enter(UserToken token) {
           if (list.Contains(token)) {
               return false;
           }
           list.Add(token);
           return true;
       }
       /// <summary>
       /// 用戶是否在此子模塊
       /// </summary>
       /// <param name="token"></param>
       /// <returns></returns>
       public bool isEntered(UserToken token) {
           return list.Contains(token);
       }
       /// <summary>
       /// 用戶離開當前子模塊
       /// </summary>
       /// <param name="token"></param>
       /// <returns></returns>
       public bool leave(UserToken token) {
           if (list.Contains(token)) {
               list.Remove(token);
               return true;
           }
           return false;
       }
       #region 消息羣發API

       public void brocast(int command, object message,UserToken exToken=null) {
           brocast(GetArea(), command, message, exToken);
       }
       public void brocast(int area, int command, object message, UserToken exToken = null)
       {
           brocast(GetType(), area, command, message, exToken);
       }
       public void brocast(byte type, int area, int command, object message, UserToken exToken = null)
       {
           byte[] value = MessageEncoding.encode(CreateSocketModel(type, area, command, message));
           value = LengthEncoding.encode(value);
           foreach (UserToken item in list)
           {
               if (item != exToken)
               {
                   byte[] bs = new byte[value.Length];
                   Array.Copy(value, 0, bs, 0, value.Length);
                   item.write(bs);
               }
           }
       }
       #endregion
    }
}

  (2)biz事務層:事務處理,保證數據安全的邏輯處理,如帳號、用戶信息相關的處理,impl是相關的實現類;

  (3)cache緩存層:讀取數據庫中的內容放在內存中,加快訪問速度;

  (4)dao數據層:服務器和數據庫之間的中間件;

  (5)工具類:一些實用的工具類放在這裏,如定時任務列表,用來實現遊戲中的刷怪,buff等;

  邏輯處理流程以下:

 

  • 6、優化思路

  思考了一些優化思路,自文章發佈後也收到了許多來自朋友圈或留言評論中大神們給出的優化思路,大多數建議都質量很高,極具參考價值和學習意義,大概這就是開源的魅力所在吧。如今把這些思路整理出來分享給你們:

  (1)在原有架構基礎上,能夠進一步考慮下:協議的自動化生成,託管內存的gc消耗控制,更小的網絡延遲和更大的網絡併發;

  (2)若是用上異步消息機制和Nosql 單服承載人數或許還可以上升一些,目前Nosql中MongoDB在遊戲服務端中有較多應用,Redis是筆者我的很喜歡的一個開源Nosql數據庫,也有一些遊戲項目已經在嘗試集成;

  (3).net 自帶的二進制序列化性能誤差,文章中代碼裏數據接收發送時的內存拷貝次數偏多,序列化能夠嘗試Google開源的protobuf,目前不少線上遊戲都在應用;

  (4)用.net framework其實就把服務器綁定到windows上了,同時mono性能堪憂,若是非要用c#的話,能夠嘗試.net core + docker ,網絡庫能夠libuv ,這個方案無論是從擴展仍是性能監控管理上都比windows要優秀許多,業界的遊戲服務器也確實大多在Linux上部署;

  (5)收發消息部分太複雜,使用現成的RPC框架性能、安全性會更好。

  • 7、寫在最後

  好了,這篇文章就分享到這裏,從項目的製做週期,到沉澱積累,到重構設計,到總結與反思,到怎麼把整個架構的設計與實現分享出來,再到寫出一篇文章確實經歷了很長的一段時間,有些圖因爲比較長常常須要把電腦屏幕來回旋轉繪製,但願對您有所幫助,感謝閱讀,篇幅有限不能一一詳述,若有問題或改進優化建議歡迎留言討論。下一篇可能會開始剖析開源MMORPG遊戲服務端引擎KBEngine的源碼,也可能寫C++或python相關,若是您也對這些內容感興趣,或者對筆者感興趣,能夠繼續關注個人後續文章。^_^

  上篇博客後有很多小夥伴給筆者發了私信,大可能是技術生涯的一些迷茫與選擇,最後有一句筆者很喜歡的話分享給你們:衡量一我的才的標準,在於一我的在有限的時間內所展示出來的成長速度。持續學習,持續進步,持續成長,才能持續幸運,持續實現價值。

 

Changelog:

2017-07-22 21:36 發佈

2017-07-24 13:37 更新優化思路

相關文章
相關標籤/搜索