在服务端写 Swift 是一种怎样的体验

发布于:2018-06-21 22:32,阅读数:3358,点赞数:7


> 本文为本人原创并首发于2017年3月发布于《iOS成长之路 2017春》中。
>
> 由于时间关系,部分内容在当前版本的工具着或已失效,本文仅供参考。

  不得不说,在生产中使用Swift开发服务端是一次大胆的尝试。是其发展速度和活跃程度给了我足够的信心,而其开发速度和运行效率又给了我足够的动力。在实际项目中以 MySQL 和 HDFS 作为支撑,应用了包含 Web 服务、JSON 接口服务、以及基于 Socket 的文件传输和视频直播流服务在内的业务。这些服务最终都运行在 Linux 服务器上,实践的结果让人满意。因此笔者非常期待 Swift 能在服务端市场中能大显身手。

## 溯源

### Swift在Linux上的表现

  北京时间 2015 年 6 月 9 日凌晨的 WWDC 大会上,发布 Swift 2.0 的同时宣布 Swift 即将**开源**,开源内容包括**编译器**和**标准库**,并支持 Linux。开源和跨平台给语言带来了更宽的发展通道。

  那么在 Linux 中 Swift 表现到底如何呢?

  其实早期的在 iOS 上使用 Swift 的开发者可能会发现,iOS 和macOS 中的 Swift 并没有脱离 Objective-C。很多 SDK 中的 Swift 类其实仅仅是 Objective-C 类的**封装**。例如在开发应用的时候不可避免的要使用`UIViewController`或者`NSViewController`这些标准框架类,这些类存在于 UIKit 等标准库中。虽然 App 主体使用了 Swift 开发,但这些标准库仍然是旧的 Objective-C 库,造成的结果就是这些 Swift 代码仍然跑在 Objective-C runtime 中。这常常让人觉得所谓的纯 Swift 项目一点都不纯,写 Swift 简直多此一举,还不如直接操作 Objective-C 呢。

  但是在 Linux 中,纯 Swift 可以真的是**纯 Swift**。Apple 这些年在软件产品上一直背负着沉重的历史负担,大量的库都使用 Objective-C 编写,运行在 Objective-C runtime 中。为了不使原本的功能失效,Apple 在把所有的库重新实现一遍之前是无法撤掉 runtime 的。而在 Linux 中,Apple 没有任何负担,可以直接使用新的库。包括 Foundation 在内的系统库已经被打造成为了C语言和 Swift 编写的库,它不再运行在 Objective-C runtime 上了。此时调用函数将在**编译时被链接**,代码的性能会得到提高,而原本的 runtime 的黑魔法也都将失效。

  经过笔者这段时间对 Linux 上的 Swift 的体验,它基本上可以满足日常项目的需要。虽然有些 API 还未被实现,但总能找到替代的办法。其运行也是稳定的,至少笔者手上的数据是零崩溃。目前语言还在快速发展中,问题会逐渐被修复,库函数也会逐渐被完善,因此笔者对 Linux 上的 Swift 还是充满信心的。

### Swift开发服务端的资质

  按照目前 Swift 的情况是具有在 Linux 上开发**完整**服务端软件能力的。

  Linux 中使用`Swift Package Manager(SPM)` 来构建项目,SPM 以文件夹**目录**为项目分支结构,以`Module`为单位构建项目。其中,SPM 支持 C/C++ Module ,同时也支持 stl 等标准 C++ 库,这为我们提供了无限的扩展可能。

  操作系统本身提供了大量C语言接口可供调用。Swift 可以原生访问系统的库,同样是以 Module 的形式。C语言的数据结构会被自动桥接为`struct`,直接使用。当然从语法上来讲,最佳的办法是自己再包装一层 Module,毕竟在 Swift 中操作的是`String`而不是`char *`,是`Data`而不是`unsigned char *`。

  只要是提供 C/C++ 接口的各类服务,都可以集成进 Swift 项目中。不论是 MySQL,还是 Hadoop,它们都提供了C语言的库,如果想要在 Swift 的服务端中调用,直接写一个 C Module 开放接口即可。由于 Swift 并非纯面向对象语言,因此在这里开放接口甚至都不需要用类来封装。具体的做法将在后面的内容中提到。

  One more thing,Swift 可以很简单地调用系统命令行。熟悉 Cocoa 的读者应该知道`NSTask`这个类可以调用系统命令,同样的我们也可以用这个在 Linux 中调用系统命令行。这个类在 Swift 中改成了`Process`,在 Linux 中这个类叫`Task`,使用方法相同。既然可以调用系统命令行,那么我们就可以直接执行 shell 脚本来更多的事了。考虑到 macOS 和 Linux 中使用不同的 API 所造成的麻烦,我们可以用**条件编译**来很方便地解决这个问题:

```c++
#if os(Linux)
//在Linux中编译的代码
#else
//在macOS中编译的代码
#end
```

### Swift开发服务端的优点

  既然 Swift 拥有了开发完整服务端软件的能力,那么就可以用它开发服务端了。可是现在 .Net、Java 等各大服务端框架已经非常成熟,如果用 Swift 开发,到底有哪些好处呢?

首先是代码层面:

- 编译后为原生代码,静态链接,运行效率高。
- Swift 在字符串、集合等数据的业务处理上有着较好的性能和极简的语法。
- 仍然可以使用如闭包、GCD 等开发者熟悉的东西,为开发带来极大便利。
- 内存管理仍然采用引用计数,及时回收,避免了例如 C++ 忘记手动回收造成泄露和 Java 延时回收难以 Debug 的弊端。
- Swift 目前已经开源,正处于快速迭代的成长期,社区非常活跃,具有潜力。

项目层面:

- 支持纯C/C++语言 Module,无限的接入扩展性。
- SPM 支持直接从 GitHub 同步代码,方便管理。
- Swift 开发的服务端和 Swift 开发的 iOS、macOS 客户端可以共用同一套模型源文件,非常方便。
- 目前已有相对成熟的框架以及各类工具库可供使用,正在快速成长。

  现在的 iOS 招聘信息里普遍都会考虑 Swift,有很多新项目已经采用 Swift 开发出来了。在客户端上,大家正在逐渐接受 Swift。那么在服务端,Swift 是否也可以占有一席之地呢?这个问题就留给时间回答吧。

## 实践

### Swift服务端框架

  目前 Swift 的后端框架主要有`Perfect`、`Vapor`、`Kitura`和`Zewo`等。前段时间掘金上有一篇文章在几个维度对比了几个框架的性能:[不服跑个分 - 顶级 Swift 服务端框架对决 Node.js](https://gold.xitu.io/entry/57e296af0bd1d000570ee3b4)。 文章中测试了以上四种框架以及`Node.js`在性能上的表现,结果汇总如下:

![server-side_1](..///cdn.yuusann.com/img/posts/18014_1.png)  在测试结果中,Perfect 以非常优秀的成绩胜出,因此笔者最终也选择了Perfect。[PerfectlySoft](https://github.com/PerfectlySoft) 公司一直保持着比较高产的状态,同时也是一家非常亲中国的公司。不仅有简体中文的文档,还有官方微信账号。也有不少同行们收到了公司发来的中文邮件。

  在实际的开发体验中,Perfect 的表现值得肯定。内存占用很低,官方提供的库也非常全面。从基本的 HTTP 服务器(`Perfect-HTTPServer`),多线程库(`Perfect-Threading`),日志库(`Perfect-Logger`),到各类数据库链接库(`Perfect-MySQL`, `Perfect-MongoDB`等),甚至到Hadoop(`WebHDFS`, `MapReduce`等功能)和邮件库(`Perfect-SMTP`)都提供了。就在写文的今天,官方在服务器助手中新增了在 Mac 上交叉编译,一键测试 Linux 版的功能。这些更新每周都会在官方微信中推送,而且是简体中文的文章,这对中国开发者来说是极为友好的。关于详细的开发体验将在下文提到。

  由于其他框架笔者接触的不多,因此就此带过。但也并不是说分数低其他框架就不好,例如 Vapor 的路由写法更简单一点,MVC 的结构更舒服一点,读者可以自由选择。另外对测试结果好奇的读者可以自行测试,测试代码都在原文链接之中。

### 基于Perfect的HTTP Server开发体验

  如何使用 Perfect 开发服务端呢?这里笔者将分为**开发过程**和**部署过程**进行介绍。

#### 开发过程

  程序最终是跑在 Linux 机器里的,但依然可以继续在 Mac 中开发和调试,最终再移植到 Linux 中进行编译运行。PerfectlySoft 提供了例子 [PerfectTemplate](https://github.com/PerfectlySoft/PerfectTemplate) ,包含了简单的 HTTP 服务器创建的过程。README 中的过程是基于 SPM 编译管理的,但在 Mac 下,我们有更好的选择 —— Xcode。

  克隆官方的例子后使用`build`命令自动下载依赖库:

```bash
$ swift build
```

  之后这个命令生成 Xcode 工程:

```bash
$ swift package generate-xcodeproj
```

  接着我们就可以像开发 macOS 应用那样开发服务端了。官方在文档中提醒我们不要直接编辑这个 xcodeproj 文件。因为有时我们需要修改 Package.swift 来下载更多依赖库,这时候这个 xcodeproj 会被重新生成,之前所做的所有内容都会被覆盖。本书的 Sample 只做演示用,所以并没有这样做。读者在自己正式的项目中要注意这一点,避免以后发生麻烦。

  在现在版本的 README 中,提供了一种非常炫酷的创建服务的方式,直接构造一个多维数组,填写相关信息,之后就可以直接跑起一组服务:

```swift
import PerfectLib
import PerfectHTTP
import PerfectHTTPServer

let port1 = 8080, port2 = 8181

let confData = [
"servers": [
[
"name":"localhost",
"port":port1,
"routes":[
//...
],
"filters":[
//...
]
]
]
]

do {
// Launch the servers based on the configuration data.
try HTTPServer.launch(configurationData: confData)
} catch {
fatalError("\(error)") // fatal error launching one of the servers
}
```

  由于这种方式十分简洁干练但不利于读者理解内部实现的结构,笔者会使用原来的接口创建方法来讲。

  在 Perfect 中创建 HTTP 服务极为简单,在路由的创建过程中,开发者只需要提供了一个处理该路由的回调函数,其余大量的工作 Perfect 已经帮我们完成了。当 Perfect 服务器接收到一个HTTP请求后,服务器首先会将请求交给当前已经注册了的过滤器。这些过滤器可能会修改或者重定向请求,转发给别的路由。当过滤器处理完成后服务端会寻找有没有相应的回调,如果有则会交给回调函数进行处理,如果没有则会返回 404 。来直接看下面的例子,完成一个最简单的 HTTP Server 程序:

```swift
import Foundation
import PerfectLib
import PerfectHTTP
import PerfectHTTPServer

open class MyServer {

fileprivate var server: HTTPServer

internal init(root: String, port: UInt16) {
//构造 HTTPServer 对象
server = HTTPServer.init()
//构造路由对象,这只是个容器,现在这里面并没有内容
var routes = Routes.init()
//配置路由,添加URL以及回调函数
configure(routes: &routes)
//将路由添加进服务
server.addRoutes(routes)
//设置端口和根目录
server.serverPort = port
server.documentRoot = root
}

//配置路由函数
fileprivate func configure(routes: inout Routes) {
//添加接口,路径为/,方法为GET,回调函数为闭包
routes.add(method: .get, uri: "/", handler: { request, response in
//取得url中的参数,类型是`[(String, String)]`,遍历即可
let param = request.params()
//返回数据头
response.setHeader(.contentType, value: "text/html")
//返回数据体
response.appendBody(string: "Hello World")
//返回
response.completed()
})
}

//开始服务
open func start() {
do {
try self.server.start()
} catch PerfectError.networkError(let err, let msg) {
print("Network error thrown: \(err) \(msg)")
} catch {
print("Network unknow error")
}
}
}
```

  包含一个 URL 接口的 HTTP Server 就写完了,最终在`main.swift`中调用:

```swift
import Foundation

let myServer = MyServer.init(root: "Your Path To Root", port: 8080)

myServer.start()
```

  此时可以看到控制台输出:

```
[INFO] Starting HTTP server on 0.0.0.0:8080 with document root "Your Path To Root"
```

  现在我们就可以在浏览器看到结果了:

![server-side_2](..///cdn.yuusann.com/img/posts/18014_2.png)

  如果在 Response 中返回页面 HTML 代码,那么这就是个 Web 服务器。如果在 Response 中返回 JSON/XML 字符串,那么这就是个业务接口,可以给手机 App 使用。

  HelloWorld 固然简单,实际情况往往没那么简单。倘若要建个站点,必然包含了大量`css`、`JavaScript`以及图片等静态资源,这些静态资源如果需要开发者手动加入路由显然不现实。所以在`PerfectHTTP`中提供了`StaticFileHandlerd`模块帮我们实现了静态资源的处理。

  若不修改任何配置,则在最初设置的 root 目录下所有的文件都是可以被直接下载的。描述文件 URL 的这些请求会被方法`handleRequest`处理,在路由设置不存在的情况下会访问本地文件,若文件存在则会直接发送文件。如果读者需要建立一个静态 Web 站点,甚至可以一个路由都不配置,直接运行一个空的 HTTPServer,这些静态资源会被直接开放在网络中,访问者可以直接根据 URL 访问不同的页面。当然,也可以手动映射静态资源,利用通配符将一个本地目录映射到一个 URL 上,具体实现过程可以参考官方文档,在此不多介绍。

  另外官方提供的`File`模块也能以**数据流**的形式很便利地操作文件。在页面方面,Perfect 同样支持`Mustache`模板功能。官方提供的`Perfect-Mustache`库提供了相关的功能支撑。这是一个非常好用的页面模板引擎,一个很好的页面中动态元素替换的解决方案。在页面 HTML 代码中嵌入例如`{{var}}`的占位字段,在 Swift 代码中提供一个字典,将页面中的占位符替换成字典中的值。详细的使用过程可以参阅官方文档,这个库可以独立于服务器使用。

  总之,官方提供了大量的工具库且正在以非常高的活跃度继续开发,大大便利开发者们。

#### 部署过程

  Linux 上的 Swift 环境配置在此不做介绍,具体配置过程可使用 PerfectlySoft 提供的安装脚本:[Perfect-Ubuntu](http://www.enumsblog.com/post?pid=16009),或自行搜索配置。

  现在我们的服务已经在 Mac 上跑起来了,那么如何部署到 Linux 服务器中呢?例子中的 HelloWorld 直接拷贝到 Linux 服务器中 build 是直接可以通过的,这个例子太过初级,这里讲个稍微复杂一点的帮助读者理解。

  在 Linux 中我们使用 SPM 管理项目。例如我们的项目包含了 Swift 主函数代码和一个纯C语言的 Framework。在 Xcode 中开发时,可以按照自己的习惯建立两个工程,主工程链接库工程生成的库,这个过程相信大家都很熟悉了。但为了 Linux 工程的管理,源文件需要放在两个独立的文件夹中。具体目录结构如下图:

![server-side_3](..///cdn.yuusann.com/img/posts/18014_3.png)

  SPM 会按照`Package.swift`文件来为我们生成`Target`,处理依赖关系。SwiftCode 模块依赖了 CCode,因此此时要将该文件改成这样:

```swift
import PackageDescription

let package = Package(
name: "PerfectTemplate",
targets: [Target(name: "SwiftCode", dependencies:["CCode"])],
dependencies: [
.Package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", majorVersion: 2, minor: 0),
]
)
```

  这时候在 Linux 中 build 该项目时,CCode Module 会被单独编译成`CCode.so`供主模块调用。无论是混编还是纯 Swift 项目也可以这样处理,当然,别想着混编 Objective-C 了,这儿已经没有 Objective-C runtime了。按照这样的目录结构在 Linux 中 build,将会生成隐藏的`.build`目录,执行代码运行:

````bash
$ ./.build/debug/SwiftCode
````

​  这样我们的服务就已经在 Linux 上跑起来了。嗯,如果代码确认没问题,需要发行 Release 版本呢?

```bash
$ swift build -c release
```

​  之后会生成`./.build/release`文件夹,其中包含了 Release 版的库和可执行文件。

​  清理工程可以使用下面这个命令:

```bash
swift build --clean
//当然,也可以粗暴一点,直接删除.build目录,效果是一样的
rm -rf .build
```

  如果需要连带依赖库一起清理,使用下面这个命令:

```bash
swift build --clean=dist
//同样的,可以粗暴地直接手动删除目录
rm -rf .build
rm -rf Packages
```

​  链接其他第三方库如 libhdfs.so 并编译 release 版本:

```bash
$ swift build -Xlinker -lhdfs -c release
```

  静态编译:

```bash
$ swift build -static-stdlib
```

  其他编译器功能请参阅:

```bash
$ swift --help
```

#### 小结

​  通过以上这个例子,我们创建了一个简单的 HTTP 服务器,并且部署到了 Linux 服务器上。由于框架是开源的,甚至可以直接修改源代码来完成例如埋点等功能。在接下来的部分,笔者将会添加一些常规的服务来进一步让大家体会 Swift 在服务端的开发体验。

### 基于Perfect的常规服务接入体验

  一个完整的服务器只包含 HTTP 服务和文件系统功能肯定是不够的,必须有一种方法让开发者接入其他第三方服务。第三方服务的 SDK 有 Swift 版的吗?嗯,很少很少。但多数厂商都会提供C语言接口,这就可以作为开发者们的突破口,利用C语言接口接入服务。

  笔者会举两个简单的例子来介绍第三方服务接入的过程:**MySQL**和**HDFS**。

#### MySQL接入

​  对于厂商没有提供接口的服务,Perfect 官方“造了很多轮子”来帮助开发者接入。MySQL 的接入可以直接选用官方提供的`Perfect-MySQL`。

​  编辑 Package.swift ,在依赖库中加入对`Perfect-MySQL`的依赖:

```swift
.Package(url:"https://github.com/PerfectlySoft/Perfect-MySQL.git", majorVersion: 2, minor: 0)
```

​  下载依赖后就可以使用了,同样来看一个简单的例子:

```swift
import Foundation
import PerfectLib
import PerfectHTTP
import MySQL
//定义在Linux中的数据库参数
#if os(Linux)
let testHost = "数据库IP"
let testUser = "数据库登录名"
let testPassword = "数据库密码"
let testSchema = "Schema名"
#else
//定义在其他平台的数据库参数参数
#endif

internal class MyDB {
//构造一个库中的MySQL对象
fileprivate let mysql = MySQL.init()

internal init?() {
//设置客户端字符集,这是非常必要的操作,否则所有中文可能都会变成问号
guard mysql.setOption(.MYSQL_SET_CHARSET_NAME, "utf8mb4") else {
return nil
}
//连接数据库
guard mysql.connect(host: testHost, user: testUser, password: testPassword) else {
return nil
}
}
//执行SQL语句并返回结果
@discardableResult
internal func query(_ s: String) -> [[String?]]? {
guard mysql.selectDatabase(named: testSchema), mysql.query(statement: s) else {
return nil
}
let results = mysql.storeResults()
var resultArray = [[String?]]()
while let row = results?.next() {
resultArray.append(row)
}
return resultArray
}
}
```

  一个简单的类就可以帮助开发者操作数据库了,使用时直接调用`query`方法即可。

  `Perfect-MySQL`基于C接口实现,其默认链接的库为`libmysqlclient.so`。官方为开发者做了一次中间封装,处理了大量指针操作,让语法更符合 Swift 标准。库中查询语句的结果会返回一个`MySQL.Results`对象,其包含了查询结果的记录数、字段数等方法可供调用。可能是为了保证性能,官方并未把查询结果全部取出来做 Swift 数据结构封装。数据仍然存在于 C++ 层,以指针`UnsafeMutablePointer<MYSQL_RES>`进行操作,当`MySQL.Results`对象被`ARC`回收时释放 C++ 资源。这种方法相当于维护了一个服务端,在客户端操作类似于操作一个状态机,造成在处理查询结果上语法还是充满了 C Style ,例如需要取第N条数据时必须先调用以下方法将指针移动到目标位置然后读取,读取时也只能用类似生成器的方法读:

```swift
//移动到第N条记录
public func dataSeek(_ offset: UInt)
//读取这条内容,指针自动后移到下一条记录处
public func next() -> Element?
```

  需要注意的是,这个库默认链接的是`libmysqlclient.so`,在库内部调用`mysql_real_connect`可能会出现线程安全问题。因此多线程调用`connect`方法时需要处理好线程问题。

#### HDFS接入

​  这是一个典型的调用C接口接入的服务。

​  HDFS 是一个运行在 Java 上的分布式文件系统,它提供了C接口可供开发者调用。在使用之前需要开发者配置相关环境以及在本机编译 Hadoop 的 Native 库,这个过程不在讨论范围之内。编译完成后会得到一系列 Hadoop 的库,在本文中只选用 HDFS 库,即 libhdfs.so。

  在上文中提到了如何利用 SPM 建立 C Module,而C Module可以被自动桥接到 Swift。但不幸的是用于描述文件信息的结构体`hdfsFileInfo`中的变量在 Swift 中是无法被访问到的,因此在数据结构上需要在 C++ 层重新做一下封装。做完封装后将接口封装为纯C语言接口即可供 Swift 端调用。

​  这里需要一点C语言基础,由于接口数量众多,本文中仅举例连接 HDFS 的函数封装过程。根据 SPM 管理 Module 的目录结构,假设结构如下图所示:

![server-side_4](..///cdn.yuusann.com/img/posts/18014_4.png)

- `CCode.h`:Module 暴露的接口。

```c++
#include "../CWrap.h"
```

- `HDFSType.h`:针对库中的数据结构的另一层封装,在原有数据结构前加入`cw_`前缀定义新结构。
- `CWrap.h`:HDFS 接口头文件。

```c++
#include "HDFSType.h"
#ifdef __cplusplus
extern "C" {
cw_hdfsFS cw_func_connect(const char* nn, cw_tPort port);
#endif
#ifdef __cplusplus
}
#endif
```

`CWrap.cpp`:HDFS 接口封装的具体实现过程。

```c++
#include "CWrap.h"
#include "HDFS头文件路径/hdfs.h"

cw_hdfsFS cw_func_connect(const char* nn, cw_tPort port) {
hdfsBuilder * builder = hdfsNewBuilder();
hdfsBuilderSetNameNode(builder, nn);
hdfsBuilderSetNameNodePort(builder, port);
hdfsBuilderConfSetStr(builder, "dfs.support.append", "true");
hdfsBuilderConfSetStr(builder, "dfs.replication", "1");
return (cw_hdfsFS)hdfsBuilderConnect(builder);
}
```

​ 最终,方法`cw_func_connect`能在 Swift 端被调用:

```swift
let fs = cw_func_connect("目标NameNode", cw_tPort(目标端口))
```

​  事实上笔者在 C++ 层不仅是简单封装了接口,更是封装了功能。在平时的开发过程中经常也会在厂商 SDK 和自己的业务代码中间做封装,笔者将其移到了 C++ 中实现,同时也可以暴露更少的数据结构和细节。当然这一层也可以放到 Swift 中去完成,但封装数据结构的工作量会更大。

​ 最后使用如下命令链接 HDFS 库并编译:

```bash
$ swift build -Xlinker -lhdfs
```

​  实际应用中,笔者的项目是接入了 HDFS 的。在 Swift 端调用时,因为是原生调用,因此性能和稳定性都不错。但关于 HDFS 的开发,其实还存在很多坑。在 Mac 上开发时会遇到例如`CLASSPATH`环境变量未定义的问题,无法`Debug执行`的问题等等。读者若有兴趣可以自行研究。

#### 小结

  其实 Swift 端在调用原生服务时并没有给开发者带来太多的额外工作量,其麻烦的主要来源也在各语言桥接上。目前桥接C语言还是比较方便的,其他语言的接入笔者并没有深入研究。但随着 Swift 的发展,如果将来可以普及,那么会有更多厂商推出 Swift SDK,这些问题也就可以解决了。

### 基于BlueSocket的Socket服务端开发体验

  很多服务是基于 Socket 的,那么 Swift 的 Socket 服务端开发体验又如何呢?这里笔者推荐使用 [BlueSocket](https://github.com/IBM-Swift/BlueSocket)。这是IBM推出的一款在 iOS、macOS 和 Linux 上通用的 Socket 库,在三个平台上通用,能在调试上带来很大的便利。

  做 Socket 的应用层开发开发,开发者只要操作收、发缓冲区,即可实现数据的接收和发送。在 macOS 和 iOS 上,Socket 相关 API 存在于`Darwin`库中,而 Linux 上则是`Glibc`库中,是一组C语言接口,开发者也可以自己向内核申请 Socket 去完成业务逻辑。在这里BlueSocket 已经帮助开发者实现了 Socket 的内核调用了。

  既然是调用系统库的C语言接口,那么调用逻辑肯定是跟其他语言是一样的,这一点可以让熟悉 Socket 开发的开发者直接上手。现在来看一个简单的 TCP Socket 服务端的创建过程:

```swift
//声明服务器类
internal class SocketService {
//停止标记
fileprivate var stopFlag = false
//监听Socket
fileprivate var listenSocket: Socket
//记录客户端列表,以便退出时关闭连接
fileprivate var clients = Dictionary<Int32, Socket>()

//开始运行服务端
internal func start(port: Int) throws {
//创建一个Socket
self.listenSocket = try Socket.create()
//开始监听
try listenSocket.listen(on: port)
//死循环监听客户端的连接
while !stopFlag {
//当有客户端连接时添加到客户端列表
let client = try listenSocket.acceptClientConnection()
self.add(client: client)
}
}

//停止时关闭客户端所有连接
internal func stop() {
stopFlag = true
for socket in clients.values {
socket.close()
}
listenSocket.close()
}

//添加客户端方法
fileprivate func add(client: Socket) {
DispatchQueue.global().async {
do {
//登记客户端
self.clients[client.socketfd] = client
//死循环读取数据
while !self.stopFlag {
var tmpBuffer = Data.init()
//读取接收缓冲区数据,如果没有数据,线程会等在这里
let readSize = try client.read(into: &tmpBuffer)
//如果长度为0,表示连接断开
if readSize == 0 {
break
} else {
//处理缓冲区数据
}
}
//注销客户端的登记
self.clients[client.socketfd] = nil
} catch {
//处理异常
}
}
}
}
```
  从代码中可以看出,每当一个客户机申请连接时,GCD 都会获取一条线程来跑。理论上一个客户端独占一条线程,这和其他语言的开发是一样的,但不一样的是,Swift 使用的 GCD 和闭包在这里能极大提高代码可读性。`read`方法会将缓冲区数据写出来交给开发者处理。TCP 的数据是基于流的,包与包之间没有固定的分隔,所有包粘在一起。这里如果需要拆固定长度的包,Swift 有一种很好的方式。

  代码中定期调用`read`方法,它会将缓冲区里**所有**的内容都写出来。这个内容的长度是不确定的,取决于发送方和网络状况,以及这段代码中处理数据部分的耗时。在实际开发中开发者会定义自己的数据包,可能是有意义的间隔符,也有可能是固定长度。笔者在这里举一个固定长度包的例子,利用 Swift 的数组操作方法来拆包:

```swift
//定义数据包长度
let packetSize = 4096
//声明存放数据的缓冲区,如需限制大小,可自己修改代码
var buffer = Array<UInt8>()
while !self.stopFlag {
//临时缓冲区
var tmpBuffer = Data.init()
let readSize = try client.read(into: &tmpBuffer)
if readSize == 0 {
break
} else {
//将临时缓冲区的数据追加到上面定义的缓冲区里
buffer += tmpBuffer.bytes
//当数据足够长时
while buffer.count >= packetSize {
//获取包长度的数据
let packetBytes = Array(buffer.dropLast(buffer.count - self.packetSize))
//删去取出的部分
buffer.removeFirst(packetSize)
//处理数据
}
}
}
```

  以上代码能实现一个缓冲区队列来按顺序取所需要的数据包,代码简单且有效。但需要注意的是这里的`dropLast`和`removeFirst`都是时间复杂度`O(n)`的方法。但作为 Socket 缓冲区,这个数组并不会很大,因此这里对性能的影响很小。但如果服务端对性能极为敏感且缓冲区有可能会很大的时候,这里最好采用常规**翻转栈**的方法设计队列,这里不再涉及。

  在设计模式上例如处理数据部分,可以使用`Delegate`来将数据包回调给委托方实现,这样一个通用的定长数据包的 SocketServer 工具类就这样简单地实现了。关于客户端的实现,同样极为简单,开发者可以参考上面给出的 BlueSocket 的链接,查看官方的 README 文档以及历程,笔者在这里不再贴出代码了。
  
#### 小结
  使用 BlueSocket 能让开发者在很短的时间内创建自己的 Socket 服务器。无论是做即时通信,文件传输还是直播,Swift 都能轻松胜任。正如本文开头提到的,Swift 开发服务端时,GCD 等工具和极简的语法能给开发者带来极大便利。同时也是个正在快速发展的语言,将来会有更多这样优秀的工具库的诞生,让开发过程更加高效。
  
#### Swift服务端和iOS客户端同时进行的开发体验

  使用 Swift 做服务端一个很大的优势就在于,可以和客户端共用一套模型源文件。在服务端传送一个对象到客户端是业务开发中经常遇到的需求,数据发送时开发者有很多方法,转成数据二进制流、XML字符串、JSON字符串、ProtoBuf等发送,当客户端收到时再转模型。如果开发者使用 Swift 开发,那么这套代码只需要写一次,且以后再修改需求加字段,也不会出现服务端改了客户端忘改了的现象了。

  笔者在这里举一个简单的使用 JSON 字符串进行传输的模型,解析库使用的是 [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)。

```swift
//声明一个协议,可以被JSON序列化以及反序列化
public protocol JSONable {
init?(json: JSON)
func toJSON() -> JSON
}
```

```swift
//声明对象,遵守JSON序列化协议
open class MyObject: JSONable {

open var mName: String!

public required init?(json: JSON) {
guard let mName = json["mName"].string else {
return nil
}
self.mName = mName
}

open func toJSON() -> JSON {
var dict = Dictionary<String, Any>()
dict["mName"] = mName
return JSON.init(dict)
}
}

```
  建完模型以后,这个源文件在两端都是可以用的,服务端和客户端同时引用这个源文件。当需求发生变更,需要加入字段时,服务端开发者直接修改这个源文件,由于客户端引用的也是同一个文件,因此客户端那边也被修改了。如果服务端需要在对象里加入一个验证算法,例如根据当前时间计算出一个 Key 来验证,那么直接在这个源文件里写就可以了,客户端开发者根本不需要关心这个算法是什么,叫什么,放在哪,因为服务端的哥们已经全部完成了。当对象被序列化调用`toJSON`方法时,这个方法已经被服务端开发者重写了,因此新的参数已经被加入序列了。因此这些与客户端业务毫无关系的代码,甚至可以做到不需要客户端开发者的参与就可以完成。

  另外关于跨平台的问题,与 Cocoa 无关的 Swift 代码,理论上是可以在三个平台中通用的。笔者在前段时间完成了包括文件流和内存流的操作库,这些代码在一行未改的情况下在三个平台都可以直接使用,因此极大方便了“现造的轮子”在项目中的快速推进。

  综上,在和 iOS 和 macOS 客户端协作开发的情况下,Swift 有着天生的优势,为开发提供便利。

## 尾声
### 问题和不足
  在 macOS 上正常工作的代码在 Linux 上并不总能正常工作。笔者在开发过程中也遇到过许多问题。主要的问题归纳起来可以总结为以下几点:

- **未实现的 API**:一些较为常用的 API 未被实现,典型的有`String`的`init(contentsOf url: URL)`,`FileManager`的`default`属性等等。不过这个问题随着时间的推进,最终都将被解决。
- **API 执行结果不一致**:体现为 macOS 上工作良好的代码在 Linux 上罢工。例如从`Data`构造`String`时若遇到`\0`字符,在 macOS 上会忽略这个字符,而在 Linux 上字符串会直接截断。这些问题可能由于底层实现不同所导致的。
- **API Bug**:表现为 macOS 上工作良好的代码在 Linux 出现非代码逻辑错误的问题。例如同时在 N 条使用`DispatchQueue.global()`获得的线程里使用`Data`的`init(contentsOf url: URL)`时直接崩溃报错`falat error`的问题。

  大多数情况下发生以上情况都能找到替代的 API 来实现。例如上面提到的`Data`的`init(contentsOf url: URL)`时报错的问题可以使用`URLSession`来解决,`DateFormatter`时区错乱的问题可以使用`TimeZone`的`init(secondsFromGMT: Int)`来手动设置时差秒数。但这些都是临时的解决方案。笔者认为只有当原生的 API 足够可靠时,才会吸引更多的开发者加入阵营。

### 总结和体会
  本文主要讲了如何使用 Swift 语言开发简单的 HTTP 和 Socket 服务器,以及如何连接 MySQL 数据库和 HDFS,并最终部署到 Linux 服务器上。所用到的框架为`Perfect`和`BlueSocket`,以及和`iOS`客户端协作开发时的体验。

  使用 Swift 开发服务端,开发时的体验是极好的。干净的文件结构,没有各项杂乱的环境配置,因此工程也极少出问题。在 Xcode 中开发时由于也是笔者比较熟悉的环境,可以使用 GCD 等熟悉的工具,因此开发过程是非常舒服的。最终部署时也非常简单,运行情况也比较好。Swift 诞生时间虽然不长,但是网络上资料已经比较多了,不再是以前的满眼 HelloWorld 了,Perfect 官方更是提供了简体中文的文档,因此现在想要深入学习 Swift 的话时机是非常合适的。对于目前存在的问题,相信随着时间的推移,会逐步得到解决。

  Swift 虽好,但完善之路任重而道远。

## 相关链接

- [PerfectlySoft 官方网站](http://perfect.org)
- [PerfectlySoft GitHub](https://github.com/PerfectlySoft)
- [Perfect 简体中文官方文档](http://perfect.org/docs/index_zh_CN.html)
- [IBM BlueSocket GitHub](https://github.com/IBM-Swift/BlueSocket)
- [笔者博客中的 Perfect TAG](http://enumsblog.com/list?tag=Perfect)



评论:2条


1楼:2019-01-17 14:19:12

苏-ios:

围观大神。。


2楼:2019-05-18 15:32:58

糖炒栗子:

围观大神


返回列表

返回归档

返回主页