制作游戏压测机器人工具

世间万物,表里如一者,又有几何? --婕拉

当游戏项目开发到后期,进入全面内测阶段时,很多项目团队会开发一款游戏压测机器人工具去测试本游戏的各种性能,如支持的最高在线人数,消息吞吐量,以及不同在线和消息量时的内存消耗和CPU使用率,游戏线程的处理能力,数据库IO的处理能力,此外,还可以用机器人去测多人玩法功能,甚至还可以用机器人模仿攻击服务器,等等。总之,用压测机器人工具跑几天服务器后,再结合JDK的命令行工具和可视化工具,就可以很直观的发现游戏是否有性能瓶颈了,是否需要调整JVM参数,是否有可优化的地方了。机器人乱传参数给服务端时,就可知道游戏的功能玩法是否有bug,是否可刷资源,是否会报空指针了,等等。可见,开发一款游戏压测机器人工具作用还是很大的,通过不断的压力测试,发现问题,再将之调优或解决bug,一个稳健的服务器后台便产生了。

通常,一款压测机器人工具应至少具备以下功能:

1.登陆机器人。通过同时或随机登陆机器人,可以测试游戏的最高在线人数,地图同屏人数,或让机器人不断登陆下线,测试游戏的网关处理能力。

2.协议测试机器人。通过随机发送游戏的所有请求协议,可以测试游戏功能是否可作弊,是否能刷资源,是否会报空指针。

3.功能机器人。通常为一些重点玩法或多人玩法订做的专业机器人。特别是一些多线程处理的功能玩法,公司测试环境很难达到多人同时操作的情景,但用机器人就可以很方便的模拟出来。

以下为具体思路实现,以供参考:
1.因为是可能跑很多个机器人的,需提供机器人配置文件供测试人员灵活配置,在配置文件中,定义了机器人类型,开始账号和数量,及服务器ip和端口等字段,用来定位该机器人是测试什么功能的,需开多少个机器人,以及连哪台服务器。更专业的配置还可加上机器人线程数,机器人开启时长,及测试哪些模块的协议等配置,以便更效率和针对性的测试。配置信息示例如下,因为这个工具将来是很有可能打包成工具分给测试、策划、程序等人测试的,这里提供了两个配置文件,一个简易的给非程序人员配置,一个专业的给程序人员配置。

机器人配置文件.png

2.功能和配置方案确定后,就可以开始撸代码了。因为有配置文件,当然就需要加载配置文件了;因为可能跑成千上万个机器人,这里最好开启多线程来管理这些机器人;因为以后需方便地扩展其它功能机器人,这里最好是有一套机器人模板。
启动入口代码如下:
Bootstrap.java

public static void main(String[] args) throws Exception {
        try {
            String configPath = args[0];
            if (configPath.isEmpty()) {
                log.error("机器人参数配置文件为空...");
                return;
            }
            //加载外部机器人配置
            RobotMgr.instance().loadRobotConfig(configPath);
            //读取内部机器人配置文件
            String serverConfig = GameConfig.getInstance().getServerConfigPath() + "/config/server.properties";
            InputStream fis = new FileInputStream(new File(serverConfig));
            Properties properties = new Properties();
            properties.load(fis);
            GameConfig.getInstance().setProperties(properties);
            //初始化协议
            ProtoMgr.instance().initProtos();
            //初始化网络
            RobotMgr.instance().initNetty();
            //启动机器人
            RobotMgr.instance().start();
        } catch (Exception e) {
            log.error("机器人服务启动失败...", e);
            return;
        }

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try {
                RobotMgr.instance().shutDownRobotMgr();
            } catch (Exception e) {
                log.error("机器人服务关闭失败...", e);
            }
        }));
    }

接下来可以把游戏里的所有protobuf协议解析出来,按照游戏机器人分类可以选择性地解析协议,比如登录机器人就只解析登录协议;世界boss机器人就解析世界boss模块的协议,外加登录的协议;如需检查项目哪里会报空指针异常或服务器性能,则可以解析所有的协议文件。这里也验证了在 制作一款游戏协议联调工具 博文中所说的规范化协议设计的重要性,良好的协议设计在做这些联调工具和机器人压测工具时能方便地解析出来,核心代码如下:

        String packageName = "proto";
        Class clazz = Message.class;
        try {
            TreeMap<Integer, Class<?>> reqMap = ClassUtils.getClasses(packageName, clazz, "Req_");
            TreeMap<Integer, Class<?>> respMap = ClassUtils.getClasses(packageName, clazz, "Resp_");
            
         } catch (Throwable e) {
             e.printStackTrace();
         }

    /**
     * 迭代组装协议
     */
    public static TreeMap<Integer, Class<?>> getClasses(String packageName, Class<?> clazz, String delimiter)
        throws ClassNotFoundException {
        TreeMap<Integer, Class<?>> map = new TreeMap<>();
        String path = packageName.replace('.', '/');
        ClassLoader classloader = Thread.currentThread().getContextClassLoader();
        URL url = classloader.getResource(path);
        for (Class<?> c : getClasses(new File(url.getFile()), packageName)) {
            if (Message.class.isAssignableFrom(c) && !Message.class.equals(c)) {
                if (c.getSimpleName().contains(delimiter)) {
                    String[] subDelimiter = delimiter.split("_");
                    int protocol = Integer.parseInt(
                        c.getSimpleName().substring(c.getSimpleName().indexOf(delimiter) + subDelimiter[0].length() + 1));
                    map.put(protocol, c);
                }
            }
        }
        return map;
    }

接下来是初始化网络,这和 使用Netty+Protobuf实现游戏WebSocket通信 里的一致,这款机器人工具连的就是这篇博文实现的服务端,核心代码如下:

    /**
     * 初始化网络
     */
    public void initNetty(){
        client = new NioEventLoopGroup();
        bootstrap = new Bootstrap();
        bootstrap.group(client);
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
        bootstrap.option(ChannelOption.TCP_NODELAY, true);
        
        bootstrap.handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                ChannelPipeline pipeline = ch.pipeline();
                pipeline.addLast("http_codec", new HttpClientCodec());
                pipeline.addLast("http_aggregator", new HttpObjectAggregator(65536));
                pipeline.addLast("protobuf_decoder", new ProtoDecoder(null, 5120));
                pipeline.addLast("client_handler", new WebSocketClientHandler());
                pipeline.addLast("protobuf_encoder", new ProtoEncoder(new CRC16CheckSum(), 2048));
            }
        });
    }

接下来便是机器人管理器的实现了,因为要管理众多的机器人,这里把这些机器人按照线程配置数量平均分配到这些线程里去管理,以减少压力提高效率,核心代码如下:

/**
     * 启动机器人
     * @throws InterruptedException 
     */
    public void start() throws InterruptedException{
        RobotType type = RobotType.valueOf(robotType);
        if(type == null){
            log.error("RobotType not exist, robotType:" + robotType);
            return;
        }

        for (int i = accountStart; i <= (accountStart + robotNum - 1); i++) {
            String account = new StringBuilder("robot").append(i).toString();
            type.registerRobot(account, ConnectStatus.STATUS_UNCONNECT);
        }
        
        //启动机器人任务调度
        int threads = GameConfig.getInstance().getRobotThreads();
        int openTime = GameConfig.getInstance().getOpenTime();
        initRobotThreads(threads, openTime);
    }

    //初始化机器人线程
    private void initRobotThreads(int threads, int openTime){
        int openBegin = (int)(System.currentTimeMillis()/1000);

        robotThreads = new ArrayList<>();
        threadNum = threads;
        if(robotsList.size() < threads){//如果机器人数  < 配置的线程数
            threadNum = robotsList.size();
        }

        //每个线程控制的机器人数量,如果15个机器人,有3个线程,则每个线程操控5个机器人,如果17个机器人,有3个线程,则每个线程操控6个机器人
        int ctrlRobotNum = robotsList.size() % threadNum == 0 ? robotsList.size() / threadNum : (int)Math.ceil((double)robotsList.size() / threadNum);
        for (int i = 0; i < threadNum ; i++) {
            RobotThread thread = new RobotThread(i, ctrlRobotNum);
            thread.setName(i+"号机器人管理线程");
            robotThreads.add(thread);
            thread.start();
        }
        
        int now = (int)(System.currentTimeMillis()/1000);
        if(openTime > 0 && now - openBegin >= openTime){
            shutDownRobotMgr();
        }
    }

    /**机器人管理线程*/
    private class RobotThread extends Thread{
        private int index;//机器人线程索引
        private int ctrlRobotNum;//当前线程操控的机器人数量
        private boolean isRun = true;//当前线程是否在运行状态

        public RobotThread(int index, int ctrlRobotNum){
            this.index = index;
            this.ctrlRobotNum = ctrlRobotNum;
        }

        @Override
        public void run() {
            int curCtrlIndex = 0; //当前线程当前操控的机器人索引
            while (isRun) {
                if(curCtrlIndex >= ctrlRobotNum){
                    curCtrlIndex = 0;
                }
                if(index * ctrlRobotNum + curCtrlIndex >= robotsList.size()){
                    curCtrlIndex = 0;
                }
                RobotBase robot = robotsList.get(index * ctrlRobotNum + curCtrlIndex);
                System.out.println(Thread.currentThread().getName()+"运行,当前运行的机器人是:"+robot.getAccount());
                robot.robotRun(System.currentTimeMillis());
                
                try {
                    Thread.sleep(Probability.rand(200, 3000));//随机休眠一下,让每个机器人看起来不是一致的
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally{
                    curCtrlIndex++;
                }
            }
        }

        //停止线程
        public void stopRun(){
            this.isRun = false;
        }
    }

最终启动的输出信息如下,可见机器人的管理线程可以正确工作了:

机器人管理器随机请求的协议列表为:
ReqMap cmd:1001003,simple class name:ListTestReq_1001003
ReqMap cmd:1001002,simple class name:LogoutReq_1001002
ReqMap cmd:1002002,simple class name:HurtAwardReq_1002002
ReqMap cmd:1001001,simple class name:LoginReq_1001001
ReqMap cmd:1002001,simple class name:AttackBossReq_1002001
机器人管理器筛选返回的协议列表为:
RespMap cmd:1001003,simple class name:ListTestResp_1001003
RespMap cmd:1001002,simple class name:LogoutResp_1001002
RespMap cmd:1002002,simple class name:HurtAwardResp_1002002
RespMap cmd:1001001,simple class name:LoginResp_1001001
RespMap cmd:1002001,simple class name:AttackBossResp_1002001
1号机器人管理线程运行,当前运行的机器人是:robot51
0号机器人管理线程运行,当前运行的机器人是:robot1
2号机器人管理线程运行,当前运行的机器人是:robot101
3号机器人管理线程运行,当前运行的机器人是:robot151
0号机器人管理线程运行,当前运行的机器人是:robot2
2号机器人管理线程运行,当前运行的机器人是:robot102
3号机器人管理线程运行,当前运行的机器人是:robot152
1号机器人管理线程运行,当前运行的机器人是:robot52
2号机器人管理线程运行,当前运行的机器人是:robot103
1号机器人管理线程运行,当前运行的机器人是:robot53
3号机器人管理线程运行,当前运行的机器人是:robot153
2号机器人管理线程运行,当前运行的机器人是:robot104
2号机器人管理线程运行,当前运行的机器人是:robot105
0号机器人管理线程运行,当前运行的机器人是:robot3
3号机器人管理线程运行,当前运行的机器人是:robot154
1号机器人管理线程运行,当前运行的机器人是:robot54
1号机器人管理线程运行,当前运行的机器人是:robot55
2号机器人管理线程运行,当前运行的机器人是:robot106
……

在本文开头说过,要方便以后扩展其他的功能机器人,比如还有组队副本机器人,聊天机器人等,这里是以枚举的方式实现的,代码如下:

public enum RobotType {
    /**登录机器人*/
    ROBOT_TYPE_LOGIN(1) {
        
        @Override
        public void registerRobot(String account, int status) {
            RobotBase robot = new RobotLogin(account, status);
            RobotMgr.instance().register(robot);
        }
    },
    
    /**协议机器人**/
    ROBOT_TYPE_PROTO(2) {

        @Override
        public void registerRobot(String account, int status) {
            RobotBase robot = new RobotProto(account, status);
            RobotMgr.instance().register(robot);
        } 
    },
    
    /**世界boss机器人**/
    ROBOT_TYPE_WORLDBOSS(3) {

        @Override
        public void registerRobot(String account, int status) {
            // TODO 自动生成的方法存根
            
        }
    }
    ;
    
    private final int type;

    RobotType(int type) {
        this.type = type;
    }
    
    public int getType() {
        return type;
    }
    
    public static RobotType valueOf(int type) {
        for (RobotType robotType : RobotType.values()) {
            if (robotType.getType() == type) {
                return robotType;
            }
        }
        return null;
    }
    
    public abstract void registerRobot(String account, int status);
}

这样,以后增加别的功能机器人时都在这里增加即可,这些机器人都有一个共同的父类,记录了这个机器人的基本信息,如账号信息,通信Channel等,先看实现如下:

public abstract class RobotBase{
    private String account = null;//机器人账号
    private RobotType robotType = null;//机器人类型
    private int status = 0;//机器人状态
    
    private long robotRid = 0;//机器人rid
    private Channel channel = null;//机器人通信channel
    
    public static Log log = LogFactory.getLog(RobotBase.class);
    
    //机器人各自行为函数
    abstract public void robotRun(long time);
    //机器人协议处理函数
    abstract public void process(int cmd, Message message);
    
    public RobotBase(String account, int status, RobotType robotType){
        this.account = account;
        this.status = status;
        this.robotType = robotType;
    }
    
    //发送消息给服务器
    public void send(Message message) {
        int cmd = ClassUtils.getMessageID(message.getClass());
        Packet packet = new Packet(Packet.HEAD_TCP, cmd, message.toByteArray());
        channel.writeAndFlush(packet).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                if(channelFuture.isSuccess()){
                    System.out.println("\n"+ getAccount() + ">>>>>>>>>>>发送协议:"+ cmd + "成功>>>>>>>>>>>");
                }else{
                    System.out.println("\n"+ getAccount() + ">>>>>>>>>>>发送协议:"+ cmd + "失败xxxxxxxxxxx");
                }
            }
        });
        if (log.isDebugEnabled())
            log.debug("send to " + robotRid + ":" + message.getClass().getSimpleName() + "," + TextFormat.printToUnicodeString(message));
    }

    //......
}

这些是所有机器人都应该具有的属性和方法,至于继承自它的其他功能性机器人,则可以有它们自己的属性和行为,比如登录机器人的实现功能是每隔一段时间便登录或下线游戏,则它的实现可为:

public class RobotLogin extends RobotBase{
    private long lastLoginTime = 0;//上次登录时间
    private long lastOffLineTime = 0;//上次下线时间
    
    private static final int LOGINOUT_INTER_TIME_MILLIS = 5000;//登录下线间隔
    
    public RobotLogin(String account, int status) {
        super(account, status, RobotType.ROBOT_TYPE_LOGIN);
    }
    
    @Override
    public void robotRun(long time) {
        if(getStatus() == ConnectStatus.STATUS_CONNECTING){
            login();
        }else if (getStatus() == ConnectStatus.STATUS_CONNECTED) {
            if(time -lastLoginTime > LOGINOUT_INTER_TIME_MILLIS){
                logout();
            }
        }else if (getStatus() == ConnectStatus.STATUS_UNCONNECT) {
            if(time - lastOffLineTime > LOGINOUT_INTER_TIME_MILLIS){
                connect();
            }
        }
    }
    
    @Override
    public void process(int respCmd, Message message) {
        if (respCmd == 1001001){
            setStatus(ConnectStatus.STATUS_CONNECTED);
        }else if(respCmd == 1001002){
            setStatus(ConnectStatus.STATUS_UNCONNECT);
            try {
                Channel channel = getChannel();
                ChannelFuture future = channel.close().sync();
                if (future.isSuccess()){
                    System.out.println(channel.hashCode() + "退出登录成功.......");
                }
            } catch (InterruptedException e) {
                log.error("退出失败:", e);
            }
        }
    }

    //...
}

这里需要注意的是以下两个方法,它们是实现各种功能机器人不同行为的核心方法:

    //机器人各自行为函数
    abstract public void robotRun(long time);
    //机器人协议处理函数
    abstract public void process(int cmd, Message message);

如机器人的随机行为就是主要通过上面代码的这里实现的,相应实现可参考上面的RobotLogin的robotRun方法:

机器人随机行为.png

另外,当在Netty的消息处理Handler中收到服务端的消息后:

    public void process(Channel channel, Packet packet){
        RobotBase robot = robotsMap.get(channel);
        if(robot == null){
            log.error("channel:" + channel.id() + "不存在该消息频道的机器人, robotsMap:" + robotsMap);
            return;
        }
        System.out.println("\n" + robot.getAccount() + "<<<<<<<<<<<<收到服务端协议:"+packet.getCmd()+"<<<<<<<<<<<<");

        //打印返回协议
        Class<?> clazz = ProtoMgr.instance().getMessageRespMap().get(packet.getCmd());
        if(clazz == null){
            System.out.println(packet.getCmd() + "返回协议不存在!");
            return;
        }
        Method m = ClassUtils.findMethod(clazz, "getDefaultInstance");
        Message msg = null;
        try {
            Message message = (Message) m.invoke(null);
            msg = message.newBuilderForType().mergeFrom(packet.getBytes()).build();
            System.out.println("返回协议 打印开始------------------------------------");
            ProtoPrinter.print(msg);
            System.out.println("返回协议 打印结束------------------------------------");
        } catch (Exception e) {
            log.error("处理返回协议:"+packet.getCmd()+"时报错", e);
        } 
        
        /**相应机器人模块处理协议*/
        robot.process(packet.getCmd(), msg);
    }

它们的消息处理则是通过上面的 "robot.process(packet.getCmd(), msg);" 这句发送到相应的功能机器人中处理的,相应的功能机器人实现这两个方法,便可以拥有各自的行为和消息处理结果了。

最终的实现效果打印如下:

收到robot195请求的协议:1001001
account:robot195
password:jianshu
收到robot10请求的协议:1002001
收到robot196请求的协议:1001001
account:robot196
password:jianshu
收到robot105请求的协议:1001003
id:[3,3,3]
num:[422810455,895525841]
players:[O6CEpW3R,hRvrsU0d,fSDAbx0]
收到robot11请求的协议:1001003
id:[8,7,2]
num:[670149699,555087686]
players:[7m3mbraN,LQj]
收到robot52请求的协议:1001002
rid:619224922
……

至此,游戏的压测机器人工具便基本实现了,因为上述的代码只是该款工具主要功能的大致实现,具体的细节还需根据具体的游戏去扩展丰富。
上述源码在github的地址为:
https://github.com/zhou-hj/GameServerRobot

/---------------------------------------------------------------------------------------------
2020/03/19 补充:
年初为公司另一个项目写机器人压测程序时,为这个程序写了很多改进的地方及修复了一些bug,发现原来写的程序已不能看了,也不知道当时怎么想的:
1.两个配置合二为一了,仅用一个配置文件即可,之前的配置协议过滤改在相应机器人实现类里做;

#机器人参数配置

#机器人类型1-登录机器人,该种机器人会随机登录下线,测试服务器在线抗压
#机器人类型2-协议机器人,该种机器人一直在线随机发送游戏类所有协议,测试服务器是否报错或模拟客户端修改协议服务端会怎样
#机器人类型3-世界boss机器人,该种机器人用于测试世界boss功能
#机器人类型......后续支持的其他机器人
robot_type = 1
#机器人账号起始id,有则用原来机器人账号,无则新增机器人账号
account_start = 1
#机器人数量
robot_num = 1
#服务器id
server_id = 32
#服务器ip
server_ip = 192.168.16.239
#服务器端口
server_port = 33211
#机器人工具运行时长0-永久开启,> 0,开启时长,秒
run_time = 0
#机器人线程开启数量,该数量线程平均管理robot_num个机器人
robot_threads = 4

2.增加了注解的配置方式,方便开发员直接在启动程序里配置注解即可,不要老是跳到配置文件里来修改;

@Target({TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RobotConfig {
    int robotType(); //机器人类型
    int accountStart(); //机器人账号起始id,机器人名字会从“Robot”+accountStart开始
    int robotNum(); //机器人数量
    int serverId(); //游戏服id
    String serverIP() default "192.168.16.239";//游戏服IP
    int serverPort() default 33212; //游戏服端口
    int runTime() default 0;//单位秒,>0 开启时长;=0无限时长开启
    int threadNum() default 64; //管理机器人线程数
}

启动程序修改为:

/**
 * 机器人启动类,如果不想用外部配置文件,也可用此注解方式({@link}RobotConfig)
 * 注意注解方式里的默认值 --> ({@link}RobotConfig)
 * 如果报netty无法连接,请查看配置中的serverId、serverIP、serverPort是否设置正确
 * 
 * 《更多机器人程序介绍请见{@link}ReadMe}》
 */
@RobotConfig(accountStart = 0, robotNum = 320, robotType = 3, serverId = 32)
public class Bootstrap {

    private static final Log log = LogFactory.getLog(Bootstrap.class);

    public static void main(String[] args) throws Exception {
        try {
            if (args.length <= 0) {
                //注解方式解析机器人配置
                parseRobotAnotionConfig();
            }else {
                //加载外部机器人配置
                RobotMgr.instance().loadRobotConfig(args[0]);
            }
            
            //初始化协议-因游戏protobuf协议众多,此处加载较久
            ProtoMgr.instance().initProtos();
            //初始化网络
            RobotMgr.instance().initNetty();
            //启动机器人
            RobotMgr.instance().start();
            
        } catch (Exception e) {
            log.error("机器人服务启动失败...", e);
            return;
        }

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try {
                RobotMgr.instance().shutDownRobotMgr();
            } catch (Exception e) {
                log.error("机器人服务关闭失败...", e);
            }
        }));
    }
    
    private static void parseRobotAnotionConfig(){
        RobotConfig config = Bootstrap.class.getAnnotation(RobotConfig.class);
        RobotMgr.instance().parseRobotAnotionConfig(config.robotType(), config.accountStart(),
                config.robotNum(), config.serverId(), config.serverIP(), config.serverPort(),
                config.runTime(), config.threadNum());
    }
}

3.RobotType.java中小修改:

    /**
     * 注册机器人
     * @param account
     */
    public void registerRobot(String account){
        RobotBase robot = createRobot(account);
        RobotMgr.instance().register(robot);
    }
    
    /**
     * 创建机器人
     * @param account
     * @return
     */
    public abstract RobotBase createRobot(String account);

4.相应的机器人实现类构造方法及协议过滤修改:

/**
 * 世界boss机器人
 * 测试世界boss功能完整性,及测试所有战斗技能,测试战斗稳健性
 */
public class RobotWorldBoss extends RobotBase {
    // 战场的其他玩家
    protected Set<Long> battleRoles = new HashSet<>();
    // 机器人状态
    protected BossBattleStatus battleStatus = BossBattleStatus.UN_ENTER;
    
    /*在ProtoMgr中管理了游戏中所有的请求协议和返回协议,但是某些功能机器人并不是全部需要它们,
    而是在某些指定的协议中请求或打印返回,以下两个就是做这用处的*/
    //101001--请求登录;101003--创建角色;109001--聊天请求(主要作执行GM命令用)  很多机器人依赖这三条协议,建议都添加
    public static Set<Integer> filterProtoReq = new HashSet<>();
    //101001--请求登录;100001--错误码返回;101006--更新货币,通过监控货币数值,超过一定数量则预警,可以知道游戏是否存在刷货币的情况
    public static Set<Integer> filterProtoResp = new HashSet<>();
    
    static{
        // 世界boss机器人将在以下协议中请求游戏服
        List<Integer> filterReq = new ArrayList<>(Arrays.asList(1701002/* 攻打boss */, 11200004/* 战斗开始 */,
                1701003/* 攻击他人 */, 11200008/* 请求复活 */, 11200005/* 退出战场 */));
        filterProtoReq.addAll(filterReq);
        
        // 世界boss机器人将在以下协议中解析游戏服的返回协议
        List<Integer> filterResp = new ArrayList<>(
                Arrays.asList(100001/* 错误码协议 */, 101002/* 角色列表 */, 11200001/* 入场协议 */, 11200003/* 战报协议 */));
        filterProtoResp.addAll(filterResp);
    }

    public RobotWorldBoss(String account) {
        super(account, RobotType.ROBOT_TYPE_WORLDBOSS);
    }
    ......
}

5.因为要解析游戏服的所有protobuf协议,因此采用了软连接的方式连接了游戏服的proto文件夹;又因为所有proto.java文件包路径都为游戏服的包路径,因此软连接后的包路径应与游戏服的包路径一致,因此增加了protoLink.bat文件方便连接游戏服的proto文件夹。(否则要项目依赖解析游戏服的proto文件,或者直接复制所有proto文件,但这都不太灵活)

mklink /J D:\svn-workspace\legends-robot\src\main\java\game\legends\gameserver\protocol D:\svn-workspace\legends\src\main\java\game\legends\gameserver\protocol
pause

6.之前的倒计时关闭机器人程序是有bug的,配置时间到了后不能自动关闭机器人程序:

if (runTime > 0) {
    ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();
    ses.schedule(() -> {
        shutDownRobotMgr();
        System.out.println("===机器人运行时长:"+runTime+"结束,所有机器人退出游戏。===");
    }, runTime, TimeUnit.SECONDS);
    ses.shutdown();
}

及之前关闭程序没有彻底退出所有线程:

    public void shutDownRobotMgr(){
        robotThreads.forEach(e -> e.stopRun());
        robotThreads.clear();
        robotsList.forEach(e -> e.getChannel().close());
        robotsList.clear();
        robotsMap.clear();
        try {
            client.shutdownGracefully().sync();
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
    }

7.之前的机器人线程管理也有bug,设置不同数量的机器人可能报数组越界bug,应修改如下:

for (int i = 0; i < threadsNum ; i++) {
    if (i*ctrlRobotNum >= robotsList.size()) {//比如6个机器人,配了4个线程,这时只需3个线程就可以了
        break;
    }
    RobotThread thread = new RobotThread(i, ctrlRobotNum);
    thread.setName(i+"号机器人管理线程");
    robotThreads.add(thread);
    thread.start();
}
        @Override
        public void run() {
            int curCtrlIndex = 0; //当前线程当前操控的机器人索引
            while (isRun) {
                if(curCtrlIndex >= ctrlRobotNum){
                    curCtrlIndex = 0;
                }
                if(index * ctrlRobotNum + curCtrlIndex >= robotsList.size()){
                    break;//这里原来的写法是有bug的
                }
                RobotBase robot = robotsList.get(index * ctrlRobotNum + curCtrlIndex);
                System.out.println(Thread.currentThread().getName()+"运行,当前运行的机器人是:"+robot.getAccount());
                robot.robotRun(System.currentTimeMillis());
                
                try {
                    //随机休眠一下,让每个机器人看起来不是一致的,后面的最大值可根据ctrlRobotNum设置,要知道执行完总的一轮最大时间为(后面的最大值*ctrlRobotNum)ms
                    Thread.sleep(Probability.rand(500, 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally{
                    curCtrlIndex++;
                }
            }
        }

8.如果protobuf协议嵌套太深,解析会报错,因为这里没有层层递进解析。
以上是几处重大改动,源代码不再附上了。
---------------------------------------------------------------------------------------------------/

至此,游戏的网络部分就介绍到这篇结束了,一共有6篇,相应链接如下:
游戏之网络初篇
游戏之网络进阶
使用Netty+Protobuf实现游戏TCP通信
制作一款游戏协议联调工具
使用Netty+Protobuf实现游戏WebSocket通信
制作游戏压测机器人工具


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,082评论 1 32
  • “真正的极地漫步”可能导致了冰河世纪 科学家们利用夏威夷热点来研究地球两极的运动 地球最近的冰河时代可能是由地球内...
    wumingzhi111阅读 390评论 0 0
  • 镜中词 文/斯禅 半握青丝镜前小坐,容颜依旧几分幽怨。衣香鬓影,相思闲愁。阅己容颜多情嗟叹,青丝覆雪转瞬成空。孤...
    斯禅阅读 261评论 0 1
  • 一直对死亡的恐惧,是不是要去了,每当这个念来时,会紧张并用力的抑制,压着,身体处于紧紧的状态,无形中给了自己压力,...
    红尘之爱阅读 177评论 0 0
  • 在生活中,我们经常拒绝别人。 一个汽车销售的电话,你拒绝了,因为暂时没有买车的计划。 一个理财课程学习销售的电话,...
    文博_4f10阅读 2,251评论 0 1