8.8IO多路复用与零拷贝

协议原理:HTTP为例

有许多服务器,比如http服务器,ftp服务器,websocket服务器等等。
举例http服务器原理:

  1. 创建一个ServerSocket,监听并绑定一个端口
  2. 一系列客户端来请求这个端口
  3. 服务器使用Accept,获得一个来自客户端的Socket连接对象
  4. 启动一个新线程处理连接 :
    • 读Socket,得到字节流
    • 解码协议,得到Http请求对象
    • 处理Http请求,得到一个结果,封装成一个HttpResponse对象
    • 编码协议,将结果序列化字节流 写Socket,将字节流发给客户端
  5. 继续循环步骤3

http服务器之所以叫http服务器是因为编码解码协议都是用的http协议,如果协议是redis协议,websocket协议那么就是redis服务器,websocket服务器。

BIO与NIO

NIO是由操作系统提供的系统调用。是IO多路复用的代名词。早期这个操作系统调用的名字叫select,后来逐步演化成了

  • epoll(Linux操作系统)
  • kqueque(MAC操作系统)
    要讲NIO得先讲BIO

BIO-blocking IO(同步阻塞模式)

BIO全称叫Blocking IO,为同步阻塞模式
BIO同步阻塞IO模型是最简单的IO模型,用户线程在内核进行IO操作时如果数据没有准备号会被阻塞。
BIO流程
伪代码如下

{
    // 阻塞,直到有数据
    read(socket, buffer);
    process(buffer);
}

BIO通讯方式的特点

  1. 一个线程负责连接,多线程则为每一个接入开启一个线程。
  2. 一个请求一个应答。
  3. 请求之后应答之前客户端会一直等待(阻塞)。

BIO通讯模型
如果使用BIO模型使用到的http服务器,会出现下面三个阻塞:

  1. 客户端监听(Listen)时,Accept是阻塞的,只有新连接来了,Accept才会返回,主线程才能继
  2. 读写socket时,Read是阻塞的,只有请求消息来了,Read才能返回,子线程才能继续处理
  3. 读写socket时,Write是阻塞的,只有客户端把消息收了,Write才能返回,子线程才能继续读取下一个请求

传统的BIO模式下,从头到尾的所有线程都是阻塞的,这些线程就干等着,占用系统的资源,什么事也不干。

NIO-non-blocking IO(非阻塞模式)

NIO实际上采用了Reactor 编程模型的思想,Reactor之所以能高效是采用了采用了分而治之和事件驱动的设计。

分而治之

Reactor 模式将处理过程分为多个小任务,每个任务执行一个非阻塞的操作,任务通常由一个 IO 事件触发执行。
BIO 线程是以 read->decode->process->encode->send 的顺序串行处理,NIO 将其分成了三个执行单元:读取、业务处理和发送。处理过程如下

  • 读取:如果无数据可读,线程返回线程池;发生读IO事件,申请一个线程处理读取,读取结束后处理业务
  • 业务处理:线程同步处理完业务后,生成响应内容并编码,返回线程池
  • 发送:发生写IO事件,申请一个线程进行发送

可以看出一个明显的区别就是,一次请求的处理过程是由多个不同的线程完成的,感觉和指令的串行执行和并行执行有点类似。分而治之的关键在于非阻塞,这样就能充分利用线程,压榨 CPU,提高系统的吞吐能力。

事件驱动

通常比其他模型更高效,它使用的资源更少,不用针对每个请求启用一条线程,减少了上下文切换,减少阻塞。但任务调度可能会慢,必须手动将事件和处理动作绑定。

通常编程比较困难,它必须为服务设计多个逻辑状态,以便跟踪和中断恢复,这也是在非阻塞编程中有大量状态机运用的原因。

NIO 中总共设计了 4 种事件,每个事件发生都会调度关联的任务,分别是:

  • OP_ACCEPT: 服务端监听到了一个连接,准备接收
  • OP_CONNECT: 客户端与服务器连接建立成功
  • OP_READ: 读事件就绪,通道有数据可读
  • OP_WRITE: 写事件就绪,可以向通道写入数据

采用事件机制用一个线程把Accept,读写操作,请求处理的逻辑全干了。如果什么事都没得做,它也不会死循环,它会将线程休眠起来,直到下一个事件来了再继续干活,这样的一个线程称之为NIO线程。
用伪代码表示:

while true {
    events = takeEvents(fds)  // 获取事件,如果没有事件,线程就休眠
    for event in events {
        if event.isAcceptable {
            doAccept() // 新链接来了
        } elif event.isReadable {
            request = doRead() // 读消息
            if request.isComplete() {
                doProcess()
            }
        } elif event.isWriteable {
            doWrite()  // 写消息
        }
    }
}

Reactor 模式

Reactor 是一种设计模式,wikipedia 对其定义如下:

Reactor 是一个或多个输入事件的处理模式,用于处理并发传递给服务处理程序的服务请求。服务处理程序判断传入请求发生的事件,并将它们同步的分派给关联的请求处理程序。

Reactor 模式按照职责不同,通常可以把线程分为 Reactor 线程、IO 线程和业务线程:

  • Reactor 线程:轮询通知发生IO的通道,并分派合适的 Handler 处理
  • IO 线程:执行实际的读写操作
  • 业务线程:执行应用程序的业务逻辑
单线程模式

单线程版本就是用一个线程完成事件的通知、实际的 I/O 操作和业务处理。Reactor 作用就是要迅速的触发 Handler ,显然 Handler 处理的过程会导致 Reactor 变慢,此时可以将 非 IO 操作从 Reactor 线程中分离。
单线程模式

多线程版本

多线程版本将业务处理和 I/O 操作进行分离,Reactor 线程只关注事件分发和实际的 IO 操作,业务处理如协议的编解码都分配给线程池处理。可能会有这样的情况发生,业务处理很快,大部分的 Reactor 线程都在处理 IO,导致 CPU 闲置,降低了响应速度。
多线程版本

主从 Reactor 版本

主从 Reactor 版本设计了一个 主Reactor 用于处理连接接收事件,多个 从Reactor 处理实际的 I/O,分工合作,匹配 CPU 和 IO 速率。
主从Reactor版本

什么是TCP 粘包/拆包 ?

基本原理就是不断从 TCP 缓冲区中读取数据,每次读取完都需要判断是否是一个完整的数据包 如果当前读取的数据不足以拼接成一个完整的业务数据包,那就保留该数据,继续从 TCP 缓冲区中读取,直到得到一个完整的数据包。如果当前读到的数据加上已经读取的数据足够拼接成一个数据包,那就将已经读取的数据拼接上本次读取的数据,构成一个完整的业务数据包传递到业务逻辑,多余的数据仍然保留,以便和下次读到的数据尝试拼接。
拆包方法:

  1. 固定长度
  2. 行拆包器
  3. 分隔符拆包器
  4. 基于长度域拆包

零拷贝

传统意义的拷贝是在发送数据的时候,传统的实现方式是:

  1. File.read(bytes)
  2. Socket.send(bytes)

这种方式需要四次数据拷贝和四次上下文切换:

  1. 数据从磁盘读取到内核的read buffer
  2. 数据从内核缓冲区拷贝到用户缓冲区
  3. 数据从用户缓冲区拷贝到内核的socket buffer
  4. 数据从内核的socket buffer拷贝到网卡接口(硬件)的缓冲区

明显上面的第二步和第三步是没有必要的,如何省去这两步需要底层操作系统支持。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值