Netty简单学习 - @by_TWJ

1、学习的前提背景和概念

1.1、互联网服务端处理网络请求的原理

参考文章:http://www.52im.net/thread-1935-1-1.html
在这里插入图片描述

1.2、网络IO模型的迭代

介绍:,从多线程(BIO)–>应用(NIO)–>底层事件回调(AIO)

  1. BIO:阻塞性IO
    最早的网络IO模型BIO,
    BIO 是一个线程处理一个请求,若创建了线程,但一直没有接收到IO数据,则会一直阻塞,所以后来提出了NIO。
    BIO是阻塞性IO,socket.accept()的时候需要线程等待。

  2. NIO: 是非阻塞性IO
    NIO 是非阻塞性IO,它不需要线程等待,这个IO数据来了就处理,没来就返回状态信息。
    NIO 采用事件驱动方式分发,实际上使用轮询的方式检查事件,当IO数据就绪后,分发给线程池处理业务。
    NIO 是一个线程处理多个请求,具体是使用事件驱动方式分发事件,这样做极大的压缩了需要使用的线程,极大的提高了并发量。
    NIO 由调度员(Selector) 把来客户端上送的数据缓冲到 SocketChannel通道中, 等待用户处理,处理好后,关闭来客连接,等待其他人上送数据。
    缺点是不适合做上传文件等的长连接,因为消耗时间太多了,并且它是单线程的selector,导致后面的连接请求不及时处理。

  3. AIO:是非阻塞性IO
    AIO 需要计算机操作系统底层支持事件注册回调函数,有一部分操作系统不支持。
    AIO 它省去了事件驱动方式分发事件,直接由操作系统来接收缓冲区数据,IO数据就绪后,然后分发事件。
    AIO 是由操作系统底层接收缓冲区数据,通过事件方法触发回调,然后从内核地址copy一份数据到用户空间,最后给java应用使用。

这里并不是说AIO就是最好的,因为Netty在NIO的基础上做了些改进,例如Netty主从线程模型,使用subReactor多线程去接收socket缓冲区数据,而且Netty是零拷贝,省略了内核空间地址和用户空间地址之间的数据copy,所以这里不做深究。可以看看这个文章:https://zhuanlan.zhihu.com/p/638627006

1.3、IO线程模型的迭代

介绍:socket接收数据时需要等待,线程也会阻塞,这样线程可操作性很大,所以有了IO线程模型的概念。

参考文章:https://www.zhihu.com/question/33316227?sort=created

1. 原始线程模型是,使用while轮询处理请求

这种方法的最大问题是:如果前一个网络连接的handle(socket)没有处理完,那么后面的新连接没法被服务端接收,于是后面的请求就会被阻塞住,这样就导致服务器的吞吐量太低。

2. Connection Per Thread模式

Connection Per Thread模式(一个线程处理一个连接)的优点是:解决了前面的新连接被严重阻塞的问题,在一定程度上,较大的提高了服务器的吞吐量。
其实就是BIO模式

3. Reactor线程模型

Reactor是反应堆/反应器的意思,Reactor模型,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。 服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫Dispatcher模式,即I/O多了复用统一监听事件,收到事件后分发(Dispatch给某进程),是编写高性能网络服务器的必备技术之一。

Reactor模型中有2个关键组成

  • Reactor Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人

  • Handlers 处理程序执行I/O事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作

  • 在这里插入图片描述
    取决于Reactor的数量和Hanndler线程数量的不同,Reactor模型有3个变种

  • 单Reactor单线程

  • 单Reactor多线程

  • 主从Reactor多线程

可以这样理解,Reactor就是一个执行while (true) { selector.select(); …}循环的线程,会源源不断的产生新的事件,称作反应堆很贴切。

4. Netty线程模型

Netty主要基于主从Reactors多线程模型(如下图)做了一定的修改,其中主从Reactor多线程模型有多个Reactor:MainReactor和SubReactor:

  • MainReactor负责客户端的连接请求,并将请求转交给SubReactor
  • SubReactor负责相应通道的IO读写请求
  • 非IO请求(具体逻辑处理)的任务则会直接写入队列,等待worker threads进行处理

特别说明的是: 虽然Netty的线程模型基于主从Reactor多线程,借用了MainReactor和SubReactor的结构,但是实际实现上,SubReactor和Worker线程在同一个线程池中。
在这里插入图片描述
术语转换:
bossGroup 就是 subreactor IO读写线程
workerGroup 就是 worker threads 也可以说是业务操作的线程

netty单线程模型

//1.eventGroup既用于处理客户端连接,又负责具体的处理。
  EventLoopGroup eventGroup = new NioEventLoopGroup(1);
  //2.创建服务端启动引导/辅助类:ServerBootstrap
  ServerBootstrap b = new ServerBootstrap();
            boobtstrap.group(eventGroup, eventGroup)
            //......

netty多线程模型

// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
  //2.创建服务端启动引导/辅助类:ServerBootstrap
  ServerBootstrap b = new ServerBootstrap();
  //3.给引导类配置两大线程组,确定了线程模型
  b.group(bossGroup, workerGroup)
    //......

netty主从线程模型

// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup();//线程数默认cpu核数(逻辑处理器)*2
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
  //2.创建服务端启动引导/辅助类:ServerBootstrap
  ServerBootstrap b = new ServerBootstrap();
  //3.给引导类配置两大线程组,确定了线程模型
  b.group(bossGroup, workerGroup)
    //......

2、NIO简单学习(不详细讲)

NIO是JDK1.6 才有的,AIO是JDK1.7才有的

2.1、三大核心部分

NIO设计图
在这里插入图片描述

2.1.1、缓冲区(Buffer)

介绍:缓冲区就是读写IO数据的区域。
缓冲区数据类型,有7个,和基本数据类型相比,少了boolean的数据类型。

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

2.1.2、通道(Channel)

介绍:专门为这个缓冲区进行读写操作。

2.1.3、Selector

介绍:这个是事件驱动,负责监听和分发的IO数据就绪事件。

这里Channel需要注册到Selector中,Selector监听到数据了才会分发给Channel

2.2、一个例子,让我们更加认识NIO

这个例子是不完整的,因为忽略了一些不可控的因素。
这里是单线程模型,因为事件监听/分发事件和Channel操作都是同一个线程。
如果是多线程模型,事件监听/分发事件是一个线程,Channel操作是一个线程,一般使用线程池分配线程。

public void test() throws Exception {
    // 第一步、创建selector
    Selector selector = Selector.open();// 1-打开多路复用器
    // 第二步、创建通道
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 2-打开一个ServerSocketChannel通道
    serverSocketChannel = serverSocketChannel.bind(new InetSocketAddress(8888));// 3-绑定到8888端口
    serverSocketChannel.configureBlocking(false);        // 4-ServerSocketChannel通道设置为非阻塞
    // 第三步、注册通道到selector
    SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);// 5-ServerSocketChannel通道注册到Selector多路复用器

    Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
    // 第四步、selector事件监听和分发事件
    while(keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if(key.isAcceptable()) {
            // a connection was accepted by a ServerSocketChannel.
        } else if (key.isConnectable()) {
            // a connection was established with a remote server.
        } else if (key.isReadable()) {
            // a channel is ready for reading
        } else if (key.isWritable()) {
            // a channel is ready for writing
        }
        keyIterator.remove();
    }
}

3、Netty学习

Netty是基于NIO,对NIO做了深度的优化,使NIO使用更加简单、方便。

官方文档:https://netty.io/wiki/user-guide-for-4.x.html
NIO与Netty对比图
在这里插入图片描述

3.1、Netty 内存 和 Netty零拷贝

NIO内存支持JVM内存,也支持使用堆外直接内存。
Netty不使用JVM内存,它是有自己的内存管理的。

参考文章:https://www.cnblogs.com/crazymakercircle/p/16181994.html

3.2、Netty简单的demo

netty 简单的发送和接收

左边是发送,右边是接收
在这里插入图片描述

3.3、Netty 核心组件

参考文档:https://www.51cto.com/article/720022.html
在这里插入图片描述
我把Netty的核心组件分为三层,分别是网络通信层、事件调度层和服务编排层。
(这里我在原来的参考文档的基础上,再添加了几个)

3.3.1、网络通信层

在网络通信层有三个核心组件:Bootstrap、ServerBootStrap、Channel。

  1. Bootstrap:相当于完成网络通信的载体。
  2. ServerBootStrap:负责服务端监听,用来监听指定端口;
  3. Channel:相当于完成网络通信的载体。

3.3.2、事件调度层

事件调度器有两个核心组件:EventLoopGroup与EventLoop。

  1. EventLoopGroup:本质上是一个线程池,主要负责接收I/O请求,并分配线程执行处理请求。
  2. EventLoop:相当于线程池中的线程。

3.3.3、服务编排层

在服务编排层有三个核心组件ChannelPipeline、ChannelHandler、ChannelHandlerContext、编码器/解码器。

  1. ChannelPipeline:负责将多个ChannelHandler链接在一起。
  2. ChannelHandler:针对I/O的数据处理器,数据接收后,通过指定的Handler进行处理。
  3. ChannelHandlerContext:用来保存ChannelHandler的上下文信息。
  4. 编码器/解码器:用于数据转换

3.4、网络通信层

Bootstrap是引导的意思,它的作⽤是配置整个Netty程序,将各个组件都串起来,最后绑定端⼝、启动Netty服务。
Netty中提供了2种类型的引导类,⼀种⽤于客户端(Bootstrap),⽽另⼀种(ServerBootstrap)⽤于服务器。

Bootstrap

(略)

ServerBootStrap

(略)

Channel

基本的IO操作(bind(), connet(), read(), write()), 依赖于底层网络传输

channel是提供读写操作IO数据的通道。

Channel、ChannelHandler、ChannelPipeline 它们的关系
在这里插入图片描述
这两个图其实都对,因为它们都在channel里操作,接收和发送/读写都需要通过channel
在这里插入图片描述

Channel状态

Channel一般我们都不会直接操作,都会使用ChannelHandler 来间接操作。
ChannelHandlerContext 包含了 ChannelHandler 生命周期的所有事件,如 connect、bind、read、flush、write、close 等。

Channel的状态有四种:

  1. ChannelUnregistered:已创建但还未被注册到监听器EventLoop中
  2. ChannelRegistered :已注册到监听器EventLoop中
  3. ChannelActive :连接完成处于活跃状态,此时可以接收和发送数据
  4. ChannelInactive :非活跃状态,代表连接未建立或者已断开

判断Channel通道状态:

  • isOpen():判断当前Channel是否已经打开
  • isRegistered():判断当Channel是否已经注册到NioEventLoop上
  • isActive():判断当前Channel是否已经处于激活状态
ChannelOption参数

参考文章:https://www.jianshu.com/p/975b30171352

  1. ChannelOption.SO_BACKLOG 请求队列大小
  2. ChannelOption.SO_REUSEADDR 允许重复使用本地地址和端口
  3. ChannelOption.SO_KEEPALIVE 保持TCP连接,设置TCP每隔一段时间发送一次
  4. ChannelOption.SO_SNDBUF和ChannelOption.SO_RCVBUF 用于操作接收缓冲区和发送缓冲区的大小
  5. ChannelOption.SO_LINGER SO_LINGER可以阻塞close()的调用时间,直到数据完全发送
  6. ChannelOption.TCP_NODELAY 该参数的作用就是禁止使用Nagle算法,不允许打包发送
  7. IP_TOS 用于描述IP包的优先级和QoS选项
  8. ALLOW_HALF_CLOSURE 默认为false,连接自动关闭,若为true,则触发关闭事件。

具体意思:
1、ChannelOption.SO_BACKLOG

ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小

2、ChannelOption.SO_REUSEADDR

ChanneOption.SO_REUSEADDR对应于套接字选项中的SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口,

比如,某个服务器进程占用了TCP的80端口进行监听,此时再次监听该端口就会返回错误,使用该参数就可以解决问题,该参数允许共用该端口,这个在服务器程序中比较常使用,

比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置SO_REUSEADDR就无法正常使用该端口。

3、ChannelOption.SO_KEEPALIVE

Channeloption.SO_KEEPALIVE参数对应于套接字选项中的SO_KEEPALIVE,该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。

4、ChannelOption.SO_SNDBUF和ChannelOption.SO_RCVBUF

ChannelOption.SO_SNDBUF参数对应于套接字选项中的SO_SNDBUF,ChannelOption.SO_RCVBUF参数对应于套接字选项中的SO_RCVBUF这两个参数用于操作接收缓冲区和发送缓冲区的大小,接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。

5、ChannelOption.SO_LINGER

ChannelOption.SO_LINGER参数对应于套接字选项中的SO_LINGER,Linux内核默认的处理方式是当用户调用close()方法的时候,函数返回,在可能的情况下,尽量发送数据,不一定保证会发生剩余的数据,造成了数据的不确定性,使用SO_LINGER可以阻塞close()的调用时间,直到数据完全发送

6、ChannelOption.TCP_NODELAY

ChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关,Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网络的有效负载,但是却造成了延时,而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输,于TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。

7、IP_TOS

IP参数,设置IP头部的Type-of-Service字段,用于描述IP包的优先级和QoS选项。

8、ALLOW_HALF_CLOSURE

Netty参数,一个连接的远端关闭时本地端是否关闭,默认值为False。值为False时,连接自动关闭;为True时,触发ChannelInboundHandler的userEventTriggered()方法,事件为ChannelInputShutdownEvent。

Channel 有哪些呢?
  1. ServerSocketChannel 用于接受传入TCP/IP连接的TCP/IP ServerChannel。
  2. SctpServerChannel 用于接受传入的SCTP/IP关联的SCTP/IP ServerChannel。
  3. LocalServerChannel 用于允许VM通信的本地传输的ServerChannel。
  4. UdtServerChannel 已弃用,不赞成UDT传输不再维护,并将被移除
  5. DatagramChannel 用于UDP/IP Channel.
  6. DuplexChannel 具有可独立关闭的两端的双工信道。
  7. DomainDatagramChannel 一个支持通过UNIX域数据报套接字进行通信的UnixChannel。
  8. Http2StreamChannel http1是一个请求独占一个链接,是http2 要解决的一个痛点,解决方法是在链接的基础上提出了stream的概念,通过stream 来区别不同的请求。参考文档:https://www.jianshu.com/p/122c36809827
  9. UnixChannel 该通道公开仅在类似UNIX的系统上存在的操作。
ServerSocketChannel

用于接受传入TCP/IP连接的TCP/IP ServerChannel。
有如下几个实现:

  1. KQueueServerSocketChannel Kqueue用在mac系统中,比NioServerSocketChannel的更高级,详细看 #3.7、IO多路复用模型
  2. EpollServerSocketChannel Epoll用在liunx系统中,比NioServerSocketChannel的更高级,详细看 #3.7、IO多路复用模型
  3. OioServerSocketChannel 原始的阻塞IO
  4. NioServerSocketChannel NIO异步非阻塞IO
Epoll / KQueue 使用

在epoll中则需要使用EpollEventLoopGroup。
KQueue也如此,需要使用KQueueEventLoopGroup。

参考文章:https://blog.51cto.com/flydean/5690204

Epoll例子


EventLoopGroup bossGroup = new EpollEventLoopGroup(1);
EventLoopGroup workerGroup = new EpollEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
    b.group(bossGroup, workerGroup)
        .channel(EpollServerSocketChannel.class)
        .handler(new LoggingHandler(LogLevel.INFO))
        .childHandler(new NativeChatServerInitializer());

3.5、事件调度层

EventLoopGroup

本质上是一个线程池,主要负责接收I/O请求,并分配线程执行处理请求。

EventLoop

相当于线程池中的线程。

3.6、服务编排层

ChannelPipeline

负责将多个ChannelHandler链接在一起。
ChannelPipeline为ChannelHandler链提供了容器,并定义了用于在该链上传播入站和出站事件流的API。

ChannelHandler

针对I/O的数据处理器,数据接收后,通过指定的Handler进行处理。
它充当了所有处理入站和出站数据的应用程序逻辑的容器。

ChannelHandler 分为 入站和出站
在这里插入图片描述
常用的ChannelHandler

  1. ChannelHandlerAdapter
  2. ChannelInboundHandlerAdapter 处理入站IO数据
  3. ChannelOutboundHandlerAdapter 处理出站IO数据
  4. ChannelDuplexHandler 处理入站和出站IO数据

ChannelDuplexHandler具备 ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter的特性

ChannelHandlerContext

用来保存ChannelHandler的上下文信息。包含channel状态信息等。

编码器/解码器

用于数据转换
用作封装/解析成其他协议的,例如Http协议等

ChannelOutboundHandlerAdapter 提供的编码器有:

  • MessageToMessageEncoder
  • MessageToByteEncoder

ChannelInboundHandlerAdapter 提供的解码器有:

  • ByteToMessageDecoder
  • MessageToMessageDecoder

3.7、IO多路复用模型

参考文章:https://cloud.tencent.com/developer/news/897381

  1. select
  2. poll
  3. epoll
  4. kqueue

select

select是最为常见的一种。实时不管是 netty 还是 JAVA 的 NIO 使用的都是 select 模型。

事实上 select 模型和非阻塞 IO 有点相似,不同的是 select 模型中有一个单独的线程专门用来检查 socket 中的数据是否就绪。如果发现数据已经就绪,select 可以通过之前注册的事件处理器,选择通知具体的某一个数据处理线程。

这样的好处是虽然 select 这个线程本身是阻塞的,但是其他用来真正处理数据的线程却是非阻塞的。并且一个 select 线程其实可以用来监控多个 socket 连接,从而提高了 IO 的处理效率,因此 select 模型被应用在多个场合中。

poll

poll 和 select 类很类似,只是描述 fd 集合的方式不同. poll 主要是用在 POSIX 系统中。
我的理解是 select 单个进程所能打开的最大连接数,是由FD_SETSIZE宏定义的,其大小是32个整数大小(在32位的机器上,大小是3232,64位机器上FD_SETSIZE=3264),而poll可调。

epoll

实时上,select 和 poll 虽然都是多路复用 IO,但是他们都有些缺点。而 epoll 和 kqueue 就是对他们的优化。

epoll 是 linux 系统中的系统命令,可以将其看做是 event poll。首次是在 linux 核心的 2.5.44 版本引入的。

主要用来监控多个 file descriptors 其中的 IO 是否 ready。

对于传统的 select 和 poll 来说,因为需要不断的遍历所有的 file descriptors,所以每一次的 select 的执行效率是 O(n) ,但是对于 epoll 来说,这个时间可以提升到 O(1)。

这是因为 epoll 会在具体的监控事件发生的时候触发通知,所以不需要使用像 select 这样的轮询,其效率会更高。

epoll 使用红黑树 (RB-tree) 数据结构来跟踪当前正在监视的所有文件描述符。

kqueue

kqueue 和 epoll 一样,都是用来替换 select 和 poll 的。不同的是 kqueue 被用在 FreeBSD,NetBSD, OpenBSD, DragonFly BSD, 和 macOS 中。

kqueue 不仅能够处理文件描述符事件,还可以用于各种其他通知,例如文件修改监视、信号、异步 I/O 事件 (AIO)、子进程状态更改监视和支持纳秒级分辨率的计时器,此外 kqueue 提供了一种方式除了内核提供的事件之外,还可以使用用户定义的事件。

epoll 和 kqueue 的优势

epoll 和 kqueue 之所以比 select 和 poll 更加高级, 是因为他们充分利用操作系统底层的功能,对于操作系统来说,数据什么时候 ready 是肯定知道的,通过向操作系统注册对应的事件,可以避免 select 的轮询操作,提升操作效率。

要注意的是,epoll 和 kqueue 需要底层操作系统的支持,在使用的时候一定要注意对应的 native libraries 支持。

总结:NIO使用了select模型循环检查IO就绪事件,并分发事件,若有1w个IO事件,就要循环1次就要检查1w个事件是否就绪,这不太合理吧,所以搞了个更加高级的epoll/kqueue,充分利用操作系统底层去监控就绪事件,就绪后,通过查找红黑树,分发事件。

这里已经写完了,剩下的就是实际使用方面了。

相关文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值