Slug jeff bergier cover

开发一款 Swift 服务器应用的经验总结

当你在使用 Xcode 开发 Swift 应用的时候,有没有考虑过 Linux 系统?或者说有没有想过开发一款网页应用程序?甚至接入微软出品的邮件系统 (Microsoft Exchange Servers)?😱我想大概不会吧。

而在 Jeff Bergier 所做的工作中,就将这三者集为一体开发了一款应用程序。特别要指出的是,整个应用程序只用 Swift 语言进行编码。从 Darwin 和 Foundation 的安全世界移步到 Linux 的西部蛮荒之地,你可能会遇到相当多的“障碍物”。本次讲演来自 Swift Language 组织,Jeff 将与你分享他的经历,帮助你在新的征途上走得更远,更轻松!


介绍 (0:36)

我是来自 Riverbed 公司的一名交互设计师。我司专门为企业开发网络硬件相关的产品,虽然与移动开发、iOS 或苹果公司并无紧密联系。不过我在接触 Swift 之前就学习过 Objective-C 语言,并且自学 iOS 大约有三年时间了。

此款应用程序的由来 (1:20)

我开发的这款 Swift 后端应用程序用于会议室的调度工作。它起初是一个纯粹的 StoryBoard 原型。StoryBoard 真的是一款相当棒的原型工具——你可以拖拽任意多个表视图到 StoryBoard 中,然后在它们之间点击进行切换,轻松制作一部演示视频,以便人们在不获悉代码的情况下就能了解你想要实现的东西。

由于大家都希望在工作中接入 Outlook 办公软件,因此我们需要将 StoryBoard 原型在 Swift Playground 中实现。想要接入 Outlook 办公软件,我们需要用到 XML 格式的 Exchange 应用接口。工作期间,我基于 CherryPy 框架配合 Python 开发了真正意义上第一版会议室调度应用程序。

由于我不太满意 Python ,加之想学习下 Swift 服务端开发,所以闲暇之余我重写了这款应用程序。目前有几个流行的 Swift 服务端框架,特别是 IBM 开发的 Kitura

然而我最终没有使用 IBM 的框架,即使他们为 Linux 提供了一个非常赞的 GUI 帮助工具。 权衡之下我选择了 Perfect,因为看起来它似乎最容易上手和运行。

为什么选择 Swift 进行服务端开发? (7:01)

我想学习 JavaScript 和 Swift 服务端开发。尽管 Python 做得不错,但我更偏好于 Xcode 的自动补全和语法检查特性。在 Swift 服务端开发中,你不需要使用界面构建器,这将大大减少崩溃的出现率。

Receive news and updates from Realm straight to your inbox

Swift 中的类型安全特性也是相当不错的。它阻止非预期类型参数被传递到函数中。此外,Safari 和 Xcode 的调试工具非常近似。

应用的构建方式 (7:01)

这款网页应用采用了时下流行的 Javascript 加载 HTML 的方式来呈现内容。JavaScript 发送一个 post 请求给服务器,然后返回一个 JSON 格式的字符串,客户端中的 JavaScript 代码将返回的 JSON 字符串解析成 HTML 元素然后再呈现出来。这里我用到了 Bootstrap 和 JavaScript,以及使用一些 cookie 来存储会话信息。另外,我使用 AES 加密一些凭证并将其存储到 cookie 中,这主要考虑到之后用户再次访问页面时可以解密密码。

诸如使用 CherryPy 的后端应用程序,大部分 Web 框架的工作方式都是类似的,都采用了路由概念。我为所有 / 斜杠的 GET 和 POST 请求都设置了监听,接着递交请求给 Swift 代码处理对应事务。Perfect 框架并没有会话管理者,所以我自己实现了设置和重置请求的 cookie 操作。每个请求都有一个 JSON 格式的有效载荷,Swift 会通过解析来它们处于进程中的哪个步骤,以及选择了哪些数据和 Exchange 服务器进行通信,之后向它们返回一个新的 JSON 字符串。

忽略 .XcodeProj 工程文件 (11:02)

框架并非一定需要创建 Xcode 工程文件,如果有的话当然是个不错的选择,你可以在 Xcode 工具编辑代码。由于生成的 XcodeProj 工程文件完全是一次性的,我甚至不把它放进代码仓中。这里我更倾向于使用 Swift 包管理工具。

由于某些原因,它将操作系统构建版本设置为 10.10 ,因此如果你使用任何较新的代码,必须明确这些代码只能在 10.12 版本下被执行。此外,你的 Web 框架会用到一些静态文件,必须添加一个副本文件存储路径 (copy files phase),否则 Web 服务器会找不到它们。

Foundation 的主要介绍 (17:13)

一般情况下,我仍然会使用 Foundation 中的类型,即使 Xcode 对于验证代码在 Linux 系统上是否有效还不够友好。起初我使用 Perfect 时还没有 NSURLSession 这样的类,但是现在已经加入了。

下面是我写的一段代码,用来对日期进行“取整”操作;你可以取到最近的 15 分钟时刻。正如你所看到的那样,它看起来就像标准的 iOS Foundation 代码。


extension Date {

private static func roundedTimeInterval(from date: Date) -> TimeInterval {
   let dc = Calendar.current.dateComponents([.minute, .second], from: date)
   let originalMinute = Double(dc.minute ?? 0)
   let originalSeconds = Double(dc.second ?? 0)
   let roundTo = 15.0
   let roundedMinute = round(originalMinute / roundTo) * roundTo
   let interval = ((roundedMinute - originalMinute) * 60) - originalSeconds
   return interval
}

 mutating func roundMinutes() {
 let timeInterval = type(of: self).roundedTimeInterval(from: self)
 self += timeInterval
 }
}

在 Linux 系统上进行开发时,如何判断 Foundation 类型是否有效呢? 你可以前往 Apple 的 GitHub 页面,找到 Foundation 代码仓查看相关信息。如果你看到的代码和 Foundation 代码仓给出的类似,这意味着它可能是被允许的。


public struct DateComponents : ReferenceConvertible, Hashable, Equatable, _MutableBoxing {
  public typealias ReferenceType = NSDateComponents

  internal var _handle: _MutableHandle<NSDateComponents>

  //// Initialize a 'DateComponents', optionally specifying values for its fields.
  public init(calendar: Calendar? = nil,
              timeZone: TimeZone? = nil,
              era: Int? = nil,
              ...
              )
    _handle = MutableHandle(adoptingReference: NSDateComponents())
    if let _calendar = calendar { self.calendar = _calendar }
    if let _timezone = timeZone { self.timeZone = _timeZone }
    if let _era = era { self.era = era }
    ...
}

正如你所看到的,这是一个 NSURLAuthenticationChallenge 对象,此刻还没有被实现。 这是一个不好的预兆。 因为它会在运行时引发崩溃。

open class URLAuthenticationChallenge: NSObject, NSSecureCoding {
  static public var supportsSecureCoding: Bool {
    return true
  }

  public required init?(coder aDecoder:NSCoder) {
    NSUnimplemented()
  }

  open func encode(with aCoder: NSCoder) {
    NSUnimplented()
  }
}

下面是我对前面实现算法的一次改写,看起来这种实现方式更加合适(我设定取整的单元为15分钟)。


import Foundation

// 获取日期和包含的组件
var dc = Calendar.current.dateComponents(
 [.year, .month, .day, .hour, .minute, .second, .calendar, .timeZone],
 from: Date()
)

// 取到原始数据进行取整
let originalMinute = Double(dc.minute ?? 0)
let roundTo = 15.0
let roundedMinute = Int(round(originalMinute / roundTo) * roundTo)

// 修改分钟和小时的值
dc.minute = roundedMinute
dc.second = 0

// 生成新的日期
let roundedDate = dc.date! // 在 Linux 中会引发崩溃

// fatal error: copy(with:) is not yet implemented: file Foundation/NSCalendar.swift, line 1434

这里我分别设置 minutesecond 属性值为 roundMinute0,接着通过原始日期得到一个时间间隔,对它进行增减运算,最终获得一个取整后的新日期替换掉原始值。事实证明,这部分代码在 Linux 上会引发崩溃,因为 NSDate 并没有实现 copyWithZone 协议。你可能会遇到像这样的小意外。

Linux 上频繁测试的必要性 (20:05)

你无法预料何时被告知 NSUnimplementedcopyWithZone 没有实现。 如果你真的担心,我建议为每次提交设定持续集成(CI)事项。 我一直在使用一款名为 Veertu 的应用程序;你可以前往 App 商店免费下载到,这款应用程序以 headless 模式运行虚拟机。

你不需要面对 Linux 系统可怕的安装界面。 它会帮你自动下载和安装 Linux 系统。

借助 JSON 开发更简单 (21:21)

你已经熟知如何处理 NSJSONSerialization。它的处理结果其实就和 data.jsonEncodedString() 返回值一样(译者注:返回String 类型),同样你可以对一些集合调用 jsonEncodedString 方法。而对于字符串和其他一些东西可以调用 jsonDecode 做相反处理,所以你可以将任何字符串转换成对象,确保它是真正的 JSON 格式数据就可以了。 如果你想要进行异常处理,try 语句已经提供有效的错误抛出时机。


import PerfectLib
let data: [String : Any] = [
 "date" : "2016-01-01T12-12-00",
 "name" : "Billy",
 "age" : 22,
 "emails" : [
 "[email protected]",
 "[email protected]"
 ]
]
let json = try data.jsonEncodedString()

生成随机数的难点 (21:58)

arc4random_uniform() 并不是 Linux 系统中的函数,但我也注意到一些系统下支持你安装使用它。

#if os(Linux)
import Glibc
#else
import Darwin
#endif
for i in 1 ... 5 {
 #if os(Linux)
 let randomNumber = random()
 #else
 let randomNumber = Int(arc4random_uniform(UInt32.max))
 #endif
 print("Round \(i): \(randomNumber)")
}

这段代码生成五个随机数并打印出来。如果是在 Linux 系统上,它调用 Glibc 的 random() 函数。而 arc4random_uniform() 这个随机函数是完全无用的。

产生随机数最简单快捷的方式是访问 devrandom (译者注:linux 下的/dev/random ),读取随机数文件的字节流。它适用于 Mac 和 Linux 系统,所以你不用分开实现 arc4random 方法。 当前还存在一些产生随机数的库,例如 TurnstileCrypto

避免使用 #if os(Linux) (24:25)

如果你这么干,将失去所有 Xcode 的帮助,包括语法检查以及其他一些最基本的事情,甚至 Xcode 无法识别代码的有效性。从本质上来说,#if os(Linux) 声明作用域中的代码块可视为不存在。我强烈建议避免这种方式,正在试图寻求一个解决方案,能够适用于 MacOS 和 Linux 这两个平台。

另一件事,这主要是在 Linux 平台中,如果你对 Foundation 很失望,那么你可以选择任何一个框架作为新的 Foundation 框架来使用。 就拿 Perfect 来说,它的内容丰富,模块化形式非常显著。

线程 (26:13)

线程无法工作。 但你可以设置定时器和计划任务,允许你手动触发,一次响应之后不再触发。我认为这只是 Perfect 这么干而已; 他们有自己的 runloop 处理将期望的 NSTimer 分开。

Prefect 拥有自己的线程框架,当然你也可以选择手动创建。 如果它们是串行或并发的,您可以派发工作给它们。

强制解包的坏处 (28:31)

强制解包 (forced unwrapping) 非常糟糕。 当你在某人的 iOS 应用程序中强制解包一些东西,该用户的设备会引发崩溃。而当你在服务器上强制解包某个东西,那么每个用户都会受影响。

问答时间到 (31:24)

问:当你调用 URLs 时遇到了哪些限制因素?

Jeff: 我编码时更喜欢生成多个对象,这样代码看上去更整洁、更容易慢慢阅读,而不是长时间停留在某处。我没有测评过应用的性能,它没有在负载下进行测试。一般来说,Perfect 的开发者已经对各种 Swift 网页框架和传统的网页框架之间进行性能比较,Swift 似乎做得很好。但我老实说真的没有测试过它的性能。

问:在未来三到五年内,您如何看待服务器端 Swift 的前景?

Jeff: 我认为现在还为时尚早。即使创业公司想要在生产中使用它,我认为至少还要一年的时间,而对于大公司来说还要更久。

问:使用 Swift 而不是 Python 的代价是什么?

Jeff: Jeff:我发现 Xcode 中的 autocompletes 和 stuff 是一个巨大的帮助。类型检查也相当有用。我们在 Python 中编写它的方式不是真正的面向对象,只是因为每个人都认为是罢了。 不过 Python 能够更好地处理 JSON 类型的数据结构。

About the content

This content has been published here with the express permission of the author.

Jeff Bergier

Jeff comes to iOS development from an unusual past. He earned a B.S. in Industrial Design and currently works as an in-house UX Designer in the enterprise networking space. Jeff enjoys learning everything there is to know about iOS development, and using that knowledge to make apps that feature a strong emphasis on design and user experience.

4 design patterns for a RESTless mobile integration »

close