• Netty学习笔记


    Netty 学习笔记

    一、Netty介绍和应用场景

    1、Netty介绍

    • Netty是由JBoss提供的一个java开源框架。
    • Netty是一个异步的基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络IO程序。
    • Netty主要针对在TCP协议下,面向Clients端的高并发应用,或者Peer-to-Peer场景下的大量数据持续传输的应用。
    • Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景。要透彻理解Netty ,需要先学习NIO。

    2、Netty的应用场景

    1)互联网行业
    • 在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些RPC框架使用。
    • 典型应用:阿里分布式服务框架Dubbo的 RPC框架使用Dubbo协议进行节点间通信,Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。
    2)游戏行业
    • 无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用。
    • Netty作为高性能的基础通信组件,提供了TCP/UDP和 HITP协议栈,方便定制和开发私有协议栈,账号登录服务器。
    • 地图服务器之间可以方便的通过Netty进行高性能的通信。
    3)大数据领域
    • 经典的Hadoop的高性能通信和序列化组件Avro的RPC框架,默认采用Netty进行跨界点通信。
    • Avro的Netty Service基于Netty框架二次封装实现。
    4)其他开源项目
    • Akka、Flink、Spark等。

    二、IO模型

    1、基本说明

    • I/O模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。
    • Java共支持3种网络编程模型(IO模式):BIO、NIO、AIO。

    2、Java BIO

    • BIO:同步并阻塞(传统阻塞型),以流的方式处理数据。服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。

    (1)适用场景

    • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。

    3、Java NIO

    • NIO:同步非阻塞,以块的方式处理数据。服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。

      在这里插入图片描述

      在这里插入图片描述

    (1)适用场景

    • NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。

    (2)核心部分

    • 核心组件:Channel(通道)、Buffer(缓冲区)、Selector(选择器)

      • Selector 、Channel和Buffer的关系说明

        在这里插入图片描述

        • 每个channel都会对应一个Buffer。
        • 每个Selector对应一个线程,对应多个channel(连接)。
        • 该图反应了有三个channel注册到该selector。
        • 程序切换到哪个channel是由事件Event决定的。
        • Selector会根据不同的事件,在各个通道上切换。
        • Buffer就是一个内存块,底层是有一个数组。
        • 数据的读取写入是通过Buffer。BIO中要么是输入流,要么是输出流,不能双向。但是NIO的Buffer是可以读也可以写,需要flip方法切换。
        • Channel是双向的,反映底层操作系统的情况。比如Linux,底层的操作系统通道就是双向的。

    (3)Buffer

    • 核心属性:capacity、position、limit、mark。

      在这里插入图片描述

    • 子类:

      • ByteBuffer
      • IntBuffer
      • FloatBuffer
      • CharBuffer
      • DoubleBuffer
      • ShortBuffer
      • LongBuffer

    (4)Channel

    • 说明
      • 通道(Channel)类似流(Stream),区别是通道可以同时进行读写,而流只能读或者只能写。
    • 重要类
      • FileChannel:用于文件的数据读写
      • ServerSocketChannel:用于TCP的数据读写(服务端)
        • 相关方法
          • public static ServerSocketChannelopen();//得到一个通道
          • public abstract ServerSocketChannelbind(SocketAddress local);//绑定地址
          • public abstract SelectableChannel configureBlocking(boolean block);//设置阻塞或者非阻塞
          • public SocketChannel accept();//接收一个连接
          • public final SelectionKey register(Selector sel, int ops);//注册一个选择器并且设置监听事件
      • SocketChannel:用于TCP的数据读写(客户端)
        • 相关方法
          • public static SocketChannel open();//得到一个通道
          • public abstract SelectableChannel configureBlocking(boolean block);//设置阻塞或者非阻塞
          • public abstract boolean connect(SocketAddress remote);//连接服务端
          • public abstract boolean finishConnect();//上面连接出错之后使用这个进行连接
          • public abstract int write(ByteBuffer src);//往通道里写数据
          • public abstract int read(ByteBuffer dst)//从通道里读数据
          • public abstract SelectionKey register(Selector sel, int ops, Object att);//注册一个选择器并且设置监听事件,最后一个参数可以设置共享数据
          • public void close();//关闭通道
      • DatagramChannel:用于UDP数据读写

    (5)Selector

    • 说明
      • 多个Channel以事件的方式可以注册到同一个Selector。
      • Selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件。然后针对每个事件进行相应的处理。
      • 这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
      • 只有在连接真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
      • 避免了多线程之间的上下文切换导致的开销。
    • 主要子类:SelectorImpl。
    • 重要方法
      • public static Selector open();//得到一个选择器对象。
      • public int select([long timeout]);//监控所有注册的通道,当其中有IO操作时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间(可以不设置参数)。
      • public int selectNow();//不阻塞,立即返回。
      • public Set selectedKeys();//从内部集合中得到所有的selectionKey。
      • public Selector wakeup();//唤醒该选择器对象。
    • SelectionKey
      • 表示Selector和网络通道的注册关系,共四种:
        • int OP_ACCEPT:有新的网络连接可以accept,值为 1<<4 = 16。
        • int OP_CONNECT:代表连接已经建立,值为 1<<3 = 8。
        • int OP_READ:代表读操作,值为 1<<0 = 1。
        • int OP_WRITE:代表写操作,值为 1<<2 = 4。
      • 相关方法
        • public abstract Selector selector();//得到与之关联的Selector对象
        • public abstract SelectableChannel channel();//得到与之关联的通道
        • public final Object attachment();//得到与之关联的共享数据
        • public abstract SelectionKey interestOps(int ops);//设置或者改变监听事件
        • public final boolean isAcceptable();//是否可以accept
        • public final boolean isReadable();//是否可以读
        • public final boolean isWritable();//是否可以写
        • public abstract void cancel();//取消注册

    (6)零拷贝

    • 说明

      • 传统IO是4次拷贝,4次切换。(DMA:Direct Memory Access 直接内存拷贝,不使用CPU)

        在这里插入图片描述

      • 零拷贝是网络编程的关键,很多性能优化都离不开。

      • 零拷贝从操作系统角度看,是没有cpu拷贝,DMA拷贝是一定会存在的。

    • mmap

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

        在这里插入图片描述

    • sendFile

      • Linux 2.1

        • 数据根本不经过用户态,直接从内核缓冲区到SocketBuffer,同时,由于和用户态完全无关,就减少了一次上下文切换。(3次拷贝、3次切换)

          在这里插入图片描述

      • Linux 2.4

        • 避免了从内核缓冲区拷贝到SocketBuffer的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。(2次拷贝、3次切换)

          在这里插入图片描述

    4、Java AIO

    • AIO:异步非阻塞,引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。

    (1)适用场景

    • AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用操作系统参与并发操作,编程比较复杂,JDK7开始支持。

    三、Netty概述

    1、背景

    (1)原生NIO存在的问题

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

    (2)Netty的优点

    • Netty对JDK自带的NIO的API进行了封装。
    • 设计优雅:适用于各种传输类型的统一API阻塞和非阻塞Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型-单线程,一个或多个线程池。
    • 使用方便:详细记录的Javadoc,用户指南和示例;没有其他依赖项,JDK 5 (Netty3.x)或6 (Netty 4.x)就足够了。
    • 高性能、吞吐量更高;延迟更低;减少资源消耗;最小化不必要的内存复制。
    • 安全:完整的 SSL/TLS和 StartTLS支持。
    • 社区活跃、不断更新:社区活跃,版本迭代周期短,发现的Bug可以被及时修复,同时,更多的新功能会被加入。

    (3)官网

    • https://netty.io/ ,推荐使用Netty4.x。3.x太古老,5.x有重大bug被官网放弃。

    在这里插入图片描述

    2、线程模型概述

    (1)基本介绍

    • 目前存在的线程模型

      • 传统阻塞IO服务模型

        • 原理图:对象(黄色)、线程(蓝色)、方法API(白色)

          在这里插入图片描述

        • 特点

          • 采用阻塞IO模式获取输入的数据。
          • 每个连接都需要独立的线程完成数据的输入,业务处理,数据返回。
        • 问题

          • 当并发数很大,就会创建大量的线程,占用很大系统资源。
          • 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在read操作,造成线程资源浪费。
      • Reactor模式(反应器模式、分发者模式、通知者模式)

        • 原理图:对象(黄色)、线程(蓝色)、方法API(白色)

          在这里插入图片描述

        • 说明

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

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

          • 单Reactor单线程
          • 单Reactor多线程
          • 主从Reactor多线程
    • Netty线程模型:基于主从Reactor多线程模型做了一定的改进。

    (2)Reactor模式

    (1)单Reactor单线程
    • 原理图:

      在这里插入图片描述

    • 说明:

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

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

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

      • 客户端的数量有限,业务处理非常快速,比如Redis在业务处理的时间复杂度O(1)的情况
    (2)单Reactor多线程
    • 原理图:

      在这里插入图片描述

    • 说明:

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

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

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

      在这里插入图片描述

    • 说明:

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

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

      • 编程复杂度较高。

    (3)Netty模型

    (1)简单版
    • 原理图:

    在这里插入图片描述

    在这里插入图片描述

    • 说明:

      • BossGroup线程维护Selector,只关注Accecpt事件。
      • 当接收到Accept事件,获取到对应的SocketChannel,封装成NIOScoketChannel并注册到Worker 线程(事件循环),并进行维护。
      • 当Worker线程监听到selector中的通道发生自己感兴趣的事件后,就进行处理(由handler完成),注意handler已经加入到通道。
    (2)详细版
    • 原理图:

      在这里插入图片描述

    • 说明:

      • Netty抽象出两组线程池。BossGroup专门负责接收客户端的连接,WorkerGroup专门负责网络的读写。
      • BossGroup和WorkerGroup类型都是NioEventLoopGroup。
      • NioEventLoopGroup相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是NioEventLoop。
      • NioEventLoop表示一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上的socket的网络通讯。
      • NioEventLoopGroup 可以有多个线程,即可以含有多个NioEventLoop。
      • 每个Boss NioEventLoop循环执行的步骤有3步:
        • 轮询accept事件。
        • 处理accept事件,与client建立连接,生成NIOSocketChannel,并将其注册到某个worker NIOEventLoop上的selector。
        • 处理任务队列的任务,即runAllTasks。
      • 每个Worker NIOEventLoop循环执行的步骤:
        • 轮询read,、write事件。
        • 处理I/O事件,即read 、write事件,在对应NIOSocketChannel处理。
        • 处理任务队列的任务,即runAllTasks。
      • 每个Worker NIOEventLoop处理业务时,会使用pipeline(管道),pipeline中包含了channel,即通过pipeline可以获取到对应通道,管道中维护了很多的处理器。

    3、异步模型

    (1)基本介绍

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

    (2)工作原理图

    在这里插入图片描述

    在这里插入图片描述

    • 说明:
      • 在使用Netty进行编程时,拦截操作和转换出入站数据只需要提供callback或利用future即可。这使得链式操作简单、高效,并有利于编写可重用的、通用的代码。
      • Netty框架的目标就是让业务逻辑从网络基础应用编码中分离出来、解脱出来。

    (3)Future说明

    • 表示异步的执行结果,可以通过它提供的方法来检测执行是否完成,比如检索计算等等。
    • ChannelFuture是一个接口: public interface ChannelFuture extends Future。我们可以添加监听器,当监听的事件发生时,就会通知到监听器。

    (4)Future-Listener机制

    • 当Future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture来获取操作执行的状态,注册监听函数来执行完成后的操作。
    • 常见有如下操作:
      • 通过isDone方法来判断当前操作是否完成;
      • 通过isSuccess方法来判断已完成的当前操作是否成功;
      • 通过getCause方法来获取已完成的当前操作失败的原因;
      • 通过isCancelled方法来判断已完成的当前操作是否被取消;
      • 通过addlistener方法来注册监听器,当操作已完成(isDone方法返回完成),将会通知指定的监听器;如果Future对象已完成,则通知指定的监听器。
    • 优点:
      • 相比传统阻塞I/O,执行I/O操作后线程会被阻塞住,直到操作完成;异步处理的好处是不会造成线程阻塞,线程在I/O操作期间可以执行别的程序,在高并发情形下会更稳定和更高的吞吐量。

    四、Netty核心

    1、Netty核心组件

    (1)Bootstrap、ServerBootstrap

    • Bootstrap意思是引导,一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类。

    • 常见方法:

      • //该方法用于服务器端,用来设置两个EventLoopGroup
        public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup);
        
        • 1
        • 2
      • //该方法用于客户端,在AbstractBootstrap中,用来设置一个EventLoopGroup
        public B group(EventLoopGroup group);
        
        • 1
        • 2
      • //该方法在AbstractBootstrap中,用来设置服务端或者客户端的通道实现
        public B channel(Class<? extends C> channelClass);
        
        • 1
        • 2
      • //该方法在AbstractBootstrap中,用来给通道添加配置
        public <T> B option(ChannelOption<T> option, T value);
        
        • 1
        • 2
      • //用来给接收到的通道添加配置
        public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value);
        
        • 1
        • 2
      • //该方法用来设置业务处理类(自定义的handler)
        public ServerBootstrap childHandler(ChannelHandler childHandler);
        
        • 1
        • 2
      • //该方法用于服务器端,用来设置占用的端口号
        public ChannelFuture bind(int inetPort);
        
        • 1
        • 2
      • //该方法用于客户端,用来连接服务器端
        public ChannelFuture connect(String inetHost, int inetPort);
        
        • 1
        • 2

    (2)Future、ChannelFuture

    • Netty中所有的IO操作都是异步的,不能立刻得知消息是否被正确处理。但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过Future和ChannelFuture,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。

    • 常见方法:

      • //返回当前正在进行IO操作的通道
        Channel channel();
        
        • 1
        • 2
      • //等待异步操作执行完毕
        ChannelFuture sync();
        
        • 1
        • 2

    (3)Channel

    • Netty网络通信的组件,能够用于执行网络I/O操作。
    • 通过Channel可获得当前网络连接的通道的状态。
    • 通过Channel可获得网络连接的配置参数(例如接收缓冲区大小)。
    • Channel提供异步的网络I/O操作(如建立连接,读写,绑定端口),异步调用意味着任何I/O调用都将立即返回,并且不保证在调用结束时所请求的I/O操作已完成。
    • 调用立即返回一个ChannelFuture实例,通过注册监听器到ChannelFuture上,可以在I/O操作成功、失败或取消时回调通知调用方。
    • 支持关联I/O操作与对应的处理程序。
    • 不同协议、不同的阻塞类型的连接都有不同的Channel类型与之对应。
    • 常用的Channel类型(这些通道涵盖了UDP和TCP网络IO以及文件IO):
      • NioSocketChannel,异步的客户端 TCP Socket连接。
      • NioServerSocketChannel,异步的服务器端 TCP Socket连接。
      • NioDatagramChannel,异步的 UDP连接。
      • NioSctpChannel,异步的客户端 Sctp连接。
      • NioSctpServerChannel,异步的Sctp服务器端连接。

    (4)Selector

    • Netty基于Selector对象实现I/O多路复用,通过Selector一个线程可以监听多个连接的Channel事件。
    • 当向一个Selector中注册Channel后,Selector内部的机制就可以自动不断地查询(Select)这些注册的Channel是否有已就绪的IO事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个Channel。

    (5)ChannelHandler

    • ChannelHandler是一个接口,处理I/О事件或拦截I/O操作,并将其转发到其ChannelPipeline(业务处理链)中的下一个处理程序。

    • ChannelHandler本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间可以继承它的子类。

    • ChannelHandler及其实现类一览图:

      在这里插入图片描述

      • ChannelInboundHandler用于处理入站I/O事件。
      • ChannelOutboundHandler用于处理出站I/O操作。
    • 自定义一个Handler类去继承ChannelInboundHandlerAdapter,然后通过重写相应方法实现业务逻辑,一般需要重写以下方法:

      • //注册事件
        @Override
        public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
            ctx.fireChannelRegistered();
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
      • //取消注册事件
        @Override
        public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
            ctx.fireChannelUnregistered();
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
      • //通道就绪事件
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                ctx.fireChannelActive();
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
      • //通道非激活事件
        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            ctx.fireChannelInactive();
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
      • //通道读取数据事件
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            ctx.fireChannelRead(msg);
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
      • //数据读取完毕事件
        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            ctx.fireChannelReadComplete();
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5

    (6)Pipeline、ChannelPipeline

    • ChannelPipeline是一个Handler的集合,它负责处理和拦截inbound或者outbound的事件和操作,相当于一个贯穿Netty的链。(也可以这样理解:ChannelPipeline是保存 ChannelHandler的List,用于处理或拦截Channel的入站事件和出站操作)。

    • ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel中各个的 ChannelHandler如何相互交互。

    • 在Netty中每个Channel都有且仅有一个ChannelPipeline与之对应,它们的组成关系如下:

      在这里插入图片描述

      • 一个Channel包含了一个ChannelPipeline(互相包含),而ChannelPipeline中又维护了一个由ChannelHandlerContext组成的双向链表,并且每个ChannelHandlerContext中又关联着一个 ChannelHandler。
      • 入站事件和出站事件在一个双向链表中,入站事件会从链表head往后传递到最后一个人站的 handler,出站事件会从链表tail往前传递到最前一个出站的handler,两种类型的handler互不干扰。
    • 常见方法:

      • //把一个业务处理类(handle)添加到链中的第一个位置
        ChannelPipeline addFirst(ChannelHandler... handlers);
        
        • 1
        • 2
      • //把一个业务处理类(handler)添加到链中的最后一个位置
        ChannelPipeline addLast(ChannelHandler... handlers);
        
        • 1
        • 2

    (7)ChannelHandlerContext

    • 保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象。

    • ChannelHandlerContext中包含一个具体的事件处理器ChannelHandler,同时ChannelHandlerContext中也绑定了对应的pipeline和Channel的信息,方便对ChannelHandler进行调用。

    • 常见方法:

      • //关闭通道
        public ChannelFuture close();
        
        • 1
        • 2
      • //刷新
        ChannelHandlerContext flush();
        
        • 1
        • 2
      • //将数据写到ChannelPipeline中当前ChannelHandler的下一个ChannelHandler开始处理(出站)
        ChannelFuture writeAndFlush(Object msg);
        
        • 1
        • 2

    (8)ChannelOption

    • Netty在创建Channel实例后,一般都需要设置ChannelOption参数。

    • ChannelOption参数如下:

      • /**
        * 对应TCP/IP协议 listen 函数中的 backlog参数,用来初始化服务器可连接队列大小。服务端处理客
        * 户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。多个客户端来的时候,服务端将不能 * 处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小。
        */
        ChannelOption.SO_BACKLOG;
        
        • 1
        • 2
        • 3
        • 4
        • 5
      • /**
        * 设置是否一直保持连接活动状态
        */
        ChannelOption.SO_KEEPALIVE;
        
        • 1
        • 2
        • 3
        • 4

    (9)EventLoopGroup(NioEventLoopGroup)

    • EventLoopGroup是一组EventLoop的抽象,Netty为了更好的利用多核CPU资源,一般会有多个EventLoop同时工作,每个EventLoop维护着一个Selector实例。

    • EventLoopGroup提供next接口,可以从组里面按照一定规则获取其中一个EventLoop来处理任务。在 Netty服务器端编程中,我们一般都需要提供两个EventLoopGroup,例如: BossEventLoopGroup和 WorkerEventLoopGroup。

    • 通常一个服务端口即一个ServerSocketChannel对应一个Selector和一个EventLoop线程。BossEventLoop负责接收客户端的连接并将SocketChannel交给WorkerEventLoopGroup来进行IO处理,如下图所示:

      在这里插入图片描述

      • BossEventLoopGroup通常是一个单线程的EventLoop,EventLoop维护着一个注册了ServerSocketChannel的Selector实例。
      • BossEventLoop不断轮询Selector将连接事件分离出来,通常是OP_ACCEPT事件,然后将接收到的SocketChannel交给WorkerEventLoopGroup。
      • WorkerEventLoopGroup会由next选择其中一个EventLoop来将这个SocketChannel注册到其维护的Selector,并对其后续的IO事件进行处理。
    • 常用方法:

      • //构造方法
        public NioEventLoopGroup() {
                this(0);
        }
        public NioEventLoopGroup(int nThreads);
        
        • 1
        • 2
        • 3
        • 4
        • 5
      • //断开连接,关闭线程
        public Future<?> shutdownGracefully();
        
        • 1
        • 2

    (10)UnPooled和ByteBuf

    • UnPooled:Netty提供一个专门用来操作缓冲区(即Netty的数据容器)的工具类。

    • ByteBuf:类似于NIO中的ByteBuffer,但有区别。读写操作不需要使用flip方法进行反转。

      • 底层维护了 readerIndex、writerIndex、capacity。
      • 0~readerIndex:已经读取的区域。
      • readerIndex~writerIndex:可读的区域。
      • writerIndex~capacity:可写的区域。
    • 常用方法:

      • //UnPooled类:
        
        //通过给定的数据和字符编码返回一个ByteBuf对象
        public static ByteBuf copiedBuffer(CharSequence string, Charset charset);
        
        • 1
        • 2
        • 3
        • 4
      • //ByteBuf类:
        
        //可读的字节数
        public int readableBytes();
        
        //读取字节
        public byte readByte();
        
        //获取指定索引的字节
        public byte getByte(int index);
        
        //根据编码获取一段字符,指定起点和长度
        public CharSequence getCharSequence(int index, int length, Charset charset);
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13

    2、Netty的编码解码机制

    (1)基本介绍

    • 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码。
    • codec(编解码器)的组成部分有两个:decoder(解码器)和encoder(编码器)。
    • encoder负责把业务数据转换成字节码数据,decoder负责把字节码数据转换成业务数据。

    在这里插入图片描述

    (2)Netty原生codec

    • Netty自身提供了一些codec(编解码器)。
      • Encoder:
        • StringEncoder:对字符串数据进行编码。
        • ObjectEncoder:对Java对象进行编码。
      • Decoder:
        • StringDecoder:对字符串数据进行解码。
        • ObjectDecoder:对Java对象进行解码。
    • 问题:
      • Netty本身自带的ObjectDecoder和ObjectEncoder可以用来实现POJO对象或各种业务对象的编码和解码,底层使用的仍是Java序列化技术,而Java序列化技术本身效率就不高,存在如下问题:
        • 无法跨语言。
        • 序列化后的体积太大,是二进制编码的5倍多。
        • 序列化性能太低。

    (3)Google Protobuf

    • Protobuf是Google发布的开源项目,全称 Google Protocol Buffers,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或RPC数据交换格式。
    • 参考文档:https://developers.google.com/protocol-buffers/docs/proto (语言指南)
    • Protobuf是以message的方式来管理数据的。
    • 支持跨平台、跨语言,即客户端和服务器端可以是不同的语言编写的,支持目前绝大多数语言,例如C++、C#、Java、python等。
    • 高性能,高可靠性。
    • 使用protobuf编译器能自动生成代码。Protobuf是将类的定义使用.proto文件进行描述。然后通过protoc.exe编译器根据.proto自动生成.java文件。

    在这里插入图片描述

    3、Netty的入站与出站机制

    (1)基本说明

    • ChannelHandler充当了处理入站和出站数据的应用程序逻辑的容器。例如,实现ChannellnboundHandler接口(或ChannellnboundHandlerAdapter),你就可以接收入站事件和数据,这些数据会被业务逻辑处理。当要给客户端发送响应时,也可以从ChannellnboundHardler冲刷数据。业务逻辑通常写在一个或者多个ChannellnboundHandler中。ChannelOutboundHandler原理一样,只不过它是用来处理出站数据的。
    • ChannelPipeline提供了ChannelHandler链的容器。以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过pipeline中的一系列ChannelOutboundHandler,并被这些Handler处理,反之则称为入站的。

    在这里插入图片描述

    4、TCP的粘包与拆包

    (1)基本介绍

    • TCP是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的。

    • 由于TCP无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题。

    • 图解:

      在这里插入图片描述

      • 假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:
        • 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包。
        • 服务端一次接受到了两个数据包,D1和D2粘合在一起,称之为TCP粘包。
        • 服务端分两次读取到了数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这称之为TCP拆包。
        • 服务端分两次读取到了数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余部分内容D1_2和完整的D2包。同样是TCP拆包。

    (2)解决方案

    • 对于粘包和拆包问题,常见的解决方案有四种:
      • 客户端在发送数据包的时候,每个包都固定长度,比如1024个字节大小,如果客户端发送的数据长度不足1024个字节,则通过补充空格的方式补全到指定长度;
      • 客户端在每个包的末尾使用固定的分隔符,例如\r\n,如果一个包被拆分了,则等待下一个包发送过来之后找到其中的\r\n,然后对其拆分后的头部部分与前一个包的剩余部分进行合并,这样就得到了一个完整的包;
      • 将消息分为头部和消息体,在头部中保存有当前整个消息的长度,只有在读取到足够长度的消息之后才算是读到了一个完整的消息;
      • 通过自定义协议进行粘包和拆包的处理。
        • 使用自定义协议+编解码器。
        • 关键就是要解决服务器端每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免的TCP粘包、拆包。

    (3)Netty内置编解码器

    • LineBasedFrameDecoder:换行符解码器,报文尾部增加固定换行符rn,解析数据时以换行符作为报文结尾。
    • DelimiterBasedFrameDecoder:分隔符解码器,使用特定分隔符作为报文的结尾,解析数据时以定义的分隔符作为报文结尾。
    • LengthFieldBasedFrameDecoder:通过在包头增加消息体长度的解码器,解析数据时首先获取首部长度,然后定长读取socket中的数据。
    • FixedLengthFrameDecoder:定长解码器,这个最简单,消息体固定长度,解析数据时按长度读取即可。

    (4)Netty自定义编解码器

    • 通过实现MessageToByteEncoder和ByteToMessageDecoder来实现。

      • MessageToByteEncoder的作用是将响应数据编码为一个ByteBuf对象。

        public abstract class MessageToByteEncoder<I> extends ChannelOutboundHandlerAdapter {
            
            protected abstract void encode(ChannelHandlerContext ctx, I msg, ByteBuf out) throws Exception;
            
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
      • ByteToMessageDecoder则是将接收到的ByteBuf数据转换为某个对象数据。

        public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {
            
            protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
            
        }
        
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
  • 相关阅读:
    【C语言刷LeetCode】395. 至少有 K 个重复字符的最长子串(M)
    screen对象
    所见即所得的3D打印建模设计
    1秒钟搞懂tee和vim文件的使用命令(超级详细)
    linux下基于boost/process库实现多进程管理,基于c++开发
    Spring-boot初级
    如何降级node 版本
    测试原则-阶段-测试用例设计-调试
    【SNP 喜讯】贝里精英集团SAP S/4 HANA PCE系统成功上线
    SPI接口协议的学习3
  • 原文地址:https://blog.csdn.net/m0_37385780/article/details/126150956