I/O

优质
小牛编辑
135浏览
2023-12-01

注:本节未经校验,如有问题欢迎提issue

介绍

akka.io包是由Akka和spray.io团队协作开发的。它的设计结合了spray-io模块的经验,并共同进行了改进,使其适应基于actor服务的更加普遍的消费需求。

该 I/O 实现的指导设计目标是要达到极端的可扩展性,要毫不妥协地提供一个API正确匹配底层传输机制,并且是完全的事件驱动、无阻塞和异步。该API命中注定是网络协议实现和构建更高抽象的坚实基础;它不是为终端用户提供的全套的高级别的NIO包装。

术语,概念

I/O API完全是基于actor的,意味着所有的操作实现都是通过消息传递而不是直接方法调用。每个 I/O 驱动程序 (TCP、 UDP) 有一个特殊的actor,被称为一个管理器,用作 API 的入口点。I/O 被分成几个驱动程序。用于某个特定驱动程序的管理器是通过 IO 入口点获取的。例如下面的代码查找 TCP 管理器,并返回其 ActorRef

import akka.io.{ IO, Tcp }
import context.system // implicitly used by IO(Tcp)

val manager = IO(Tcp)

管理器接收 I/O 命令消息并实例化工作actor作为回应。工作actor将自身返回给 API 用户作为发送该命令的答复。例如给TCP 管理器发送Connect命令后,管理器创建了代表 TCP 连接的actor。当该actor通过发送一个Connected消息宣布自身后,所有与给定 TCP 连接相关的操作都可以通过发送消息到连接actor来调用。

DeathWatch和资源管理

I/O 工作actor接收命令,并且也发出事件。它们通常需要一个用户端对应的actor监听这些事件 (此类事件可以是入站的连接,传入的字节或写操作确认)。这些工作actor观察它们的对应监听。如果监听器停止工作,则工作actor将自动释放它所拥有的任何资源。这种设计使得该 API 更能抵抗资源泄漏。

多亏I/O API是完全基于actor设计的,相反的方向也可以工作:一个负责处理连接的用户actor可以观察连接actor,如果它意外终止也将收到通知。

写模型(Ack, Nack)

I/O设备有一个最大吞吐量来限制写操作的频率和大小。当一个应用程序试图推相比设备处理能力更多的数据时,驱动程序不得不缓冲字节,直到设备能够继续写他们。缓冲可以处理短暂的密集写入——但没有缓冲区是无限的。这时需要"流控制"来避免设备缓冲区不足的问题。

Akka支持两种类型的流量控制:

  • 基于Ack,当写操作成功的时候,驱动程序通知写者。

  • 基于Nack,当写操作失败时,驱动程序会通知写者。

每一种模型在Akka I/O的 TCP 和 UDP 实现中都可用。

单独的写操作可以通过在写入消息(TCP中的Write 和UDP中的Send)中提供一个 ack 对象来确认。写操作完成时工作者将发送 ack 对象给写actor。这可以用于实现基于Ack的流量控制;只有当老数据被确认时才发送新数据。

如果写入(或任何其他命令)失败,驱动程序会发送具有该命令一个特殊消息(UDP 和 TCP中是CommandFailed)来通知actor。此消息也会通知写者一个失败,作为那个写的一个nack。请注意,在基于nack 的流控制设置中,写者必须准备到,失败的写操作可能不是最近写操作的事实。例如,对于W1的写入失败通知可能在后来的写命令 W2W3被发送之后到达。如果写者想要重发送任何nack消息,它可能需要保留一个挂起消息的缓冲区。

警告

一个确认的写并不意味着确认送达或存储的;收到一个写ack只是表明I/O 驱动程序成功处理了写操作。这里描述的 Ack/Nack 协议是一种流量控制手段而不是错误处理。换句话说,数据仍然可能会丢失,即使每一个写操作都被确认。

ByteString

为了保持隔离,actor应该只通过不可变对象沟通。ByteString 是bytes的不可的容器。它被用在Akka I/O系统中,作为在jvm上处理IO的传统字节容器,如Array[Byte]ByteBuffer的一种高效的、 不可变的替代者。

ByteString 是一个绳状)数据结构,它不可变且提供了高效地连接和切片操作(完美的 I/O). 当两个ByteString被连接在一起时,是将两者都保存在结果ByteString内部而不是将它们复制到新的Array中. 像 droptake这种操作返回的 ByteString仍引用之前的 Array, 只是改变了外部可见的offset和length。我们花了很大力气保证内部的 Array 不能被修改. 每次当不安全的 Array 被用于创建新的 ByteString 时,会创建一个防御性拷贝。如果你需要一个ByteString为其内容占用尽可能少的内存,使用 compact 方法来获取一个 CompactByteString 实例。如果 ByteString 表示只是原始数组中的一片,这将导致复制在片中的所有字节。

ByteStringIndexedSeq继承了所有方法,它也有一些新的方法。更多信息请参考 akka.util.ByteString 类和其伴生对象的 ScalaDoc。

ByteString还带有它自己优化类——ByteStringBuilderByteIterator,在普通生成器和迭代器之外提供额外的功能。

与 java.io 的兼容性

ByteStringBuilder 可以通过 asOutputStream 方法包装为一个 java.io.OutputStream。同样,ByteIterator可以通过 asInputStream包装为一个java.io.InputStream。使用这些,akka.io 应用程序可以集成基于 java.io 流的遗留代码。

深入体系结构

关于内部体系结构设计有关的详细信息请参阅I/O 层设计。