单线程服务端框架
一、架构消息处理流程
- 服务端开启监听,利用多路复用select(epoll)监听客户端请求。
- select(epoll)收到socket消息,如果触发的socketfd是监听socket,那么新创建一个state(每个state对应一个客户端),并加入stateList中。如果是已建立了通信的socket客户端发来消息,然后解析消息,根据消息取出协议,根据协议通过反射获取协议对应的处理函数,调用处理函数。
- 在协议处理函数中处理完逻辑后,一般会Send()消息会给客户端。
二、游戏序列化
服务端程序会有两处功能涉及类的序列化。
- 与客户端信息交互(send、receive)需要编码和和解码
- 玩家数据保存(序列化成bytes保存到数据库)
三、框架模块
分为4个部分:
- 处理select(epoll)多路复用的网络管理器NetManager,它是服务端网络模块的核心部件
- 定义客户端信息的ClientState类。每一个客户端连接对应一个ClientState对象,含有与客户端连接的套接字socket和读缓冲区readBuff。
- 处理网络消息的MsgHandle类。
- 事件处理类EventHandler。
程序入口
using System;
namespace Game
{
class MainClass
{
public static void Main(string[] args)
{
NetManager.StartLoop(8888);
}
}
}
1.NetManager
public static void StartLoop(int listenPort)
{
//Socket
listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//Bind
IPAddress ipAdr = IPAddress.Parse("0.0.0.0");
IPEndPoint ipEp = new IPEndPoint(ipAdr, listenPort);
listenfd.Bind(ipEp);
//Listen
listenfd.Listen(5);
Console.WriteLine("[服务器]启动成功");
//循环
while (true)
{
//重置CheckRead
ResetCheckRead();
//Select
Socket.Select(checkRead, null, null, 1000);
//检查可读对象
for (int i = checkRead.Count -1; i >= 0; i--) {
Socket s = checkRead[i];
if (s == listenfd) {
ReadListenfd(s);
}
else {
ReadClientfd(s);
}
}
}
}
//填充checkRead列表
public static void ResetCheckRead() {
checkRead.Clear();
checkRead.Add(listenfd);
foreach(ClientState s in clients.Values) {
checkRead.Add(s.socket);
}
}
1.2 处理监听事件方法
ReadListenfd会调用Accept接收客户端连接,然后新建一个客户端信息对象state,把它存入客户端信息列表clients。
public static void ReadListenfd(Socket listenfd)
{
try {
Socket clientfd = listenfd.Accept();
Console.WriteLine("Accept " + clientfd.RemoteEndPoint.ToString());
state.socket = clientfd;
clients.Add(clientfd, state);
}
catch (SocketException ex) {
Console.WriteLine("Accept fail" + ex.ToString());
}
}
1.3 处理客户端消息
public static void ReadClientfd(Socket clientfd)
{
ClientState state = clients[clientfd];
ByteArray readBuff = state.readBuff;
//接收
int count = 0;
//缓冲区不够,清除,若依然不够,只能返回
//缓冲区长度只有1024,单条协议超过缓冲区长度时会发生错误,根据需要调整长度
if (readBuff.remain <= 0) {
OnReceiveData(state);
readBuff.MoveBytes();
}
if (readBuff.remain <= 0) {
Console.WriteLine("Receive fail, maybe msg length > buff capacity");
Close(state);
return;
}
try {
//接收消息
count = clientfd.Receive(readBuff.bytes, readBuff.writeIdx, readBuff.remain, 0);
}
catch (SocketException ex) {
Console.WriteLine("Receive SocketException " + ex.ToString());
Close(state);
return;
}
//客户端关闭
if (count <= 0) {
Console.WriteLine("Socket Close " + clientfd.RemoteEndPoint.ToString());
Close(state);
return;
}
//消息处理
readBuff.wirteIdx += count;
//处理二进制消息
OnReceiveData(state);
//移动缓冲区
readBuff.CheckAndMoveBytes();
}
1.4.处理协议
它会先判断读缓冲区的数据是否足够长,如果条件满足则调用MsgBase.DecodeName和MsgBase.Decode解析出协议名和协议体。最后做消息分发(利用反射获取MsgHandler类中对应的处理方法),即调用MsgHandler类名为protoName(例如:MsgHandler::MsgMove、MsgHandler::MsgPing)的方法。
//处理二进制消息
public static void OnReceiveData(ClientState state)
{
ByteArray readBuff = state.readBuff;
byte[] bytes = readBuff.bytes;
//消息长度
if (readBuff.length < 2) {
return;
}
Int16 bodyLength = (Int16)((bytes[readIdx+1] << 8) | bytes[readIdx]);
//消息体
if (readBuff.length < bodyLength) {
return;
}
readBuff.readIdx += 2;
//解析协议名
int nameCount = 0;
string protoName = MsgBase.DecodeName(readbuff.bytes, readBuff.readIdx, out nameCount);
if (protoName == "") {
Console.WriteLine("OnReceiveData MsgBase.DecodeName fail");
Close(state);
}
readBuff.readIdx += nameCount;
//解析协议体
int bodyCount = bodyLength - nameCount;
MsgBase msgBase = MsgBase.Decode(protoName, readBuff.bytes, readBuff.readIdx, bodyCount);
readBuff.readIdx += bodyCount;
readBuff.CheckAndMoveBytes();
//分发消息
MethodInfo mi = typeof(MsgHandler).GetMethod(protoName); //MethodInfo类对象mi包含它所指代的方法的所有信息,通过这个类可以得到方法的名称、参数、返回值等,并且可以调用它。假设所有消息处理方法都定义在MsgHandler类中,且都是静态方法,通过typeof(MsgHandler).GetMethod(funName)便能够获取MsgHandler类中的名为funName的静态方法。由于MethodInfo定义于System.Reflection命名空间下,因此需要引用(using)该命名空间。
object[] o = {state, msgBase};
Console.WriteLine("Receive: " + protoName);
if (mi != null) {
mi.Invoke(null, o); //mi.Invoke(null, o)代表调用mi所包含的方法。第一个参数null代表this指针,由于消息处理方法都是静态方法,因此此处要填null。第二个参数o代表的是参数列表。这里定义的消息处理函数都有两个参数,第一个参数是客户端状态state,第二个参数是消息的内容msgArgs。
}
else {
Console.WriteLine("OnReceiveData Invoke fail " + protoName);
}
//继续读取消息
if (readBuff.length > 2) {
OnReceiveData(state);
}
}
2.MsgHandler
C#中partical修饰的类,表明类是局部类型,它允许我们将一个类、结构或接口分成几个部分,分别实现在几个不同的.cs文件中。
BattleMsgHandler.cs
public partical class MsgHandler {
public static void MsgMove(ClientState c, MsgBase msgBase) {
MsgMove msgMove = (MsgMove)msgBase;
Console.WriteLine(msgMove.x);
msgMove.x++;
NetManager.Send(c, msgMove);
}
}
SysMsgHandler.cs
public partical class MsgHandler {
public static void MsgPing(ClientState c, MsgBase msgBase) {
Console.WriteLine("MsgPing");
c.lastPingTime = NetManager.GetTimeStamp();
MsgPong msgPong = new MsgPong();
NetManager.Send(c, msgPong);
}
}
3.EventHandler
using System;
public partial class EventHandler
{
public static void OnDisconnect(ClientState c)
{
Console.WriteLine("Close");
}
public static void OnTimer() {}
}