目录
Netty与SpringBoot的游戏服务器
前言
一、项目整体流程
二、项目启动
1.克隆或下载该项目
2.导入idea,或者上一步用idea导入
3.导入数据库
4、启动redis
5、启动登录和游戏服务器
6、启动客户端操作
三、项目设计概要
1、整合springboot
2、整合google-protobuf
3、order的策略模式
总结
该项目为2017年入职第一家公司时的一个项目,当时仅凭着一腔热血,只身进入公司,也可以不要工资,只要能让我学习到项目经验,当然这不可取,实在是条件比较艰苦。言归正传,当初所使用的技术为mybatis+netty,还未使用到spring,其实是我当时还不会用。最近准备学习微服务和rpc等相关技术,所以又把这个项目掏出来重新复习一下netty了。
项目架构:
springboot-2.1.7
netty-4
fastjson
jedis(连接池)
mybatis
google-protobuf(消息传输协议)
项目亮点:
1、登录服务器与游戏服务器可分离,可在登录服务器做多个游戏服务器地址实现负载均衡
2、通过验证redis中的token判断是否有效登录,如无登录信息则无法进入游戏服务器,使用redis管理房间和大厅,提升IO读写速度,加快服务器响应
3、使用protobuf来最大程度减少网络请求中的时间和空间消耗
4、使用策略模式+线程池对消息做异步处理,提高服务器处理效率
客户端通过已知的服务器域名/IP进行登录
Git clone https://gitee.com/pineappleLB/mx-work.git
链接导入idea:file -> new -> Project from Version Control -> Git -> 粘贴上方链接
下载后导入:file -> Open -> 选择到文件位置(选择maven项目)
新建数据库`mxsoha`,将mxsoha.sql中的sql语句导入到数据库
可以修改单独从LoginServer中启动,为GameServer提供负载均衡,也可直接启动GameServer登录和游戏一起
@SpringBootApplication
public class GameApplication implements CommandLineRunner {
@Autowired
private GameServer gameServer;
@Autowired
private LoginServer loginServer;
@Autowired
private CommandLoader commandLoader;
public static void main(String[] args) {
new SpringApplicationBuilder(GameApplication.class).web(WebApplicationType.NONE).run(args);
}
@Override
public void run(String... args) throws Exception {
// 依次启动服务器
new Thread(loginServer).start();
new Thread(gameServer).start();
// 加载指令映射
commandLoader.loadCommand();
}
}
使用CommandLineRunner 接口在spring加载完成后启动nettyServer,
new SpringApplicationBuilder(GameApplication.class).web(WebApplicationType.NONE).run(args);
使用SpringBoot的非Web方式启动,不占用Web端口
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline()
// .addLast(new IdleStateHandler(5, 0, 0))
.addLast(new ProtobufVarint32FrameDecoder())
//给服务器添加编码解码器
.addLast(new ProtobufDecoder(BasicProto.BaseMessage.getDefaultInstance()))
.addLast(new ProtobufVarint32LengthFieldPrepender())
.addLast(new ProtobufEncoder())
//添加服务端业务处理类
.addLast(loginServerHandler);
}
proto文件:
/*请求消息*/
message BaseMessage {
int32 order = 1; //命令
bytes body = 2; // 消息体
}
// 登录请求
message LoginRequest {
string name = 1; // 姓名
string pass = 2; // 密码
}
// 登录返回实体
message LoginResponse {
Res res = 1; // 请求返回状态
int32 lobbyPort = 2; // 大厅端口
string lobbyHost = 3; // 大厅地址
User user = 4; // 用户信息
}
登录解析:
@Override
public void doMessage(BasicProto.BaseMessage msg, IOSession session) throws Exception {
UserProto.LoginRequest loginRequest = UserProto.LoginRequest.parseFrom(msg.getBody());
String name = loginRequest.getName();
String pass = loginRequest.getPass();
//调用service中的方法验证用户信息,并返回完整的用户信息
User u = loginUserService.userLogin(name, pass);
// 登录逻辑判断
// 省略....
}
添加protobuf的编解码类,以及全局protobuf基类原型,通过proto类的parseForm方法将基类中的实际请求参数转换为指定的类
private Map> handlerMap = new ConcurrentHashMap<>();
private final String BASE_PACKAGE = "club.pinea.handler";
private final String RESOURCE_PATTERN = "/**/*.class";
/**
* 加载command对应的处理类
*/
public void loadCommand() {
//spring工具类,可以获取指定路径下的全部类
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
try {
String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
ClassUtils.convertClassNameToResourcePath(BASE_PACKAGE) + RESOURCE_PATTERN;
Resource[] resources = resourcePatternResolver.getResources(pattern);
//MetadataReader 的工厂类
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
for (Resource resource : resources) {
//用于读取类信息
MetadataReader reader = readerFactory.getMetadataReader(resource);
//扫描到的class
String classname = reader.getClassMetadata().getClassName();
Class> clazz = Class.forName(classname);
//判断是否有指定主解
Head anno = clazz.getAnnotation(Head.class);
if (anno != null) {
//将注解中的类型值作为key,对应的类作为 value
// handlerMap.put(classname, clazz);
Object bean = SpringContextUtils.getBean(clazz);
if (bean instanceof TCPMessage) {
TCPMessage> tcpMessage = (TCPMessage>) bean;
handlerMap.put(anno.value(), tcpMessage);
}
}
}
} catch (IOException | ClassNotFoundException e) {
}
}
/**
* 获取处理类
*
* @param command
* @return
*/
public TCPMessage getHandler(int command) {
return handlerMap.get(command);
}
通过ResourcePatternResolver类获取项目中指定包下的类,并判断是否有包含自定义注解,将注解对应的order与消息中的order值对应,即可在netty的channelRead中获取到该次请求消息应该由哪个handler处理,即调用getHandler方法传入baseMessage中的order属性即可
项目地址:https://gitee.com/pineappleLB/mx-work
项目有部分内容未完全完成,仅作为学习框架之余的练手项目,如有兴趣可以自行修改完成,对项目有疑问或建议可以评论留言或者在gitee留言