目录
任务队列
任务队列的task
有3种典型的使用场景
- 用户程序自定义的普通任务
- 用户自定义定时任务
- 非当前
Reactor
线程调用Channel
的各种方法
例如:在推送系统的业务线程里面,根据用户的标识,找到对应的Channel
引用,然后调用write
类方法向该用户推送消息,就会进入到这种场景。最终的write
会提交到任务队列中后被异步消费。
假设有非常耗时的业务场景,我们希望使用异步执行解决这个问题。
解决方式一:用户程序自定义的普通任务
普通异步任务使用 execute 方法进行调度。
服务端的Handler
:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//比如这里我们有一个非常耗时的业务->异步执行->提交改channel对应的
//NIOEventLoop的taskQueue中
//解决方式一:用户程序自定义的普通任务
//执行业务逻辑任务是通过workGroup的eventLoop,注意workgroup才是线程池,eventloop是池里的线程
//所以这里并没有开辟一条新的线程,还是同一条线程
//只是给这个线程eventloop添加了一个任务到它的任务队列中,在Netty模型中,也是可以看到eventloop中是含有taskQueue的
//添加任务一。调用 NioEventLoop 线程的 execute 方法 , 即可将 Runnable 异步任务放入任务队列 TaskQueue ;
ctx.channel().eventLoop().execute(()->{
try {
Thread.sleep(10000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端000~",CharsetUtil.UTF_8));
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//添加任务二
ctx.channel().eventLoop().execute(()->{
try {
Thread.sleep(20000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端000~",CharsetUtil.UTF_8));
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("go on...");
}
这里通过图解,也可以看到线程eventLoop
的任务队列里多了两个任务,因为是队列,所以执行顺序是先执行任务一,再执行任务二,执行完这两任务需要30
秒。
普通异步任务提交到 TaskQueue 任务队列中 。
解决方式二:用户自定义定时任务
定时异步任务使用 schedule 方法进行调度。
//解决方式二:用户自定义定时任务
ctx.channel().eventLoop().schedule(()->{
try {
Thread.sleep(5000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端666~",CharsetUtil.UTF_8));
} catch (InterruptedException e) {
e.printStackTrace();
}
},5, TimeUnit.SECONDS);
在eventloop
也是能看到这个定时任务队列的。
定时异步任务提交到 ScheduleTaskQueue 任务队列中 。
解决方式三:非当前Reactor线程调用Channel的各种方法
可以在ChannelInitializer
中的initChannel
方法中,将Channel
的hashcode
与用户绑定,加入到集合中,需要推送消息时,从集合中获取Channel
,加入到相应的NIOEventLoop
的TaskQueue
或schdueleTaskQueue
推送消息
https://blog.csdn.net/HongZeng_CSDN/article/details/130226526
异步模型
基本介绍
- 异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者。
Netty
中的 I/O 操作是异步的,包括Bind、Write、Connect
等操作会简单的返回一个ChannelFuture
。- 调用者并不能立刻获得结果,而是通过
Future-Listener
机制,用户可以方便的主动获取或者通过通知机制获得IO
操作结果 Netty
的异步模型是建立在future
和callback
的之上的。callback
就是回调。重点说Future
,它的核心思想是:假设一个方法fun
,计算过程可能非常耗时,等待fun
返回显然不合适。那么可以在调用fun
的时候,立马返回一个Future
,后续可以通过Future
去监控方法fun
的处理过程(即 :Future-Listener
机制)
Future 说明
- 表示异步的执行结果, 可以通过它提供的方法来检测执行是否完成,比如检索计算等等.
ChannelFuture
是一个接口,表示通道异步的执行结果:public interface ChannelFuture extends Future<Void>
我们可以添加监听器,当监听的事件发生时,就会通知到监听器. 案例说明
Future-Listener 机制
Future-Listener
:表示异步执行结果——监听机制。
- 当
Future
对象刚刚创建时,处于非完成状态,调用者可以通过返回ChannelFuture
来获取操作执行的状态,注册监听函数来执行完成后的操作 - 常见有如下操作:
通过isDone
方法来判断当前操作是否完成;
通过isSuccess
方法来判断已完成的当前操作是否成功;
通过getCause
方法来获取已完成的当前操作失败的原因;
通过isCancelled
方法来判断已完成的当前操作是否被取消;
通过addListener
方法来注册监听器,当操作已完成(isDone
方法返回完成),将会通知指定的监听器;如果Future
对象已完成,则通知指定的监听器 - 举例说明:绑定端口
Bind
是异步操作,当绑定操作处理完,将会调用相应的监听器处理逻辑
//启动服务器并绑定端口
ChannelFuture sync = serverBootstrap.bind(6668).sync();
sync.addListener((ChannelFutureListener) channelFuture -> {
if (channelFuture.isSuccess()){
System.out.println("绑定完成");
}else {
System.out.println("绑定失败");
}
});
Netty实现一个简单的http服务器
由于客户端是浏览器,所以只需要编写服务端
服务端:
绑定6668
端口
public class HttpServer {
public static void main(String[] args) throws InterruptedException {
//设置main方法日志级别
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
List<Logger> loggerList = loggerContext.getLoggerList();
loggerList.forEach(logger -> {
logger.setLevel(Level.WARN);
});
//bossGroup专门负责接收客户端的连接
EventLoopGroup bossGroup=new NioEventLoopGroup();
//workerGroup专门负责网络的读写(真正的和客户端进行业务处理)
EventLoopGroup workerGroup=new NioEventLoopGroup();
try {
//创建服务器端的启动对象,配置参数
ServerBootstrap serverBootstrap = new ServerBootstrap();
//使用链式编程进行配置参数
serverBootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
//初始化通道
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//1.获取管道
//2.加入一个netty提供的httpServerCodec(netty提供的一个http的编--解码器)
//codec中的co是coder编码器的意思, dec是decoder解码器的意思
//第一个字符串是编解码器的名称
//3.为管道 Pipeline添加一个处理器
socketChannel.pipeline()
.addLast("HttpServerCodec",new HttpServerCodec())
.addLast("HTTPServerHandler",new HttpServerHandler());
}
});
//绑定一个端口并且同步,生成一个ChannelFuture对象
//启动服务器并绑定端口
ChannelFuture sync = serverBootstrap.bind(6668).sync();
//异步执行结果监听器
sync.addListener((ChannelFutureListener) channelFuture -> {
if(channelFuture.isSuccess()){
System.out.println("监听端口 6668 成功");
} else {
System.out.println("监听端口失败");
}
});
//对关闭通道进行监听
sync.channel().closeFuture().sync();
} finally {
//关闭线程池
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
自定义handler处理器
浏览器会发送两个请求,一个正常的请求,一个网页图标,要分开处理
/**
* 自定义handler处理器。
* SimpleChannelInboundHandler说明:
* 1.是ChannelInboundHandlerAdapter的子类
* 2.HttpObject 客户端和服务器端相互通讯的数据被封装成 HttpObject
**/
public class HttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
private static byte[] bytes;
static{
File file=new File("G:\\Desktop\\fish3.jpg");
FileInputStream fi=null;
try {
//获得文件字节大小
long fileSize = file.length();
if (fileSize>Integer.MAX_VALUE){
throw new IOException(file.getName()+" file too big...");
}
fi= new FileInputStream(file);
//数据中转站 临时缓冲区
bytes=new byte[(int)fileSize];
int offSet=0;
int numRead=0;
//循环读取文件内容,输入流中将最多fileSize个字节的数据读入一个bytes数组中,返回类型是读取到的字节数。
while(offSet<bytes.length && (numRead=fi.read(bytes,offSet,bytes.length-offSet))>=0){
offSet+=numRead;
}
//确保所有的数据均被读取
if (offSet!=bytes.length){
throw new IOException("Could not completely read file "
+ file.getName());
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(fi!=null){
try {
fi.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//读取客户端数据
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject httpObject) throws Exception {
//判断httpObject是不是一个HttpRequest请求
if (httpObject instanceof HttpRequest){
System.out.println("msg类型:"+httpObject.getClass());
System.out.println("客户端地址:"+channelHandlerContext.channel().remoteAddress());
HttpRequest httpRequest = (HttpRequest) httpObject;
URI uri = new URI(httpRequest.uri());
//缓存
ByteBuf content=null;
//处理图标
if ("/favicon.ico".equals(uri.getPath())){
//将数据写入缓存
content = Unpooled.copiedBuffer(bytes);
}else{
//回复信息给浏览器
//将数据写入缓存
content = Unpooled.copiedBuffer("hello,我是服务器", CharsetUtil.UTF_8);
}
//构造一个httpResponse
DefaultFullHttpResponse defaultFullHttpResponse =
new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
//设置响应头
//设置内容类型是文本类型
defaultFullHttpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain;charset=utf-8");
//设置返回内容的长度
defaultFullHttpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH,content.readableBytes());
//写出HTTP数据
channelHandlerContext.writeAndFlush(defaultFullHttpResponse);
}
}
}
测试
① 启动服务器 : 启动 HTTP
服务器 , 监听 6668
端口 ;
② 浏览器访问服务器 : 浏览器中输入 http://127.0.0.1:6668/
地址 , 即可访问 Netty HTTP
服务器 , 服务器返回 Hello Client 字符串信息 ; 注意:如果再浏览器中输入http://127.0.0.1:6668/
地址,返回"无法访问此网站"的信息,换一个浏览器再试试或者把端口改成8000以上。
③ 服务器端日志 :