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的各种状态更新一遍。就是isOK、finish等。
最后作者提供了一个获取具体数据的方法GetPacket。作者在这里还对PacketParser的isOK更新了一遍,也就是说,同样的数据只会被获取一次。
TChannel
我们看到TChannel有两个构造函数,作者给了我们注释,一个是connect,一个是accept。现在还不是很清楚两者的区别。
我们来看看ConnectAsync和OnAccepted,就可以猜猜这两者的区别。connnect很可能是客户端跟远端进行连接的消息通道,accept大概就是对已有的连接创建消息通道。
我们看到ConnectAsync是一个异步的方法,他会尝试与我们提供的host跟port与远端连接,如果连接成功,则开始进行数据的接收。数据的收发StartRecv、StartSend都是异步进行的。
我们来看看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里面的差不多。这里我不禁疑惑,为什么不直接用TBuffer的RecvFrom(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。
好,接下来我们看它是怎么实现AChannel的Send跟Recv方法吧。
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学习下来,感觉还是有点混乱的,毕竟新的东西太多了。可能得等到具体用到的时候,才能对此有更深的理解。不过,我们已经知道了它每个方法的具体运行,之后真正用到的时候,我们便不会陌生了。
结束语
坚持不懈~