最近在读《Netty实战》,便使用netty搭建了一套rts游戏的服务器框架!
服务器分为三个部分:
(1)gateServer网关服务器:顾名思义网关服务器负责协议的接收与分发
(2)lobbyServer大厅服务器:大厅服务器处理用户的登录,注册,创建房间,查看战绩等逻辑
(3)battleServer战斗服务器:负责处理房间内对战的游戏逻辑,使用单例模式,帧同步的方法实现同一房间内不同玩家之间的一致性
在这里简单介绍一下帧同步:
(1)客户端每隔一段时间采集用户的所有指令发送给服务端
(2)服务器每隔一段时间将当前所有的游戏数据推送给客户端
(3)客户端每次用户做出指令的时候先做出预处理,当收到服务端的指令后进行同步
(4)无论客户端还是服务端发送协议的时间间隔都是毫秒级,每秒至少发送20-30次,这样就能基本保证不同客户端之间的同步,就想帧动画一样,我们服务端发送的同步协议也可以理解为一次为一帧
服务器使用到的技术与框架:
使用maven 模块式管理(使项目目录看起来一目了然,利用好maven的继承关系,可以避免重复写很多pom文件中的标签),netty框架(应该是最好用的java通信框架吧),redis
下面上干货:
public class LobbySever {
//基础配置信息
//log日志
private static final Logger logger = LoggerFactory.getLogger(LobbySever.class);
//服务器IP(可配置到配置文件)
private static final String IP = "127.0.0.1";
//端口号(可配置到配置文件中)
private static final int port = 8088;
//分配用于处理业务的线程组数量 Runtime.getRuntime().availableProcessors()获取jvm可用的线程数
protected static final int BisGroupSize = Runtime.getRuntime().availableProcessors() * 2;
//每个线程组中线程的数量
protected static final int worGroupSize = 4;
//NioEventLoopGroup进行事件处理,如接收新连接以及数据处理
private static final EventLoopGroup bossGruop = new NioEventLoopGroup(BisGroupSize);
private static final EventLoopGroup workerGroup = new NioEventLoopGroup(worGroupSize);
protected static void run() throws Exception{
//serverBootstrap 服务端引导
ServerBootstrap bootStrap = new ServerBootstrap();
bootStrap.group(bossGruop, workerGroup);
//指定所使用的 channel 有nio oio linux有epoll(性能比nio强大的异步非阻塞)
bootStrap.channel(NioServerSocketChannel.class);
bootStrap.childHandler(new ChannelInitializer(){
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//ChannelPipeline链 将所有的业务逻辑层连接到一起
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new RtsEncoder());
pipeline.addLast(new RtsDecoder());
//pipeline.addLast("logging",new LoggingHandler(LogLevel.WARN));
//注册HeartBeatReqHandler
pipeline.addLast(new HeartBeatReqHandler());
//注册LoginHandler 多个channelHandler执行顺序为注册顺序
pipeline.addLast(new LoginHandler());
}
//ChannelOption设置tcp缓冲区的大小
}).option(ChannelOption.SO_BACKLOG, 1024)
//通过NODELY禁用Nagle,使消息立即发出,不用等到一点的数量才发出
.option(ChannelOption.TCP_NODELAY, true)
//保持长连接
.childOption(ChannelOption.SO_KEEPALIVE,true);
logger.info("LobbySever 启动TCP长连接完成!");
//sync导致当前Thread 阻塞,一直到绑定操作完成为止 bind方法绑定服务器
ChannelFuture f = bootStrap.bind(IP,port).sync();
f.channel().closeFuture().sync();
logger.info("LobbySever Socket服务器已启动完成!");
//closeFuture会一直阻塞到channel关闭 然后调用shutdown
shutdown();
}
protected static void shutdown(){
//关闭eventLoopGroup释放所有资源
bossGruop.shutdownGracefully();
workerGroup.shutdownGracefully();
}
public static void main(String[] args) throws Exception{
logger.info("开始启动LobbySever服务器...");
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
logger.info("装载spring容器成功");
LobbyManager lobbyManager = LobbyManager.getInstance();
lobbyManager.init();
logger.info("初始化管理器成功");
RedisUtil.getInstance();
run();
}
}
public class LobbyManager {
/**
* 协议管理器
*/
public static Map protocolManager;
/**
* 客户端连接管理器
*/
public static Map gateManager;
/**
* GameSession管理器
*/
public static Map sessionManager;
/**
* 玩家管理器
*/
public static Map playerManager;
public void init(){
protocolManager = LobbyProtocol.toMap();
gateManager = new HashMap();
sessionManager = new HashMap();
playerManager = new HashMap();
System.out.println("初始化管理器成功!");
}
private LobbyManager(){
}
private static LobbyManager lobbyManager = null;
/**
* 单例模式
* @return
*/
public static synchronized LobbyManager getInstance(){
if(lobbyManager == null){
lobbyManager = new LobbyManager();
}
return lobbyManager;
}
/**
* 将新获取的连接增加到gateManager中
* @param sessionId
* @param socketChannel
*/
public synchronized void addChannel(String sessionId,SocketChannel socketChannel){
//直接put 如果已经存在相当于其他地方登录,直接覆盖
if(socketChannel != null){
gateManager.put(sessionId, socketChannel);
}
}
/**
* 根据sessionId获取SocketChannel
* @param sessionId
*/
public SocketChannel getChannel(String sessionId){
if(StringUtils.isEmpty(sessionId)){
return null;
}
return gateManager.get(sessionId);
}
/**
* 新增gameSession
* 如果已经存在了 很可能存在同一个ip用户又进行了其他账号的登录,这时直接覆盖
* @param sessionId
* @param gameSession
*/
public synchronized void addSession(String sessionId,LobbyGameSession gameSession){
if(gameSession != null){
sessionManager.put(sessionId,gameSession);
}
}
/**
* 根据sessionId获取session
* @param sessionId
* @return
*/
public LobbyGameSession getSession(String sessionId){
if(StringUtils.isEmpty(sessionId)){
return null;
}
return sessionManager.get(sessionId);
}
/**
* 玩家离线,将玩家从gateManager中删掉
* @param uuid
*/
public synchronized void removeChannel(String sessionId){
gateManager.remove(sessionId);
//同时 删除掉玩家的session信息
sessionManager.remove(sessionId);
}
/**
* 新增用户到玩家管理器
* @param uuid
* @param player
*/
public synchronized void addPlayer(String uuid,Player player){
if(player != null){
playerManager.put(uuid,player);
}
}
/**
* 根据用户Id从用户管理器中获取用户
* @param uuid
* @return
*/
public Player getPlayer(String uuid){
if(StringUtils.isEmpty(uuid)){
return null;
}
return playerManager.get(uuid);
}
/**
* 从玩家管理器中删除用户
* @param uuid
*/
public synchronized void removePlayer(String uuid){
playerManager.remove(uuid);
}
}
gateManager是连接管理器,在channelHandler的active方法中,每次客户端连接成功后都会回调到active 方法中,即使有多个channelHandler但是同一个tcp长连接,只要连接不断开就只会调用一次active方法,上文中提到过执行channelhandler的顺序为handler的注册顺序
重写encode和decode:
Encode
public class RtsEncoder extends MessageToByteEncoder{
@Override
protected void encode(ChannelHandlerContext ctx, RtsProtocal msg, ByteBuf out) throws Exception {
//写入消息SmartCar的具体内容
//1.写入消息的开头的信息标志
out.writeInt(msg.getHead_data());
//2.写入协议类型
out.writeByte(msg.getType());
//3.写入协议号
out.writeInt(msg.getProtocloNumber());
//4.写入消息的长度(此长度不包含,head_data 和 contentLength所占的字节)
out.writeInt(msg.getContentLength());
//5.写入消息的内容
out.writeBytes(msg.getContent());
}
}
Decode:
/**
* 自定义解码器
* @author miracle
*
*/
public class RtsDecoder extends ByteToMessageDecoder{
/**
* 消息头,协议开始的标志 head_data ,int类型,占4个字节
* 数据的长度contentLength,int类型,占4个字节
* 数据的类型type,byte类型,占1个字节
*/
public final int BASE_LENGTH = 4+4+1;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List
RtsProtocol:
/**
* 数据包格式
* @author miracle
*
*/
public class RtsProtocal {
/**
* 消息头,消息开始的标志
*/
private int head_data = ConstantValue.HEAD_DATA;
/**
* 协议类型
* 1 登录相关协议(连接服务器检测,登录,注册...)
* 2 心跳协议
* 3 大厅内协议(查看战绩,排行,查找,添加好友等...)
* 4 游戏(创建房间,解散房间等)
*/
private byte type;
/**
* 协议号
*/
private int protocloNumber;
/**
* 消息的长度
*/
private int contentLength;
/**
* 消息的内容
*/
private byte[] content;
public int getHead_data() {
return head_data;
}
public void setHead_data(int head_data) {
this.head_data = head_data;
}
public byte getType() {
return type;
}
public void setType(byte type) {
this.type = type;
}
public int getProtocloNumber() {
return protocloNumber;
}
public void setProtocloNumber(int protocloNumber) {
this.protocloNumber = protocloNumber;
}
public int getContentLength() {
return contentLength;
}
public void setContentLength(int contentLength) {
this.contentLength = contentLength;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
/**
* 用于初始化,SmartCarProtocal
* @param contentLength
* 消息长度
* @param content
* 消息内容
*/
public RtsProtocal(byte type,int protocloNumber,int contentLength,byte[] content){
this.type = type;
this.protocloNumber = protocloNumber;
this.contentLength = contentLength;
this.content = content;
}
public RtsProtocal(byte type,MsgRsponse rsp){
this.type = type;
this.protocloNumber = rsp.toClassName();
this.content = JSONObject.toJSONString(rsp).getBytes();
this.contentLength = content.length;
}
@Override
public String toString() {
return "SmartCarProtocol [head_data=" + head_data + ", type=" + type +
",protocloNumber=" + protocloNumber +",contentLength="
+ contentLength + ", content=" + Arrays.toString(content) + "]";
}
}
编码和解码方式比较简单,值得注意的是netty中使用的是byteBuf想比较jdk中的byteBuffer性能要好很多
netty中的核心类是重写的handler,重点看一下loginHandler
public class LoginHandler extends ChannelHandlerAdapter{
public static final Logger logger = LoggerFactory.getLogger(LoginHandler.class);
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.error(ctx.channel().remoteAddress()+" 错误关闭");
cause.printStackTrace();
ctx.close();
}
/**
* 用于获取客户端发送的消息
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//用于获取客户端发送的消息
RtsProtocal body = (RtsProtocal)msg;
logger.info("LobbyServer接受的客户端的信息:"+body.toString());
//如果是登录相关协议
if(body.getType() == 1){
logger.info("是登录相关的协议!");
LobbyManager lobbyManager = LobbyManager.getInstance();
//根据sessionId 获取gameSession
LobbyGameSession gameSession = lobbyManager.sessionManager.get(ctx.channel().id().asLongText());
if(gameSession == null){
gameSession = new LobbyGameSession();
//设置ip
gameSession.setIP(((InetSocketAddress)ctx.channel().remoteAddress()).getAddress()
.getHostAddress());
//设置sessionId
gameSession.setSessionId(ctx.channel().id().asLongText());
lobbyManager.addSession(ctx.channel().id().asLongText(), gameSession);
}
//根据协议号调用业务
Processors processors = lobbyManager.protocolManager.get(body.getProtocloNumber());
processors.setGameSession(gameSession);
processors.setJson(new String(body.getContent()));
//执行具体的业务逻辑
processors.process();
}else{
//通知下一个channelHandler执行
ctx.fireChannelRead(msg);
}
}
}
每次收到客户端的协议后会先通过预设好的编码器将byteBuf解析为我们要的类RtsProtocol,然后调用channelRead方法,ctx.fireChannelRead(msg)这个方法是通知下一个channelRead来执行read方法,当收到客户端的协议后,根据协议号找到protocolManager中对应的协议,protocolManager中的key是协议号,value是processors对应的子类,将客户端传过来的参数json赋值给processors后,每个子类重写了父类的process()方法,在这里调用这个方法就可以进入对应协议的业务逻辑
在heartBeatHandler中调用的active方法如下:
/**
* 多个active同时存在的时候
* 根据handler注册的先后顺序active只在第一次
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String sessionId = ctx.channel().id().asLongText();
LobbyManager lobbyManger = LobbyManager.getInstance();
lobbyManger.addChannel(sessionId, (SocketChannel)ctx.channel());
logger.info("HeartBeatReq active...1");
}
项目还在开发中,后期会不定时更新,笔者水平有限,如有错误,请及时指出!