Netty——入门+任务队列+异步模型


JAVA后端开发知识总结(持续更新…)


Netty——入门+任务队列+异步模型



一、Netty入门

要求

在这里插入图片描述

引入Netty

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.1 服务端

  • 创建BossGroupWorkerGroup
  1. 创建两个线程组,bossGroup 和 workerGroup;
  2. bossGroup 只是处理连接请求,真正的和客户端业务处理,会交给 workerGroup 完成;
  3. 两个逻辑都是无限循环
  4. bossGroup 和 workerGroup 含有的子线程(NioEventLoop)个数为实际CPU核数 * 2。
  • 创建服务器端的启动对象ServerBootstrap,并使用链式编程来配置参数。
  1. 设置两个线程组;
  2. 使用 NioServerSocketChannel 作为服务器的通道实现;
  3. 设置线程队列得到连接个数;
  4. 设置保持活动连接状态;
  5. 给 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 概述

  1. 当一个异步过程调用发出后,调用者不能立刻得到结果,而是通过 Future-Listener 机制。实际处理这个调用的组件在完成任务后,通过状态、通知和回调来通知调用者。
  2. Netty 中的 I/O 操作是异步的,Bind、Write、Connect 等操作会返回一个 ChannelFuture
  3. Netty 的异步模型是建立在 future 和 callback 之上的。Future的核心思想是:假设一个方法非常耗时,阻塞等待返回显然不合适。那么可以在调用方法后,立马返回一个 Future,后续可以通过 Future去监控方法的处理过程。
  • Future
  1. 异步的执行结果,可以通过它提供的方法来检测执行是否完成。
  2. ChannelFuture 是一个接口:public interface ChannelFuture extends Future< Void >,可以添加监听器,当监听的事件发生时,就会通知到监听器。
  3. 在使用 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);
        }
    }
}

在这里插入图片描述

在这里插入图片描述

总结

  1. 请求两次是因为浏览器还会自动请求一次图标。
  2. 不同的浏览器拥有独立的Channel和Pipeline
  3. 基于HTTP1.0协议的特点,每次刷新网页重新请求,都会自动生成全新的Channel和Pipeline。
  4. 对于HTTP1.1,默认长连接,会保持一样。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值