Netty通信流程及零拷贝原理

581 阅读8分钟

Netty 的零拷贝

第一、传统意义的拷贝

是在发送数据的时候,传统的实现方式是:

  1. File.read(bytes)
  2. Socket.send(bytes)

这种方式需要四次数据拷贝和四次上下文切换:

  1. 数据从磁盘读取到内核的read buffer
  2. 数据从内核缓冲区拷贝到用户缓冲区
  3. 数据从用户缓冲区拷贝到内核的socket buffer
  4. 数据从内核的socket buffer拷贝到网卡接口(硬件)的缓冲区
第二、零拷贝的概念

明显上面的第二步和第三步是没有必要的,通过java的FileChannel.transferTo方法,可以避免上面两次多余的拷贝(当然这需要底层操作系统支持)

  1. 调用transferTo,数据从文件由DMA引擎拷贝到内核read buffer
  2. 接着DMA从内核read buffer将数据拷贝到网卡接口buffer

上面的两次操作都不需要CPU参与,所以就达到了零拷贝。

第三、Netty中的零拷贝

主要体现在三个方面:

1、bytebuffer【字节缓冲区】

Netty发送和接收消息主要使用bytebuffer,bytebuffer使用对外内存(DirectMemory)直接进行Socket读写。

原因:如果使用传统的堆内存进行Socket读写,JVM会将堆内存buffer拷贝一份到直接内存中然后再写入socket,多了一次缓冲区的内存拷贝。DirectMemory中可以直接通过DMA【直接存储器访问 】发送到网卡接口

2、Composite Buffers

传统的ByteBuffer,如果需要将两个ByteBuffer中的数据组合到一起,我们需要首先创建一个size=size1+size2大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。但是使用Netty提供的组合ByteBuf,就可以避免这样的操作,因为CompositeByteBuf并没有真正将多个Buffer组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。

3、对于FileChannel.transferTo的使用

Netty中使用了FileChannel的transferTo方法,该方法依赖于操作系统实现零拷贝。

Netty 内部执行流程

Netty 核心组件及作用

1.Channel

Channel 接口是 Netty 对网络操作抽象类,它除了包括基本的 I/O 操作,如 bind()、connect()、read()、write() 等。

比较常用的Channel接口实现类是NioServerSocketChannel(服务端)和NioSocketChannel(客户端),这两个 Channel 可以和 BIO 编程模型中的ServerSocket以及Socket两个概念对应上。Netty 的 Channel 接口所提供的 API,大大地降低了直接使用 Socket 类的复杂性。

2.EventLoop

EventLoop 定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。

EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。

Channel 和 EventLoop 直接有啥联系呢?

Channel 为 Netty 网络操作(读写等操作)抽象类,EventLoop 负责处理注册到其上的Channel 处理 I/O 操作,两者配合参与 I/O 操作。

3.ChannelFuture

Netty 是异步非阻塞的,所有的 I/O 操作都为异步的。

因此,我们不能立刻得到操作是否执行成功,但是,你可以通过 ChannelFuture 接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。

并且,你还可以通过ChannelFuture 的 channel() 方法获取关联的Channel

另外,我们还可以通过 ChannelFuture 接口的 sync()方法让异步的操作变成同步的。

public interface ChannelFuture extends Future {

Channel channel();
 
ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> var1);
 
 ......
 
ChannelFuture sync() throws InterruptedException;

}

4.ChannelHandler 和 ChannelPipeline

ChannelHandler 是消息的具体处理器。他负责处理读写操作、客户端连接等事情。

ChannelPipeline 为 ChannelHandler 的链,提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API 。当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline。

我们可以在 ChannelPipeline 上通过 addLast() 方法添加一个或者多个ChannelHandler ,因为一个数据或者事件可能会被多个 Handler 处理。当一个 ChannelHandler 处理完之后就将数据交给下一个 ChannelHandler 。b.group(eventLoopGroup)

b.handler(new ChannelInitializer<SocketChannel>() {
 
                @Override
 
                protected void initChannel(SocketChannel ch) {
 
                    ch.pipeline().addLast(new NettyKryoDecoder(kryoSerializer, RpcResponse.class));
 
                    ch.pipeline().addLast(new NettyKryoEncoder(kryoSerializer, RpcRequest.class));
 
                    ch.pipeline().addLast(new KryoClientHandler());
 
                }
 
            });

服务端执行流程

// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
 
    EventLoopGroup workerGroup = new NioEventLoopGroup();
 
    try {
 
        //2.创建服务端启动引导/辅助类:ServerBootstrap
 
        ServerBootstrap b = new ServerBootstrap();
 
        //3.给引导类配置两大线程组,确定了线程模型
 
        b.group(bossGroup, workerGroup)
 
                // (非必备)打印日志
 
                .handler(new LoggingHandler(LogLevel.INFO))
 
                // 4.指定 IO 模型
 
                .channel(NioServerSocketChannel.class)
 
                .childHandler(new ChannelInitializer<SocketChannel>() {
 
                    @Override
 
                    public void initChannel(SocketChannel ch) {
 
                        ChannelPipeline p = ch.pipeline();
 
                        //5.可以自定义客户端消息的业务处理逻辑
 
                        p.addLast(new HelloServerHandler());
 
                    }
 
                });
 
        // 6.绑定端口,调用 sync 方法阻塞知道绑定完成
 
        ChannelFuture f = b.bind(port).sync();
 
        // 7.阻塞等待直到服务器Channel关闭(closeFuture()方法获取Channel 的CloseFuture对象,然后调用sync()方法)
 
        f.channel().closeFuture().sync();
 
    } finally {
 
        //8.优雅关闭相关线程组资源
 
        bossGroup.shutdownGracefully();
 
        workerGroup.shutdownGracefully();
 
    }

————————————————

1.首先你创建了两个 NioEventLoopGroup 对象实例:bossGroup 和 workerGroup。

bossGroup : 用于处理客户端的 TCP 连接请求。

workerGroup :负责每一条连接的具体读写数据的处理逻辑,真正负责 I/O 读写操作,交由对应的 Handler 处理。

举个例子:我们把公司的老板当做 bossGroup,员工当做 workerGroup,bossGroup 在外面接完活之后,扔给 workerGroup 去处理。一般情况下我们会指定 bossGroup 的 线程数为 1(并发连接量不大的时候) ,workGroup 的线程数量为CPU 核心数 *2。另外,根据源码来看,使用 NioEventLoopGroup 类的无参构造函数设置线程数量的默认值就是CPU 核心数 *2。

2.接下来 我们创建了一个服务端启动引导/辅助类:ServerBootstrap,这个类将引导我们进行服务端的启动工作。

3.通过 .group() 方法给引导类 ServerBootstrap 配置两大线程组,确定了线程模型。

通过下面的代码,我们实际配置的是多线程模型,这个在上面提到过。

EventLoopGroup bossGroup = new NioEventLoopGroup(1);

EventLoopGroup workerGroup = new NioEventLoopGroup();

4.通过channel()方法给引导类 ServerBootstrap指定了 IO 模型为NIO

NioServerSocketChannel :指定服务端的 IO 模型为 NIO,与 BIO 编程模型中的ServerSocket对应

NioSocketChannel : 指定客户端的 IO 模型为 NIO, 与 BIO 编程模型中的Socket对应5.通过 .childHandler()给引导类创建一个ChannelInitializer ,然后制定了服务端消息的业务处理逻辑 HelloServerHandler 对象6.调用 ServerBootstrap 类的 bind()方法绑定端口 ————————————————

1、channel :通道,相当于一个连接

2、channelHandler:通道的处理器,类似于处理器、拦截器这样的概念。请求过来之后,会一个一个的通过channelHandler来得到一个一个的处理,处理之后交给业务方法完成真正的处理,然后按照相反的顺序进行原路的返回

3、pipeline:管道。一个 pipeline是由多个channelHandler 构成的,构成管道的形式。请求过来的时候,会通过一个一个的处理器沿着管道不断的往前走

1、创建ServerBootStrap实例

2、设置并绑定Reactor线程池:EventLoopGroup,EventLoop就是处理所有注册到本线程的Selector上面的Channel

3、设置并绑定服务端的channel

4、5、创建处理网络事件的ChannelPipeline和handler,网络时间以流的形式在其中流转,handler完成多数的功能定制:比如编解码 SSl安全认证

6、绑定并启动监听端口

7、当轮训到准备就绪的channel后,由Reactor线程:NioEventLoop执行pipline中的方法,最终调度并执行channelHandler

8、说到这里顺便给大家推荐一个Java的交流学习社区:586446657,里面不仅可以交流讨论,还有面试经验分享以及免费的资料下载,包括Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。相信对于已经工作和遇到技术瓶颈的码友,在这里会有你需要的内容。 ————————————————

客户端执行流程

//1.创建一个 NioEventLoopGroup 对象实例

EventLoopGroup group = new NioEventLoopGroup();
 
    try {
 
        //2.创建客户端启动引导/辅助类:Bootstrap
 
        Bootstrap b = new Bootstrap();
 
        //3.指定线程组
 
        b.group(group)
 
                //4.指定 IO 模型
 
                .channel(NioSocketChannel.class)
 
                .handler(new ChannelInitializer<SocketChannel>() {
 
                    @Override
 
                    public void initChannel(SocketChannel ch) throws Exception {
 
                        ChannelPipeline p = ch.pipeline();
 
                        // 5.这里可以自定义消息的业务处理逻辑
 
                        p.addLast(new HelloClientHandler(message));
 
                    }
 
                });
 
        // 6.尝试建立连接
 
        ChannelFuture f = b.connect(host, port).sync();
 
        // 7.等待连接关闭(阻塞,直到Channel关闭)
 
        f.channel().closeFuture().sync();
 
    } finally {
 
        group.shutdownGracefully();
 
    }

————————————————

.创建一个 NioEventLoopGroup 对象实例

2.创建客户端启动的引导类是 Bootstrap

3.通过 .group() 方法给引导类 Bootstrap 配置一个线程组

4.通过channel()方法给引导类 Bootstrap指定了 IO 模型为NIO

5.通过 .childHandler()给引导类创建一个ChannelInitializer ,然后制定了客户端消息的业务处理逻辑 HelloClientHandler 对象

6.调用 Bootstrap 类的 connect()方法进行连接,这个方法需要指定两个参数:

inetHost : ip 地址

inetPort : 端口号

public ChannelFuture connect(String inetHost, int inetPort) {

return this.connect(InetSocketAddress.createUnresolved(inetHost, inetPort));
 
}
 
public ChannelFuture connect(SocketAddress remoteAddress) {
 
    ObjectUtil.checkNotNull(remoteAddress, "remoteAddress");
 
    this.validate();
 
    return this.doResolveAndConnect(remoteAddress, this.config.localAddress());
 
}

connect 方法返回的是一个 Future 类型的对象

public interface ChannelFuture extends Future {

......

} 也就是说这个方是异步的,我们通过 addListener 方法可以监听到连接是否成功,进而打印出连接信息。具体做法很简单,只需要对代码进行以下改动:

ChannelFuture f = b.connect(host, port).addListener(future -> {

if (future.isSuccess()) {

System.out.println("连接成功!");

} else {

System.err.println("连接失败!");

}

}).sync(); ————————————————

1、创建ServerBootStrap实例

2.创建处理客户端连接Reactor线程组

3.创建客户端连接的NioSocketChannel

4.创建pipeline和channelHadler

5.异步发起TCP连接并判断是否成功