目录
一、netty五大组件
1.1 EventLoop
1.2 Channel && ChannelFuture
1.3 Future && Promise
1.4 Handler & Pipeline
1.5 ByteBuf
1.5.1 内容打印工具类
1.5.2 常用创建方式
1.5.3 ByteBuf组成
EventLoop本质上是一个单线程执行器(在内部维护了一个线程和一个Selector),在线程内部的run方法处理Channel上的IO事件。
它继承了三个父类:
代码示例:
@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(){
//连接之后执行
@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);
}
});
}
}
Channel主要方法:
ChannelFuture:继承了JUC下的Future接口,拥有异步返回结果的能力,是异步 Channel IO 操作的结果,Netty 中的所有 IO 操作都是异步的。这意味着任何 IO 调用都会立即返回一个 ChannelFuture 实例,但不能保证请求的 IO 操作在调用结束时已经完成,该实例提供有关 IO 操作的结果或状态的信息。 ChannelFuture 要么未完成,要么已完成。当 IO 操作开始时,会创建一个新的 future 对象。新的 future 最初是未完成的——它既没有成功,也没有失败,也没有取消,因为 IO 操作还没有完成。如果 IO 操作成功、失败或取消完成,则将来会标记为已完成,并提供更具体的信息,例如失败的原因。请注意,即使失败和取消都属于完成状态。
主要方法:
在异步处理时经常会用到这两个接口,netty中的Future继承自JDK中的Future,而Promise又对netty自己的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 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());
}
}
ChannelHandler是用于处理channel上各种事件的处理器,分为入站(Inbound)、出站(Outbound)两种。所有的ChannelHandler会组成一条长链,也就是pipeline(管道)。
通俗来讲,一个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()));
}
}
ByteBuf是netty对NIO中的ByteBuffer进行了一个扩展包装,新增了可动态扩容等一系列的优点。
/**
* 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());
}
}
通常使用ByteBufAllocator.DEFAULT.buffer()来创建获取,但其实ByteBuf区分为池化、非池化、直接内存与堆内存等多种创建方式。
ByteBuf buf = ByteBufAllocator.DEFAULT.directBuffer();
ByteBuf buf = ByteBufAllocator.DEFAULT.heapBuffer();
通常直接内存创建和销毁的代价是否昂贵,但由于少一次内存复制,其读写性能也比较高,适合配合池化功能使用;同时直接内存对JVM GC压力小,不受JVM垃圾回收的管理,但是要注意及时主动释放。而堆内存创建和销毁效率高,但其读写性能较差。所以默认情况下,netty是使用的直接内存。
池化:池化的意义在于能够重用ByteBuf,与直接内存联用,不用每次使用都重新创建和销毁,性能较高;并且池化在分配内存时采用了与Jemalloc类似的内存分配算法来提升分配效率,在面对高并发时,能有效节约内存,减小内存溢出的可能。池化功能的开启通过系统环境变量来设置:
-Dio.netty.allocator.type={unpooled|pooled}
在版本4.1以后,非Android 平台默认启用池化实现,Android平台启用非池化实现;4.1之前默认非池化。
ByteBuf由以下部分组成:初始容量、最大容量(默认Integer最大值)、读指针、写指针;
常用方法:
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 |
在写入数据时若容量不够会自动进行扩容,扩容遵循以下两点规则:
2)内存释放(retain & release):
netty中一般使用的直接内存ByteBuf,直接内存需要手动来释放,而不是等GC垃圾回收。在Netty中采用了引用计数法来控制内存回收,每个ByteBuf都实现了ReferenceCounted接口:
3)slice:
零拷贝的体现之一,对原始ByteBuf进行切片成多个ByteBuf,切片后的ByteBuf并没有发生内存复制,还是使用原始ByteBuf的内存,只是切片后的ByteBuf维护了各自独立的readIndex、writeIndex,修改原始ByteBuf同时也会对切分后的ByteBuf造成影响,例如:释放了原始ByteBuf,切分后的ByteBuf也无法使用,但是可以切片后的ByteBuf调用retain方法,消除此影响;并且切片后的容量是有限制的,不可以再进行写入。
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)