Session学习笔记

Session

请大家关注我的微博:@NormanLin_BadPixel坏像素


3.0已经更新了。大家可以在学习完这篇之后去看Session3.0的差异


大致浏览了一下代码,我决定还是先去学习一下.Net异步编程网络RPC模式是啥。

稍微学习了一下后,我们大致对RPC模式和异步编程有了一定了解。

private static uint RpcId { get; set; }

这个就是RPC模式中消息传递时候的CallID吗?用来标识调用远端的哪个方法?如果让我写我可能会这样,但是,大佬肯定想的比我更多,而且,我们大概能猜到用来标识调用的应该是我们之前接触到的Opcode,所以,对这个RPCID我们还不能太早下定论。而且,它是私有的,很可能是对自身的一些数据进行标识的。

private readonly NetworkComponent network;

创建这个Session的网络组件,应该是在创建这个Session的时候赋值的。方便这个Session对上级进行访问。就像我们Unity里面,gameObject.transformtransform.gameObject一样方便。不过这个是只能自己访问的,毕竟私有只读的属性在那嘛。

private readonly Dictionary<uint, Action<object>> requestCallback = new Dictionary<uint, Action<object>>();

这个应该是接收完数据后的回调函数。变量名说明一切。

private readonly AChannel channel;
private readonly List<byte[]> byteses = new List<byte[]>() {new byte[0], new byte[0]};

channel我们之前讲过了,应该是专属于这个Session的对话通道。而byteses应该就是通道中传输的数据了。

public Session(NetworkComponent network, AChannel channel)
{
    this.network = network;
    this.channel = channel;
    this.StartRecv();
}

构造函数,最后调用了StartRecv方法,可以预见这是启动数据传输的方法。

private async void StartRecv()
{
    while (true)
    {
        if (this.Id == 0)
        {
            return;
        }

        byte[] messageBytes;
        try
        {
            messageBytes = await channel.Recv();
            if (this.Id == 0)
            {
                return;
            }
        }
        catch (Exception e)
        {
            Log.Error(e.ToString());
            continue;
        }

        if (messageBytes.Length < 3)
        {
            continue;
        }

        ushort opcode = BitConverter.ToUInt16(messageBytes, 0);
        try
        {
            this.Run(opcode, messageBytes);
        }
        catch (Exception e)
        {
            Log.Error(e.ToString());
        }
    }
}

这段代码前面很好理解,就是在异步接收对话通道里传递的消息。大家需要理解的是if (messageBytes.Length < 3) 之后的代码。

我们先来看

ushort opcode = BitConverter.ToUInt16(messageBytes, 0);

BitConverter.ToUInt16 这个方法是将字节数组指定位置起的两个字节转换为无符号整数。所以我们得先保证messageBytes的长度是大于等于3的。

而且,我们也不难猜出这里消息传递的结构:2个字节的操作类型+具体消息。

我们继续看Run 方法。

private void Run(ushort opcode, byte[] messageBytes)
{
    int offset = 0;
    // opcode最高位表示是否压缩
    bool isCompressed = (opcode & 0x8000) > 0;
    if (isCompressed) // 最高位为1,表示有压缩,需要解压缩
    {
        messageBytes = ZipHelper.Decompress(messageBytes, 2, messageBytes.Length - 2);
        offset = 0;
    }
    else
    {
        offset = 2;
    }
    opcode &= 0x7fff;
    this.RunDecompressedBytes(opcode, messageBytes, offset);
}

呵呵,可能大家跟我一样,看到0x8000,0x7fff头都大了,不过,如果大家没记错的话,opcode是ushort类型的数据,而ushort类型的数据是占2个字节的,表示无符号整型,其范围为[0,65535]。而short型表示的范围是[-32768,32767]。而0x8000是16进制表示的32767,有没有发现,刚好是short型的边缘。而它的二进制表示是1000 0000 0000 0000用一个ushort类型的去跟0x8000进行逻辑与运算,便能很方便的判断最高位是否为1。为什么要这么判断呢?首先有的时候按位搜索或是按位运算都会相对快一些,最重要的是这样写装逼。不过这里作者很明显赋予了这个ushort以特别的使命,用来判断数据是否进行过压缩。这里大家要保持有疑问:
1. 什么样的数据需要压缩?
2. 数据是怎样进行压缩的?
3. 数据是在什么时候进行压缩的?
4. 又是在什么时候解压缩的?

很方便就能知道,数据的解压缩是在此时进行的。第1跟第3个问题我们等看到的时候再聊,现在我们能解答的是第2个问题,数据是怎样进行压缩的?

ZipHelper

public static byte[] Compress(byte[] content)
{
    //return content;
    Deflater compressor = new Deflater();
    compressor.SetLevel(Deflater.BEST_COMPRESSION);

    compressor.SetInput(content);
    compressor.Finish();

    using (MemoryStream bos = new MemoryStream(content.Length))
    {
        var buf = new byte[1024];
        while (!compressor.IsFinished)
        {
            int n = compressor.Deflate(buf);
            bos.Write(buf, 0, n);
        }
        return bos.ToArray();
    }
}

Deflater这个类是在ICSharpCode.SharpZipLib.Zip.Compression
Git/icsharpcode/SharpZipLib这是一个开源的项目,有兴趣的大家可以自己去看看。其实我们只要知道,这个家伙能帮我们进行数据的压缩,减少我们网络传输的带宽就可以了。

而从这段我们可以看出,这个Demo中数据是以1KB为单位进行压缩的,为什么这么压缩呢,不知道。如果有大佬知道的话请解惑。而后面的解压缩我就不讲了。

Session

messageBytes = ZipHelper.Decompress(messageBytes, 2, messageBytes.Length - 2);

我们从这里可以看出,网络传输中的数据只有具体的数据被压缩了,而前面两个字节的操作符(Opcode)并不会被压缩。而offset这个变量则是表示整段数据的偏差量,如果是操作压缩数据,我们经过解压缩后获得的是不含操作符的具体数据,所以是没有偏差量的。而如果操作的是没有压缩的数据,则数据是包含前两个字节的操作符的,所以要记录这个偏差值,为2。

opcode &= 0x7fff;


这段代码,在我看来,应该就是把最高位变为0。于是我猜测,作者应该是对每一段数据这样处理的:首先获取具体数据data1,然后获取数据的操作符opcode1,然后,判断data1是否需要进行压缩,如果需要进行压缩,则把opcode1的最高位设为1(因为ushort本身最高位不会为1,这也是我猜的),变成opcode2。如果不需要进行压缩,则opcode不变,opcode2 = opcode1,然后对data1进行数据压缩变为data2,最后,opcode2 + data2就是最后得到的传输数据。
所以,这段代码其实可以理解为把opcode2转换为opcode1。(如果我猜的没错的话嘿嘿)

接下来我们看看RunDecompressedBytes

private void RunDecompressedBytes(ushort opcode, byte[] messageBytes, int offset)
        {
            Type messageType = this.network.Entity.GetComponent<OpcodeTypeComponent>().GetType(opcode);
            object message = this.network.MessagePacker.DeserializeFrom(messageType, messageBytes, offset, messageBytes.Length - offset);

            //Log.Debug($"recv: {MongoHelper.ToJson(message)}");

            AResponse response = message as AResponse;
            if (response != null)
            {
                // rpcFlag>0 表示这是一个rpc响应消息
                // Rpc回调有找不着的可能,因为client可能取消Rpc调用
                Action<object> action;
                if (!this.requestCallback.TryGetValue(response.RpcId, out action))
                {
                    return;
                }
                this.requestCallback.Remove(response.RpcId);
                action(message);
                return;
            }

            this.network.MessageDispatcher.Dispatch(this, opcode, offset, messageBytes, (AMessage)message);
        }

根据我们之前学过的OpcodeTypeComponent,我们可以通过一个opcode(ushort)来获取到对应的操作类。

我们来瞅瞅this.network.MessagePacker是什么东西。

public interface IMessagePacker
{
    byte[] SerializeToByteArray(object obj);
    string SerializeToText(object obj);

    object DeserializeFrom(Type type, byte[] bytes);
    object DeserializeFrom(Type type, byte[] bytes, int index, int count);
    T DeserializeFrom<T>(byte[] bytes);
    T DeserializeFrom<T>(byte[] bytes, int index, int count);

    T DeserializeFrom<T>(string str);
    object DeserializeFrom(Type type, string str);
}

啊哈,终于给我们找着了,不知道大家还记不记得,我们之前在学习MessageDispatherComponent的时候说的

这个时候,消息调度器的第一个作用就出来了,它会把这些信息给统一打包,然后通过自定义的数据类型,把这些信息序列化成json或者protobuff或其他的一些数据,方便网络传输。

我本来以为这一步是会在MessageDispather里面实现的,不过作者把这一步放在了MessagePacker里面,不过这样职责更单一,更优雅。

我们知道,我们发送的数据其实是从一个个对象变化而来的,比如我传输一个移动的操作,那么我们就可能需要传输一个Vecter2或Vecter3的数据或者其他我们自定义的数据类型。但是,在网络的传输过程当中,我们是通过字节流进行传输的,所以,我们需要把这些对象序列化。而序列化的方法有很多种,比如json,比如protobuf等。而这个接口就是让我们自定义序列化和反序列化方式的地方了。不管我们选用了哪种方式,只要实现这些接口就好。

所以,我们便是从这段代码获取到反序列化后的消息对象。最后进行一次强转即可。一般都是自定义的数据类型。

object message = this.network.MessagePacker.DeserializeFrom(messageType, messageBytes, offset, messageBytes.Length - offset);

这里需要注意的是,作者在这里给网络传输的信息分了两个类型,一种是AResponse,一种是AMessage。这里作者很好心的给了注释,不然我独自理解得花好多时间。AResponse是服务端回的RPC消息,而AMessage应该是普通的消息。对于从服务端传回的RPC消息,因为这个消息是回应从客户端传出去的RPC请求的,所以在客户端发送请求的时候肯定会有回调函数的,所以,收到传回的消息后就可以直接调用回调函数了,而从代码中,我们就可以看出requestCallback这里就是存放回调函数的,这在我们开头的时候也说了。

如果是普通的消息,则是通过消息调度器对消息进行调度。

上面的是接收消息与处理的内容,接下来我们看一下发送消息的。


我们先来看一下SendMessage

private void SendMessage(object message)
{
    //Log.Debug($"send: {MongoHelper.ToJson(message)}");
    //获得到消息的操作符
    ushort opcode = this.network.Entity.GetComponent<OpcodeTypeComponent>().GetOpcode(message.GetType());
    //讲消息从对象序列化成字节数组
    byte[] messageBytes = this.network.MessagePacker.SerializeToByteArray(message);
    //判断并对数据进行压缩
    if (messageBytes.Length > 100)
    {
        byte[] newMessageBytes = ZipHelper.Compress(messageBytes);
        if (newMessageBytes.Length < messageBytes.Length)
        {
            messageBytes = newMessageBytes;
            //如果需要压缩,则把操作符的最高位设为1
            opcode |= 0x8000;
        }
    }
    //将ushort类型的opcode转成字节数组
    byte[] opcodeBytes = BitConverter.GetBytes(opcode);

    this.byteses[0] = opcodeBytes;
    this.byteses[1] = messageBytes;
    channel.Send(this.byteses);
}

看到这里,大家会惊喜的发现,这不就是我们之前[对发送数据处理方式的猜测](#caice
)吗?(我会对代码进行注释)
最后,我们通过消息通道传输的数据是List

/// <summary>
/// Rpc调用
/// </summary>
public Task<Response> Call<Response>(ARequest request, CancellationToken cancellationToken)
    where Response : AResponse
{
    request.RpcId = ++RpcId;
    this.SendMessage(request);
    var tcs = new TaskCompletionSource<Response>();
    this.requestCallback[RpcId] = (message) =>
    {
        try
        {
            Response response = (Response)message;
            if (response.Error > 100)
            {
                tcs.SetException(new RpcException(response.Error, response.Message));
                return;
            }
            //Log.Debug($"recv: {MongoHelper.ToJson(response)}");
            tcs.SetResult(response);
        }
        catch (Exception e)
        {
            tcs.SetException(new Exception($"Rpc Error: {typeof(Response).FullName}", e));
        }
    };
    cancellationToken.Register(() => { this.requestCallback.Remove(RpcId); });
    return tcs.Task;
}

看到这段代码里对RpcId的调用,大家大概也能猜到this.RpcId的作用了。没错,就是用来标识由这个Session发送出去的Rpc请求的。也就是说,每个Session都管理着一套自己的Id标识,这个Rpc只会在自己发送请求的时候进行自增,也只会在接收到返回消息的时候判断。而每个Session独立的AChannel通信通道也保证了自己发送的Rpc请求的返回只会被自己接收到而不会被其他Session误收。

赋予了RpcId之后,我们通过SendMessage把消息发送出去了。但是,大家注意了,我们这个方法的返回值是一个Task,我们必定会在接收到消息的回复后把回复内容返回,接下来便是这部分的代码。

这里我们又遇到了TaskCompletionSource这个在我们之前对TimerComponent的学习时候就遇到过的东西,那个时候我对它的描述不是很多,这里给大家详细说一下。

当我们实例化一个TaskCompletionSource对象tcs的时候,它会帮我们创建一个Task,也就是tcs.Task。我们可以对这个tcs进行一些操作,这些操作会相应的对tcs.Task造成影响,比如我们最常用的tcs.SetResult(TResult) 便能把tcs的基础 Task 转换为 RanToCompletion 状态。 并把TResult作为任务的返回结果。

而这段代码同时起到了对异常的捕获。并且,把接收到回复信息的回调函数根据RpcId存进了requestCallback字典里面,供之后调用。

具体在哪里调用,应该是在请求失败的时候。我们在后面的ActorProxy学习笔记内会解释。(2018/4/16写)

并且,注册一个将在取消此 CancellationToken 时调用的委托,根据RpcId移除相应的回调函数。
而后面还有一个Call

/// <summary>
/// Rpc调用,发送一个消息,等待返回一个消息
/// </summary>
public Task<Response> Call<Response>(ARequest request) where Response : AResponse

跟之前那个相比,只是少了CancellationToken。也就是说,如果用第一种方法,带CancellationToken参数的,那么它可以在对方没有回复之前通过CancellationToken来取消掉这次Call,而用第二种方法,则一定会等待对方答复,就算得到的回复是异常。

而后面的SendReply则都是容易理解的。

细心的同学可能已经发现,这里很多的共用方法都没有被其它地方引用,是他们没用吗?那作者为什么要写他们?这里我突然想到了ILRuntime,之前因为这个跟热更新有关所以一直没有提,(毕竟刚开是还不想接触热更,那又是一个很大的课题)但是既然这个Demo用到了热更,那肯定有很多逻辑是写在ILRuntime里面的,而如果我猜的没错的话,ILRuntimeLua一样,是不被VS编辑器编译的,我们也找不到其中对其他脚本公有方法的调用,这样我们之前累积的很多问题,如果看不到具体在哪里调用的话,我们会很难理解其中的作用。所以,我想还是得去学习一下ILRuntime。不过,我遵循的是,不到万不得已的时候,我们还是继续我们现阶段的学习吧。

结束语

我们要学习的东西还很多 ————– Norman林

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值