从零开始搭建游戏服务器 第七节 创建GameServer

目录

  • 前言
  • 正文
    • 创建GameServer模块
    • 修改配置
    • 创建NettyClient连接到登录服
    • 登录服修改
    • 创建协议
    • 游戏服注册到登录服
  • 总结

前言

上一节我们使用自定义注解+反射简化了协议解包和逻辑处理分发流程。
那么到了这里登录服登录服的架构已经搭建的差不多了,一些比较简单的、并发量较低的游戏,希望使用单体服务器,其实就在这LoginServer的基础上继续开发即可。

但是一些需要能支撑高一些并发,并且希望做到能横向扩容的游戏,就需要用到分布式的思想来做。

笔者这边先简单地将其分成 登录服和游戏服。
登录服用于提供客户端信息入口,处理账号鉴权和协议转发的功能。
游戏服用于处理游戏业务的具体逻辑,可以设定不同的游戏服处理不同的功能。比如游戏服A用于跑主城地图的逻辑,游戏服B用于跑副本相关逻辑。

玩家连接到登录服(前面应该还有一层做负载均衡的nginx用来选择一个低负载的登录服),在登录服登录账号,选择一个角色,然后登录服分配一个游戏服将玩家后续协议转发到游戏服中进行处理。

这么做的好处在于,当游戏服的人数变多,一台机器无法支撑,可以随时在其他机器上创建更多的游戏服进程进行扩容。
而当一台服务器crash时,可以将玩家切换到另一台服务器上游玩。

而我们本节内容就是创建一个游戏服GameServer。

正文

首先思考一下GameServer启动需要做些什么。

  1. 需要与登录服连接以便接受协议转发
  2. 需要注册协议分发器ProtoDispatcher用于处理业务逻辑

那我们一步一步来。

创建GameServer模块

ctrl+alt+shift+s进入项目设置,选中Modules, 右键根模块创建一个gameServer子模块
创建子模块
创建GameMain.java作为程序入口, 让其继承BaseMain,并启动Spring容器。

@Slf4j
@Component
public class GameMain extends BaseMain {public static void main(String[] args) {AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(GameBeanConfig.class);applicationContext.start();GameMain gameMain = SpringUtils.getBean(GameMain.class);gameMain.init();System.exit(0);}@Overrideprotected void handleBackGroundCmd(String cmd) {}@Overrideprotected void initServer() {}
}

修改配置

现在将登录服的端口配置到common.conf中,注意现在登录服不仅要提供一个对外的端口,还需要一个对内部服务的端口。

# mongo相关
...
# redis相关
...
# 登录服对外host 可以配置多个用逗分隔
login.player_host=127.0.0.1:9999
# 登录服对内host
login.inner_host=127.0.0.1:8888

修改CommonConfig.java

@Getter
@Component
@PropertySource("classpath:common.conf")
public class CommonConfig {@Value("${mongodb.host}")String mongoHost;@Value("${mongodb.user}")String mongoUser;@Value("${mongodb.password}")String mongoPassword;@Value("${mongodb.login.db}")String loginDbName;@Value("${redis.host}")String redisHost;@Value("${redis.password}")String redisPassword;@Value("${login.player_host}")String loginPlayerHost;@Value("${login.inner_host}")String loginInnerHost;
}

创建NettyClient连接到登录服

还记得我们在做客户端测试代码时使用了NettyClient连接到LoginServer吗?
现在GameServer也需要做一样的事情,我们将代码抽出到common模块中以便复用。

package org.common.netty;import ...@Slf4j
@Component
public class NettyClient {/*** 连接多个服务* @param hostStr "127.0.0.1:8081,127.0.0.1:8082"*/public HashMap<String, Channel> start(String hostStr, BaseNettyHandler nettyHandler) {HashMap<String, Channel> map = new HashMap<>();String[] hostArray = hostStr.split(",");if (hostArray.length == 0) {log.error("hostStr split error! hostStr = {}", hostStr);return map;}for (String host : hostArray) {String[] split = host.split(":");if (split.length != 2) {log.error("host list config error! host = {}", host);return map;}String ip = split[0];int port = Integer.parseInt(split[1]);Channel channel = start(ip, port, nettyHandler);map.put(host, channel);}return map;}/*** 连接单个服务*/public Channel start(String host, int port, BaseNettyHandler nettyHandler) {Channel channel;final EventLoopGroup group = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.group(group);bootstrap.channel(NioSocketChannel.class);bootstrap.handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();NettyServer.pipelineConfig(pipeline);// ----------  自定义消息处理器 -----------if (nettyHandler != null) {pipeline.addLast(nettyHandler);}}});ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port)).sync();channel = future.channel();} catch (InterruptedException e) {throw new RuntimeException(e);}Runtime.getRuntime().addShutdownHook(new Thread(group::shutdownGracefully));log.info("Start NettyClient ok! host = {}, port = {}", host, port);return channel;}
}

在NettyClient.start时需要传入服务端地址,以及一个自定义的NettyHandler来做协议处理逻辑,我们创建一个GameToLoginNettyHandler.java,用于处理游戏服连接到登录服的逻辑处理。

package org.game.handler;import .../*** GameServer连接登录服的NettyHandler* 主要做两件事* 1. 连接上登录服后主动推送本服务的信息进行注册* 2. 定时ping一下登录服*/
@Slf4j
public class GameToLoginNettyHandler extends BaseNettyHandler {/*** 收到协议数据*/@Overrideprotected void channelRead0(ChannelHandlerContext ctx, byte[] msg) throws Exception {//TODO 转发到对应的Actor进行处理}/*** 建立连接*/@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();String ip = address.getAddress().getHostAddress();log.info("连接登录服—成功:ip = {}", ip);}
}

同时修改initServer,用于启动NettyClient

	protected void initServer() {CommonConfig commonConfig = SpringUtils.getBean(CommonConfig.class);// netty启动, 连接到登录服NettyClient nettyClient = SpringUtils.getBean(NettyClient.class);String loginInnerHost = commonConfig.getLoginInnerHost();nettyClient.start(loginInnerHost, new GameToLoginNettyHandler());//}

好,GameServer服务基本搭建完成。接下来我们修改LoginServer,使其开放一个用于内部服务连接的端口。

登录服修改

修改LoginBeanConfig,注册两个NettyServer的bean

@Slf4j
@Configuration
@ComponentScan(basePackages = {"org.login", "org.common"})
public class LoginBeanConfig {/*** 对外的netty服务*/@Bean("playerNettyServer")NettyServer getPlayerNettyServer() {LoginNettyHandler handler = new LoginNettyHandler();return new NettyServer(handler);}/*** 对内的netty服务*/@Bean("innerNettyServer")NettyServer getInnerNettyServer() {LoginToGameNettyHandler handler = new LoginToGameNettyHandler();return new NettyServer(handler);}
}

创建一个LoginToGameNettyHandler.java, 用于处理登录服与游戏服之间的信息交互

package org.login.handler;import .../*** 游戏服netty事件处理*/
@Slf4j
public class LoginToGameNettyHandler extends BaseNettyHandler {/*** 收到协议数据*/@Overrideprotected void channelRead0(ChannelHandlerContext ctx, byte[] msg) throws Exception {}/*** 建立连接*/@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();String ip = address.getAddress().getHostAddress();if (ctx.channel().isActive()) {log.info("内部服务创建连接—成功:ip = {}", ip);}}}

修改initServer,增加启动对内的NettyServer

    @Overrideprotected void initServer() {CommonConfig commonConfig = SpringUtils.getBean(CommonConfig.class);...LoginConfig loginConfig = SpringUtils.getBean(LoginConfig.class);// 对内netty启动NettyServer innerNettyServer = SpringUtils.getBean("innerNettyServer");innerNettyServer.start(loginConfig.getInnerPort());// 对外netty启动NettyServer playerNettyServer = SpringUtils.getBean("playerNettyServer");playerNettyServer.start(loginConfig.getPlayerPort());log.info("LoginServer start!");}

那么登录服和游戏服都修改完毕,当你启动了LoginServer再启动GameServer,会发现控制台打印 内部服务创建连接 的字样,表示游戏服已经连接上了登录服。

但是这还不够,连接上了还需要将双方的信息交换,包括游戏服id,登录服id等。注册成功之后,该游戏服节点才能被标记为可用节点。

创建协议

我们创建一个新的proto文件,用于归纳内部服务之间的信息交互。
创建InnerServerMsg.proto

syntax = "proto3";option java_outer_classname = "InnerServerMsg";
option java_package = "org.protobuf";
/**
内部服务之间的协议*/// 游戏服注册到登录服
message G2LRegister { // 游戏服注册到登录服,返回S2CPlayerRegisterint32 serverId = 1; // 游戏服id
}
message L2GRegister {bool success = 1;   // 是否成功int32 serverId = 2;  // 登录服id
}// 登录服转发到游戏服的客户端上行协议
message L2GClientUpMsg {int64 playerId = 1; // 玩家idbytes data = 2;     // 协议数据
}

这个协议由GameServer发起,将自己注册到登录服。然后登录服回包给游戏服,将登陆服的id回复给游戏服进行保存。
另外L2GClientUpMsg用于登陆服将玩家的协议转发到游戏服。

游戏服注册到登录服

我们修改GameToLoginNettyHandler使其在连接完成后发送注册协议到登陆服,同时增加L2GRegister的协议解析。

	/*** 收到协议数据*/@Overrideprotected void channelRead0(ChannelHandlerContext ctx, byte[] msg) throws Exception {//TODO 转发到对应的Actor进行处理Pack decode = PackCodec.decode(msg);int cmdId = decode.getCmdId();if (cmdId == ProtoEnumMsg.CMD.ID.GAME_SERVER_REGISTER_VALUE) {// login返回到登录服的协议InnerServerMsg.L2GRegister g2LRegister = InnerServerMsg.L2GRegister.parseFrom(decode.getData());int serverId = g2LRegister.getServerId();LoginServerManager loginServerManager = SpringUtils.getBean(LoginServerManager.class);loginServerManager.registerLoginServer(serverId, ctx.channel());log.info("登录服服注册,serverId={}", serverId);return;}}/*** 建立连接*/@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();String ip = address.getAddress().getHostAddress();log.info("连接登录服—成功:ip = {}", ip);//发送注册协议GameConfig gameConfig = SpringUtils.getBean(GameConfig.class);InnerServerMsg.G2LRegister.Builder builder = InnerServerMsg.G2LRegister.newBuilder();builder.setServerId(gameConfig.getServerId());Pack pack = new Pack(ProtoEnumMsg.CMD.ID.GAME_SERVER_REGISTER_VALUE, builder);ctx.writeAndFlush(PackCodec.encode(pack));}

创建LoginServerContext和LoginServerrManager,用来存放登陆服节点的数据。

package org.game.obj;import io.netty.channel.Channel;public class LoginServerContext {private final int serverId;private Channel channel;public LoginServerContext(int serverId, Channel channel) {this.serverId = serverId;this.channel = channel;}public int getServerId() {return serverId;}public Channel getChannel() {return channel;}public void setChannel(Channel channel) {this.channel = channel;}
}
package org.game;import ...
@Slf4j
@Component
public class LoginServerManager {private Map<Integer, LoginServerContext> loginServerContextMap = new HashMap<>();public void registerLoginServer(int serverId, Channel channel) {if (loginServerContextMap.containsKey(serverId)) {log.error("游戏服节点已经注册 serverId = {}", serverId);return;}loginServerContextMap.put(serverId, new LoginServerContext(serverId, channel));}
}

接下来修改LoginToGameNettyHandler,当收到注册协议时,保存游戏服信息并回包。

	/*** 收到协议数据*/@Overrideprotected void channelRead0(ChannelHandlerContext ctx, byte[] msg) throws Exception {Pack decode = PackCodec.decode(msg);int cmdId = decode.getCmdId();if (cmdId == ProtoEnumMsg.CMD.ID.GAME_SERVER_REGISTER_VALUE) {InnerServerMsg.G2LRegister g2LRegister = InnerServerMsg.G2LRegister.parseFrom(decode.getData());int serverId = g2LRegister.getServerId();log.info("游戏服注册,serverId={}", serverId);GameServerManger gameServerManger = SpringUtils.getBean(GameServerManger.class);gameServerManger.registerGameServer(serverId, ctx.channel());LoginConfig loginConfig = SpringUtils.getBean(LoginConfig.class);InnerServerMsg.L2GRegister.Builder builder = InnerServerMsg.L2GRegister.newBuilder();builder.setSuccess(true);builder.setServerId(loginConfig.getServerId());Pack pack = new Pack(cmdId, builder);ctx.writeAndFlush(PackCodec.encode(pack));}}

我们创建一个GameServerContext和GameServerManager用来存放游戏服节点的数据。

/*** 游戏服节点的信息*/
public class GameServerContext {/*** 服务id*/private final int serverId;/*** 信息通道*/private final Channel channel;public GameServerContext(int serverId, Channel channel) {this.serverId = serverId;this.channel = channel;}public int getServerId() {return serverId;}public Channel getChannel() {return channel;}
}
/*** 游戏服管理器*/
@Slf4j
@Component
public class GameServerManger {private Map<Integer, GameServerContext> gameServerContextMap = new HashMap<>();public void registerGameServer(int serverId, Channel channel) {if (gameServerContextMap.containsKey(serverId)) {log.error("游戏服节点已经注册 serverId = {}", serverId);return;}gameServerContextMap.put(serverId, new GameServerContext(serverId, channel));}public int gameServerNum() {return gameServerContextMap.size();}/*** 随机一个游戏服节点*/public GameServerContext randomGameServer() {return RandomUtils.selectOne(gameServerContextMap.values());}
}

一个很简单的缓存管理器,里面就放一个HashMap用来存放游戏服Id=>GameServerContext。

总结

上面的代码逻辑懒得看的话我在这里做一次总结。

  1. 游戏服在连接上登录服时发送自己的id到登录服。
  2. 登录服收到游戏服注册协议,将游戏服的id缓存起来纳入管理用于协议转发。
  3. 登录服回包告诉游戏服自己的服务器id。
  4. 游戏服记录登录服id并纳入管理,用于通过登录服给客户端回包。

这一节的逻辑比较简单,没有纳入新的技术点。基本都是使用好我们之前开发好的功能进行代码复用。比如BaseMain、NettyClient、NettyServer。在平时的开发过程中遇到需要多次使用的代码块,可以积极抽象,减少重复代码的产出。

下一节我们会对LoginServer的账号登录流程进行优化,并开发角色的创建与登录。
基本的思路是这样:一个账号可以有多个角色,在登录账号后返回角色列表给客户端,并且为该账号分配一个游戏服节点,客户端选择一个角色进行创建登录,角色创建登录的协议将由登录服转发到游戏服进行处理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/287404.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

elementui的table根据是否符合需求合并列

<el-table :data"tableData" border style"width: 100%;" :span-method"objectSpanMethodAuto"><!-- 空状态 --><template slot"empty"><div><img src"/assets/images/noData.png" /></di…

【多模态融合】SuperFusion 激光雷达与相机多层次融合 远距离高清地图预测 ICRA 2024

前言 本文介绍激光雷达与相机进行多层次融合&#xff0c;包括数据级融合、特征级融合和BEV级融合。 融合后的BEV特征可以支持不同的任务头&#xff0c;包括语义分割、实例编码和方向预测&#xff0c;最后进行后处理生成高清地图预测&#xff0c;它是来自ICRA 2024的。 会讲解…

【Java并发知识总结 | 第五篇】深入理解Synchronized底层原理(Monitor对象、Synchronized锁优化)

文章目录 5.深入理解Synchronized底层原理&#xff08;Monitor对象、Synchronized锁优化&#xff09;5.1Synchronized的特性5.1.1原子性5.1.2可见性5.1.3有序性5.1.4可重入性 5.2Synchronized的用法5.3Synchronized的两种同步方式4.3.1同步代码块5.3.2同步方法 5.4Synchronized…

脏牛提权(靶机复现)

目录 一、脏牛漏洞概述 二、漏洞复现 1.nmap信息收集 1.1.查看当前IP地址 1.2.扫描当前网段&#xff0c;找出目标机器 1.3.快速扫描目标机全端口 三、访问收集到的资产 192.168.40.134:80 192.168.40.134:1898 四、msf攻击 1.查找对应exp 2.选择对应exp并配置相关设…

uniApp中使用小程序XR-Frame创建3D场景(2)加载模型

上篇文章讲述了如何将XR-Frame作为子组件集成到uniApp中使用&#xff0c;只完成了简单的环境搭建&#xff0c;这篇文章讲解如何加载3D模型。 1 加入模型加载标签 在XR-Frame框架中&#xff0c;加载资源都是在wxml文件的标签中实现的。下面是wxml中完整的代码 index.wxml &l…

java Web线上网游商品交易平台用eclipse定制开发mysql数据库BS模式java编程jdbc

一、源码特点 jsp线上网游商品交易平台是一套完善的web设计系统&#xff0c;对理解JSP java SERLVET mvc编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,eclipse开发&#xff0c;数据库为Mysql5.0…

React Native 应用打包

引言 在将React Native应用上架至App Store时&#xff0c;除了通常的上架流程外&#xff0c;还需考虑一些额外的优化策略。本文将介绍如何通过配置App Transport Security、Release Scheme和启动屏优化技巧来提升React Native应用的上架质量和用户体验。 配置 App Transport…

Linux文件和文件夹操作

一、文件操作 功能项命令实例作用文件创建vi /opt/learn/hello.txt 在目录/opt/learn下创建文件hello.txt并进入vi编辑界面 touch /opt/learn/test在目录/opt/learn下创建空白文件testcat > /opt/catfile创建文件catfile并在屏幕上输入内容&#xff0c;最后按 Ctrl D 退出…

【Node.js】WebSockets

概述 WebSockets是一种在浏览器和服务器之间建立持久连接的协议&#xff0c;它允许服务器主动推送数据给客户端&#xff0c;并且在客户端和服务器之间实现双向通信。 建立连接&#xff1a;客户端通过在JavaScript代码中使用WebSocket对象来建立WebSockets连接。例如&#xff1…

如何定位预防死锁

如何定位&预防死锁 什么是死锁&#xff1f; 简单来说就是并发环境下&#xff0c;两个或两个以上的线程互相等待资源&#xff0c;导致“永久阻塞”的现象 代码示例&#xff1a; public class Main {private static Object resource1 new Object();private static Objec…

达梦数据库自动备份(全库)+还原(全库) 控制台

一 前提 1.安装达梦数据库DB8(请参照以前文章) 我的数据库安装目录是 /app/dmDB8 2.已创建实例 (请参照上一篇文章) 二 准备测试数据 三 自动备份步骤 1.开启归档模式 开启DM管理工具管理控制台 弹不出来工具的 输入命令 xhost 第一步 将服务器转换为配置状态 右键-&g…

Kibana的安装(Linux版)

Kibana是一个针对Elasticsearch的开源分析及可视化平台&#xff0c;用来搜索、查看交互存储在Elasticsearch索引中的数据。使用Kibana&#xff0c;可以通过各种图表进行高级数据分析及展示。 Kibana让海量数据更容易理解。它操作简单&#xff0c;基于浏览器的用户界面可以快速创…

汽车电子行业知识:智能汽车电子架构

文章目录 3.智能汽车电子架构3.1.汽车电子概念及发展3.2.汽车电子架构类型3.2.1.博世汽车电子架构3.2.2.联合电子未来汽车电子架构3.2.3.安波福汽车电子架构3.2.4.丰田汽车电子架构3.2.5.华为汽车电子架构 3.智能汽车电子架构 3.1.汽车电子概念及发展 汽车电子是车体汽车电子…

阿里云对象存储OSS入门

阅读目录 一、阿里云OSS的使用 1、OSS是什么&#xff1f;2、OSS的使用 二、阿里云OSS的使用三、图床的搭建四&#xff1a;图床绑定阿里云OSS 编写不易&#xff0c;如果我的文章对你有帮助的话&#xff0c;麻烦小伙伴还帮忙点个赞再走&#xff01; 如果有小伙伴觉得写的啰嗦&am…

基于SIR模型的疫情发展趋势预测算法matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 基于SIR模型的疫情发展趋势预测算法.对病例增长进行SIR模型拟合分析&#xff0c;并采用模型参数拟合结果对疫情防控力度进行比较。整体思路为采用SIR微分方程模型…

PC电脑技巧[笔记本通过网线访问设备CMW500]

笔记本局域网访问设备 现在我有一台CMW500,我要用笔记本去访问它,但是我发现没有路由器就是不能够访问,通过网线连接设备就是ping不通: 这里设置TCP/IPv4的IP地址如下,这时候就可以pin通了:

Aspose.PDF功能演示:在 JavaScript 中合并两个 PDF 文件

在 Web 应用程序的世界中&#xff0c;处理和操作文档是一项常见的要求。当谈到 PDF 文件时&#xff0c;开发人员经常发现自己需要将 PDF 合并为单个 PDF 文件。因此&#xff0c;在这篇博文中&#xff0c;我们将探索如何使用强大的 PDF 库在 JavaScript 中轻松合并两个 PDF 文件…

【学习】软件测试行业 ,有哪些以就业为主的学习侧重点

今天给所有入行软测的同学们&#xff0c;帮大家梳理下以就业为主的学习侧重点&#xff0c;简单来说就是【这些都是重点&#xff0c;圈起来&#xff0c;要考的】&#xff0c;有需要的小伙伴可以往下看。 建议一&#xff1a;一定要学习一门编程语言&#xff0c;再开始使用自动化测…

Qt Design Studio 软件怎么用(详细+通俗+有趣)

建议&#xff1a;本文长期更新&#xff0c;建议点赞/收藏&#xff01; 1. 啥是Qt Design Studio&#xff1f; Qt Design Studio 是一个用于设计和开发用户界面的工具&#xff0c;特别适合开发跨平台应用程序。它结合了UI设计和开发的工作流程&#xff0c;使得设计师和开发者可…

[WTL/Win32]_[初级]_[如何设置ListView的列宽不出现水平滚动条]

场景 开发WTL/Win32的程序时&#xff0c;经常会用到表格控件CListViewCtrl。这个控件需要设置列的宽度&#xff0c;当用完100%的宽度来平均分配给列宽时&#xff0c;一加载数据多&#xff0c;就会出现垂直滚动条后&#xff0c;水平滚动条也会同时出现的问题。怎么设置才能让水…