游戏测试 java 写游戏协议机器人

我是一个迷 · 2023年06月16日 · 4155 次阅读

背景

我看到 python 写的游戏机器人,正好学了一下 netty,所以我想用下 java 怎么写
正好想总结一下学习过程,尽量让大家看得明明白白
分享是我滴兴趣😆

在此之前在网上看到小圣 996 做过,学习了挺多,非常感谢

需要知识:

  • java
  • netty

java 水平有限,轻喷😆

第一步

创建一个最基本的 netty 客户端
NettyClient.java


    public void run(String host, int port) throws Exception {
//        LoggingHandler Logging_Handler = new LoggingHandler(LogLevel.DEBUG);
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(EventLoopHandler.getGroup());
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.handler(new ChannelInitializer<Channel>() {
                @Override
                protected void initChannel(Channel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();
//                    pipeline.addLast(Logging_Handler);
                    pipeline.addLast("decoder", new ProtoDecode());  //处理解码
                    pipeline.addLast("encoder", new ProtoEncode(65535)); // 处理编码
                    pipeline.addLast("messageHandler", new MessageHandler()); // 处理协议相应后续的操作
                    pipeline.addLast("heartbeat", new HeartHandler());// 发送心跳
                    pipeline.addLast("clientHandler", new ClientHandler());  // 初始化链接
                }
            });
            ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port)).sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

第二步

所有的数据需要一个载体运行,我想到的是创建一个实体类用于发送或者接受一条协议全部内容
ProtocolPacket.java

public class ProtocolPacket {
    private final int length;
    private final int protoID;
    private final byte[] content;

    public ProtocolPacket(int length, int protoID, byte[] content) {
        this.length = length;
        this.protoID = protoID;
        this.content = content;
    }
}

为每个协议单独定义每个类,这要看公司的协议文档是什么格式比如说 protobuf、xml,这里用 xml 为例

需要写方法 XmlParserJava 这种,由于太长我就不贴了,总体就是解析每个 xml 节点,根据字段类型 set 对应类,注意数据类型不能错

好像说了废话 😂

private static String getFieldType(String xmlType) {
    switch (xmlType) {
        case "int64":
            return "Long";
        case "int32":
            return "Integer";
        case "int16":
            return "Short";
        case "int8":
            return "Byte";
        case "string":
            return "String";
        default:
            return xmlType;
    }
}

这里最好是生成包装类,因为有的协议包括可以重复的使用字段,用类型反射会比较容易处理

public class C1000{
    private String user_name;
    private String password;
    //仅供参考
}

第三步

游戏里面的数据都需要解析
先来看看初始化的 ClientHandler
ClientHandler.java

public class ClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//        System.out.println("channelRead");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.channel().close();
    }
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {

        if (NettyClient.instance().getChannel() != null) {
            NettyClient.instance().getChannel().disconnect();
        }

        NettyClient.instance().setChannel(ctx.channel());
        System.out.println("登录  " + ctx.channel().remoteAddress().toString().replace("/", "") + "  成功");

        Object LoginObj = argsToObject(1000, "myName", " password" );
        ProtocolPacket packet = objectToByteArray(LoginObj );

        ctx.writeAndFlush(packet);
    }
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {

        System.out.println("\n断开  " + ctx.channel().remoteAddress().toString().replace("/", "") + "  连接");
        NettyClient.instance().close();
    }


我在这里放了账号信息相关的信息,可以后续会拿出来参数化跑并发

argsToObject 作用是通过协议 id 找到对应的类并且根据参数创建对应的类
argsToObject.java

public static  <T> T argsToObject(int protoId, T... args) throws IllegalAccessException, InvocationTargetException, InstantiationException {

    Constructor<T> constructor = ProtocolHandle.findProtocolClass(protoId);
    constructor.setAccessible(true);

    T instance = constructor.newInstance();

    Field[] fields = instance.getClass().getDeclaredFields();

    for (int i = 0; i < args.length; i++) {
        Field field = fields[i];
        field.setAccessible(true);
        field.set(instance, args[i]);
    }

    return instance;
}

objectToByteArray 作用是把 Packet 里面的数据转换成后端协定好的协议
其实根据遍历类的字段数值并转化成对应的字节数组并把它们拼接起来
反射 yyds
举个例子转成 [60, 80, 74, 11] 这种十六进制的字节数组,然后通过 ctx.writeAndFlush(packet) 发送

所以写了挺多类似这样的方法


public static byte[] convertIntToHexByteArray(int value) {
    byte[] byteArray = new byte[4];
    byteArray[0] = (byte) (value >> 24);
    byteArray[1] = (byte) (value >> 16);
    byteArray[2] = (byte) (value >> 8);
    byteArray[3] = (byte) (value);
    return byteArray;
}

public static byte[] convertShortToHexByteArray(short value) {
    byte[] byteArray = new byte[2];
    byteArray[0] = (byte) (value >> 8);
    byteArray[1] = (byte) (value);
    return byteArray;
}

public static byte[] convertLongToHexByteArray(long value) {
    byte[] byteArray = new byte[8];
    for (int i = 0; i < 8; i++) {
        byteArray[i] = (byte) (value >> ((7 - i) * 8));
    }
    return byteArray;
}

值得注意并确定一点:一般游戏是接受十六进制的数据,netty 是发送二进制,需要进行转换

当初发送老不对😂

统一发送 Packet 会到哪里呢?轮到我们的编码 ProtoEncode 登场了,所有的出站的消息都要经过它的处理

ProtoEncode.java

protected void encode(ChannelHandlerContext ctx, Object o, ByteBuf out) throws Exception {

       ProtocolPacket packet = (ProtocolPacket) o;


       if ((packet.getContent().length > this.limit) && (log.isWarnEnabled()))
           log.warn("packet size[{}] is over limit[{}]" , packet.getContent().length , this.limit);
       Object serverProto = argsToObject(packet.getProtoID());

       byte[] content = packet.getContent();
       byte[] copy = Arrays.copyOfRange(content, 5, content.length);

       Object obj = byteArrayToObj(copy, serverProto);
       System.out.println("send -----"+  obj);

       ByteBuf byteBuf = Unpooled.wrappedBuffer(packet.getContent());
       out.writeBytes(byteBuf);

   }

所有的消息数据由 out.writeBytes(byteBuf) 发出去

说到发送那么肯定会需要用到接收,所有的入站的消息都要经过它的处理,也就是 ProtoDecode
ProtoDecode.java

public class ProtoDecode extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> list) throws Exception {
        // 检查是否有足够的字节可读
        if (in.readableBytes() < 5) {
            return;
        }

        // 保存当前的读索引
        int readerIndex = in.readerIndex();

        // 读取长度和协议ID
        short length = in.readShort();
        int protoId = in.readInt();

        // 检查可读字节的长度是否足够
        if (in.readableBytes() < length) {
            // 重置读索引
            in.readerIndex(readerIndex);
            return;
        }

        // 读取指定长度的字节数组
        byte[] bytes = new byte[length];
        in.readBytes(bytes);

        // 创建 ProtocolPacket 对象并添加到列表中
        ProtocolPacket packet = new ProtocolPacket(length, protoId, bytes);
        list.add(packet);
    }
}

因为你会了 encode,decode 也就是反过来解析,这就不详细叙述~

这里有个坑点,因为会存在返回协议数据量过大,第一次收到的数据流可能不够你所需要的长度,所以要等待完协议长度才处理

目前我们只看到了发送一条协议(1000),怎么处理后续发送呢?这就要看看这个 MessageHandler
MessageHandler.java

public class MessageHandler extends ChannelInboundHandlerAdapter {

    private ProtocolHandlerRegistry registry;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {


        // 获取未解析的Packet对象
        ProtocolPacket RawContentPacket = (ProtocolPacket) msg;

//        printHexByteArray(packet.getContent());
        Object serverProto = argsToObject(RawContentPacket.getProtoID());


        Object obj = byteArrayToObj(RawContentPacket.getContent(), serverProto);
        System.out.println("接受到的协议id: " + RawContentPacket.getProtoID() + " 协议内容为:" + obj);

        ProtocolHandler protocolHandler = registry.getHandler(RawContentPacket.getProtoID());

        if (protocolHandler != null) {
            Object sendObject = protocolHandler.handleProtocol(obj);
            if (sendObject == null){
                protocolHandler.handleNoResponseProtocol(obj);
            }else {
                ProtocolPacket sendPacket = objectToByteArray(sendObject);
                ctx.writeAndFlush(sendPacket);
            }
        }
        super.channelRead(ctx, msg);
    }

    public MessageHandler() {
        registry = new ProtocolHandlerRegistry();
        registry.registerHandler(1001, new response1001());
        //收到服务的返回的1001会跳到1001()
        // 注册其他协议处理器....
    }
}

有的协议返回需要处理就用到 Object sendObject = protocolHandler.handleProtocol(obj);
有的协议不需要处理就 protocolHandler.handleNoResponseProtocol(obj);

关于协议返回我是这样设计的

public class response1001 implements ProtocolHandler {

    @Override
    public Object handleProtocol(Object protocol) throws InvocationTargetException, IllegalAccessException, InstantiationException {

        S1001 s1001 = (S1001) protocol;
        if (s1001.getResult() == 1){
            System.out.println("账号登录服务器!");
            return argsToObject(1002);
        }else {
            System.out.println("无法登录服务器,原因: "+  s1001.getState());
            return null;
        }

    }

}

通过返回会调用 ctx.writeAndFlush(sendPacket) 实现登录流程。

游戏里面需要实现保持长链接,所以我们需要发送心跳

HeartHandler.java


@ChannelHandler.Sharable
public class HeartHandler extends ChannelInboundHandlerAdapter {
    private ScheduledExecutorService executorService;

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
        // 创建 ScheduledExecutorService,用于执行定时任务
        executorService = Executors.newScheduledThreadPool(1);
        // 启动心跳定时任务,延迟3秒后开始,每3秒执行一次
        executorService.scheduleAtFixedRate(() -> {
            try {
                sendHeartbeat(ctx);
            } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }, 3, 3, TimeUnit.SECONDS);
    }


    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        super.channelInactive(ctx);
        // 关闭 ScheduledExecutorService
        if (executorService != null) {
            executorService.shutdown();
        }
    }

    private void sendHeartbeat(ChannelHandlerContext ctx) throws InvocationTargetException, IllegalAccessException, InstantiationException {
        // 构造心跳数据字节数组
        Object o = argsToObject(99999);
        ProtocolPacket packet = objectToByteArray(o);

        // 发送心跳数据
        ctx.writeAndFlush(packet);
    }
}


目前来说在此基础上扩展更多的玩法:比如说跑主线任务,跑业务,实现机器人全自动跑流程

完结撒花

总结一下:学了 java 反射相关知识,接口解耦,策略模式.......总的来说收获满满

题外话 :都是自己一个人瞎搞,开始做之前没想到我这个 java 小白也能写出来 hhhhhh,疯狂资料学习,记得要相信自己

有什么不明白可以告诉我,或者项目里面有不错的建议也欢迎提出

知识碰撞😍

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册