一、Netty编解码器
Netty
涉及到编解码的组件有Channel
、ChannelHandler
、ChannelPipe
等,先大概了解下这几个组件的作用。
1、Channel组件
Channel
是Netty
网络通信的组件,客户端与服务端建立的一个连接通道,能够用于执行网络I/O
操作。不同协议、不同的阻塞类型的连接都有不同的Channel
类型与之对应。
2、ChannelHandler组件
ChannelHandler
充当了处理入站和出站数据的逻辑容器。例如,实现ChannelInboundHandler
接口(或继承ChannelInboundHandlerAdapter
类),就可以接收入站事件和数据,这些数据随后会被应用程序的业务逻辑处理。当要给连接的客户端发送响应时,也可以从ChannelInboundHandler
冲刷数据。业务逻辑通常写在一个或者多个ChannelInboundHandler
中。ChannelOutboundHandler
原理一样,只不过它是用来处理出站数据的。
3、ChannelPipeline组件
ChannelPipeline
提供了ChannelHandler
链的容器。以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过pipeline
中的一系列ChannelOutboundHandler
(ChannelOutboundHandler
调用是从tail
到head
方向逐个调用每个handler
的逻辑),并被这些Handler
处理,反之则称为入站的,入站只调用pipeline
里的ChannelInboundHandler
逻辑(ChannelInboundHandler
调用是从head
到tail
方向逐个调用每个handler
的逻辑)。

4、编码解码器
- 解码器:负责处理入站
InboundHandler
数据,将字节数组转换为消息对象 - 编码器:负责处理出站
OutboundHandler
数据,将消息对象转换为字节数组
当通过Netty
发送或者接受一个消息的时候,就会发生一次数据的转换。入站消息会被解码,出站消息会被编码。
Netty
提供了一系列实用的编码解码器,他们都实现了ChannelInboundHadnler
或者ChannelOutboundHandler
接口。在这些类中,channelRead
方法已经被重写了。以入站为例,对于每个从入站Channel
读取的消息,这个方法会被调用。随后,它将调用由已知解码器所提供的decode()
方法进行解码,并将已经解码的字节转发给ChannelPipeline
中的下一个ChannelInboundHandler
。
Netty
提供了很多编解码器,例如:
StringEncoder
字符串编码器StringDecoder
字符串解码器ObjectEncoder
对象编码器ObjectDecoder
对象解码器FixedLengthFrameDecoder
固定长度的解码器LineBasedFrameDecoder
以换行符为结束标识的解码器DelimiterBasedFrameDecoder
指定消息分隔符的解码器LengthFieldBasedFrameDecoder
基于长度通用解码器
5、自定义编解码器
- 通过继承
ByteToMessageDecoder
自定义解码器 - 通过继承
MessageToByteEncoder<T>
自定义编码器
/**
* todo 自定义解码器
*/
public class ByteToLongDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
System.out.println("ByteToLongDecoder decode 被调用");
//todo 因为long占8个字节, 需要判断大于等于8个字节时才能读取一个long
if(in.readableBytes() >= 8) {
out.add(in.readLong());
}
}
}
/**
* 自定义编码器
*/
public class LongToByteEncoder extends MessageToByteEncoder<Long> {
@Override
protected void encode(ChannelHandlerContext ctx, Long msg, ByteBuf out) throws Exception {
System.out.println("LongToByteEncoder encode被调用");
System.out.println("msg=" + msg);
out.writeLong(msg);
}
}
二、TCP粘包拆包
1、什么是TCP粘包拆包
TCP
粘包:把多个小的包封装成一个大的数据包发送,发送方发送的若干数据包到接收方时粘成一个包
TCP
拆包:把一个完整的包拆分为多个小包进行发送,发送方发送一个数据包到接收方时被拆分为若干个小包

2、为什么会出现粘包拆包现象
TCP
是面向连接的,面向字节流的, 提供可靠交互。发送端为了提高发送效率,使用了Nagle算法,将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的。
3、粘包拆包解决方案
Netty
拆包的基类ByteToMessageDecoder
ByteToMessageDecoder
内部维护了一个数据累积器,每次读取到数据都会进行累加,然后尝试对累加的数据进行拆包- 每次都将读取到的数据通过内存拷贝的方式,累积到
ByteToMessageDecoder
的数据累积器中 - 调用子类的
decode()
方法对累积的数据尝试进行拆包
拆包解决思路:
基本思路就是不断的从TCP
缓冲区中读取数据,每次读取完都需要判断是否是一个完整的数据包
- 若当前读取的数据不足以拼接成一个完整的业务数据包,那就保留该数据,继续从tcp缓冲区中读取,直到得到一个完整的数据包
- 若当前读到的数据加上已经读取的数据足够拼接成一个数据包,那就将已经读取的数据拼接上本次读取的数据,构成一个完整的业务数据包传递到业务逻辑,多余的数据仍然保留,以便和下次读到的数据尝试拼接
- 关键点是如何判断是一个完整的数据包
方案一:设置定长消息(对应Netty
提供的FixedLengthFrameDecoder
解码器)
方案二:设置消息边界(分隔符,对应Netty
提供的DelimiterBasedFrameDecoder
解码器)
方案三:使用带消息头的协议,消息头存储消息开始标识及消息的长度信息Header+Body
(对应Netty
提供的LengthFieldBasedFrameDecoder
解码器)
方案四:发送消息长度,自定义消息解码器
4、分隔符案例(部分代码)
//todo 客户端发送数据
public class ClientHandler extends ChannelInboundHandlerAdapter {
/**
* todo 当客户端连接服务器完成就会触发该方法
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String message = "aaaaaaaaaaaaaaaa&_bbbbbbbbbbbbbbbbbb&_ccccccccccc&_";
ByteBuf byteBuf = Unpooled.buffer(message.getBytes().length);
byteBuf.writeBytes(message.getBytes());
ctx.writeAndFlush(byteBuf);
}
}
//todo 服务端添加分隔符解码器
public class EchoServer {
public static void main(String[] args) {
//todo 设置两个线程组
serverBootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//todo 向pipeline加入分隔符解码器
ByteBuf delimiter = Unpooled.copiedBuffer("&_".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,true,delimiter));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ServerHandler());
}
});
}
}
5、自定义解码器案例(部分代码)
/**
* todo 自定义协议包
*/
public class MyMessageProtocol {
//todo 定义一次发送包体长度
private int len;
//todo 一次发送包体内容
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;
}
}
/**
* todo 自定义编码器
*/
public class MyMessageEncoder extends MessageToByteEncoder<MyMessageProtocol> {
@Override
protected void encode(ChannelHandlerContext ctx, MyMessageProtocol msg, ByteBuf out) throws Exception {
System.out.println("MyMessageEncoder encode 方法被调用");
out.writeInt(msg.getLen());
out.writeBytes(msg.getContent());
}
}
/**
* todo 自定义解码器
*/
public class MyMessageDecoder extends ByteToMessageDecoder {
private int length = 0;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
System.out.println("MyMessageDecoder decode 被调用");
//todo 需要将得到二进制字节码-> MyMessageProtocol 数据包(对象)
if(in.readableBytes() >= 4) {
if (length == 0){
length = in.readInt();
}
if (in.readableBytes() < length) {
System.out.println("当前可读数据不够,继续等待 ...");
return;
}
byte[] content = new byte[length];
if (in.readableBytes() >= length){
in.readBytes(content);
//todo 封装成MyMessageProtocol对象,传递到下一个handler业务处理
MyMessageProtocol messageProtocol = new MyMessageProtocol();
messageProtocol.setLen(length);
messageProtocol.setContent(content);
out.add(messageProtocol);
}
length = 0;
}
}
}
//todo 服务端
public class MyServer {
public static void main(String[] args) throws Exception {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//todo 添加自定义解码器
pipeline.addLast(new MyMessageDecoder());
pipeline.addLast(new MyServerHandler());
}
});
}
}
//todo 服务端 handler
public class MyServerHandler extends SimpleChannelInboundHandler<MyMessageProtocol> {
private int count;
@Override
protected void channelRead0(ChannelHandlerContext ctx, MyMessageProtocol msg) throws Exception {
System.out.println("====服务端接收到消息如下====");
System.out.println("长度=" + msg.getLen());
System.out.println("内容=" + new String(msg.getContent(), CharsetUtil.UTF_8));
System.out.println("服务端接收到消息包数量=" + (++this.count));
}
}
//todo 客户端
public class MyClient {
public static void main(String[] args) throws Exception{
bootstrap.group(group).channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//todo 添加自定义编码器
pipeline.addLast(new MyMessageEncoder());
pipeline.addLast(new MyClientHandler());
}
});
}
}
//todo 客户端 handler
public class MyClientHandler extends SimpleChannelInboundHandler<MyMessageProtocol> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for(int i = 0; i< 200; i++) {
String msg = "你好,我是张三!";
//创建协议包对象
MyMessageProtocol messageProtocol = new MyMessageProtocol();
messageProtocol.setLen(msg.getBytes(CharsetUtil.UTF_8).length);
messageProtocol.setContent(msg.getBytes(CharsetUtil.UTF_8));
ctx.writeAndFlush(messageProtocol);
}
}
}
三、Netty零拷贝
1、直接内存
直接内存(Direct Memory
)并不是虚拟机运行时数据区的一部分,也不是Java
虚拟机规范中定义的内存区域,某些情况下这部分内存也会被频繁地使用,而且也可能导致OutOfMemoryError
异常出现。Java
里用DirectByteBuffer
可以分配一块直接内存(堆外内存),元空间对应的内存也叫作直接内存,它们对应的都是机器的物理内存。

2、Netty零拷贝
Netty
的接收和发送数据采用堆外直接内存,使用堆外直接内存进行Socket
读写,不需要进行字节缓冲区的二次拷贝。
如果使用传统的JVM
堆内存(HEAP BUFFERS
)进行Socket
读写,JVM
会将堆内存数据拷贝一份到直接内存中,然后才能写入Socket
中。JVM
堆内存的数据是不能直接写入Socket
中的。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
3、直接内存的优缺点
优点:
- 不占用堆内存空间,减少了发生GC的可能
- java虚拟机实现上,本地IO会直接操作直接内存,而非直接内存则需要二次拷贝(堆内存=>直接内存=>系统调用=>硬盘/网卡)
缺点:
- 初始分配较慢
- 没有JVM直接帮助管理内存,容易发生内存溢出