【Unity】MMORPG游戏开发(一)身份认证
更新日期:2025年10月13日。
项目源码:获取源码。
索引
- MMORPG身份认证
- 一、客户端连接服务端
- 1.服务端监听客户端连接
- 2.客户端连接服务端
- 二、MMORPG网络通信协议
- 三、身份认证
- 1.为玩家分配身份ID
- ①.消息定义
- ②.服务端分配身份ID
- ③.客户端回复基础信息
MMORPG身份认证
前面几章讲到了uNet
的一些基础特性,从此篇开始,我们正式进入MMORPG游戏开发的进程,由于在网络游戏中很多东西必须保持服务端与客户端的一致性,所以后续很多模块的讲解我会同时引用服务端与客户端的代码。
首先是身份认证
模块,这是客户端与服务端通信的基础。
一、客户端连接服务端
1.服务端监听客户端连接
通过之前的博客uNet游戏服务端框架可知,服务端的启动代码如下:
namespace uNet.Example.MMO
{class Program{static void Main(string[] args){//定义服务端配置信息,比如监听的IP地址、端口号,玩家类型(继承至NetworkPlayer,同一游戏服务端中只能存在一种玩家类型)ServerConfig config = new ServerConfig("127.0.0.1", 11000, typeof(MMO_Player), 10, 20);//创建服务端入口ServerEntry entry = new ServerEntry(config);//创建一个地图(比如这里是初始地图:暴风要塞)entry.CreateMap<MMO_BeginnerMap>();//启动服务端entry.Start();//启用常规日志的打印显示Log.IsEnableInfo = true;//按下ESC键退出程序ConsoleKeyInfo consoleKeyInfo = Console.ReadKey();while (consoleKeyInfo.Key != ConsoleKey.Escape){consoleKeyInfo = Console.ReadKey();}}}
}
服务端启动后,会启用一个TCPListener
用于监听客户端的连接:
namespace uNet.Core.Entry
{/// <summary>/// 服务器入口/// </summary>public sealed class ServerEntry{/// <summary>/// 服务器入口/// </summary>/// <param name="config">服务器配置</param>public ServerEntry(ServerConfig config){Current = this;_config = config;//创建监听器,当有客户端连接时,回调CreatePlayer方法用于创建网络玩家_tcpListener = new TCPListener(_config.IP, _config.Port, CreatePlayer);_cancel = new CancellationTokenSource();}/// <summary>/// 启动服务器/// </summary>public void Start(){_tcpListener.ListenAsync();Log.LogWarning("服务已启动。");}}
}
ListenAsync
会使用一个永久轮询来监听客户端的连接(直到被取消):
namespace uNet.Core.Listener
{/// <summary>/// TCP协议监听器(监听TCP连接)/// </summary>internal sealed class TCPListener : IDisposable{/// <summary>/// 监听IP地址/// </summary>public string IP { get; init; }/// <summary>/// 监听端口号/// </summary>public int Port { get; init; }private Socket _socket;private CancellationTokenSource _cancel;private Action<Socket>? _onClientConnect;/// <summary>/// TCP协议监听器(监听TCP连接)/// </summary>/// <param name="ip">服务器IP地址</param>/// <param name="port">端口号</param>/// <param name="onClientConnect">当客户端连接时回调</param>public TCPListener(string ip, int port, Action<Socket> onClientConnect){IP = ip;Port = port;_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);_socket.Bind(new IPEndPoint(IPAddress.Parse(IP), Port));//设置socket为监听模式,此后该socket将只能用于监听连接_socket.Listen();_cancel = new CancellationTokenSource();_onClientConnect = onClientConnect;}/// <summary>/// 开始监听/// </summary>public async void ListenAsync(){while (!_cancel.IsCancellationRequested){try{Socket client = await _socket.AcceptAsync(_cancel.Token).ConfigureAwait(false);//当有客户端连接时,触发回调方法_onClientConnect?.Invoke(client);}catch (Exception e){Log.LogError($"Socket监听出现异常: {e.Message}");}}}/// <summary>/// 销毁/// </summary>public void Dispose(){_cancel.Cancel();_cancel.Dispose();try{_socket.Shutdown(SocketShutdown.Both);}catch (Exception){ }_socket.Close();_onClientConnect = null;}}
}
2.客户端连接服务端
在此处我们将跳过输入账号密码登录
的过程,因为登录过程的业务不涉及到游戏核心服务端任何业务(比如网络同步、心跳验证),只需另一个专属的登录服务器
提供一个登录API
(HTTP协议即可),客户端调用该API验证登录,如果登录成功,再令客户端调用如下代码连接至核心服务端:
Main.m_Network.ConnectServer<TcpChannel>();
在此之前需要在客户端的Network
模块的检视器面板设置服务端的IP地址及端口号(比如启动服务端时传入的127.0.0.1
和11000
):
二、MMORPG网络通信协议
MMORPG
的网络通信协议可在以下路径查看,这个协议严格定义了每种消息类型的双端通信格式:
点击其中橙色的脚本链接
,可以直接定位到该消息的脚本定义。
三、身份认证
身份认证
相关的协议为主命令:0
。
1.为玩家分配身份ID
其中,主命令:0,子命令:0
的消息类型为:为玩家分配身份ID。
为玩家分配身份ID的时机在玩家成功连接服务端之后,由服务端为其分配身份ID,并告知该客户端,后续的所有网络数据交互都将以该身份ID为唯一标识符。
因为服务端只需将身份ID告知客户端即可,所以服务端需要发送的消息体为null(身份ID包含在消息头中)。
这里由于未涉及到真实的玩家注册流程(玩家注册后应有玩家姓名
和角色
等信息存入数据库),只是在客户端提供了一个界面让玩家输入自己的姓名和选择角色来模拟注册,所以姓名和角色等信息需要客户端告知服务端,这即是此消息客户端需要回复服务端的内容MMOData_PlayerBaseInfo
。
客户端模拟注册界面:
①.消息定义
MMOData_PlayerBaseInfo
的定义如下(在服务端和客户端该消息的定义是一模一样的,且ProtoMember
标记的顺序一致,以使得protobuf
能够正确序列化和反序列化数据):
/// <summary>/// 网络数据:玩家基础信息/// </summary>[ProtoContract]internal class MMOData_PlayerBaseInfo : IObjectPoolable{/// <summary>/// 玩家ID/// </summary>[ProtoMember(1)]public long ID { get; set; }/// <summary>/// 玩家姓名/// </summary>[ProtoMember(2)]public string? Name { get; set; }/// <summary>/// 玩家角色类型/// </summary>[ProtoMember(3)]public int Role { get; set; }public void Dispose(){ID = 0;Name = null;Role = 0;}}
②.服务端分配身份ID
服务端分配身份ID的时机是在玩家成功连接服务端之后,在TCPListener
中玩家成功连接服务端之后会通过回调创建一个网络玩家实例,而该实例的ID会在Awake
时通过自增获取(这即是身份ID):
namespace uNet.Core.Entry
{/// <summary>/// 服务器入口/// </summary>public sealed class ServerEntry{/// <summary>/// 创建网络玩家/// </summary>/// <param name="socket">网络玩家持有的socket</param>public void CreatePlayer(Socket socket){if (socket == null)return;NetworkPlayer? networkPlayer = SpawnObjectFromPool(_config.NetworkPlayerType) as NetworkPlayer;if (networkPlayer != null){networkPlayer.Awake(socket, _config.HeartbeatInterval, _config.HeartbeatTimeout);lock (_lockObj){_allPlayers.Add(networkPlayer);}}}}
}
所以此时只需要将身份ID发送给客户端即可,而NetworkPlayer
有一个专用于接收并处理客户端消息的方法,该方法在Awake
后便会启动,且为抽象方法需要子类重写:
namespace uNet.Core.Player
{/// <summary>/// 网络玩家/// </summary>public abstract class NetworkPlayer : IObjectPoolable{/// <summary>/// 初始化/// </summary>/// <param name="socket">Socket实例</param>/// <param name="heartbeatInterval">心跳检测间隔时间(秒)</param>/// <param name="heartbeatTimeout">心跳检测超时时间(秒)</param>public virtual void Awake(Socket socket, float heartbeatInterval, float heartbeatTimeout){//......ReceiveMessageAsync(_cancel.Token);}/// <summary>/// 接收并处理客户端发来的消息/// </summary>/// <param name="token">用于取消异步的token</param>protected abstract void ReceiveMessageAsync(CancellationToken token);}
}
在MMO_Player
中重写,重写为异步async
方法,并永久轮询(用于接收并处理该客户端发来的消息),在方法开始的时候向客户端发送身份ID:
namespace uNet.Example.MMO
{/// <summary>/// MMORPG玩家/// </summary>internal class MMO_Player : NetworkPlayer{/// <summary>/// 接收并处理客户端发来的消息/// </summary>/// <param name="token">用于取消异步的token</param>protected override async void ReceiveMessageAsync(CancellationToken token){//向客户端发送身份IDbool result = await SendMessage_AllocateID(token).ConfigureAwait(false);if (!result){Log.LogWarning($"玩家【ID:{ID}】向其分配身份ID出错,已将其踢出服务器。");ServerEntry.Current.DestroyPlayer(this);return;}while (!token.IsCancellationRequested){NetworkMessage? message = await ReceiveDataAsync(token).ConfigureAwait(false);if (message != null){//接收并处理其他消息}}}}
}
发送身份ID的方法SendMessage_AllocateID
如下:
/// <summary>/// 发送消息到客户端:为玩家分配身份ID(主命令:0,子命令:0)/// </summary>/// <returns>是否发送成功</returns>private async Task<bool> SendMessage_AllocateID(CancellationToken token){//通过对象池创建实例NetworkMessage networkMessage = ServerEntry.Current.CreateMessage<NetworkMessage>();networkMessage.CheckCode = NORMAL;networkMessage.Sessionid = ID;//传入身份IDnetworkMessage.Command = 0;//主命令:0networkMessage.Subcommand = 0;//子命令:0networkMessage.Encrypt = 0;networkMessage.ReturnCode = 200;//返回码200networkMessage.Message = null;//消息体为nullbool result = await SendDataAsync(networkMessage.Encapsulate(), token).ConfigureAwait(false);//通过对象池回收实例ServerEntry.Current.DestroyMessage(networkMessage);return result;}
③.客户端回复基础信息
客户端则需要监听如下事件,以获得服务端的消息并做出回复:
private void OnInit(){Main.m_Network.ReceiveMessageEvent += OnReceiveMessage;}/// <summary>/// 接收到服务端的消息/// </summary>private void OnReceiveMessage(ProtocolChannelBase channel, INetworkMessage message){if (channel is TcpChannel){TcpNetworkInfo networkInfo = message as TcpNetworkInfo;//主命令:0,子命令:0,为玩家分配身份ID的消息if (networkInfo.Command == 0){if (networkInfo.Subcommand == 0) ReceiveMessage_AllocateID(networkInfo);}//......}}/// <summary>/// 接收服务端发来的消息:为玩家分配身份ID(主命令:0,子命令:0)/// </summary>/// <param name="networkInfo">消息</param>private void ReceiveMessage_AllocateID(TcpNetworkInfo networkInfo){if (networkInfo.ReturnCode == 200){ID = networkInfo.Sessionid;//回复服务端SendMessage_AllocateID();Log.Info($"收到服务器分配的身份ID:{networkInfo.Sessionid}");}else{Log.Error($"服务器分配身份ID出错【{networkInfo.ReturnCode}】:{networkInfo.Message}");}}/// <summary>/// 发送消息到服务端:为玩家分配身份ID(主命令:0,子命令:0)/// </summary>public void SendMessage_AllocateID(){MMOData_PlayerBaseInfo playerBaseInfo = new MMOData_PlayerBaseInfo();//告知服务端玩家的姓名和角色playerBaseInfo.ID = ID;playerBaseInfo.Name = Name;playerBaseInfo.Role = Role;TcpNetworkInfo networkInfo = Main.m_ReferencePool.Spawn<TcpNetworkInfo>();networkInfo.CheckCode = TcpChannel.NORMAL;networkInfo.Sessionid = ID;networkInfo.Command = 0;networkInfo.Subcommand = 0;networkInfo.Encrypt = 0;networkInfo.ReturnCode = 0;//使用protobuf转换数据为字节数组(byte[]),然后作为消息体传入networkInfo.Message = ProtoBufToolkit.Serialize(playerBaseInfo);bool result = Main.m_Network.SendMessage<TcpChannel>(networkInfo);if (!result){//发送失败,切换到断线流程Main.m_Procedure.SwitchProcedure<MMO_ReconnectProcedure>();}}
服务端:
客户端:
至此,便完成了一名玩家客户端的身份鉴定与身份ID分配的过程,且代表该玩家的网络玩家
实例已添加到服务端中,为后续的网络交互打下了基础。