前言
前边的几篇文章一直在介绍各种通信协议和动态编解码,但是解决的都不是很完美,但是通过前几篇文章,相信还是有提升的,这里给出一个较完善的方案:如果你的项目中是多客户端,多服务端,多数据格式,多对象,如果你没有更好的办法来融合他们,达到最佳的传输效率,可以参考一下本文。
实现场景:
java项目用netty实现一个客户端,一个服务端,客户端和服务端通信的数据是发送实体类对象,同时这个服务端还要被c语言项目的客户端连接,通信的数据用的是json字符串方式。
1、netty之间的通信传输数据,为了能够更好的编解码,快速识别对象的类型(比如有user、dept、dict等等实体类),我们采用ObjectEncoder和ObjectDecoder编解码器,这套编解码器是netty提供的,直接拿来用;
2、 netty服务端与c++的客户端通信,需求原因,只能采用json字符串的方式,也就是将对象转成json字符串来进行传输。
在实现之前,我们先来实际的选择一下编解码器。
编解码器的选择
对象传输的编解码器
首先,netty之间的对象传输,我们选择了Object编解码器,原因是它能帮我们将字节流直接解析成对应数据类型,比如:user对象,而不需要我们去解析字节流,然后通过数据里的内容来判断属于什么pojo对象,具体这套编解码器怎么用,请参考这篇文章:
netty实现pojo对象的编解码(ObjectDecoder、ObjectEncoder详解)。
字符串的编解码器
为了防止拆包、粘包,首先我们来选择一套编解码器来解决,这里我们采用的是字符串分隔符的方式来解决,那么解码器我们可以选择netty提供的DelimiterBasedFrameDecoder
解码器,编码器的话netty没有提供,那就自定义一个编码器即可。
自定义编码器:DelimiterBasedFrameEncoder
public class DelimiterBasedFrameEncoder extends MessageToByteEncoder<String> {
private String delimiter;
private Charset charset;
public DelimiterBasedFrameEncoder(String delimiter,Charset charset) {
this.delimiter = delimiter;
this.charset = charset;
}
@Override
protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) {
out.writeBytes(ByteBufUtil.encodeString(ctx.alloc(), CharBuffer.wrap(msg+delimiter), charset));
}
}
这里自定义的编码器,直接融合了StringEecoder的功能,这样咱们在编码的时候就可以少走一个出栈handler处理器了。
拆包、粘包的编码器选择完之后,继续选择字符串的编解码器
,这里我们直接用netty提供的StringDecoder
和StringEecoder
,因为上边将StringEecoder的功能融合进自定义的DelimiterBasedFrameEncoder编码器中了,就省略了一个,稍微看一下源码,很简单的:
好啦,编解码器我们准备好啦,下边就来实战,注意啦!!!可不是你想的那么简单的。。。
实战融合
为了减少篇幅,这里我都截图或贴上关键代码。
netty客户端
这个很简单,没什么可说的,把两个Object编解码器写上即可
netty服务端
编解码器详情
重点在这里,一定要将各个编解码器排好,否则无法达到预想效果,至于编解码器的执行顺序和条件问题,不明白的,请参考:netty实现多协议,多编解码器。
现在要用到这么多的编解码器:
编解码器详情:
-
ObjectEncoder
通过泛型可知,只要是实现了Serializable接口的消息都优先走这个编码器,对消息编码之后,通过继承了MessageToByteEncoder类可知,会继续将消息转换成bytebuf形式,然后继续传递。
-
ObjectDecoder
通过上边的两个图可知,ObjectDecoder解码器会将bytebuf消息转换成Object,这里有一个技巧:
比如这个:ByteToMessageDecoder,只看前部分ByteToMessage;
Byte:代表此handler接收的消息类型为bytbuf;
Message:代表经过处理之后,消息被处理成的类型为Object,当然这个message具体是啥类型,是根据你指定的泛型决定的,,比如ByteToMessage<User>
,就代表message类型为User,如果不指定泛型就是Object类型。 -
DelimiterBasedFrameDecoder
优先接收bytebuf信息,转换成Object消息
-
DelimiterBasedFrameEncoder
优先接收String消息,转换成bytebuf类型
-
StringDecoder
优先接收ByteBuf消息,转换成Object消息
-
LightsHandler
这是最终用的消息处理器,优先接收实现Serializable接口的消息
-
RsuHandler
这是最终用的另一个消息处理器,优先接收String类型的消息
编解码器排序
编解码器以及逻辑处理hander下边我直接排序好,然后针对性一个个讲。
为什么这么排,下边一一讲解,这个顺序是试验了很多次才弄好的,照这个做,绝对没错!!!
c++发送信息,netty接收信息
细心的朋友可能会发现ObjectDecoder
变成了MyObjectDecoder
,原因是ObjectDecoder解码器排在最上边,消息只要进来就会被它拦截,但是进来的是Sting字符串消息,肯定解析会失败或者报错,但是失败之后ObjectDecoder并不会将消息继续往下传递给其它能解析它的解码器,又由于是netty提供的解码器,无法修改源码,所以只能自己写一个类继承这个类,然后重写解码方法了。
MyObjectDecoder
/**
* @author: zhouwenjie
* @description:
* @create: 2022-07-21 11:19
**/
public class MyObjectDecoder extends ObjectDecoder {
public MyObjectDecoder(ClassResolver classResolver) {
super(classResolver);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) {
try {
Object o = super.decode(ctx, in);
if (o == null) {
in.resetReaderIndex();
ByteBuf byteBuf = in.retainedDuplicate();
in.skipBytes(in.readableBytes());
return byteBuf;
}else {
return o;
}
} catch (Exception e) {
in.resetReaderIndex();
ByteBuf byteBuf = in.retainedDuplicate();
in.skipBytes(in.readableBytes());
return byteBuf;
}
}
}
这段代码就是让消息继续传递下去,不明白的可以看本专栏其他文章,里边有介绍到
netty服务端发消息,c++ socker客户端接收消息
这里可以看到字符串消息从RsuHandler
发送出来,经过DelimiterBasedFrameEncoder
编码,然后直接发出去了,为什么不继续往上走将消息传递给ObjectEncoder编码器呢?因为ObjectEncoder只拦截实现了Serializable接口的消息,然而,经过DelimiterBasedFrameEncoder编码的字符串消息变成了ByteBuf类型,所以ObjectEncoder不拦截,直接出栈,传递到c++客户端了。
提醒一下,如果消息是在LightsHandler中发出去的,那么一定要用ctx.pipeline().writeAndFlush,不然无法被DelimiterBasedFrameEncoder编码,因为ctx.writeAndFlush只会从当前位置往上找编码器,ctx.pipeline().writeAndFlush会从最下方开始找编码器。
netty客户端发消息,netty服务端收消息
客户端发送对象消息过来,被MyObjectDecoder接收并解析,然后因为消息是实现了Serializable接口的,所以优先被LightsHandler拦截,最终处理完成,不再继续传递。
netty服务端发消息,netty客户端收消息
不用再解释了吧!!!
这里可能有人有疑问,如果在RsuHandler
发送对象信息,那么消息岂不是会被DelimiterBasedFrameEncoder编码加上分隔符?那不会,因为DelimiterBasedFrameEncoder的泛型是String,不会拦截对象消息,另外,对象消息实现了Serializable接口,优先会被ObjectEncoder拦截。
Ok,结束……