IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> ET6.0服务器框架学习笔记(二、一条登录协议) -> 正文阅读

[网络协议]ET6.0服务器框架学习笔记(二、一条登录协议)

ET6.0服务器框架学习笔记(二、一条登录协议)

上一篇主要记录ET6.0的服务器启动功能,本篇主要记录ET6.0完整的一条协议,从配置到生成协议数据,到从客户端发送给服务端,再发送回客户端的流程



一、登录协议流程

1、协议配置

ET6.0客户端与服务器通讯,使用的序列化方式为protobuf,使用的库为protobuf-net。
要使用protobuf,需要先定义协议通信数据的结构条目。对应的路径:
在这里插入图片描述
因为登录协议是客户端与服务器通信的,不属于服务器内部协议,所以打开OuterMessage.proto,里面存放的都是客户端与服务器通信定义的协议数据。
比如定义如下,登录协议:
在这里插入图片描述
注意点:
1.因为登录是请求-响应类型协议(即发送一条数据,并期望返回一条数据),所以注意对应C2R_Login协议带有“//ResponseType R2C_Login”标志,在生成协议时,用于标记这个C2R_Login请求对应的响应类型为R2C_Login
2.因为请求是直接发送给realm服的,所以是普通的IRequest类型协议,标记为IRequest
3.R2C_Login回复类消息结构,因为是Realm服发送给客户端的,因此是一个普通IResponse
4.注意两个协议类里面都有RpcId,主要用于发送请求-响应类消息时,发送将自己的RpcID发送出去,返回时带回这个值,用于发送方接受到返回协议时,可以找到对应的是哪一个请求协议返回来的。

2、协议数据生成

运行Proto2CS程序,会自动将InnerMessage协议生成到serverMessagePath目录下,将OuterMessage协议分别生成到serverMessagePath与clientMessagePath目录下。

在这里插入图片描述
可以看到会生成如下协议数据类(为了能看全,把代码折叠了,服务器与客户端生成的是一样的)
在这里插入图片描述

3、发送协议

找到客户端的LoginHelper类,这是ET帮我们写好的发送登录DEMO示例。如下:

public static async ETTask Login(Scene zoneScene, string address, string account, string password)
        {
            try
            {
                // 创建一个ETModel层的Session
                R2C_Login r2CLogin;
                using (Session session = zoneScene.GetComponent<NetKcpComponent>().Create(NetworkHelper.ToIPEndPoint(address)))
                {
                    r2CLogin = (R2C_Login) await session.Call(new C2R_Login() { Account = account, Password = password });
                }

                // 创建一个gate Session,并且保存到SessionComponent中
                Session gateSession = zoneScene.GetComponent<NetKcpComponent>().Create(NetworkHelper.ToIPEndPoint(r2CLogin.Address));
                gateSession.AddComponent<PingComponent>();
                zoneScene.AddComponent<SessionComponent>().Session = gateSession;
				
                G2C_LoginGate g2CLoginGate = (G2C_LoginGate)await gateSession.Call(
                    new C2G_LoginGate() { Key = r2CLogin.Key, GateId = r2CLogin.GateId});

                Log.Debug("登陆gate成功!");

                await Game.EventSystem.Publish(new EventType.LoginFinish() {ZoneScene = zoneScene});
            }
            catch (Exception e)
            {
                Log.Error(e);
            }
        } 

其中核心代码:

// 创建一个ETModel层的Session
                R2C_Login r2CLogin;
                using (Session session = zoneScene.GetComponent<NetKcpComponent>().Create(NetworkHelper.ToIPEndPoint(address)))
                {
                    r2CLogin = (R2C_Login) await session.Call(new C2R_Login() { Account = account, Password = password });
                }
  1. 首先从当前场景Scene的外网组件NetKcpComponent创建或获取已存在的一个对应IP地址与端口的连接。
  2. 使用await方法,等待返回协议数据。调用session.call,返回的是带期望结果的ETTask<IResponse>
  3. 创建一个新的C2R_Login结构体,填充对应的数据,直接发送。

4、处理协议

查看服务器端的C2R_LoginHandler类,这个类就是收到客户端发来的普通处理协议。

[MessageHandler]
	public class C2R_LoginHandler : AMRpcHandler<C2R_Login, R2C_Login>
	{
		protected override async ETTask Run(Session session, C2R_Login request, R2C_Login response, Action reply)
		{
			// 随机分配一个Gate
			StartSceneConfig config = RealmGateAddressHelper.GetGate(session.DomainZone());
			//Log.Debug($"gate address: {MongoHelper.ToJson(config)}");
			
			// 向gate请求一个key,客户端可以拿着这个key连接gate
			G2R_GetLoginKey g2RGetLoginKey = (G2R_GetLoginKey) await ActorMessageSenderComponent.Instance.Call(
				config.InstanceId, new R2G_GetLoginKey() {Account = request.Account});

			response.Address = config.OuterIPPort.ToString();
			response.Key = g2RGetLoginKey.Key;
			response.GateId = g2RGetLoginKey.GateId;
			reply();
		}
	}

注意点:

  1. [MessageHandler]特性标记,标记为这个的方法,会由MessageDispatcherComponent分发处理,即由当前进程服务器的消息派发器直接处理(也表示这个处理的协议为普通的协议)
  2. 派生于AMRpcHandler<C2R_Login, R2C_Login>,表示接受一个C2R_Login类型协议,返回一个R2C_Login协议,AMRpcHandler类封装了回复消息的统一处理,即传入的Action reply
  3. 具体的服务器C2R_Login处理为:获取一个随机的Gate的相关配置,Realm向Gate服务发送一条Actor请求,拿到一个客户端登录Gate用的认证Key。Actor请求,这里先不关注,等下一篇进行详细说明,这里姑且当直接拿到一个认证key,并且填到回复类实例中,调用reply将回复协议发送回给客户端。

5、接受返回协议

再看回客户端的LoginHelper类,在服务器接收到C2R_Login类协议,返回R2C_Login后,客户端收到消息,经过一系列处理(中间过程,再本篇下文中的代码解析中详细讲解),r2CLogin = (R2C_Login) await session.Call(new C2R_Login() { Account = account, Password = password });回到这行代码继续执行。因为使用了await 与ETTask异步处理,接收到协议时会回到await之后的代码,即直接获取到了R2C_Login类实例。

这种直接走请求-响应的协议,只需要客户端进行await session.call,然后服务器处理并填充好数据后返回数据,整个流程就已经跑通了。

二、普通协议处理相关代码解析

1.外网NetKcpComponent组件

服务端引用客户端的整套NetKcpComponent组件,包括其扩展方法。
NetKcpComponent类定义比较简单:一个代表提供服务器监听功能的AService类实例,一个用于派发协议数据的IMessageDispatcher派发器实例。

真正逻辑功能都在扩展方法里,文件位置:在这里插入图片描述

初始化相关:

public override void Awake(NetKcpComponent self, IPEndPoint address)
        {
            self.MessageDispatcher = new OuterMessageDispatcher();
            
            self.Service = new TService(NetThreadComponent.Instance.ThreadSynchronizationContext, address, ServiceType.Outer);
            self.Service.ErrorCallback += self.OnError;
            self.Service.ReadCallback += self.OnRead;
            self.Service.AcceptCallback += self.OnAccept;

            NetThreadComponent.Instance.Add(self.Service);
        }
  • 初始化消息派发器为外网派发器:OuterMessageDispatcher
  • 创建一个网络服务器TService
  • 初始化网络服务器的几个Action,用于将网络服务器消息反应到NetKcpComponent扩展方法。
  • 将创建的网络服务器加到NetThreadComponent组件中,用于NetThreadComponent来驱动网络服务器。
    其中需要关注的方法为:OnRead与OnAccept方法。
    OnAccept方法用于创建好一条新的对外连接Socket时,执行此方法,创建一个与之关联的Session实例。
    OnRead方法用于当收到来自某个外网连接的数据时,接受到MemoryStream流数据,找到关联的Session类与对应的流数据,通过消息派发器进行数据派发。

2.TService类

用于提供网络服务的主要类型,同时管理由此网络服务建立的网络连接TChannel类实例。
与网络服务相关的类型:SocketSocketAsyncEventArgs,这两个类的具体组合用法可查看微软官方文档,再结合Tservice类的使用来理解。

public TService(ThreadSynchronizationContext threadSynchronizationContext, IPEndPoint ipEndPoint, ServiceType serviceType)
		{
			this.ServiceType = serviceType;
			this.ThreadSynchronizationContext = threadSynchronizationContext;
			
			this.acceptor = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
			this.acceptor.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
			this.innArgs.Completed += this.OnComplete;
			this.acceptor.Bind(ipEndPoint);
			this.acceptor.Listen(1000);
			
			this.ThreadSynchronizationContext.PostNext(this.AcceptAsync);
		}

核心代码如上,主要流程:

  1. 设置同步上下文,设置服务类型。
  2. 新建一个socket,并开启监听方式,来监听来自他socket的连接,用于提供网络服务功能。
  3. 设置SocketAsyncEventArgs类实例innArgs的异步完成回调。
  4. 将AcceptAsync方法扔到主线程启动开始接受监听
  5. 如果检测到有其他连接,分支成是否异步完成(异步完成,交由innArgs实例的OnComplete方法处理,扔到主线程执行OnAcceptComplete方法;非异步完成,直接调用OnAcceptComplete方法,开启下一轮连接检测。
  6. OnAcceptComplete处理中,还包括新建一个TChannel类实例,绑定来自其他连接的socket,并调用OnAccept方法(即回到NetKcpComponent组件的接受方法)

3.TChannel类

主要用于绑定一个socket连接,并管理这个socket消息的发送与接收。其内拥有两个SocketAsyncEventArgs类实例innArgs,outArgs,对应的拥有两个ET6.0封装好的CircularBuffer类实例recvBuffer与sendBuffer,用于处理来自socket发来的steam流转换为字节流的处理。
“SocketAsyncEventArgs”的用法与Tservice一样,CircularBuffer类的主要思想就是循环利用字节数组,用于处理steam流,从而做到0GC。
其中有很多有关字节的细节处理,相关类:PacketParserCircularBuffer解析数据包数据(协议数据开头都包含了消息数据大小),CircularBuffer用于循环接受socket消息,能够分段(多个字节数组)存储数据,读取又能从这些段中取出之前存储的数据。

4.Session类

在ET6.0中能代表一个连接的Entity类,用于抽象底层连接TChannel,且附带Entity类的功能,拥有InstanceID可用于发送Actor消息,也能附加各种组件。

发送带返回协议的核心方法:

public async ETTask<IResponse> Call(IRequest request)
        {
            int rpcId = ++RpcId;
            RpcInfo rpcInfo = new RpcInfo(request);
            this.requestCallbacks[rpcId] = rpcInfo;
            request.RpcId = rpcId;
            this.Send(request);
            return await rpcInfo.Tcs;
        }

rpcId为自增方式标记,并直接封装放入了发送的协议里,不需要开发者担心了,同时使用了ETTask异步处理。

具体发送的方法:

public void Send(IMessage message)
        {
            switch (this.AService.ServiceType)
            {
                case ServiceType.Inner:
                {
                    (ushort opcode, MemoryStream stream) = MessageSerializeHelper.MessageToStream(0, message);
                    OpcodeHelper.LogMsg(this.DomainZone(), opcode, message);
                    this.Send(0, stream);
                    break;
                }
                case ServiceType.Outer:
                {
                    (ushort opcode, MemoryStream stream) = MessageSerializeHelper.MessageToStream(message);
                    OpcodeHelper.LogMsg(this.DomainZone(), opcode, message);
                    this.Send(0, stream);
                    break;
                }
            }
        }

public void Send(long actorId, MemoryStream memoryStream)
        {
            this.LastSendTime = TimeHelper.ClientNow();
            this.AService.SendStream(this.Id, actorId, memoryStream);
        }

调用发送方法,根据内外网做区分。使用MessageSerializeHelper类,序列化对象到stream流,同时也包含反序列化steam流到对象的功能,还封装了协议号进stream中。接着调用对应的Service发送,转接到对应的TChannel,进行底层socket发送,需要注意在TChannel发送时,会主动加入协议大小进stream流中。

具体的接受回复消息处理:
当TChannel底层受到协议时,会层层跳转最后到OuterMessageDispatcher派发时,判定是回复类协议,直接走对应的Session的OnRead处理。在这里插入图片描述
对应的Session的OnRead方法:

public void OnRead(ushort opcode, IResponse response)
        {
            OpcodeHelper.LogMsg(this.DomainZone(), opcode, response);
            
            if (!this.requestCallbacks.TryGetValue(response.RpcId, out var action))
            {
                return;
            }

            this.requestCallbacks.Remove(response.RpcId);
            if (ErrorCode.IsRpcNeedThrowException(response.Error))
            {
                action.Tcs.SetException(new Exception($"Rpc error, request: {action.Request} response: {response}"));
                return;
            }
            action.Tcs.SetResult(response);
        }

通过返回的rpcID,找到之前调用Call时缓存的requestCallbacks消息,内部是调用对应的Task的SetResult方法,让之前调用Call时的异步方法可以继续执行下去。从而走完一套,await发送,到消息处理,到发送回消息,并调用SetResult方法恢复异步执行的流程。

整个流程采用ETTask,将复杂的回调改成了类似同步的逻辑,又不会阻塞主线程,写起来十分顺畅。同时封装了整个底层的发送,监听,回调,协议序列化与反序列化,RpcID,Reply方法封装,数据填充等方法。书写整个通信逻辑变得很简单,唯一就是要注意,proto文件的书写,协议处理类的特性标签,与继承的处理类。

总结

本篇记录了第一条登录协议如何走通。需要注意的点:
1.普通外网协议,在OuterMessage.proto文件中定义数据结构,同时注意需要标记注释,用于生成正确的协议数据类与标记
2.生成好协议数据后,使用对应的Session直接发送对应的类实例即可,带有回复的消息,记得使用await进行异步等待处理。
3.收到普通消息的协议处理类,需要继承AMRpcHandler类,封装了普通消息的一系列处理方式与回复方式。
4.发送消息可定义取消ETCancellationToken类实例,方便撤回一些消息发送。

下一篇准备记录一下普通Actor类消息的处理。

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-09-06 11:28:49  更:2021-09-06 11:30:03 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年6日历 -2024/6/27 1:46:10-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码