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
{
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 });
}
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);
}
}
其中核心代码:
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 });
}
- 首先从当前场景Scene的外网组件
NetKcpComponent 创建或获取已存在的一个对应IP地址与端口的连接。 - 使用await方法,等待返回协议数据。调用session.call,返回的是带期望结果的
ETTask<IResponse> 。 - 创建一个新的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)
{
StartSceneConfig config = RealmGateAddressHelper.GetGate(session.DomainZone());
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();
}
}
注意点:
- [MessageHandler]特性标记,标记为这个的方法,会由MessageDispatcherComponent分发处理,即由当前进程服务器的消息派发器直接处理(也表示这个处理的协议为普通的协议)
- 派生于AMRpcHandler<C2R_Login, R2C_Login>,表示接受一个C2R_Login类型协议,返回一个R2C_Login协议,AMRpcHandler类封装了回复消息的统一处理,即传入的
Action reply 。 - 具体的服务器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类实例。 与网络服务相关的类型:Socket ,SocketAsyncEventArgs ,这两个类的具体组合用法可查看微软官方文档,再结合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);
}
核心代码如上,主要流程:
- 设置同步上下文,设置服务类型。
- 新建一个socket,并开启监听方式,来监听来自他socket的连接,用于提供网络服务功能。
- 设置SocketAsyncEventArgs类实例innArgs的异步完成回调。
- 将AcceptAsync方法扔到主线程启动开始接受监听
- 如果检测到有其他连接,分支成是否异步完成(异步完成,交由innArgs实例的OnComplete方法处理,扔到主线程执行OnAcceptComplete方法;非异步完成,直接调用OnAcceptComplete方法,开启下一轮连接检测。
- OnAcceptComplete处理中,还包括新建一个
TChannel 类实例,绑定来自其他连接的socket,并调用OnAccept方法(即回到NetKcpComponent 组件的接受方法)
3.TChannel类
主要用于绑定一个socket连接,并管理这个socket消息的发送与接收。其内拥有两个SocketAsyncEventArgs 类实例innArgs,outArgs,对应的拥有两个ET6.0封装好的CircularBuffer类实例recvBuffer与sendBuffer,用于处理来自socket发来的steam流转换为字节流的处理。 “SocketAsyncEventArgs”的用法与Tservice一样,CircularBuffer 类的主要思想就是循环利用字节数组,用于处理steam流,从而做到0GC。 其中有很多有关字节的细节处理,相关类:PacketParser 从CircularBuffer 解析数据包数据(协议数据开头都包含了消息数据大小),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类消息的处理。
|