Netty是 一个异步事件驱动的网络应用程序框架, 用于快速开发可维护的高性能协议服务器和客户端。
事件驱动模型
事件驱动模型主要应用在图形用户界面(GUI)、网络服务和 Web 前端上。
比如编写图形用户界面程序, 要给界面上每个按钮都添加监听函数, 而该函数只有在相应的按钮被用户点击的事件发生时才会执行, 开发者并不需要事先确定事件何时发生, 只需要编写事件的响应函数即可。监听函数或者响应函数就是所谓的事件处理器(event handler), 类似的事件还有鼠标移动、按下、松开、双击等等, 这就是事件驱动。
事件驱动的程序一般都有一个主循环(main loop)或称事件循环(event loop), 该循环不停地做两件事: 事件监测和事件处理。首先要监测是否发生了事件, 如果有事件发生则调用相应的事件处理程序, 处理完毕再继续监测新事件。
创建一个 ChannelInitializer(通道初始化),这里主要就是管理自定义 Handler,最终把这些 Handler 组装成一条双向链表,Channel 有事件时则触发链表进行业务处理逻辑
ChannelPipeline 可认为是一个管道,是管理业务 Handler,通俗理解是保存 ChannelHandler 的 List 集合,负责编排ChannelHandler以使ChannelHandler能有效的协同作业
ChannelPipeline 底层设计是采用责任链设计模式,作用是管理 Handler 双向链表,包括入站和出站,主要是拦截 inboud 和 outbound 事件,然后每个 handler 节点负责处理具体逻辑
ChannelHandler: 在Netty中作为处理Channel中的事件以及数据的一种方式。
对于入站与出站消息又分别使用ChannelInboundHandler与ChannelOutboundHandler来处理
ChannelInboundHandler 的实现类 ChannelInboundHandlerAdapter 处理入站 I/O 事件
ChannelOutboundHandler 的实现类 ChannelOutboundHandlerAdapter 处理出站 I/O 事件
ChannelHandler生命周期:
类型 | 描述 |
---|---|
handlerAdded | 当把ChannelHandler添加到ChannelPipeline中时被调用 |
handlerRemoved | 当把ChannelHandler在ChannelPipeline中移除时调用 |
exceptionCaught | 当ChannelHandler在处理过程中出现异常时调用 |
ChannelInboundHandler
ChannelInboundHandler中可以获取网络数据并处理各种事件,列出能处理的各种事件
入站事件是被动接收事件,例如接收远端数据,通道注册成功,通道变的活跃等等。
事件 说明 channelRegistered 当Channel注册到它的EventLoop并且能够处理I/O时调用 channelUnregistered 当Channel从它的EventLoop中注销并且无法处理任何I/O时调用 channelActive 当Channel处理于活动状态时被调用 channelInactive 不再是活动状态且不再连接它的远程节点时被调用 channelReadComplete 当Channel上的一个读操作完成时被调 channelRead 当从Channel读取数据时被调用 channelWritabilityChanged 当Channel的可写状态发生改变时被调用 userEventTriggered 当ChannelInboundHandler.fireUserEventTriggered()方法被调用时触发 Netty另外还提供了一个类来简化这一过程SimpleChannelInboundHandler类,
@Sharable// 表示它可以被多个channel安全地共享。在实际运行时,是起不到什么作用的 public class ClientHandler extends SimpleChannelInboundHandler
{ @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { System.err.println(msg); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } 继承SimpleChannelInboundHandler后,处理入站的数据我们只需要重新实现channelRead0()方法。
当channelRead真正被调用的时候我们的逻辑才会被处理。这里使用的是模板模式,让主要的处理逻辑保持不变,让变化的步骤通过接口实现来完成
ChannelOutboundHandler
出站的数据和操作由ChannelOutboundHandler接口处理
出站事件是主动触发事件,例如绑定,注册,连接,断开,写入等等。
ChannelOutboundHandler定义的方法列出
方法 描述 bind 当请求将Channel绑定到本地地址时被调用 connet 当请求将Channel连接到远程节点时被调用 disconnect 当请求将Channel从远程节点断开时调用 close 当请求关闭Channel时调用 deregister 当请求将Channel从它的EventLoop注销时调用 read 当请求从Channel中读取数据时调用 flush 当请求通过Channel将入队数据冲刷到远程节点时调用 write 当请求通过Channel将数据写入远程节点时被调用
ChannelHandlerContext 是对 ChannelHandler 的封装。
当一个 ChannelHandler
添加到管道ChannelPipeline
时,由ChannelPipeline
创建一个包裹 ChannelHandler
的上下文ChannelHandlerContext
对象添加进去。
在
ChannelHandler
中可以得到这个上下文对象ChannelHandlerContext
,这样它就可以:可以向上游或下游传递事件,实现责任链的功能,将事件传递给下一个处理器
ChannelHandler
处理。
责任传播和终止机制:以 ChannelInboundHandlerAdapter 的 channelRead 方法为例
ChannelHandlerContext 会默认调用 fireChannelRead 方法将事件默认传递到下一个处理器。
如果我们重写了 ChannelInboundHandlerAdapter 的 channelRead 方法,并且没有调用 fireChannelRead 进行事件传播,那么表示此次事件传播已终止。
ChannelHandlerContext 负责保存责任链节点上下文信息
首先ChannelHandlerContext是一个AttributeMap,可以用来存储多个数据。
可以从ChannelHandlerContext中获取到对应的channel,handler和pipline:
Channel channel();
ChannelHandler handler();
ChannelPipeline pipeline();
Channel:Netty 的网络通信组件,客户端和服务端建立连接之后会维持一个 Channel。读写网络数据等等都需要Channel这个组件的参与
Channel生命同期:
状态 | 描述 |
---|---|
ChannelUnregistered | Channel已经被创建,但还未注册到EventLoop |
ChannelRegistered | Channel已经被注册到EventLoop |
ChannelActive | Channel已经处理活动状态并可以接收与发送数据 |
ChannelInactive | Channel没有连接到远程节点 |
ChannelHandlerContext
;ChannelHandlerContext
则关联一个 ChannelHandler
(自定义 Handler);入站和出站的顺序:
ChannelInboundHandlerAdapter
子类会被触发;ChannelOutboundHandlerAdapter
子类会被触发。
io.netty
netty-all
4.1.39.Final
NioEventLoopGroup是EventLoopGroup的具体实现,
NioEventLoopGroup是线程组,包含一组NIO线程,专门用于网络事件的处理
netty的实例化,需要启动两个EventLoopGroup
一个boss,一个worker
EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup();
初始化两个线程: 一个线程(boss)负责接受新的连接,一个(worker)负责处理读写;
bootstrap:直译为"引导"
Netty 中 ServerBootStrap 和 Bootstrap 引导器是最经典的建造者模式实现,在构建过程中需要设置非常多的参数。
//服务器端
public class HelloServer {
public static void main(String[] args) {
//创建boss线程组 用于服务端接受客户端的连接
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//创建 worker 线程组 用于进行 SocketChannel 的数据读写
EventLoopGroup workerGroup = new NioEventLoopGroup();
//创建 ServerBootstrap 对象,用于设置服务端的启动配置
ServerBootstrap b = new ServerBootstrap()
.group(bossGroup, workerGroup)
//选择服务器的ServerSocketChannel实现
.channel(NioServerSocketChannel.class)
//通道初始化
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
//添加具体的handler
channel.pipeline().addLast(new StringDecoder());
channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg);
}
});
}
});
//服务端启动绑定端口号
ChannelFuture future = b.bind(8888);
}
}
获取一个ChannelFuture对象,它的作用是用来保存 Channel 异步操作的结果(咋感觉那么像promise对象??)
在 Netty 中所有的 IO 操作都是异步的,不能立刻得到 IO 操作的执行结果,但是可以通过注册一个监听器来监听其执行结果。在 Netty 当中是通过 ChannelFuture 来实现异步结果的监听
Netty 是异步操作,无法知道什么时候执行完成,在 Netty 当中 Bind 、Write 、Connect 等操作会简单的返回一个 ChannelFuture,来进行执行结果的监听。
//ChannelFuture 用来保存 Channel 异步操作的结果
ChannelFuture future = b.connect(new InetSocketAddress("localhost", 8888));
//等待异步操作执行完毕
future.sync();
------------------------------
//服务端启动绑定端口号
ChannelFuture future = b.bind(8888);
future.addListener(new GenericFutureListener>() {
public void operationComplete(Future super Void> future) {
if (future.isSuccess()) {
System.out.println("端口绑定成功!");
} else {
System.err.println("端口绑定失败!");
}
}
});
序号 | 方法 | 描述 |
---|---|---|
1 | addListener | 注册监听器,当操作已完成 (isDone 方法返回完成),将会通知指定的监听器;如果 Future 对象已完成,则通知指定的监听器 |
2 | removeListener | 移除监听器 |
3 | sync | 等待异步操作执行完毕 |
4 | await | 等待异步操作执行完毕 |
5 | isDone | 判断当前操作是否完成 |
6 | isSuccess | 判断已完成的当前操作是否成功 |
7 | isCancellable | 判断已完成的当前操作是否被取消 |
8 | cause | 获取已完成的当前操作失败的原因 |
sync () 和 await () 都是等待异步操作执行完成,那么它们有什么区别呢?
Future 可以通过四个核心方法来判断任务的执行情况。
状态 | 说明 |
---|---|
isDone() | 任务是否执行完成,无论成功还是失败 |
isSuccess() | 任务是否执行采购 |
isCancelled() | 任务是否被取消 |
cause() | 获取执行异常信息 |
if(future.isDone()){
if(future.isSuccess()){
System.out.println("执行成功...");
}else if(future.isCancelled()){
System.out.println("任务被取消...");
}else if(future.cause()!=null){
System.out.println("执行出错:"+future.cause().getMessage());
}
}
连接的时候第一个参数为 IP 或者域名,第二个参数为端口号
public class HelloClient {
public static void main(String[] args) throws InterruptedException {
Bootstrap b = new Bootstrap()
//EventLoop即(selector,thread)
.group(new NioEventLoopGroup())
//选择客户端实现
.channel(NioSocketChannel.class)
//添加处理器
.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
channel.pipeline().addLast(new StringEncoder());
}
});
//ChannelFuture 用来保存 Channel 异步操作的结果
ChannelFuture future = b.connect(new InetSocketAddress("localhost", 8888));
//等待异步操作执行完毕.sync () 会抛出异常
future.sync();
Channel channel = future.channel();
channel.writeAndFlush("hello,world");
}
}
Channe组件:Netty 的网络通信组件,客户端和服务端建立连接之后会维持一个 Channel
在网络情况差的情况下,客户端第一次连接可能会连接失败,这个时候我们可能会尝试重新连接
private static void connect(Bootstrap bootstrap, String host, int port) {
bootstrap.connect(host, port).addListener(future -> {
if (future.isSuccess()) {
System.out.println("连接成功!");
} else {
//获取EventLoopGroup
EventLoopGroup thread=bootstrap.config().group();
//每隔5秒钟重连一次
thread.schedule(new Runnable() {
public void run() {
//递归调用连接方法
connect(bootstrap, host, port);
}
}, 5, TimeUnit.SECONDS);
}
});
}
bootstrap.config().group()
bootstrap.config().group()
返回的是我们最开始配置的线程模型workerGroup
,调用workerGroup
的schedule
方法即可实现定时任务逻辑。
通常情况下,连接建立失败不会立即重新连接,而是会通过一个指数退避的方式,根据一定是的时间间隔,比如 2 的次幂来简历连接,到达一定次数之后就放弃连接
private static void connect(Bootstrap bootstrap, String host, int port, int retry) {
bootstrap.connect(host, port).addListener(future -> {
if (future.isSuccess()) {
System.out.println("连接成功!");
} else if (retry == 0) {
System.err.println("重试次数已用完,放弃连接!");
} else {
// 第几次重连
int order = (MAX_RETRY - retry) + 1;
// 本次重连的间隔
int delay = 1 << order;
System.err.println(new Date() + ": 连接失败,第 " + order + " 次重连……");
bootstrap.config()
.group()
.schedule(() -> connect(bootstrap, host, port, retry - 1), delay, TimeUnit.SECONDS);
}
});
}
// 调用方式
// MAX_RETRY 最大重试次数
connect(bootstrap, "127.0.0.1", 8070, MAX_RETRY);
1.只启动客户端HelloClient,会报错
2、只启动服务端HelloServer,不会报错
3、启动服务端后,再次启动客户端