
# 编写MMO网络服务器(框架)需要什么功能

1. [x] 序列化(用户实现(对序列化类库的封装))  
2. [ ] 零拷贝(框架实现)  
3. [x] Socket封装(框架实现)  
4. [x] RPC功能(框架实现)  
5. [ ] 分布式设计(框架实现)  
6. [ ] ECS(模式)(框架实现)  
7. [x] 基于await/async的易用API(框架实现)  
8. [x] .NET Standard 2.x

## 为什么是C# 

- C# 足够出色
- 希望能为Unity独立游戏开发者开发网络游戏提供可能。
- **.NetCore**

## 为什么不是Netty, Proto.Actor, Googles gRPC, Microsoft Orleans，Skynet，Photon

MMO服务器不同于WEB服务器，MMO服务器网络吞吐量更大，要求延迟更低，功能之间耦合度高，不能使用微服务的设计模式。微服务本质就是责任链模式，依赖可能无限长，每一层依赖都会增加调度的最小时间开销，放在游戏环境中，释放一个技能，1秒才能得到结果，是难以接收的。[参考](https://docs-multiplayer.unity3d.com/netcode/current/learn/lagandpacketloss/index.html)
以上框架都是WEB[服务器框架，Proto.Actor和Orleans的 IActor的模式不适合编写MMO游戏](https://www.zhihu.com/question/359630395)。

gRPC是基于HTTP/2协议的，对于WEB业务或者棋牌类等低吞吐量游戏或许不是问题，但是对于MMO，这会造成性能问题。

另一个关键原因是，对于游戏逻辑的时序性要求更为严格，还需要帧的特性，用一个“主线程”按照指定的顺序执行游戏逻辑。也要保证await异步的后续方法在主线程执行(对于Unity尤为重要)。

## 序列化 零拷贝 Socket封装 RPC功能

这个4个功能息息相关,客户端服务端公用。 

- 为什么`序列化`要求用户(游戏开发者，下同)实现？  因为用户有选择[序列化协议](https://github.com/eishay/jvm-serializers/wiki)的权利。  
    - `序列化过程`是：`消息类`实例 -> `byte[]/字节数组` 的过程。(框架实现)  
    - `串行器`是`序列化过程`使用的类，具有序列化反序列化函数(用户实现，对序列化函数库的封装)。
    - `消息类Key` 是对序列化协议二次封装产生的东西，用于查找`串行器`。框架使用int类型(用户实现，给每一个消息类要给唯一编号。**0~999被框架占用**)。
- `零拷贝`的关键问题不在于数据拷贝的性能消耗，问题是拷贝过程中对象内存的申请。  
千兆网卡生产环境中大约能发送60~100MB不等，取近似平均值，那么每秒申请80MB的`byte[]`内存，然后GC。  
别忘了还有`消息类`实例，如果仅仅是为了发送消息，new Class,赋值，发送，那么这个开销就是80*2。并且，`序列化过程`中每多一个中间层，那么这个值就会多一倍，这是非常大的性能损失，`池`必不可少。发送和接收过程中用到的所有中间类，框架都会使用池。  
所以，`池`分2部分，`byte[]池`和 `消息类实例池`。  
    - `byte[]池`由框架实现，用户不用关心，代价是：**`消息类`实例必须序列化到给定的byte[]上，以减少中间过程，这也是为什么要用户实现`序列化`部分的主要理由。**  框架会针对每一个序列化协议提供包装函数，本质上不需要用户编写代码。  
    - `消息类实例池`几乎不可实现，因为没有办法预知`消息实例`的生命周期，它很可能在游戏逻辑中长期存在。new `消息类`实例就是网络部分的主要开销，无法避免。

- `Socket封装`  
    - 封装网络传输协议，含有TCP,UDP,KCP功能，满足`RemoteStandard`(框架定义的网络层API接口) 中定义的标准。
    - 封装RPC

            private async void TestAsync()
            {
                IRemote remote = new TCPRemote();
                var res = await remote.ConnectAsync(IPAddress.Loopback, 54321);
                if (res == null)
                {
                    Console.WriteLine($"Remote{clientIndex}:Success");
                }
                else
                {
                    throw res;
                }

                var resp = await remote.SafeRpcSendAsync<TestPacket2>(new TestPacket2() { Value = 1 });
                Console.WriteLine($"Rpc调用返回---------------------------------------- {resp.Value}");
            }


- `RPC功能` 内置在`Socket封装`中，用户不可见，通过await语法自动判断是不是RPC调用(框架实现)。

## 分布式设计 ECS

现代服务器必备功能，底层提供分布式容器，上层基于ECS实现游戏业务逻辑，API贴近Unity。  
- `分布式容器`每个进程唯一，开发时所有业务在一个进程，方便调试，上线时分布在不同进程不同地理位置的物理机中方便动态扩展。  
一个Entity包含N个业务System，每个业务System可能在本地进程也可能在远端，也就是说一个Entity同时存在多个进程的容器中，例如一个player的装备信息业务System存在服务器A，但是战斗业务System存在于服务器B，战斗业务System异步访问装备信息Component，就像访问同进程资源一样。合理的System分配依赖，以降低服务器之间的通讯量和延迟。  
与此同时，对于业务System可能同时存在于不同的分布式服务器中，例如装备业务System同时存在于服务器C，D中，C位于上海而D位于北京。因此Entity1观察Entity2的装备信息Component，在装备业务内部也是异步访问。  
最终目的，访问的信息Component在同一进程，那么同步返回，位于不同进程，则等待异步返回。API统一。
- `分布式容器`为业务System提供广播机制和访问机制。一个System不必知道另一个System在哪，就像访问同一个进程一样。

## .NET Standard 2.x

unity 和 server 的公共类库一定要是 .NET Standard 2.x  

注意：  
- .NET core 和 .NET Standard 2.x 相同的Socket API 表现不同。
- .NET Framewrok 和 .NET Standard 2.x 基本一致。

## 框架不是全部，框架没有提供什么：

- 负载均衡，框架会提供`分布式容器`的信息和物理机的信息，但是负载均衡的逻辑仍然需要用户编写。因为框架不可能知道在需要扩容的时候新的服务器进程在A地机房启动还是在B地机房启动。
- Cache机制，`分布式容器`提供不同System之间的访问功能，但是如果信息位于异地机房，那么肯定会增加延迟，什么时候Cache什么消息，这些需要用户决定。  基于`CAP原则`。
 
    例如：player1位于上海服务器，player2位于北京服务器，player1攻击player2。  
    1. 上海战斗业务System访问 player1的装备信息Component，我们可以肯定装备信息Component位于上海但是并不一定存在于同一个进程，这取决于System依赖设计，所以肯定是异步访问，但由于装备信息Component的物理位置极大可能在上海，所以延迟会很低。
    2. 上海战斗业务System访问 player2的装备信息Component,player2的装备信息Component位于北京，那么这个访问肯定产生延迟。那么如果player2的装备信息Component的在上海服务器中具有缓存，会极大的降低延迟。
    3. 攻击生效，对player1,player2应用战斗数据。流程结束。    
Cache就有可能出错，就会增大内存，用户需要在性能，准确性，延迟等诸多需求中取舍。

## Remark

- `发送过程`中序列化使用的线程是调用SendAsync的线程，在异步方法挂起前保证序列化执行完毕。所以，同步调用SendAsync后，更改`消息实例`的内容是安全的，不会影响待发送的字节。
- `接收过程`和`异步方法的后续部分`执行线程是"主线程"(框架做了线程切换)。