Netty(一)

4 篇文章 0 订阅

概述

  • Netty是一个异步事件驱动的网络应用框架, 可以快速开发高性能服务器和客户端
  • Netty对 JDK的 NIO API进行了封装, 解决了 Epoll Bug, 导致 Selector空轮询的问题& 改善了 NIO的使用繁琐& 难的等问题
  • 官网: https://netty.io

在这里插入图片描述

版本

  • 版本分为 netty3.x, netty4.x和 netty5.x. 其中5.x, 出现重大 bug, 已被官网废弃, 目前推荐使用4.x的稳定版

架构设计

  • 线程模型: 目前有两种线程模型 1. 阻塞 I/O模型 2. Reactor事件驱动模型
  • 阻塞 I/O模型:
    (-) 每个连接都需要通过独立的线程来处理数据
    (-) 当并发数多时, 由于每个连接都需要开启线程, 导致严重影响性能
    (-) 连接建立后, 如果当前线程, 暂时, 没有可读数据, 此时该线程会阻塞 read操作, 造成线程资源浪费
  • Reactor事件驱动模型:
    (-) 基于 I/O复用模型: 多个连接共用一个阻塞对象, 程序只需在一个阻塞对象上等待, 无需阻塞等待所有连接. 当某个连接有新的数据可以处理时, 操作系统通知程序, 线程从阻塞状态返回, 开始处理业务
    (-) 不必再为每个连接创建线程, 一个线程可以处理多个连接的业务
    (-) Reactor又称: 反应器模式, 分发者模式(Dispatcher)或通知者模式(Notifier)
  • Reactor模式基本设计图:

在这里插入图片描述

Reactor模式又分3种实现

  • 单 Reactor单线程:

在这里插入图片描述

  1. Select是 I/O复用模型的标准网络编程 API, 可以实现程序通过一个阻塞对象监听多路连接请求
  2. Reactor对象通过 Select监控客户端请求事件, 收到事件后通过 Dispatch进行分发
  3. 如果客户端建立连接, 则通过 Accept获得通道, 再创建一个 Handler对象待后续业务处理
  4. 如果不是建立连接事件, 则 Reactor会分发调用连接对应的 Handler来处理
  5. Handler会完成 Read -> 业务处理 -> Send
  • 缺点:
  1. 只有一个线程, 未充分利用多核 CPU的性能. 并发多时, 容易导致性能瓶颈
  2. 发生线程意外终止等意外情况, 会导致整个系统的不可用, 也就是可靠性上的问题
  • 单 Reactor多线程:

在这里插入图片描述

  1. Reactor对象通过 select监控客户端请求, 收到事件后,通过 dispatch进行分发
  2. 如果客户端建立连接, 则通过 accept获得通道, 再创建一个 handler对象, 处理完成连接的各种事件
  3. 如果不是建立连接事件, 则 reactor会分发调用连接对应的 handler来响应
  4. Handler只负责响应事件, 不做具体的业务处理, 通过 read读取数据后, 会分发给后面的 worker线程池的某个线程处理业务
  5. Worker线程池会分配独立线程完成实际业务, 并将结果返回给 handler
  6. Handler收到响应后, 通过 send, 将结果返回给 client
  • 缺点: reactor处理所有事件的监听和响应, 可是在单线程上运行, 这会导致高并发场景上发生性能瓶颈
  • 主从 Reactor(Netty线程模式是基于主从 Reactor):

在这里插入图片描述

  1. Reactor主线程 Main reactor对象通过 select监听连接事件, 收到连接事件后建立连接
  2. 处理连接事件后, 将连接分配给 subReactor
  3. Subreactor将连接加入到连接队列进行监听, 并创建 handler进行各种事件处理
  4. 当有新事件发生时, subreactor就会调用对应的 handler来处理
  5. Handler通过 read读取数据, 分发给后面的 worker线程处理
  6. Worker线程池分配独立的 worker线程进行业务处理, 并返回结果
  7. Handler收到结果后, 再通过 send将结果返回给 client
  8. Reactor主线程可以对应多个 reactor子线程, 即 Main recator可以关联多个 subReactor
  • 优点: 父线程与子线程的数据交互简单职责明确, 父线程只需要接收新连接, 子线程完成后续的业务处理
  • 缺点: 编程复杂度较高
  • 各种模式的生活案例:
  1. 单 Reactor单线程, 前台接待员和服务员是同一个人
  2. 单 Reactor多线程, 1个前台接待员, 多个服务员, 接待员只负责接待
  3. 主从 Reactor多线程, 多个前台接待员, 多个服务员
  • Reactor模式具有如下的优点:
  1. 响应快, 不必为单个同步时间所阻塞, 虽然 reactor本身依然是同步的
  2. 可以最大程度的避免复杂的多线程及同步问题, 且避免了多线程/进程的切换开销
  3. 扩展性好, 可以方便的通过增加 reactor实例个数来充分利用 CPU资源
  4. 复用性好, reactor模型本身与具体事件处理逻辑无关, 具有很高的复用性
  • Netty模型:
  1. BossGroup线程维护的 selector, 当接收到 accept事件(连接事件)后, 首先获取到对应的 SocketChannel, 封装成 NIOScoketChannel, 并注册到 worker线程中(进行事件循环)
  2. 当 Worker线程的 selector监听到自己所关注的事件后, 就由 handler进行处理(读写业务)

在这里插入图片描述

  1. Netty抽象出两组线程池 BossGroup只负责接收客户端的连接(accept事件), WorkerGroup负责实际业务
  2. BossGroup和 WorkerGroup类型都是 NioEventLoopGroup
  3. NioEventLoopGroup是事件循环组, 组内含有多个事件循环 NioEventLoop
  4. NioEventLoop表示一个任务的线程, 每个 NioEventLoop都有一个 selector, 用于监听绑定在其上的 socket
  5. 每个 Boss NioEventLoop循环执行的步骤有3步
    5-1) 轮询 accept事件
    5-2) 处理 accept事件, 与 client建立连接, 生成 NioScocketChannel, 并将其注册到某个 worker NIOEventLoop上的 selector
    5-3) 处理任务队列的任务, 即 runAllTasks
  6. 每个 Worker NIOEventLoop循环执行的步骤
    6-1) 轮询 read& write事件
    6-2) 处理 read, write事件
    6-3) 处理任务队列的任务, 即 runAllTasks
  7. 每个 Worker NIOEventLoop处理业务是在 pipeline(管道), 即通过pipeline获取到对应通道, 并处理业务
  • Netty实例(TCP服务)
  • NioEventLoopGroup下包含多个 NioEventLoop
    (-) 每个 NioEventLoop中包含有一个 Selector和一个 taskQueue
    (-) 每个 NioEventLoop的 Selector上可以注册监听多个 NioChannel
    (-) 每个 NioChannel只会绑定在唯一的 NioEventLoop上
    (-) 每个 NioChannel都绑定有一个自己的 ChannelPipeline

  • 异步模型
    (-) Netty中的 I/O操作是异步的 如 Bind, Write, Connect等操作会返回一个 ChannelFuture
    (-) 调用者需主动或通过通知机制(Future-Listener)获得 IO操作结果
    * Future-Listener机制:

  • 当 Future对象刚刚创建时, 处于非完成状态, 此时可以通过返回的 ChannelFuture来获得操作执行的状态, 注册监听函数, 再通过该函数来完成执行操作

  • 常用操作:

  1. isDone(): 判断当前操作是否完成
  2. isSuccess(): 判断已完成的当前操作是否成功
  3. cause(): 获取已完成的当前操作失败的原因
  4. isCancelled(): 判断已完成的当前操作是否被取消
  5. addListener(): 注册监听器, 当操作已完成(isDone方法返回完成), 将会通知指定的监听器

public class NettyServer {
    public static void main(String[] args) throws Exception {
        // 1. bossGroup处理客户端连接请求, workerGroup处理客户端业务请求
        // 2. 两个组都是无限循环
        // 3. NioEventLoopGroup参数为启动子线程(NioEventLoop)个数(可选), 默认 CPU核数 x 2
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(); //8
        try {
            // 创建服务器端的启动对象
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup) // 设置线程组
                    .channel(NioServerSocketChannel.class) // 设置服务器的通道实现
                    // SO_BACKLOG对应 tcp/ip协议的监听函数 listen(int socketfd,int backlog)中的 backlog参数, 用来设置, 等待处理客户端连接服务端的队列大小
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true) // 设置保持活动连接状态
                    //.handler(null) // handler()对应 serverBootstrap, childHandler()对应 workerGroup
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        // 初始化通道对象
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            System.out.println("客户端: SocketChannel.hashcode=" + ch.hashCode());
                            // 给 workerGroup(NioEventLoopGroup)中, 各 NioEventLoop, 对应 pipeline设置处理器
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    });
            System.out.println("Netty Server is ready...");
            // 服务器绑定端口
            ChannelFuture cf = serverBootstrap.bind(6666).sync();
            // 给 cf注册监听器
            cf.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) {
                    if (future.isSuccess()) {
                        System.out.println("监听端口6666成功!");
                    } else {
                        System.out.println("监听端口6666失败!");
                    }
                }
            });
            // 对关闭通道进行监听
            cf.channel().closeFuture().sync();
        } finally {
            // 优雅关闭
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    // 读取客户端发送的数据
    // ChannelHandlerContext ctx:上下文对象, 含管道 pipeline, 通道channel等
    // Object msg: 客户端发送的数据
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("读事件: 服务器线程=" + Thread.currentThread().getName() + " channle=" + ctx.channel());
        System.out.println("客户端发送的消息=" + ((ByteBuf) msg).toString(CharsetUtil.UTF_8) + " 客户端地址=" + ctx.channel().remoteAddress());

        // TODO: 当有比较耗时的业务, 可以异步处理;
        // 解决方案 1: 添加普通任务(任务会加到 NioEventLoop的 taskQueue中
        ctx.channel().eventLoop().execute(() -> {
            try {
                Thread.sleep(5 * 1000);
                ctx.writeAndFlush(Unpooled.copiedBuffer("hello, client!!", CharsetUtil.UTF_8));
                System.out.println("服务器任务线程1=" + Thread.currentThread().getName() + " channel code=" + ctx.channel().hashCode());
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        });
        // 在多任务, 在同一个 NioEventLoop时, 由于是在同一个线程内, 所以异步按顺序执行的
        ctx.channel().eventLoop().execute(() -> {
            try {
                Thread.sleep(10 * 1000);
                ctx.writeAndFlush(Unpooled.copiedBuffer("hello, client!!!", CharsetUtil.UTF_8));
                System.out.println("服务器任务线程2=" + Thread.currentThread().getName() + " channel code=" + ctx.channel().hashCode());
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        });

        //解决方案 2: 定时任务 -> 该任务是提交到 scheduleTaskQueue中
        ctx.channel().eventLoop().schedule(new Runnable() {
            @Override
            public void run() {
                try {
                    ctx.writeAndFlush(Unpooled.copiedBuffer("hello, client!!!!", CharsetUtil.UTF_8));
                    System.out.println("服务器任务线程3=" + Thread.currentThread().getName() + " channel code=" + ctx.channel().hashCode());
                } catch (Exception e) {
                    System.out.println(e.getMessage());
                }
            }
        }, 10, TimeUnit.SECONDS);
        System.out.println("go on...");
    }

    // 数据读取完毕
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        // 将对数据进行编码后写入到缓存, 并刷新
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, client!", CharsetUtil.UTF_8));
    }

    // 处理异常, 一般是需要关闭通道
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

public class NettyClient {
    public static void main(String[] args) throws Exception {
        // 创建客户端的事件循环组
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            // 创建客户端启动对象
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group) // 设置线程组
                    .channel(NioSocketChannel.class) // 设置客户端通道实现类(反射)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            // 加入自己的处理器
                            // 给 group(NioEventLoopGroup)中, 各 NioEventLoop, 对应 pipeline设置处理器
                            ch.pipeline().addLast(new NettyClientHandler());
                        }
                    });
            System.out.println("NettyClient is ok...");
            //启动客户端去连接服务器端
            //关于 ChannelFuture 要分析,涉及到netty的异步模型
            ChannelFuture cf = bootstrap.connect("127.0.0.1", 6666).sync();
            // 给 cf注册监听器
            cf.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) {
                    if (future.isSuccess()) {
                        System.out.println("监听服务器成功!");
                    } else {
                        System.out.println("监听服务器失败!");
                    }
                }
            });
            // 给关闭通道进行监听
            cf.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

public class NettyClientHandler extends ChannelInboundHandlerAdapter {
    // 当通道就绪时触发
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("client.ChannelHandlerContext=" + ctx);
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, server!", CharsetUtil.UTF_8));
    }

    // 当通道有读取事件时触发
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("服务器的地址=" + ctx.channel().remoteAddress() + ", 消息=" + ((ByteBuf) msg).toString(CharsetUtil.UTF_8));
    }

    // 处理异常, 一般是需要关闭通道
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

===> 客户端连接时, 服务端打印:
Netty Server is ready...
监听端口6666成功!
客户端: SocketChannel.hashcode=106062425
读事件: 服务器线程=nioEventLoopGroup-3-1 channle=[id: 0x176ae39e, L:/127.0.0.1:6666 - R:/127.0.0.1:60577]
客户端发送的消息=hello, server! 客户端地址=/127.0.0.1:60577
go on...
服务器任务线程1=nioEventLoopGroup-3-1 channel code=106062425
服务器任务线程2=nioEventLoopGroup-3-1 channel code=106062425
服务器任务线程3=nioEventLoopGroup-3-1 channel code=106062425

*可以看到 Handler处理器内普通异步任务的线程和 channelRead方法的线程实际上是同一个, 所以该方式处理耗时工作(如 数据库, 网络请求等操作)依然会严重影响 Netty对 Socket的处理速度

  • 处理器加异步线程池两种方式:
  1. Handler中加线程池: 使用灵活, 比如有耗时操作时异步, 没有就不使用
  2. Context中加线程池: 不管耗不耗时都会放到异步队列里处理, 不够灵活

方式 1: Handler中加线程池
public class NettyServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        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 ch) throws Exception {
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    });
            ChannelFuture cf = serverBootstrap.bind(6666).sync();
            cf.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    // 通过 DefaultEventExecutorGroup额外开辟业务线程池, 用于处理耗时任务
    static final EventExecutorGroup executorGroup = new DefaultEventExecutorGroup(16);

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("读事件: 服务器线程=" + Thread.currentThread().getName() + " channle=" + ctx.channel());
        System.out.println("客户端发送的消息=" + ((ByteBuf) msg).toString(CharsetUtil.UTF_8) + " 客户端地址=" + ctx.channel().remoteAddress());

        // 使用异步线程处理业务
        executorGroup.submit(new Callable<Object>() {
            @Override
            public Object call() throws Exception {
                Thread.sleep(5 * 1000); // 休眠5秒
                System.out.println("异步线程 1=" + Thread.currentThread().getName());
                ctx.writeAndFlush(Unpooled.copiedBuffer("哈 1", CharsetUtil.UTF_8));
                return null;
            }
        });

        executorGroup.submit(() -> {
            Thread.sleep(10 * 1000); // 休眠10秒
            System.out.println("异步线程 2=" + Thread.currentThread().getName());
            ctx.writeAndFlush(Unpooled.copiedBuffer("哈 2", CharsetUtil.UTF_8));
            return null;
        });

        executorGroup.submit(() -> {
            Thread.sleep(15 * 1000); // 休眠15秒
            System.out.println("异步线程 3=" + Thread.currentThread().getName());
            ctx.writeAndFlush(Unpooled.copiedBuffer("哈 3", CharsetUtil.UTF_8));
            return null;
        });
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, client!", CharsetUtil.UTF_8));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

===> 客户端连接时, 服务端打印:
读事件: 服务器线程=nioEventLoopGroup-3-1 channle=[id: 0xea4d1524, L:/127.0.0.1:6666 - R:/127.0.0.1:65202]
客户端发送的消息=hello, server! 客户端地址=/127.0.0.1:65202
异步线程 1=defaultEventExecutorGroup-4-1
异步线程 2=defaultEventExecutorGroup-4-2
异步线程 3=defaultEventExecutorGroup-4-3

方式 2: Context中加线程池
public class NettyServer {
    // 通过 DefaultEventExecutorGroup额外开辟业务线程池, 用于处理耗时任务
    static final EventExecutorGroup executorGroup = new DefaultEventExecutorGroup(2);

    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        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 ch) throws Exception {
                            ch.pipeline().addLast(executorGroup, new NettyServerHandler());
                        }
                    });
            ChannelFuture cf = serverBootstrap.bind(6666).sync();
            cf.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("读事件: 服务器线程=" + Thread.currentThread().getName() + " channle=" + ctx.channel());
        System.out.println("客户端发送的消息=" + ((ByteBuf) msg).toString(CharsetUtil.UTF_8) + " 客户端地址=" + ctx.channel().remoteAddress());
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, client!", CharsetUtil.UTF_8));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

===> 客户端连接时, 服务端打印:
读事件: 服务器线程=defaultEventExecutorGroup-2-1 channle=[id: 0x76b09cdb, L:/127.0.0.1:6666 - R:/127.0.0.1:49628]
客户端发送的消息=hello, server! 客户端地址=/127.0.0.1:49628

  • Netty实例(HTTP服务)
    浏览器打开 http://localhost

// Netty Http Server
public class TestServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new TestServerInitializer());
            // 服务器绑定端口
            ChannelFuture cf = serverBootstrap.bind(80).sync();
            System.out.println("Netty Http Server is ready...");
            // 给 cf注册监听器
            cf.addListener((ChannelFuture future) -> {
                if (future.isSuccess()) {
                    System.out.println("监听端口80成功!");
                } else {
                    System.out.println("监听端口80失败!");
                }
            });
            // 对关闭通道进行监听
            cf.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

}

// init Handler
public class TestServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        // 取得管道为了加入处理器(Worker Group -> NioEventLoopGroup -> NioEventLoop -> pipeline)
        ChannelPipeline pipeline = ch.pipeline();
        // 配置 netty的 http的编/解码器
        pipeline.addLast("MyHttpServerCodec", new HttpServerCodec());
        //2. 加一个自定义的 handler
        pipeline.addLast("MyTestHttpServerHandler", new TestHttpServerHandler());
    }

}

// SimpleChannelInboundHandler继承了 ChannelInboundHandlerAdapter
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
    // channelRead0读取客户端数据
    // HttpObject 客户端与服务器端相互通讯的数据
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
        System.out.println("current channel=" + ctx.channel() + " pipeline=" + ctx.pipeline() + " > channel" + ctx.pipeline().channel());
        //判断 msg 是不是 httprequest请求
        if (msg instanceof HttpRequest) {
            System.out.println("客户端地址=" + ctx.channel().remoteAddress() + " ctx类型=" + ctx.getClass());
            System.out.println("pipeline hashcode=" + ctx.pipeline().hashCode() + " TestHttpServerHandler hashcode=" + this.hashCode());
            // 获取客户端数据
            HttpRequest httpRequest = (HttpRequest) msg;
            // 获取uri
            URI uri = new URI(httpRequest.uri());
            // 可以过滤指定的资源
            if ("/favicon.ico".equals(uri.getPath())) {
                System.out.println("请求了 favicon.ico, 不做响应");
                return;
            }
            // 回复信息给浏览器 [http协议]
            ByteBuf content = Unpooled.copiedBuffer("hello, this is http server!", CharsetUtil.UTF_8);
            // 构造一个 http相应
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
            // 配置 http header
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
            // 返回 response
            ctx.writeAndFlush(response);
        }
    }

}

如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值