SwiftNIO的协议缓冲区
总览
您可以在.proto
文件中定义消息模式,然后使用protobuf编译器为您的语言生成数据结构,从而减少编写样板分析和数据访问代码的过程。 然后,您可以与系统的其他部分共享此.proto
文件,并为这些语言生成数据访问类。 例如,您可能有一个Java后端以及一个iOS,Android和Web前端,它们都共享此.proto
文件来定义共享模式。
优点
Protobuf设计为快速而紧凑。 根据Google的说法,协议缓冲区比XML 小3至10倍,并且快20至100倍 。
Protobuf与语言无关,并提供向后兼容性。 您可以使用完全不同的语言,用新的高性能系统组件替换旧的缓慢系统组件,并确信只要您使用相同的.proto
模式,系统就可以继续工作。 实际上,您甚至可以在.proto
文件中的消息格式中添加新字段,并且系统将继续运行。 具有旧.proto
系统在解析时将仅忽略新字段。
缺点
Protobuf并不适合所有人,尽管它们支持多种语言,但可能不支持您的语言。
XML和JSON比Protobuf更具可读性。 由于它们的设计紧凑,因此当它们通过网络发送时,protobuf会删除字段名称。 仅当您具有.proto
文件时,protobuf才有用。
Google设计了用于应用程序间通信的协议缓冲区。 如果您正在构建供外部消费者使用的公共API,则您确实不希望服务器的客户端使用您定义的.proto
文件,为其服务生成数据访问类并以这种方式与您的服务进行通信。
入门
您将需要protobuf编译器来生成特定于语言的文件。 这里有安装说明:https://github.com/apple/swift-protobuf。 由于已经安装了Homebrew,因此选择了Homebrew选项。
$ brew install swift-protobuf
注意:此安装可能需要一些时间…
要检查其是否正确安装,可以键入:
$ protoc-版本
//我的输出是libprotoc 3.5.1
.proto
Protobuf使用扩展名.proto
作为其文件格式。 您创建.proto
文件来定义您的消息传递模式,然后运行protobuf编译器生成您的Swift代码。
您可以使用任何IDE或文本编辑器来创建.proto
文件,包括Xcode。 但是,我不喜欢使用Xcode的原因是缺少适当的缩进和语法突出显示。 我最终在他们的市场中使用了VS Code以及免费的vscode-proto3 protobuf扩展。
现在,我们已经安装了protobuf编译器,让我们创建一个将通过网络发送的Movie
类型。 创建一个名为movie.proto
的新文件。
.proto
文件的第一行是要使用的协议缓冲区版本。 在撰写本文时,我们将使用最新版本proto3。
语法=“ proto3”
接下来,我们使用message
关键字声明消息类型。 对于我们来说,这将是Movie
类型的消息。
消息电影{
}
接下来,为电影类型声明一个枚举。 您可以在全局范围内的message外部声明一个枚举,但是对于我们的示例,由于流派正在描述电影,因此我们将在Movie
内部声明它。
消息电影{
枚举类型{
COMEDY = 0;
动作= 1;
恐怖= 2;
浪漫= 3;
DRAMA = 4;
}
}
让我们在Movie
添加一些实际字段。
消息电影{
枚举类型{
COMEDY = 0;
动作= 1;
恐怖= 2;
浪漫= 3;
DRAMA = 4;
}
字符串标题= 1;
流派流派= 2;
int32 year = 3;
}
在这里,我们定义了一个string
来描述标题,类型的enum
和电影发行年份的int32
。 您可以在Google协议缓冲区文档中找到有关不同字段的更多信息。
字段名称应为小写字母。 如果您有多个单词,则首选方法是使用下划线(“ _”)分隔单词。 不建议使用Pascal套管和Camel套管。 不用担心,当我们运行protobuf编译器时,它将生成遵循Swift的标准编码样式的Swift代码。
字段中的最后一个元素是字段标记。 这是通过电线发送的,以减小有效负载的大小。 使用JSON,我们将发送字段说明以及使其更易读的值,以及更大的有效负载。
{“ title”:“ Avengers”}
但是使用protobuf,它更像是:
1复仇者
这些字段标签用作标识符,并且必须是唯一的整数。 它们将被转换为字节并通过导线发送,因此较小的值会更有效。 如果您有很多可以选择的字段,则最有可能设置的字段应具有最少的数字。 未设置的可选字段不会通过电线发送。 字段的默认值为它们的零值(例如,对于int为0,对于字符串为空字符串)。 请注意,对于枚举,您应该为值0定义一个默认值,因为枚举的默认值为0。
这是完成后我们的movie.proto
样子。
编译
现在我们有了消息格式,我们可以使用protobuf编译器生成数据访问类。 将目录切换到创建.proto
文件的位置,然后运行该文件以生成Swift结构。
$ protoc --swift_out =。 movie.proto
分解此命令,我们使用带有--swift_out
参数的protobuf Swift编译器。 这告诉编译器我们要生成一个Swift源文件。 该参数期望您要生成源文件的目录的位置。 在本例中,我们使用.
表示我们希望将源文件生成到当前目录。 我们传递的下一个参数是movie.proto
,这是我们的输入文件。
编译器生成movies.pb.swift
。 pb前缀表示它是协议缓冲区生成的类。 现在,我们准备将这段代码集成到我们的项目中。 您不想编辑此文件,我们只想使用此文件。 如果您不小心进行编辑,请不要费力。 只需再次运行编译器即可重新生成数据。
设定
我们将建立一个名为MovieClient
的客户端和一个名为MovieServer
的服务器,并使用MovieServer
将我们的Movie
结构从客户端发送到服务器。 如果您需要有关使用Swift Package Manager(SPM)和SwiftNIO设置客户端和服务器的详细信息,请查看本文:SwiftNIO入门。
除了使用SwiftNIO,我们还将使用Swift protobufs,因此我们需要在Package.swift
文件中将其声明为依赖项。
我的MovieClient
Package.swift
文件如下所示:
// swift-tools-version:4.0
// swift-tools-version声明构建此软件包所需的最低Swift版本。
导入PackageDescription
让包=包(
名称:“ MovieClient”,
依赖项:[
.package(URL:“ https://github.com/apple/swift-nio.git”,.exact(“ 1.6.0”)),
.package(URL:“ https://github.com/apple/swift-protobuf.git”,.exact(“ 1.0.3”))
],
目标:[
。目标(
名称:“ MovieClient”,
依赖项:[ “ NIO”,“ SwiftProtobuf” ]),
]
)
同样,我的MovieServer
Package.swift
文件如下所示:
// swift-tools-version:4.0
// swift-tools-version声明构建此软件包所需的最低Swift版本。
导入PackageDescription
让包=包(
名称:“ MovieServer”,
依赖项:[
.package(URL:“ https://github.com/apple/swift-nio.git”,.exact(“ 1.6.0”)),
.package(URL:“ https://github.com/apple/swift-protobuf.git”,.exact(“ 1.0.3”))
],
目标:[
。目标(
名称:“ MovieServer”,
依赖项:[ “ NIO”,“ SwiftProtobuf” ]),
]
)
注意:我们在本文中使用了确切的依赖关系,但是您也可以利用SPM的语义版本控制并使用:
.package(网址:“ https://github.com/apple/swift-nio.git”,来自“ 1.6.0”),
.package(网址:“ https://github.com/apple/swift-protobuf.git”,来自:“ 1.0.3”)
这样,我们依赖于所需的次要版本,仍然可以从Apple的出色团队以及SwiftNIO的其他贡献者那里获得所有最新和最佳的修复以及性能增强!
更新Package.swift
,请不要忘记运行:
$ swift软件包更新
$ swift包generate-xcodeproj
现在已经设置了项目,将movie.pb.swift
文件拖到两个Xcode项目中。 确保选中了“ 如果需要复制项目”框。
电影客户端
现在,通过网络发送简单的String
,我们要做的主要更改是发送Movie
实例。 我们在ChannelInboundHandler
子类MovieClientHandler
执行此操作。 因为我们在项目中包含了movie.pb.swift
,所以我们现在可以创建Movie
对象。
var movie = Movie()
movie.genre = .action
movie.title =“复仇者联盟:无限战争”
movie.year = 2018
然后,我们可以使用protobuf生成的文件中的serializedData()
函数对数据进行serializedData()
。
让binaryData:数据=试试movie.serializedData()
从那里开始,我们遵循之前的操作,创建一个ByteBuffer
并写入缓冲区。
var buffer = ctx.channel.allocator.buffer(容量:binaryData.count)
buffer.write(bytes:binaryData)
至此,我们准备发送数据了。 但是,让我们介绍SwiftNIO的另一个概念EventLoopPromise
。
承诺与期货
取自SwiftNIO的Github页面:
EventLoopFuture
本质上是一个函数的返回值的容器,该函数的返回值将在将来的某个时间填充。 每个EventLoopFuture
有一个对应的EventLoopPromise
,这是将结果放入其中的对象。 当诺言成功时,未来就会实现。
如果您熟悉另一种编程语言的承诺,那么您应该感到宾至如归。 通过使用promise和future,我们能够编写在事件发生时将被执行的代码。 例如,当您从互联网上下载大图像时,您不想阻塞主线程并使UI没有响应。 相反,您的目标应该是开始一个长任务,然后在完成该长任务时执行一个操作。 承诺提供此功能。
我们创建一个Promise,该Promise将消息打印到控制台,然后在成功发送数据后关闭来自客户端的通道。
让我们保证:EventLoopPromise = ctx.eventLoop.newPromise()
promise.futureResult.whenComplete {
打印(“发送数据,关闭通道”)
ctx.close(承诺:无)
}
现在,当我们将数据写入套接字时,我们通过了这个承诺。
ctx.writeAndFlush(wrapOutboundOut(buffer),promise:promise)
MovieClientHandler.swift
的channelActive
方法应类似于:
电影服务器
在服务器上,我们创建一个新的MovieServerHandler
来处理传入的数据。 该代码类似于我们先前从网络读取String
时所做的事情。 我们将NIOAny
解NIOAny
到ByteBuffer
并获得可读的字节数。
var缓冲区= unwrapInboundIn(数据)
让可读字节=缓冲区。可读字节
我们使用guard
语句从缓冲区读取和解包字节。
保护让接收到的= buffer.readBytes(length:可读字节)else {
返回
}
然后,我们使用接收的字节创建一个Data
对象。
让receiveData = Data(字节:已接收,计数:received.count)
使用movie.pb.swift
生成的方法movie.pb.swift
,我们可以反序列化数据。
let movie = try Movie(serializedData: receivedData)
现在,您可以在应用程序中处理Movie
类型了,而不是原始字节!
MovieServerHandler.swift
中的MovieServerHandler.swift
方法应类似于:
运行
服务器:
- 选择要在My Mac上运行的
MovieServer
方案,然后单击“运行” - 一旦在Xcode控制台中看到一条消息,说明您已在端口
3010
上进行了连接,请运行客户端
客户:
- 选择
MovieClient
方案以在“我的Mac”上运行,然后单击“运行”
在MovieClient
的控制台中,您应该看到已发送数据,正在关闭通道,然后关闭 以退出代码0结束的程序 。 客户端已连接到服务器,发送了Movie
实例,然后关闭了Promise中的通道。 在MovieServer
的控制台中,您应该看到显示了收到的Movie
对象!
对于完整的项目:
jonathanwong / MovieClient
具有SwiftNIO和协议缓冲区的MovieClient。 github.com jonathanwong /电影服务器
具有SwiftNIO和协议缓冲区的MovieServer。 github.com
结论
您探索了协议缓冲区,定义了一个.proto
文件来描述Movie
模式,并使用protobuf编译器生成了Swift数据访问代码。 您了解了使用protobufs序列化和反序列化Movie
实例有多么容易。 从那里,您可以使用SwiftNIO通过TCP发送一个Movie
实例,并简要了解了Promise。
编辑: 在真实的TCP服务器中,具有通过网络传输的各种长度的多种不同消息类型,您将需要一种方法来确定消息边界在哪里。 例如,您可以在每个消息之前添加一个length字段。 本示例不涉及那种情况。