前言
网络游戏涉及客户端和服务端。服务端程序记录玩家数据,处理客户端发来的协议。本文就介绍一套通用客户端的实现。
该框架基于Select多路复用处理网络消息,具有粘包半包处理、心跳机制等功能,还是用MySQL数据库存储玩家数据,是一套功能较完备的C#服务端程序。一般单个服务端进程可以承载数百名玩家,如果更多就需要改为分布式架构。
7.1服务端架构
服务端两大核心是处理客户端的消息和存储玩家数据。
客户端与服务端通过TCP连接,使两者可以传递数据,服务端还连接着MySQL数据库,可将玩家数据保存到数据库中。
7.1.2模块划分
- 逻辑层:消息处理,事件处理,存储结构。
- 网络底层:连接客户端,消息处理,事件处理
- 数据库底层:连接数据库,存储结构。
7.2Json编码解码
和客户端不同的是,因为服务端程序和Unity无关,无法使用JsonUtility,所以改用System.Web提供的方法实现。(需要手动引用System.web.Extensions)

新建一个控制台程序,将协议文件全部放在proto下。
7.2.3修改MsgBase
引入System.Web.Script.Serialization头文件后,和JsonUtility调用方法不同。因为JavaScriptSerializer不是静态类,需要定义一个该类型的对象,再让对象调用Serialize和Deserialize方法进行编码解码
7.3网络模块
7.3.1整体架构
- 协议解析
- 处理select多路复用的网络管理器NetManager(核心)
- 定义客户端信息ClientState类
- 处理网络消息的MsgHandler类,但是把不同消息类型的处理分拆到多个文件中。(BattleMsgHandler处理战斗协议、SysMsgHandler处理ping、pong协议)
- 事件处理类EventHandler
7.3.2ClientState
客户端信息,每一个客户端连接对应一个ClientState,含有与客户端连接的套接字socket和读缓冲区readBuff,以及对应的玩家数据和最后一次收到ping的时间。
using System.Net.Sockets;
public class ClientState
{
public Socket socket;
public ByteArray readBuff = new ByteArray();
//Ping
public long lastPingTime = 0;
//玩家
public Player player;
}
7.3.3开启监听和多路复用
客户端和服务端的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(0);
Console.WriteLine("[服务器]启动成功");
//循环
while (true)
{
ResetCheckRead(); //重置checkRead
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);
}
}
//超时
Timer();
}
}
服务端开启了端口监听后,进入循环。针对Select返回的列表,程序遍历它判断有新的客户端连接还是某个客户端发来消息,然后分别调用处理函数ReadListenfd和ReadClientfd。
当程序在Select有可读事件和超时都会调用Timer,空闲状态每秒调用一次。
处理监听消息以及处理客户端消息和前面写的都差不多,就不详细介绍了。
7.4心跳机制
我们在前面的clientstate已经加入了lastpingtime,注意是long,游戏客户端只运行几小时,unity提供的Time.time即可记录。但是服务端可能运行纪念,所以要用long保存。
7.4.2时间戳
时间戳是一种记录时间的方法,也就是1970年1月1日零点到现在的秒数。
//获取时间戳
public static long GetTimeStamp()
{
TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToInt64(ts.TotalSeconds);
}
7.4.3回应MsgPing协议
更新lastPingTime并回应
using System;
public partial 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);
}
}
7.4.4超时处理
遍历客户端连接,太久没收到就断开连接,并删除clients列表元素。注意这是在遍历clients,删除后再遍历会出错,所以直接break。每次checkping最多断开一个连接。
在ontimer中调用,ontimer是在timer里通过反射调用,timer在startloop里调用
public static void OnTimer()
{
CheckPing();
}
//Ping检查
public static void CheckPing()
{
//现在的时间戳
long timeNow = NetManager.GetTimeStamp();
//遍历,删除
foreach (ClientState s in NetManager.clients.Values)
{
if (timeNow - s.lastPingTime > NetManager.pingInterval * 4)
{
Console.WriteLine

最低0.47元/天 解锁文章
1175

被折叠的 条评论
为什么被折叠?



