网络游戏《丛林战争》开发与学习之(三):游戏服务器端的功能开发

游戏服务端作为信息交互与处理的核心,由下面5个部分组成

1.Server(创建socket以及监听客户端连接)

2.ConnHelper(工具类,建立与MySQL的连接)

3.Controller(处理客户端发送的请求)

4.Model(与数据库中的表对应)

5.DAO(Data Access Object,操作数据库)

本篇博客介绍基础的服务器端搭建方法,由于代码还不够完善,整个工程文件暂时不贴出来,完善后再上传。

首先梳理一下流程:当客户端访问服务器端Server时,Server首先建立一个Client,用来维护与某一个客户端的连接,并且收发消息。之后通过Client调用Controller处理客户端的请求,如果需要访问数据库,就让Client调用ConnHelper进行访问,Controller在处理请求时如果需要对数据库进行交互,就要访问DAO层,DAO在访问Model时要通过Client取得与数据库的连接,简略图如下所示。

理解了本游戏的层级结构,接下来创建游戏服务器端,在工程下新建一个项目GameServer作为服务器端,添加5个文件夹(对应上图的5个部分)以及连接MySQL的引用,如下图所示

1.管理客户端的连接

客户端的连接管理放在Server文件夹下,在文件夹下创建Server和Client两个类,其中Server负责管理客户端的连接请求,Client负责管理与各个客户端的收发数据。

1.1 Server类

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;
namespace GameServer.Server
{
    class Server
    {
        private IPEndPoint ipEndPoint;
        private Socket serverSocket;
        public Server() { }
        public Server(string ipAddr, int port)
        {
            SetIPandPort(ipAddr, port);
        }
        public void SetIPandPort(string ipAddr, int port)
        {
            ipEndPoint = new IPEndPoint(IPAddress.Parse(ipAddr), port);
        }
        public void StartServer()   //开启监听
        {
            Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            serverSocket.Bind(ipEndPoint);  //绑定
            serverSocket.Listen(0); //监听数无限
            serverSocket.BeginAccept(AcceptCallBack, null);
        }
        private void AcceptCallBack(IAsyncResult ar)    //与客户端的连接通过新创建的Client进行
        {
            Socket clientSocket = serverSocket.EndAccept(ar);   //接收到客户端的连接
            Client client = new Client(clientSocket,this);    //创建一个与客户端的实例,并把server自身传递过去,方便Client后期与Server交互
        }
    }
}

为了在Server中查看并管理已连接的客户端,我们将这些信息存储到List中,

private Socket serverSocket;
        private void AcceptCallBack(IAsyncResult ar)   
        {
            Socket clientSocket = serverSocket.EndAccept(ar);   
            Client client = new Client(clientSocket,this);
            clientList.Add(client);    //添加到List中
        }

1.2 Client类

Client类负责操作与每个客户端的收发数据,方法与(一)类似:

https://blog.csdn.net/s1314_JHC/article/details/80914044

代码如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Sockets;
namespace GameServer.Server
{
    class Client
    {
        private Socket clientSocket;
        private Server server;
        public Client(){ }
        public Client(Socket clientSocket,Server server)  //形参 3.27
        {
            this.clientSocket = clientSocket;
            this.server = server;
        }
        public void StartRecv()
        {
            clientSocket.BeginReceive(null, 0, 0, SocketFlags.None, RecvCallBack,null);    //消息类还没有创建,因此参数先为null
        }
        private void RecvCallBack(IAsyncResult ar)
        {
            try  //接收数据时可能出现异常
            {
                int count = clientSocket.EndReceive(ar);    //数据长度
                if (count == 0)
                {
                    CloseConn();
                }
                //todo:处理接收到的数据
                clientSocket.BeginReceive(null, 0, 0, SocketFlags.None, RecvCallBack, null);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);   //输出错误信息
                CloseConn();
            }
        }
        private void CloseConn()    //断开连接
        {
            if (clientSocket != null)
            {
                clientSocket.Close();
                server.RemoveClient(this);  //断开连接时,Server还持有对Client的引用,因此要移除自身
            }
            
        }
    }
}

其中server.RemoveClient()实现如下

public void RemoveClient(Client client)
        {
            lock (clientList)   //防止client与多个客户端的通信,因此先加锁,确保移除的安全性
            {
                clientList.Remove(client);
            }
        }

1.3Message类

Message类负责对消息的解析,对收发消息的处理单独通过一个Message类管理,方便后期对接收方法的修改。处理方法与(一)中类似,代码如下    

  class Message
    {
        private byte[] data = new byte[1024];   //确保一条完整的数据可以存放
        private int startIdx = 0;   //表示数组中已经存储了多少字节的数据
        public byte[] Data  //获取的值
        {
            get { return data; }
        }
        public int GetStartIdx  //开始的位置
        {
            get { return startIdx; }
        }
        public int RemainSize   //data中剩余数据
        {
            get { return Data.Length - startIdx; }
        }
        //public void AddCount(int count)
        //{
        //    startIdx += count;
        //}
        public void ReadMsg(int newDataAmount)   //完成一条消息的解析,这里直接将Client中消息的长度count传递进来,就不用额外调用一次AddCount(int count)了
        {
            startIdx += newDataAmount;
            while (true)
            {
                if (startIdx <= 4) return;   //小于4表示信息不完整,不解析,继续接收下一条
                int count = BitConverter.ToInt32(data, 0);  //读取数据长度,0表示startIdx的位置
                if ((startIdx - 4) >= count)
                {
                    string s = Encoding.UTF8.GetString(data, 4, count);
                    Console.WriteLine("解析出一条数据:" + s);
                    Array.Copy(data, count + 4, data, 0, startIdx - 4 - count); //接收完数据后将其余数据往前移,便于下一次解析
                    startIdx -= (count + 4);    //更新startIdx
                }
                else
                {
                    break;
                }
            }
        }
    }

并将Client中clientSocket.BeginReceive()修改为

clientSocket.BeginReceive(msg.Data, msg.GetStartIdx, msg.RemainSize, SocketFlags.None, RecvCallBack,null);

在之前的//todo中使用

msg.ReadMsg(count);

读取数据。但在代码中可以看出,现有对数据的操作只是一些输出功能,实际上Client在对数据处理时,要调用Controller处理数据,因此接下来解决Controller类。

1.4 Controller类

Controller作为处理消息数据的类,首先为其创建一个基类,并让其余的类继承自该基类,基类BaseController定义为抽象类,具体实现如下。

抽象类(abstract)概念:https://blog.csdn.net/wzj0808/article/details/51388034

abstract class BaseController
    {
        RequestCode requestCode = RequestCode.None;
        public RequestCode RequestCode
        {
            get
            {
                return requestCode;
            }
        }
        public virtual string DefaultHandle(string data,Client client,Server server) //data是客户端发送的数据,因为是默认请求,因此不需要传递RequestCode和ActionCode,返回给客户端发送的数据,并且由于data可能要在服务端和客户端进行访问,因此加入服务端和客户端的引用
        {
            return null;
        }
    }

在实现Controller功能之前,我们在工程下新建一个项目,命名为Common,修改目标框架为.NET 2.0(考虑到Unity的兼容性),作为共享项目,用以处理服务端与客户端的共同请求,并添加两个类 RequestCode和ActionCode。当客户端与服务器端进行通信时,需要互相传递请求代码(RequestCode,决定请求类型)和命令代码(ActionCode,决定具体的请求)设置如下

我们在Controller中添加对Common的引用,如下

Client在调用Controller时需要与服务器进行通信,判断当前消息的请求方式,是数据库命令请求还是普通操作请求,如下图所示。

其中request为客户端向服务端发送的请求,这些请求信息包括数据长度、requestCode(每个请求对应一个独有的requestCode),ActionCode(对应的操作)以及数据,服务器端处理完这些信息后,某些信息(比如客户端的位置信息)只需要共享给其余客户端,不需要返回数据。对于需要返回的数据,内容包括数据长度、requestCode和数据。

我们在Controller中创建一个ControllerManager以管理所有的控制请求,代码如下  

   class ControllerManager
    {
        private Dictionary<RequestCode, BaseController> controllerDict = new Dictionary<RequestCode, BaseController>();    //RequestCode作为索引,访问BaseController
        public ControllerManager()
        {
            Init();
        }
        void Init()
        {
        }
    }

为了保证安全性,Client在调用ControllerManager功能时需要通过Server访问,即Server起到了一个中介的功能,因此Server应当要持有ControllerManager的引用,如下

using GameServer.Controller;
private ControllerManager ctrlManager = new ControllerManager();

ControllerManager中处理请求的方式如下
        public void HandleRequest(RequestCode requestCode, ActionCode actionCode,string data)    //RequestCode找到Controller,ActionCode找到Controller中的方法
        {
            BaseController baseCtrl;
            bool isSuccessGet = controllerDict.TryGetValue(requestCode, out baseCtrl);
            if (isSuccessGet == false)
            {
                Console.WriteLine("无法得到" + requestCode + "对应的Controller,读取失败"); //商业化的服务器一般使用日志记录
                return;
            }
            else  //在得到requestCode后,通过反射的方法调用actionCode方法
            {
                string methodName = Enum.GetName(typeof(ActionCode), actionCode);   //将actionCode转换成methodName方法
                MethodInfo mi = baseCtrl.GetType().GetMethod(methodName);   //得到方法信息
                if (mi == null)
                {
                    Console.WriteLine("warning:未能在baseCtrl[" + baseCtrl.GetType() + "]找到" + mi + "方法!");
                    return;
                }
                object[] paraData = new object[]{data};
                object o = mi.Invoke(baseCtrl, paraData); //在baseCtrl中调用mi方法,mi方法中的参数为paraData
            }
        }

2.客户端对消息进行解析并传递给ControllerManager

目前的Client在接收到消息时采用的是msg.ReadMsg(count)进行处理,现有的ReadMsg只是将信息读取并输出到控制台,需要改进。即将消息解析后通过ControllerManager调用具体的Controller进行处理。

需要解析的数据有数据长度、requestCode、actionCode和数据,因此将ReadMsg()函数修改为      

 public void ReadMsg(int newDataAmount)   //完成一条消息的解析
        {
            startIdx += newDataAmount;
            while (true)
            {
                if (startIdx <= 4) return;   //小于4表示信息不完整,不解析,继续接收下一条
                int count = BitConverter.ToInt32(data, 0);  //读取数据长度,0表示startIdx的位置
                if ((startIdx - 4) >= count)
                {
                    RequestCode reqCode = (RequestCode)BitConverter.ToInt32(data, 4);   //从4的位置开始解析,解析出RequestCode
                    ActionCode actionCode = (ActionCode)BitConverter.ToInt32(data, 8); //解析出ActionCode
                    string s = Encoding.UTF8.GetString(data, 12, count - 8);  //读取的长度需要扣除RequestCode和ActionCode
                    Array.Copy(data, count + 4, data, 0, startIdx - 4 - count); //接收完数据后将其余数据往前移,便于下一次解析
                    startIdx -= (count + 4);    //更新startIdx
                }
                else
                {
                    break;
                }
            }
        }

并在Server中进行消息处理   

    public void HandleRequest(RequestCode requestCode, ActionCode actionCode, string data, Client client)
        {
            ctrlManager.HandleRequest(requestCode, actionCode, data, client);
        }

 

3.数据打包并发送到客户端

数据打包的功能在Message类中实现,

public static byte[] PackData(RequestCode requestData,string data)
        {
            byte[] requestCodeByte = BitConverter.GetBytes((int)requestData);   //获取个部分的数据长度并进行拼接
            byte[] dataBytes = Encoding.UTF8.GetBytes(data);
            int dataAmount = requestCodeByte.Length + dataBytes.Length;
            byte[] dataAmountBytes = BitConverter.GetBytes(dataAmount);
            dataAmountBytes.Concat(requestCodeByte).Concat(dataBytes);
            return dataAmountBytes; //error
        }

Client通过一下代码完成数据发送    

  public void Send(RequestCode requestCode,string data)
        {
            byte[] bytes = Message.PackData(requestCode, data);
            clientSocket.Send(bytes);
        }

之后可以通过Server中的SendResponse()给客户端响应,   

 public void SendResponse(Client client,RequestCode requestCode,string data)
        {
            client.Send(requestCode, data);
        }

SendResponse()的调用是在Controller处理完请求之后进行调用,至此对消息的解析与发送就完成了。

4.Conn与数据库的连接操作

再回顾下整体的服务器架构图,蓝框部分已经开发地差不多了,接下来解决ConnHelper,即与数据库的连接操作。

在ConnHelper文件夹下创建Conn类,用以处理与数据库的连接

代码如下   

  class Conn
    {
        public const string CONNECTIONSTRING = "datasource=127.0.0.1;port=3306;database=MyGame;user=root;pwd=Syj1993..;";
        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 Close(MySqlConnection conn)
        {
            if (conn != null)
            {
                conn.Close();
            }
            else
            {
                Console.WriteLine("MySqlConnection不能为空");
            }
        }
    }

创建之后,在Client中进行使用即可。

以上部分就是大体上的服务器逻辑,至于具体与U3D客户端的信息交互与功能完善会在之后的学习中进行,相应代码也会在之后进行优化。

猜你喜欢

转载自blog.csdn.net/s1314_jhc/article/details/81152269