-
Notifications
You must be signed in to change notification settings - Fork 7
Intro
Connections
RPC communication method between servers

技术方案: 随着元宇宙概念的兴起,越来越多的开发者开始涉足这个领域,而其中一个核心的技术就是分布式服务端框架。本文将从设计角度探讨这一框架的实现原理和优势,以及在实践中需要注意的一些问题。 首先,分布式服务端框架的设计目标是提供一个高度可扩展、高效的服务端框架,以支持元宇宙应用程序的开发和部署。该框架应该能够处理大规模用户的同时保持低延迟和高吞吐量。因此,需要考虑以下几个关键因素:
- 负载均衡:为了应对大量的用户请求,需要将请求分散到多个服务器上进行处理。设计者需要考虑如何实现负载均衡,以便均衡地分配请求到服务器。
- 数据一致性:元宇宙应用程序通常需要存储大量的用户数据,因此需要确保多个服务器之间的数据一致性。这通常需要使用分布式数据库或其他一致性算法来实现。
- 高可用性:由于元宇宙应用程序的特殊性质,出现故障可能会对用户造成极大的影响。因此,设计者需要考虑如何确保系统的高可用性,例如使用故障转移或备份服务器等方式。
- 安全性:元宇宙应用程序通常需要处理用户的敏感信息,因此需要采取安全措施来保护用户数据和隐私。设计者需要考虑如何实现身份验证、访问控制等安全措施。 基于上述因素,一个典型的元宇宙分布式服务端框架通常包括以下组件:
- 服务注册中心:用于管理服务器的注册和发现。当一个新的服务器启动时,它会向服务注册中心注册自己,然后其他服务器可以通过服务注册中心找到它并与之通信。
- 负载均衡器:用于将请求均衡地分配到多个服务器上进行处理。它通常包括一个负载均衡算法,例如轮询、加权轮询、最小连接数等。
- 分布式数据库:用于存储元宇宙应用程序的数据。它需要支持高并发读写、数据分片、数据备份等功能,以确保数据一致性和高可用性。
- 安全管理器:用于管理用户身份验证、访问控制等安全措施。
技术路线 本系统主要采用c++、lua、go语言进行开发,测试或工具代码由脚本语言进行开发。网络协议涉及TCP、UDP、KCP、HTTP/HTTPS、WebSocket等;数据序列化涉及Protobuf、Json;分布式管理涉及Docker容器技术、K8S、数据同步等等;其中也还涉及热更和模块化;跨平台技术;分布式技术;Redis和Mysql数据库技术等等。
实现方式 本系统主要采用c++、lua语言进行开发,测试或工具代码由脚本语言进行开发。网络协议已支持TCP、HTTP/HTTPS、WebSocket等;数据序列化涉及Protobuf、Json;分布式管理涉及Docker容器技术、K8S、数据同步等等;其中也还涉及热更和模块化;跨平台技术;分布式技术;涉及Redis、Mysql、Mongo数据库等等。分布式解决方案采用中心服务器对子节点进行监控,管理,动态扩容,容灾处理等等,各服务器节点之间也能借助中心服务器节点进行通信,再结合容器化技术实现快速部署和管理分布式系统。服务器与客户端之间的通信协议主要以TCP + Protobuf自定义协议来实现RPC,通过消息ID来区分不同的RPC接口。跨平台技术采用宏定义区分各个平台系统接口。其中将服务拆分为不同节点,涉及有master、lobby、micro、game、gamepaly_manager、proxy等等,每个节点自由添加目标节点,连接好的节点之间可以自由通信。服务之间的rpc有三种形式进行调用,一种是基于world节点进行转发的rpc的,第二种是基于代理转发rpc,第三种采用直接通信方式调用rpc,根据业务环境选择不同服务端之间的rpc调用方式。商业级游戏引擎的网络SDK,Unity采用C#编写TCP + Protobuf的RPC接口,Unreal Engine基于腾讯的Unlua插件和LuaProtobuf、LuaSocket采用Lua编写TCP + Protobuf的RPC接口。c++层热更和模块化将业务代码封装在动态链接库里,采用动态加载的方式进行执行动态链接库里的代码,在更新的时候,只需让程序卸载该插件,可以在程序不停止的情况下可以实现dll动态替换从而实现代码热更和模块化,模块之间的调用,是每个模块都可以提供c++纯虚函数接口,其他模块只需获取该模块的对象,将其对象强转为接口类,就可以实现跨动态链接库的函数调用。连接测试需要模拟客户端同时连接服务器,采用多进程方式来开启多个测试客户端,每个连接都计算一下整个RPC过程的调用时间,采用K8S或在容器内通过命令监控服务端内存占用,CPU消耗,带宽消耗等信息。
目前进展: 代码完成度基本达到85%,还差测试代码和测试分析。目前已有三款商业游戏正在采用该框架研发,核心引擎会不断完善。 第一款: Web3链游,Unity研发的多人卡牌VR游戏,拥有每个玩家的面具、手同步,棋子,丧尸同步等,该游戏核心玩法已基本完成。 第二款:Web3链游,采用UE4研发的多人射击游戏,gameplay部分采用UE4来进行开发的,采用该框架来管理UE4的专用服务器以及除gameplay的逻辑。 第三款:采用Unity开发的休闲类游戏盒子,里面包含了很多小游戏,Squick负责玩家的数据存储以及部分对战功能。
该工程项目结构如下:
deploy: // 服务端生成可部署文件
config: // 服务端配置
data: // 服务程序储存数据
bin: // 服务端程序
tools: // 工具
src: // 主要源码文件夹
lua: // lua脚本代码
server: // 各服务器代码
squick: // 核心代码
tester: // 测试代码
tools: // 工具代码
tutorial: // 教学示例代码
test: // 测试代码
proto: // protobuf代码
www: // 网站系统代码
admin: // 后台前端代码
server: // web服务端代码
website: // 官网前端代码
third_party: // 第三方代码
cache: // 编译时的临时文件
others: // 其他
deploy: 服务端生成可独立运行的文件集,其中包含了可执行的文件、脚本、配置文件等等,用于直接上传到服务器上运行,好比Unity或UE4打包出来的文件一样。
config: 配置文件,里面包含了日志配置文件、插件配置文件、Excel生成的配置表等等。如下:
struct: sqkctl将{project_path}/resource/excel 下的所有xlsx文件转化成的xml文件,主要记录配置表中字段的属性,其中包含了,名称、描述等这些信息。
ini: sqkctl将{project_path}/resource/excel 下的所有xlsx文件转化成的xml文件,主要记录配置表中目前有哪些内容。
plugin: 各种服务器的插件配置文件
log: 服务器的日志配置文件
更新中...
src/lua:
src/server:
src/tools:
在空白目录下,输入
sqkctl init
会在当前目录下初始化工程,如下:
.gitignore
squick
files
base.json
chnaged.json
README.md
之后就可以基于squick目录下的工程来改动为自己的工程了。
sqkctl是squick的项目管理工具,为了方便让Squick核心代码与项目工程代码实现解耦管理,提供了部分命令在工程中使用,例如squick工程的初始化,squick核心代码更新,squick核心代码打patch,对比等等,类似于git的操作对版本进行管控,但我们的目的不是为了版本的管控,而是让项目代码与squick代码进行在管理层面进行解耦。除此之外也提供了其他命令,比如excel命令,会根据excel文件生成squick所需要的配置文件。
项目工程主要包含了如下:
project:
.gitignore : 忽略squick文件夹
files: 在该目录下已更改的文件:通过squick_ctl add 命令提交squick目录下已更改的文件或增加的文件
squick: 项目的全部文件,包含了suqikc和我们自己项目的文件
base.json : 记录了squick最初代码的所有文件hash
chnaged.json : 已更改的所有文件hash
在我们的git远程项目工程中也只有:
.gitignore
files
base.json
chnaged.json
这些文件或文件夹,没有squick的代码,这样对于后期升级squick核心代码的时候就比较方便了,diff下存储的就是我们改动的代码,只要我们动的核心文件不是升级中改动的文件,squick核心代码就可以升级了,采用squick update命令即可实现,该命令也会校验文件是否冲突,如果出现文件冲突,那么说明升级的文件和我们改动的文件是同一个文件,可能强制升级之后会出现报错。
提供的命令如下:
sqkctl命令:
excel: 将excel文件转化称配置文件。
init: 初始化工程,通过squick_path环境变量拷贝squick代码到当前工程,并计算squick代码的所有文件hash,并生成base.json
patch: 将diff下的代码patch到squick代码中。
add: 将获取改动的文件保存到diff文件夹下。
diff: 显示所改动的文件
version: 获取squick代码的版本号
update: 更新除了改动文件的squick所有文件 (谨慎使用)
pull: 拉去当前squick版本下的代码
**1. client <-> gateway **
向网关服务器获取代理服务器,网关服务器会根据代理服务器的连接情况选择工作量最小的代理服务器给客户端
2 client <-> login
玩家先进行登录验证,通过mysql玩家数据库来进行验证,登录成功,会将玩家的验证token、proxy连接秘钥保存在redis缓存数据库里,返回proxy连接信息和连接秘钥给客户端。
3 client <-> proxy
客户端通过连接秘钥连接代理服务器,通过redis缓存数据库来进行验证,验证成功后,对该socket进行授权。
若玩家出现离线重连,可以无需登录,直接通过proxy key来连接proxy。
连接成功后,可获取到世界服务器列表,玩家选择区服,也相当于选择一个世界服务器,发送给代理。
代理会根据世界服务器的服务器表根据负载均衡派发出一个工作量最小的游戏服务器给客户端。
4 client <-> proxy <-> game
在此之后,客户端可有权访问游戏服务器。
5 client <-> proxy <-> game <-> db_proxy
在进入游戏之前,需要从redis游戏数据库先获取玩家的基本数据,才能正式进入游戏服。内核事件通知是需要基于对象之上才能够进行的,所以玩家初次进行服务器需要创建一个服务器上的对象,好比类似于创建一个游戏角色。在创建过程会根据玩家配置表生成内存对象,存储在redis游戏数据库中,创建成功依次响应客户端,游戏服务器上的玩家数据会每隔3分钟将数据同步给数据库服务器。
5 client <-> proxy <-> game
服务的上创建玩家数据后,返回对象id,之后就是基于该对象id上进行发包收包了,即是正式进入游戏。
若玩家离线,服务端会销毁该对象,游戏服务器也会将玩家数据同步给数据库服务器。
这里分为两种类型的gameplay开局方式,一种是每一局,启动一个gameplay,第二种是充当一个模块运行在game服务器上。
两种架构方式都有不同的特点。
该方式可以自定义专用服务器,比如这个gameplay服务器可以由UE4、Unity3d或者自己写的服务器来作为一个独立的gameplay服务器,这种方式管理起来方便,可自定义强,部署方便,耦合度低。但这种方式也有缺点,消耗内存和cpu较高,服务器成本比较高。
基于玩家登陆成功之后
1 client <-> proxy <-> game
创建房间的逻辑在game服务器上,在玩家创建房间,可以等待其他玩家的加入,房主在设定游戏对局参数时,数据会保存所设定参数值在房间对象中。
2 client <-> proxy <-> game <-> gameplay manager
玩家点击开始时,由game服务器生成 gameplay服务器的instance id、key、game_id将其发送给gameplay manager服务器,gameplay manager收到后,通过设置启动参数来启动gameplay服务器。
3 gameplay <-> gameplay manager <-> game
此时 gameplay manager服务器充当 gameplay服务器与 game服务器之间的代理服务器。
gameplay 启动后,连接gameplay manager服务器,在gameplay manager服务器上请求与game服务器进行连接,验证成功后gameplay manager充当代理。
gameplay 服务器向game服务器获取房主设定的参数,并获取该房间所有玩家属性的信息,根据房主设定的参数,初始化数据,并加载相应的地图,加载完毕后,反馈gameplay 服务的基本信息给game服务器。
4 client <-> proxy <-> game
gameplay服务器初始化完毕后,由game服务器反馈gameplay的ip端口给客户端。
5 client <-> gameplay
在gameplay对局中,玩家直接与gameplay服务器进行连接对局,再次过程中,玩家也保持与game服务器进行连接,如果gameplay对局中,中途有奖励礼物情况gameplay服务器通知玩家与game服务器。
5 gameplay <-> game
gameplay对局结束或者玩家全部离线退出房间时,由gameplay_manager通知gameplay服务器自行销毁。
该方式作为一个模块运行在game服务器之上,详情可以查看 src/server/game/play 下的代码。
通关gameplay_manager模块来对gameplay进行管理,只需继承于igameplay就可以轻松的开发游戏的玩法部分。其中也封装了 玩家加入、玩家退出、玩家全部已加入、玩家断线重连,游戏结算自动销毁等等。该方式消耗内存低,占用cpu低,服务器成本低。
后台服务端基于Go语言的Gin框架来做,前端采用vue2的antd。
为服务器增加http接口,通过调用该接口,服务器会通过lua模块重新加载lua脚本,从而达到动态更新服务端逻辑。只能对登录服务器、游戏服务器、世界服务器进行热重载。
目前还暂未实现,当然也没有必要。
在更新新插件时,通过中央服务器对每一个子被更新的服务器进行进程环境保护,缓存代理服务器上客户端的请求包,等待被更新的服务端计算完毕之后,对其响应包进行缓存,在被监控的服务器处于安全空闲状态的时候,将其被更新的插件的所有模块安全卸载掉,重新加载新的插件进来,通知中央服务器然后继续运行,中央服务器再通知所有代理服务器,继续转发请求包。
SquickPluginLoad -> 插件构造函数 -> Install -> Uninstall -> 插件析构函数-> SquickPluginUnload
模块构造函数 -> Awake -> Start -> AfterStart -> ReadyUpdate -> Update -> BeforeDestory -> Destory -> Finalize -> 模块析构函数
awake -> init -> after_init -> before_destry -> destry
表示一类逻辑业务的合集, 相对来说功能比较集中, 可以做到低耦合, 并且可以通过IOP
(面向接口编程)的方式来给其他模块提供耦合功能.例如LogModule等。
表示一系列Module的集合, 按照更大的业务来分类, 例如GameLogic插件, Navimesh插件等。
表示一个独立的完整功能的进程, 可以包含大量插件, 例如squick.exe启动时,加载各个插件来执行。
表示一维数据, 通常用来表示Object附带的任意一维数据结构, 当前可以为常用内置数据类型(bool
int
float
string
GUID
). 例如玩家对象
附带的血量,名称等数据。
表示二维数据, 通常用来表示Object附带的任意二维数据结构,结构与Excel的二维结构类似, 包含Row
和Column
, 并且结构可以通过Excel动态传入, 记录值可以为常用内置数据类型(bool
int
float
string
GUID
). 例如玩家对象的
附带的背包物体
。
表示游戏内动态创建的任意对象, 该对象可携带有Property
和Record
。
用于区别玩家连接或游戏对象的唯一ID。
游戏逻辑监听和产生事件, 用来解耦游戏逻辑。
程序结构
采用加载不同插件方式来实现不同服务功能,都可适合小、中、大型团队人员进行同时开发,各自只需将自己的功能封装到自己的插件里,通过模块接口实现跨插件调用,提高开发效率。
遵循google c++开发规范。
ref: https://blog.csdn.net/qq_41854911/article/details/125115692
Squick当前所有重要插件如下:
插件与模块的关系
每一个插件为一个动态链接库文件(.so文件),将功能代码封装为插件的模块,可通过插件来加载各个插件的功能模块。
每个插件可以包含一个或多个模块
在采用squick-dev容器编译打包出来的deploy文件,可以直接采用squick-runtime
容器运行。
docker pull i0gan/squick-runtime:1.0 # 拉取容器
... # 配置你的容器启动
docker cp deploy squick-runtime:/ # 拷贝部署文件到容器中
需要环境docker + kubernetes,采用Linux Docker进行编译,打包为发布版本,再采用K8s进行分布式运行。
ref: https://clang.llvm.org/docs/ClangFormatStyleOptions.html
下载安装: https://github.com/llvm/llvm-project/releases/tag/llvmorg-16.0.0-rc4
进入 {project_tools}/tools/format
点击 clang_format.bat即可格式化src下的proto和c++代码