当前位置: 首页 > 工具软件 > TChannel > 使用案例 >

ET---TChannel学习笔记

石苏燕
2023-12-01

TChannel

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


3.0的TChannel的变化不大,大家可以放心观看。


private readonly TcpClient tcpClient;

好的,保存了一个TCP客户端连接。然后,我们又看到新东西了,TBuffer,而且这个也很长,有必要再分个P,希望大家能记得我们一起走过的路程。ET—TBuffer学习笔记

TBuffer学习归来,相信大家都对它有了足够多的了解。

private readonly TBuffer recvBuffer = new TBuffer();
private readonly TBuffer sendBuffer = new TBuffer();

private bool isSending;
private readonly PacketParser parser;
private bool isConnected;
private TaskCompletionSource<byte[]> recvTcs;

这里的变量都很好理解,作者的命名很直观的说明了这些变量的作用。我们唯一不知道的是,PacketParser是什么。

PacketParser

新版本3.0已经对PacketParser进行了更新,这里不删除2.0版本的,提供3.0版本的PacketParser学习笔记,思路是不变的。


顾名思义,“包解析”。解析什么包?解析的是传输的数据包。怎么解析?就得看我们是怎么定义这个数据包的。仔细看代码,我们不难看出,数据包的结构如下:

Packet = dataLength + data;

其中,dataLength占用2个字节。
我看到许多的框架对数据包的结构构造都是这样来的,开头2个字节的长度,后面是该长度这么多的数据。可能这样的设计能很好的利用数据缓冲区吧?

我们来大致看以下PacketParser对包的解析流程。

首先,用ParserState state; 来决定包已经解析到什么程度了,是刚开始还在读取数据长度,还是已经在读数据了。

刚开始,我们当然是先过去到数据的长度了,这个时候,我们需要从我们的数据缓冲区TBuffer里面获取到2个字节的数据,buffer.RecvFrom(byte[2]),我们上一篇刚讲过这个方法,大家应该都还记得吧。获取到数据的长度后,如果发现包的大小太大了,则抛出一个异常。否则,把状态调整为读取具体数据。

之后我们便开始读具体长度的数据了,经过了第一步,我们得到了数据的长度packetSize,而且我们把TBuffer里面的游标,也就是TBuffer.FirstIndex往后移了两格,来到了具体数据的下标处,这个时候,我们再从TBuffer中获取数据就是正确的具体数据了。(这里是对TBuffer的应用补充)

待我们获取完具体的数据并把它保存在packet变量当中,我们便把PacketParser的各种状态更新一遍。就是isOKfinish等。

最后作者提供了一个获取具体数据的方法GetPacket。作者在这里还对PacketParserisOK更新了一遍,也就是说,同样的数据只会被获取一次。

TChannel

我们看到TChannel有两个构造函数,作者给了我们注释,一个是connect,一个是accept。现在还不是很清楚两者的区别。

我们来看看ConnectAsyncOnAccepted,就可以猜猜这两者的区别。connnect很可能是客户端跟远端进行连接的消息通道,accept大概就是对已有的连接创建消息通道。

我们看到ConnectAsync是一个异步的方法,他会尝试与我们提供的hostport与远端连接,如果连接成功,则开始进行数据的接收。数据的收发StartRecvStartSend都是异步进行的。

我们来看看StartSend

int sendSize = TBuffer.ChunkSize - this.sendBuffer.FirstIndex;
if (sendSize > this.sendBuffer.Count)
{
    sendSize = this.sendBuffer.Count;
}
await this.tcpClient.GetStream().WriteAsync(this.sendBuffer.First, this.sendBuffer.FirstIndex, sendSize);
this.sendBuffer.FirstIndex += sendSize;
if (this.sendBuffer.FirstIndex == TBuffer.ChunkSize)
{
    this.sendBuffer.FirstIndex = 0;
    this.sendBuffer.RemoveFirst();
}

前面的一些代码我就不贴了,直接来看重要的代码。这段代码就是把sendBuffer里面的数据通过tcpClient写入数据流中,道理跟PacketParser里面的差不多。这里我不禁疑惑,为什么不直接用TBufferRecvFrom(byte[]) 的方法?为什么不直接这样写呢?

int sendSize = this.sendBuffer.Count;
byte[] value = new byte[sendSize];
this.sendBuffer.RecvFrom(value);
await this.tcpClient.GetStream().WriteAsync(value, 0, sendSize);

大佬回复:因为可以减少网络调用,减少gc,你这样写那就是一个消息调用一次write,一个消息要额外再new一块内存

StartRecv也差不多。不过操作的是recvBuffer。需要注意的是,当有等待接收到消息的任务recvTcs时,它会在这里被处理,也就是会在这里结束Task。

好,接下来我们看它是怎么实现AChannelSendRecv方法吧。

public override Task<byte[]> Recv()
{
    if (this.Id == 0)
    {
        throw new Exception("TChannel已经被Dispose, 不能接收消息");
    }

    byte[] packet = this.parser.GetPacket();
    if (packet != null)
    {
        return Task.FromResult(packet);
    }

    recvTcs = new TaskCompletionSource<byte[]>();
    return recvTcs.Task;
}

这段代码是这样理解的,首先它是一个异步的方法,如果我们的parser里边已经有可以完全解析的数据了,则把这解析的具体数据以异步的方法返回出去。而如果没有数据,则我们要创建一个新的Task,等带新的数据的接收。也就是我们之前在StartRecv里面说的,会在接收到新的数据后结束这个Task,并把接收到的数据返回出去。

TChannel学习下来,感觉还是有点混乱的,毕竟新的东西太多了。可能得等到具体用到的时候,才能对此有更深的理解。不过,我们已经知道了它每个方法的具体运行,之后真正用到的时候,我们便不会陌生了。

结束语

坚持不懈~

 类似资料: