06 netty的基础组件介绍
1 数据通道:Channel
概述:在Netty中,所有数据的都是从channel中获得的,可以将其看作“数据流通的通道”
channel 的常用方法
- close() 可以用来关闭 channel
- closeFuture() 用来处理 channel 的关闭
- sync 方法作用是同步等待 channel 关闭
- 而 addListener 方法是异步等待 channel 关闭
- pipeline() 方法添加处理器
- write() 方法将数据写入
- writeAndFlush() 方法将数据写入并刷出
1-1 Channel和ChannelFuture对象关系
package netty_basic.otherNettyClassIntroduction;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
/*Netty中Channel与ChannelFuture的关系展示*/
public class EventLoopClient {
public static void main(String[] args) throws InterruptedException {
ChannelFuture channelFuture = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
// 添加客户端通道的处理程序,对String进行编码,这里逻辑与服务端程序要进行对照
nioSocketChannel.pipeline().addLast(new StringEncoder(StandardCharsets.UTF_8));
}
})
.connect(new InetSocketAddress("localhost",8080));
// connect是异步非阻塞方法,本线程发起调用后由NIO线程进行具体调用
// sync是阻塞方法,只有与服务端建立连接后才会往下执行,这里本质上是等待上面connnet的完成
channelFuture.sync();
/*
如果之前没有调用sync,则chanenlFutue所转化来的chanenl是无效的,因为还没有建立与remote host的连接,
这种编程失误在实际中需要注意
*/
Channel channel = channelFuture.channel(); // ChannelFute对象转换为channel对象
channel.writeAndFlush("hello world");
}
}
- 代码中可看出客户端的ChannelFuture是一个表示中间状态的类,有效的Channel对象必须已经与remote host建立连接。
小知识: 带有Future和Promise的类通常与异步方法调用配合使用
实例:channelFuture通过异步非阻塞方法connect与远程服务器建立连接,但需要保证连接建立后才能变为有效的channel。本质上是网络连接状态的同步。
1-2 channelFuture处理结果
package netty_basic.channelFuture;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
/*Netty中Channel与ChannelFuture的关系展示*/
public class EventLoopClient {
public static void main(String[] args) throws InterruptedException {
ChannelFuture channelFuture = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new StringEncoder(StandardCharsets.UTF_8));
}
})
.connect(new InetSocketAddress("localhost",8080));
// connect是异步非阻塞方法,本线程发起调用后由NIO线程远程建立服务器连接并返回结果给发起调用线程
/*
1)使用sync同步处理结果(阻塞调用线程,直到结果返回)
sync是阻塞方法,只有与服务端建立连接后才会往下执行
(连接建立是通过另外一个NIO线程完成,该线程需要与另外一个线程同步
*/
// channelFuture.sync();
// Channel channel = channelFuture.channel(); // ChannelFute对象转换为channel对象
// channel.writeAndFlush("同步调用,等待建立channel");
/*
2) 使用addListener方法异步处理结果
发起调用线程无需等待连接的建立,发起调用后将回调对象作为参数,连接建立后,在调用回调对象。
*/
channelFuture.addListener(new ChannelFutureListener() {
@Override
// nio线程在连接建立后会自己调用operationComplete方法
public void operationComplete(ChannelFuture future) throws Exception {
Channel channel = future.channel();
channel.writeAndFlush("异步调用,回调建立channel");
}
});
}
}
连接建立的处理总结:
客户端线程向远程服务器发起请求,需要建立连接。实际场景中,连接的建立完成时间并不确定。
因此主线程采用异步方法connect请求建立连接,调用connect后,会有另外的NIO线程去完成连接建立的工作。
主线程对于nio线程的处理结果可以采用两种方式进行处理:
- 同步调用:主线程调用sync方法阻塞后并等待NIO线程的调用结果,当有结果返回是在继续执行,此时channelFuture转化为channel的操作是通过主线程完成的
- 异步调用:主线程提供回调函数给NIO线程,NIO线程连接建立后,调用回到函数处理结果,即完成channelFuture转化为channel的操作
1-3 channelFuture关闭连接
需求:当满足某个条件时,客户端需要关闭远程服务器的channel,并在关闭后进行一些操作,由于关闭操作是异步操作,我们需要确保部分代码确实是在连接关闭后进行的
-netty通过channelFuture对象来满足上述需求
使用实例
package netty_basic.channelFuture;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
@Slf4j
public class CloseFutureClient {
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>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
nioSocketChannel.pipeline().addLast(new StringEncoder(StandardCharsets.UTF_8));
}
})
.connect(new InetSocketAddress("localhost",8080));
channelFuture.sync();
// 线程关闭channel
Channel ch = channelFuture.channel();
Thread t = new Thread(new closeChannel(ch,group),"input");
t.start();
}
}
/*
线程接受控制台输入并发送给服务端,当控制台输入q,关闭与服务端的连接
*/
@Slf4j
class closeChannel implements Runnable{
private Channel ch;
private NioEventLoopGroup gp;
closeChannel(Channel ch,NioEventLoopGroup gp){
this.ch = ch;
this.gp = gp;
}
@SneakyThrows
@Override
public void run() {
Scanner sc = new Scanner(System.in);
while(true) {
String line = sc.nextLine();
if (line.equals("q")) {
ch.close();
break;
}
ch.writeAndFlush(line);
}
/*
closeFuture进行连接关闭后的处理,处理方式也是两种:
1) 同步等待连接关闭,然后执行处理代码
2) 异步调用,由其他线程完成channel的关闭后执行处理代码
*/
ChannelFuture closeFuture = ch.closeFuture();
// 方式1:同步处理
// closeFuture.sync();
// log.debug("执行关闭chanenl后的代码");
// 方式2:异步处理
closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
log.debug("执行关闭channel后的代码");
// 关闭channel再将事件循环组关系从而实现整个java程序的结束
gp.shutdownGracefully();
}
});
}
}
- 上述代码启动一个线程监控控制台输入,当输入为“q"时,关闭channel连接
为了确保channel已经关闭,类似于channel的建立,可以借助chanenlFuture对象感知关闭的结果,该类同样提供了同步和异步两种方式:
同步等待关闭: closeFuture.sync();
异步调用关闭:
closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
log.debug("执行关闭channel后的代码");
// 关闭channel再将事件循环组关系从而实现整个java程序的结束
gp.shutdownGracefully();
}
});
=异步建立和关闭channel的意义==========
netty采用异步操作的目的是为了提升单位时间连接处理的个数,客户端与服务器整个连接的生命周期包含连接建立、数据传输、连接关闭这些基本阶段。异步实现了每个线程只负责一个阶段。由于多核CPU能够支持线程并发执行,因此能够形成流水线提高连接的吞吐率。
2 多线程间结果的传递:Future和Promise
| 接口名称 | 作用 |
|---|---|
| JUC中的Future | 同步等待任务结束(成功或失败)获取执行结果 |
| Netty中Future | 继承juc的Future,可以采用异步的方式等待任务结束获取执行结果 |
| Netty的Promise | 进一步增强Netty中Future,能够单独定义,作为两个线程之间传递结果的容器 |
注意:Future接口通常配合线程池使用,netty中的线程池通常指的是eventLoop,这个线程池是单线程线程池。
2-1 juc的Future接口实现
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
package netty_basic.channelFuture;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;
@Slf4j
public class TestJucFuture {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(2);
Future<Integer> future = service.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("执行计算");
Thread.sleep(1000);
return 50;
}
});
log.debug("等待线程池中线程执行结果");
log.debug("结果{}",future.get());
}
}
执行结果
09:12:39 [DEBUG] [main] n.c.TestJucFuture - 等待线程池中线程执行结果
09:12:39 [DEBUG] [pool-1-thread-1] n.c.TestJucFuture - 执行计算
09:12:40 [DEBUG] [main] n.c.TestJucFuture - 结果50
2-2 netty的Future接口实现
public interface Future<V> extends java.util.concurrent.Future<V> {
boolean isSuccess();
boolean isCancellable();
Throwable cause();
Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener);
Future<V> addListeners(GenericFutureListener<? extends Future<? super V>>... listeners);
Future<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener);
Future<V> removeListeners(GenericFutureListener<? extends Future<? super V>>... listeners);
Future<V> sync() throws InterruptedException;
Future<V> syncUninterruptibly();
Future<V> await() throws InterruptedException;
Future<V> awaitUninterruptibly();
boolean await(long timeout, TimeUnit unit) throws InterruptedException;
boolean await(long timeoutMillis) throws InterruptedException;
boolean awaitUninterruptibly(long timeout, TimeUnit unit);
boolean awaitUninterruptibly(long timeoutMillis);
V getNow();
@Override
boolean cancel(boolean mayInterruptIfRunning);
}
package netty_basic.channelFuture;
import io.netty.channel.EventLoop;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
@Slf4j
public class TestNettyFuture {
// netty中eventLoopGroup中可以包含多个eventLoop,每个evenLoop都是单线程线程池
public static void main(String[] args) throws ExecutionException, InterruptedException {
NioEventLoopGroup g = new NioEventLoopGroup();
EventLoop el = g.next();
Future<Integer> f = el.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("线程计算结果");
return 5;
}
});
// get主线程阻塞同步获取结果
/*
log.debug("等待线程池中线程执行结果");
log.debug("结果{}",f.get());
*/
// 通过回调函数异步执行
f.addListener(new GenericFutureListener<Future<? super Integer>>() {
@Override
public void operationComplete(Future<? super Integer> future) throws Exception {
log.debug("等待线程池中线程执行结果");
log.debug("结果{}",f.getNow());
}
});
}
}
执行结果
09:14:52 [DEBUG] [nioEventLoopGroup-2-1] n.c.TestNettyFuture - 线程计算结果
09:14:52 [DEBUG] [nioEventLoopGroup-2-1] n.c.TestNettyFuture - 等待线程池中线程执行结果
09:14:52 [DEBUG] [nioEventLoopGroup-2-1] n.c.TestNettyFuture - 结果5
2-3 Promise接口实现
public interface Promise<V> extends Future<V> {
Promise<V> setSuccess(V result); // 设置成功并通知所有listeners
boolean trySuccess(V result); // mark成功并通知所有的listeners
Promise<V> setFailure(Throwable cause); // 设置失败并通知所有的listeners
boolean tryFailure(Throwable cause); // mark失败并通知所有的listeners
boolean setUncancellable();
@Override
Promise<V> addListener(GenericFutureListener<? extends Future<? super V>> listener);
@Override
Promise<V> addListeners(GenericFutureListener<? extends Future<? super V>>... listeners);
@Override
Promise<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener);
@Override
Promise<V> removeListeners(GenericFutureListener<? extends Future<? super V>>... listeners);
@Override
Promise<V> await() throws InterruptedException;
@Override
Promise<V> awaitUninterruptibly();
@Override
Promise<V> sync() throws InterruptedException;
@Override
Promise<V> syncUninterruptibly();
}
package netty_basic.channelFuture;
import io.netty.channel.DefaultEventLoop;
import io.netty.channel.EventLoop;
import io.netty.util.concurrent.DefaultPromise;
import io.netty.util.concurrent.Promise;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutionException;
@Slf4j
public class TestNettyPromise {
public static void main(String[] args) throws ExecutionException, InterruptedException {
EventLoop el = new DefaultEventLoop();
Promise<Integer> promise = new DefaultPromise<>(el);
new Thread(()->{
log.debug("开始计算");
try {
Thread.sleep(1000);
promise.setSuccess(100);
} catch (InterruptedException e) {
promise.setFailure(e);
e.printStackTrace();
}
}).start();
log.debug("等待执行结果");
log.debug("执行结果{}",promise.get());
}
}
执行结果
09:25:59 [DEBUG] [main] n.c.TestNettyPromise - 等待执行结果
09:25:59 [DEBUG] [Thread-0] n.c.TestNettyPromise - 开始计算
09:26:00 [DEBUG] [main] n.c.TestNettyPromise - 执行结果100
Future与Promise接口总结============
两个接口的用于进程间结果的传递,netty中的Future接口相比JUC中的Future接口增加了通过回调函数异步调用的方式。但Future接口在编码上本质还是依赖线程submit的返回值。而Promise能够单独定义,在实现Future接口的同时更加的灵活。
3 出入网络数据的处理:handler & pipeline
3-1 channelHander的入站与出站的区分
handler作用:实际网络编程,业务代码通常在handler中实现,入站信息与出站信息的处理都是在handler中进行,多个handler可以形成pipeline
nettty提供的handler
| 名称 | 作用 |
|---|---|
| ChannelHandler | 处理channel上的事件,分为入站handler和出站handler两种 |
- 入站处理器通常是 ChannelInboundHandlerAdapter 的子类,主要用来读取客户端数据,写回结果
- 出站处理器通常是 ChannelOutboundHandlerAdapter 的子类,主要对写回结果进行加工
handler使用实例:
package netty_basic.handler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
/*
ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter的区别
当有外部数据入站时,触发入站handler,handler的触发顺序从双向链表头部开始,即与handler添加顺序一直
当有外部数据出站时,触发出站handler,handler的触发顺序从双向链表的尾部开始,即与handler添加的顺序相反
区别:触发的时机不同,触发顺序分别从pipeline的头部和尾部开始
*/
@Slf4j
public class TestPipeline {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup(),new NioEventLoopGroup(2))
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("h1",new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("1");
// 这个方法是将msg交给下一个handler处理,底层是ctx.fireChannelRead
ByteBuf buf = (ByteBuf) msg;
String s = "h1"+buf.toString(StandardCharsets.UTF_8);
super.channelRead(ctx,s);
// ctx.fireChannelRead(msg); // 与super.channelRead等价
}
});
pipeline.addLast("h2",new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("2");
String cur = "h2"+ msg;
log.debug(cur);
ch.writeAndFlush(ctx.alloc().buffer().writeBytes("servers..".getBytes()));
}
});
pipeline.addLast("h3",new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("3 "+msg);
super.write(ctx, msg, promise);
}
});
pipeline.addLast("h4",new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("4 "+msg);
super.write(ctx, msg, promise);
}
});
}
})
.bind(8080);
}
}
上述代码中按顺序添加了2个入站handler和2个出站handler,第2个入站handler中向channel中写入信息,从而触发出站handler,通过客户端发送两次信息,分别是client1:hello1和client:hello后程序的运行结果:
- 可以发现入站的信息是从头部handler(1,2)开始处理,出站的信息是从尾部handler(4,3)开始处理
16:52:21 [DEBUG] [nioEventLoopGroup-3-1] n.h.TestPipeline - 1
16:52:21 [DEBUG] [nioEventLoopGroup-3-1] n.h.TestPipeline - 2
16:52:21 [DEBUG] [nioEventLoopGroup-3-1] n.h.TestPipeline - h2h1client1:hello1
16:52:21 [DEBUG] [nioEventLoopGroup-3-1] n.h.TestPipeline - 4 PooledUnsafeDirectByteBuf(ridx: 0, widx: 9, cap: 256)
16:52:21 [DEBUG] [nioEventLoopGroup-3-1] n.h.TestPipeline - 3 PooledUnsafeDirectByteBuf(ridx: 0, widx: 9, cap: 256)
16:52:25 [DEBUG] [nioEventLoopGroup-3-1] n.h.TestPipeline - 1
16:52:25 [DEBUG] [nioEventLoopGroup-3-1] n.h.TestPipeline - 2
16:52:25 [DEBUG] [nioEventLoopGroup-3-1] n.h.TestPipeline - h2h1client2:hello
16:52:25 [DEBUG] [nioEventLoopGroup-3-1] n.h.TestPipeline - 4 PooledUnsafeDirectByteBuf(ridx: 0, widx: 9, cap: 256)
16:52:25 [DEBUG] [nioEventLoopGroup-3-1] n.h.TestPipeline - 3 PooledUnsafeDirectByteBuf(ridx: 0, widx: 9, cap: 256)

=====channelHander和pipeLine总结=
服务端每个channel可以看成是双向链表,入站信息处理是正向调用入站handler,出站信息处理是逆向调用出站handler
channel的设计思想:体现了责任链模式(Chain of Responsibility Pattern)这种行为型设计模式,它为请求创建了一个处理对象的链(pipeline)。其链中每一个节点都看作是一个对象(handler),每个节点处理的请求均不同,且内部自动维护一个下一节点对象。当一个请求从链式的首端发出时,会沿着链的路径依次传递给每一个节点对象,直至有对象处理这个请求为止。
注意点:调用对象不同对于出站handler使用的影响
ctx.channel().write(msg) // 从尾部开始查找出站处理器(整个pipeline的尾部)
ctx.write(msg) // ctx:ChannelHandlerContext
// 从当前节点(pipeline中在当前handler前面的handler)找上一个出站处理器
- 上述两个方法虽然都是向channel中写入数据,但是检索出站处理器的范围不同,实际开发中需要注意。
3-2 用于测试的channel
package netty_basic.handler;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.channel.embedded.EmbeddedChannel;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
/*
* 需求:当网络通信涉及比较复杂的业务需求时(多个handler的实现),每次测试都需要同时运行服务端程序和客户端程序,这样显然是比较麻烦的。
* EmbededChannel可以避免上述的问题,
* */
@Slf4j
public class TestEmbededChannel {
public static void main(String[] args) {
ChannelInboundHandlerAdapter h1 = new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("1");
super.channelRead(ctx, msg);
}
};
ChannelInboundHandlerAdapter h2 = new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("2");
super.channelRead(ctx, msg);
}
};
ChannelOutboundHandlerAdapter h3 = new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("3");
super.write(ctx, msg, promise);
}
};
ChannelOutboundHandlerAdapter h4 = new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("4");
super.write(ctx, msg, promise);
}
};
EmbeddedChannel channel = new EmbeddedChannel(h1,h2,h3,h4);
// 模拟入站操作
channel.writeInbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("INPUT".getBytes()));
// 模拟出站操作
channel.writeOutbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("OUT".getBytes()));
}
}
执行结果
22:13:04 [DEBUG] [main] n.h.TestEmbededChannel - 1
22:13:04 [DEBUG] [main] n.h.TestEmbededChannel - 2
22:13:04 [DEBUG] [main] n.h.TestEmbededChannel - 4
22:13:04 [DEBUG] [main] n.h.TestEmbededChannel - 3
总结:通过netty提供的EmbeddedChannel可以较为方便的测试复杂的pipelin是否能够有效工作,避免每次代码测试都需要启动客户端与服务端程序。
4 数据的缓冲:bytebuf
4-1 bytebuf的动态扩容
概述:java原生的NIO相关的API中用于数据缓冲的是ByteBuffer,netty中与之对应的就是ByteBuf,ByteBuf能够随着输入数据的大小进行动态的扩容。
ByteBuf的优点
- 池化 - 可以重用池中 ByteBuf 实例,更节约内存,减少内存溢出的可能
- 读写指针分离,不需要像 ByteBuffer 一样切换读写模式
- 可以自动扩容
- 支持链式调用,使用更流畅
- 很多地方体现零拷贝,例如 slice、duplicate、CompositeByteBuf
package netty_basic.bytebuf;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import static io.netty.buffer.ByteBufUtil.appendPrettyHexDump;
import static io.netty.util.internal.StringUtil.NEWLINE;
public class TestBytebuf {
public static void main(String[] args) {
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
log(buf);
StringBuilder sb = new StringBuilder();
for(int i = 0;i < 300;++i) sb.append("a");
buf.writeBytes(sb.toString().getBytes());
log(buf);
}
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());
}
}
执行结果
read index:0 write index:0 capacity:256 // 读指针位置,写指针位置,容量
read index:0 write index:300 capacity:512
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|00000010| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|00000020| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|00000030| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|00000040| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|00000050| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|00000060| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|00000070| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|00000080| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|00000090| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|000000a0| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|000000b0| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|000000c0| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|000000d0| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|000000e0| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|000000f0| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|00000100| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|00000110| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|00000120| 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaa |
+--------+-------------------------------------------------+----------------+
ByteBuf的容量由256扩充到了512.
4-2 bytebuf的内存类型和池化
特点:bytebuf在内存使用上,支持堆内存和直接内存,并且支持内存池化。
直接内存:
- 分配与销毁代价大,但是读写效率高,NIO的缓冲区通常默认使用直接内存。
- 不受垃圾回收机制影响
池化机制:池本质上是资源池,实现资源的重复利用,减少重复创建和销毁资源的开销。
池化思想的实例:线程池,内存池,连接池,对象池
a pool is a collection of resources that are kept ready to use, rather than acquired on use and released
开启池化:重用池中bytebuf的实例,并且采用了与jemalloc类似的内存分配算法提高分配效率
关闭池化:每次都需要创建bytebuf实例,内存重复创建和开销比较大,如果使用的是堆内存,对GC会产生很大的压力。
- 池化功能是否开启需要根据所使用的nettty版本和应用平台确定。
通过虚拟机参数开启/关闭池化
-Dio.netty.allocator.type={unpooled|pooled}
package netty_basic.bytebuf;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
public class TestBytebuf {
public static void main(String[] args) {
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
System.out.println(buf.getClass());
StringBuilder sb = new StringBuilder();
for(int i = 0;i < 300;++i) sb.append("a");
buf.writeBytes(sb.toString().getBytes());
}
}
输出:
class io.netty.buffer.PooledUnsafeDirectByteBuf
输出PooledUnsafeDirectByteBuf表明当前采用的是池化机制并且使用的是直接内存。
4-3 bytebuf的操作

特点
两个容量:bytebuf在创建的时候需要指定容量,最大容量通常是指系统能够支持分配的最大容量(理论最大值)。最大容量和指定容量之间可以作为可扩容的容量。
两个指针:分别是读写指针,buf初始化时都在起始位置。
空间分配:废弃字节(已写已读),可读字节(已写未读),可写字节,可扩容字节
=nio.bytebuffer和netty.bytebuffer的比较
| 类 | nio.bytebuffer | netty.buffer.ByteBuf |
|---|---|---|
| 容量 | 容量固定,定义后无法更改 | 设置初始容量,能够动态扩容,支持池化 |
| 实现原理 | 读写指针共享,需要切换读写模式 | 读写指针分开 |
写入
特点:
- int类型数据的写入支持大端(先写高位)和小端模式(小写地位),网络编程中通常使用大端模式
- 支持写入StringBuffer和StringBuilder和nio.bytebuffer。
| 方法签名 | 含义 | 注意点 |
|---|---|---|
| writeBoolean(boolean value) | 写入 boolean 值 | 用一字节 01|00 代表 true|false |
| 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 |
| 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) | 写入字符串 |
读取
buffer.readByte(); //从buffer中读取一个字节,此时读指针会移动
// 重复读取某段数据
buffer.markReaderIndex(); // 标记
buffer.readInt(); // 读取
buffer.resetReaderIndex(); // 回到标记(此时可以从标记位置继续读取)
内存回收
| ByteBuf 实现 | 回收方式 |
|---|---|
| UnpooledHeapByteBuf | JVM 内存,只需等 GC 回收 |
| UnpooledDirectByteBuf | 直接内存需要特殊的方法及时回收,虽然JVM采用虚引用确保最终回收,但实际场景中我们需要及时回收 |
| PooledByteBuf | 使用了池化机制,需要更复杂的规则来回收内存 |
内存管理的方法
buf.release();
buf.retain();
源码中不同实现都是通过实现protected abstract void deallocate()进行内存回收

Netty 这里采用引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口
该接口中主要对引用进行管理
- 每个 ByteBuf 对象的初始计数为 1
- 调用 release 方法计数减 1,如果计数为 0,ByteBuf 内存被回收
- 调用 retain 方法计数加 1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收
- 当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用
问题:bytebuf在使用的过程中,应该何时调用relase方法释放内存?
并非完全适用的方法
ByteBuf buf = ...
try {
...
} finally {
buf.release();
}
正确做法:谁是最后使用者,谁负责 release,因为 pipeline 的存在,一般需要将 ByteBuf 传递给下一个 ChannelHandler,如果在 finally 中 release 了,就失去了传递性,除非当前的bytebuf确实不再适用
业务代码中bytebuf的relase的通用规则:
- 起点,对于 NIO 实现来讲,在 io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe首次创建 ByteBuf 放入 pipeline入站 ByteBuf 处理原则
- 对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,这时无须 release
- 将原始 ByteBuf 转换为其它类型的 Java 对象,这时 ByteBuf 就没用了,必须 release
- 如果不调用 ctx.fireChannelRead(msg) 向后传递,那么也必须 release
- 注意各种异常,如果 ByteBuf 没有成功传递到下一个 ChannelHandler,必须 release
- 假设消息一直向后传,那么 TailContext 会负责释放未处理消息(原始的 ByteBuf)
- 出站 ByteBuf 处理原则
- 出站消息最终都会转为 ByteBuf 输出,一直向前传,由 HeadContext flush 后 release
- 异常处理原则
- 有时候不清楚 ByteBuf 被引用了多少次,但又必须彻底释放,可以循环调用 release 直到返回 true
Netty的pipeline底层的数据组织结构是双向链表,默认有头尾handler节点,在头尾节点中有负责bytybuf内存释放的源码。
pipeLine的默认尾节点源码
// final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
onUnhandledInboundMessage(ctx, msg);
}
-----------------------------------------------------------------------------
protected void onUnhandledInboundMessage(ChannelHandlerContext ctx, Object msg) {
onUnhandledInboundMessage(msg);
if (logger.isDebugEnabled()) {
logger.debug("Discarded message pipeline : {}. Channel : {}.",
ctx.pipeline().names(), ctx.channel());
}
}
-------------------------------------------------------------------------------
protected void onUnhandledInboundMessage(Object msg) {
try {
logger.debug(
"Discarded inbound message {} that reached at the tail of the pipeline. " +
"Please check your pipeline configuration.", msg);
} finally {
// 最终调用了ReferenceCountUtil的release方法
ReferenceCountUtil.release(msg);
}
}
--------------------------------------------------------------------------------
public static boolean release(Object msg) {
if (msg instanceof ReferenceCounted) { // 判断是否是bytebuf
return ((ReferenceCounted) msg).release();
}
return false;
}
总结:可以看到默认提供的尾节点handler中的channelRead方法最终会调用release方法释放bytebuf内存
pipeLine的默认头节点HeadContext源码

- 可以看到该类实现了ChannelOutboundHandler, ChannelInboundHandler,因此既可以对入栈信息处理也可以对出栈信息处理。
// final class HeadContext extends AbstractChannelHandlerContext
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
unsafe.write(msg, promise);
}
====================================================================
@Override
public final void write(Object msg, ChannelPromise promise) {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
// If the outboundBuffer is null we know the channel was closed and so
// need to fail the future right away. If it is not null the handling of the rest
// will be done in flush0()
// See https://github.com/netty/netty/issues/2362
safeSetFailure(promise, newClosedChannelException(initialCloseCause));
// release message now to prevent resource-leak
ReferenceCountUtil.release(msg); // 对内存进行释放
return;
}
int size;
try {
msg = filterOutboundMessage(msg);
size = pipeline.estimatorHandle().size(msg);
if (size < 0) {
size = 0;
}
} catch (Throwable t) {
safeSetFailure(promise, t);
ReferenceCountUtil.release(msg); // 对内存进行释放
return;
}
outboundBuffer.addMessage(msg, size, promise);
}
无复制操作
| 操作名称 | 说明 |
|---|---|
| slice | 没有内存复制对原始 ByteBuf 进行切片成多个 ByteBuf,共享内存但维护独立的 read,write 指针,并且容量固定无法写入。 |
| duplicate | 与原始 ByteBuf 使用同一块底层内存,只是读写指针是独立的 |
| CompositeByteBuf | 不发生复制可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf |
注意点:
- 由于上述操作内存共享,因此在操作后必须进行retain操作,避免原始ByteBuf在release后对引用的ByteBuf造成影响
- 切片后的ByteBuf由于内存共享,无法写入。
package netty_basic.bytebuf;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.util.internal.SocketUtils;
import static io.netty.buffer.ByteBufUtil.appendPrettyHexDump;
import static io.netty.util.internal.StringUtil.NEWLINE;
public class TestSliceAndDuplicate {
public static void main(String[] args) {
ByteBuf bf = ByteBufAllocator.DEFAULT.buffer();
bf.writeBytes(new byte[]{1, 2, 3, 4});
System.out.println("Slice和duplicate操作获取的ByteBuf:p1,p2,p3的与原始ByteBuf:bf共享相同的内存:");
ByteBuf p1 = bf.slice(0,2); log(p1);
p1.retain();
ByteBuf p2 = bf.slice(2,2); log(p2);
p2.retain();
ByteBuf p3 = bf.duplicate(); log(p3); p3.retain();
// retain表示原始ByteBuf的内存引用+1,防止原始的ByteBuf的release后其他引用失效
System.out.println("修改p1,则其他ByteBuf由于共享内存因此其访问的结果也是修改后的结果");
p1.setByte(0,6); log(bf);
log(p3);
bf.release(); p1.release(); p2.release(); p3.release();
}
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());
}
}
运行结果:可以发现内存确实是共享的
Slice和duplicate操作获取的ByteBuf:p1,p2,p3的与原始ByteBuf:bf共享相同的内存:
read index:0 write index:2 capacity:2
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 |.. |
+--------+-------------------------------------------------+----------------+
read index:0 write index:2 capacity:2
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 03 04 |.. |
+--------+-------------------------------------------------+----------------+
read index:0 write index:4 capacity:256
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 |.... |
+--------+-------------------------------------------------+----------------+
修改p1,则其他ByteBuf由于共享内存因此其访问的结果也是修改后的结果
read index:0 write index:4 capacity:256
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 06 02 03 04 |.... |
+--------+-------------------------------------------------+----------------+
read index:0 write index:4 capacity:256
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 06 02 03 04 |.... |
+--------+-------------------------------------------------+----------------+
不发生复制合并两个ByteBuf的方法如下:
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});
CompositeByteBuf buf3 = ByteBufAllocator.DEFAULT.compositeBuffer();
// true 表示增加新的 ByteBuf 自动递增 write index, 否则 write index 会始终为 0
buf3.addComponents(true, buf1, buf2);
非池化的ByteBuf的合并
package netty_basic.bytebuf;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
public class TestUnpooled {
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});
// Unpooled底层使用了 CompositeByteBuf从而避免拷贝的发生
ByteBuf buf3 = Unpooled.wrappedBuffer(buf1, buf2);
System.out.println(ByteBufUtil.prettyHexDump(buf3));
ByteBuf buf4 = Unpooled.wrappedBuffer(new byte[]{1, 2, 3}, new byte[]{4, 5, 6});
System.out.println(buf4.getClass());
System.out.println(ByteBufUtil.prettyHexDump(buf4));
}
}
执行结果
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 07 08 09 0a |.......... |
+--------+-------------------------------------------------+----------------+
class io.netty.buffer.CompositeByteBuf
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 |...... |
+--------+-------------------------------------------------+----------------+

主要介绍了Netty的数据通道Chanenl,进程间结果传递Future和Promise,数据处理组件handler,数据缓冲的ByteBuf的使用方法
重点关注:
a)其中ByteBuf的内存管理的基本原则
b) pipeLine的双向链表结构、pipeLine中进出站handler的调用顺序,pipeLine中默认头尾handler的作用(数据处理和内存管理)
c) Future和Promise接口的作用
浙公网安备 33010602011771号