ET服务器框架学习笔记(九)
前言
本篇记录ET内部相关的通讯底层的数据处理相关。
一、ET之Channel
AChannel抽象类,主要是用来描述如何处理数据的方式,其核心功能是封装了socket。由于AChannel是抽象类,我们直接看基于TCP得TChannel类。
1.TChannel
一个TChannel代表着一个连接,可以通过他发送与接收数据
内部包含:1个Socket类 ,2个SocketAsyncEventArgs,2个CircularBuffer类,1个MemoryStream类。
- 1个MemoryStream:用于将Socket套接字数据存放进去处理的类,存入后方便上层逻辑处理。
- 2个CircularBuffer:主要封装了字节数组,在内部通过循环使用的方式提升性能,其中1个用于处理发送数据的buffer,另外一个用于接收套接字数据,他们主要与MemoryStream进行交互。
- 2个SocketAsyncEventArgs:其中1个(outArgs)用于主动发起连接时的socket事件监听,另外1个(innArgs)用于监听接收接收数据的事件监听。
- 1个Socket类:基本的套接字功能类
注意点
- 2个SocketAsyncEventArgs的Completed都关联到TChannel的OnComplete上,与之前所写一样,由于socket是IO异步的操作,ET为了实现游戏逻辑单线程异步,OnComplete实际很可能由其他线程调用,所以根据LastOperation,封装好各类回调,然后以
OneThreadSynchronizationContext.Instance.Post(this.OnConnectComplete, e);
的方式传入到主线程进行调用,实现游戏逻辑上的单线程异步。 - TChannel内部用了C#的socket,以及SocketAsyncEventArgs,需要自行去查阅相关用法了。
2.PacketParser
单独拿出这个类来记录一下,是因为这个类,是相关数据解析的类,就是它将CircularBuffer,与MemoryStream类关联起来。
- Parse()方法,确保了将Buffer里面的数据以规定的规格读取到MemoryStream中,同时以下面的方式确保数据被正确的处理:
while (!finish)
{
switch (this.state)
{
case ParserState.PacketSize:
if (this.buffer.Length < this.packetSizeLength)
{
finish = true;
}
else
{
this.buffer.Read(this.memoryStream.GetBuffer(), 0, this.packetSizeLength);
switch (this.packetSizeLength)
{
case Packet.PacketSizeLength4:
this.packetSize = BitConverter.ToInt32(this.memoryStream.GetBuffer(), 0);
if (this.packetSize > ushort.MaxValue * 16 || this.packetSize < Packet.MinPacketSize)
{
throw new Exception($"recv packet size error, 可能是外网探测端口: {this.packetSize}");
}
break;
case Packet.PacketSizeLength2:
this.packetSize = BitConverter.ToUInt16(this.memoryStream.GetBuffer(), 0);
if (this.packetSize > ushort.MaxValue || this.packetSize < Packet.MinPacketSize)
{
throw new Exception($"recv packet size error:, 可能是外网探测端口: {this.packetSize}");
}
break;
default:
throw new Exception("packet size byte count must be 2 or 4!");
}
this.state = ParserState.PacketBody;
}
break;
case ParserState.PacketBody:
if (this.buffer.Length < this.packetSize)
{
finish = true;
}
else
{
this.memoryStream.Seek(0, SeekOrigin.Begin);
this.memoryStream.SetLength(this.packetSize);
byte[] bytes = this.memoryStream.GetBuffer();
this.buffer.Read(bytes, 0, this.packetSize);
this.isOK = true;
this.state = ParserState.PacketSize;
finish = true;
}
break;
}
}
- 通过状态,判定区分是PacketSize(确认消息数据大小阶段),还是处于PacketBody(处理消息数据本身阶段),
- 如果是数据大小阶段,则首先通过buffer长度,与packetSizeLength比较,确定数据是否处理完毕。
- 未处理完毕,则从buffer取出规定长度的字节,再根据设定的包体大小字节数,将其转换成对应的ToInt32,ToUInt16值存放到packetSize中,将这个值作为后续包体大小的值,然后与设置好的值进行比较,通过了设置状态,从而进行下一步处理。
- PacketBody阶段,判定buffer剩余长度与解出来的packetSize进行比较,小于它,则认为已经处理完毕。
- 如果不小于他,则正常流程,就是将buffer内,长度为packetSize的内容,读取到memoryStream中,然后处理完毕。
上面的流程,可以保证黏包问题,有多余的数据,不会进入到memoryStream影响后续的数据处理。
下面的流程,用于取到处理好的memoryStream,然后将OK状态设置为false。这样下次循环时,调用Parse()会返回false。
public MemoryStream GetPacket()
{
this.isOK = false;
return this.memoryStream;
}
3.TChannel开始
TChannel接收数据都是从Start()函数开始,意味着外层调用Start之后,TChannel开始工作,包括接收数据与发送数据。至于在哪调用这个函数,得从上层逻辑看,这里先看TChannel的接收数据工作流程。
- 如果TChannel未连接,则走异步连接逻辑ConnectAsync,最终会回到Start()函数
- 如果没有开始接收数据,则调用StartRecv,处理一些recvBuffer状态,便于数据处理。然后调用RecvAsync
- RecvAsync开始接收数据,异步或同步接收数据完毕,会调用到OnRecvComplete。
- OnRecvComplete内部对接收到的数据进行处理,会用到上面的parser处理buffer到memoryStream,然后触发readCallback事件,将数据返回给上层逻辑。内部还包含了Socket异常状态处理,以及传输数据为0的处理。
- 最后又回到StartRecv,这样就能一旦有数据过来马上就能监听并处理。
再来看下发送数据的工作流程。
this.GetService().MarkNeedStartSend(this.Id);
这行代码会将发送的启动交给上层统一调度。
主要得发送入口是StartSend()
- 对sendBuffer进行一些处理,便于发送,然后调用SendAsync
- 再次处理数据,设置SocketAsyncEventArgs,然后调用
this.socket.SendAsync(this.outArgs)
即可发送 - 发送完毕,进入OnSendComplete,处理发送数据是否发送完毕,进行一些处理,如果没有发送完,则再次调用StartSend
- TChannel提供了一个额外接口,可以直接发数据,
public override void Send(MemoryStream stream)
,这个函数,内部对传过来的MemoryStream 进行处理,将其存入到sendbuff中,在调用MarkNeedStartSend,交给上层调度。同时应该注意到,在Start()函数内部也有this.GetService().MarkNeedStartSend(this.Id);
总结
TChannel由于内部包含了两种通信方式,一种是主动,一种是被动,所以看起来很多地方都是有两套的,包括上面的接受数据,与发送数据。实际上一个TChannel,只会处于一种方式,要么被动等待连接的方式,要么主动连接的方式,但是大多数处理相同,所有ET将两种方式都写在一起了。
比如,发送数据,在start与send里面都包含了MarkNeedStartSend函数,将发送交给上层,实际上应该只会有一种方式运行。
上面纯粹个人理解,如果有误,欢迎讨论。下篇继续记录通讯里的Service。