Netty——入门+任务队列+异步模型
文章目录
一、Netty入门
要求:
引入Netty:
1.1 服务端
- 创建BossGroup 和 WorkerGroup
- 创建两个线程组,bossGroup 和 workerGroup;
- bossGroup 只是处理连接请求,真正的和客户端业务处理,会交给 workerGroup 完成;
- 两个逻辑都是无限循环;
- bossGroup 和 workerGroup 含有的子线程(NioEventLoop)个数为实际CPU核数 * 2。
- 创建服务器端的启动对象ServerBootstrap,并使用链式编程来配置参数。
- 设置两个线程组;
- 使用 NioServerSocketChannel 作为服务器的通道实现;
- 设置线程队列得到连接个数;
- 设置保持活动连接状态;
- 给 WorkerGroup 的 EventLoop 对应的 PipeLine 管道设置处理器。
-
ChannelInitializer用于给Channel对应的pipeline添加handler,该ChannelInitializer中的代码会在SocketChannel被创建时执行。
-
绑定一个端口并且同步,生成一个 ChannelFuture 对象。
-
对关闭通道进行监听。
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
// 创建 BossGroup 和 WorkerGroup
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 创建服务器端的启动对象,配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
// 使用链式编程来进行配置
bootstrap.group(bossGroup, workerGroup) // 设置两个线程组
.channel(NioServerSocketChannel.class) // 使用 NioServerSocketChannel 作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE, true) // 设置保持活动连接状态
.childHandler(new ChannelInitializer<SocketChannel>() { // 为 accept channel 的 pipeline 预添加的 handler
// 给 Pipeline 添加处理器,每当有连接 accept 时,就会运行到此处
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new NettyServerHandler());
}
}); // 给 workerGroup 的 EventLoop 对应的管道设置处理器
System.out.println("........服务器 is ready...");
// 绑定一个端口并且同步,生成一个 ChannelFuture 对象
// 启动服务器(并绑定端口)
ChannelFuture future = bootstrap.bind(6639).sync();
// 对关闭通道进行监听
future.channel().closeFuture().sync();
} finally {
// 必须关闭
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
- 服务端Handler处理
自定义一个Handler必须继承Netty规定好的某个HandlerAdapter,这是规范。
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 读取客户端发送过来的消息
* @param ctx 上下文对象:含有管道 pipeline,通道 channel,地址
* @param msg 客户端发送的数据,默认为 Object
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服务器读取线程:" + Thread.currentThread().getName());
System.out.println("server ctx = " + ctx);
// 查看 Channel 和 Pipeline 的关系
Channel channel = ctx.channel();
// 本质是个双向链表,出栈入栈
ChannelPipeline pipeline = ctx.pipeline();
// 将 msg 转成一个 ByteBuf,比 NIO 的 ByteBuffer 性能更高
ByteBuf buf = (ByteBuf)msg;
System.out.println("客户端发送的消息是:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("客户端地址:" + ctx.channel().remoteAddress());
}
// 数据读取完毕
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 它是 write + flush,将数据写入到缓存 buffer,并将 buffer 中的数据 flush 进通道
// 对这个发送的数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~", CharsetUtil.UTF_8));
}
// 处理异常,一般是关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
1.2 客户端实现
-
客户端需要一个事件循环组。
-
创建客户端启动对象,客户端使用的不是 ServerBootStrap 而是 Bootstrap。
-
链式编程设置相关参数。
-
启动客户端去连接服务器端,关于 channelFuture 涉及到 Netty 的异步模型。
-
对关闭通道进行监听。
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
// 客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
// 创建客户端启动对象
// 注意:客户端使用的不是 ServerBootStrap 而是 Bootstrap
Bootstrap bootstrap = new Bootstrap();
// 设置相关参数
bootstrap.group(group) // 设置线程组
.channel(NioSocketChannel.class) // 设置客户端通道的实现类(使用反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 加入自定义的处理器
socketChannel.pipeline().addLast(new NettyClientHandler());
}
});
System.out.println("客户端 OK...");
// 启动客户端去连接服务器端
// channelFuture 涉及到 netty 的异步模型
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6639).sync();
// 对关闭通道进行监听
channelFuture.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
- 客户端Handler处理
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 当通道就绪就会触发
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client: " + ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, server", CharsetUtil.UTF_8));
}
/**
* 当通道有读取事件时,会触发
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
System.out.println("服务器回复的消息:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("服务器的地址:" + ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
二、案例源码分析
2.1 服务端
- 每个Group组的子线程个数为:实际CPU核数 * 2
// 默认的 EventLoop 线程数
private static final int DEFAULT_EVENT_LOOP_THREADS;
static {
// 获取 CPU 核数
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
}
- ctx上下文对象
包含了非常全面的信息,包括 pipeline 和 channel。
- pipeline 和 channel 是相互包含的关系
三、任务队列 TaskQueue
任务队列由NioEventLoop维护并不断执行,当收到请求之后,在当前Channel对应的Pipeline中的各个Handler里面进行业务处理和请求过滤。
当某些业务需要耗费大量时间的时候,可以将任务提交到由NioEventLoop维护的taskQueue或scheduleTaskQueue中,让当前的NioEventLoop线程在空闲时间去执行这些任务。
3.1 三种使用场景
- 用户程序自定义的普通任务
- 该方式会将任务提交到taskQueue队列中,提交到该队列中的任务会按照提交顺序依次执行。
- 本质还是新开一个线程进行任务的异步处理,服务端的主线程就不会阻塞。
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 用户程序自定义的普通任务
// 异步执行 -> 提交到 Channel 对应的 NioEventLoop 的 taskQueue 中
ctx.channel().eventLoop().execute(new Runnable(){
@Override
public void run() {
try{
Thread.sleep(10 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~111111", CharsetUtil.UTF_8));
}catch (Exception e){
System.out.println(e.getMessage());
}
}
});
...
}
- 用户自定义的定时任务
该方式会将任务提交到scheduleTaskQueue定时任务队列中。该队列底层是优先队列PriorityQueue实现的,所以该队列中的任务会按照时间的先后顺序定时执行。
// 2.用户自定义的定时任务
ctx.channel().eventLoop().schedule(new Runnable(){
@Override
public void run() {
try{
Thread.sleep(10 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~111111", CharsetUtil.UTF_8));
}catch (Exception e){
System.out.println(e.getMessage());
}
}
}, 5, TimeUnit.SECONDS);
- 为其它EventLoop线程对应的Channel添加任务
可以在ChannelInitializer中,将刚创建的各个Channel以及对应的标识加入到统一的集合中去,然后可以根据标识获取Channel及其对应的NioEventLoop,然后就可以调用execute()或者schedule()方法。
四、Netty的异步模型——ChannelFuture
4.1 概述
- 当一个异步过程调用发出后,调用者不能立刻得到结果,而是通过 Future-Listener 机制。实际处理这个调用的组件在完成任务后,通过状态、通知和回调来通知调用者。
- Netty 中的 I/O 操作是异步的,Bind、Write、Connect 等操作会返回一个 ChannelFuture。
- Netty 的异步模型是建立在 future 和 callback 之上的。Future的核心思想是:假设一个方法非常耗时,阻塞等待返回显然不合适。那么可以在调用方法后,立马返回一个 Future,后续可以通过 Future去监控方法的处理过程。
- Future
- 异步的执行结果,可以通过它提供的方法来检测执行是否完成。
- ChannelFuture 是一个接口:public interface ChannelFuture extends Future< Void >,可以添加监听器,当监听的事件发生时,就会通知到监听器。
- 在使用 Netty 进行编程时,拦截操作和转换出入栈数据只需提供 Callback 或利用Future 即可。这使得链式操作简单、高效,并有利于编写可重用的、通用的代码。
4.2 Future-Listener机制
当 Future 对象刚刚创建时,处于非完成状态,可以通过返回的 ChannelFuture 来获取操作执行的状态,注册监听函数来执行完成后的操作。
// 绑定一个端口并且同步,生成一个 ChannelFuture 对象
// 启动服务器(并绑定端口)
ChannelFuture future1 = bootstrap.bind(6639).sync();
// 给 future1 注册监听器,监控事件
future1.addListener(future -> {
if(future.isSuccess()) {
System.out.println("绑定成功!");
} else{
System.err.println("绑定失败!");
}
});
五、入门实例——HTTP服务程序
要求:
5.1 服务端
- HttpServerCodec 是 Netty 提供的处理 HTTP 的编/解码器。
- SimpleChannelInboundHandler是ChannelInboundHandlerAdapter的子类。
- HttpObject:客户端和服务器端互相通讯的数据被封装成 HttpObject。
TestServer:
public class TestServer {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new TestServerInitializer());
ChannelFuture channelFuture = serverBootstrap.bind(9999).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
ChannelInitializer:
public class TestServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 向管道加入处理器
// 得到管道
ChannelPipeline pipeline = socketChannel.pipeline();
// 加入一个 netty 提供的 httpServerCodec:codec => [coder - decoder]
// 1、HttpServerCodec 是 netty 提供的处理 http 的编/解码器
pipeline.addLast("MyHttpServerCodec", new HttpServerCodec());
// 2、增加自定义的 Handler
pipeline.addLast("MyTestHttpServerHandler", new TestHttpServerHander());
}
}
SimpleChannelInboundHandler:
public class TestHttpServerHander extends SimpleChannelInboundHandler<HttpObject> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject httpObject) throws Exception {
//判断 msg 是不是 HTTPRequest 请求
if (httpObject instanceof HttpRequest) {
System.out.println("msg 类型 = " + httpObject.getClass());
System.out.println("客户端地址:" + channelHandlerContext.channel().remoteAddress());
// 强转
HttpRequest request = (HttpRequest) httpObject;
// 获取 URI,进行路径过滤
URI uri = new URI(request.uri());
if ("/favicon.ico".equals(uri.getPath())) {
System.out.println("请求了 favicon.ico,不做响应");
return;
}
// 回复信息给浏览器[ http 协议]
ByteBuf content = Unpooled.copiedBuffer("HelloWorld", CharsetUtil.UTF_8);
// 构造一个 http 的响应,即 HTTPResponse
DefaultFullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
// 将构建好的 response 返回
channelHandlerContext.writeAndFlush(response);
}
}
}
总结:
- 请求两次是因为浏览器还会自动请求一次图标。
- 不同的浏览器拥有独立的Channel和Pipeline。
- 基于HTTP1.0协议的特点,每次刷新网页重新请求,都会自动生成全新的Channel和Pipeline。
- 对于HTTP1.1,默认长连接,会保持一样。