- 前言
- 依赖
- 定义编解码器
- 工具类
- 编码器
- 解码器
- 整合到netty
- 注意事项
比起上文的Protobuf协议,Protostuff的好处就是不用去编辑proto文件,然后生成对应的java文件,直接可以利用现有的实体类来通信,而且性能性能损失也是很少的。
github官方链接地址
依赖定义编解码器io.protostuff protostuff-core 1.7.2 io.protostuff protostuff-runtime 1.7.2
Protobuf有netty提供已经封装好的编解码器,但是Protostuff需要自己定义。
工具类public class ProtostuffUtils {
private static Map, Schema>> schemaCache = new ConcurrentHashMap<>();
@SuppressWarnings("unchecked")
public static byte[] serialize(T obj) {
Class clazz = (Class) obj.getClass();
Schema 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;
}
public static T deserialize(byte[] data, Class clazz) {
Schema schema = getSchema(clazz);
T obj = schema.newMessage();
ProtobufIOUtil.mergeFrom(data, obj, schema);
return obj;
}
@SuppressWarnings("unchecked")
private static Schema getSchema(Class clazz) {
Schema schema = (Schema) 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)
好像是因为的问题,所以只能采用不带的分隔符了,这里选用@来作为分隔符。
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);
}
}
}
解码器
public class ProtostuffDecoder extends ByteToMessageDecoder {
// 写在这里,方便扩展
private Class> clazz;
public ProtostuffDecoder(Class> clazz) {
this.clazz = clazz;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List
整合到netty
这里以客户端为例,服务端同理。
启动类
@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() {
@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();
}
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("客户端连接成功。。。");
}
});
}
@PreDestroy
public void shutdown() {
// 优雅关闭 EventLoopGroup 对象
eventLoopGroup.shutdownGracefully();
log.info("[*Netty客户端关闭]");
}
}
处理器
@Component @Slf4j @ChannelHandler.Sharable public class NettyClientHandler extends SimpleChannelInboundHandler注意事项{ @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,因为是按顺序来序列化的。
案例二:实体类字段类型不一致
客户端
服务端
如果是这样,那么服务端反序列化将会报错,报错如下:
所以,一定要保证双方实体类字段的顺序和类型一致。



