Netty简介

目录

Netty是什么

Netty中的技术

为什么出现Netty

技术背景

I/O模型

BIO

NIO

AIO

零拷贝技术

线程模型介绍

传统阻塞式I/O服务模型

Reactor模型

单Reactor单线程模型

单Reactor多线程

主从Reactor多线程

Netty线程模型



Netty是什么

Netty中的技术


1. 一个Java开源框架,现为Github的独立项目
2. 一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络 IO 程序。
3. 主要针对TCP协议下,面向Clients端的高并发应用,或者P2P场景下的大量数据持续传输的应用。
4. 本质是一个NIO框架,适用于服务器通讯相关的多种应用场景。

为什么出现Netty


其实Java是有自己的原生NIO库的,不过存在以下问题

1)NIO 的类库和 API 繁杂,使用麻烦:需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。

2)需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序。

3)开发工作量和难度都非常大:例如客户端面临**断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理**等等。

4)JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。直到 JDK 1.7 版本该问题仍旧存在,没有被根本解决。

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

技术背景

I/O模型

        Java中的IO模型:BIO、NIO、AIO。

BIO

其中BIO就是同步阻塞I/O模式。

  1. 服务器启动一个ServerSocket
  2. 客户端启动Socket对服务器进行通信,默认情况下服务器需要对每个客户建立一个线程与之通信。
  3. 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝。
  4. 如果有响应,客户端线程会等待请求结束后,再继续进行。

简答的Java demo如下:

public static void main(String[] args) throws IOException {
        //线程池机制
        
        /*
        思路:
        1.创建一个线程池
        2.如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
         */

        //线程池
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        
        //创建一个ServerSocket
        ServerSocket serverSocket = new ServerSocket(6666);
        while(true){
            //监听,等待客户端连接
            final Socket socket = serverSocket.accept();//阻塞
            System.out.println("s连接到一个客户端");
            //启动一个线程,与之通信
            newCachedThreadPool.execute(()->{
                //可以和客户端通信
                handler(socket);
            });
            
            
        }
        
    }
//连接后的处理

public static void handler(Socket socket){
        try {
            byte[] bytes = new byte[1024];
            //通过socket 获取一个输入流
            InputStream inputStream = socket.getInputStream();

            while(true){
                int read = inputStream.read(bytes);//阻塞
                if(read!=-1){
                    System.out.println(new String(bytes,0,read));//输出客户端发送的数据
                }else{
                    break;
                }

            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            System.out.println("关闭和client的连接");
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

        明显BIO有着许多问题。

1)每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write 。

2)当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。

3)连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费

NIO

        于是就出现了NIO(同步非阻塞IO)。Java中的NIO有三大核心部分:Channel、Buffer、Selector(如下图)。NIO是面向缓冲区的,或者说是面向块编程的——数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。

  • 每个Channel都会对应一个Buffer
  • Selector对应一个线程,一个线程对应多个Channel。
  • 上图也反映了有3个Channel注册到该Selector中去。
  • 程序切换到哪个Channel是由事件决定的,基于此,Event就是一个重要的概念。
  • Selector会根据不同的事件,在各个通道上切换。
  • Buffer就是一个内存块,底层是一个数组。
  • 数据的读取和写入是通过Buffer,这个和BIO对比:BIO中要么是输入流,要么是输出流,不能双向,但是NIO中的Buffer是可以读也可以写的,需要flip方法转换。
  • Channel是双向的,可以返回底层操作系统的情况,比如linux,底层操作系统通道就是双向的。

比较:

NIO与BIO比较
处理方式是否阻塞选择器
BIO流(字节流和字符流)阻塞
NIO块(Channel和Buffer)非阻塞Selector监听多个Channel的事件

        关于Selector:Selector 能够检测多个注册的通道上是否有事件发生 (注意多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

通过SelectionKey。

  1. 当客户端连接时,会通过ServerSocketChannel得到SocketChannel。
  2. Selector进行监听select方法,返回有事件发生的Channel的个数。
  3. 将SocketChannel注册到Selector上(register),一个selector上可以注册多个SocketChannel。
  4. 注册后返回一个SelectionKey,会和该Selector关联。
  5. 这样在有事件发生时能得到各个SelectionKey。
  6. 通过SelectionKey能反向获取SocketChannel(方法channel)。
  7. 可以通过得到的channel进行业务处理。

代码demo:

服务器端:

public class NIOServer {
    public static void main(String[] args) throws IOException {
        //创建ServerSocketChannel ->ServerSocket

        ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();

        //得到一个Selector对象
        Selector selector = Selector.open();

        //绑定一个端口6666,在服务器端监听

        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //设置为非阻塞
        serverSocketChannel.configureBlocking(false);

        //把 serverSocketChannel 注册到 selector 关心 事件为 OP_ACCEPT
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //循环等待客户端连接
        while(true){

            //这里我们等待1s,如果没有事件发生(连接事件)
            if(selector.select(1000)==0){ //没有任何事件发生
                System.out.println("服务器等待了1s,无连接");
                continue;
            }

            //如果返回的>0,就获取到相关的selectionKeys集合
            //1.如果返回的>0,表示已经获取到关注的事件
            //2.selector.selectedKeys() 返回关注事件的集合
            // 通过selectedKeys 反向获取通道
            Set<SelectionKey> selectionKeys = selector.selectedKeys();

            //遍历集合  Set<SelectionKey>,使用迭代器
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();

            while(keyIterator.hasNext()){
                //获取到SelectionKey
                SelectionKey key = keyIterator.next();
                //根据key 对应的通道发生的事件做相应的处理
                if(key.isAcceptable()){//有新的客户端来连接,有新的客户端来连接
                    //给该客户端生成一个SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();

                    System.out.println("客户端连接成功 生成了一个socketChannel "+socketChannel.hashCode());

                    //将socketChannel设置为非阻塞
                    socketChannel.configureBlocking(false);

                    //将当前socketChannel 注册到selector,关注事件为OP_READ,同时给该socketChannel
                    //关联一个Buffer
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));

                }
                if(key.isReadable()){ //发生 OP_READ
                    //通过key 反向获取对应channel
                    SocketChannel channel =(SocketChannel) key.channel();
                    //获取到该channel关联的buffer
                    ByteBuffer buffer =(ByteBuffer) key.attachment();

                    channel.read(buffer);//读到channel
                    System.out.println("from 客户端 "+ new String(buffer.array()));
                }

                //手动从集合中移除当前的selectionKey,防止重复操作。
                keyIterator.remove();
            }
        }
    }
}

        可以看出服务器端在一个大循环里面会轮询目前的selectionKeys集合,通过检查每一个key的状态(acceptable或readable)来判断现在是连接是连接事件或读写事件。

客户端


public class NIOClient {
    public static void main(String[] args) throws IOException {

        //得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        //设置非阻塞模式
        socketChannel.configureBlocking(false);
        //提供服务器端的ip和端口
        InetSocketAddress inetSocketAddress =
                new InetSocketAddress("localhost", 6666);

        //连接服务器
        if(!socketChannel.connect(inetSocketAddress)){

            while(!socketChannel.finishConnect()){
                System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作");
            }
        }

        //...如果连接成功,就发送数据
        String str="hello,尚硅谷~";
        //wraps a byte array into a buffer
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        //发送数据,将buffer写入channel
        socketChannel.write(buffer);
        System.in.read();

    }
}

AIO

        JDK7引入了AIO(异步IO模型),在进行 I/O 编程中,常用到两种模式:Reactor和 Proactor。Java 的 NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理。叫做异步不阻塞的 IO。AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。目前AIO还没有广泛应用。

零拷贝技术

        零拷贝是网络编程的技术,很多性能优化都离不开这个技术点。在Java程序中,常用的零拷贝有mmap(内存映射)sendFile。

  • mmap:通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,可以减少内核空间到用户空间的拷贝次数。

  • sendFile:Linux 2.1 版本 提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换Linux 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。

即从操作系统来看,零拷贝就是没有CPU拷贝。

线程模型介绍

        不同的线程模型,对程序的性能有很大影响。

目前常用的线程模型有:

  1. 传统阻塞I/O线程模型
  2. Reactor模型

        而Reactor线程模型根据数量和资源池的数量不同,有3种典型的实现。

  • 单Reactor单线程
  • 单Reactor多线程
  • 主从Reactor多线程

传统阻塞式I/O服务模型

模型的特点:

  1. 采用阻塞式I/O模式获取输入的数据
  2. 每个连接都需要独立的线程完成数据的输入,业务处理,数据返回。

问题:

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

因此出现了Reactor线程模型

Reactor模型

  1. 基于I/O复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象中等待,无需阻塞等待所有连接。当某个连接有新的数据需要处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
  2. 基于线程池复用线程资源:不必再为每个连接创建独立的线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务。

整体思路如下图:

        Reactor模型中的核心组成:

  1. Reactor:Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序对I/O事件作出反应。它就像公司的电话接线员,接听来自客户的电话并将线路转移到适当的联系人。
  2. Handlers:处理程序执行I/O事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作。

单Reactor单线程模型

        该模型的大致思路如下:

  1. Select 是前面 I/O 复用模型介绍的标准网络编程 API,可以实现应用程序通过一个阻塞对象监听多路连接请求。
  2. Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发。
  3. 如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务处理。
  4. 如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应。
  5. Handler 会完成 Read→业务处理→Send 的完整业务流程。

优缺点比较:

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

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

使用场景:客户端的数量有限,业务处理非常快速,比如 Redis在业务处理的时间复杂度 O(1) 的情况。

单Reactor多线程

        具体业务会分发给线程池来处理。

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

优缺点比较:

优点:可以充分的利用多核cpu 的处理能力。

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

主从Reactor多线程

        三级模型:主Reactor线程只处理连接,I/O read/send分配给其他多个SubReactor中,然后其中的业务处理又分给各自的Worker线程池中。

  1. Reactor主线程 MainReactor 对象通过select 监听连接事件, 收到事件后,通过Acceptor 处理连接事件。
  2. 当 Acceptor 处理连接事件后,MainReactor 将连接分配给SubReactor线程。
  3. subreactor 将连接加入到连接队列进行监听,并创建handler进行各种事件处理。
  4. 当有新事件发生时, subreactor 就会调用对应的handler处理。
  5. handler 通过read 读取数据,分发给后面的worker 线程处理。
  6. worker 线程池分配独立的worker 线程进行业务处理,并返回结果。
  7. handler 收到响应的结果后,再通过send 将结果返回给client。
  8. Reactor 主线程可以对应多个Reactor 子线程, 即MainRecator 可以关联多个SubReactor。

优缺点:

优点:父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理;父线程与子线程的数据交互简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据。

缺点:编程复杂度较高。

Reactor模式具有的优点:

  1. 响应快,不必为单个同步时间所阻塞,虽然 Reactor 本身依然是同步的。
  2. 可以最大程度地避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销。
  3. 扩展性好,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源。
  4. 复用性好,Reactor 模型本身与具体事件处理逻辑无关,具有很高的复用性。

Netty线程模型

基础版:


  1. BossGroup线程维护Selector,只关注Accept。
  2. 当收到Accept事件,获取到对应的SocketChannel,封装成NioSocketChannel并注册到Woker线程进行维护。
  3. 当Worker线程监听到selector中通道发生自己感兴趣的事件后,就进行处理(handler),这里handler已经加入通道。

详细版(重要):

1)Netty抽象出两组线程池 BossGroup 专门负责接收客户端的连接, WorkerGroup 专门负责网络的读写

2)BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup

3)NioEventLoopGroup 相当于一个事件循环组, 这个组中含有多个事件循环 ,每一个事件循环是 NioEventLoop

4)NioEventLoop 表示一个不断循环的执行处理任务的线程, 每个NioEventLoop 都有一个selector , 用于监听绑定在其上的socket的网络通讯

5)NioEventLoopGroup 可以有多个线程, 即可以含有多个NioEventLoop

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

  • 轮询accept 事件
  • 处理accept 事件 , 与client建立连接 , 生成NioScocketChannel , 并将其注册到某个worker NIOEventLoop 上的 selector
  • 处理任务队列的任务 , 即 runAllTasks

7) 每个 Worker NIOEventLoop 循环执行的步骤

  • 轮询read, write 事件
  • 处理i/o事件, 即read , write 事件,在对应NioScocketChannel 处理
  • 处理任务队列的任务 , 即 runAllTasks

8) 每个Worker NIOEventLoop 处理业务时,会使用pipeline(管道), pipeline 中包含了 channel , 即通过pipeline 可以获取到对应通道, 管道中维护了很多的 处理器(handler)

        简介在此就结束,接下来我将就Netty模型就从demo代码开始深入源码剖析Netty模型的方方方面面,大家稍等哈。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值