文章目录
1.概述
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
Netty 是一个异步的、基于事件驱动的网络应用框架,用于快速开发可维护、高性能的网络服务器和客户端。
注意:这里的异步不要和异步IO混淆,这里的异步指Netty使用多线程来完成结果传送,让发送请求的线程不阻塞。
优点
Netty vs NIO
- Nio开发工作量大,bug多
- 需要自己构建协议
- netty解决 TCP 传输问题,如粘包、半包
- Nio中在liunx系统中epoll 空轮询导致 CPU 100%
- netty对API进行增强,使之更易用,如 FastThreadLocal=>ThreadLocal,ByteBuf=> ByteBuffer。
Netty vs 其它网络应用框架
- Mina 由apache 维护,将来 3.x 版本可能会有较大重构,破坏API向下兼容性,Netty 的开发选代更速,API 更简洁、文档更优秀
- 久经考验,20年,Netty 版本
2.x 2004
3.x 2008
4.x 2013
5.x 已废弃(没有明显的性能提升,维护成本高)
2.Hello World
maven依赖
<!--netty依赖-->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.51.Final</version>
</dependency>
server端
public class HelloServer {
public static void main(String[] args) {
// 1. 服务器端的启动器,组装netty组件,启动服务
new ServerBootstrap()
// 2. BossEvenLoop,WorkEventLoop 监听事件 一个selector一个thread
.group(new NioEventLoopGroup())
// 3.信道的实现,nio / oio(即bio)
.channel(NioServerSocketChannel.class)
// 4.添加处理器,用于处理消息 ChannelInitializer也相当于一个处理器,不同的是需要它来添加其他处理器
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) throws Exception {
// 5.通过pipeline来添加处理器 stringDecoder将byteBuf解码成string
ch.pipeline().addLast(new StringDecoder());
// 自定义的处理器,监听到读事件后会执行
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg);
}
});
}
})
// 6.绑定端口
.bind(8080);
}
}
client端
public class HelloClient {
public static void main(String[] args) throws InterruptedException {
// 1. 创建客户端启动器
new Bootstrap()
// 2.选择事件轮询组
.group(new NioEventLoopGroup())
// 3.指定信道类型
.channel(NioSocketChannel.class)
// 4.添加处理器
.handler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
// 将消息编码成byteBuf
nioSocketChannel.pipeline().addLast(new StringEncoder());
}
})
// 5.连接
.connect(new InetSocketAddress("localhost", 8080))
// 6.阻塞 等待connect之后再往下执行
.sync()
.channel()// 与服务器建立的信道
// 7.发送消息
.writeAndFlush("hello world");
}
}
控制台
3.组件
3.1 EventLoop
事件循环对象:
EventLoop 本质是一个单线程执行器(同时维护了一个 Selector),里面有 run 方法处理Channel上源源不断的 io 事件。
继承关系
- 继承自ScheduledExecutorService,因此包含了线程池中所有的方法,同时还继承自 netty 自己的OrderedEventExecutor(有顺的)
1、提供了 boolean inEventLoop(Thread thread) 方法判断一个线程是否属于此 EventLoop
2、提供了parent() 方法来看看自己属于哪个 EventLoopGroup
3.1.1 EventLoopGroup
EventLoopGroup 是一组 EventLoop,Channel 一般会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop,后续这个 Channel 上的 io 事件都由此 EventLoop 来处理(保证了 io 事件处理时的线程安全),注意这里是IO对象,其他事件是可以换EventLoop的。
- 继承自 netty 的 EventExecutorGroup
1、实现了 Iterable 接口提供遍历 EventLoop 的能力
2、另有 next 方法获取集合中下一个 EventLoop
3.1.2 处理普通任务
代码
public static void main(String[] args) {
// 支持处理IO事件、普通任务和定时任务。
// 可以指定线程数,如果是无参构造,线程基本取NettyRuntime.availableProcessors() * 2
NioEventLoopGroup loopGroup = new NioEventLoopGroup(2);
// 不支持处理IO事件,支持普通任务和定时任务,处理非IO事件时可以使用
DefaultEventLoopGroup group = new DefaultEventLoopGroup();
// 1.next()获取下一个线程
System.out.println(loopGroup.next());
System.out.println(loopGroup.next());
System.out.println(loopGroup.next());
// 2. 执行普通任务
loopGroup.next().execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("普通任务");
});
log.info("main线程");
// 3. 执行定时任务
loopGroup.next().scheduleAtFixedRate(()->log.info("定时任务"),2,1, TimeUnit.SECONDS);
}
控制台
3.1.3 处理io任务
server代码
@Slf4j
public class EventLoopGroupServer {
public static void main(String[] args) {
new ServerBootstrap().group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf message = (ByteBuf) msg;
log.info(message.toString(Charset.defaultCharset()));
}
});
}
})
.bind(8080);
}
}
client端
控制台
结论:对于同一个channel上的发送请求,是由EventLoop中的指定的线程来执行的,也就是绑定起来了,后续此信道上的io操作都会是这个线程来执行,但是一个线程可以管多个channel。
3.1.4 代码优化
优化一:细化EventLoopGroup,分工更明细
优化二:将耗时的业务处理,迁移到非NioEventLoopGroup处理
代码
public static void main(String[] args) {
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
new ServerBootstrap()
// 优化一:此方法也可以接收两个传参,分工更加明细
// 参数1:boss,负责处理accept事件,不需要设置多个线程,因为对于accept这一种类型的,只会创建一个线程来处理
// 参数2:worker,负责socketChannel上的读写
.group(new NioEventLoopGroup(), new NioEventLoopGroup(4))
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast("handler1", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf message = (ByteBuf) msg;
log.info(message.toString(Charset.defaultCharset()));
// 传递给下一个处理器
ctx.fireChannelRead(msg);
}
// 优化二:对于比较耗时的业务,可以指定其他的eventLoopGroup进行处理,减少同一线程下某一信道慢处理导致其他信道阻塞
}).addLast(defaultEventLoopGroup,"handler2", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf message = (ByteBuf) msg;
log.info(message.toString(Charset.defaultCharset()));
}
});
}
})
.bind(8080);
}
控制台
3.2 Channel
3.2.1 基本用法
-
close() 可以用来关闭Channel
-
closeFuture() 用来处理 Channel 的关闭
1、sync 方法作用是同步等待 Channel关闭
2、而 addListener 方法是异步等待 Channel 关闭 -
pipeline() 方法添加处理器
-
write() 方法将数据写入,不会立刻就发出,而是会写入缓冲区,当缓冲区的数据满了或者调用了flush() 方法的时候就会立刻发出。
-
writeAndFlush() 方法将数据写入并立刻刷出
3.2.2 ChannelFuture-连接
原因:connect(new InetSocketAddress(“localhost”, 8080))方法是异步的,后续操作需要在确保连接建立完成后执行,避免造成影响。
方法一:channelFuture.sync() 同步阻塞,等待连接建立完成后再执行后续操作。
方法二:addListener监听连接,连接建立完成后,由连接线程执行后续操作
方法一代码
public static void main(String[] args) throws InterruptedException {
ChannelFuture channelFuture = new Bootstrap().group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new StringEncoder());
}
})
// 1. 异步非阻塞,执行连接操作的是nio线程,不是main
.connect(new InetSocketAddress("localhost", 8080));
// 2. 此处同步阻塞,避免在连接还没建立完成前业务向下执行
channelFuture.sync();
Channel channel = channelFuture.channel();
channel.writeAndFlush("hello world");
}
方法二代码
// channelFuture.sync();
// 2. 监听连接是否完成
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
Channel channel = channelFuture.channel();
channel.writeAndFlush("hello world");
}
});
3.2.3 ChannelFuture-关闭
同理,close()方法也是异步关闭的,所以我们在channel关闭后执行一些后置处理的话,也分为同步和异步两种处理方式。channelFuture.sync()同步阻塞和addListener异步处理
sync()代码
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
ChannelFuture channelFuture = new Bootstrap().group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new StringEncoder());
}
})
.connect(new InetSocketAddress("localhost", 8080));
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
channelFuture.channel().writeAndFlush("hello world");
}
});
// 方法一:同步阻塞
channelFuture.sync();
Channel channel = channelFuture.channel();
new Thread(() -> {
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
if ("q".equals(line)) {
// close方法也是异步的
channel.close();
// eventloop也需要关闭
group.shutdownGracefully();
break;
}
channel.writeAndFlush(line);
}
}, "input").start();
log.info("close success");
}
控制台
addListener()代码
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
ChannelFuture channelFuture = new Bootstrap().group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new StringEncoder());
}
})
.connect(new InetSocketAddress("localhost", 8080));
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
channelFuture.channel().writeAndFlush("hello world");
}
});
// 方法一:同步阻塞
// channelFuture.sync();
// Channel channel = channelFuture.channel();
// new Thread(() -> {
// Scanner scanner = new Scanner(System.in);
// while (true) {
// String line = scanner.nextLine();
// if ("q".equals(line)) {
// // close方法也是异步的
// channel.close();
// // eventloop也需要关闭
// group.shutdownGracefully();
// break;
// }
// channel.writeAndFlush(line);
// }
// }, "input").start();
// 方法二:异步监听
ChannelFuture closeFuture = channelFuture.channel().close();
closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
log.info("addListener close success");
group.shutdownGracefully();
}
});
log.info("close success");
}
控制台
3.3 Future & Promise
在异步调用的时候,经常会用到这两个接口。首先要说明 netty 中的 Future 接口和 jdk 中的 Future 接口同名,但是是两个接口,netty中的Future接口继承自JDK中的Future,而Promise 又对 netty Future 进行了扩展。
- jdk Future 只能同步等待任务结束(或成功、或失败)才能得到结果
- netty Future 可以同步等待任务结束得到结果,也可以异步方式得到结果,但都是要等任务结束
- netty Promise 不仅有 netty Future 的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器
功能/名称 | jdk Future | netty Future | Promise |
---|---|---|---|
cancel | 取消任务 | - | - |
isCanceled | 任务是否取消 | - | - |
isDone | 任务是否完成,不能区分成功失败 | - | - |
get | 获取任务结果,阻塞等待 | - | - |
getNow | - | 获取任务结果,非阻塞,还未产生结果时返回null | - |
await | - | 等待任务结束,如果任务失败,不会抛异常,而是通过 isSuccess 判断 | - |
sync | - | 等待任务结束,如果任务失败,抛出异常 | - |
isSuccess | - | 判断任务是否成功 | - |
cause | - | 获取失败信息,非阻塞,如果没有失败,返回null | - |
addLinstener | - | 添加回调,异步接收结果 | - |
setSuccess | - | - | 设置成功结果 |
setFailure | - | - | 设置失败结果 |
3.3.1 jdk Future
代码
@Slf4j
public class JdkFutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1.创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 2.提交线程任务,使用callable,可以返回一个future对象,用于获取结果
Future<Integer> future = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.info("执行任务");
return 50;
}
});
log.info("success");
log.info("获取执行结果:{}",future.get());
}
}
控制台
3.3.2 netty Future
代码
@Slf4j
public class NettyFutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
EventLoop eventLoop = eventLoopGroup.next();
Future<Integer> future = eventLoop.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.info("执行任务");
return 50;
}
});
log.info("success");
// log.info("获取执行结果:{}",future.get());
// 相比jdk future,netty中的支持异步回调结果
future.addListener(new GenericFutureListener<Future<? super Integer>>() {
@Override
public void operationComplete(Future<? super Integer> future) throws Exception {
log.info("接收结果:{}",future.getNow());
}
});
log.info("结果success");
}
}
控制台
3.3.3 netty Promise
相比于前两者,promise对象是我们主动创建的,而且我们可以根据线程中执行的结果来返回success或者异常fail。
@Slf4j
public class PromiseTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
EventLoop eventLoop = eventLoopGroup.next();
// DefaultPromise<Object> defaultPromise = new DefaultPromise<>(eventLoop);
Promise<Integer> promise = eventLoop.newPromise();
new Thread(() -> {
log.info("执行业务");
try {
int a = 1 / 0;
Thread.sleep(1000);
promise.setSuccess(50);
} catch (Exception e) {
promise.setFailure(e);
}
}).start();
log.info("等待结果...");
log.info("结果{}", promise.get());
}
}
控制台
3.4 Handler & Pipeline
ChannelHandler 用来处理 Channel 上的各种事件,分为入站,出站两种,所有ChannelHandler 被连成一串,就是Pipeline。
- 入站处理器通常是 ChannelInboundHandlerAdapter的子类,主要用来读取客户端数据,写回数据
- 出站处理器通常是 ChannelOutboundHandlerAdapter 的子类,主要用于对写回结果进行加工
3.4.1 InboundHandler 和 OutboundHandler
- netty自身会在pipeline上初始两个处理器,分别是head和tail,我们自定义的处理器都是这两者之间的,类似双向链表。比如,我们用addLast方法依次添加三个入站处理器和三个出站处理器,此时类似于head -> h1 -> h2 -> h3 -> h4 -> h5 -> h6 ->tail。
- 读入的数据,只在入站处理器中处理,即数据流向 h1 -> h2 -> h3。
- 写出的数据,只在出站处理器中处理,即数据流向 h6 -> h5 -> h4。
- 调用 ctx.channel().write(msg) 的时候会从tail往前找出站处理器来处理。调用 ctx.write(msg) 会从当前处理器往前找出站处理器来处理
代码
下面代码中,在h2和h3中我们都调用了ctx.channel().writeAndFlush(“aaa”),只要我们在处理中写出数据,这时数据就会流转到出站处理器中,出站处理器都执行完后,再向下一级入站处理器中处理读入的数据。所以流程是h1->h2(写出数据)->h6->h5->h4->h3(写出数据)->h6->h5->h4
@Slf4j
public class HandlerTest {
public static void main(String[] args) {
new ServerBootstrap().group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast("h1", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("h1");
super.channelRead(ctx, msg);
}
}).addLast("h2", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("h2");
ctx.channel().writeAndFlush("aaa");
super.channelRead(ctx, msg);
}
}).addLast("h3", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("h3");
ctx.channel().writeAndFlush("bbb");
super.channelRead(ctx, msg);
}
}).addLast("h4", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
throws Exception {
log.info("h4");
super.write(ctx, msg, promise);
}
}).addLast("h5", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
throws Exception {
log.info("h5");
super.write(ctx, msg, promise);
}
}).addLast("h6", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
throws Exception {
log.info("h6");
super.write(ctx, msg, promise);
}
});
}
})
.bind(8080);
}
}
控制台
3.4.2 EmbeddedChannel调试工具类
@Slf4j
public class EmbeddedTest {
public static void main(String[] args) {
ChannelInboundHandlerAdapter h1 = new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("1");
super.channelRead(ctx, msg);
}
};
ChannelInboundHandlerAdapter h2 = new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("2");
super.channelRead(ctx, msg);
}
};
ChannelOutboundHandlerAdapter h3 = new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.info("3");
super.write(ctx, msg, promise);
}
};
ChannelOutboundHandlerAdapter h4 = new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.info("4");
super.write(ctx, msg, promise);
}
};
EmbeddedChannel channel = new EmbeddedChannel(h1, h2, h3, h4);
// 模拟入站
channel.writeInbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello".getBytes()));
// 模拟出站
channel.writeOutbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("world".getBytes()));
}
}
控制台
3.5 ByteBuf
3.5.1 创建–直接内存 VS 堆内存
- 初始容量,默认256字节。我们可以指定初始容量,如果写入字节超过容量,则自动扩容。
代码
public class ByteBufCreateTest {
public static void main(String[] args) {
// 不指定初始容量,默认256字节
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
log(buffer);
// 指定初始容量,如果写入字节超过容量,则自动扩容
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(16);
log(buf);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 17; i++) {
sb.append("a");
}
buf.writeBytes(sb.toString().getBytes());
log(buf);
}
/**
* 打印日志工具,方便查看
*
* @param buffer
*/
private static void log(ByteBuf buffer) {
int length = buffer.readableBytes();
int rows = length / 16 + (length % 15 == 0 ? 0 : 1) + 4;
StringBuilder buf = new StringBuilder(rows * 80 * 2).append("read index:")
.append(buffer.readerIndex())
.append(" write index:")
.append(buffer.writerIndex())
.append(" capacity:")
.append(buffer.capacity())
.append(NEWLINE);
appendPrettyHexDump(buf, buffer);
System.out.println(buf.toString());
}
}
控制台
- 默认创建的ByteBuf是使用直接内存的,直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用。直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放。
- 我们可以用ByteBufAllocator.DEFAULT.heapBuffer()指定使用堆内存
@Slf4j
public class ByteBufTest {
public static void main(String[] args) {
// 使用直接内存
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
log.info("{}",buffer);
// 使用直接内存
ByteBuf buffer2 = ByteBufAllocator.DEFAULT.directBuffer();
log.info("{}",buffer2);
// 使用堆内存
ByteBuf buffer3 = ByteBufAllocator.DEFAULT.heapBuffer();
log.info("{}",buffer3);
}
}
控制台
3.5.2 创建–池化 VS 非池化
ByteBuf 中支持池化的管理,池化的最大意义在于可以重用 ByteBuf,对于那些创建比较慢的资源我们可以用池的资源进行优化,类似于线程池或者连接池。
- 我们预先创建好多个 ByteBuf 实例, 在创建的时候就直接调用就行,这样的好处就是节省内存,因为 Java 中创建 ByteBuf 默认是直接内存,但是要知道直接内存是存在于操作系统中的,创建的代价是很高的,就算是堆内存,不断地创建 ByteBuf 实例也只会增添 GC 回收对象的压力。
- 有了池化,则可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率
- 高并发的时候采用池化技术更能节省内存,减少内存溢出的风险。
我们可以通过以下方式来决定是否开启池化功能:在 VM 参数中设置
-Dio.netty.allocator.type={unpooled|pooled}
我们指定VM参数后,执行上面的ByteBufTest,控制台输出
3.5.3 组成
- 四个元素,读指针、写指针、容量、最大容量(int的最大值)
- 四个部分,废弃部分(已经读取了的部分)、可读部分(未读的部分)、可写部分(写指针与容量之间的部分)、可扩容部分
3.5.4 写入
方法签名 | 含义 | 备注 |
---|---|---|
writeBoolean(boolean value) | 写入 boolean 值 | 用一字节 01\00 代表 true\false |
writeByte(int value) | 写入 byte 值 | 用一字节 01\00 代表 true\false |
writeShort(int value) | 写入 short 值 | 用一字节 01\00 代表 true\false |
writeInt(int value) | 写入 int 值 | Big Endian(大端写入),即 0x250,写入后 00 00 02 50 ,低位靠后 |
writeIntLE(int value) | 写入 int 值 | Little Endian(低位写入),即 0x250,写入后 50 02 00 00 ,高位靠后 |
writeLong(long value) | 写入 long 值 | |
writeChar(int value) | 写入 char 值 | |
writeFloat(float value) | 写入 float 值 | |
writeDouble(double value) | 写入 double 值 | |
writeBytes(ByteBuf src) | 写入 netty 的 ByteBuf | |
writeBytes(byte[] src) | 写入 byte[] | |
writeBytes(ByteBuffer src)) | 写入 nio 的 ByteBuffer | |
int writeCharSequence(CharSequence sequence, Charset charset) | 写入字符串 |
public static void main(String[] args) {
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
buffer.writeBytes(new byte[]{1,2,3,4});
log(buffer);
buffer.writeInt(5);
log(buffer);
buffer.writeIntLE(5);
log(buffer);
}
控制台
- 有一类方法是 set 开头的一系列方法,也可以写入数据,但不会改变写指针位置。类似于redis中bitmap的使用。
3.5.5 扩容
扩容机制需要涉及到源码解析,这里不做研究。
public static void main(String[] args) {
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(6);
log(buffer);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 7; i++) {
sb.append("a");
}
buffer.writeBytes(sb.toString().getBytes());
log(buffer);
StringBuilder sb2 = new StringBuilder();
for (int i = 6; i < 17; i++) {
sb2.append("a");
}
buffer.writeBytes(sb2.toString().getBytes());
log(buffer);
StringBuilder sb3 = new StringBuilder();
for (int i = 16; i < 65; i++) {
sb3.append("a");
}
buffer.writeBytes(sb3.toString().getBytes());
log(buffer);
StringBuilder sb4 = new StringBuilder();
for (int i = 65; i < 257; i++) {
sb4.append("a");
}
buffer.writeBytes(sb4.toString().getBytes());
log(buffer);
}
控制台
3.5.6 读取
在我们读取的过程中,读指针会一直向后移动,读过的部分变成了废弃部分。
public static void main(String[] args) {
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
buffer.writeBytes(new byte[]{1,2,3,4});
buffer.writeInt(5);
log(buffer);
System.out.println(buffer.readByte()); // 1
System.out.println(buffer.readByte()); // 2
System.out.println(buffer.readByte()); // 3
System.out.println(buffer.readByte()); // 4
log(buffer);
}
如果想要重复读取的话,可以像Nio里面的ByteBuffer那样设置一个标记位。
// 标记读指针位置
buffer.markReaderIndex();
System.out.println(buffer.readInt());
log(buffer);
// 重置读指针位置
buffer.resetReaderIndex();
log(buffer);
- 采用 get 开头的一系列方法,这些方法不会改变 read index,因为这类方法通常都是按照索引去获取的,所以不会改变读指针的位置
3.5.7 retain 和 release
- UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存即可
- UnpooledDirectByteBuf 使用的就是直接内存了,需要我们主动调用特殊的方法来回收内存,因为等到垃圾回收机制去主动回收的时候其实是不及时的
- PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存
对于回收实现 Netty 采用了引用计数法来控制回收内存,主要是每个ByteBuf 都实现了一个通用的接口 ReferenceCounted
- 每个 ByteBuf 对象的初始计数为 1,表示有人在使用,不能回收
调用 release 方法计数减 1,如果计数为 0,证明没有人用了,ByteBuf 内存被回收 - 调用 retain 方法计数加 1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收
- 当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用
- 在 pineline中的双向存储结构中,head 和 tail的作用之一就是来释放掉入站或者出站的byteBuf。但是前提是,这个byteBuf传到head或者tail中,如果在中间的某一个hangler中执行完就不传递了,就需要我们手动调用release方法释放。 具体源码可以查看HeadContext和tailContext;
3.5.8 slice 和 Composite
【零拷贝】 的体现之一,对原始 ByteBuf 进行切片成多个 ByteBuf,切片后的 ByteBuf 并没有发生内存复制,还是使用原始 ByteBuf 的内存,切片后的 ByteBuf 维护独立的 read,write 指针 。
public class SliceTest {
public static void main(String[] args) {
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10);
buffer.writeBytes(new byte[]{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'});
log(buffer);
// 进行切片,底层是同一块空间
ByteBuf slice1 = buffer.slice(0, 5);
ByteBuf slice2 = buffer.slice(5, 5);
log(slice1);
log(slice2);
// 可以用set方法改变某字节的值,但是不能变更长度,比如写入新的字节,因为有其他对象引用
slice1.setByte(0,'b');
log(buffer);
}
private static void log(ByteBuf buffer) {
int length = buffer.readableBytes();
int rows = length / 16 + (length % 15 == 0 ? 0 : 1) + 4;
StringBuilder buf = new StringBuilder(rows * 80 * 2).append("read index:")
.append(buffer.readerIndex())
.append(" write index:")
.append(buffer.writerIndex())
.append(" capacity:")
.append(buffer.capacity())
.append(NEWLINE);
appendPrettyHexDump(buf, buffer);
System.out.println(buf.toString());
}
}
当然,可以将多个 ByteBuf 合并为一个ByteBuf,这样也避免了一次拷贝。
public static void main(String[] args) {
ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5);
buf1.writeBytes(new byte[]{1, 2, 3, 4, 5});
ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(5);
buf2.writeBytes(new byte[]{6, 7, 8, 9, 10});
// writeBytes是基于数据复制的,这里会发生两次数据复制,将buf1和buf2复制到buffer中
// ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
// ByteBuf byteBuf = buffer.writeBytes(buf1).writeBytes(buf2);
// 组合,避免复制。但是重新组合后,需要修改读写指针,第一个传参为true
CompositeByteBuf byteBufs = ByteBufAllocator.DEFAULT.compositeBuffer();
byteBufs.addComponents(true, buf1, buf2);
}