Channel
首先强调一点:NIO的Channel与Netty的Channel不是一个东西!
Netty重新设计了Channel接口,并且给予了很多不同的实现。Channel时Netty的网络抽象类,除了NIO中Channel所包含的网络I/O操作,主动建立/关闭连接,获取双方网络地址的功能外,还包含了Netty框架的功能,例如:获取Channel的EventLoop\Pipeline等。
- Channel接口是能与一个网络套接字(或组件)进行I/0操作(读取\写入\连接\绑定)的纽带.
- 通过Channel可以获取连接的状态(是否连接/是否打开),配置通道的参数(设置缓冲区大小等),进行I/O操作
channel的主要方法是:
- id():返回此通道的全局唯一标识符.
- isActive():如果通道处于活动状态并连接,则返回true.
- isOpen():如果通道打开并且可能稍后激活,则返回true.
- close():关闭channel
- closeFuture():处理channel的关闭,并处理善后事件
- isRegistered():如果通道注册了EventLoop,则返回true.
- localAddress():返回此通道绑定的本地地址.
- pipeline():返回分派的ChannelPipeline.
- write():将数据写入.
- writeAndFlush():写入数据并刷出 ,write+flush方法.
ChannelFuture
由于Netty中的所有I / O操作都是异步的,因此Netty为了解决调用者如何获取异步操作结果的问题而专门设计了ChannelFuture接口。
因此,Channel与ChannelFuture可以说形影不离的。
channel和channelfuture的使用:
public class HelloClient {
public static void main(String[] args) throws InterruptedException, IOException {
//下面connect()返回值为ChannelFuture。一般带有Future、Promise的类型都是和异步方法配套使用的,用来处理结果
ChannelFuture channelFuture = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
}
})
//一般作为事件处理的都是返回一个channelFuture对象,如连接、关闭等
//此方法是异步非阻塞的,当前main线程发起的调用,真正执行连接的是上面的NioEventLoopGroup,nio线程
.connect(new InetSocketAddress("localhost", 8080));
//需要加下面的方法,否则服务器端收不到hello,因为连接还没建立就发送了
//1)使用sync方法,同步处理
//sync的主要作用是channelFuture同步处理结果,阻塞当前线程直到nio连接建立完毕
channelFuture.sync();
//获取channel
Channel channel = channelFuture.channel();
channel.writeAndFlush("hello");
//2)使用addListener(回调对象) 方法,异步处理结果
//参数是一个回调对象,现在是主线程不阻塞等结果了,把等待连接建立和建立后做的活给nio线程处理,只需把需要做的事传递过去即可
channelFuture.addListener(new ChannelFutureListener() {
@Override
//在nio线程建立好后,会调用此方法(区别:前一种方法是主线程调用的),该channelFuture就是调用时的channelFuture
public void operationComplete(ChannelFuture channelFuture) throws Exception {
Channel c = channelFuture.channel();
c.writeAndFlush("hello");
}
});
}
演示channel的关闭:
设计一个需求:客户端可以不断的发送数据到服务器,当发送quit时就退出,并正常关闭channel。代码如下:
//…………
Channel channel = channelFuture.sync().channel();
//新开一个输入数据的线程,不影响主线程的运行
new Thread(()->{
while (true){
Scanner scanner = new Scanner(System.in);
String msg = scanner.nextLine();
if ("quit".equals(msg)){
channel.close();
break;
}
channel.writeAndFlush(msg);
}
},"input").start();
//主线程结束标志
System.out.println("main结束");
现在多了个需求,关闭之后需要立即处理善后工作。
此时我们就需要用到closeFuture()方法了,为了证明此方法的必要性,先举几个反例。
1.有小伙伴会想把善后工作的代码写在上面主线程结束标志之前,新线程之后,那么这就不叫善后工作了,主线程创建一个新线程之后,会接着执行,主线程会先于输入线程结束。
2.那把善后工作放在channel.close()之后呢?也不行,因为close()方法是异步操作,关闭这个动作的真正线程是nioEventLoop线程,有可能它还没有把channel关闭,我们就已经把善后工作做完了。小伙伴们可以添加日志自行调试喔
netty结合Slf4j打印日志_清风拂来水波不兴的博客-CSDN博客
正确姿势:
1)同步关闭:先获取closeFuture,再sync,让主线程同步等待,当输入数据的线程close()通道后,sync方法就会停止阻塞,主线程就会继续执行下面的代码
//1)同步关闭,返回值同样为ChannelFuture ,请勿混淆
ChannelFuture closeFuture = channel.closeFuture();
closeFuture.sync();
//优雅的关闭事件循环组,因为连channel都关闭了,事件循环组里的线程们也需要关闭,结束程序的运行
group.shutdownGracefully();
log.debug("遗言");
2)异步关闭:
ChannelFuture closeFuture = channel.closeFuture();
//2)异步关闭,注意是closeFuture调用,给它添加一个操作完成后执行的方法
closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
log.debug("遗言");
//关闭事件循环组,优雅的关闭
group.shutdownGracefully();
}
});
是不是感觉这两种方法和connect时的处理逻辑一模一样。
为什么netty要用异步的方法去处理事件?
像一些connect、close等方法都是异步调用,实际是事件循环组给你真正的执行,为什么要这样呢?同步方法不好吗?
其实Netty采用的是流水线的工作方式,分工细化,提升效率吞吐率,充分发挥处理器的性能。
- 单线程没法异步提高效率,必须配合多线程、多核cpu才能发挥异步的优势
- 异步并没有缩短响应时间,反而有所增加
- 合理进行任务拆分,也是利用异步的关键
Futrue&Promise
在异步处理时,经常用到这两个接口,类关系如下,上面的channelFuture就是实现了Future
首先要说明netty中的Future 与 jdk juc中的Future同名,但是是两个接口,netty的Future继承自jdk的Future,而Promise又对netty Future进行了扩展。
- jdk Future 只能同步等待任务结束(或成功、或失败)才能得到结果
- netty Future可以同步等待任务结束得到结果,也可以异步方式得到结果,但都是要等任务结束
- netty Promise不仅有netty Future的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器
方法名 | jdk Future | netty Future | netty Promise |
cancel | 取消任务 | - | - |
isCanceled | 任务是否取消 | - | - |
isDone | 任务是否完成 不能区分成功失败 | - | - |
get | 获取任务结果,阻塞等待 | - | - |
getNow | - | 获取任务结果,非阻塞,还未产生结果为null | - |
await | - | 等待任务结束,如果任务失败不会抛异常,而是通过isSuccess判断 | - |
sync | - | 等待任务结束,如果任务失败抛异常 | - |
isSuccess | - | 判断任务是否成功 | - |
cause | - | 获取失败信息,非阻塞,如果没有失败返回null | - |
addListener | - | 添加回调,异步接收结果 | - |
setSuccess | - | - | 设置成功结果结果 |
setFailure | - | - | 设置任务失败结果 |
netty Future:
演示1:同步等待结果,也就是在当前线程同步等待获取结果
演示2:异步等待,由nio线程获取结果 ,下面是利用回调方法得到结果
netty Promise:
可以看到Future是提交任务时返回的,Future的创建和结果的设置权都不是我们控制的,不够灵活;下面讲解Promise的使用
//主动的创建Promise对象(作为结果容器),构造方法需要传一个eventLoop对象
DefaultPromise<Integer> promise = new DefaultPromise<Integer>(eventLoop);
new Thread(()->{
//任意一个线程执行计算,计算完毕向promise填充结果
log.debug("开始计算....");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//放置成功的结果到promise
promise.setSuccess(5);
//放置异常对象,放在catch里
//promise.setFailure(e)
}).start();
Handler&Pipeline
ChannelHandler用来处理Channel上的各种事件,分为入站(inbound)、出站(outbount)两种。所有ChannelHandler被连成一串,就是Pipeline
- 入站通常是ChannelInboundHandlerAdapter的子类,主要用于读取客户端数据,写回结果
- 出站通常是ChannelOutboundHandlerAdapter的子类,主要对写回结果进行加工
演示入站:
此时的pipeline中的handler为:head->h1->h2->tail
演示出站:
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//1.通过channel拿到pipeline
ChannelPipeline pipeline = ch.pipeline();
//2.添加处理器,netty默认添加了head-> xxx -> tail处理器
pipeline.addLast("h1",new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("h1");
//切换到下一个read处理器。同上一篇
super.channelRead(ctx,msg);
}
});
pipeline.addLast("h2",new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("h2");
//可以不写了,因为下一个没有read的处理器了
super.channelRead(ctx,msg);
//随便写一个数据,帮助触发下面的出站处理器
ch.writeAndFlush(ctx.alloc().buffer().writeBytes("server".getBytes()));
}
});
//添加出站handler,只有当向channel里写入了数据才会触发,如果没写数据,此handler就形同虚设
pipeline.addLast("h3",new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("h3");
super.write(ctx, msg, promise);
}
});
pipeline.addLast("h4",new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("h4");
super.write(ctx, msg, promise);
}
});
}
此时的pipeline中的handler为:head->h1->h2->h3->h4->tail
当客户端发送数据时结果为:
可以看到出站和入站有所不同,入站时从前往后依次执行,而出站处理器从后往前依次执行。
还有一个注意点:writeAndFlush方法上面代码里的NioSocketChannel ch可以调用,ChannelHandlerContext ctx也可以调用,但是ctx调用时是从当前位置开始往前面找,后面的write事件会没有机会执行。
ChannelDuplexHandler结合了出站和入站