学习地址: https://dongzl.github.io/netty-handbook/#/
1、编解码
Netty
涉及到编解码的组件有Channel
、ChannelHandler
、ChannelPipe
等,先大概了解下这几个组件的作用。
ChannelHandler
ChannelHandler
充当了处理入站和出站数据的应用程序逻辑容器。例如,实现ChannelInboundHandler
接口(或 ChannelInboundHandlerAdapter
),你就可以接收入站事件和数据,这些数据随后会被你的应用程序的业务逻辑处理。当你要给连接的客户端发送响应时,也可以从ChannelInboundHandler
冲刷数据。你的业务逻辑通常写在一个或者多个ChannelInboundHandler
中。 ChannelOutboundHandler
原理一样,只不过它是用来处理出站数据的。
ChannelPipeline
ChannelPipeline
提供了ChannelHandler
链的容器。以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过pipeline
中的一系列ChannelOutboundHandler
(ChannelOutboundHandler
调用是从tail
到head
方向逐个调用每个handler
的逻辑),并被这些Handler
处理,反之则称为入站的,入站只调用pipeline
里的 ChannelInboundHandler
逻辑(ChannelInboundHandler
调用是从head
到tail
方向逐个调用每个handler的逻辑)。
编码解码器
当你通过Netty
发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码:从字节转换为另一种格式(比如java
对象);如果是出站消息,它会被编码成字节。
Netty
提供了一系列实用的编码解码器,他们都实现了ChannelInboundHadnler
或者ChannelOutboundHandler
接口。在这些类中, channelRead
方法已经被重写了。
以入站为例,对于每个从入站Channel读取的消息,这个方法会被调用。随后,它将调用由已知解码器 所提供的decode()方法进行解码,并将已经解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler。
Netty提供了很多编解码器,比如编解码字符串的StringEncoder和StringDecoder,编解码对象的ObjectEncoder和ObjectDecoder 等。如果要实现高效的编解码可以用protobuf,但是protobuf需要维护大量的proto文件比较麻烦,现在一般可以使用protostuff。 protostuff是一个基于protobuf实现的序列化方法,它较于protobuf最明显的好处是,在几乎不损耗性能的情况下做到了不用我们 写.proto文件来实现序列化。使用它也非常简单,代码如下:
protostuff编解码实现
1、引入依赖
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-api</artifactId>
<version>1.0.10</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.0.10</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.0.10</version>
2、序列化工具类
public class ProtostuffUtil {
private static final Map<Class<?>, Schema<?>> CACHED_SCHEMA = new ConcurrentHashMap<>();
private static <T> Schema<T> getSchema(Class<T> clazz) {
@SuppressWarnings("unchecked")
Schema<T> schema = (Schema<T>) CACHED_SCHEMA.get(clazz);
if (schema == null) {
schema = RuntimeSchema.getSchema(clazz);
if (schema != null) {
CACHED_SCHEMA.put(clazz, schema);
}
}
return schema;
}
/**
* 序列化
*
* @param obj
* @return
*/
public static <T> byte[] serializer(T obj) {
@SuppressWarnings("unchecked")
Class<T> clazz = (Class<T>) obj.getClass();
LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
try {
Schema<T> schema = getSchema(clazz);
return ProtostuffIOUtil.toByteArray(obj, schema, buffer);
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
} finally {
buffer.clear();
}
}
/**
* 反序列化
*
* @param data
* @param clazz
* @return
*/
public static <T> T deserializer(byte[] data, Class<T> clazz) {
try {
T obj = clazz.newInstance();
Schema<T> schema = getSchema(clazz);
ProtostuffIOUtil.mergeFrom(data, obj, schema);
return obj;
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
}
3、编码器
public class ProtoEncoder extends MessageToByteEncoder<Object> {
private final Class<?> genericClass;
public ProtoEncoder(Class<?> genericClass) {
this.genericClass = genericClass;
}
@Override
public void encode(ChannelHandlerContext ctx, Object in, ByteBuf out) throws Exception {
if (genericClass.isInstance(in)) {
byte[] data = ProtostuffUtil.serializer(in);
out.writeInt(data.length);
out.writeBytes(data);
}
}
}
4、解码器
public class ProtoDecoder extends ByteToMessageDecoder {
private final Class<?> genericClass;
public ProtoDecoder(Class<?> genericClass) {
this.genericClass = genericClass;
}
@Override
public final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
if (in.readableBytes() < 4) {
return;
}
in.markReaderIndex();
int dataLength = in.readInt();
if (dataLength < 0) {
ctx.close();
}
if (in.readableBytes() < dataLength) {
in.resetReaderIndex();
}
byte[] data = new byte[dataLength];
in.readBytes(data);
Object obj = ProtostuffUtil.deserializer(data, genericClass);
out.add(obj);
}
}
2、Netty粘包拆包
TCP
是一个流协议,就是没有界限的一长串二进制数据。TCP
作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP
缓冲区 的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被TCP
拆分成多个包进行发送,也有可能把多个小的包封装成 一个大的数据包发送,这就是所谓的TCP
粘包和拆包问题。面向流的通信是无消息保护边界的。 如下图所示,client
发了两个数据包D1
和D2
,但是server
端可能会收到如下几种情况的数据。
解决方案
- 消息定长度,传输的数据大小固定长度,例如每段的长度固定为100字节,如果不够空位补空格
- 在数据包尾部添加特殊分隔符,比如下划线,中划线等,这种方法简单易行,但选择分隔符的时候一定要注意每条数据的内部一定不 能出现分隔符。
- 发送长度:发送每条数据的时候,将数据的长度一并发送,比如可以选择每条数据的前4位是数据的长度,应用层处理时可以根据长度来判断每条数据的开始和结束。
Netty提供了多个解码器,可以进行分包的操作,如下:
- LineBasedFrameDecoder (回车换行分包)
- DelimiterBasedFrameDecoder(特殊分隔符分包)
- FixedLengthFrameDecoder(固定长度报文来分包)
现在常用的是第三种解决方案:
- 先定义一个协议包
/**
* @Author xiao7
* @Description 协议包
* @Date 8:32 下午 2021/6/24
**/
public class MessageProtocol {
/**
* 信息长度
*/
private int len;
/**
* 内容
*/
private byte[] content;
public int getLen() {
return len;
}
public void setLen(int len) {
this.len = len;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
}
- 定义编码器
public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
@Override
protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
System.out.println("MyMessageEncoder encode 方法被调用");
// 写长度
out.writeInt(msg.getLen());
// 写内容
out.writeBytes(msg.getContent());
}
}
- 定义解码器
public class MyMessageDecoder extends ReplayingDecoder<Void> {
private int length = 0;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
System.out.println("MyMessageDecoder decode 被调用");
// 可读必须大于int长度才去读
if (in.readableBytes() >= 4) {
if (length == 0) {
length = in.readInt();
}
// 读完长度后,再去读内容长度,长度不足length的时候就不读,等下一个包再说
if (in.readableBytes() >= length) {
// 足够长了就读一个包
byte[] content = new byte[length];
in.readBytes(content);
//封装成 MessageProtocol 对象,放入 out, 传递下一个handler业务处理
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(length);
messageProtocol.setContent(content);
out.add(messageProtocol);
}
// 不读直接跳过这个包
else {
return;
}
// 读完一个包,记得归零需要读的长度
length = 0;
}
}
}
- 最后给客户端配置编解码器,测试就可以了。