TService和KService学习笔记
请大家关注我的微博:@NormanLin_BadPixel坏像素
这里我默认大家已经了解TCP跟UDP咯。如果真有不了解的,这里提供一个传送门学习,保证学会。
因为3.0更新后把UDP变为KCP了,这里给不了解KCP的同学提供一个传送门学习,保证学会。
千万别因为变为KCP就不去了解UDP了,KCP的底层协议一般就是UDP。
TService
继承自AService。
private TcpListener acceptor;
呃。。虽然我对TCP跟UDP了解了,但不代表我了解了它们对应的编程呀。对于只在大二JAVA网络编程课程中接触了下网络编程的我,看来还是得先补补基础,了解一下TcpListener。这篇博文对TCPListener和TCPClient都有很详细的解释。
private readonly Dictionary<long, TChannel> idChannels = new Dictionary<long, TChannel>();
之前我们看过了AChannel,现在我们看到它的具体派生类了,我们进去看看。
TChannel
我的龟龟,好长。分P吧。TChannel学习笔记
T_T,没想到,才两段代码,我们就花了大量时间去学习。我们继续回到TService
TService
/// <summary>
/// 即可做client也可做server
/// </summary>
/// <param name="host"></param>
/// <param name="port"></param>
public TService(string host, int port)
{
this.acceptor = new TcpListener(new IPEndPoint(IPAddress.Parse(host), port));
this.acceptor.Start();
}
这就是建立一个TcpListener的方法。
public void Add(Action action)
{
this.actions.Enqueue(action);
}
public override AChannel GetChannel(long id)
{
TChannel channel = null;
this.idChannels.TryGetValue(id, out channel);
return channel;
}
这两段代码就不用我多说了吧?我相信大家都是聪明人。
public override async Task<AChannel> AcceptChannel()
{
if (this.acceptor == null)
{
throw new Exception("service construct must use host and port param");
}
TcpClient tcpClient = await this.acceptor.AcceptTcpClientAsync();
TChannel channel = new TChannel(tcpClient, this);
this.idChannels[channel.Id] = channel;
return channel;
}
这段代码,首先通过TcpListener异步获取到TcpClient,再根据这个TcpClient创建TChannel,存起来并返回。这就是一个异步获取消息通道AChannel的方法。
public override AChannel ConnectChannel(string host, int port)
{
TcpClient tcpClient = new TcpClient();
TChannel channel = new TChannel(tcpClient, host, port, this);
this.idChannels[channel.Id] = channel;
return channel;
}
这段代码,创建了一个新的连接,并且根据新的连接获得新的TChannel,储存并返回。
我们从这里便可以了解到,一个是从已有的连接当中拉出一个新的消息通道,一个是重新创建一个新的连接,并返回一个消息通道。
Remove方法就不说了。就是根据ID从字典里移除对应的数据,并做相应的Dispose处理。
而最后的Update,是对actions这个变量的遍历处理,不过这里我们还看不出这个action具体是做什么的。(3.0版本已经删除)
接下来我们来看看KService,应该跟TService差不多。
KService
public static class KcpProtocalType
{
public const uint SYN = 1;
public const uint ACK = 2;
public const uint FIN = 3;
}
这里,根据我浏览后面的代码可知,是用来标识特殊消息用的。具体什么特殊消息,往下看咯。
private readonly Dictionary<long, KChannel> idChannels = new Dictionary<long, KChannel>();
哈哈哈,这怕是一篇衍生最多的笔记了,我们去看看 ET—KChannel学习笔记。
之后的变量作者已经给出了注释。我们来看一下其带参数的构造函数。
public KService(IPEndPoint ipEndPoint)
{
this.TimeNow = (uint)TimeHelper.Now();
this.socket = new UdpClient(ipEndPoint);
#if SERVER
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
const uint IOC_IN = 0x80000000;
const uint IOC_VENDOR = 0x18000000;
uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12;
this.socket.Client.IOControl((int)SIO_UDP_CONNRESET, new[] { Convert.ToByte(false) }, null);
}
#endif
this.StartRecv();
}
这里#if SERVER … #endif是Unity宏定义的用法。我们看到,在实例化KService的时候,就调用了StartRecv方法,这个方法应该就是关键了。很长,但我也贴出来。代码一长就容易占篇幅,所以我希望大家学习的时候都能有一份源代码在边上,也方便自己扩展学习。
public async void StartRecv()
{
while (true)
{
if (this.socket == null)
{
return;
}
UdpReceiveResult udpReceiveResult;
try
{
udpReceiveResult = await this.socket.ReceiveAsync();
}
catch (Exception e)
{
Log.Error(e.ToString());
continue;
}
int messageLength = udpReceiveResult.Buffer.Length;
// 长度小于4,不是正常的消息
if (messageLength < 4)
{
continue;
}
// accept
uint conn = BitConverter.ToUInt32(udpReceiveResult.Buffer, 0);
// conn从1000开始,如果为1,2,3则是特殊包
switch (conn)
{
case KcpProtocalType.SYN:
// 长度!=8,不是accpet消息
if (messageLength != 8)
{
break;
}
this.HandleAccept(udpReceiveResult);
break;
case KcpProtocalType.ACK:
// 长度!=12,不是connect消息
if (messageLength != 12)
{
break;
}
this.HandleConnect(udpReceiveResult);
break;
case KcpProtocalType.FIN:
// 长度!=12,不是DisConnect消息
if (messageLength != 12)
{
break;
}
this.HandleDisConnect(udpReceiveResult);
break;
default:
this.HandleRecv(udpReceiveResult, conn);
break;
}
}
}
async标签让我们知道了里面是可以调用异步方法的。
前面几段的作用是以异步方式获取一台远程主机返回的UDP数据报。
我们来看看KService对集中特殊消息的处理,分别是accpet消息、connect消息、DisConnect消息。如果不是特殊处理,则分到HandleRecv
private void HandleAccept(UdpReceiveResult udpReceiveResult)
{
if (this.acceptTcs == null)
{
return;
}
uint requestConn = BitConverter.ToUInt32(udpReceiveResult.Buffer, 4);
// 如果已经连接上,则重新响应请求
KChannel kChannel;
if (this.idChannels.TryGetValue(requestConn, out kChannel))
{
kChannel.HandleAccept(requestConn);
return;
}
TaskCompletionSource<AChannel> t = this.acceptTcs;
this.acceptTcs = null;
kChannel = this.CreateAcceptChannel(udpReceiveResult.RemoteEndPoint, requestConn);
kChannel.HandleAccept(requestConn);
t.SetResult(kChannel);
}
之后那几个方法都能看懂,但是我们还不是很清楚它的作用。
这里我疑惑的是,为什么Accept的Channel的Id是用IdGenerater递增的,而Connect的则是随机的。
最后我们看一下Update方法,这里管理了该Service下所有Channel的更新。
我们之前略过的AddToUpdate跟AddToNextTimeUpdate方法,就是往更新队列和定时更新的队列中添加需要更新的Channel。
public override void Update()
{
this.TimeNow = (uint)TimeHelper.Now();
while (true)
{
if (this.timerMap.Count <= 0)
{
break;
}
var kv = this.timerMap.First();
if (kv.Key > TimeNow)
{
break;
}
List<long> timeOutId = kv.Value;
foreach (long id in timeOutId)
{
this.updateChannels.Add(id);
}
this.timerMap.Remove(kv.Key);
}
foreach (long id in updateChannels)
{
KChannel kChannel;
if (!this.idChannels.TryGetValue(id, out kChannel))
{
continue;
}
if (kChannel.Id == 0)
{
continue;
}
kChannel.Update(this.TimeNow);
}
this.updateChannels.Clear();
while (true)
{
if (this.removedChannels.Count <= 0)
{
break;
}
long id = this.removedChannels.Dequeue();
this.idChannels.Remove(id);
}
}
在更新Channel前,会先检查延迟更新队列,看看里面是否有已经到时的Channel,如果有,则把Channel加入更新队列。我们知道multMap会根据键值自动排序,而timerMap的键值是时间,所以这样加入更新队列的Channel也是按时间排好序的。检查完延迟更新队列,这时候更新队列里的就是这次更新需要更新的Channel了。所以,一个简单的遍历,调用Channel的Update方法。然后,清空更新队列。延迟更新队列在检查的时候就进行了相应的清理。最后,则是检查Channel字典中是否有需要移除的Channel,如果有,则移除。
讲完了,大家别忘了我们来时的地方。
结束语
无语。