`
朋在无锡
  • 浏览: 34295 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

java游戏开发入门2_基于netty+protobuf的游戏框架

阅读更多
/**
刚开始学习游戏开发时想找一个基于netty的游戏demo十分困难,工作一段时间后了解框架后将其分享出来;
该框架是从别人框架移植修改完善而来,不是我一个人写,算是借花献佛;
实际业务开发比此框架要复杂得多,去繁从简主在体现核心思想;
这是游戏开发入门的第2篇,如果有不完善的地方请多多指导.
*/
 框架示意图如下,源代码参看:github:

  1.  客户端连接进来,由acceptor负责接入验证,创立channel后再转发给从线程池(workerReactor);
    package com.server.java.netty;
    
    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelOption;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    import io.netty.handler.logging.LogLevel;
    import io.netty.handler.logging.LoggingHandler;
    
    public class NettyServer implements Runnable {
    	private int port;
    
    	public NettyServer(int port) {
    		super();
    		this.port = port;
    	}
    
    	public int getPort() {
    		return port;
    	}
    
    	public void setPort(int port) {
    		this.port = port;
    	}
    
    	@Override
    	public void run() {
    		try {// netty支持3种reactor,netty推荐主从(boss和worker)
    			EventLoopGroup bossGroup = new NioEventLoopGroup(4);// acceptor,负责tcp接入,接入认证,创立socketChannel等;
    			EventLoopGroup workerGroup = new NioEventLoopGroup();// netty默认设置:Runtime.getRuntime().availableProcessors()
    																	// 负责io的读写和编码
    
    			try {
    				ServerBootstrap b = new ServerBootstrap();
    				b.group(bossGroup, workerGroup).option(ChannelOption.TCP_NODELAY, true).option(ChannelOption.SO_KEEPALIVE, true).channel(NioServerSocketChannel.class)
    						.handler(new LoggingHandler(LogLevel.INFO)).childHandler(new NettyServerInitializer());
    
    				ChannelFuture f = b.bind(port).sync();
    
    				// Wait until the server socket is closed.
    				// In this example, this does not happen, but you can do that to
    				// gracefully
    				// shut down your server.
    				f.channel().closeFuture().sync();
    			} finally {
    				workerGroup.shutdownGracefully();
    				bossGroup.shutdownGracefully();
    			}
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    	}
    
    }
    
     
  2. worker Reactor 按照指定协议解码,构件出msgEntity对象,触发对应事件,在根据不同命令码(cmdCode)传递给对应的保存队列(blockqueue)
    package com.server.java.netty;
    
    import io.netty.buffer.ByteBuf;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
    
    import java.nio.ByteOrder;
    
    import com.server.java.entity.MsgEntity;
    
    /**
     * 定长解码器
     * */
    public class NettyMsgDecoder extends LengthFieldBasedFrameDecoder {
    
    	/**
    	 * @param byteOrder
    	 * @param maxFrameLength
    	 *            字节最大长度,大于此长度则抛出异常
    	 * @param lengthFieldOffset
    	 *            开始计算长度位置,这里使用0代表放置到最开始
    	 * @param lengthFieldLength
    	 *            描述长度所用字节数
    	 * @param lengthAdjustment
    	 *            长度补偿,这里由于命令码使用2个字节.需要将原来长度计算加2
    	 * @param initialBytesToStrip
    	 *            开始计算长度需要跳过的字节数
    	 * @param failFast
    	 */
    	public NettyMsgDecoder(ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
    		super(byteOrder, maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip, failFast);
    	}
    
    	public NettyMsgDecoder() {
    		this(ByteOrder.BIG_ENDIAN, 100000, 0, 4, 2, 4, true);
    	}
    
    	/**
    	 * 根据构造方法自动处理粘包,半包.然后调用此decode
    	 * */
    	@Override
    	protected Object decode(ChannelHandlerContext ctx, ByteBuf byteBuf) throws Exception {
    
    		ByteBuf frame = (ByteBuf) super.decode(ctx, byteBuf);
    		if (frame == null) {
    			return null;
    		}
    
    		short cmd = frame.readShort();// 先读取两个字节命令码
    
    		byte[] data = new byte[frame.readableBytes()];// 其它数据为实际数据
    		frame.readBytes(data);
    
    		MsgEntity msgVO = new MsgEntity();
    		msgVO.setCmdCode(cmd);
    		msgVO.setData(data);
    		return msgVO;
    	}
    
    }
    
     
    package com.server.java.netty;
    
    import io.netty.channel.ChannelHandler.Sharable;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
    
    import com.server.java.entity.MsgEntity;
    import com.server.java.queue.CommonQueue;
    import com.server.java.queue.LoginQueue;
    
    @Sharable
    public class ServerHanlder extends SimpleChannelInboundHandler<MsgEntity> {
    
    	@Override
    	protected void messageReceived(ChannelHandlerContext ctx, MsgEntity msg) throws Exception {
    
    		if (msg == null) {
    			return;
    		}
    		msg.setChannel(ctx.channel());
    
    		// int playerid = ServerCache.get(ctx.channel());
    
    		int csCommondCode = msg.getCmdCode();
    		if (csCommondCode < 100) {// 100以内暂时不用
    
    		} else if (csCommondCode >= 100 && csCommondCode < 200) { // 100-200用于注册
    			LoginQueue.getInstance().put(msg);
    
    		} else {// 消息可能较多,可以分几个队列,这里先放一个
    			CommonQueue.getInstance().put(msg);
    		}
    
    	}
    }
    
     
  3. 每个blockQueue负责执行的线程异步取出msgEntity,通过cmdCode获得解码protobuf的GeneralMessage,解码出请求数据;
    package com.server.java.handler;
    
    import java.util.List;
    
    import com.google.protobuf.InvalidProtocolBufferException;
    import com.server.java.CmdHandler;
    import com.server.java.cache.ServerCache;
    import com.server.java.constants.CmdConstant;
    import com.server.java.entity.MsgEntity;
    import com.server.java.entity.PlayerEntity;
    import com.server.proto.demo.DemoProto.NameCheckReq;
    import com.server.proto.demo.DemoProto.NameCheckResp;
    import com.server.proto.demo.DemoProto.SayHelloReq;
    import com.server.proto.demo.DemoProto.SayHelloResp;
    
    public class DemoHanlder extends CmdHandler {
    
    	@Override
    	public void handleMsg(MsgEntity msgEntity, List<MsgEntity> commandList) {
    		switch (msgEntity.getCmdCode()) {// 根据命令码对应找到对应处理方法
    		case CmdConstant.NAME_CHECK:
    			handleNameCheck(msgEntity, commandList);
    			break;
    		case CmdConstant.SAY_HELLO:
    			handleSayHello(msgEntity, commandList);
    			break;
    		default:
    			System.out.println("找不到对应的命令码");
    		}
    	}
    
    	private void handleNameCheck(MsgEntity msgEntity, List<MsgEntity> commandList) {
    		// com.server.proto.demo.
    		NameCheckReq req = null;
    		try {// 按照与客户端约定,指定命令码使用指定的解码class解码
    				// 这里可以通过反射做成自动解码,参考:http://vincepeng.iteye.com/blog/2171310
    			req = NameCheckReq.parseFrom(msgEntity.getData());
    		} catch (InvalidProtocolBufferException e) {
    			System.out.println("protobuf解码错误");
    			e.printStackTrace();
    			return;
    		}
    		String name = req.getName();
    		if (name == null || name.isEmpty()) {
    			return;
    		}
    
    		boolean isExist = ServerCache.CheckName(name);
    		if (!isExist) {// 如果没有存在/则模拟注册
    			ServerCache.addNewPlayer(name, msgEntity.getChannel());// 由于是单线程操作,无需加锁.参考:实战1的第2条
    		}
    		NameCheckResp.Builder resp = NameCheckResp.newBuilder();
    		resp.setIsExist(isExist);
    		msgEntity.setData(resp.build().toByteArray());// 将原来的消息内容替换为回包,命令码无需变化
    		commandList.add(msgEntity);// 加入到发送数组
    		if (!isExist) {
    			SayHelloResp helloResp = SayHelloResp.newBuilder().setContent("欢迎" + name + "的到来").setSpeaker("系统").build();
    			MsgEntity helloMsg = new MsgEntity();
    			helloMsg.setCmdCode(CmdConstant.SAY_HELLO);
    			helloMsg.setData(helloResp.toByteArray());
    			ServerCache.sendToAll(helloMsg);// 此操作开销较大,一般不要如此或者分离到其它服务器
    		}
    
    	}
    
    	private void handleSayHello(MsgEntity msgEntity, List<MsgEntity> commandList) {
    		SayHelloReq req = null;
    		try {
    			req = SayHelloReq.parseFrom(msgEntity.getData());
    		} catch (InvalidProtocolBufferException e) {
    			System.err.println("protobuf解码错误");
    			e.printStackTrace();
    			return;
    		}
    		// 关键词过滤
    		// 发言频率检测
    		int playerId = ServerCache.get(msgEntity.getChannel());
    		PlayerEntity pe = ServerCache.getPlayerById(playerId);
    		if (pe != null) {
    			SayHelloResp resp = SayHelloResp.newBuilder().setContent(req.getContent()).setSpeaker(pe.getName()).build();
    			msgEntity.setData(resp.toByteArray());
    			ServerCache.sendToAll(msgEntity);
    		} else {
    			System.err.println("玩家不存在");
    		}
    
    	}
    }
    
     
  4. 业务逻辑处理后,按照相反的路径,先构件protobuf的消息对象,在编码成二进制,计算出长度,加上命令码分别写入byteBuf中,传递给客户端.完成一个业务的处理操作.

实际业务中添加了spring以及mybatis持久化,redis来处理缓存,这里暂时略去,可以根据自身需求慢慢添加;

protobuf配置参看链接

解码操作可以设计为自动完成,参看protobuf解码

 

 

2.
  • 大小: 53.6 KB
分享到:
评论
2 楼 zcqshine 2015-10-16  
protobuf 的编解码为什么没直接采用 netty 提供的ProtobufEncoder 和 ProtobufDecoder呢?
1 楼 liyuzhel 2015-08-17  
为什么需要把登陆队列与逻辑处理队列分开呢

相关推荐

Global site tag (gtag.js) - Google Analytics