基于netty的rts游戏服务器搭建

最近在读《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<SocketChannel>(){
			@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();
	}
}

上面的每一行注释都很详细,lobbyManager使用单例模式管理玩家和连接,RtsEncode和RtsDecode重新定义了编码,redisUtil是对redis的简单管理

public class LobbyManager {
	
	/**
	 * 协议管理器
	 */
	public static Map<Integer,Processors> protocolManager; 
	
	/**
	 * 客户端连接管理器
	 */
	public static Map<String, SocketChannel> gateManager;
	
	/**
	 * GameSession管理器
	 */
	public static Map<String,LobbyGameSession> sessionManager;
	
	/**
	 * 玩家管理器
	 */
	public static Map<String,Player> playerManager;
	
	public void init(){
		protocolManager = LobbyProtocol.toMap();
		gateManager = new HashMap<String,SocketChannel>(); 
		sessionManager = new HashMap<String,LobbyGameSession>();
		playerManager = new HashMap<String,Player>();
		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);
	}
}

protocolManager是协议管理器,在lobbyserver启动的时候将所有的协议装载到协议管理器中,一会儿在下文中我们将详细介绍协议的分发

gateManager是连接管理器,在channelHandler的active方法中,每次客户端连接成功后都会回调到active 方法中,即使有多个channelHandler但是同一个tcp长连接,只要连接不断开就只会调用一次active方法,上文中提到过执行channelhandler的顺序为handler的注册顺序

重写encode和decode:

Encode

public class RtsEncoder extends MessageToByteEncoder<RtsProtocal>{

	@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<Object> out) throws Exception {
		
		//可读长度必须大于基本长度
		if(in.readableBytes() >= BASE_LENGTH){
			//防止socket字节流攻击
			//防止客户端传过来的数据过大,因为太大的数据,是不合理的
			if(in.readableBytes() > 2048){
				in.skipBytes(in.readableBytes());
			}
			
			//记录包头开始的index
			int beginReader;
			while(true){
				//获取包头开始的index
				beginReader = in.readerIndex();
				//标记包头开始的index
				in.markReaderIndex();
				
				//读到了协议开始的标志,结束while循环
				if(in.readInt() == ConstantValue.HEAD_DATA){
					break;
				}
				
				//如果未读到包头略过一个字节去读包头,去读取是否是消息的开头
				in.resetReaderIndex();
				in.readByte();
				
				//如果这时候包的长度不足 最低要求,return
				if(in.readableBytes() < BASE_LENGTH){
					return;
				}
			}
			
			//获取协议类型
			byte type = in.readByte();
			
			//如果type == 2表示是心跳协议,心跳协议没有协议号和协议体
			if(type == 2){
				SmartCarProtocal protocal = new SmartCarProtocal(type,0,0,new byte[]{});
				out.add(protocal);
				return;
			}
			
			//协议号
			int protocloNumber = in.readInt();
			//消息的长度
			int length = in.readInt();
			//判断请求数据包数据是否到齐
			if(in.readableBytes() < length){
				//消息未到齐,还远读指针
				in.readerIndex(beginReader);
				return;
			}
			
			//读取data数据
			byte[] data = new byte[length];
			in.readBytes(data);
			
			SmartCarProtocal protocal = new SmartCarProtocal(type,protocloNumber,data.length,data);
			out.add(protocal);			
		}
	}
}
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");
	}




项目还在开发中,后期会不定时更新,笔者水平有限,如有错误,请及时指出!

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页