面试准备——JAVA NIO&Netty总结

本文是结合JavaGuide的总结

IO基本知识

IO读写基本原理

read系统调用和write系统调用都只是内核缓冲区和进程缓冲区的操作。

read系统调用将内核缓冲区的数据复制到进程缓冲区

write系统调用将进程缓冲区数据复制到内核缓冲区。

至于具体的内核缓冲区到磁盘的过程则由操作系统内核进行
由此可知,用户程序的IO操作如Socket,文件IO都是上层应用开发,他们的输入输出处理,在编程流程上都是一致的。

缓冲区的作用

减少频繁的与物理设备的数据交换。在数据写入到磁盘的过程中会产生中断,系统中断的时候,系统需要保持相应的进程信息,而中断结束后需要进行恢复。缓冲区就减少了这种中断对于系统的开销。

四大IO模型

  • 阻塞IO:指用户空间程序的状态,内核IO操作彻底完成了后,才返回到用户空间执行用户操作。Java中的默认创建的socket就是阻塞IO
  • 同步IO和异步IO:
    同步IO是指:用户空间程序的线程是主动发起IO的一方,内核空间是被动接受的一方。
    异步IO是指:内核空间时发起IO的一方而用户空间程序是接受的一方。

同步阻塞IO(Blocking IO):用户程序需要等待内核IO操作完成才能继续执行。

同步非阻塞IO(Non-bolcking IO):用户程序并不需要等待内核IO操作完成就可以执行下一步操作。在这个过程中内核会返回给用户空间一个状态值。
不是JAVA中的NIO(New IO)

IO多路复用又称为异步阻塞IO,就是Reactor反应器模式,Java中的Selector选择器和Linux中的epoll都是这种模型。

异步IO(Asynchronous IO):用户进程的线程向内核空间注册了各种IO事件回调函数,由内核去主动调用。

如何通过合理的配置支持百万级并发连接

在开发高并发的系统时,需要解除Linux的文件句柄限制。Linux默认的是1024.也就是说一个进程可以接受1024个socket连接。

文件句柄,也叫文件描述符。文件句柄是内核为了高效管理已经被打开的文件所常创建的索引。

Linux通过ulimite -n 文件句柄数量 来修改文件句柄数。但是这种方式只是在用户登录时有效。
如果想永久修改,需要修改/etc/rc.local开机启动文件,添加:ulimite -SHn 数量
也可以直接通过修etc/secuity/limites.conf’
加入如下内容:

soft nofile 1000000 //(最大值为100万)
hard nofile 1000000
//soft nofile表示软性极限,
//hard nofile表示硬性极限,

Java NIO

Java NIO(New IO)在Java4中引入,目的是解决面向流的同步阻塞问题,因此很多人也叫他Non-Block IO 非阻塞IO。
Java NIO也属于IO多路复用模型。

NIO组成3大构件:
Channel 通道
Buffer 缓冲区
Selector 选择器

NIO和OIO的区别

OIO是Java4之前所使用的,面向流的同步阻塞IO,面向字节流或字符流,从字节流(字符流)中读取数据。一次读取一个或者多个不能随意改变指针位置。OIO没有选择器这一个概念

NIO是Java4引入来解决同步阻塞IO的,面向缓冲区的非阻塞IO。每次读取是从通道中读入缓冲区,而写入操作是从缓冲区写入通道中。
可以任意在缓冲区中的位置读取数据。

  • NIO如何实现的非阻塞呢?
    通过通道和通道的多路复用技术
  • NIO有选择器这个概念,NIO的实现基于系统的选择器调用。

使用Buffer的方式

  • 使用allocate()方法去创建一个buffer对象
  • 使用put,将数据写入缓冲池
  • 使用filp()可以将写模式换为读模式
  • 使用get()可以从缓冲区读取数据
  • 读取完毕后,可以使用Buffer.clear()或者是Buffe.compat()方法,将缓冲区转为写模式。

Channel类型

  • FileChannel文件通道,用于文件的数据的读写
  • SocketChannel套接字通道,用于socket套接字TCP连接和数据读写。常用下放的ServerSoketChannel进行连用。
  • ServerSocketChannel服务器套接字通道,允许监听TCP连接请求,为每个连接的请求创建一个SockeTChannel。
  • DatagramChannl数据报通道:用于UDP数据的读写。

在使用SocketChannel时,它默认的是同步阻塞,因此需要调用configureBlocking(false)来设置,变为异步非阻塞IO。

NIO Selector选择器

选择器的作用就是完成IO的多路复用。一个通道代表一个连接,而通过选择器可以实现同时监控多个连接通道的作用。使得一个线程完成多个通道的监听。

  1. Selector的IO事件类型
  • 可读 OP_READ
  • 可写OP_WRITE
  • 连接OP_CONNECT 完成了对端的握手,就处于连接就绪状态。
  • 接收OP_ACCEPT 当检测到一个新的连接到来则处于接收就绪转态。
  1. FileChannel没有继承SelectableChannel因此不能被选择器管理。

选择器使用流程

  1. 获取选择器实例
  2. 将通道注册到选择器中
  3. 轮询感兴趣的IO就绪事件(选择键的集合)

Reactor反应器模式

什么是反应器模式?

反应器模式由Reactor反应器线程和Handlers处理器组成。

  • Reactor反应器线程职责:负责响应IO事件,并且分发到Handlers处理器
    Handles处理器职责:非阻塞的执行业务逻辑。完成真正的连接建立,通道读取,处理业务逻辑,负责将结果写入通道。
    在这里插入图片描述

Netty

什么是Netty

Netty是一个基于NIO(Java的NEW IO)的client-server(客户端服务器)框架,使用它可以快速简单的开发网络应用程序。

Netty好在哪里?

Netty相较于JDK自带的NIO,更加加单。

  • 支持多种协议,FTP,SMTP,HTTP以及各种二进制和基于文本的传统协议。
  • 简单强大的线程模型
  • 自带多种编解码器
  • 真正的无连接数据包套接字支持
  • 有完整的SSL、/TLS以及StartTLS支持
  • 开发的社区环境好,然后很多开源项目的解决方案。

Netty的核心组件

  • Channel,接口是 Netty 对网络操作抽象类,它除了包括基本的 I/O 操作,如 bind()、connect()、read()、write() 等。
    比较常见的实现类就是NioServerSocketChannel服务端,NioSocketChannel(客户端).
  • EventLoop(事件循环)接口,Netty中最核心的概念。主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。
  • ChannelFuture
    Netty是异步非阻塞的,所有I/O操作都是异步的。而ChannelFuture就是Netty的异步解决方案,通过它可以进行值的返回,知道操作是否执行成功。

通过 ChannelFuture 接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。
并且,你还可以通过ChannelFuture 的 channel() 方法获取关联的Channel

  • ChannelHandler:消息具体处理器,负责读写操作,客户端连接等。

  • ChannelPipeline(通道流水线): 为 ChannelHandler 的链,提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API 。当 Channel 被创建时,它会被自动地分配到它专属的ChannelPipeline。绑定到一个通道的多个Handler处理器被加入到里面。
    ChannelPipline被设计为双向链表,而其中的Handler就是其节点。

Netty中的Reactor反应器模式使用的体现?

Netty中的Reactor的反应器模式体现在:

  • Channel:首先,Netty虽然没直接使用Java NIO的Channel通道组件,Netty对其进行了封装,使其适用于多种协议,同时还能保持异步IO和阻塞IO都提供。Channel的底层封装了SelectableChannel底层通道。
  • Netty中的Reactor反应器:Netty的反应器的名字叫做:NioEventLoop。他的作用和JavaNIO的反应器相似都是有一个Thread线程,一个负责IO事件的轮询。
  • Netty中的Handler:Netty的Handler处理器分为两大类,一类是ChannelInboundHandler通道入栈处理器,第二类是ChannelOutboundHandler通道出站处理器。都继承自ChannelHandler。

EventloopGroup 了解么?和 EventLoop 啥关系?

  • Netty中的反应器是多线程的反应器。而EventLoop相当于一个子反应器。而EventLoopGroup就是管理这些子反应器的线程组。
  • 在使用Netty的时候,使用的线程组EventLoopGroup而不是单个EventLoop。而EventLoopGroup的构造参数可以指定线程数量(默认为CPU的2倍),这些线程数量和EventLoop是一一对应的。一个EventLoop有一个专属的线程用于处理IO事件。(监听,调用处理器)
    在这里插入图片描述
    从上图可以看出: 当客户端通过 connect 方法连接服务端时,bossGroup 处理客户端连接请求。当客户端处理完成后,会将这个连接提交给 workerGroup 来处理,然后 workerGroup 负责处理其 IO 相关操作。

Bootstrap 和 ServerBootstrap 了解么?

  • Bootstrap 是客户端的启动引导类/辅助类
     EventLoopGroup group = new NioEventLoopGroup();
        try {
    
    
            //创建客户端启动引导/辅助类:Bootstrap
            Bootstrap b = new Bootstrap();
            //指定线程模型
            b.group(group).
                    ......
            // 尝试建立连接
            ChannelFuture f = b.connect(host, port).sync();
            f.channel().closeFuture().sync();
        } finally {
    
    
            // 优雅关闭相关线程组资源
            group.shutdownGracefully();
        }

Bootstrap 通常使用 connet() 方法连接到远程的主机和端口,作为一个 Netty TCP 协议通信中的客户端。Bootstrap 也可以通过 bind() 方法绑定本地的一个端口,作为 UDP 协议通信中的一端。

  • ServerBootstrap 服务器端的启动引导类/辅助类
/1.创建线程反应组
//bossGroup 处理连接监听IO事件
//workerGroup 用于负责数据IO事件和Handler业务处理
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
    
    
            //.创建服务端启动引导/辅助类:ServerBootstrap
            ServerBootstrap b = new ServerBootstrap();
            //.给引导类配置两大线程组,确定了线程模型
            b.group(bossGroup, workerGroup).
            /2.设置通道的IO类型
            b.channel(NioServerSocketChannel.class)
            /3.设置监听端口
            b.localAddress(new InetSocketAddress(port))
            /4.设置通道传输参数
            b.option(给父通道接收连接通道设置的选项)
            /5.装配流水线
            b.pipeline().addLast(new Handler)
            / 6.绑定服务器新连接的端口号
            ChannelFuture f = b.bind(port).sync();
            // 等待连接关闭
            f.channel().closeFuture().sync();
        } finally {
    
    
            //7.优雅关闭相关线程组资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

ServerBootstrap通常使用 bind() 方法绑定本地的端口上,然后等待客户端的连接。

Bootstrap 只需要配置一个线程组— EventLoopGroup ,而 ServerBootstrap需要配置两个线程组— EventLoopGroup ,一个用于接收连接,一个用于具体的处理。

NioEventLoopGroup 默认的构造函数会起多少线程?

默认的是CPU*2,但是可以通过构造函数进行更改。

Netty的线程模型

在 Netty 主要靠 NioEventLoopGroup 线程池来实现具体的线程模型的 (反应器模式)。

Reactor 模式基于事件驱动,采用多路复用将事件分发给相应的 Handler 处理,非常适合处理海量 IO 的场景。

什么是TCP沾包/拆包?他的解决方案是?

  • TCP 粘包/拆包 就是你基于 TCP 发送数据的时候,出现了多个字符串“粘”在了一起或者一个字符串被“拆”开的问题。
  • Netty中的沾包拆包问题:每次读取底层数据的容量是有限制的,当TCP底层数据报比较大的时候就会将一个底层数据报进行分包复制,这样就会产生半包。
  • 在TCP底层缓冲的数据包比较小的时候,一次负责不止一个内核的缓冲区包,造成了程序缓冲区读到了沾包。
  • 解决方法?
    (1)可以使用Netty提供的解码器:
  1. LineBasedFrameDecoder : 发送端发送数据包的时候,每个数据包之间以换行符作为分隔,LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节,判断是否有换行符,然后进行相应的截取。
  2. DelimiterBasedFrameDecoder : 可以自定义分隔符解码器,
  3. LineBasedFrameDecoder 实际上是一种特殊的 DelimiterBasedFrameDecoder 解码器。
  4. FixedLengthFrameDecoder: 固定长度解码器,它能够按照指定的长度对消息进行相应的拆包。
  5. LengthFieldBasedFrameDecoder:自定义长度数据包解码器。
    (2)自定义序列化解码器分包器:如:JSON序列化。

Netty长连接

我们知道 TCP 在进行读写之前,server 与 client 之间必须提前建立一个连接。建立连接的过程,需要我们常说的三次握手,释放/关闭连接的话需要四次挥手。这个过程是比较消耗网络资源并且有时间延迟的。
因此TCP采用了长连接的方式。

Netty的心跳机制

TCP在长连接中会出现各种异常情况,导致服务器等断开连接。而如果没有心跳机制的话,客户端就不知道服务器宕机了,而服务器重启后也不知道客户端还以为自己连接着。而当客户端因为意外断开连接后,服务器如果不知道底层的TCP已经断开了连接,这个连接就会变成假死连接,占用大量资源。因此需要心跳机制来解决。
TCP 实际上自带的就有长连接选项,本身是也有心跳包机制,也就是 TCP 的选项:SO_KEEPALIVE。 但是,TCP 协议层面的长连接灵活性不够。所以,一般情况下我们都是在应用层协议上实现自定义心跳机制的,也就是在 Netty 层面通过编码实现。通过 Netty 实现心跳机制的话,核心类是 IdleStateHandler。

Netty的零拷贝

零复制(英语:Zero-copy;也译零拷贝)

  • 技术是指计算机执行操作时,CPU 不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省 CPU 周期和内存带宽。
  • 从操作系统层面架构,就是指避免用户态和内核态之间来回拷贝数据。

Netty的零拷贝主要体现在:

  • 使用 Netty 提供的 CompositeByteBuf 类, 可以将多个ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝。
  • ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝。
  • 通过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题.

猜你喜欢

转载自blog.csdn.net/H1517043456/article/details/107747970