RPC框架设计 三、Netty核心原理

一、Netty介绍

1.原生NIO存在的问题

  1. NIO的类库和API繁杂,使用麻烦:需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等
  2. 需要具备其他的额外技能:要熟悉Java多线程变成,因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序。
  3. 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等
  4. JDK NIO的Bug:Epoll Bug,它会导致Selector空轮询,最终导致CPU100%。直到JDK1.7版本该问题仍然存在,没有被根本解决

2.Netty概述

Netty是有JBOSS提供的一个Java开源框架。Netty提供异步的、急基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络IO程序。Netty是一个基于NIO的网络编程框架,使用Netty可以帮助你快速、简单的开发出一个网络应用,相当于简化和流程化了NIO的开发过程。作为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,知名的Elasticsearch、Dubbo框架内部都采用了Netty。

具备如下优点:

  1. 设计优雅,提供阻塞和非阻塞的Socket;提供灵活可拓展的事件模型;提供高度可定制的线程模型
  2. 具备更高的性能和更大的吞吐量,使用零拷贝技术最小化不必要的内存复制,减少资源的消耗。
  3. 提供安全传输特性
  4. 支持多种主流协议;预置多种编解码功能,支持用户开发私有协议

二、线程模型

1.线程模型基本介绍

不同的线程模式,对程序的性能有很大影响。
目前存在的线程模型有:

  • 传统阻塞I/O服务模型
  • Reactor模型,根据Reactor的数量和处理资源池线程的数量不同,有3中典型的实现
    • 单Reactor 单线程
    • 单Reactor 多线程
    • 主从Reactor 多线程

2.传统阻塞I/O服务模型

采用阻塞IO模式获取输入的数据,每个连接都需要独立的线程完成数据的输入,业务处理和数据返回工作。
image.png

存在问题:

  1. 当并发数很大,就会创建大量的线程,占用很大系统资源
  2. 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在read操作,造成线程资源浪费

3.Reactor 模型

Reactor模型,通过一个或多个输入同时传递给服务处理器的模式,服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程,因此Reactor模式也叫Dispatcher模式。Reactor模式使用IO复用监听事件,收到事件后,分发给某个线程(进程),这点就是网络服务器高并发处理的关键。

1)单Reactor单线程

image.png

  • Selector是可以实现应用程序通过一个阻塞对象监听多路连接请求
  • Reactor对象通过Selector监控客户端请求事件,收到事件后通过Dispatch进行分发
  • 是建立连接请求事件,则由Acceptor通过Accpet处理连接请求,然后创建一个Handler对象处理连接完成后的后续业务处理
  • Handler会完成Read–>业务处理–>Send的完整业务流程

优点:

  • 模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成

缺点:

  • 性能问题:只有一个线程,无法完全发挥多核CPU的性能。Handler在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈
  • 可靠性问题:线程意外终止或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障
2)单Reactor多线程

image.png

  • Reactor对象通过Selector监控客户端请求事件,收到事件后,通过dispatch进行分发
  • 如果建立连接请求,则右Acceptor通过accept处理连接请求
  • 如果不是连接请求,则由reactor分发调用连接对应的handler来处理
  • handler只负责响应事件,不做具体的业务处理,通过read读取数据后,会分给后面的worker线程池的某个线程处理业务
  • worker线程池会分配独立线程完成真正的业务,并将结果返回给handler
  • handler收到响应后,通过send将结果返回给client

优点:

  • 可以充分的利用多核CPU的处理能力

缺点:

  • 多线程数据共享和访问比较复杂,reactor处理所有的事件的监听和响应,在单线程运行,在高并发场景容易出现性能瓶颈
3)主从Reactor多线程

image.png

  • Reactor主线程MainReactor对象通过select监听客户端连接事件,收到事件后,通过Acceptor处理客户端连接事件
  • 当Acceptor处理完成客户端连接事件之后(与客户端建立好Socket连接),MainReactor将连接分配给SubReactor。(即MainReactor只负责监听客户端连接请求,和客户端建立连接之后将连接交由SubReactor监听后面的IO事件)
  • SubReactor将连接加入到自己的连接队列进行监听,并创建Handler对各种事件进行处理
  • 当连接上有新事件发生的时候,SubReactor就会调用对应的Handler处理
  • Handler通过read从连接上读取请求数据,将请求数据分发给Worker线程池进行业务处理
  • Worker线程池会分配独立线程来完成真正的业务处理,并将处理结果返回给Handler。Handler通过send向客户端发送响应数据
  • 一个MainReactor可以对应多个SubReactor,即一个MainReactor线程可以对应多个SubReactor线程

优点:

  • MainReactor线程与SubReactor线程的数据交互简单职责明确,MainReactor线程只需要接收新连接,SubReactor线程完成后续的业务处理
  • MainReactor线程与SubReactor线程的数据交互简单,MainReactor线程只需要把新连接传给SubReactor线程,SubReactor线程无需返回数据
  • 多个SubReactor线程能够应对更高的并发请求

缺点:

  • 变成复杂度高

4.Netty线程模型

Netty的设计主要基于主从Reactor多线程模式,并做了一定的改进

1)简单版Netty模型

image.png

  • BossGroup线程维护Selector,ServerSocketChannel注册到这个Selector上,只关注连接建立请求事件(主Reactor)
  • 当接收到来自客户端的连接建立请求事件的时候,通过ServerSocketChannel.accept方法获得对应的SocketChannel,并封装成NioSocketChannel注册到WorkerGroup线程中的Selector,每个Selector运行在一个线程中(从Reactor)
  • 当WorkerGroup线程中的Selector监听到自己感兴趣的IO事件后,就调用Handler进行处理
2)进阶版Netty模型

image.png

  • 有两组线程池:BossGroup和WorkerGroup,BossGroup中的线程专门负责和客户端建立连接,WorkerGroup中的线程专门负责处理连接上的读写
  • BossGroup和WorkerGroup含有多个不断循环的执行事件处理的线程,每个线程都包含一个Selector,用于监听注册在其上的Channel
  • 每个BossGroup中的线程循环执行以下三个步骤
    • 轮询注册在其上的ServerSocketChannel的accept事件(OP_ACCEPT事件)
    • 处理accept事件,与客户端建立连接,生成一个NioSocketChannel,并将其注册到WorkerGroup中某个线程上的Selector上
    • 再去依次循环处理任务队列中的下一个事件
  • 每个WorkerGroupGroup中的线程循环执行以下三个步骤
    • 轮询注册在其上的NioSocketChannel的read/write事件(OP_READ/OP_WRITE事件)
    • 在对应的NioSocketChannel上处理的read/write事件
    • 再去依次循环处理任务队列中的下一个事件
3)详细版Netty模型

image.png

  • Netty抽象出两组线程池:BossGroup和WorkerGroup,也可以叫做BossNioEventLoopGroup和WorkerNioEventLoopGroup。每个线程池中都有NioEventLoop线程。BossGroup中的线程专门负责和客户端建立连接,WorkerGroup中的线程专门负责处理连接上的读写。BossGroup和WorkerGroup的类型都是NioEventLoopGroup
  • NioEventLoopGroup相当于一个事件循环组,这个组中含有多个事件循环,每个事件循环就是一个NioEventLoop
  • NioEventLoop表示一个不断循环的执行事件处理的线程,每个NioEventLoop都包含一个Selector,用于监听注册在其上的Socket网络连接(Channel)
  • NioEventLoopGroup可以含有多个线程,即可以含有多个NioEventLoop
  • 每个BossNioEventLoop中循环执行以下三个步骤
    • select:轮询注册在其上的ServerSocketChannel的accept事件(OP_ACCEPT事件)
    • processSelectedKeys:处理accept事件,与客户端建立连接,生成一个NioSocketChannel,并将其注册到某个WorkerNioEventLoop上的Selector上
    • runAllTasks:再去依次循环处理任务队列中的其他任务
  • 每个WorkerNioEventLoop中循环执行以下三个步骤
    • select:轮询注册在其上的NioSocketChannel的read/write事件(OP_READ/OP_WRITE事件)
    • processSelectedKeys:在对应的NioSocketChannel上处理read/write事件
    • runAllTasks:再去依次循环处理任务队列中的其他任务
  • 在以上两个processSelectedKeys步骤中,会使用Pipeline(管道),Pipeline中引用了Channel,即通过Pipeline可以获取到对应的Channel,Pipeline中维护了很多的处理器(拦截处理器、过滤处理器、自定义处理器等)

三、核心API介绍

1.ChannelHandler及其实现类

ChannelHandler接口定义了许多事件处理的方法,我们可以通过重写这些方法去实现具体的业务逻辑。
API关系如下图所示:
image.png

Netty开发中需要自定义一个Handler类去实现ChannelHandler接口或其子接口或其实现类,然后通过重写相应方法实现业务逻辑,我们接下来看看一般需要重写哪些方法

方法解释
public void channelActive(ChannelHandler Context ctx)通道就绪事件
public void channelRead(ChannelHandlerContext ctx,Object msg)通道读取数据事件
public void channelReadComplete(ChannelHandlerContext ctx)数据读取完毕事件
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause)通道发生异常事件

2.ChannelPipeline

ChannelPipeline是一个Handler的集合,它负责处理和拦截inbound或者outbound的事件和操作,相当于一个贯穿Netty的责任链。
image.png
如果客户端和服务器的Handler是一样的,消息从客户端到服务端或者反过来,每个Inbound类型或OutBound类型的Handler只会经过一次,混合类型的Handler(实现了Inbound和OutBound的Handler)会经过两次。准确的说ChannelPipeline中是一个ChannelHandlerContext,每个上下文对象中有ChannelHandler。InboundHandler是按照Pipeline的加载顺序的顺序执行,OutboundHandler是按照Pipeline的加载顺序,逆序执行。

3.ChannelHandlerContext

这是事件处理器上下文对象,Pipeline链中的实际处理节点。每个处理节点ChannelHandlerContext中包含一个具体的事件处理器ChannelHandler,同时ChannelHandlerContext中也绑定了对应的ChannelPipeline和Channel的信息,方便对ChannelHandler进行调用。常用方法如下所示:

  • ChannelFuture clos(),关闭通道
  • ChannelOutboundInvoker flush(),刷新
  • ChannelFuture writeAndFlush(Object msg),将数据写到ChannelPipeline中当前ChannelHandler的下一个ChannelHandler开始处理(出站)

4.ChannelOption

Netty在创建Channel实例后,一般都需要设置ChannelOption参数。ChannelOption是Socket的标准参数,而非Netty独创的。常用的参数配置有:

  • ChannelOption.SO_BACKLOG
    • 对应TCP/IP协议listen函数中的backlog参数,用来初始化服务器可连接队列大小。服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小
    • ChannelOption.SO_KEEPALIVE,一直保持连接活动状态。该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。

5.ChannelFuture

表示Channel中异步I/O操作的结果,在Netty中所有的I/O操作都是异步的,I/O的调用会直接返回,调用并不能立刻获得结果,但是可以通过ChannelFuture来获取I/O操作的处理状态。
常用方法如下所示:

  • Channel channel(),返回当前正在进行IO操作的通道
  • ChannelFuture sync(),等待异步操作执行完毕,将异步改为同步

6.EventLoopGroup和实现类NioEventLoopGroup

EventLoopGroup是一组EventLoop的抽象,Netty为了更好的利用多核CPU资源,一般会有多个EventLoop同时工作,每个EventLoop维护着一个Selector实例。
EventLoopGroup提供next接口,可以从组里面按照一定规则获取其中一个EventLoop来处理任务。在Netty服务器端编程中,我们一般都需要提供两个EventLoopGroup,例如:BossEventLoopGroup和WorkerEventLoopGroup。通常一个服务端口即一个ServerSocketChannel对应一个Selector和一个EventLoop线程。BossEventLoop负责接收客户端的连接并将SocketChannel交给WorkerEventLoopGroup来进行IO处理,如下图所示:
image.png
BossEventLoopGroup通常是一个单线程的EventLoop,EventLoop维护着一个注册了ServerSocketChannel的Selector实例,BossEventLoop不断轮询Selector将连接事件分离出来,通常是OP_ACCEPT事件,然后将接收到的SocketChannel交给WorkerEventLoopGroup,WorkerEventLoopGroup会由next选择其中一个EventLoopGroup来将这个SocketChannel注册到其维护的Selector并对其后续的IO事件进行处理。
一般情况下我们都是用实现类NioEventLoopGroup
常用方法如下所示:

  • public NioEventLoopGroup(),构造方法,创建线程组
  • public Future<?> shutdownGracefully(),断开连接,关闭线程

7.ServerBootstrap和Bootstrap

ServerBootStrap是Netty中的服务器端启动助手,通过它可以完成服务器端的各种配置;Bootstrap是Netty中的客户端启动助手,通过它可以完成客户端的各种配置。常用方法如下所示:

方法作用
public ServerBootstrap group(EventLoopGroup parentGroup,EventLoopGroup childGroup)该反复噶用于服务器端,用来设置两个EventLoop
public B group(EventLoopGroup group)该方法用于客户端,用来设置一个EventLoop
public B channel(Class<? extends C> channelClass)该方法用来设置一个服务器端的通道实现
public B option(ChannelOption option,T value)用来给ServerChannel添加配置
public ServerBootstrap childOption(ChnnelOption childOption,T value)用来给接收到的通道添加配置

8.Unpooled类

这是Netty提供的一个专门用来操作缓冲区的工具类,常用方法如下所示:

  • public static ByteBuf copiedBuffer(CharSequence string,Charset charset),通过给定的数据和字符编码返回一个ByteBuf对象(类似于NIO中的ByteBuffer对象)

四、Netty入门案例

Netty是由JBOSS提供的一个Java开源框架,所以在使用的时候首先得导入Netty的maven坐标

<dependency>
  <groupId>io.netty</groupId>
  <artifactId>netty-all</artifactId>
  <version>4.1.42.Final</version>
</dependency>

1.服务端

步骤:

  1. 创建bossGroup线程组:处理网络事件–连接事件
  2. 创建workerGroup线程组:处理网络事件–读写事件
  3. 创建服务端启动助手
  4. 设置bossGroup线程组和workerGroup线程组
  5. 设置服务端通道实现为NIO
  6. 参数设置
  7. 创建一个通道初始化对象
  8. 向Pipeline中添加自定义业务处理handler
  9. 启动服务端并绑定端口,同时将异步改为同步
  10. 关闭通道和关闭连接池
1)服务端代码
public class NettyServer {
    public static void main(String[] args) throws InterruptedException {
        //1. 创建bossGroup线程组:处理网络事件--连接事件
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        //2. 创建workerGroup线程组:处理网络事件--读写事件 2*处理器线程数
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        //3. 创建服务端启动助手
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        //4. 设置bossGroup线程组和workerGroup线程组
        serverBootstrap.group(bossGroup,workerGroup)
                .channel(NioServerSocketChannel.class)//5. 设置服务端通道实现为NIO
                .option(ChannelOption.SO_BACKLOG,128)//6. 参数设置
                .childOption(ChannelOption.SO_KEEPALIVE,Boolean.TRUE)//6. 参数设置
                .childHandler(new ChannelInitializer<SocketChannel>() {//7. 创建一个通道初始化对象
                    @Override
                    protected void initChannel(SocketChannel channel) throws Exception {
                        //8. 向Pipeline中添加自定义业务处理handler
                        channel.pipeline().addLast(new NettyServerHandler());
                    }
                });
        //9. 启动服务端并绑定端口,同时将异步改为同步
        ChannelFuture future = serverBootstrap.bind(9999).sync();
        System.out.println("服务端启动成功");
        // 10. 关闭通道(并不是真正意义上的关闭,而是监听通道关闭的状态)和关闭连接池
        future.channel().closeFuture().sync();
        bossGroup.shutdownGracefully();
        workerGroup.isShuttingDown();
    }
}
2)自定义服务端handler
public class NettyServerHandler implements ChannelInboundHandler {

    /**
     * @Author liufj
     * @Description 通道读取事件
     * @Date 10:05 2024/6/10
     **/
    @Override
    public void channelRead(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("客户端发送过来的消息:"+byteBuf);
    }
    /**
     * @Author liufj
     * @Description 通道读取完毕事件
     * @Date 10:05 2024/6/10
     **/
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("这里是Netty服务端", CharsetUtil.UTF_8));//消息出站
    }
    /**
     * @Author liufj
     * @Description 通道异常事件
     * @Date 10:05 2024/6/10
     **/
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
    @Override
    public void channelRegistered(ChannelHandlerContext channelHandlerContext) throws Exception {

    }
    @Override
    public void channelUnregistered(ChannelHandlerContext channelHandlerContext) throws Exception {

    }
    @Override
    public void channelActive(ChannelHandlerContext channelHandlerContext) throws Exception {

    }
    @Override
    public void channelInactive(ChannelHandlerContext channelHandlerContext) throws Exception {

    }

    @Override
    public void userEventTriggered(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {

    }
    @Override
    public void channelWritabilityChanged(ChannelHandlerContext channelHandlerContext) throws Exception {

    }
    @Override
    public void handlerAdded(ChannelHandlerContext channelHandlerContext) throws Exception {

    }
    @Override
    public void handlerRemoved(ChannelHandlerContext channelHandlerContext) throws Exception {

    }

}

2.客户端

步骤:

  1. 创建线程组
  2. 创建客户端启动助手
  3. 设置线程组
  4. 创建客户端通道实现为NIO
  5. 创建一个通道初始化对象
  6. 向Pipeline中添加自定义业务处理handler
  7. 启动客户端,等待连接服务端,同时将异步改为同步
  8. 关闭通道和关闭连接池
1)客户端代码
public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        //1. 创建线程组
        NioEventLoopGroup group = new NioEventLoopGroup();
        //2. 创建客户端启动助手
        Bootstrap bootstrap = new Bootstrap();
        //3. 设置线程组
        bootstrap.group(group)
                .channel(NioSocketChannel.class)//4. 创建客户端通道实现为NIO
                .handler(new ChannelInitializer<SocketChannel>() {//5. 创建一个通道初始化对象
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        //6. 向Pipeline中添加自定义业务处理handler
                        socketChannel.pipeline().addLast(new NettyClientHandler());
                    }
                });
        //7. 启动客户端,等待连接服务端,同时将异步改为同步
        ChannelFuture channelFuture = bootstrap.connect("127.0.0.1",9999).sync();
        //8. 关闭通道和关闭连接池
        channelFuture.channel().closeFuture().sync();
        group.shutdownGracefully();

    }
}
2)自定义客户端handler
public class NettyClientHandler implements ChannelInboundHandler {

    /**
     * @Author liufj
     * @Description 通道就绪事件
     * @Date 10:05 2024/6/10
     **/
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("这里是Netty客户端", CharsetUtil.UTF_8));
    }
    /**
     * @Author liufj
     * @Description 通道读就绪事件
     * @Date 10:05 2024/6/10
     **/
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("服务端发送的消息:"+byteBuf.toString(CharsetUtil.UTF_8));
    }
    @Override
    public void channelRegistered(ChannelHandlerContext channelHandlerContext) throws Exception {

    }
    @Override
    public void channelUnregistered(ChannelHandlerContext channelHandlerContext) throws Exception {

    }
    @Override
    public void channelInactive(ChannelHandlerContext channelHandlerContext) throws Exception {

    }
    @Override
    public void channelReadComplete(ChannelHandlerContext channelHandlerContext) throws Exception {

    }
    @Override
    public void userEventTriggered(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {

    }
    @Override
    public void channelWritabilityChanged(ChannelHandlerContext channelHandlerContext) throws Exception {

    }
    @Override
    public void handlerAdded(ChannelHandlerContext channelHandlerContext) throws Exception {

    }
    @Override
    public void handlerRemoved(ChannelHandlerContext channelHandlerContext) throws Exception {

    }
    @Override
    public void exceptionCaught(ChannelHandlerContext channelHandlerContext, Throwable throwable) throws Exception {

    }
}

五、Netty异步模型

1.基本介绍

异步的概念和同步相对。当一个线程过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者。
image.png
image.png
Netty中的I/O操作是异步的,包括Bind、Write、Connect等操作会简单的返回一个ChannelFuture。调用者并不能立刻获得结果,而是通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。Netty的异步模型是建立在future和callback之上的。callback就是回调。
Future的核心思想:假设一个方法fun,计算过程可能非常耗时,等待fun返回显然不合适。那么可以在调用fun的时候,立马返回一个Future,后续可以通过Future去监控fun的处理过程(即Future-Listener机制)

2.Future和Future-Listener

1)Future

表示异步的执行结果,可以通过它提供的方法来检测执行是否完成,ChannelFuture是他的一个子接口。ChannelFuture是一个接口,可以添加监听器,当监听的事件发生时,就会通知到监听器。
当Future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture来获取操作执行的状态,注册监听函数来执行完成后的操作。
常用的方法:

方法介绍
sync阻塞等待程序结果返回
isDone判断当前操作是否完成
isSuccess判断已完成的当前操作是否成功
getCause获取已完成的当前操作失败的原因
isCancelled判断已完成的当前操作是否被取消
addListener注册监听器,当操作已完成(isDone方法返回完成),将会通知指定的监听器;如果Future对象已完成,则通知知道的监听器
2)Future-Listener机制

给Future添加监听器,监听操作结果

在服务端代码中修改如下:

        //9. 启动服务端并绑定端口,使用异步
        ChannelFuture future = serverBootstrap.bind(9999);
        future.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                if (channelFuture.isSuccess()){
                    System.out.println("端口绑定成功");
                }else {
                    System.out.println("端口绑定失败");
                }
            }
        });

在客户端handler中修改如下:

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ChannelFuture future = ctx.writeAndFlush(Unpooled.copiedBuffer("这里是Netty客户端", CharsetUtil.UTF_8));
        future.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                if (channelFuture.isSuccess()){
                    System.out.println("数据发送成功");
                }else {
                    System.out.println("数据发送失败");
                }
            }
        });
    }
  • 7
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值