小熊1986

导航

第二天 游戏开发入门-协议

消息协议基础

消息的设计

1.首先前两个字节是消息的长度

2.后面两个字节是消息的编号,比如1是登录,2是返回登录,后面就是消息体

为什么会有消息长度这个字段

是因为会出现粘包的存在,这个是因为socket编程他是一个tcp的编程,而每一次发送的消息不一定是一次性发完整的消息,所以可能出现多个消息才能组成一个消息的这种形式,所以需要一个长度,根据长度判断我这个消息是否已经读取完毕。

粘包的的示意图

Protobuf 协议文档

准备工作:

1.下载windows下面编辑工具

Protobufwindos转换工具

下载自己对应的windows版本的工具,我是64位的就下载64位的操作

2.下载完成解压以后把盘符:photobuf/bin/protoc.exe加入到path里面,然后cmd在dos环境下打印protoc 看是否有说明文档如果有就证明已经安装完成。

3.关键命令 protoc.exe --java_out=${目标目录} .\对应的proto文件

4.文件的编写规则请参考https://www.cnblogs.com/talenth/p/5820040.html

5.使用文件在day02里面有一个GameMsgProtocol.proto这个文件

Netty群发消息

进行主类的编写

package org.joy.game;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import org.slf4j.LoggerFactory;

/**
 * @author joy
 * @version 1.0
 * @date 2020/4/28 14:33
 */
public class ServerGame {

    //日志类
    static private final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(ServerGame.class);

    public static void main(String[] args) {
        //boss客户端连接,work业务
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup work = new NioEventLoopGroup();
        //NioEventLoopGroup

        ServerBootstrap b = new ServerBootstrap();
        b.group(boss, work);//装入到线程组里面
        b.channel(NioServerSocketChannel.class);//服务器的处理信道的方式nio
        b.childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                ch.pipeline().addLast(
                        new HttpServerCodec(), // Http 服务器编解码器
                        new HttpObjectAggregator(65535), // 内容长度限制
                        new WebSocketServerProtocolHandler("/websocket"), // WebSocket 协议处理器, 在这里处理握手、ping、pong 等消息
                        new GameMsgDecoder(),//自定义消息解码器
                        new GameMsgEncoder(),//游戏消息编码器
                        new GameMsgHandler() // 自定义的消息处理器
                );
            }
        });

        try {
            ChannelFuture f = b.bind(12345).sync();//进行端口的绑定

            if (f.isSuccess()) {
                LOGGER.info("服务器已经启动");
            }
            //等待关闭服务器
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

自定义消息的处理类GameMsgHandler 这个比较关键它是继承SimpleChannelInboundHandlerObject,然后需要重写方法channelActive(这个是用户做连接的时候操作)channelRead0(这个是收到客户端进行操作)。

问题:群发怎么实现?

1.首先顶一个ChannelGroup这个进行装载我们连接的channel,或者是通信的渠道。

2.在channelActive方法中使用ChannelGroup.add方法增加到channel组里面

3.然后在收到的消息当中使用ChannelGroup.writeAndFlush进行发送。

4.如果有断开的时候就要删除ChannelGroup对应的channel

package org.tinygame.herostory;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tinygame.herostory.msg.GameMsgProtocol;

import java.util.HashMap;
import java.util.Map;

/**
 * 自定义的消息处理器
 */
public class GameMsgHandler extends SimpleChannelInboundHandler<Object> {
    /**
     * 日志对象
     */
    static private final Logger LOGGER = LoggerFactory.getLogger(GameMsgHandler.class);

    /**
     * 1.定义ChannelGroup用来装channel
     * 信道组, 注意这里一定要用 static,
     * 否则无法实现群发
     */
    static private final ChannelGroup _channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    //2.重写channelActive
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        if (null == ctx) {
            return;
        }

        try {
            super.channelActive(ctx);
            //3.如果有客户端加入我们就用add方法把当前用户的channel加入到组里面
            _channelGroup.add(ctx.channel());
        } catch (Exception ex) {
            // 记录错误日志
            LOGGER.error(ex.getMessage(), ex);
        }
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
        if (null == ctx ||
            null == msg) {
            return;
        }

        LOGGER.info(
            "收到客户端消息, msgClazz = {}, msgBody = {}",
            msg.getClass().getSimpleName(),
            msg
        );
		_channelGroup.writeAndFlush(msg);//对当前所有的用户进行发送数据
    }
}

核心的编码解码的编写

创建GameMsgDecoder的类,继承ChannelInboundHandlerAdapter,重写它的channelRead这个方法

代码如下

package org.tinygame.herostory;

import com.google.protobuf.GeneratedMessageV3;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tinygame.herostory.msg.GameMsgProtocol;

/**
 * 自定义的消息解码器
 */
public class GameMsgDecoder extends ChannelInboundHandlerAdapter {
    /**
     * 日志对象
     */
    static private final Logger LOGGER = LoggerFactory.getLogger(GameMsgDecoder.class);

    //接受到客户端发送的信息读
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        //判断对象是否为null跳出去
        if (null == ctx ||
            null == msg) {
            return;
        }

        //判断websocket发送过来是否是二进制的数据
        if (!(msg instanceof BinaryWebSocketFrame)) {
            return;
        }

        try {
            //二进制有一个读取习惯,如果已经读取的就会指针向后移动对应的大小
            //就转msg对象为二进制数据对象
            BinaryWebSocketFrame inputFrame = (BinaryWebSocketFrame) msg;
            ByteBuf byteBuf = inputFrame.content();//获得数据对象这里就是protobuf对象数据

            byteBuf.readShort(); // 读取消息的长度
            int msgCode = byteBuf.readShort(); // 读取消息编号

            // 拿到消息体
            byte[] msgBody = new byte[byteBuf.readableBytes()];
            byteBuf.readBytes(msgBody);

            GeneratedMessageV3 cmd = null;

            //根据固定的code进行判断是否完成那个操作
            /**
            enum MsgCode {
                USER_ENTRY_CMD = 0;
                USER_ENTRY_RESULT = 1;
                WHO_ELSE_IS_HERE_CMD = 2;
                WHO_ELSE_IS_HERE_RESULT = 3;
                USER_MOVE_TO_CMD = 4;
                USER_MOVE_TO_RESULT = 5;
                USER_QUIT_RESULT = 6;
                USER_STOP_CMD = 7;
                USER_STOP_RESULT = 8;
                USER_ATTK_CMD = 9;
                USER_ATTK_RESULT = 10;
                USER_SUBTRACT_HP_RESULT = 11;
                USER_DIE_RESULT = 12;
			};
            根据结构体的数据来写出对应的逻辑
            */
            switch (msgCode) {
                case GameMsgProtocol.MsgCode.USER_ENTRY_CMD_VALUE:
                    //parseFrom方法是实现反序列化,并且获得对象
                    cmd = GameMsgProtocol.UserEntryCmd.parseFrom(msgBody);
                    break;

                case GameMsgProtocol.MsgCode.WHO_ELSE_IS_HERE_CMD_VALUE:
                    cmd = GameMsgProtocol.WhoElseIsHereCmd.parseFrom(msgBody);
                    break;

                default:
                    break;
            }

            if (null != cmd) {
                //把数据传给对象处理类这里是GameMsgHandler
                ctx.fireChannelRead(cmd);
            }
        } catch (Exception ex) {
            // 记录错误日志
            LOGGER.error(ex.getMessage(), ex);
        }
    }
}

上面GameMsgHandler类中的channelRead0方法此时我们可以收到来自解码器的对应数据

代码如下

package org.joy.game;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.slf4j.LoggerFactory;
import org.tinygame.herostory.msg.GameMsgProtocol;

import java.util.HashMap;
import java.util.Map;

/**
 * @author joy
 * @version 1.0
 * @date 2020/4/28 15:22
 */
public class GameMsgHandler extends SimpleChannelInboundHandler<Object> {
    //日志对象
    static private final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(GameMsgHandler.class);
    //这个地方增加集合操作
    static private final ChannelGroup _channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    /**
     * 用户字典
     */
    static private final Map<Integer, User> _userMap = new HashMap<>();

    //连接操作
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        if (null == ctx) {
            return;
        }
        try {
            super.channelActive(ctx);
            //添加子对象
            _channelGroup.add(ctx.channel());
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
//        System.out.println("服务器收到客户端消息 msg=" + o);
        if (null == ctx || null == msg) {
            LOGGER.info("channelRead0数据或则消息体出错");
            return;
        }

        try {
            //测试的msg就是刚才解码器传过来的对应protobuf的对象
            /**
				message UserEntryCmd {
    				// 用户 Id
    				uint32 userId = 1;
    				// 英雄形象
    				string heroAvatar = 2;
				}
            */
            if (msg instanceof GameMsgProtocol.UserEntryCmd) {
                // 用户入场消息通过序列化对象以后,我们读取客户端发给我的序列化的对象的数据
                GameMsgProtocol.UserEntryCmd cmd = (GameMsgProtocol.UserEntryCmd) msg;
                int userId = cmd.getUserId();
                String heroAvatar = cmd.getHeroAvatar();

                //创建一个返回对象
                /**
                    message UserEntryResult {
                        // 用户 Id
                        uint32 userId = 1;
                        // 英雄形象
                        string heroAvatar = 2;
                   }
                */
                GameMsgProtocol.UserEntryResult.Builder resultBuilder = GameMsgProtocol.UserEntryResult.newBuilder();//newBuilder()new的关键点
                resultBuilder.setUserId(userId);
                resultBuilder.setHeroAvatar(heroAvatar);

                // 构建结果并广播
                GameMsgProtocol.UserEntryResult newResult = resultBuilder.build();
                _channelGroup.writeAndFlush(newResult);
            }
        } catch (Exception ex) {
            // 记录错误日志
            LOGGER.error(ex.getMessage(), ex);
        }
    }
}

还有一个消息的编码器,负责把对应的消息编码以后发送给客户端

GameMsgEncoder,它继承ChannelOutboundHandlerAdapter重写了write方法

package org.tinygame.herostory;

import com.google.protobuf.GeneratedMessageV3;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tinygame.herostory.msg.GameMsgProtocol;

/**
 * 游戏消息编码器
 	继承ChannelOutboundHandlerAdapter
 */
public class GameMsgEncoder extends ChannelOutboundHandlerAdapter {
    /**
     * 日志对象
     */
    static private final Logger LOGGER = LoggerFactory.getLogger(GameMsgEncoder.class);

    //重写write方法
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        if (null == ctx ||
            null == msg) {
            return;
        }

        try {
            if (!(msg instanceof GeneratedMessageV3)) {
                super.write(ctx, msg, promise);
                return;
            }

            // 消息编码
            int msgCode = -1;
            /**
            	message UserEntryResult {
                    // 用户 Id
                    uint32 userId = 1;
                    // 英雄形象
                    string heroAvatar = 2;
				}
            */
			//判断发送的对象是那个对象
            if (msg instanceof GameMsgProtocol.UserEntryResult) {
                //msgCode = GameMsgProtocol.MsgCode.USER_ENTRY_RESULT_VALUE;
            } else if (msg instanceof GameMsgProtocol.WhoElseIsHereResult) {
                msgCode = GameMsgProtocol.MsgCode.WHO_ELSE_IS_HERE_RESULT_VALUE;
            } else {
                LOGGER.error(
                    "无法识别的消息类型, msgClazz = {}",
                    msg.getClass().getSimpleName()
                );
                super.write(ctx, msg, promise);
                return;
            }

            // 消息体
            byte[] msgBody = ((GeneratedMessageV3) msg).toByteArray();

            ByteBuf byteBuf = ctx.alloc().buffer();//申请bytebuf这个对象的空间相当于c语言的malloc
            byteBuf.writeShort((short) msgBody.length); // 消息的长度
            byteBuf.writeShort((short) msgCode); // 消息编号
            byteBuf.writeBytes(msgBody); // 消息体

            // 写出 ByteBuf
            BinaryWebSocketFrame outputFrame = new BinaryWebSocketFrame(byteBuf);
            super.write(ctx, outputFrame, promise);
        } catch (Exception ex) {
            // 记录错误日志
            LOGGER.error(ex.getMessage(), ex);
        }
    }
}

这整个一套protobuf加websocket的基础使用,已经有一个流程了,后面的游戏的逻辑部分大同小异。

git 代码地址

posted on 2020-11-23 11:26  小熊1986  阅读(133)  评论(0编辑  收藏  举报