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.swiftpb前缀表示它是协议缓冲区生成的类。 现在,我们准备将这段代码集成到我们的项目中。 您不想编辑此文件,我们只想使用此文件。 如果您不小心进行编辑,请不要费力。 只需再次运行编译器即可重新生成数据。

设定

我们将建立一个名为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.swiftchannelActive方法应类似于:

电影服务器

在服务器上,我们创建一个新的MovieServerHandler来处理传入的数据。 该代码类似于我们先前从网络读取String时所做的事情。 我们将NIOAnyNIOAnyByteBuffer并获得可读的字节数。

  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字段。 本示例不涉及那种情况。