游戏服务器学习

游戏服务器学习

十一月 06, 2022

游戏大厅

棋牌类大厅

MMORPG大厅

大厅功能

(1)服务器启动,等待客户端连入

(2)维护一个当前所有连入的客户端的数据列表,数据内容包含IP地址,名字等。

(3)如果客户端创建了一个游戏服务(房间),则把该游戏服务加入到一个列表中,并向其他客户端刷新列表数据,触发该客户端成为游戏的主机。
(4)当有客户端选择加入某个游戏服务时,帮助它连接到主机。

(5)维护当前所有游戏服务的参加游戏人数、游戏是否已经开始等相关信息,这些信息由服务器向建立服务的主机玩家获取。
(6)当一场游戏结束后,服务器从主机玩家那里获取游戏结果和分数等信息,并更新排行榜。

服务器热更

当引用无依赖的库

Dll生成库代码

1
2
3
4
5
6
7
8
9
using System;

namespace ClassLibrary1 {
public class Class1 {
public void add() {
Console.WriteLine("uuuuuuuuu");
}
}
}

服务器部分
CodeLoader.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;

namespace MMONetworkServer {
class CodeLoader {
AssemblyLoadContext assemblyLoadContext;
Assembly hotfix;
public void Reload() {
assemblyLoadContext?.Unload();
GC.Collect();
assemblyLoadContext = new AssemblyLoadContext("ClassLibrary1", true);
byte[] dllBytes = File.ReadAllBytes(@"F:\project\VSProject\ClassLibrary1\bin\Debug\netcoreapp3.1\ClassLibrary1.dll");//加载dll
byte[] pdbBytes = File.ReadAllBytes(@"F:\project\VSProject\ClassLibrary1\bin\Debug\netcoreapp3.1\ClassLibrary1.pdb");//加载pdb
hotfix = assemblyLoadContext.LoadFromStream(new MemoryStream(dllBytes), new MemoryStream(pdbBytes));
object obj = hotfix.CreateInstance("ClassLibrary1.Class1");
MethodInfo mm = obj.GetType().GetMethod("add");
mm.Invoke(obj, null);
}
}
}

Main.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;

namespace MMONetworkServer {
class Program {
static void Main(string[] args) {

CodeLoader codeLoader = new CodeLoader();
while(true) {
Console.ReadLine();
codeLoader.Reload();
}
}
}
}

运行后修改add函数然后生成新的dll,让main重载。

运用到游戏,思路逻辑写入热更项目中,非热更部分做好接口,通过反射创建实例。

思路2

让热更项目依赖于总项目,然后总项目调用热更新部分时使用反射来获取相关实例。
CodeLoader.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using System;

using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;

namespace MMONetworkServer.Core {
public class CodeLoader {
AssemblyLoadContext assemblyLoadContext;
Assembly hotfix;
Dictionary<string, object> hotfixInstance = new Dictionary<string, object>();
public static CodeLoader instance ;

public CodeLoader() {

instance = this;

}
public static CodeLoader GetInstance() {
return instance;
}
public void Reload() {
hotfixInstance.Clear();
assemblyLoadContext?.Unload();
GC.Collect();
assemblyLoadContext = new AssemblyLoadContext("ClassLibrary1", true);
byte[] dllBytes = File.ReadAllBytes(@"F:\project\VSProject\ClassLibrary1\bin\Debug\netcoreapp3.1\ClassLibrary1.dll");//加载dll
byte[] pdbBytes = File.ReadAllBytes(@"F:\project\VSProject\ClassLibrary1\bin\Debug\netcoreapp3.1\ClassLibrary1.pdb");//加载pdb
hotfix = assemblyLoadContext.LoadFromStream(new MemoryStream(dllBytes), new MemoryStream(pdbBytes));
//object obj = hotfix.CreateInstance("ClassLibrary1.Class1");
//MethodInfo mm = obj.GetType().GetMethod("add");
//mm.Invoke(obj, null);

foreach(Type type in hotfix.GetTypes()) {
object instance = Activator.CreateInstance(type);
hotfixInstance.Add(type.FullName, instance);
Console.WriteLine(type.FullName);
}
}
public object Find(string className) {
return hotfixInstance[className];
}
public void FindFunRun(string className,string funName,object[] objs) {
MethodInfo mm = Find(className).GetType().GetMethod(funName);
mm.Invoke(Find(className), objs);
}

}
}

当总项目使用热更部分时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private void HandleMsg(Conn conn, ProtocolBase protoBase) {
string name = protoBase.GetName();
string methodName = "Msg" + name;
//连接协议分发
if (conn.player == null || name == "HeatBeat" || name == "Logout") {
MethodInfo mm = CodeLoader.GetInstance().Find("MMONetworkServer.Logic.HandleConnMsg").GetType().GetMethod(methodName);
if (mm == null) {
string str = "[警告]HandleMsg没有处理连接方法 ";
Console.WriteLine(str + methodName);
return;
}
Object[] obj = new object[] { conn, protoBase };
Console.WriteLine("[处理连接消息]" + conn.GetAdress() + " :" + name);
mm.Invoke(CodeLoader.GetInstance().Find("MMONetworkServer.Logic.HandleConnMsg"), obj);
}
//角色协议分发
else {
MethodInfo mm = CodeLoader.GetInstance().Find("MMONetworkServer.Logic.HandlePlayerMsg").GetType().GetMethod(methodName);
if (mm == null) {
string str = "[警告]HandleMsg没有处理玩家方法";
Console.WriteLine(str + methodName);
return;
}
Object[] obj = new object[] { conn.player, protoBase };
Console.WriteLine("[处理玩家消息]" + conn.player.id + " :" + name);
mm.Invoke(CodeLoader.GetInstance().Find("MMONetworkServer.Logic.HandlePlayerMsg"), obj);
}
}

事件部分因为依赖于总项目所以能直接编译。

对于反射部分的优化

使用委托来完成。学习渠道参考文章 性能测试

反射

1
2
3
4
5
6
7
8
//HandleConnMsg
MethodInfo mm = CodeLoader.GetInstance().Find("MMONetworkServer.Logic.HandleConnMsg").GetType().GetMethod(methodName);
Object[] obj = new object[] { conn, protoBase };
mm.Invoke(CodeLoader.GetInstance().Find("ServerLoginHotfix.HandleConnMsg"), obj);
//HandlePlayerMsg
MethodInfo mm = CodeLoader.GetInstance().Find("MMONetworkServer.Logic.HandlePlayerMsg").GetType().GetMethod(methodName);
Object[] obj = new object[] { conn.player, protoBase };
Console.WriteLine("[处理玩家消息]" + conn.player.id + " :" + name);

委托

1
2
3
4
5
6
7
8
//HandleConnMsg
MethodInfo mm = CodeLoader.GetInstance().Find("MMONetworkServer.Logic.HandleConnMsg").GetType().GetMethod(methodName);
Action<Conn, ProtocolBase> updateDel = (Action<Conn,ProtocolBase>)Delegate.CreateDelegate(typeof(Action<Conn, ProtocolBase>), null, mm);
updateDel(conn, protoBase);
//HandlePlayerMsg
MethodInfo mm = CodeLoader.GetInstance().Find("MMONetworkServer.Logic.HandleConnMsg").GetType().GetMethod(methodName);
Action<IPlayer, ProtocolBase> updateDel = (Action<IPlayer, ProtocolBase>)Delegate.CreateDelegate(typeof(Action<IPlayer, ProtocolBase>), null, mm);
updateDel(conn.player, protoBase);

比较

运行速度

虽然上面参考文章上写着委托比反射快,但我测试数据如下

反射 委托
[处理玩家消息] Time: 00:00:00.188 3370 [处理玩家消息] Time: 00:00:00.188 6885
[处理玩家消息] Time: 00:00:00.022 5805 [处理玩家消息] Time: 00:00:00.023 2066
[处理玩家消息] Time: 00:00:00.010 5767 [处理玩家消息] Time: 00:00:00.006 4742
[处理玩家消息] Time: 00:00:00.000 7305 [处理玩家消息] Time: 00:00:00.000 8893
[处理玩家消息] Time: 00:00:00.005 2011 [处理玩家消息] Time: 00:00:00.005 4224
[处理玩家消息] Time: 00:00:00.000 6916 [处理玩家消息] Time: 00:00:00.000 7680

可能是因为我在codeloader读取数据集时将所有类进行了实例化,因此加快了反射的速度。

内存开销

委托会比反射造成内存开销小,因为舍去了将运行参数装箱拆箱的过程,减少了内存gc。

热更类的变化

  1. 委托方法调用方法及方法内使用的变量和参数的逻辑类将进行静态处理否则将会委托绑定不到具体对象。例如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public partial class HandleConnMsg {

    static HandlePlayerEvent handlePlayerEvent = new HandlePlayerEvent();
    static LogicManager logicManager;
    public static void MsgHeatBeat(Conn conn, ProtocolBase protoBase) {
    conn.lastTickTime = Sys.GetTimeStamp();
    Console.WriteLine("[更新心跳时间]" + conn.GetAdress());

    }
    }
  2. 委托的具体参数一定要一致,不能为某个类的子类,否则会委托与具体函数签名不一致。

    1
    2
    3
    4
    5
    public partial class HandlePlayerMsg {
    public static void MsgWWWW(IPlayer iplayer, ProtocolBase protoBase) {
    Console.WriteLine("wwwwwww");
    }
    }

    ​ 反射可以直接写为player,但委托要与委托变量一致

数据库

Mysql数据库二进制文件的方式

在Mysql存储二进制数据那一栏选Blob

1
2
3
4
5
string formatStr = "update player set data =@data,ip ='{0}' where id = '{1}';";
string cmdStr = string.Format(formatStr,ip, id);
MySqlCommand cmd = new MySqlCommand(cmdStr, sqlConn);
cmd.Parameters.Add("@data", MySqlDbType.Blob);
cmd.Parameters[0].Value = playerStream;

playerStream为二进制数据文件