前言
接着上一章讲(netty实现多协议,多编解码器),上文提到String数据和protostuff序列化对象数据使用不同编解码器共同使用,这一章讲讲,多数据格式(多对象)该怎么去解析,当然还是用protostuff序列化对象传输。
具体实现
定义实体类
@Data
public class User implements Serializable {
private String id;
private String name;
private Integer age;
}
@Data
public class Department implements Serializable {
private String id;
private String name;
private String code;
}
定义通用类
/**
* @author: zhouwenjie
* @description:
* @create: 2022-07-18 10:21
**/
@Data
public class MessageData implements Serializable {
public enum DataType{
user,
department
}
private DataType data_type;
private User user;
private Department department;
}
编解码器
编码器
/**
* @author: zhouwenjie
* @description:
* @create: 2022-07-12 11:19
**/
public class ProtostuffEncoder extends MessageToByteEncoder<MessageData> {
private String delimiter;
public ProtostuffEncoder(String delimiter) {
this.delimiter = delimiter;
}
@Override
protected void encode(ChannelHandlerContext ctx, MessageData msg, ByteBuf out) throws Exception {
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(Unpooled.wrappedBuffer(total));
}
解码器
public class ProtostuffDecoder<T> extends MessageToMessageDecoder<ByteBuf> {
private Class<T> clazz;
public ProtostuffDecoder(Class<T> clazz) {
this.clazz = clazz;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
try {
byte[] body = new byte[in.readableBytes()];
in.readBytes(body);
out.add(ProtostuffUtils.deserialize(body, clazz));
} catch (Exception e) {
e.printStackTrace();
}
}
}
集成编解码器
发送和接收消息
发送消息
@Override
public void channelActive(ChannelHandlerContext ctx) {
User user= new User();
user.setName("张三");
user.setAge(28);
MessageData messageData = new MessageData();
messageData.setData_type(MessageData.DataType.User);
messageData.setComputeResult(user);
ctx.writeAndFlush(messageData);
}
接收消息
@Override
protected void channelRead0(ChannelHandlerContext ctx, MessageData messageData) throws Exception {
if (messageData.getData_type()== MessageData.DataType.User){
System.out.println(messageData.getUser());
}else {
System.out.println(messageData.getDepartment());
}
}
扩展
如果不想在一个读方法里处理所有对象数据接收区分的逻辑,那么可以将区分逻辑放在一个自定义的解码器中,然后分别定义user和department的hanlder处理器,这样的也是可以的。
例如下边的,改造一下上边的解码器代码:
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
try {
byte[] body = new byte[in.readableBytes()];
in.readBytes(body);
MessageData messageData = (MessageData) ProtostuffUtils.deserialize(body, clazz);
if (messageData.getData_type()== MessageData.DataType.CarHeartBeat){
out.add(messageData.getCarHeartBeat());
}else {
out.add(messageData.getComputeResult());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
处理逻辑的handler泛型也改一下:
再加一个Department的handler专门用来处理部门的逻辑即可,这样代码分离的更彻底一点,不过还得看自己需求吧。
终极解决方案(反射)
看了前边的文章和各种方法,相信一定对netty编解码有了更深的理解,针对前边的方法,在只有少量对象格式的情况下,判断也不多,我们可以用,但是一旦对象格式很多,那么判断语句就会大量增加,再看我们的逻辑代码,解码器里只不过是把对象解析出来,然后传递下去而已,这里并没有太多复杂的逻辑,所以,考虑用反射来优化多if判断的代码,这样就兼容性就更好了。
改造解码器
public class ProtostuffDecoder<T> extends MessageToMessageDecoder<ByteBuf> {
private Class<T> clazz;
public ProtostuffDecoder(Class<T> clazz) {
this.clazz = clazz;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
try {
byte[] body = new byte[in.readableBytes()];
in.readBytes(body);
MessageData messageData = (MessageData) ProtostuffUtils.deserialize(body, clazz);
MessageData.DataType data_type = messageData.getData_type();
String className = data_type.toString();
//className 一定要是字段的名称
Field field = messageData.getClass().getDeclaredField(className);
field.setAccessible(true);
Object o = field.get(messageData);
if (o == null) {
return;
}
out.add(o);
} catch (Exception e) {
e.printStackTrace();
}
}
}
改造编码器
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 {
MessageData messageData = new MessageData();
Class<?> aClass = msg.getClass();
String simpleName = aClass.getSimpleName();
//设置首字母小写
simpleName = CommonUtil.lowerFirstCase(simpleName);
//设置datatype
messageData.setData_type(MessageData.DataType.valueOf(simpleName));
Field[] fields = messageData.getClass().getDeclaredFields();
for (Field field : fields) {
Class<?> fieldType = field.getType();
if (msg.getClass().getName().equals(fieldType.getName())) {
field.setAccessible(true);
field.set(messageData, msg);
break;
}
}
byte[] bytes = ProtostuffUtils.serialize(messageData);
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);
}
}
这样一来,不管你增加再多的实体类,这块代码都不用再去修改啦!!!
但是一定要记得保证MessageData里DataType的值保持跟下边实体类字段的名字一样。
提示:
经过进一步的项目实践发现,如果你的客户端和服务端都只有一个的情况下,MessageData中可以不要DataType字段,解码判断的时候直接判断字段值是否是空即可,编码器也不用动态去添加类型了,比如下边这样:
但是如果是有多个客户端,那么服务端就不能这样做了,因为在解码的时候,因为都是protobuf协议,所以即使不是自己的消息也能解码,虽然解码的内容为空,比如下边这样:
PlaneMessageData的消息也能被LightMessageData消息的解码器解码。