目录
一、netty五大组件
1.1 EventLoop
EventLoop本质上是一个单线程执行器(在内部维护了一个线程和一个Selector),在线程内部的run方法处理Channel上的IO事件。
它继承了三个父类:
- 一个是JUC下面的ScheduledExecutorService,因此它用于线程池中所以的方法;
- 一个是Netty自己提供的OrderedEventExecutor,提供了两个重要方法:boolean inEventLoop(Thread t)用于哦按的一个线程是否属于此EventLoop; EventLoopGroup parent()用于查看自己属于哪个EventLoopGroup。 EventLoopGroup是一组EventLoop,Channel会调用EventLoopGroup的register方法来绑定其中一个EventLoop,后续这个Channel上的所有IO事件都会由此EventLoop来处理(也就是单线程处理,保证IO事件处理时的线程安全问题)
- 另一个是Netty自己提供的EventExecutorGroup,该接口实现了Iterable迭代器接口,能够遍历EventLoop,同时也提供了next方法能够获取集合中的下一个EventLoop。
代码示例:
@Slf4j
public class EventLoopServer {
public static void main(String[] args) {
//默认group,用于执行需要较长时间执行的handler,避免阻塞负责整个channel的EventLoop
DefaultEventLoopGroup defaultGroup = new DefaultEventLoopGroup(2);
new ServerBootstrap()
//细分为boss和Worker,boss只负责ServerSocketChannel的accept事件,worker负责SocketChannel的读写事件
.group(new NioEventLoopGroup(), new NioEventLoopGroup(2))
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>(){
//连接之后执行
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast("handle1", new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
log.info(buf.toString(Charset.defaultCharset()));
//如果是添加多handle,必须调用上下文的fireChannelxx()方法
//该方法会调用invokeChannelRead(findContextInbound(), msg)方法,invokeChannelRead中会首先获取下一个handler的执行线程,
//然后判断该线程是否属于当前EventLoop,若属于,则由当前线程直接调用,若不属于,则使用该线程调用
ctx.fireChannelRead(msg);
}
}).addLast(defaultGroup, "handler2 ", new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
log.info(buf.toString(Charset.defaultCharset()));
}
});
}
}).bind(8091);
}
}
ctx.fireChannelRead(msg)逻辑:
@Override
public ChannelHandlerContext fireChannelRead(final Object msg) {
invokeChannelRead(findContextInbound(), msg);
return this;
}
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeChannelRead(m);
} else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRead(m);
}
});
}
}
1.2 Channel && ChannelFuture
Channel主要方法:
- close方法用来关闭Channel;
- closeFuture,返回一个CloseFuture对象,可以基于sync()方法或者addListener方法实现在Channel的关闭之后执行指定逻辑;
- pipeline方法用于添加handle处理器;
- write方法用于将数据写入但不刷出;
- writeAndFlush用于将数据写入并刷出;
ChannelFuture:继承了JUC下的Future接口,拥有异步返回结果的能力,是异步 Channel IO 操作的结果,Netty 中的所有 IO 操作都是异步的。这意味着任何 IO 调用都会立即返回一个 ChannelFuture 实例,但不能保证请求的 IO 操作在调用结束时已经完成,该实例提供有关 IO 操作的结果或状态的信息。 ChannelFuture 要么未完成,要么已完成。当 IO 操作开始时,会创建一个新的 future 对象。新的 future 最初是未完成的——它既没有成功,也没有失败,也没有取消,因为 IO 操作还没有完成。如果 IO 操作成功、失败或取消完成,则将来会标记为已完成,并提供更具体的信息,例如失败的原因。请注意,即使失败和取消都属于完成状态。
主要方法:
- addListener(GenericFutureListener) :用于在ChannelFuture上添加一个listener,在 IO 操作完成时收到通知。建议尽可能首选 addListener(GenericFutureListener) 而不是 await(),以便在 IO 操作完成时获得通知并执行任何后续任务。 addListener(GenericFutureListener) 是非阻塞的。它只是简单地将指定的 ChannelFutureListener 添加到 ChannelFuture 中,当与 future 相关的 IO 操作完成时,IO 线程会通知监听器。
- await():await() 是一个阻塞操作,一旦被调用,调用者线程就会阻塞,直到操作完成。注意不要在 ChannelHandler 内部调用 await() 方法,因为ChannelHandler 中的事件处理方法通常由 IO 线程调用,如果 await() 被 IO 线程调用的事件处理方法调用,它正在等待的 IO 操作可能永远不会完成,因为 await() 会阻塞它正在等待的 IO 操作,这是一个死锁。
- sync():同步阻塞方法,让调用者线程同步等待,直到NIO线程连接建立后才会执行;
1.3 Future && Promise
在异步处理时经常会用到这两个接口,netty中的Future继承自JDK中的Future,而Promise又对netty自己的Future做了扩展:
- JDK中的Future只能同步等待任务结束(成功或失败)后才能得到结果,而netty中的Future既可以同步等待任务结束得到返回结果,也可以通过其它线程异步等待任务结束后返回结果,但都是要等待任务结束;
- netty中的Promise不仅有Future的功能,还脱离了任务可以独自存在,只作为两个线程间传递结果的容器,可以主动创建,并设置结果和失败异常原因等功能。
Future在多线程已经是比较常用的了,看下promise的代码示例:
@Slf4j
public class TestNettyPromise {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1. 准备EventLoop对象
EventLoop eventLoop = new NioEventLoopGroup().next();
//2. 主动构建一个promise,是一个结果容器;相比Future而言,Future无法主动构建,只能被动获取
DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop);
new Thread(() -> {
//3. 任意一个线程开始执行计算,计算完毕后向promise填充结果
log.info("开始计算");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
promise.setFailure(e);
}
promise.setSuccess(10);
});
//接收结果
log.info("等待结果");
log.info("计算完毕,结果是:{}", promise.get());
}
}
1.4 Handler & Pipeline
ChannelHandler是用于处理channel上各种事件的处理器,分为入站(Inbound)、出站(Outbound)两种。所有的ChannelHandler会组成一条长链,也就是pipeline(管道)。
- 入站handler: 通常是ChannelInboundHandlerAdapter的子类,主要用来读取客户端发送的数据,并写回结果;
- 出站handler: 通常是ChannelOutboundHandlerAdapter的子类,主要用于对写回结果进行加工
通俗来讲,一个Channel就像一个产品的加工车间,而Pipeline是车间中的流水线,ChannelHandler是流水线上的各道工序,ByteBuf就是原材料,经过很多工序的加工最终变成产品(注意:通过pipeline在添加handler时一般都是使用的addLast方法,虽然看上去是在管道尾添加,但其实netty自动在管道头和尾添加了handler,所以我们添加的handler都是在中间)。由于handler在开发中顺序经常需要服务端与客户端一起联调,比较麻烦,所以netty提供了一个工具类EmbeddedChannel,能够模仿channel的入站和出站,示例代码如下:
/**
* channelHandler执行顺序测试工具类
* @create 2022/7/25 2:19 PM
*/
@Slf4j
public class TestEmbeddedChannel {
public static void main(String[] args) {
ChannelInboundHandlerAdapter h1 = new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("inboundChannel-1");
super.channelRead(ctx, msg);
}
};
ChannelInboundHandlerAdapter h2 = new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("inboundChannel-2");
super.channelRead(ctx, msg);
}
};
ChannelOutboundHandlerAdapter h3 = new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.info("outboundChannel-3");
super.write(ctx, msg, promise);
}
};
ChannelOutboundHandlerAdapter h4 = new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.info("outboundChannel-4");
super.write(ctx, msg, promise);
}
};
//构建一个EmbeddedChannel
EmbeddedChannel embeddedChannel = new EmbeddedChannel(h1, h2, h3, h4);
//模拟入站顺序
embeddedChannel.writeInbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello,inbound".getBytes()));
//模拟出站顺序
// embeddedChannel.writeOutbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("world,otbound".getBytes()));
}
}
1.5 ByteBuf
ByteBuf是netty对NIO中的ByteBuffer进行了一个扩展包装,新增了可动态扩容等一系列的优点。
1.5.1 内容打印工具类
/**
* ByteBuf调试工具类
* @create 2022/7/25 2:34 PM
*/
public class ByteBufLogUtil {
public static void log(ByteBuf buf){
int length = buf.readableBytes();
int rows = length / 16 + (length%15 == 0 ? 0:1) +4;
StringBuilder builder = new StringBuilder(rows * 80 * 2)
.append("read index:").append(buf.readerIndex())
.append(" write index:").append(buf.writerIndex())
.append(" capacity:").append(buf.capacity())
.append(NEWLINE);
appendPrettyHexDump(builder, buf);
System.out.println(builder.toString());
}
}
1.5.2 常用创建方式
通常使用ByteBufAllocator.DEFAULT.buffer()来创建获取,但其实ByteBuf区分为池化、非池化、直接内存与堆内存等多种创建方式。
- 池化基于直接内存的ByteBuf:
ByteBuf buf = ByteBufAllocator.DEFAULT.directBuffer();
- 池化基于堆内存的ByteBuf:
ByteBuf buf = ByteBufAllocator.DEFAULT.heapBuffer();
通常直接内存创建和销毁的代价是否昂贵,但由于少一次内存复制,其读写性能也比较高,适合配合池化功能使用;同时直接内存对JVM GC压力小,不受JVM垃圾回收的管理,但是要注意及时主动释放。而堆内存创建和销毁效率高,但其读写性能较差。所以默认情况下,netty是使用的直接内存。
池化:池化的意义在于能够重用ByteBuf,与直接内存联用,不用每次使用都重新创建和销毁,性能较高;并且池化在分配内存时采用了与Jemalloc类似的内存分配算法来提升分配效率,在面对高并发时,能有效节约内存,减小内存溢出的可能。池化功能的开启通过系统环境变量来设置:
-Dio.netty.allocator.type={unpooled|pooled}
在版本4.1以后,非Android 平台默认启用池化实现,Android平台启用非池化实现;4.1之前默认非池化。
1.5.3 ByteBuf组成
ByteBuf由以下部分组成:初始容量、最大容量(默认Integer最大值)、读指针、写指针;
- 废弃区域:读指针之前的区域,也就是已读的区域,可以通过调用 discardReadBytes()去丢弃这部分;
- 可读部分:读指针与写指针之间的区域;
- 可写部分:写指针与初始容量之间的区域;
- 可扩容部分:初始容量与最大容量之间的区域;
常用方法:
1)读写:
方法签名 | 含义 | 备注 |
writeBoolean(boolean value) | 写入boolean值 | 用一字节0|1表示false|true |
writeByte(int value) | 写入byte值 | |
writeShort(int value) | 写入short值 | |
writeInt(int value) | 写入int 值(默认) | Big Endian,即0x250,写入后00 00 02 50 |
writeIntLE(int value) | 写入int 值 | Little Endian,即0x250,写入后50 02 00 00 |
writeBytes(ByteBuf src) | 写入netty中的ByteBuf | |
writeByte(byte[] src) | 写入byte数组 | |
writeBytes(ByteBuffer src) | 写入nio中的ByteBuffer | |
writeCharSequence(CharSequence sequence, Charset charset) | 写入指定字符集的字符串 | |
readByte() | 读取一个字节 | |
readInt() | 读取一个int | |
还有一系列的set/get方法,与ByteBuffer中类似,可以写入值和读取值,但是不会影响readIndex和writeIndex |
在写入数据时若容量不够会自动进行扩容,扩容遵循以下两点规则:
- 如果写入后数据大小未超过512,则选择下一个16的整数倍,例如写入后大小为26,则扩容后的capacity为32;
- 如果写入后数据大小超过512,则选择下一个2^n,例如写入后大小为511,则扩容后的capacity为512;
- 如果扩容后的capacity超过Integer.MAXVALUE,会报错
2)内存释放(retain & release):
netty中一般使用的直接内存ByteBuf,直接内存需要手动来释放,而不是等GC垃圾回收。在Netty中采用了引用计数法来控制内存回收,每个ByteBuf都实现了ReferenceCounted接口:
- 每个ByteBuf对象的初始计数都为1
- 调用release方法计数减1,当计数减到0时,代表此ByteBuf内存被回收
- 调用retain方法计数加1,代表此ByteBuf正在使用中,即使其它handler调用了release也不会回收此ByteBuf
- 当ByteBuf底层内存被回收后,即使ByteBuf对象还在,但该对象的方法已无法正常使用
- 那个Handler最后使用了ByteBuf,则由那个Handler调用release方法;虽然在netty内部的head handler和tair handler调用了release方法,但若该ByteBuf没有传递过去,也会无法释放,因此还是需要手动调用release。
3)slice:
零拷贝的体现之一,对原始ByteBuf进行切片成多个ByteBuf,切片后的ByteBuf并没有发生内存复制,还是使用原始ByteBuf的内存,只是切片后的ByteBuf维护了各自独立的readIndex、writeIndex,修改原始ByteBuf同时也会对切分后的ByteBuf造成影响,例如:释放了原始ByteBuf,切分后的ByteBuf也无法使用,但是可以切片后的ByteBuf调用retain方法,消除此影响;并且切片后的容量是有限制的,不可以再进行写入。
- slice():不传参数默认从readIndex开始切分
- slice(int index, int length):从index开始,切分length长度
4)duplicate:
零拷贝的体现之一,截取了原始ByteBuf所有内容,并且没有max capacity的限制,也是原原始ByteBuf使用同一块底层内存,但是读写指针独立。
5)copy:
与零拷贝相反,会将底层内存数据进行深拷贝,因此无论读写都与原始ByteBuf无关。
6)compositeBuffer:
也是零拷贝的体现之一,创建compositeBuffer生成一个复制buffer,然后调用addComponents方法添加ByteBuf,将添加的ByteBuf组合到新的compositeBuffer中,避免了内存复制,例如:addComponents(true, buf1, buf2),同时也要调用一次retain方法消除同一块内存带来的影响。
7)Unpooled:
Unpooled是一个工具类,提供了非池化的ByteBuf创建、组合和复制操作。其中的wrappedBuffer可以用来包装ByteBuf或者普通字节数组,当wrappedBuffer中的参数个数超过一个时,底层会使用compositeByteBuf方法:wrappedBuffer(buf1, buf2)