# sunrise **Repository Path**: lieng618/sunrise ## Basic Information - **Project Name**: sunrise - **Description**: 高性能分布式游戏服务器框架 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 2 - **Created**: 2024-07-17 - **Last Updated**: 2026-03-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: 游戏, 游戏服务器, 游戏开发 ## README # sunrise ## 简介 sunrise 是一个分布式游戏服务器框架,底层网络通信基于 Netty实现。 框架不依赖任何第三方中间件,减少开发与部署的负担,只需具备 Java 环境即可运行。 ## 框架结构 sunrise 由多个模块组成: - **sunrise-common**:基础网络服务模块。 - **sunrise-center**:中心服模块。 - **sunrise-external**:对外服模块。 - **sunrise-game**:游戏服模块。 - **sunrise-global**:公共服模块。 - **sunrise-http**:HTTP 服务模块。 - **sunrise-rpc**:RPC 服务模块。 - **sunrise-gen**:代码生成模块。 - **sunrise-tables**:配置表生成模块。 - **sunrise-client**:测试客户端模块。 ## 框架图 框架采用分布式设计,通过中心节点同步其他服务信息,所有服务均支持动态增加和减少。 ![sunrise.png](https://s2.loli.net/2024/10/08/C5NSBzgtDVvdixq.png) - 对外服与游戏服之间互相连接。 - 每个游戏服内部包含一个RPC节点,每个公共服内部包含一个RPC节点。 - 游戏服与每个公共服、所有的公共服之间互相连接,但游戏服间的RPC节点并没有连接(如果游戏服之间互连会导致业务逻辑混乱,一般公共数据的处理都放在公共服)。 - 对外服、游戏服、公共服、HTTP 服均与中心服连接,并定时上报当前服务信息(IP、端口、类型)。 - 客户端与对外服建立连接,会先从Http服务获取对外服地址,并且会分配一个游戏服的id,后续的请求都会发给此游戏服进行处理。 - 中心服将其他节点信息同步给需要的服务。 - 游戏服需要对外服的信息,收到中心服下发的地址后,会检测连接。 - RPC节点需要其他RPC节点的信息,收到中心服下发的地址后,会检测连接。 - Http服需要对外服和游戏服的信息,收到中心服下发的地址后,将信息更新到内存中。 ## 网络模块 sunrise 抽象了网络通信模块,基于 Netty 进行封装,简化了服务器和客户端的创建。所有模块均基于此网络模块进行扩展。 [sunrise-common](https://gitee.com/lieng618/sunrise-common) ## 对外服 对外服负责监听客户端连接和游戏服连接,转发消息。 - 包含一个 `ReportClient`,连接到中心服。 - 包含一个 `BaseServer`,监听客户端连接和游戏服的`BaseClient`的连接。 - 对外服与客户端建立连接后,规定接收到的第一条消息为校验消息,约定消息前缀为"CLIENT_CONNECT_",后面拼接上要连接的gameId。若解析出的gameId不存在则会关闭连接。 - 对外服与游戏服建立连接后,规定接收到的第一条消息为校验消息,约定消息前缀为"GAME_CONNECT_",后面拼接上游戏服自己的gameId。若解析出的gameId已存在则会关闭连接。 - 消息处理类为`ExternalServerHandler`和`ExternalWsServerHandler`。 - 对外服和游戏服通信的数据结构为 `GameMessage`,收到客户端发来的字节数据后,将字节数据和客户端信息包装到`GameMessage`中发送给此客户端连接的游戏服。收到游戏服的数据时,将`GameMessage`中的字节数据发给对应的客户端。 - 包含一个 `WebSocket` 服务器,同时支持 WebSocket 协议。 - 默认规则:TCP 端口 + 1 为 WebSocket 端口(如 TCP 端口为 `10000`,则 WebSocket 端口为 `10001`)。因此一个对外服会占用2个端口。 - `external_system`sql表会记录所有对外服的运行状态,启动多个对外服时,通过此表检测服务id是否重复,服务id可用时将会分配端口,启动服务器。 ## 游戏服 游戏服连接多个对外服,并创建 `RpcNode` 用于 RPC 注册与调用。 - 包含一个 `ReportClient`,连接到中心服。 - 包含多个 `BaseClient`,连接到多个对外服。 - 多个 `BaseClient`采用同一个消息管理器`GameMasterMessageManager`,所有从对外服接收到的玩家消息,都会放入同一个消息队列中,进行单线程处理。 - 包含一个`RpcNode`,用于RPC的注册与调用。 - 游戏服调用其他节点的RPC方法时,调用的返回会在消息管理器`GameMasterMessageManager`中处理。 - 如果游戏服注册了RPC方法,收到其他节点发来的调用时,使用的消息管理器为`RpcNode.rpcServer`中的`RpcServerMessageManager`,与`GameMasterMessageManager`消息管理器不在同一线程。因此,游戏服注册的RPC 服务不能直接对玩家数据进行操作,以免出现多线程问题。 - RPC方法内需要对玩家数据进行操作时,使用方法`AsyncEventManager.addAsyncEvent()`,添加异步任务,所有的异步任务都会在消息管理器`GameMasterMessageManager`中处理。 - 业务逻辑处理 - [logic包下实现了协议处理函数](https://gitee.com/lieng618/sunrise/tree/master/game/src/org/sunrise/game/game/logic),使用注解`@MsgHandlerClass`、`@MsgHandlerMethod`将函数标记为协议处理函数,实现对protobuf协议的处理。新增协议包时,会在`ProtoParserUtils`类中进行自动注册。 - [modules包下实现了玩家模块](https://gitee.com/lieng618/sunrise/tree/master/game/src/org/sunrise/game/game/modules),继承类`BaseModule`添加新的模块,包含数据的加载与存储、跨天刷新、登录发包等。新增模块时,需在`HumanObject.createModules()`中进行注册。 - 业务开发流程 - [在gen模块下](https://gitee.com/lieng618/sunrise/tree/master/gen/src/org/sunrise/game/gen/genProto),新增协议文件,使用gen.bat生成协议代码。 - [在logic包下](https://gitee.com/lieng618/sunrise/tree/master/game/src/org/sunrise/game/game/logic),新增协议处理函数,在`ProtoParserUtils`类中会进行自动注册。 - [在modules包下](https://gitee.com/lieng618/sunrise/tree/master/game/src/org/sunrise/game/game/modules),新增玩家模块,在`HumanObject.createModules()`中进行注册。 - [在global模块下](https://gitee.com/lieng618/sunrise/tree/master/global/src/org/sunrise/game/global/service),新增RPC方法,使用genRpc生成CallEnum文件,在游戏服模块中进行RPC调用。 - 业务开发指南 - 详见文档 [业务模块开发指南](https://gitee.com/lieng618/sunrise/blob/master/game/%E4%B8%9A%E5%8A%A1%E6%A8%A1%E5%9D%97%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97.md) ## 公共服 公共服仅需创建一个 `RpcNode`,注册当前服务提供的 RPC方法。 - 公共服可以启动多个,注册不同的RPC方法,负责不同的业务模块。在配置文件中配置同一个中心服地址,即可让所有RPC节点互相连接。 - RPC节点互相建立连接时,会将当前节点注册的RPC方法发给对方。某个节点下线时,其他节点也会收到通知,移除下线节点所管理的RPC方法。 - RPC调用有两种模式,将调用发给全部节点、或者随机节点发送。 - 处理公共数据时,最好只用单个节点注册业务模块。比如聊天模块,只用一个节点存储聊天记录,就无需考虑多个节点如何保证数据的一致性。 - 不涉及到公共数据的处理时,就可以多个节点注册同一个业务模块。RPC调用时随机发送给某一个节点,不用关心哪个节点处理。 ## RPC 服务 RPC模块提供 RPC通信支持,无需单独启动,其他模块依赖此模块实现 RPC 调用。 - 包含一个 `ReportClient`,连接到中心服。 - 包含一个 `BaseServer`,监听客户端(其他的RPC节点内部的`BaseClient`)连接。 - 消息处理器为`RpcServerMessageManager`, 会处理其他节点的`BaseClient`发来的call调用,并将处理后的返回数据发回去。 - 包含多个 `BaseClient`,连接到其他RPC节点的 `BaseServer`。 - 消息处理器为`RpcClientMessageManager`,会向其他节点的`BaseServer`发送call调用,并对调用后的返回数据进行处理。 - 创建 RPC节点:`RpcNodeManager.createRpcNode(nodeId)` - 注册服务包路径 ```java var rpcNode = RpcNodeManager.createRpcNode(1); // 添加当前模块要注册的rpc ArrayList list = new ArrayList<>(); list.add("org.sunrise.game.global.service.union"); list.add("org.sunrise.game.global.service.pvp"); CallUtils.init(rpcNode.getNodeId(), list, CallEnum.class); //启动 rpcNode.start(new DbService()); ``` - 部署方式上,服务注册支持随意的拆分和组合,既可以全部放入同一个RPC节点,也可以拆分到不同的RPC节点。 发起call调用时,如果当前节点注册了此方法,则一定会由当前节点处理。 - 比如原本在跨服注册的公会服务,后期想改为本服公会,只需在游戏服进程注册公会服务,则后续发起的call调用一定会由本服RPC节点处理,无需修改业务代码就实现了业务迁移。 - `CallEnum.java`相当于双方的通信约定,此文件可使用Gen模块进行生成(类`GenRpcStartUp`,需更改自己的服务包路径和生成路径)。 - 包[`org.sunrise.game.rpc.example`](https://gitee.com/lieng618/sunrise/tree/master/rpc/src/org/sunrise/game/rpc/example)为完整的使用用例,包含多个RPC节点的启动、多个节点RPC方法的创建与调用 ## HTTP 服务 HTTP 服务从中心服获取对外服地址和 gameId列表,客户端以发送url请求的方式获取服务器地址和gameId。 - 包含一个 `ReportClient`,连接到中心服。 ## GM后台 中央服内部创建web服务器,为后台网页提供api接口。 - `AdminServer`提供接口,`admin-ui`目录下为前端页面。 - 中央服配置文件中配置后台登录账号、密码,网页端口号。 - 中央服启动时会在控制台输出后台地址,浏览器输入地址后进行登录。 - ``` INFO Server.start(AdminServer.java:113) AdminServer started on : http://192.168.105.174:8010/ ``` - 目前是由中央服创建的Javalin Web服务器托管静态文件,后续可通过nginx进行托管。 - 实现功能:节点监控、节点管理、配置更新、发送邮件、玩家下线、操作日志、用户管理。 ## 客户端 客户端与服务器之间使用 Protobuf 协议通信,部分协议结构如下: ```protobuf message MBasePacketData { TOPIC packet_type = 1; uint32 packet_id = 2; bytes packet_data = 3; } enum TOPIC { TOPIC_TYPE_LOGIN = 0 ; } enum FROM_CLIENT { C2S_Login = 0; //账号登陆 C2S_HumanList = 1; //角色列表 C2S_SelectHuman = 2; //选择角色 } enum FROM_SERVER { S2C_Login = 0; //账号登陆 S2C_HumanList = 1; //角色列表 S2C_SelectHuman = 2; //选择角色 } message STHumanShowInfo { uint32 pos = 1; //角色位置 string name = 2; //角色名字 string human_id = 3; //角色id uint32 level = 4; //角色等级 uint32 server_id = 5; //所属服务器 } message MC2S_Login { string uid = 1; //用户UID } message MC2S_SelectHuman { uint32 pos = 1; //角色位置 uint32 server_id = 2; //所属服务器 } message MS2C_Login { uint64 account_id = 1; //账号ID } message MS2C_HumanList { repeated STHumanShowInfo human_list = 1; //角色列表 } ``` - `packet_type`为封包类型,一个业务模块使用一种类型。 - `packet_id`为封包ID,一个业务模块的不同操作使用不同的ID。 - `packet_data`为封包数据,规定每个封包ID前缀加上M,则为此操作的封包数据格式。 - 每个协议文件的包名必须按照此规则命名,例如:`TOPIC_TYPE_LOGIN`,对应的协议文件`login.proto`包名为`LoginProto`。 - 按照此规则命名,即可自动注册到协议数据解析器:`ProtoParserUtils.protoParserMap`。 客户端通信示例: - sunrise-client: [Java版本通信客户端](https://gitee.com/lieng618/sunrise/tree/master/client/src/org/sunrise/game/client) - sunrise-goldminer:[网页版通信客户端](https://gitee.com/lieng618/sunrise-goldminer) - sunrise-goldminer-wx:[微信小游戏版通信客户端](https://gitee.com/lieng618/sunrise-goldminer-wx) - sunrise-client-unity:[unity版通信客户端](https://gitee.com/lieng618/sunrise-client-unity) WebSocket通信流程解析: - 从http服务器获取game_id和external_address,测试版本可以直接连接ip+port,但正式版本需要使用域名和ssl证书,所以连接的最终地址里,把ip替换为域名,拼接上/ws/port,后端由nginx解析转发给对应的端口。 ```js // 第一步:请求 game_id // 对应 url: https://url/game_id?uid=xxx const gameIdUrl = `https://${networkConfig.serverdomain}/game_id?uid=${uid}`; const gameIdData = await requestAsync(gameIdUrl); const serverId = gameIdData.server_id; // 第二步:请求 external_address // 对应 url: https://url/external_address?type=websocket const addressUrl = `https://${networkConfig.serverdomain}/external_address?type=websocket`; const addressData = await requestAsync(addressUrl); // 返回的 JSON 结构中有 address 字段,格式如 "192.168.1.5:10001" const rawAddress = addressData.address; const parts = rawAddress.split(':'); const targetPort = parts[1]; // 拿到对外服端口 // 正式上线需要域名,所以获取的ip+端口是没法用的 // 所以ip使用域名,拼接上/ws/port,后端会由nginx解析转发到对应的端口 const wsUrl = `wss://${networkConfig.serverdomain}/ws/${targetPort}`; ``` - nginx配置 - 此配置会转发给本机的指定端口,可根据环境配置为不同的服务器地址。 ```nginx server { listen 443 ssl; server_name goldminer.cloud www.goldminer.cloud; ssl_certificate /etc/nginx/cert/www.goldminer.cloud_bundle.crt; ssl_certificate_key /etc/nginx/cert/www.goldminer.cloud.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_session_timeout 20m; ssl_verify_client off; # 动态匹配 /ws/端口号 # 客户端连接地址示例: wss://www.goldminer.cloud/ws/10001 location ~ ^/ws/(?\d+)$ { rewrite ^/ws/\d+ / break; # 将路径中的端口号变量,拼接到 proxy_pass 中 proxy_pass http://127.0.0.1:$forward_port; # WebSocket 标准头 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; # 传递真实 IP proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; } # 将 /game_id 和 /external_address 转发到 Java 的 HTTP Server (8090) location ~ ^/(game_id|external_address) { proxy_pass http://127.0.0.1:8090; # 转发给你的 HttpServer.java # 传递真实IP等头信息 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` ## 启动 ### 安装 1. **安装 Maven** 参考 [Maven 官方安装指南](https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html) 2. **配置镜像(腾讯云)** 在 `settings.xml` 中添加以下配置: ```xml tencent tencent maven http://mirrors.cloud.tencent.com/nexus/repository/maven-public/ central ``` 3. **下载依赖项目** [sunrise-common](https://gitee.com/lieng618/sunrise-common) 4. **安装 sunrise-common** ```bash cd sunrise-common mvn install ``` 5. **构建 sunrise 项目** ```bash cd sunrise mvn clean package ``` 6. **安装 JDK(>=21)** [JDK 下载地址](https://jdk.java.net/archive/) 7. **安装 MySQL(推荐 5.7)** [MySQL 下载地址](https://dev.mysql.com/downloads/mysql/) ### Windows 环境 #### 环境配置 1. 修改 `config` 目录下的所有配置文件,配置 JDBC 地址。 2. 修改 `game-config.properties` 中的 `config.path`。 3. 修改 `start/create_sql_table.bat`,配置 JDBC 地址与 MySQL 安装路径。 4. 执行 `start/create_sql_table.bat` 创建数据表。 #### 快速启动 1. 执行 `start/server_run_all.bat` 启动所有服务器。 2. 执行 `start/client.bat` 启动测试客户端)。 #### 编译器启动 1. 在 IDE 中打开项目根目录下的 `pom.xml`。 2. 启动中心服: - 类:`CenterServerStartUp` - 参数:`{args[0]: config path, args[1]: center_id}` 3. 启动对外服: - 类:`ExternalServerStartUp` - 参数:`{args[0]: config path, args[1]: external_id}` 4. 启动跨服: - 类:`GlobalServerStartUp` - 参数:`{args[0]: config path, args[1]: global_id}` 5. 启动游戏服: - 类:`GameServerStartUp` - 参数:`{args[0]: config path, args[1]: gameId}` 6. 启动 HTTP 服务: - 类:`HttpServerStartUp` - 参数:`{args[0]: config path}` 7. 启动客户端: - 类:`ClientStartUp` > **注意**:所有模块启动顺序无先后之分,**同一服务的 ID 不能重复**,所有服务ID每次递增时+1。 > > 假设有两个对外服,服务ID可以分别为1、2。 > > 假设有两个游戏服,服务ID可以分别为1、2。 > > 假设有两个跨服,分别负责不同的模块,服务ID可以分别为1001、1002。 > > 跨服的服务ID不能与游戏服相同,(因为游戏服和跨服内部都包含一个RPC节点,所以需要使用不同的服务ID)。 > 跨服的服务ID从1001开始,与游戏服ID做出区分。中心服通过服务ID是否大于1000,判断RPC节点是否为跨服节点。 > > 节点ID的范围为1~4096,即最多支持4096个节点。 [配置详情](https://gitee.com/lieng618/sunrise-common/blob/master/src/org/sunrise/game/common/utils/IdGenerator.java) ### Linux 环境 #### 环境配置 1. 修改 `config` 目录下的配置文件,配置 JDBC 地址。 2. 修改 `game-config.properties` 中的 `config.path`。 3. 修改 `start/create_sql_table.sh`,配置 JDBC 地址与 MySQL 安装路径。 4. 执行 `start/create_sql_table.sh` 创建数据表。 #### 快速启动 1. **安装 PM2** ```bash npm install -g pm2 ``` 2. **开放端口** - External 和 RPC 端口按需开放,也可提前预留打开。 - MySQL: `3306` - Center: `8000` - HTTP: `8090` - External: `10000-100010` - RPC: `20000-20010` - GM:`8010` 3. **配置 IP地址** - 若所有模块运行在同一机器,需配置对外服的 `report.address` 为公网 IP。 - 若模块运行在不同机器,确保所有模块的 `master.address` 为中心服公网 IP,`report.address` 为各自机器的公网 IP。 4. **配置日志等级** - 配置文件中的 `log.level` 默认为 `DEBUG` 模式,此模式下会打印大量的日志,方便调试,可根据不同场景进行修改。 5. **启动所有服务** ```bash cd start ./server_run_all.sh ``` 6. **启动客户端** ![client.png](https://picturebed.pixelarrayai.com/356/client.png) - 可在Windows下使用client模块下的客户端进行测试,实现了完整的登录流程,支持tcp与websocket两种通信方式。 - 此客户端实现了模拟发送协议消息,并支持多个玩家登录,每个玩家在自己的窗口中发送。 - 需修改 `client-config.properties` 中的 HTTP 地址为 HTTP 模块绑定的服务器地址。 ## 联系我 有任何疑问和想法,都可以和我联系。 QQ:1906438581