unity網路實戰開發(叢林戰爭)-正式開發階段(013-遊戲伺服器端框架搭建)
使用工具:VS2015
使用語言:c#
作者:Gemini_xujian
參考:siki老師-《叢林戰爭》視訊教程
繼上一篇文章內容,這節課講解一下游戲伺服器端的開發。
01-專案目錄結構建立:
首先開啟VS並建立一個c#控制檯應用程式專案,起名為“遊戲伺服器端”,建立好後,右鍵專案->屬性,將預設的名稱空間改為GameServer(使用英文名稱空間,對中文支援不好),然後建立幾個資料夾,分別是:Model,Server,DAO,Tool,Controller。在之後的開發中將我們編寫的程式碼指令碼分別放到相應的資料夾中即可。另外,我們需要新增與MySQL資料庫建立連線的引用庫,引用方法在我之前的文章中講到過(具體參考unity網路開發實戰-011篇文章)。目錄結構如圖所示:
02-開啟接收客戶端連線,處理跟客戶端的資料通訊並對客戶端訊息進行解析,開發controller控制層
先上程式碼:
server類:
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace GameServer.Server {//這個類用來啟動我們的伺服器端並進行監聽 class Server { private IPEndPoint ipEndPoint; private Socket serverSocket; private List<Client> clientList;//用來儲存所有連線的客戶端 public Server() { } public Server(string ipStr, int port) { SetIpAndPort(ipStr, port); } //設定ip和埠號 public void SetIpAndPort(string ipStr, int port) { ipEndPoint = new IPEndPoint(IPAddress.Parse(ipStr), port); } //建立連線 public void Start() { serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); serverSocket.Bind(ipEndPoint);//繫結ip serverSocket.Listen(0);//設定監聽,為0表示不限制連線數 serverSocket.BeginAccept(AcceptCallBack, null);//開始接收客戶端連線 } //建立接收連線的回撥函式 private void AcceptCallBack(IAsyncResult ar) { Socket clientSocket = serverSocket.EndAccept(ar);//接收到連線並將返回的客戶端socket進行得到 Client client = new Client(clientSocket, this);//建立一個client類,用來管理一個與客戶端的連線 client.Start(); clientList.Add(client);//將此客戶端新增到list集合中 } //移除某個client public void RemoveClient(Client client) { lock (clientList) { clientList.Remove(client); } } } }
client類:
using System; using System.Collections.Generic; using System.Linq; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace GameServer.Server {//用來處理與客戶端的通訊問題 class Client { private Socket clientSocket; private Server server;//持有一個server類的引用 private Message msg = new Message(); public Client() { } public Client(Socket clientSocket,Server server) { this.clientSocket = clientSocket; this.server = server; } //開啟監聽 public void Start() { clientSocket.BeginReceive(msg.Data,msg.StartIndex, msg.RemainSizs, SocketFlags.None,ReceiveCallBack, null); } //接收監聽的回撥函式 private void ReceiveCallBack(IAsyncResult ar) { //做異常捕捉 try { int count = clientSocket.EndReceive(ar);//結束監聽,並返回接收到的資料長度 //如果count=0說明客戶端已經斷開連線,則直接關閉 if (count == 0) { Close(); } msg.ReadMessage(count);//對訊息的處理,進行訊息的解析 Start();//重新呼叫監聽函式 } catch (Exception e) { Console.WriteLine(e); //出現異常則退出 Close(); } } private void Close() { if (clientSocket != null) { clientSocket.Close(); } server.RemoveClient(this); } } }
message類:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GameServer.Server
{
class Message
{
private byte[] data = new byte[1024];//用來儲存現在的資料,需要足夠大
private int startIndex = 0;//用來儲存當前已經存取的資料位置
public byte[] Data
{
get
{
return data;
}
}
public int StartIndex
{
get
{
return startIndex;
}
}
public int RemainSizs
{
get
{
return data.Length - startIndex;
}
}
////更新索引
//public void AddCount(int count)
//{
// startIndex += count;
//}
/// <summary>
/// 解析資料
/// </summary>
public void ReadMessage(int newDataAmount)
{
startIndex += newDataAmount;
while (true)
{
if (startIndex <= 4) return;
int count = BitConverter.ToInt32(data, 0);
if (startIndex - 4 >= count)
{
string s = Encoding.UTF8.GetString(data, 4, count);
Array.Copy(data, count+4, data, 0, startIndex - 4 - count);
startIndex -= count + 4;
}
else
{
break;
}
}
}
}
}
basecontroller類:
using Common;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GameServer.Controller
{
abstract class BaseController
{
RequestCode requestCode = RequestCode.None;//設定請求型別
//預設的處理方法
public virtual void DefaultHandle()
{
}
}
}
requestcode列舉:
using System;
using System.Collections.Generic;
using System.Text;
namespace Common
{
public enum RequestCode
{
None,
}
}
actioncode列舉:
using System;
using System.Collections.Generic;
using System.Text;
namespace Common
{
public enum ActionCode
{
None,
}
}
步驟說明:在我們建立好專案後,首先建立一個server類用來啟動伺服器端並進行監聽,在此類中,首先設定伺服器ip和埠號,然後在start方法中建立socket物件繫結ip並設定監聽,之後開始進行接收客戶端的連線,每當有一個客戶端連線時,建立一個client類,這個類專門用來管理客戶端,一個客戶端對應一個client類例項,建立一個client物件後,將此物件新增到list集合中進行儲存,這樣就完成了server類的基本任務和功能;在client類中,處理客戶端訊息的接收,當接收到客戶端的訊息資料時,則呼叫message類對資料訊息進行解析,暫時實現到這裡,接著建立一個basecontroller類,用來作為請求處理的基類,在此類中設定了一個請求型別requestcode,這個型別是我們要使用那個controller進行處理,actioncode是我們要使用哪個方法進行處理,這兩中列舉型別是共享專案,也就是在客戶端與伺服器端要同時新增,並且內容要相同,所以在建立這兩個列舉型別時,首先在VS中新建一個專案,專案型別選擇類庫,命名為common,建立好後,右鍵屬性,將它的目標框架改為2.0,之後建立這兩個列舉型別即可。關於message類的詳細解釋在前面的文章中有過講解,這裡就不過多敘述了。有疑問的朋友可以往上翻一翻我的文章。
03-管理控制器進行請求分發的處理,客戶端請求響應的處理並完成客戶端訊息的解析和傳送
先貼程式碼(高亮部分為修改或新增部分,新建類不做高亮處理):
server類:
using Common;
using GameServer.Controller;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace GameServer.Servers
{//這個類用來啟動我們的伺服器端並進行監聽
class Server
{
private IPEndPoint ipEndPoint;
private Socket serverSocket;
private List<Client> clientList;//用來儲存所有連線的客戶端
private ControllerManager controllerManager;
public Server() { }
public Server(string ipStr, int port)
{
controllerManager = new ControllerManager(this);
SetIpAndPort(ipStr, port);
}
//設定ip和埠號
public void SetIpAndPort(string ipStr, int port)
{
ipEndPoint = new IPEndPoint(IPAddress.Parse(ipStr), port);
}
//建立連線
public void Start()
{
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
serverSocket.Bind(ipEndPoint);//繫結ip
serverSocket.Listen(0);//設定監聽,為0表示不限制連線數
serverSocket.BeginAccept(AcceptCallBack, null);//開始接收客戶端連線
}
//建立接收連線的回撥函式
private void AcceptCallBack(IAsyncResult ar)
{
Socket clientSocket = serverSocket.EndAccept(ar);//接收到連線並將返回的客戶端socket進行得到
Client client = new Client(clientSocket, this);//建立一個client類,用來管理一個與客戶端的連線
client.Start();
clientList.Add(client);//將此客戶端新增到list集合中
}
//移除某個client
public void RemoveClient(Client client)
{
lock (clientList)
{
clientList.Remove(client);
}
}
//向客戶端發起響應
public void SendResponse(Client client,ActionCode actionCode,string data)
{
client.Send(actionCode,data);
}
public void HandleRequest(RequestCode requestCode,ActionCode actionCode,string data,Client client)
{
controllerManager.HandleRequest(requestCode, actionCode, data, client);
}
}
}
client類:
using Common;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace GameServer.Servers
{//用來處理與客戶端的通訊問題
class Client
{
private Socket clientSocket;
private Server server;//持有一個server類的引用
private Message msg = new Message();
public Client() { }
public Client(Socket clientSocket,Server server)
{
this.clientSocket = clientSocket;
this.server = server;
}
//開啟監聽
public void Start()
{
clientSocket.BeginReceive(msg.Data,msg.StartIndex, msg.RemainSizs, SocketFlags.None,ReceiveCallBack, null);
}
//接收監聽的回撥函式
private void ReceiveCallBack(IAsyncResult ar)
{
//做異常捕捉
try
{
int count = clientSocket.EndReceive(ar);//結束監聽,並返回接收到的資料長度
//如果count=0說明客戶端已經斷開連線,則直接關閉
if (count == 0)
{
Close();
}
msg.ReadMessage(count,OnProcessMessage);//對訊息的處理,進行訊息的解析
Start();//重新呼叫監聽函式
}
catch (Exception e)
{
Console.WriteLine(e);
//出現異常則退出
Close();
}
}
private void OnProcessMessage(RequestCode requestCode,ActionCode actionCode,string data)
{
server.HandleRequest(requestCode, actionCode, data, this);
}
private void Close()
{
if (clientSocket != null)
{
clientSocket.Close();
}
server.RemoveClient(this);
}
//向客戶端傳送資料
public void Send(ActionCode actionCode,string data)
{
byte[] bytes = Message.PackData(actionCode, data);
clientSocket.Send(bytes);
}
}
}
message類:
using Common;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GameServer.Servers
{
class Message
{
private byte[] data = new byte[1024];//用來儲存現在的資料,需要足夠大
private int startIndex = 0;//用來儲存當前已經存取的資料位置
public byte[] Data
{
get
{
return data;
}
}
public int StartIndex
{
get
{
return startIndex;
}
}
public int RemainSizs
{
get
{
return data.Length - startIndex;
}
}
////更新索引
//public void AddCount(int count)
//{
// startIndex += count;
//}
/// <summary>
/// 解析資料
/// </summary>
public void ReadMessage(int newDataAmount,Action<RequestCode,ActionCode,string> processDataCallback)
{
startIndex += newDataAmount;
while (true)
{
if (startIndex <= 4) return;
int count = BitConverter.ToInt32(data, 0);
if (startIndex - 4 >= count)
{
RequestCode requestCode= (RequestCode)BitConverter.ToInt32(data, 4);//得到requestcode
ActionCode actionCode = (ActionCode)BitConverter.ToInt32(data,8);//得到actioncode
string s = Encoding.UTF8.GetString(data, 12, count-8);//得到資料
processDataCallback(requestCode, actionCode, s);
Array.Copy(data, count+4, data, 0, startIndex - 4 - count);
startIndex -= count + 4;
}
else
{
break;
}
}
}
//資料包裝
public static byte[] PackData(ActionCode actionCode,string data)
{
byte[] requestCodeBytes = BitConverter.GetBytes((int)actionCode);//將actioncode轉換成位元組陣列
byte[] dataBytes = Encoding.UTF8.GetBytes(data);//將資料轉換成byte陣列
int dataAmount = requestCodeBytes.Length + dataBytes.Length;//得到資料長度
byte[] dataAmountBytes = BitConverter.GetBytes(dataAmount);//將資料長度轉換成byte陣列
return dataAmountBytes.Concat(requestCodeBytes).ToArray().Concat(dataBytes).ToArray();
}
}
}
basecontroller類:
using Common;
using GameServer.Servers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GameServer.Controller
{
abstract class BaseController
{
RequestCode requestCode = RequestCode.None;//設定請求型別
public RequestCode RequestCode
{
get
{
return requestCode;
}
}
//預設的處理方法
public virtual string DefaultHandle(string data,Client client,Server server)
{
return null;
}
}
}
controllermanager類:
using Common;
using GameServer.Servers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace GameServer.Controller
{//用來管理controller
class ControllerManager
{
private Dictionary<RequestCode, BaseController> controllerDict = new Dictionary<RequestCode, BaseController>();//使用字典儲存有哪些controller
private Server server;
//構造方法
public ControllerManager(Server server)
{
this.server = server;
InitController();
}
//初始化方法
void InitController()
{
DefaultController defaultController = new DefaultController();
controllerDict.Add(defaultController.RequestCode,defaultController);
}
//處理請求
public void HandleRequest(RequestCode requestCode,ActionCode actionCode,string data,Client client)
{
BaseController controller;
bool isGet = controllerDict.TryGetValue(requestCode, out controller);
if (isGet == false)
{
Console.WriteLine("無法得到requestcode:"+requestCode+"所對應的controller,無法處理請求");
return;
}
//通過反射得到
string methodName = Enum.GetName(typeof(ActionCode),actionCode);//得到方法名
MethodInfo mi= controller.GetType().GetMethod(methodName);//得到方法的資訊
if (mi == null)
{
Console.WriteLine("[警告]在controller【"+controller.GetType()+"】"+"中沒有對應的處理方法【"+methodName+"】");
return;
}
object[] parameters = new object[] { data,client,server};
object o= mi.Invoke(controller, parameters);//反射呼叫方法並將得到返回值
//如果返回值為空,則表示沒有得到,那麼就結束請求的進一步處理
if(string.IsNullOrEmpty(o as string))
{
return;
}
server.SendResponse(client,actionCode,data);//呼叫server類中的響應方法,給客戶端響應
}
}
}
defaultcontroller類:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GameServer.Controller
{
class DefaultController:BaseController
{
}
}
說明:首先 建立一個controllermanager類來管理所有的controller,在本類中,我們宣告一個字典用來儲存所有的controller類,並定義一個初始化方法init來進行初始化儲存;之後,我們需要實現請求的分發處理,我們在controllermanager中建立一個handlerequest方法,既然是處理請求,那麼就需要我們將需要處理的資料傳入,所以方法的引數有requestcode,actioncode,data和client,引數分別表示請求的型別,呼叫方法的型別,資料內容,處理的客戶端;在方法中,首先得到我們需要處理的controller是哪個,我們需要從字典中通過requestcode得到,得到之後,通過反射的方式得到方法名以及方法的資訊,再然後,我們通過invoke方法來呼叫我們上面得到的對應controller中的方法並將相應的引數傳遞過去,此方法的返回值便是我們之前得到方法的返回值,對返回值進行判斷,如果返回值為空則結束請求,完成上述操作後,呼叫server類中的響應方法對客戶端進行響應。
在message類中,我們完成了資料的解析和資料的包裝,其中的方法是用來解決粘包分包問題而實現的,根據註釋就可以看明白邏輯思路。
客戶端訊息的接收和響應並不是在controllermanager類與client類中直接聯絡呼叫的,而是以server類作為中介,這樣做的好處是降低了耦合度,有利於程式的健壯性。
04-資料庫連線的建立和關閉
先上程式碼:
connhelper類:
using MySql.Data.MySqlClient;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GameServer.Tool
{
class ConnHelper
{
public const string CONNECTIONSTRING = "datasource=127.0.0.1;port=3306;database=叢林戰爭;user=root;pwd=root;";
public static MySqlConnection Connect()
{
MySqlConnection conn = new MySqlConnection(CONNECTIONSTRING);
try
{
conn.Open();
return conn;
}
catch (Exception e)
{
Console.WriteLine("連線資料庫出現異常:"+e);
return null;
}
}
public static void CloseConnection(MySqlConnection conn)
{
if(conn!=null)
conn.Close();
else
{
Console.WriteLine("mysqlconnection不能為空");
}
}
}
}
client類:
using Common;
using GameServer.Tool;
using MySql.Data.MySqlClient;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace GameServer.Servers
{//用來處理與客戶端的通訊問題
class Client
{
private Socket clientSocket;
private Server server;//持有一個server類的引用
private Message msg = new Message();
private MySqlConnection mysqlConn;//持有一個對資料庫的連線
public Client() { }
public Client(Socket clientSocket,Server server)
{
this.clientSocket = clientSocket;
this.server = server;
mysqlConn = ConnHelper.Connect();//建立於資料庫的連線
}
//開啟監聽
public void Start()
{
clientSocket.BeginReceive(msg.Data,msg.StartIndex, msg.RemainSizs, SocketFlags.None,ReceiveCallBack, null);
}
//接收監聽的回撥函式
private void ReceiveCallBack(IAsyncResult ar)
{
//做異常捕捉
try
{
int count = clientSocket.EndReceive(ar);//結束監聽,並返回接收到的資料長度
//如果count=0說明客戶端已經斷開連線,則直接關閉
if (count == 0)
{
Close();
}
msg.ReadMessage(count,OnProcessMessage);//對訊息的處理,進行訊息的解析
Start();//重新呼叫監聽函式
}
catch (Exception e)
{
Console.WriteLine(e);
//出現異常則退出
Close();
}
}
private void OnProcessMessage(RequestCode requestCode,ActionCode actionCode,string data)
{
server.HandleRequest(requestCode, actionCode, data, this);
}
private void Close()
{
ConnHelper.CloseConnection(mysqlConn);
if (clientSocket != null)
{
clientSocket.Close();
}
server.RemoveClient(this);
}
//向客戶端傳送資料
public void Send(ActionCode actionCode,string data)
{
byte[] bytes = Message.PackData(actionCode, data);
clientSocket.Send(bytes);
}
}
}
說明:connhelper類是實現與資料庫的連線功能,在類中,我們首先定義了一個常量的字串,這個字串相當於配置資訊,裡面的每一個欄位分別是通過分好隔開的,datasource是資料庫所在的ip,port是資料庫的埠號,database是資料庫的名字,user是資料庫的賬號,pwd是資料庫的密碼,在下面分別定義了兩個方法,第一個是連線資料庫,第二個是關閉與資料庫的連線。我們在實現了connhelper類之後,就可以在client類中進行呼叫了,本著一個客戶端有一個數據庫連線的原則,我們在每一個client類建立的時候就得到一個數據庫的連線,在client關閉的時候就將資料庫也關閉掉,這樣就完成了對資料庫的連線和關閉操作。