Netty基础学习Day2

206 阅读14分钟

资料cv的原文链接:blog.csdn.net/u014494148/…

Netty的介绍

下面是从Netty官网趴下来的对Netty的定义

Netty是由JBOSS提供的一个java开源框架,Netty 是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。Netty 是一个 NIO 客户端服务器框架,它可以快速轻松地开发网络应用程序,例如协议服务器和客户端。它极大地简化和精简了 TCP 和 UDP 套接字服务器等网络编程。

简单理解:Netty是基于NIO(Nonblocking I/O,非阻塞IO)实现的网络通信框架,相比传统IO他的并发性能得到了很大提高,它在NIO基础上封装了NIO的使用细节,让网络编程变得更加高效简洁,大大简化了NIO的开发过程。

Netty拥有高性能、高吞吐量、低延迟、消耗资源少;零拷贝的特点,同时支持SSL/TLS 和 StartTLS ,社区活跃、版本迭代周期短。

Netty是互联网最流行的NIO框架,广泛应用于互联网领域,游戏领域,大数据领域,比较知名的ElasticSearch,Dubbo等技术都用到了Netty。

1. Reactor线程模型

Reactor(也叫Dispacher)线程模型不再为每个请求创建线程 ,而是基于 I/O 复用模型,多个请求连接同一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当请求中有数据,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理,一个线程可以处理多个连接的业务,如图: img

  • Reactor:即图中的ServiceHandler,Reactor负责监听和事件分发,分发给适当的处理程序来对 IO 事件做出反应, 在单独的线程中执行。就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人;
  • Handlers:Reactor监听到IO事件,需要调度应用程序来处理即:Handler,Handler是真正处理 I/O 事件的程序,类似于接电话的实际的人员。

简单理解就是Reactor线程模型有两个角色 ,一个负责接待请求(Reactor), 一个负责处理请求(Handlers)

另外Reactor有三种模式 :

  1. Reactor 单线程模型
  2. 单Reactor 多线程模型
  3. Reactor主从多线程模型

1️⃣Reactor单线程

Netty框架是基于主从Reactor多线程模型进行改进(多个主Reactor)。

img

工作流程

Reactor单线程模型工作流程如下

  1. Reactor 通过 Select 监控客户端请求,收到事件后通过 Dispatch 进行请求分发

  2. 如果是请求事件是建立连接,则由 Acceptor 通过 accept 处理连接请求,然后创建一个 Handler。

  3. 如果不是连接事件,则 Reactor 会分发调用连接对应的 Handler 来处理请求

  4. Handler 会完成数据read,进行业务处理最后通过 Send将响应结果返回给Client

问题

Reactor单线程模型优点是模型简单,缺点也是比较明显

  1. 线程单一,如果并发高,业务处理慢,很容易导致性能瓶颈,
  2. 如果线程故障或终止,整个系统不可用

所以这种模型不太适合高并发场景,和业务处理比较耗时的场景。

2️⃣单Reactor多线程模型

img

工作流程

Reactor多线程模式工作流程如下

  1. Reactor 通过 Select 监控客户端请求,收到事件后通过 Dispatch 进行请求分发
  2. **如果是请求事件是建立连接,**则由 Acceptor 通过 accept 处理连接请求,然后创建一个 Handler 。
  3. **如果不是连接事件,**则 Reactor 会分发调用连接对应的 Handler 来处理请求
  4. Handler 只负责响应事件,不做具体业务处理,通过 Read 读取数据后,会分发给后面的 Worker 线程池进行业务处理
  5. Worker 线程池会分配独立的线程完成真正的业务处理,将响应结果发给 Handler 进行处理。
  6. Handler 收到响应结果后通过 Send 将响应结果返回给 Client。

问题

  • 这种模型引入了线程池,Reactor线程负责接收连接和响应事件,具体IO事件交给线程池分配的线程处理。
  • 优点是可以充分利用CPU,提供整体性能
  • 但是Reactor承担所有的事件监听和分发,容易成为性能瓶颈,多线程的数据共享也是一个问题。

3️⃣主从Reactor多线程

img

工作流程

  1. Reactor 主线程 MainReactor 对象通过 Select 监听连接事件,收到事件后通过 Acceptor 接收,处理建立连接事件。
  2. **Acceptor 处理建立连接事件后,**MainReactor 将连接分配 Reactor 子线程给 SubReactor 进行处理。
  3. SubReactor 将连接加入连接队列进行监听,并创建一个 Handler 用于处理各种连接事件,
  4. 当有新的事件发生时,SubReactor 会调用连接对应的 Handler 处理
  5. Handler 通过 Read 读取数据后,会分发给后面的 Worker 线程池进行业务处理
  6. Worker 线程池会分配独立的线程完成真正的业务处理,将响应结果发给 Handler 进行处理
  7. Handler 收到响应结果后通过 Send 将响应结果返回给 Client

好处

这种模式的优点比较明显,Reactor主线程只需要接收新连接,子线程完成后续的业务处理,Reactor的压力得到了分担。

这种模型在许多项目中广泛使用,包括 Nginx 主从 Reactor 多进程模型,Memcached 主从多线程,Netty 主从多线程模型的支持。

2. Netty的线程模型

Netty是基于主从Reactor多线程模型实现,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果

img

Netty线程模型中有两个Group:

Boss Group:用于Accetpt连接建立事件并分发请求

Worker Group:用于处理I/O读写事件和业务逻辑。

2.1 关键组件

  • Channel

Netty网络通信的组件,能够用于执行网络IO操作。

  • Selector

Netty基于Selector对象实现I/O多路复用,即:一个线程可以监听多个连接的Channel事件, 当channel向Selector注册后,Selector 就可以自动不断地查询(select) 注册的Channel是否有已就绪的I/O事件(例如可读, 可写, 网络连接完成等)。

  • NioEventLoop

NioEventLoop表示一个不断循环的执行处理任务的线程, 每个NioEventLoop 都有一个 selector。用于监听注册的Channel的IO事件。

NioEventLoop中维护一个线程和任务队列,支持异步提交任务,线程启动时会调用NioEventLoop的run方法,执行IO和非IO任务。 其中IO任务由prossSelectedKeys触发,非IO任务由runAlltasks触发。

  • NioEventLoopGroup

NioEventLoopGroup用来管理EventLoop,可以理解为线程组,内部维护了一组线程

2.2 工作流程

  1. Boss Group里面的NioEventLoop会轮询accept事件,遇到有新的连接,就生成NioSocketChannel,并把这个Channel注册到Worker Group的Selector上
  2. WorkerGroup轮询read和write事件,在可读或者可写条件满足时,就进行处理

Worker GroupBoss Group都是通过里面的NioEventLoop来操作的。NioEventLoop中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用NioEventLoop的run方法

每个Boss NioEventLoop循环执行的步骤有3步:

  1. 轮询检查accept事件

  2. 处理accept事件,与client建立连接,生成NioSocketChannel,并将其注册到某个 Worker NIOEventLoop上的 selector,这样Channel的IO事件就交给WorkGroup去监听了。

  3. 最后执行一个runAllTasks方法,用于处理任务队列中的任务

每个 Worker NIOEventLoop 循环执行的步骤:

  1. 轮询read, write 事件
  2. 处理 I/O 事件, 即 read, write 事件, 再对应NioSocketChannel处理
  3. 最后执行一个runAllTasks方法,用于处理任务队列中的任务

入门案例

我们来写一个简单的服务端和客户端通信的案例

img

我们需要有GoosGroup来循环监听请求事件,需要有WorkGroup来处理事件,而这两个角色都通过来就NioEventLoopGroup来进行事件监听,我们还需要创建事件处理器ChannelHandler,通过 Channel的ChannelPipeline把ChannelHandler进行关联。

引入依赖

首先创建Maven项目,导入Maven依赖

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

服务端代码

对于服务端我们要做如下事情

  1. 创建两个NioEventLoopGroup ,一个作为BossGroup ,一个作为 WorkGroup
  2. 创建 ServerBootstrap 加入两个NioEventLoopGroup
  3. 通过 ServerBootstrap 添加Handler ,需要自己创建Handler
  4. 编写Handler ,通过继承ChannelInboundHandlerAdapter 来对不同的事件做不同的处理

Server

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

import java.net.InetSocketAddress;

public class NettyServer {
    public static void main(String[] args) throws InterruptedException {

        //创建Boss Group :负责处理链接
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();

        //创建 workGroup :负责处理读写事件
        NioEventLoopGroup workGroup = new NioEventLoopGroup();

        //创建服务端启动对象
        ServerBootstrap bootstrap = new ServerBootstrap();

        try{
            //把 bossGroup 和 workGroup 加入启动对象
            bootstrap
                    .group(bossGroup,workGroup)
                    //保存活动链接状态,启用该功能时,TCP会主动探测空闲连接的有效性
                    .childOption(ChannelOption.SO_KEEPALIVE,true)
                    //多个客户端过来,处理不过来的请求在队列排队,指定存放请求的队列的大小
                    //ChannelOption.SO_BACKLOG:服务端处理客户端连接请求是顺序处理的,所以同一事件只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小
                    //ChannelOption.SO_KEEPALIVE:当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。
                    .option(ChannelOption.SO_BACKLOG,64)
                    //指定服务器使用什么样的通道
                    .channel(NioServerSocketChannel.class)
                    //添加事件处理器Handler
                    .childHandler(new ChannelInitializer<SocketChannel>(){

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //把处理器添加进去 , 每个channel都对应一个pieline
                            //pieline通道和Handler进行绑定,通过Pieline可以获取到Handler
                            ch.pipeline().addLast(new MyServerHandler());
                        }
                    });

            //绑定端口,开始接收客户端请求
            //bind方法中会创建 Channel,然后使用 EventLoop注册Channel 
            ChannelFuture future = bootstrap.bind(new InetSocketAddress("127.0.0.1", 6000)).sync();

            //对关闭通道进行监听
            //当此通道关闭时将收到通知
            future.channel().closeFuture().sync();

        }catch (Exception e){
            e.printStackTrace();
        }finally {
          	//关闭资源
            workGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }

    }
}

创建handler

这个Handler是用来处理客户端channel的IO事件

//这个Handler是用来处理客户端channel的IO事件
public class MyServerHandler extends ChannelInboundHandlerAdapter {

    //处理读事件 ChannelHandlerContext :上下文,含有通道,pipline ,地址 ;  msg就是数据
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buffer = (ByteBuf) msg;
        System.out.println("MyServerHandler,接收到:"+ctx.channel().remoteAddress()+"发来的消息:"+buffer.toString(CharsetUtil.UTF_8));
    }

    //数据读取完毕
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //给客户端恢复消息
        ctx.writeAndFlush(Unpooled.copiedBuffer("你好,我收到了消息",CharsetUtil.UTF_8));
    }
}

客户端代码

客户端要做如下事情

  1. 创建 NioEventLoopGroup 用于监听服务端的事件
  2. 创建 Bootstrap启动对象 ,加入 NioEventLoopGroup
  3. 一样要通过 Bootstrap 添加Handler ,需要自己创建Handler
  4. 编写Handler ,通过继承ChannelInboundHandlerAdapter 来对不同的事件做不同的处理

Client

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

import java.net.InetSocketAddress;

public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        //客户端线程循环组
        NioEventLoopGroup eventExecutors = new NioEventLoopGroup();

        //客户端启动对象
        Bootstrap bootstrap = new Bootstrap();

        try {
            bootstrap.group(eventExecutors)
                    //指定客户端使用的通道类型
                    .channel(NioSocketChannel.class)
                    //添加客户端处理器
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //添加客户端处理器
                            ch.pipeline().addLast(new MyClientHandler());
                        }
                    });
            //链接服务端
            ChannelFuture channelFuture = bootstrap
                    .connect(new InetSocketAddress("127.0.0.1", 6000)).sync();
            //监听关闭事件
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            eventExecutors.shutdownGracefully();
        }
    }
}

创建handler

这个Handler是用来处理客户端channel的IO事件

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

//这个Handler是用来处理客户端channel的IO事件
public class MyClientHandler extends ChannelInboundHandlerAdapter {
                                // ChannelInboundHandlerAdapter :这个是为Channel通道绑定处理事件的Handler,用来接收请求。
    //链接激活
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // 给服务端发送消息
        // Unpooled.copiedBuffer :工具,把数据拷贝到ByteBuf
        ctx.writeAndFlush(Unpooled.copiedBuffer("你好服务端", CharsetUtil.UTF_8));
    }

    //处理读事件 ChannelHandlerContext :上下文,含有通道,pipline ,地址 ;  msg就是数据
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // ByteBuf :Netty提供的byte缓冲区
        ByteBuf buffer = (ByteBuf) msg;
        System.out.println("MyClientHandler,接收到:"+ctx.channel().remoteAddress()+"发来的消息:"+buffer.toString(CharsetUtil.UTF_8));
    }

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

结果

image-20220712112043405

Netty核心类的认识

img

① NioEventLoopGroup

NioEventLoopGroup 继承于 EventLoopGroup ,代表Nio事件循环组,用来循环监听注册的Channel的IO事件

如上图,我们需要两个NioEventLoopGroup,一个为BossGroup做事件监听 ,一个为 WorkGroup做事件监听

② ServerBootstrap

ServerBootstrap是Netty服务端启动引导对象 ,Bootstrap是客户端的启动引导对象,作用都差不多,主要是用来配置整个netty程序,整合各个组件。

  • ServerBootstrap 常用方法如下:

    • group(EventLoopGroup parentGroup, EventLoopGroup childGroup) : 该方法用于服务端,主要是为 BossGroup(Acceptor) 设置 EventLoopGroup 来监听连接事件,以及为 WorkGroup 设置EventLoopGroup来监听读写事件。

    • channel(Class<? extends C> channelClass) : 指定用来创建channel通道实例的class

    • option(ChannelOption option, T value): 方法的作用是用来给ServerChannel添加配置项

    • childOption(ChannelOption childOption, T value) : 为请求的channel添加配置项

    • handler(ChannelHandler handler) : 给BossGrop添加Handler

    • childHandler(ChannelHandler childHandler) : 给WorkGroup添加Handler

    • bind(InetAddress inetHost, int inetPort) : 为服务端绑定监听的地址和端口

  • Bootstrap 常用方法如下

    • connect(InetAddress inetHost, int inetPort) :用来连接服务端

    • group(EventLoopGroup group) : 为客户端添加EventLoopGroup事件循环组。

③ Future,ChannelFuture

由于Netty的IO是异步的,不能立即得到消息是否被处理完成,所以需要通过Future

ChannelFuture注册事件监听,当操作完成后会触发相应的事件,从而对事件结果做出相应的处理。

④ Channel

通道,用来关联IO操作和处理的Handler,不同的协议有不同的Channel

  • NioServerSocketChannel : 服务端的TCP Socket 连接
  • NioSocketChannel :客户端的TCP Socket连接

⑤ ChannelHandler

ChannelHandler是用来处理IO事件的Handler,Handler实际上分为两种,Inbound 入站和Outbound出站

ChannelHandler比较常用的有如下两个子类:

  • ChannelInboundHandlerAdapter

    • 该适配器 实现了ChannelInboundHandler 接口,ChannelInboundHandler 又实现了ChannelHandler接口,主要是用来处理IO入站事件
  • ChannelOutboundHandlerAdapter

    • 适配器 实现了ChannelOutboundHandler,ChannelOutboundHandler又实现了 ChannelHandler接口,它主要是用处理IO出站事件

二者的结合体体——ChannelDuplexHandler

ChannelDuplexHandler :双通道处理器,它同时实现了ChannelInboundHandler 和 ChannelOutboundHandler ,既可以处理IO入站事件,也可以处理IO出站事件。

继承图:

img

⑥ ChannelPipeline

对于每个新的通道Channel,都会创建一个新的ChannelPipeline,并将器pipeline附加到channel中。

可以把ChannelPipeline看成是一个ChandlerHandler的链表,当有事件发生的时候,Pipeline负责依次调用每一个Handler进行处理或截获通道的接收和发送数据。

ChannelPipeline 通过 ChannelHandlerContext来管理ChannelHandler。 img

下面是端点后的 ChannelPipeline的调用链截图

img

通过Channel可以获取到ChannelPipeline,通过ChannelPipeline也可以获取到Channel

ChannelPipeline是ChandlerHandler形成的双向链表 ,ChandlerHandler中包含了真正的Handler , 上图的Head是头节点,tail是尾节点 。 如下图:

img

所以整个IO事件的处理是通过 Channel ,ChannelPipeline 和 Handler 三者一起来完成的。这里要注意的是,如果事件运动方向是从客户端到服务端,数据会通过pipeline中的一些列的OutboundHandler进行处理,事件被称之为出站,反之就是入站。两种类型的Handller相互不影响

⑦ Unpooled

Unpooled是Netty提供的buffer缓冲工具类,可以根据给定的数据和编码创建一个ByteBuf,如 ByteBuf buffer = Unpooled.buffer(1024);

ByteBuf类似于NIO中的ByteBuffer 。区别在于ByteBuffer需要调用flip()读写转换,而ByteBuf无需转换。

和ByteBuffer一样,ByteBuf中维护了一个byte[]来存储数据,同时还维护了一个writerIndex和readerIndex,当执行 buffer.writeByte 写操作writerIndex就会向后移动,指向下一个存储位置。当执行 buffer.readByte()操作的时候readerIndex就会向后移动指向下一个读的位置。当readerIndex等于writerIndex,ByteBuf中的数据读取完成。

基于Netty的Http服务器

资料来源:

blog.csdn.net/u014494148/…