关于后端:IO网络

55次阅读

共计 6076 个字符,预计需要花费 16 分钟才能阅读完成。

I/ O 网络

阻塞与非阻塞:

阻塞: 拜访 IO 的线程是否会阻塞(期待)。

同步和异步:

数据的申请形式。

  • 同步会期待资源返回的后果。
  • 异步通过回调的形式获取返回的后果

BIO

同步阻塞。传统的 socket 编程,实现模式为一个连贯一个线程,客户端有连贯申请时服务器就启动一个线程解决,如果这个连贯不做任何事件就会造成不必要的线程开销,能够通过线程池改善(实现多个客户连贯服务器)。

存在的问题:

  1. 针对每个申请都须要创立一个线程。
  2. 并发较大时须要创立大量线程解决,占用资源大
  3. 连贯建设后,如果以后线程临时没有数据可读,则线程阻塞在 read,造成线程资源节约

NIO

同步非阻塞。实现模式为一个线程解决多个申请(连贯),客户端发送的申请都会注册到多路复用器上多路复用器轮询到连贯有 I / O 申请就进行解决。

AIO

异步非阻塞。引入了异步通道的概念,应用 Proactor 模式, 简化了程序编写, 无效的申请才启动线程, 特点是现有操作系统实现后再告诉服务端程序启动线程去解决, 用于连接数较多且连接时间较长的利用。

Proactor : 音讯异步告诉的设计模式,Proactor 告诉的不是就绪事件, 而是实现事件。

场景剖析

  1. BIO 实用于连接数小且固定的架构,对服务器资源要求高,并发局限于利用。JDK1.4 以前。
  2. NIO 实用于连贯数目多且连贯比拟短的架构,比方聊天服务器,弹幕零碎,服务器间通信。应用较多
  3. AIO 实用于连贯数目多且和长连贯的架构,比方相册服务器,充沛调用 OS 参加并发操作。

NIO 编程

介绍

  1. 外围局部:Channel 通道,buffer 缓冲区,selector 选择器
  2. 面向缓冲区编程。数据读取到缓冲区,须要时可在缓冲区中前后挪动,减少了处理过程中的灵活性,提供非阻塞式的高伸缩性网络。
  3. 当一个申请从通道发送申请或者读取数据时:如果有数据就读取,没有数据就去做其余的事件,不会阻塞线程。写操作也是

NIO 与 BIO 比拟

  1. BIO 以流的形式解决数据,NIO 以缓冲区的形式解决数据。NIO 效率更高。
  2. BIO 是阻塞的,NIO 是非阻塞的。
  3. BIO 是基于字节和字符流操作,NIO 基于 channel 和 buffer 缓冲区进行操作。

    数据总是从通道读取到缓冲区,或者从缓冲区写入到通道,selector 用于监听多个通道的事件,因而单线程就能够监听多个客户端通道

流程:

客户端与服务器建设连贯,先获取一个通道,通道注册到 selector,selector 轮询查看通道的事件(状态),如果客户端向 channel 的 buffer 写入了数据,selector 监听到了对应事件(例如写事件),则由 server 端的线程进行操作。如果没有监听到事件则不会让服务端的线程解决。

即:IO 多路复用

缓冲区 Buffer

Buffer 是内存块。Buffer 对象就是用来操作内存块的。

介绍:缓冲区实质上是一个能够读写数据的内存块,能够了解为一个数组,Buffer 对象提供了能够读写内存块的 API,并且能够跟踪记录缓冲区的状态变动。Channel 读写数据必须通过 buffer。

常见 API

蕴含 7 个子类(byte,short,int,long,float,double,char)罕用子类 ByteBuffer.

ByteBuffer.alloate(长度)创立 byte 类型的指定长度的缓冲区。没数据的

ByteBuffer.wrap(byte[] array)创立一个有内容的 byte 类型缓冲区。有数据的。

写模式的时候 position 相当于是以后在那个地位,而后 limit 了解为 length+1

flip()

切换读模式:将 position 设置成 0 就是从头开始读,而后 limit 设置成原来 position 的地位 相当于是记录有多少个数据当 position=limit 就示意读完了。

clear()

切换写模式:将 position 设置成 0 就是从头开始笼罩写,而后 limit 设置成最大容量。

Channel

通道能够读也能够写,流一半是单向的,只能读或写,所以须要别离创立一个输出流和输入流。通道能够异步读写,都是基于缓冲区 Buffer 来读写

常见的实现类有:FileChannel,ServerSocketChannel,SocketChannel。罕用的 ServerSocket 和 Socket 就能够实现客户端服务端的通信编写。

应用

server
  1. 创立ServerSocketChannel
  2. 绑定端口
  3. 配置成非阻塞模式configureBlocking(false)
  4. while true 外面 accept。如果有 accpet 会返回一个channel
  5. 如果 channel 不为空阐明有传过来的数据
  6. 创立 ByteBuffer 用 channel 读取 read()

    返回值: 负数 无效字节数 0 没有读到数据 -1 读到开端

  7. 给客户端回写数据 write()
  8. 开释资源
client
  1. 关上通道SocketChannel.open()
  2. 设置 ip 端口号
  3. 写出数据 write()
  4. 读取 server 写回的数据 read()
  5. 开释资源

Selector

检测多个注册到服务端的通道上是否有事件产生,而后对每个事件进行相应的解决。用一个线程,解决多个客户端连贯和申请。

所以次要作用:监听通道事件,依据不同事件做不同解决。这样只有在通道监听到读写事件才会进行读写操作,不必节俭资源。

API

Selector.open 失去一个选择器

Selector.select()阻塞监听所有注册的通道,当有事件产生,放入到了 selectionkey 的汇合中。

Selector.slectedKeys返回事件汇合。

  • isAcceptable 连贯持续事件:就是发动连贯 ==》ACCEPT
  • isConnectable 连贯就绪事件:就是连贯胜利 ==》CONNECT
  • isReadable 读就绪事件 ==》READ
  • isWriteable写就绪事件 ==》WRITE

事件用完后删除,避免二次解决。

流程

  1. serverSocketChannel.open 关上一个通道
  2. selector.open 创立一个 selector
  3. serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);服务端注册连贯事件
  4. while true 判断外面有没有事件
  5. 如果是 isAcceptable,获取到的客户端通道设置成非阻塞而后注册到 selector 并设置读事件。
  6. 如果是 isReadable,取得客户端通道 key.channel 读取数据到缓冲区。
  7. 回写数据。
  8. 敞开资源。

Netty

原生 NIO 存在的 bug

  1. NIO 类库和 API 应用简单。
  2. 须要把握多线程以及 reactor 模式
  3. 开发工作了难度大:例如客户端重连断连,半包读写,失败缓存。等
  4. JDK-NIO 有 Epoll Bug 导致 selector 空轮询 CPU100%。

Netty 是 Jboos 提供的异步的基于 NIO 事件驱动的网络应用程序框架,疾速开发高性能高可靠性的网络 IO 程序。简化了 NIO 的开发过程。

劣势:

  1. 提供阻塞和非阻塞的 Socket,可灵便扩大事件,可定制的线程模型
  2. 具备更高的性能吞吐量,应用零拷贝,节俭资源。
  3. SSL
  4. 反对多种协定,预置多种编解码性能,反对开发公有协定。

线程模型

  • 传统阻塞 IO 详情见 BIO
  • Reactor 模型

    是一种散发的模式(Dispatcher 模式)一个或多个输出 (也就是申请) 同时传递给服务端的模式。服务端程序处理多个申请,同步散发到相应的解决线程。高并发的解决的要害是应用 IO 复用来监听事件,收到事件后分发给某个线程。

    Reactor 中 蕴含一个 Reactor 由 selector 和 dispatcher 组成,selector 用于监听申请,dispatch 用于散发申请,如果申请是连贯申请 会分发给 Acceptor 由 Acceptor 建设连贯,IO 的读写申请则分发给handler 由 handler 进行 读取 - 解决 - 响应

    • 单 Reactor 单线程

      长处:模型简略,没有多线程、过程通信、竞争问题。

      毛病:

      • 性能问题:单线程,Handler 在解决连贯的业务时,整个过程无奈解决其余连贯的事件,造成性能瓶颈。
      • 可靠性问题:线程意外终止或者死循环,整个零碎通信模块不可用,造成节点故障
    • 单 Reactor 多线程

      在单 Reactor 单线程的根底上有多个 handler,此时 handler 只负责读取和响应 数据,并且减少了worker 线程池,由 worker 线程来解决业务。所以多线程实际上是减少了 work 线程。

      长处:充分利用多核 CPU 的解决能力。

      毛病:多线程数据共享和拜访比较复杂,reactor 要解决所有的事件的监听和响应,而且是单线程运行,在高并发场景容易性能瓶颈

    • 主从 Reactor 多线程

      在单 reactor 多线程的根底上,reactor 降级为主从,主 Reactor(主线程)只用于监听连贯申请, 在由 Acceptor 建设连贯后交给 Reactor 子线程,子线程会将连贯退出到本人的连贯队列进行事件监听,而后再分发给 handler,再到 worker,在理论开发中 子线程是能够扩大的。所以主从多线程,集体感觉扩大的是 reactor 子线程。

      长处:

      1. 主从 reactor,职责明确,主线程只需承受新连贯,子线程实现后续业务解决
      2. 主从之间数据交互简略,主线程只须要把新连贯交给子线程,子线程不须要返回数据。

        之前的模式单 reactor 还要解决数据的响应

      3. 多个子 reactor 可能应答更高的并发申请。

      毛病:复杂度难度较高,相似的有 Nginx Netty 这种模式也叫 1 +M+ N 线程模式即应用 1 个 (代指绝对较少) 连贯建设线程 + M 个 IO 线程 + N 个业务解决线程

Netty 线程模型

基于 Reactor 主从做了改良,其实就是在主线程将建设连贯后 ServerSocketChannel 返回的 SocketChannel 封装成了 NioSocketChannel 注册进子线程的 selector。

由主从两组线程池组成 BossGroup 和 WorkerGroup,线程池由 NioEventLoop 线程组成,所以也就是 NioEventLoopGroup。NioEventLoop 蕴含了 selector 和 taskqueue

其中 Boss 次要用轮询监听建设连贯,并且将建设连贯后的连贯注册到 worker,而后在执行 taskqueue 其余 tasks

worker 也是监听读写事件,解决读写,解决其余 task

对于 Boss 来说

  • select:轮询注册 ssc 的 accpet 事件
  • processSelectedKeys:解决 accept 事件,与客户端建设连贯生成 NioSocketChannel 并注册到 work 的 selector
  • runAllTask:再去以此循环解决队列中的其余工作

对于 worker 来说:

  • select:轮询读写事件
  • processSelectedKeys:解决读写事件
  • runAllTask:顺次循环解决其余工作

processSelectedKeys 中会应用 pipeline 管道,管道中援用了 channel。也就是说通过 pipeline 能够获取到对于的 channel,并且管道中保护了很多的处理器(过滤、拦挡、自定义等)

Server 端 demo

{
    // 1. 创立 bossGroup 线程组: 解决网络事件 -- 连贯事件
    EventLoopGroup bossGroup = new NioEventLoopGroup(1);

    // 2. 创立 workerGroup 线程组: 解决网络事件 -- 读写事件 2 * 处理器线程数
    EventLoopGroup workerGroup = new NioEventLoopGroup();

    // 3. 创立服务端启动助手
    ServerBootstrap serverBootstrap = new ServerBootstrap();

    // 4. 设置 bossGroup 线程组和 workerGroup 线程组
    serverBootstrap.group(bossGroup, workerGroup)
        // 5. 设置服务端通道实现为 NIO
        .channel(NioServerSocketChannel.class)
        // 6. Boss 参数设置. 初始化服务端可连贯队列
        .option(ChannelOption.SO_BACKLOG, 128)
        // 6.1 child 参数设置。两个服务之间应用心跳来检测对方是否还活着
        //https://ihui.ink/post/netty/channel-options/
        .childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE)
        // 7. 创立一个通道初始化对象
        .childHandler(new ChannelInitializer<>() {
          @Override
          protected void initChannel(Channel ch) throws Exception {
            // 8. 向 pipeline 中增加自定义业务解决 handler
            ch.pipeline().addLast(new NettyServerHandler());
          }
        });

    // 9. 启动服务端并绑定端口, 同时将异步改为同步
    // ChannelFuture future = serverBootstrap.bind(9999).sync();// 同步
    ChannelFuture bind = serverBootstrap.bind(9999);// 异步
    bind.addListener(future -> {if (future.isSuccess()) {System.out.println("端口绑定胜利");
      } else {System.out.println("端口绑定失败");
      }
    });

    System.out.println("服务器启动胜利....");
    // 10. 敞开通道(并不是真正意义上的敞开, 而是监听通道敞开状态)
    //     敞开连接池
    bind.channel().closeFuture().sync();
    bossGroup.shutdownGracefully();
    workerGroup.shutdownGracefully();}

TCP 粘包拆包

场景

如果发送两个独立的数据包,但服务端一次性接管到了两个数据包则称为粘包;

如果第二个数据包比拟大,服务端两次读取到了两个数据包第一次读到了实现的第一个包和第二个包的局部内容,第二次读取到第二个包的残余局部则称为拆包。

如果两个数据包都跟大,服务端可能会分屡次能力将两个数据包接管齐全,期间会产生屡次拆包。

起因

因为数据的发送和接管方都须要通过操作系统的缓冲区,缓冲区数据沉积,导致多个申请数据粘在一起,拆包则可了解为发送的数据大于缓冲区,进行拆分解决。

解决方案

  1. 业内罕用

    • 音讯长度固定,累计读取长度为定长的报文。
    • 换行符作为音讯结束符
    • 非凡分隔符作为音讯完结标记,例如回车
    • 音讯头定义长度字段标识音讯总长度
  2. Netty 中的解决方案

    Netty 提供的解码器

    • 定长拆包器 FixedLengthFrameDecoder。拆分定长的数据包。
    • 行拆包器 LineBasedFrameDecoder,以换行符为分隔符拆分
    • 分隔符拆包
    • 数据包长度,此办法要求协定中要蕴含数据包的长度

正文完
 0