使用工具: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 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;
//}
///
/// 解析数据
///
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 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;
//}
///
/// 解析数据
///
public void ReadMessage(int newDataAmount,Action 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 controllerDict = new Dictionary();//使用字典存储有哪些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关闭的时候就将数据库也关闭掉,这样就完成了对数据库的连接和关闭操作。