注:本文仅用于在博客园学习分享,还在随着项目不断更新和完善中,多有不足,暂谢绝各平台或我的的转载和推广,感谢支持。python
《码神联盟》是一款为技术人作的开源情怀游戏,每一种编程语言都是一位英雄。客户端和服务端均使用C#开发,客户端使用Unity3D引擎,数据库使用MySQL。这个MOBA类游戏是笔者在学习时期和客户端美术策划的小伙伴一块儿作的游戏,笔者主要负责游戏服务端开发,客户端也参与了一部分,同时也是这个项目的发起和负责人。此次主要分享这款游戏的服务端相关的设计与实现,从总体的架构设计,到服务器网络通讯底层的搭建,通讯协议、模型定制,再到游戏逻辑的分层架构实现。同时这篇博客也沉淀了笔者在游戏公司实践五个月后对游戏架构与设计的从新审视与思考。git
这款游戏自去年完成后笔者曾屡次想写篇博客来分享,也曾屡次停笔,只因总以为灵感还不够积淀还不够思考还不够,如今终于能够跨过这一步和你们分享,但愿能够带来的是干货与诚意满满。因为目前关于游戏服务端相关的介绍文章少之又少,而为数很少的几篇也都是站在游戏服务端发展历史和架构的角度上进行分享,不多涉及具体的实现,这篇文章我将尝试多从实现的层面上加以介绍,所附的代码均有详尽注释,篇幅较长,能够关注收藏后再看。学习时期作的项目可能没法达到工业级,参考了github上开源的C#网络框架,笔者在和小伙伴作这款游戏时农药尚未如今这般火。 : ) github
上图为这款游戏的服务器架构和主要逻辑流程图,笔者将游戏的代码实现分为三个主要模块:Protocol通讯协议、NetFrame服务器网络通讯底层的搭建以及LOLServer游戏的具体逻辑分层架构实现,下面将针对每一个模块进行分别介绍。sql
先从最简单也最基本的通讯协议部分提及,咱们能够看到这部分代码主要分为xxxProtocol、xxxDTO和xxxModel、以及xxxData四种类型,让咱们来对它们的做用一探究竟。docker
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协议部分,咱们主要是定义了一些常量用于模块通讯,在这个部分分别定义了用户协议、登陆协议、战斗匹配协议、战斗选人协议以及战斗协议。编程
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; } } }
这部分的实现主要是为了将程序功能与属性配置分离,后面能够由策划来配置这部份内容,由导表工具自动生成配表,从而减轻程序的开发工做量,扩展游戏的功能。数组
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;//拥有技能 } }
这部分为服务器的网络通讯底层实现,也是游戏服务器的核心内容,下面将结合具体的代码以及代码注释一一介绍底层的实现,可能会涉及到一些C#的网络编程知识,对C#语言不熟悉不要紧,笔者对C#的运用也仅仅停留在使用阶段,只需经过C#这门简单易懂的语言来窥探整个服务器通讯底层搭建起来的过程,来到咱们的NetFrame网络通讯框架,这部分干货不少,我将用完整的代码和详尽的注释来阐明其意。
将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; } } }
同时封装了一个消息封装的方法,收到消息的处理流程如图所示:
序列化: 将数据结构或对象转换成二进制串的过程。
反序列化:将在序列化过程当中所生成的二进制串转换成数据结构或者对象的过程。
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; } } }
相应的,咱们利用上面写好的序列化和反序列化方法将咱们再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; } } }
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; } } }
粘包出现缘由:在流传输中出现(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; } } }
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); }
SocketAsyncEventArgs是微软提供的高性能异步Socket实现类,主要为高性能网络服务器应用程序而设计,主要是为了不在在异步套接字 I/O 量很是大时发生重复的对象分配和同步。使用此类执行异步套接字操做的模式包含如下步骤:
(1)分配一个新的 SocketAsyncEventArgs 上下文对象,或者从应用程序池中获取一个空闲的此类对象。
(2)将该上下文对象的属性设置为要执行的操做(例如,完成回调方法、数据缓冲区、缓冲区偏移量以及要传输的最大数据量)。
(3)调用适当的套接字方法 (xxxAsync) 以启动异步操做。
(4)若是异步套接字方法 (xxxAsync) 返回 true,则在回调中查询上下文属性来获取完成状态。
(5)若是异步套接字方法 (xxxAsync) 返回 false,则说明操做是同步完成的。能够查询上下文属性来获取操做结果。
(6)将该上下文重用于另外一个操做,将它放回到应用程序池中,或者将它丢弃。
获取或设置与此异步套接字操做关联的用户或应用程序对象。
命名空间: 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); } } } }
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; } } } }
在这里咱们定义了客户端链接、收到客户端消息和客户端断开链接的抽象类,标记为抽象或包含在抽象类中的成员必须经过从抽象类派生的类来实现。
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); } }
接下来具体实现客户端链接、断开链接以及收到消息后的协议分发到具体的逻辑处理模块,代码以下:
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; } } } }
写到这里,服务器终于能够启来了,无论你激不激动,反正坐在这里写写画画了一天我是激动了,总算要大功告成了。 : )
启动服务器->监听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(); } } } } }
至此,服务器的通讯底层已经搭建完毕,能够进一步进行具体的游戏逻辑玩法开发了。
逻辑处理主要分层架构以下:
(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等;
逻辑处理流程以下:
思考了一些优化思路,自文章发布后也收到了许多来自朋友圈或留言评论中大神们给出的优化思路,大多数建议都质量很高,极具参考价值和学习意义,大概这就是开源的魅力所在吧。如今把这些思路整理出来分享给你们:
(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框架性能、安全性会更好。
好了,这篇文章就分享到这里,从项目的制做周期,到沉淀积累,到重构设计,到总结与反思,到怎么把整个架构的设计与实现分享出来,再到写出一篇文章确实经历了很长的一段时间,有些图因为比较长常常须要把电脑屏幕来回旋转绘制,但愿对您有所帮助,感谢阅读,篇幅有限不能一一详述,若有问题或改进优化建议欢迎留言讨论。下一篇可能会开始剖析开源MMORPG游戏服务端引擎KBEngine的源码,也可能写C++或python相关,若是您也对这些内容感兴趣,或者对笔者感兴趣,能够继续关注个人后续文章。^_^
上篇博客后有很多小伙伴给笔者发了私信,大可能是技术生涯的一些迷茫与选择,最后有一句笔者很喜欢的话分享给你们:衡量一我的才的标准,在于一我的在有限的时间内所展示出来的成长速度。持续学习,持续进步,持续成长,才能持续幸运,持续实现价值。
Changelog:
2017-07-22 21:36 发布
2017-07-24 13:37 更新优化思路