一、Netty基本概念
Netty 是一个基于 Java 的高性能、异步事件驱动的网络应用框架,主要用于构建高并发的网络服务和客户端。它屏蔽了复杂的底层网络通信细节(比如我们前面在网络编程基础所提到的各种IO模型的实现),使得我们可以更加专注于业务的开发。
Netty核心架构
先介绍Netty 的几个核心组件:
Channel(通道):代表与客户端或服务器的连接,负责数据的读写操作,是 Netty 网络通信的核心。
EventLoop(事件循环):Netty 的 I/O 线程模型,管理 Channel 的 I/O 操作。每个 EventLoop 都有自己的线程,负责处理多个 Channel 的事件。
ChannelPipeline(通道管道):Channel 的处理链,用于管理数据在入站和出站的流动。它会依次调用链上的 Handler 进行数据的处理和拦截。
Handler(处理器):分为入站和出站两类,处理 Channel 中的数据操作逻辑。开发者可以自定义 Handler 来编写具体的业务逻辑。
ByteBuf(数据缓冲区):Netty 的数据存储和处理类,优化了传统 ByteBuffer,更高效灵活,支持自动扩容、零拷贝等特性。
Netty中的Reactor实现
Netty是对Reactor模型的实现,其实现思路如下:
像这幅图就是Netty中对主动Reactor模式的实现,其BoosGroup就充当主Reactor,专门负责接受客户端连接;Worker充当从Reactor,专门负责处理读写 I/O 事件。
具体实现参考下图
Netty是基于事件驱动的,专业一手超清课程 比如:连接注册,连接激活;数据读取;异常事件等等,有了事件,就需要一个组件去监控事件的产生和事件的协调处理,这个组件就是EventLoop(事件循环/EventExecutor),在Netty 中每个Channel 都会被分配到一个 EventLoop。一个 EventLoop 可以服务于多个 Channel。每个EventLoop 会占用一个 Thread,同时这个 Thread 会处理 EventLoop 上面发生的所有 IO 操作和事件。
Netty的工作流程
1)Netty抽象出两组线程池:BossGroup和WorkerGroup,每个线程池中都有EventLoop线程(可以是BIO NIO AIO)。BossGroup中的线程专门负责和客户端建立连接,WorkerGroup中的线程专门负责处理连接上的读写,EventLoopGroup相当于一个事件循环组,这个组中含有多少个事件循环
2)EventLoop表示一个不断循环的执行事件处理的线程,每个EventLoop都包含一个Selector,用于监听注册其上的Socket网络连接(Channel)
3)每个Boss EventLoop中循环执行以下三个步骤:
①select:轮询注册在其上的ServerSocketChannel的accept事件(OP_ACCEPT事件)
②processSelectedKeys:处理accept事件,与客户端建立连接,生成一个SocketChannel,并将其注册到某个Worker EventLoop上的Selector上
③runAllTasks:再去以此循环处理任务队列中的其他任务
4)每个Worker EventLoop中循环执行以下三个步骤:
①select:轮询注册在其上的SocketChannel的read/write事件(OP_READ/OP_WRITE事件)
②processSelectedKeys:在对应的SocketChannel上处理read/write事件
③runAllTasks:再去以此循环处理任务队列中的其他任务
5)在以上两个processSelectedkeys步骤中,会使用Pipeline(管道),Pipeline中引用了Channel,即通过Pipeline可以获取到对应的Channel,Pipeline中维护了很多的处理器(拦截处理器、过滤处理器、自定义处理器)。
二、HelloWorld
在这一部分,给大家演示一段用Netty编写一个最简单的客户端/服务器程序
服务端代码
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class NettyServer {
private final int port;
// 构造函数,传入端口号
public NettyServer(int port) {
this.port = port;
}
// 启动服务器的方法
public void start() throws InterruptedException {
// 用于接收客户端连接的主线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
// 用于处理连接后的I/O操作的从线程组
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 创建并配置服务器启动辅助类
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup) // 设置主从线程组
.channel(NioServerSocketChannel.class) // 指定NIO传输通道
.childHandler(new ChannelInitializer<SocketChannel>() { // 设置子处理器
@Override
public void initChannel(SocketChannel ch) {
// 获取通道的pipeline,并添加处理器
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new ServerHandler()); // 添加自定义处理器
}
});
// 绑定端口并启动服务器
ChannelFuture f = bootstrap.bind(port).sync();
System.out.println("Server started on port " + port);
// 等待关闭事件
f.channel().closeFuture().sync();
} finally {
// 优雅关闭线程组
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
new NettyServer(8080).start(); // 启动服务器
}
}
// 服务器自定义处理器
class ServerHandler extends ChannelInboundHandlerAdapter {
/**
* 有数据时的回调
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String content = new String(bytes, Charset.defaultCharset());
System.out.println("收到的数据:"+content);
super.channelRead(ctx, msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//向客户端写回数据
Channel channel = ctx.channel();
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeBytes("hello,nettyclient".getBytes(StandardCharsets.UTF_8));
channel.writeAndFlush(buffer);
super.channelReadComplete(ctx);
}
// 捕获异常时触发
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close(); // 关闭通道
}
}
客户端代码
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class NettyClient {
private final String host;
private final int port;
// 构造函数,指定服务器的地址和端口号
public NettyClient(String host, int port) {
this.host = host;
this.port = port;
}
// 启动客户端的方法
public void start() throws InterruptedException {
// 创建并配置客户端线程组
EventLoopGroup group = new NioEventLoopGroup();
try {
// 创建并配置客户端启动辅助类
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group) // 设置线程组
.channel(NioSocketChannel.class) // 指定NIO传输通道
.handler(new ChannelInitializer<SocketChannel>() { // 设置通道初始化
@Override
public void initChannel(SocketChannel ch) {
// 获取通道的pipeline,并添加处理器
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new ClientHandler()); // 添加自定义处理器
}
});
// 连接服务器并等待连接完成
ChannelFuture f = bootstrap.connect(host, port).sync();
// 发送消息给服务器
f.channel().writeAndFlush("Hello from Client");
// 等待关闭事件
f.channel().closeFuture().sync();
} finally {
// 优雅关闭线程组
group.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
new NettyClient("localhost", 8080).start(); // 启动客户端
}
}
// 客户端自定义处理器
class ClientHandler extends ChannelInboundHandlerAdapter {
// 读取服务器消息时触发
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffer = (ByteBuf) msg;
byte[] bytes = new byte[buffer.readableBytes()];
buffer.readBytes(bytes);
String content = new String(bytes, StandardCharsets.UTF_8);
System.out.println("客户端接收到数据:"+content);
super.channelRead(ctx, msg);
}
// 捕获异常时触发
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close(); // 关闭通道
}
}
三、Netty组件大全
Bootstrap(引导程序)
Bootstrap
是 Netty 用于简化 Channel 和 EventLoop 配置的启动引导类。- 主要分为
Bootstrap
(用于客户端)和ServerBootstrap
(用于服务器端),通过Bootstrap
配置连接参数、Handler、Channel 等,从而更方便地初始化 Channel 并启动。 - 通过链式调用的方式配置
EventLoopGroup
、Channel 类型、Pipeline、Handler 等,简化网络应用的创建过程。
在bootstrap里面就可以指定你需要绑定的PipeLine、Handler(代码里要专门编写好Handler等)。
Channel(通道)
Channel
是 Netty 网络通信的核心抽象,用于表示一个连接,既可以是客户端到服务器的连接,也可以是服务器到客户端的连接。- 负责数据的读写操作,比如bind() connect() read() write(),封装了底层 Socket,并提供了异步 I/O 操作。
- Netty 提供了多种 Channel 实现,例如
NioSocketChannel
、NioServerSocketChannel
、EpollSocketChannel
等,分别适用于不同的操作系统和通信模式。
EventLoop(事件循环)
EventLoop
是一个核心的线程模型,用于处理 Channel 上的 I/O 操作,是 Netty 的 I/O 线程抽象。- 每个
EventLoop
绑定到一个或多个 Channel,负责处理 Channel 的注册、读写和事件调度等操作。 EventLoopGroup
是一组EventLoop
,通常是线程池,负责管理多个EventLoop
的生命周期。客户端和服务器端通常分别会有两个EventLoopGroup
:一个用于接收连接请求,另一个用于处理 I/O 操作。
EventLoopGroup核心线程数有多大?其源代码默认值是cpu核数*2,当然我们可以自己设定,在构造函数传入即可
PipeLine&Handler
ChannelPipeline(通道管道)
ChannelPipeline
是 Netty 中 Channel 的责任链,用于管理 Channel 上的数据流动。- 负责 Channel 的入站和出站事件的拦截和处理。所有的数据事件都会经过 ChannelPipeline 中的各个 Handler 处理。
- 每个 Channel 都会有一个独立的
ChannelPipeline
,并会维护一个 Handler 链表,通过链式调用的方式实现入站和出站事件的拦截和处理。
ChannelHandler(通道处理器)
ChannelHandler
是 Netty 中用于处理 Channel 中 I/O 事件的核心接口,分为ChannelInboundHandler
和ChannelOutboundHandler
,分别处理入站和出站事件。- 入站处理器(InboundHandler):处理来自客户端的读、连接等事件,如
channelRead
、channelActive
等。 - 出站处理器(OutboundHandler):处理写、连接等事件,并向客户端发送数据,如
write
、flush
等。 - 开发者可以继承
ChannelHandlerAdapter
来实现自定义的业务逻辑,并添加到ChannelPipeline
中。
ChannelPipeline 提供了 ChannelHandler 链的容器。以服务端程序为例,客户端发送过来的数据要接收,读取处理,我们称数据是入站的,需要经过一系列Handler处理后;如果服务器想向客户端写回数据,也需要经过一系列Handler处理,我们称数据是出站的。
在processSelectedkeys步骤中,会使用Pipeline(管道),Pipeline中引用了Channel,即通过Pipeline可以获取到对应的Channel,Pipeline中维护了很多的处理器(拦截处理器、过滤处理器、自定义处理器)这里串联了上面提到的
还有一个常用到的叫做ChannelHandlerContext,那他和ChannelHandler有什么关系和区别呢
ChannelHandlerContext 是与 ChannelHandler 关联的上下文对象,用于在处理过程中传递状态和操作。
它提供了对当前 Channel 的引用,并允许访问其上下游的处理器。通过ChannelHandlerContext,可以调用方法向下一个处理器发送事件,或者从上一个处理器接收事件。
每个 ChannelHandler 都会有一个对应的 ChannelHandlerContext,后者用于管理 Channel 的生命周期和事件流动。ChannelHandlerContext 是在 ChannelPipeline 中维护的,表示每个处理器的上下文。
ChannelHandler的分类
inbound入站事件处理顺序(方向)是由链表的头到链表尾,outbound事件的处理顺序是由链表尾到链表头。
inbound入站事件由netty内部触发,最终由netty外部的代码消费。
outbound事件由netty外部的代码触发,最终由netty内部消费。
一个问题,对于编写Netty数据入站处理器,可以选择继承ChannelInboundHandlerAdapter,也可以选择继承SimpleChannelInboundHandler<I>,区别是什么?
继承SimpleChannelInboundHandler需要重写channelRead0方法,且可以通过泛型指定msg类型
SimpleChannelInboundHandler在接收到数据后会自动release掉数据占用的Bytebuffer资源
客户端推荐使用SimpleChannelInboundHandler,服务端看场景
ByteBuf(数据缓冲区)
ByteBuf
是 Netty 的字节缓冲区,用于数据的存储和操作,Java NIO 提供了ByteBuffer 作为它的字节容器,但是这个类使用起来过于复杂,而且也有些繁琐。Netty使用ByteBuf来替代ByteBuffer- 从结构上来说,ByteBuf 由一串字节数组构成。数组中每个字节用来存放信息,ByteBuf提供了两个索引,一个用于读取数据(readerIndex ),一个用于写入数据(writerIndex)。这两个索引通过在字节数组中移动,来定位需要读或者写信息的位置。而JDK的ByteBuffer只有一个索引,因此需要使用flip方法进行读写切换
- 提供了自动扩容、读写指针等机制,避免了 JDK NIO 中
ByteBuffer
手动管理容量、切换读写模式的复杂操作。 - 支持池化(池化
ByteBuf
可以重用内存,减少垃圾回收)和零拷贝(CompositeByteBuf
支持多个 ByteBuf 组合为一个,避免数据复制)。
ByteBuf的三个指针
readerIndex:指示读取的起始位置, 每读取一个字节, readerIndex自增累加1。 如果readerIndex 与writerIndex 相等,ByteBuf不可读。
writerIndex:指示写入的起始位置, 每写入一个字节, writeIndex自增累加1。如果增加到 writerIndex 与capacity()容量相等,表示 ByteBuf 已经不可写,但是这个时候,并不代表不能往 ByteBuf 中写数据了, 如果发现往ByteBuf 中写数据写不进去的话,Netty 会自动扩容 ByteBuf,直到扩容到底层的内存大小为 maxCapacity
maxCapacity:指示ByteBuf可以扩容的最大容量,如果向ByteBuf写入数据时,容量不足,可以进行扩容的最大容量
ByteBuf常用API
容量相关的API
capacity():表示 ByteBuf 底层占用了多少字节的内存(包括丢弃的字节、可读字节、可写字节),不同的底层实现机制有不同的计算方式。
maxCapacity(): ByteBuf 底层最大能够占用多少字节的内存,当向 ByteBuf 中写数据的时候,如果发现容量不足,则进行扩容,直到扩容到 maxCapacity,超过这个数,就抛异常。
readableBytes() 与 isReadable():readableBytes() 表示 ByteBuf 当前可读的字节数,它的值等于
writerIndex-readerIndex,如果两者相等,则不可读,isReadable() 方法返回 false
writableBytes()、 isWritable() 、maxWritableBytes():writableBytes() 表示 ByteBuf 当前可写的字节数,它的值等于 capacity()-writerIndex,如果两者相等,则表示不可写,isWritable() 返回 false,但是这个时候,并不代表不能往 ByteBuf 中写数据了, 如果发现往ByteBuf 中写数据写不进去的话,Netty 会自动扩容 ByteBuf,直到扩容到底层的内存大小为 maxCapacity,而 maxWritableBytes() 就表示可写的最大字节数,它的值等于maxCapacity-writerIndex
读写指针相关API
readerIndex() 与 readerIndex(int readerIndex):前者表示返回当前的读指针 readerIndex, 后者表示设置读指针
writeIndex() 与 writeIndex(int writerIndex):前者表示返回当前的写指针 writerIndex, 后者表示设置写指针
markReaderIndex() 与markWriterIndex():表示把当前的读指针/写指针保存起来,操作形式为:
markedReaderIndex = readerIndex markedWriterIndex = writerIndex
读写操作相关API
writeBytes(byte[] src):表示把字节数组 src 里面的数据全部写到 ByteBuf,src字节数组大小的长度通常小于等于writableBytes()
readBytes(byte[] dst):把 ByteBuf 里面的数据全部读取到 dst,dst 字节数组的大小通常等于 readableBytes()
writeByte(int value)、readByte():writeByte() 表示往 ByteBuf 中写一个字节,而 readByte() 表示从 ByteBuf 中读取一个字节,类似的 API 还有 writeBoolean()、writeChar()、writeShort()、writeInt()、writeLong()、writeFloat()、writeDouble() 与 readBoolean()、readChar()、readShort()、readInt()、readLong()、readFloat()、readDouble() 等等
丢弃、清理,释放API
discardReadBytes():丢弃已读取的字节空间,可写空间变多
clear():重置readerIndex 、 writerIndex 为0,需要注意的是,重置并没有删除真正的内容
release():真正去释放bytebuf中的数据
ReferenceCountUtil.release(buf):工具方法,内部还是调用release()
三类不同的ByteBuf
HeapByteBuf(堆缓冲区):内存分配在jvm堆,分配和回收速度比较快,可以被JVM自动回收,缺点是,如果进行socket的IO读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有一定程度的下降。由于在堆上被 JVM 管理,在不被使用时可以快速释放。可以通过 ByteBuf.array() 来获取 byte[] 数据。
关于这里的JVM堆内堆外内存的理解
堆内存:由 JVM 自动管理,使用垃圾回收机制进行内存的分配和回收。但是访问速度较慢,受垃圾回收影响,可能导致性能波动。
堆外内存:不由 JVM 管理,开发者需要手动管理其分配和释放。但是相比起来减少了内存拷贝,特别是在 I/O 操作时性能更佳,适合高性能需求场景。
ByteBuf的分配器
通常在实际开发中,我们一般都通过ChannelHandlerContext(当然也可以通过Channel)来获取ByteBuf的分配器
再通过分配器来获取ByteBuf实例
BufAllocator allocator = channelHandlerContext.alloc();
ByteBuf buf = channelHandlerContext.alloc().buffer();
Netty 提供了两种 ByteBufAllocator 的实现,分别是:
Netty默认使用了PooledByteBufAllocator,但可以通过引导类设置非池化模式
ByteBuf的释放
引用计数:
ByteBuf如果采用的是堆缓冲区模式的话,可以由GC回收,但是如果采用的是直接缓冲区,就不受GC的管理,就得手动释放,否则会发生内存泄露,Netty自身引入了引用计数,提供了ReferenceCounted接口,当对象的引用计数>0时要保证对象不被释放,当为0时需要被释放
那对于直接缓冲区的ByteBuf如何释放呢?
①手动释放,就是在使用完成后,调用ReferenceCountUtil.release(byteBuf); 进行释放,这种方式的弊端就是一旦忘记释放就可能会造成内存泄露
②自动释放有三种方式,分别是:
入站的TailHandler(TailContext)、继承SimpleChannelInboundHandler、HeadHandler(HeadContext)的出站释放
TailContext:Inbound流水线的末端,如果前面的handler都把消息向后传递最终由TailContext释放该消息,需要注意的是,如果没有进行向下传递,是不会进行释放操作的
SimpleChannelInboundHandler:自定义的InboundHandler继承自SimpleChannelInboundHandler,在SimpleChannelInboundHandler中自动释放
HeadContext:outbound流水线的末端,出站消息一般是由应用所申请,到达最后一站时,经过一轮复杂的调用,在flush完成后终将被release掉
总结下来,到底什么时候采取什么情况呢?
对于入站的消息:
1)对原消息不做处理的话,将依次调用 ctx.fireChannelRead(msg)把原消息往下传,如果能到TailContext,那不用做什么释放,它会自动释放
2)要将原消息转化为新的消息并调用 ctx.fireChannelRead(newMsg)往下传的话,那需要将原消息release掉
3)如果已经不再调用ctx.fireChannelRead(msg)传递任何消息,需要把原消息release掉
对于出站的消息:
则无需用户关心,消息最终都会走到HeadContext,flush之后会自动释放。
Future&Promise
Future
Netty 采用非阻塞的 I/O 模型,许多操作(如发送数据、连接等)都是异步的。Future 允许你在不阻塞主线程的情况下处理这些操作的结果。
每个异步操作都可以返回一个Future。他里面提供了一系列的API,比如检查这个任务是否执行完成等
同时,还可以为这个Future添加监听器,当Future执行到某个状态时(比如已完成)就回调某段程序
ChannelFuture future = channel.writeAndFlush(message);
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) throws Exception {
if (f.isSuccess()) {
System.out.println("执行成功");
} else {
System.err.println("执行失败,原因:" + f.cause());
}
}
});
Promise
Netty的Future,只是增加了监听器。整个异步的状态,是不能进行设置和修改的,于是Netty的 Promise接口扩展了Netty的Future接口,可以设置异步执行的结果。在IO操作过程,如果顺利完成、或者发生异常,都可以设置Promise的结果,并且通知Promise的Listener们。
public void asyncOperationWithPromise(Channel channel) {
Promise<String> promise = channel.eventLoop().newPromise(); // 创建可写的Promise
// 模拟异步操作
channel.eventLoop().execute(() -> {
try {
// 模拟执行异步操作
Thread.sleep(1000);
// 主动设置Promise的结果,相当于“写入”成功状态
promise.setSuccess("操作成功");
} catch (Exception e) {
// 主动设置Promise的异常,相当于“写入”失败状态
promise.setFailure(e);
}
});
// 添加监听器,处理Promise的结果(相当于读取操作)
promise.addListener((Future<String> future) -> {
if (future.isSuccess()) {
System.out.println("异步操作结果:" + future.get()); // 读取成功结果
} else {
System.err.println("异步操作失败:" + future.cause()); // 读取失败原因
}
});
}
在listener中拿到的future结果就是promise setSuccess后的值
编码器和解码器(Encoder/Decoder)
Encoder
和Decoder
是 Netty 的编码解码工具,分别用于将 POJO 对象转为二进制数据,以及将二进制数据解析为 POJO 对象。- 常见的编码器和解码器包括
StringDecoder
、StringEncoder
、ProtobufDecoder
、HttpObjectDecoder
等,可用于多种协议的数据传输。 - 编解码器通常被添加到
ChannelPipeline
中的特定位置,在处理入站和出站事件时自动进行编码和解码。
在Netty中编解码器有两类,分别是:一次编解码器、二次编解码器,接下来分别介绍
一次编解码器
一次编解码器主要就是解决粘包拆包问题,那在讨论粘包拆包问题之前,我们要先来聊一聊什么是TCP的粘包拆包。
TCP 粘包,拆包
什么是粘包?
①当发送方每次写入到TCP发送端缓冲区的数据小于缓冲区大小,那么多个数据会合并成一条数据发送出去,导致粘包产生
②当接收方读取套接字缓冲区数据不够及时,那么此时接收端缓冲区就会累积了多个数据包,导致粘包产生
什么是拆包?
①当发送方 写入到TCP发送端缓冲区的数据大于缓冲区大小,那么这一个数据包就会被拆成多个再分别发送出去,导致拆包产生
为什么会产生粘包拆包?
因为TCP协议是面向字节流的,它对于每一条消息只把它看成都一个一个字节为单位的,只对每个字节做一个序号编排,所以是看不到消息的边界,当有缓冲区足够的字节了,那就该发出去了,当缓冲区没有足够的字节,可以等后续的一些字节来组成一个最小发送的数据包大小
还有一点就是TCP采用了Nagle算法,这个确实会导致粘包,其核心思想就是:当网络中有未被确认的数据包时,新的小数据包不会立即发送,而是先缓存在发送端,等待前一个数据包的确认。只有当收到前一个数据包的确认(ACK)或者缓冲区内的数据量达到 MSS(最大报文长度)时,才会将数据发送出去。
如何解决粘包拆包?
解决TCP粘包,拆包问题的根本:找出消息的边界
常见方案:
1、使用固定长度的报文:即每个数据包的长度固定,接收端可以根据长度将报文内容准确拆分开。但这种方法浪费了数据包中多余的空间,且不适合变长数据包。
2、使用分隔符:在每个数据包后添加特定的分隔符(如 \n),接收端通过分隔符来判断每个数据包的边界。这适用于文本协议,但对二进制协议不适用,且需要避免分隔符出现在数据内容中。
3、在消息头中定义长度字段:发送时在数据包头部加入一个字段,表示数据包的总长度,接收端通过读取该字段来判断数据包的边界。这种方式较为通用,也是 Netty 中常用的解决方案。
Netty的三种一次解码器
固定长度FixedLengthFrameDecoder
// FixedLengthFrameDecoder 会将每 64 字节的数据作为一条消息进行处理。
pipeline.addLast(new FixedLengthFrameDecoder(64));
分隔符DelimiterBasedFrameDecoder
// DelimiterBasedFrameDecoder 会以 # 将数据流进行拆分
ByteBuf delimiter = Unpooled.copiedBuffer("#".getBytes());
pipeline.addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
pipeline.addLast(new StringDecoder());
首部字段存消息长度大小LengthFieldBasedFrameDecoder
其构造参数:
maxFrameLength
:单条消息的最大长度。lengthFieldOffset
:长度字段的偏移量,即在消息头中从哪里开始读取长度信息。lengthFieldLength
:长度字段的字节数。lengthAdjustment
:长度字段的补偿值(一般用于调整头长度和实际数据包长度的差异)。initialBytesToStrip
:表示接收端需要跳过的字节数,用于剥离消息头。
/*
这里的配置表示:
消息最大长度为 1024 字节,
长度字段偏移量为 0 字节(表示长度字段在消息的最开始位置),
长度字段长度为 4 字节,
且跳过头部 4 字节(即长度字段本身)后,直接读取消息内容。
*/
pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));
二次编解码器
我们把解决半包粘包问题的常用三种解码器叫一次解码器,其作用是将原始数据流(可能会出现粘包和拆包的数据流)转换为用户数据(ByteBuf中存储),但仍然是字节数据,所以我们需要二次解码器将字节数组转换为java对象,或者将将一种格式转化为另一种格式,方便上层应用程序使用。
一次解码器继承自:ByteToMessageDecoder;二次解码器继承自:MessageToMessageDecoder;但他们的本质都是继承ChannelInboundHandlerAdapter
所以说到这里,可以把二次编解码器理解为序列化与反序列化的机制
常用的二次编解码我们在这里介绍四种
StringDecoder、StringEncoder ProtobufDecoder、ProtobufEecoder
StringDecoder 是一个将字节数据解码为字符串的解码器。
StringEncoder 是将字符串数据编码为字节的编码器。
ProtobufDecoder 是将 Protocol Buffers 格式的字节数据解码为 Java 对象的解码器。
ProtobufEncoder 是将 Protocol Buffers Java 对象编码为字节的编码器。
ProtobufDecoder 和 ProtobufEncoder 适用于结构化二进制数据协议,适合跨语言、跨平台通信,且数据结构紧凑。
Http编解码
在 Netty 中,HTTP 协议的编解码主要依赖于 HttpRequestDecoder 和 HttpResponseEncoder 这两个类,此外还可以结合 HttpObjectAggregator、HttpServerCodec 等编解码器进行优化和简化。
四、Netty高级特性
参数设置
我们对于一些参数的设置都是通过BootStrap中的.option和.childOption方法实现的,其中
1、针对ServerSocketChannel:通过.option设置
serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
针对ScoketChannel,7个,通过.childOption设置,常用的两个如下:
2、TCP_NODELAY:设置是否启用nagle算法,该算法是tcp在发送数据时将小的、碎片化的数据拼接成一个大的报文一起发送,以此来提高效率,默认是false(启用),如果启用可能会导致有些数据有延时,如果业务不能忍受,小报文也需要立即发送则可以禁用该算法
零拷贝
我们先来聊一聊操作系统中的零拷贝是什么?
在 OS 层面上的零拷贝通常指避免在 用户态 与 内核态 之间来回拷贝数据. 例如 Linux 提供的 mmap 系统调用, 它可以将一段用户空间内存映射到内核空间,。
当映射成功后, 用户对这段内存区域的修改可以直接反映到内核空间; 同样地, 内核空间对这段区域的修改也直接反映用户空间。正因为有这样的映射关系, 我们就不需要在 用户态 与 内核态 之间拷贝数据, 提高了数据传输的效率
但是,Netty中的零拷贝概念与os的零拷贝不太一样,Netty的 零拷贝 的更多的是偏向于 优化数据操作 这样的概念
DirectBuffer:
直接堆外内存区域分配空间而不是在堆内存中分配, 如果使用传统的堆内存分配,当我们需要将数据通过socket发送的时候,需要将数据从堆内存拷贝到堆外直接内存,然后再由直接内存拷贝到网卡接口层,通过Netty提供的DirectBuffer直接将数据分配到堆外内存,避免多余的数据拷贝
CompositeBufer:
传统的ByteBuffer,如果需要将两个ByteBuffer中的数据组合到一起,我们需要首先创建一个
size=size1+size2大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。但是使用Netty提供的组合ByteBuf,就可以避免这样的操作,因为CompositeByteBuf并没有真正将多个Buffer组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝;同时也支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝。