当前位置: 首页 > news >正文

【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.111000):

在这里插入图片描述

二、MMORPG网络通信协议

MMORPG的网络通信协议可在以下路径查看,这个协议严格定义了每种消息类型的双端通信格式:

在这里插入图片描述

点击其中橙色的脚本链接,可以直接定位到该消息的脚本定义。

在这里插入图片描述

三、身份认证

身份认证相关的协议为主命令:0

在这里插入图片描述

1.为玩家分配身份ID

其中,主命令:0,子命令:0的消息类型为:为玩家分配身份ID

在这里插入图片描述

为玩家分配身份ID的时机在玩家成功连接服务端之后,由服务端为其分配身份ID,并告知该客户端,后续的所有网络数据交互都将以该身份ID为唯一标识符。

因为服务端只需将身份ID告知客户端即可,所以服务端需要发送的消息体为null(身份ID包含在消息头中)。

这里由于未涉及到真实的玩家注册流程(玩家注册后应有玩家姓名角色等信息存入数据库),只是在客户端提供了一个界面让玩家输入自己的姓名和选择角色来模拟注册,所以姓名和角色等信息需要客户端告知服务端,这即是此消息客户端需要回复服务端的内容MMOData_PlayerBaseInfo

分配身份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分配的过程,且代表该玩家的网络玩家实例已添加到服务端中,为后续的网络交互打下了基础。

http://www.dtcms.com/a/477477.html

相关文章:

  • 竞价网站与竞价网站之间做友情链接建邺区住房 建设 网站
  • Django视图与路由全解析:从URL到页面,一篇讲透
  • 推荐系统实战:python新能源汽车智能推荐(两种协同过滤+Django 全栈项目 源码)计算机专业✅
  • 数据结构二叉树——层序遍历 扩展二叉树的左视图
  • .NET Core + Nginx服务器零基础部署全流程(附资源)
  • 吴江区经济开发区规建设局网站手机app如何开发制作
  • excel中关联word邮件合并使用
  • const string getWord() ;和 string getWord() const ;是一样的效果吗
  • 语音控制 APP 开发:唤醒率 99% 的实现
  • Linux操作系统-进程(三)
  • electron中进程线程之间通信方式
  • wordpress 原图查看贵港seo
  • idea生成数据集调研
  • 深圳网站制作就找兴田德润安徽省建设厅网站资料下载
  • Java外功精要(3)——配置文件和mybatis
  • 2024年最新技术趋势分析:AI、前端与后端开发新动向
  • kanass入门到实战(20) - 项目负责人如何使用kanass驾驭项目
  • 无需公网 IP:神卓 K900 实现海康摄像头异地观看的两种简单方法
  • (19)100天python从入门到拿捏《多线程》
  • KVM-QEMU 的完整工作流程案例解析
  • 【LeetCode】69. x 的平方根
  • C语言入门教程(第6讲):函数——让程序学会“分工合作”的魔法
  • 福建定制网站开发泰安一级的企业建站公司
  • 公司要建立网站要怎么做太原优化型网站建设
  • 开源 C++ QT QML 开发(十七)进程--LocalSocket
  • 2.CSS3.(3).html
  • 【MQ】RabbitMQ:架构、工作模式、高可用与流程解析
  • 零基础学AI大模型之大模型修复机制:OutputFixingParser解析器
  • 单个服务器部署多个rabbitmq
  • 银行资产管理系统核心业务架构设计