本篇记录个人对ET6.0源代码阅读后的理解

1.安装.Net Core

选项一:
使用Visual Studio Installer自动安装/更新最新版本的.Net Core,在工作负载中勾选".Net Core跨平台开发"即可。
选项二:
.Net Core的官网下载安装指定版本。
注:两者都差不多,用Visual Studio Installer可以方便卸载/添加新组建,目前默认带有3.1和2.1版本的SDK。

2.OneThreadSynchronizationContext类

同步上下文可用于多个线程之间的交流,线程A将要执行的代码包装在Lambda表达式中,发送给线程B来执行。
Update方法:根据先进先出规则,执行完队列中的全部任务,仅主线程执行。
Post方法:非主线程都可以通过此方法向队列添加任务,主线程直接执行任务。
实现高效的单线程服务端:仅当网络组建的Socket完成了Accept、Connect、Recv、Send、Disconnect等操作时才会调用Post方法,这样一来服务端就可以实现接待海量连接的同时有序的执行任务。

3.Game类

Game类的成员都是static类型的,用于快速引用。
其成员EventSystem、Scene、ObjectPool都包含核心逻辑。

4.ET中使用到的容器类型

4.1.Dictionary类
使用案例:allComponents = new Dictionary<long, Entity>();
这是最常见的词典,存储同类型的实例,key必须要有对比方法。

4.2.UnOrderMultiMapSet类
使用案例:types = new UnOrderMultiMapSet<Type, Type>();
底层使用HashSet,用于分类整理不同类型的实例,有相同key的value都存在一个列表里面。

4.3.MultiMap类
使用案例:TimeId = new MultiMap<long, long>();
底层使用SortedDictionary,使词典内的物品根据key进行排序。

4.4.UnOrderMultiMap类
使用案例:awakeSystems = new UnOrderMultiMap<Type, IAwakeSystem>();
底层使用List。

4.5.DoubleMap类
使用案例:opcodeTypes = new DoubleMap<ushort, Type>();
用两个Dictionary实现双面词典,可用value查询key。

4.6.ListComponent类
使用案例:using(var list = EntityFactory.Create<ListComponent<T>>(domain)){ //某逻辑 }
封装List,用于重用。

5.EventSystem类

这是一个单例类,public static,Model层和Hotfix层都能访问。
Add方法:
assemblies参数中存储程序集,如“Model.dll,Assembly”。
遍历全部Assembly中的Type→排除掉Abstract(必须被Override)类型的Type→
遍历Type的继承链中的全部Attribute→找出继承了BaseAttribute的Attribute→
将Attribute的Type作为key,类的Type作为value添加到词典→对value列表进行再分类
存储在types参数中的key类型有:
ET.ConfigAttribute
ET.ObjectSystemAttribute
ET.MessageAttribute
ET.EventAttribute
对应配置、Entity、消息、事件等。
这样做的好处是批量检查代码的规范性,划分代码扩展方向。

5.1.EventSystem.awakeSystems参数
对应有awake需求的Entity,可携带0-3个参数。这些Entity在生成后立刻执行Awake方法。

5.2.EventSystem.updateSystems参数
对应内外网组件、计时器组件,每个逻辑帧都反复执行固定逻辑。

5.3.EventSystem.lateUpdateSystems参数
没有对应内容。

5.4.EventSystem.startSystems参数
start系统类似于awake系统,不同点在于:
awake方法会在Entity/Component生成后第一时间执行,start方法将在下一个逻辑帧优先执行。
使用举例:ConsoleComponent。
start方法无法传递额外参数。

5.5.EventSystem.loadSystems参数
对应有加载数据操作的组件。

5.6.EventSystem.changeSystems参数
没有对应内容。

5.7.EventSystem.destroySystems参数
对应有释放需求的Entity。

5.8.EventSystem.deserializeSystems参数
没有对应内容。

5.9.EventSystem.allEvents参数
一个事件表示一段相对独立的代码,使用EventSystem.Run方法执行。
参数:事件名和0-3个额外参数。

6.Options类

这个Entity用于保存程序启动时调试参数的解析结果。
只有一个int类型的参数Process,默认值为1,表示当前主机的编号是1。
在分布式构架中,可以存在多个主机,每个主机都运行同样的服务端程序,拉起分布式网络。
这个Options实例将成为Scene的第一个Component。

7.Entity与Component的概念区别

Entity表示场景中的道具,比如玩家、玩家手上的鸡蛋,玩家之间可以互相交易鸡蛋。
每个Entity/Component都是唯一的,构造时或从池中取出时重置InstanceId参数。
Entity用数据的形式静态保存时需要一个长期的编号,用Id参数表示。
Component则表示道具的功能,如玩家会走是因为有移动组件。
在添加Child时需要提供Id参数作为Key,而Component使用Type作为Key。

8.生成日志

LogManager.Configuration.Variables["appIdFormat"] = $"{Game.Scene.Id:0000}";
Log.Info($"server start........................ {Game.Scene.Id}");
使用Scene的Id参数作为appId,在Logs文件夹中可以看到日志文件名和日志第一条中包含appId。

9.执行事件

Game.EventSystem.Run(EventIdType.AfterScenesAdd);
在EventSystem的Add方法中已经对所有的(Model、HotFix)事件进行了整理。
要声明一个事件,需要为类添加EventAttribute(标识事件类型、作为key存储/搜索),并继承于IEvent接口(需实现带0-3和参数的Run方法)。
就本例来说AfterScenesAdd事件是一个无参AEvent,Run方法中包含场景组件初始化逻辑。

10.Entity类

Enitity类同时扮演Entity和Component,其生成、进出池、数据库存储读取等属于核心逻辑。
Game.Scene是场景中第一个Entity:
scene = EntitySceneFactory.CreateScene(1, SceneType.Process, "Process");
这个Entity是直接new出来的,同时创建了Scene、Entity、Object实例。
new出来的自定义Entity需要补全信息,如Id、InstanceId、EntityStatus。
Parent参数:表示Entity与Entity之间的父子关系,父子共享domain。
ComponentParent参数:表示Entity与Component之间的父子关系,父子共享domain。
domain参数:只有Game.Scene的domain是自己,其所有父子关系链上的Entity和Component的domian都是Game.Scene。
Id参数:除了Game.Scene,Entity使用EntityFactory时赋值,Component和父级共享Id。
InstanceId参数:设置domain时赋值。用于EventSystem。
IsRegister参数:是否注册,(除了Game.Scene)在设置domain时赋值,且跟parent一致。Game.Scene和其子级的IsRegister是true,都会执行EventSystem.Instance.RegisterSystem方法,添加到loadSystems/updateSystems/startSystems/lateUpdateSystems等。

10.1.为Entity添加Component
Game.Scene.AddComponent();
AddComponent方法:where K : Entity, new(),参数继承Entity且有无参自构方法。
可以指定如何生成Component(来自池中的回收队列或者使用无参自构方法new一个)
IsFormPool参数:标记Entity的生成方式
IsCreate参数:标记Entity使用Entity.Create方法生成
Id参数:继承父级Entity的Id值
ComponentParent参数:仅Set,设置父级Entity,标记自身为IsComponent。

EventSystem.Instance.Awake(component);
Awake操作:添加Component时立刻检查是否需要Awake
不同组件Awake时需要的参数数量不同,所以AddComponent操作也对应0-3个参数的版本。

AddToComponent方法:将生成的Component保存到Entity的components(词典)内。

10.2.EntityFatory类
这个类下包含大量static方法,用于批量生成Entity。
CreateWithParent方法:从池中生成Entity,设置Id、Parent、IsFromPool、IsCreate等属性,并Awake,直接返回新Entity。
通常生成Entity都使用这种方法,除了Scene和AChannel等特殊Entity直接自构。

11.Entity的Id与InstanceId的生成

11.1.IdStruct类
使用案例:long id = IdGenerater.GenerateId();
过程中调用:new IdStruct(process, (uint)time, (ushort)value);
process:主机编号,1-10左右。
time:精确到秒钟,比如十进制的1585110791,占30位以上。
value:秒钟内生成的物品的序号,1-1000左右。
Process占??+16+18位中的低18位。
Value占??+16+18位中的中16位。
time占??+16+18位中的高位(30位+)。

11.2.InstanceIdStruct类
使用案例:this.InstanceId = IdGenerater.GenerateInstanceId();
过程中调用:new InstanceIdStruct(process, ++MaxConfigSceneId);
MaxConfigSceneId:初始1024000,每次调用自加1,占20位以上。
Process占??+18位中的低18位。
Value占??+18位中的高位(20位+)。

12.ConfigComponent类

这个组件用于保存各个分布式服务器的配置。
Awake方法:
设置ConfigComponent.Instance为自身,使配置组件成为一个单例类。
从EventSystem.types中找到全部的Config类,这些类必须继承于ACategory。
如:ET.StartProcessConfigCategory、ET.StartSceneConfigCategory、ET.StartZoneConfigCategory、ET.UnitConfigCategory
遍历全部配置类执行自构(所以这几个类都可以直接访问Instance实例)、BeginInit/EndInit方法、添加到AllConfig词典。
AllConfig词典:没有实际使用过。可以查看所有配置,比如用typeof(StartProcessConfig)做key查询到一个StartProcessConfigCategory实例(也就是Instance实例,需要强制转换)。

ACategory<T>.BeginInit方法:
这个源自Object类的虚方法被ACategory<T>重写。
根据泛型名称在Config文件夹中寻找对应的txt文件并读取全部字符串。
将全部字符串根据换行符切分为多个字符串,将每个字符串转换为一个泛型实例,保存到dict词典。
dict词典:每个ACategory<T>的子类实例都有的参数,保存多行配置。
例如:StartProcessConfigCategory继承于ACategory<T>
在StartProcessConfig.txt中包含2行配置,转换为2个StartProcessConfig实例。key为该条配置的Id属性。
MongoHelper.FromJson(str):将Json字符串解析(反序列化)为对应泛型类。
下面将演示所有配置的作用。

ISupportInitialize.BeginInit/EndInit方法:
这2个方法均在配置实例被反序列化生成的过程中调用,用于初始化各种配置。
继承ISupportInitialize接口的类有:StartProcessConfig、StartSceneConfig、Object

12.1.StartProcessConfig
使用举例:StartProcessConfigCategory.Instance.Get(options.Process);
取StartProcessConfig.txt中的某一行配置,Process参数为App的启动调试参数,默认为1。
每行配置表示一个可用的主机,需要接收外网信息时,主机需要有独立IP。
BeginInit/EndInit方法:设置InnerAddress参数
InnerAddress参数:字符串,比如“127.0.0.1:20001”。
InnerIP参数和InnerPort参数用于初始进程的绑定监听
OuterIP参数用于其他进程访问初始进程。

12.2.StartSceneConfig
使用举例:StartSceneConfigCategory.Instance.LocationConfig.SceneId
StartSceneConfig.txt中包含全部服务器的配置。
StartSceneConfig.EndInit方法:赋值Type参数、SceneId参数。
Process参数:有独立IP的主机单位编号,可运行1个或多个服务器进程。
Id参数:服务器进程Id,服务器必须运行在有独立IP的主机上,例如租赁的阿里云ECS实例。
Zone参数:数据读写区域,该区域内使用同一个数据库进行读写。多Zone用于缓解数据库读写压力。
SceneType参数:服务器功能类型。
Name参数:服务器名称。
OuterPort参数:服务器占用的端口号,txt中可以没有该参数。
Type参数:Realm、Gate、Location、Map等,表示服务类型,可以创建新的服务器类型。
SceneId参数:利用Process参数和Id参数生成的InstanceId。

StartSceneConfigCategory.EndInit方法:
对所有服务器配置进行分类整理。
ProcessScenes:以Process参数为key保存全部配置,可查看各主机下的配置。
ZoneScenesByName:以数据读取区域为key保存全部配置,可通过数据区域ID和服务器名查看配置。
Gates:只存储Gate类型服务器配置
LocationConfig:只存储Location服务器配置,该类型服务器只存在1个。

12.3.StartZoneConfig
Id参数:大区编号,大区之间不共享数据,通常只有1个大区。
DBConnection参数:数据连接字符串
DBName参数:数据库名

13.TimerComponent类

Awake方法:单例类,设置Instance参数。
Update方法:每个逻辑帧检查是否有定时任务要执行。
NewRepeatedTimer方法:创建一个RepeatedTimer实例,安排首次任务。
minTime参数:任务列表中最近的一次任务的触发时间,默认为0表示下一个逻辑帧必定有任务。
TimeId参数:MultiMap词典,底层是SortedDictionary,会对key进行排序。以任务到期时间为key(可保证排序),Timer的Entity.Id队列为value。
timers参数:以Timer的Entity.Id为key,存储所有待执行的定时器。

13.1.RepeatedTimer类
以一定时间间隔重复执行的定时器。
Awake方法:记录开始时间、重复间隔、任务(Action)、执行次数。
timers参数:保存所有定时器,key为定时器的Entity.Id,value为定时器实例。
Run方法:安排下次任务(timerComponent.AddToTimeId)、执行本次任务。

14.OpcodeTypeComponent类

Awake方法:单例类,设置Instance参数,重置消息列表。
一个消息类,如R2G_GetLoginKey,表示这个消息将由Real服务器发送给Gate服务器(请求一个登录密钥),有MessageAttribute标识Opcode,继承于IMessage。
这些消息有明确的收发规则和流程,从而实现客户端与多台服务器之间的互动。
opcodeTypes中key为Opcode,value为消息类的Type。
typeMessages中key为Opcode,value为消息类的新建实例。
GetOpcode方法:通过消息的Type查询对应的Opcode。
GetType方法:通过Opcode查询对应的Type。

15.MessageDispatcherComponent类

使服务器具备处理客户端-服务器进程之间的Message(区别于ActorMessage)的能力。
Awake方法:单例类,设置Instance参数,整理消息处理类列表。
一个消息处理(MessageHandler)类,如C2R_LoginHandler,表示当客户端向Real服务器发送登录请求(C2R_Login,包含账号和密码),Real服务器向Gate服务器请求一个key并将key回复给客户端,客户端可以拿着key连接Gate服务器。
每个消息处理类对应着一个请求,且在特定的服务器/客户端上执行,可能收到大量种类的消息。
iMHandler.GetMessageType方法:返回请求(Request)的Type
因为请求和消息处理类是一对多关系,用Opcode作为key,MessageHandler列表作为value存储即可。
Handle方法:将消息分配给对应的消息处理类,不应发生有消息未被处理的情况。
AMHandler:客户端-服务端之间不需要回复的消息。
AMRpcHandler:客户端-服务端之间需要回复的消息。

16.服务器分工

注:同一类型服务器可能有多个实例
Realm服务器:验证登录。
Gate服务器:消息转发者,负责和客户端沟通。
Location服务器:定位Actor对象。
Map服务器:游戏逻辑。

17.CoroutineLockComponent类

协程锁组件,单例。
Awake方法:生成13个CoroutineLockQueueType类型的Entity添加到list参数,且设置组件为Parent(被添加到Children列表)。表示13种不同的协程锁。
Wait方法:
使用案例:using (await CoroutineLockComponent.Instance.Wait(Type, Id)){ //锁内逻辑 }
先从组件的Children列表中找到指定类型的子级CoroutineLockQueueType实例,再用Actor的Entity.Id在子级里面找CoroutineLockQueue实例。
不存在CoroutineLockQueue实例就从池里面new一个,并生成一个CoroutineLock实例(不在CoroutineLockQueue的队列里,而是设置CoroutineLockComponent为父级),在执行锁内逻辑后就Dispose。Dispose时如果发现后面有任务在排队,则激活一个任务;没有排队就把队列删除了。
存在CoroutineLockQueue实例(至少有一个同类任务在执行中)就向队列添加一个未完成的任务,并阻塞,前面的任务都完成了才会停止阻塞。
在这个过程中CoroutineLockQueue实例的存在表示有任务正在执行,即使队列中物品数量为0。

17.1.CoroutineLockQueueType类
CoroutineLockComponent的子级Entity,管理一个CoroutineLockQueue词典,key的根据协程锁的类型意义不同。

17.2.CoroutineLockQueue类
CoroutineLockQueueType的子级Entity,管理一ETTaskCompletionSource队列
ETTaskCompletionSource:用于管理异步任务,对应着一个ETTask。

17.3.CoroutineLock类
CoroutineLockComponent的子级Entity,标记协程锁类型和key值。

18.ActorMessageSenderComponent类

普遍用于内网消息(已知目标所在的主机),需提供目标的Entity.InstanceId作为参数。
Awake方法:单例类,设置Instance参数,创建定时器每10秒检查是否有任务超时。
Call方法:返回ETTask<IActorResponse>,这个任务需要变为完成状态才可能返回G2R_GetLoginKey消息。
ETTask<T>类:根据熊猫的注释:适用于Unity的轻量级task-like类型。
ETTaskCompletionSource实例:用于构建/维护任务(ETTask<T>实例)。
Check方法:检查是否有任务超时。
Actor消息在发送前会先添加一个存根到requestCallback:
self.requestCallback.Add(RpcId, Callback);
key是RpcId,这个Id会随着每次发信时+1;value是ActorMessageSender结构。
requestCallback中的存根有两种删除的办法:
1.任务超时(30秒),删除存根并添加错误消息到日志中。
2.收到内网回复(IActorResponse)消息,时限内删除。存根使用response作为参数执行Callback,可能报错(SetException)或返回结果(SetResult)。
SetResult方法:标记source的状态为Succeeded,设置value参数,执行continuation。
continuation:async/await方法中await后的部分。
文献参考:
C#中的异步方法译文
C#中的异步方法原文

18.1.ETTask<T>实例:
对于支线程来说,它需要一个G2R_GetLoginKey类型的回复,不然就阻塞了。ETTask<T>实例在自构时同时生成ETAsyncTaskMethodBuilder<T>实例、MoveNextRunner实例和状态机
GetAwaiter方法:new一个Awaiter结构,用于查询ETTask<T>实例的状态,查询操作最终指向ETTaskCompletionSource实例。

18.2.ETAsyncTaskMethodBuilder<T>实例:
管理状态机的分支切换
Create方法:自构
Start方法:状态机.MoveNext,执行下一个分支,第一次运行时执行分支1
AwaitUnsafeOnCompleted方法:将状态机.MoveNext作为委托赋值给ETTaskCompletionSource<T>.continuation
SetResult方法:如果分支1可以立即完成的话调用?标记任务完成。
SetException方法:捕捉异常(取消/报错)

18.3.状态机(TStateMachine实例,由编译器生成):
Call方法的最后一排:return tcs.Task;
编译器将await后的逻辑分成了2部分,作为状态机的分支:
分支1.await到return task-like实例(ETTask<T>实例)
分支2.await后的逻辑,相当于(result)=>{//continuation逻辑}
在return task-like实例之前,已经生成ETTaskCompletionSource实例(state默认为0,Pending状态),已经生成存根并发送消息。
状态机先执行分支1:
ETAsyncTaskMethodBuilder<T>.Create().Start(状态机)
分支1确认一遍ETTask<T>实例的状态,如果任务已完成,则省去设置continuation的步骤直接GetResult;如果任务未完成,设置continuation(支线程阻塞),等待外部调用解除阻塞(如:存根执行Callback)。

18.4.ETTask<T>与ETTask、ETVoid的区别:
这3者都是ET包装过的async方法返回类型,未包装时对应:Task<TResult>、Task 和 void。
ETTask<T>:有返回值,捕获异常,可await。
ETTask:无返回值,捕获异常(SetException)⇒需要手动获取异常,可await。
ETVoid:无返回值,不捕获异常⇒直接debug,不可await⇒只能用Coroutine方法作为异步入口。

19.ActorLocationSenderComponent类

普遍用于内网消息(不确定目标所在的主机),需提供目标的Entity.Id作为参数。
Awake方法:设置Instance,每10秒执行一次Check方法。
Check方法:遍历Children词典(ActorLocationSender实例),将过期(10秒)的子级Entity移除并释放。
Get方法:返回指定Id的ActorLocationSender实例,Children里没有就新生成一个。
Remove方法:移除并释放子级Entity。
Send方法:用指定Id的ActorLocationSender实例发送ActorLocation消息。
Call方法:类似于Send,await等待IActorLocationRequest回复。

19.1.ActorLocationSender实例:
Awake方法:获取一次Actor对象的InstanceId,设置ActorLocationSenderComponent组件为父级。
ActorId参数:通过LocationProxyComponent访问Location服务器更新Actor对象的InstanceId。

19.2.ActorMessage与ActorLocationMessage之间的不同
IResponse response = await ActorLocationSenderComponent.Instance.Call(unitId, actorLocationRequest);
这是一个使用ActorLocationSenderComponent组件的例子。
1.接收方不同:
在发送ActorMessage时,接收方是主机,提供的参数是IP地址和端口;
在发送ActorLocationMessage时,接收方是Entity(Actor对象),提供的参数是Entity.Id,转发到指定主机后再转交到接收方。
2.Actor对象注册/查询机制:Location Server进程中保存了Actor的Entity.Id和Entity.InstanceId键值对。ActorLocationSender通过LocationProxyComponent组件获取目标的Entity.InstanceId作为ActorId,再利用ActorMessageSenderComponent组件发送ActorMessage。
3.Actor对象迁移机制:Actor对象可以切换所在的Map服务器,需要向Location Server更新Entity.InstanceId。
4.ActorLocationMessage重发机制:如果Actor对象迁移导致返回Actor不存在的错误,则发送者等待1秒后重发,该机制可重复5次,5次过后抛出异常。
5.ActorLocationMessage回复机制:Send方法返回空消息表示对方已收到消息,Call方法返回回信。一个服务器实例不能对同一个Actor对象同时发多个ActorLocationMessage,必须等待回执。
6.ActorLocationSender仅在Awake时查询Locantion Server,之后遇到发送失败才再次查询。
7.Actor对象加锁机制:Actor对象迁移过程中Location Server会对该key加锁,对该key发送的请求会进行队列。完成迁移后,需要解锁,并更新Actor对象的Entity.InstanceId,然后响应请求。

20.LocationProxyComponent类

self.ActorId = await Game.Scene.GetComponent<LocationProxyComponent>().Get(self.Id);
获取Actor对象的Entity.InstanceId,参数为Actor对象的Entity.Id。
Awake方法:设置Instance参数,单例类。
Get方法:向Location服务器请求Actor对象的Entity.Id。

21.ActorMessageDispatcherComponent类

类似于MessageDispatcherComponent类,使服务器进程具备处理ActorMessage的能力。
Awake/Load方法:收集ActorMessage处理类,Type为key,保存到ActorMessageHandlers词典。
ActorMessage处理类:有ActorMessageHandlerAttribute特性,且继承IMActorHandler接口。
ActorMessage处理类的继承链:
AMActorHandler:服务器-服务器之间互相Send消息。
使用案例:ActorMessageSenderComponent.Instance.Send(Id, message);
AMActorRpcHandler:服务器-服务器之间互相Call消息。
使用案例:ActorMessageSenderComponent.Instance.Call(Id, message);
AMActorLocationHandler:客户端/服务器⇒位置不明的Actor对象发送Send消息。
使用案例:ActorLocationSenderComponent.Instance.Send(Id, message);
AMActorLocationRpcHandler:客户端/服务器⇒位置不明的Actor对象发送Call消息。
使用案例:ActorLocationSenderComponent.Instance.Call(Id, message);
注:Location消息通过多次普通Actor消息来实现,内网组件只会收到普通Actor消息。

22.NumericWatcherComponent类

数值变化监视组件。
Awake方法:收集所有的数值监视类。
数值监视类:有NumericWatcherAttribute,指定一种数值类型,且继承INumericWatcher接口。
使用举例:NumericWatcher_Hp_ShowUI,当HP值发生变化时修改血条长度。
目前没有实现Entity对某个类型事件进行关注,等熊猫更新。

23.ConsoleComponent类

控制台组件,用于对服务端程序添加命令。
命令:
reload:重新加载Hotfix.dll
repl:进入交互模式
exit:退出交互模式
reset:重置交互模式
一段代码文本:执行该段代码

24.NetInnerComponent类

内网组件,添加该组件后,服务器可接受来自内网中其他服务器的消息。
Awake方法:
MessageDispatcher参数:消息处理方式,使用InnerMessageDispatcher类。
MessagePacker参数:消息解析工具。
Service参数:端口监听任务,默认TCP模式,使用TService类。
Sessions:保存对话。
网络相关组件是整个框架的底层核心,涉及到数据传输和安全。
NetInnerComponentOn.OnAccept方法:当内网组件调用OnAccept时,表示有外部程序对内网组件监听的端口(比如127.0.0.1:10001)访问,由主线程执行TService.OnAcceptComplete方法时调用NetworkComponent.OnAccept方法。能知道内网端口的目标,理论上只有服务器群中其他服务器的抽象,也有可能是陌生的IP在扫端口,应判断访问者IP是否在内网主机IP列表中。

24.1.TServer类
生成Tservice实例:设置网络组件为父级
Socket类:用于建立网络通信连接。
SocketAsyncEventArgs类:提供Socket增强功能,描述/维护异步Socket操作。
Socket的自构:3个参数:AddressFamily、SocketType、ProtocolType
AddressFamily类:指定一种寻址方案。IPv4类型就是我们常用的192.0.2.235这种格式。
127.0.0.x:回送地址,主要用于网络软件测试以及本地机进程间通信,不进行任何网络传输。
SocketType类:指定Socket实例的工作方式(是否建立连接、支持哪种协议和地址),默认Stream。
ProtocolType类:协议类型,默认Tcp。
Socket.SetSocketOption方法:在Socket所有选项中设置其中某一项。
Socket.Bind方法:如"127.0.0.1:20001"。
Socket.Listen方法:Sokect开始监听,参数backlog指定pending connections queue的最大长度。
Socket.AcceptAsync方法:异步等待一个新连接,类似于Socket.Accept(阻塞等待一个连接)。
如果 I/O 操作挂起(空闲状态),返回true,触发innArgs.Completed事件。
如果 I/O 操作同步完成(报错),返回false,不会触发innArgs.Completed事件。
支线程不停的Accepet,一边保持接收,一边处理消息(报错或数据包)。
报错处理:日志输出错误,继续Accept。
正常处理:创建TChannel维护新连接,继续Accept。
TChannel类:设置Tservice为父级,接收数据包。
TChannel.isConnected参数:是否已建立连接。
Tservice.OnAccept方法:指向NetInnerComponent.OnAccept方法,创建Session(父级为NetInnerComponent)。
TChannel.Start方法:有两种接收数据包的形式:Accept/Connect。
ChannelType.Accept:用于接待新的内网连接(还不确定消息的具体内容时)。
ChannelType.Connect:用于请求新的内网连接,连接成功后与accept连接无异。
Socket.ReceiveAsync方法:异步接收一个数据包,类似于Socket.Receive。
数据包-包头:前4个byte表示packet的长度(每个byte相当于8位2进制数)。
CircularBuffer.Read方法:每次读取指定长度的byte后读取位置位置后移。
数据包-包体(PacketParser.memoryStream参数):起点为Begin、偏移0、长度packetSize。
TChannel.OnRead方法:指向Session.OnRead方法。
Session.Awake方法:设置最近接收/发送时间、TChanel的(报错/正常)消息回调方法等。
Session.OnRead方法:读取数据包-包体。
memoryStream.Seek方法:设置流的Position参数。
memoryStream.SetLength方法:设置流的字节长度。
memoryStream.GetBuffer方法:返回(创建该流的)byte数组。
读取Opcode:opcode = BitConverter.ToUInt16(memoryStream.GetBuffer(), Packet.OpcodeIndex);
读取Message:message = this.Network.MessagePacker.DeserializeFrom(instance, memoryStream);
解析工具:内网使用MongoPacker,外网使用ProtobufPacker。
派发消息:this.Network.MessageDispatcher.Dispatch(this, opcode, message);
派发工具:内网使用InnerMessageDispatcher,外网使用OuterMessageDispatcher。
Socket.ConnectAsync方法:异步向另一台主机请求建立新连接,成功后RecvAsync。
Socket.SendAsync方法:异步发送byte数组,发送完为止。
NetInnerComponent.Get方法:在发消息前,使用InnerAddress(内网IP和端口)为key查询session,没有时则创建connect连接和session。

24.2.Session类
session用于维护一个连接(对话的生命周期),可以存在很长时间,直到对方断开连接。
Awake方法:绑定一个AChannel实例,设置ErrorCallback和ReadCallback。
ErrorCallback:捕获AChannel工作过程中的报错,一旦出错移除Session。
ReadCallback:读包操作,指向Session.Run方法。
RunMessage方法:已完成消息的解析工作后的分配处理,因消息类型和接受者角色差异不同。
IMessage/IRequest/(内网)IResponse:使用Session的父级网络组件派发消息。
(客户端)IResponse:Session自己处理,用RpcId搜索requestCallback中的存根,执行存根。
Send/Reply/Call:将消息序列化后,使用绑定的AChannel的Send方法发送。

25.创建本地Scene

每个主机只需要运行一个App.exe。
在Entity的关系链中,最上层是Game.Scene。
其次是各个服务器的抽象Scene实例,如Realm、Gate、Map、Location。
使用案例:SceneFactory.Create(Game.Scene, SceneId, Zone, Name, Type);
批量创建Scene实例,以Game.Scene为父级,Id和InstanceId都是SceneId,按需添加组件。
NetOuterComponent可能存在多个,对应复数的Realm/Gate服务器,监听不同端口。
startSceneConfig.OuterAddress:使用主机外网IP和服务器外网端口。
processConfig.InnerAddress:使用主机内网IP和主机内网专用端口。

26.Actor消息、ActorLocation消息与普通消息

综合本篇以上内容,我们可以在脑内形成一个网络连接图,有:
1.很多客户端
2.1个以上阿里云ECS实例(或者自建专线机房中的主机)
3.Session维护着设备与设备之间的连接/对话。
4.如果想给谁发消息,就找到直连的或者能帮忙传话/找人的Session。
5.Scene、玩家、怪物、房间,只要这些Enity添加了邮箱组件都能收内网消息。

Actor消息为什么能送达指定InstanceId的目标:
因为从InstanceId可以推导出主机编号、主机内网地址、直连的Session。
ActorLocation消息为什么能送达指定Id的目标:
通过向Location服务器(Scene类型Entity)请求目标的InstanceId,推导出直连的Session。
Actor消息、ActorLocation消息与普通消息最终由直连的Session完成消息的发送。


关注成长,注重因果。