netty之Protostuff序列化协议

前言

比起上文的Protobuf协议,Protostuff的好处就是不用去编辑proto文件,然后生成对应的java文件,直接可以利用现有的实体类来通信,而且性能性能损失也是很少的。

github官方链接地址

依赖

	<dependency>
	     <groupId>io.protostuff</groupId>
	     <artifactId>protostuff-core</artifactId>
	     <version>1.7.2</version>
	 </dependency>
	
	 <dependency>
	     <groupId>io.protostuff</groupId>
	     <artifactId>protostuff-runtime</artifactId>
	     <version>1.7.2</version>
	 </dependency>

定义编解码器

Protobuf有netty提供已经封装好的编解码器,但是Protostuff需要自己定义。

工具类

public class ProtostuffUtils {
    
    

    /**
     * 缓存Schema
     */
    private static Map<Class<?>, Schema<?>> schemaCache = new ConcurrentHashMap<>();

    /**
     * 序列化方法,把指定对象序列化成字节数组
     *
     * @param obj
     * @param <T>
     * @return
     */
    @SuppressWarnings("unchecked")
    public static <T> byte[] serialize(T obj) {
    
    
        Class<T> clazz = (Class<T>) obj.getClass();
        Schema<T> schema = getSchema(clazz);
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        byte[] data;
        try {
    
    
            data = ProtobufIOUtil.toByteArray(obj, schema, buffer);
        } finally {
    
    
            buffer.clear();
        }

        return data;
    }

    /**
     * 反序列化方法,将字节数组反序列化成指定Class类型
     *
     * @param data
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T deserialize(byte[] data, Class<T> clazz) {
    
    
        Schema<T> schema = getSchema(clazz);
        T obj = schema.newMessage();
        ProtobufIOUtil.mergeFrom(data, obj, schema);
        return obj;
    }

    @SuppressWarnings("unchecked")
    private static <T> Schema<T> getSchema(Class<T> clazz) {
    
    
        Schema<T> schema = (Schema<T>) schemaCache.get(clazz);
        if (Objects.isNull(schema)) {
    
    
            //这个schema通过RuntimeSchema进行懒创建并缓存
            //所以可以一直调用RuntimeSchema.getSchema(),这个方法是线程安全的
            schema = RuntimeSchema.getSchema(clazz);
            if (Objects.nonNull(schema)) {
    
    
                schemaCache.put(clazz, schema);
            }
        }
        return schema;
    }
}

编码器

这里采用分隔符的方式来解决拆包粘包的问题,那么分隔符实验了多次,如果用换行符\n,会报错,错误信息如下:

Protocol message contained an invalid tag (zero)

好像是因为\的问题,所以只能采用不带\的分隔符了,这里选用@来作为分隔符。

/**
 * @author: zhouwenjie
 * @description:
 * @create: 2022-07-12 11:19
 **/
public class ProtostuffEncoder extends MessageToByteEncoder {
    
    

    //分隔符@,放在配置文件,方便修改配置
    private String delimiter;

    public ProtostuffEncoder(String delimiter) {
    
    
        this.delimiter = delimiter;
    }

    @Override
    protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
    
    
        if (msg instanceof ComputeResult) {
    
    
            byte[] bytes = ProtostuffUtils.serialize(msg);
            byte[] delimiterBytes = delimiter.getBytes();
            byte[] total = new byte[bytes.length + delimiterBytes.length];
            System.arraycopy(bytes, 0, total, 0, bytes.length);
            System.arraycopy(delimiterBytes, 0, total, bytes.length, delimiterBytes.length);
            out.writeBytes(total);
        }
    }
}

解码器

/**
 * @author: zhouwenjie
 * @description:
 * @create: 2022-07-12 11:17
 **/
public class ProtostuffDecoder extends MessageToMessageDecoder<ByteBuf>{
    
    

	// 写在这里,方便扩展
    private Class<?> clazz;

    public ProtostuffDecoder(Class<?> clazz) {
    
    
        this.clazz = clazz;
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> list) {
    
    
        try {
    
    
            byte[] body = new byte[in.readableBytes()];  //传输正常
            in.readBytes(body);
            list.add(ProtostuffUtils.deserialize(body, clazz));
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

整合到netty

这里以客户端为例,服务端同理。

启动类

/**
 * @author: zhouwenjie
 * @description: 客户端
 * @create: 2020-04-03 17:14
 **/
@Component
@Slf4j
public class NettyClient {
    
    

    @Value("${monitor.server.host}")
    private String host;

    @Value("${monitor.server.port}")
    private int port;

    @Value("${monitor.delimiter}")
    private String delimiter;

    @Autowired
    private NettyClientHandler nettyClientHandler;


    private NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();

    private Bootstrap bootstrap;

    @PostConstruct
    public void run() throws UnknownHostException {
    
    
        bootstrap = new Bootstrap();
        bootstrap.group(eventLoopGroup)
                .channel(NioSocketChannel.class)
                .remoteAddress(host, port)
                .option(ChannelOption.TCP_NODELAY, true)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .handler(new ChannelInitializer<SocketChannel>() {
    
    
                    @Override
                    protected void initChannel(SocketChannel socketChannel) {
    
    
                        //客户端初始化
                        socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024*8, Unpooled.wrappedBuffer(delimiter.getBytes())));
                        socketChannel.pipeline().addLast(new ProtostuffDecoder(ComputeResult.class));
                        socketChannel.pipeline().addLast(new ProtostuffEncoder(delimiter));
                        socketChannel.pipeline().addLast(nettyClientHandler);
                    }
                });
        String hostAddress = InetAddress.getLocalHost().getHostAddress();
        // 指定本机ip端口,用来给服务端区分,指定端口,重启客户端会等两分钟才能连接上服务端
        bootstrap.localAddress(hostAddress,0);
        //连接netty服务器
        reconnect();
    }

    /**
     * 功能描述: 断线重连,客户端有断线重连机制,就更不能使用异步阻塞了
     * @param
     * @return void
     * @author zhouwenjie
     * @date 2021/3/19 14:53
     */
    public void reconnect() {
    
    
        ChannelFuture channelFuture = bootstrap.connect();
        //使用最新的ChannelFuture -> 开启最新的监听器
        channelFuture.addListener((ChannelFutureListener) future -> {
    
    
            if (future.cause() != null) {
    
    
                log.error("连接失败。。。");
                future.channel().eventLoop().schedule(() -> reconnect(), 3, TimeUnit.SECONDS);
            } else {
    
    
                log.info("客户端连接成功。。。");
            }
        });
    }
    /**
     * 关闭 client
     */
    @PreDestroy
    public void shutdown() {
    
    
        // 优雅关闭 EventLoopGroup 对象
        eventLoopGroup.shutdownGracefully();
        log.info("[*Netty客户端关闭]");
    }
}

处理器

/**
 * @author: zhouwenjie
 * @description: 客户端处理类
 * @create: 2020-04-03 17:45
 **/
@Component
@Slf4j
@ChannelHandler.Sharable
public class NettyClientHandler extends SimpleChannelInboundHandler<ComputeResult> {
    
    

    /**
     * 注入NettyClient
     */
    @Autowired
    private NettyClient nettyClient;

    /**
     * 连接成功
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
    
    
        ComputeResult computeResult = new ComputeResult();
        computeResult.setComtype("aaaaaa");
        computeResult.setFromname("bbbbb");
        computeResult.setJunctionnum("ccccccc");
        ctx.writeAndFlush(computeResult);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    
    
        log.error("[*The netty server suspends service...]");
        super.channelInactive(ctx);
        ctx.fireChannelInactive();
        nettyClient.reconnect();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    
    
        super.exceptionCaught(ctx, cause);
        log.error("[* Netty connection exception]:{}", cause.toString());
        cause.printStackTrace();
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, ComputeResult computeResult) {
    
    
        System.out.println(computeResult);
    }
}

注意事项

protostuff的序列化和反序列化特点是一个对象序列化时是按照可序列化字段顺序把值序列化到字节码中,反序列化时也是按照当前对象可序列化字段顺序赋值,如果序列化前的对象和反序列化接收的对象,对应顺序或者字段类型不一样时会出现反序列失败报错。为了避免以上问题,在使用protostuff序列化时,一定要保证客户端和服务端实体类字段顺序一致,且字段类型一致。

案例一:实体类字段顺序不一致

客户端
在这里插入图片描述

服务端
在这里插入图片描述
如果是这样,客户端传递comtype的值,将会被赋予给服务端的fromnum,因为是按顺序来序列化的。

案例二:实体类字段类型不一致

客户端
在这里插入图片描述
服务端
在这里插入图片描述
如果是这样,那么服务端反序列化将会报错,报错如下:
在这里插入图片描述
所以,一定要保证双方实体类字段的顺序和类型一致。

猜你喜欢

转载自blog.csdn.net/zwjzone/article/details/125765057