github.com/metaworking/channeld@v0.7.3/doc/design.md (about) 1 # What 2 ## 概念 3 channeld主要有三个基本概念:Connection 连接,Channel 频道,ChannelData 频道数据。 4 ### Connection 5 每一个建立到channeld服务的外部连接,都是一个Connection。主要有两类:客户端和服务端。在某些应用场景下,如中转服务,只有客户端连接,没有后端的专用服务器。 6 7 开发者可以为每类连接配置一个有限状态机,指定某种状态下的消息类型白名单和黑名单。这是channeld提供的基本访问控制机制。 8 9 ### Channel 10 Channel可以理解为一个兴趣组,聚合了多个连接的订阅。channeld预制的频道类型包括: 11 - 全局频道。系统在启动后就会自动创建一个唯一的全局频道。所有非频道相关的消息,如:验证,创建或删除频道,都会在全局频道处理。也可用于全局广播 12 - 私有频道。每个连接可以把自己公开的数据放到这个频道,以供其它连接订阅。如:玩家的等级和基本装备信息。也可以通过这个频道进行一对一聊天 13 - 子世界频道。每个子世界是一个独立的、隔离的空间。子世界中的订阅者可以互相观察。适用于游戏房间或空间上隔离的游戏场景 14 - 空间频道。如果游戏服务器需要将玩家分布到不同的子空间来进行负载均衡,并且在不同子空间之间可以无缝移动和交互,就需要用到空间频道。开发者需要实现空间位置到频道ID的映射逻辑 15 16 开发者可以通过修改[channeld.proto](../proto/channeld.proto)来扩展频道类型。 17 18 每个频道都有一个所有者。它一般是创建频道的那个连接。在权威服务器(Server authoritative)的架构中,频道所有者往往是后端的游戏服务器。它们控制着客户端到频道的订阅,消息的广播等。 19 20 ### ChannelData 21 频道数据是订阅的核心,也就是兴趣数据。频道数据的修改,会通过[扇出 Fan-out](https://en.wikipedia.org/wiki/Fan-out_(software))的形式发送给所有订阅的连接。 22 23 每个连接可以设置自己扇出的最小间隔时间。通过这种方式,开发者可以控制现客户端对不同的兴趣数据的订阅频率。如:组队和聊天数据的同步频率较低,玩家位置的同步频率较高。 24 25 ## 和其它类似技术的对比 26 | | BigWorld | Skynet | Photon | SpatialOS | channeld(目标) | 27 | ------- | ------------ | --------- | ------------ | ---------------- | ------------------------- | 28 | 引擎集成 | 自有引擎 | 无 | Unity | UE, Unity | Unity, UE | 29 | 上手难度 | 高 | 中 | 低 | 高 | 低 | 30 | 无缝大世界支持 | 支持 | 无 | 无 | 支持 | 支持 | 31 | 兴趣管理 | Cell周边 | 无 | 无 | 跨Worker | 基于频道 | 32 | 持久化 | XML;MySQL | 内置主流数据库模块 | 无 | 二进制快照 | 快照+自定义存储 | 33 | 开发运维一体 | 私有化部署 | 无 | 公有云 | 公有云 | 公有云+私有化部署 | 34 | 负载均衡能力 | 支持动态 | 单节点,多进程 | 多节点,房间制 | 尚不支持动态 | 前端(客户端连接)+后端(模拟服务器)动态负载均衡 | 35 | 开源 | 闭源 | 开源 | 闭源 | 闭源 | 开源 | 36 | 开发语言支持 | Python | Lua | C# | C, C++, C#, Java | Go, C#, C++, Javascript | 37 | 支持传输协议 | Reliable UDP | TCP, UDP | TCP, UDP,WSS | TCP, KCP | TCP, KCP, WSS | 38 | 费用 | 高授权费 | 免费 | 按连接数收费 | 按计算量收费 | 免费 | 39 40 # Why 41 ## 网游的架构演进 42 (TODO) 43 44 45 ## 将网络层作为独立的服务 vs. 整合进开发框架 46 整合进开发框架的缺点: 47 - 固定的开发语言 48 - 往往对游戏的业务模型存在一些假设而难以通用。例如,MOBA类型往往使用房间+帧同步的框架;而MMORPG使用分布式场景+状态同步的框架 49 - 往往难以扩容,或是将游戏业务层的扩容和网络层的扩容耦合在了一起。例如,从1个游戏房间扩容到100个,意味着需要增加99个公共端点(endpoint) 50 51 独立的服务的优点: 52 - 开发语言无关。只要实现了通信协议即可,或者直接使用SDK进行调用 53 - 对各种游戏类型更通用。 54 - 容灾性更好。业务层的代码问题导致的进程崩溃,不会影响网络服务。通过负载均衡方案可以热实现灾难恢复。 55 - 客户端保持连接,体验更平滑。从大厅到房间,或从地图到另一个地图,客户端不需要重新建立连接,而且数据能够持续地同步到客户端。 56 57 ## 为什么不用Redis来实现网络层 58 - Redis的编码协议RESP基于字符串,而游戏业务中的大部分数据不是字符串 59 - 复杂的数据结构需要自己用C扩展,且传输仍基于RESP 60 - 只能基于TCP,不适用于移动设备的网络场景 61 - 通讯方式基于请求-返回模式,不支持异步,而游戏中的大部分业务不能阻塞线程,需要用异步的方式实现 62 - PUB/SUB模式下更是会阻塞住其它命令请求 63 - PUB/SUB的基准测试低于单核1000 rps。channeld的目标是至少高一个数量级。 64 65 # How 66 ## Binary protocol: 67 [TAG] [CT] [[MessagePack0 [ChannelID | BroadcastType | StubID | MessageType | MessageBody] | MessagePack1 | MessagePack2 ...] 68 1. A packet consists of a TAG, and a serial of MessagePacks (see the definition in [channeld.proto](../proto/channeld.proto)) 69 2. The tag has 4 bytes. The first byte must be 67 which is the ASCII of 'C' character. The 2-4 bytes are the "dynamic" size of the packet, which means if the size is less than 65536(2^16), the second byte is 72('H' in ASCII), otherwise the byte is used for the size; if the size is less than 256(2^8), the third byte is 78('L' in ASCII), otherwise the byte is used for the size; the fourth and last byte is always used for the size. So, if the packet size is less than 256, which is most of the case, the TAG bytes are: [67 72 78 SIZE] 70 3. Followed by the CT byte which marks the compression type to use to decode the MessagePacks. 0x0 = No compression, 0x1 = [Snappy](https://github.com/google/snappy) 71 4. Each MessagePack consists of a header and a body. The header includes an uint32 ChannelID, an enum BroadcastType, an uint32 StubId, and an uint32 MessageType. Because it utilizes [Protobuf's encoding](https://developers.google.com/protocol-buffers/docs/encoding), in most cases the header only has 4 bytes (see *BenchmarkProtobufMessageBase* in [message_test.go](../pkg/channeld/message_test.go)) 72 5. The message body is the marshalled bytes of the actual message that channeld will proceed or forward. 73 74 ## 竞态和权限问题: 75 1. 连接列表可能被各个连接和频道goroutine写,需要加锁; 76 2. 每个频道跑在不同的goroutine上;频道之间是隔离的,除了: 77 - 创建和删除频道;设置频道状态和所属连接。只能在主频道上处理 78 - 任何一个连接收取消息时都需要查询频道,所以需要对总频道列表加读写锁 79 - 原则上,每个频道内的订阅列表和数据都只能通过频率消息处理进行操作,不存在竞态问题,所以不需要加锁 80 3. channeld不假设客户端或服务端连接拥有不同的权限。这样是为了实现例如转发服务器这样没有游戏服务器的应用。 81 如果要控制客户端的访问权限,请使用连接的有限状态机来过滤消息。 82 例如:客户端在未验证时只能发送验证消息;在验证后只能发送用户自定义的消息(类型100以上)。通过这种方式,channeld就不会处理客户端发送的订阅和退订等消息。 83 84 ## Goroutines 85 ### IO 86 - Client listner.Accept() 87 - Server listner.Accept() 88 ### Connection 89 - (Per connection) Connection.Receive() 90 - (Per connection) Connection.Flush() 91 ### Channel 92 - (Per channel) Channel.Tick() 93 94 ## How channel data updates are fanned out 95 U = sends channel data update message to channeld 96 97 F = channeld sends accumulated update message to a subscribed connection 98 99 Server Connection: (C0) ------U1----U2--------U3--------- 100 101 Client Connection1:(C1) ----F1---F2---F3---F4---F5---F6-- 102 103 Client Connection2:(C2) --F7--------F8--------F9------- 104 105 Fan out interval of C1 = 50ms, C2 = 100ms 106 Time of U1 = 60ms, U2 = 120ms, U3 = 240ms, F1/F7 = 50ms, F2 = 100ms, F3/F8 = 150ms... 107 108 F1 = nil(no send), F2 = U1, F3 = U2, F4 = nil, F5 = U3, F6 = nil 109 F7 = the whole data (as the client connection2 just subscribed), F8 = U1+U2, F9 = U3 110 111 Assuming there are n connections, and each U has m properties in average. There are several ways to implement the fan-out: 112 113 A. Each connection contains its own fan-out message 114 - Space complexity: O(n)*O(m) 115 - Time complexity: O(n)*O(m) 116 117 B. Store all the U in the channel. Sort the connections by the lastFanOutTime, and then send the accumulated update message to each connection. 118 - Space complexity: O(1)*O(m) 119 - Time complexity: O(nlog(n)) + O(n)*O(m)