全栈Swifter:用Perfect框架开发服务器端

文章目录
  1. 我写了啥
  2. 开始干货
    1. 开发环境
    2. 项目初始化
    3. 项目配置
    4. 运行服务器
    5. 路由
    6. MongoDB数据库
      1. 安装
      2. 配置
      3. 可视化工具
      4. 数据库连接
    7. 过滤器
  3. 何去何从

上个月有一件让Swifter兴奋的事情:苹果官方启动了Swift语言服务器端开发工作组。这意味着官方正式表态,Swift进军服务器端开发。

前几天我参加了iDev全平台苹果开发者大会,会上杨晖老师带领我们对Swift的服务器端开发前景做了分析,对Swift语言目前的几大服务器端开发框架进行了剖析,说实话收获并不多,因为我之前也在持续关注这一块的信息,但对我的启发是非常大的,这直接导致会后几天每天都心里痒,想写点什么。

于是我就写了点什么😂

我写了啥

我的毕业设计正好需要一个服务器端API,之前用Node.js写了一点,不过还没做完,这次正好趁着这个机会,用Swift来写一写,岂不是美哉美哉。

这次我用最新的Perfect 2.0.2框架,最新的Xcode 8.1(实测,8.0编译时有bug),进行开发。至于为什么在众多Swift服务器端框架中挑中了Perfect,无他,星多而已。

我不保证你在后续版本中,可以继续使用我介绍的API,而且官方虽然提供了中文文档,还有其他一些周边工具、中间件的详尽文档,但有相当一部分的内容不符合最新版本Perfect的API。所以踩坑还需后来人。扯的太多了,下面上干货。

开始干货

开发环境

首先你要有个macOS来进行开发,并保证安装了最新的Xcode 8.1。Ubuntu确实可以安装Swift,并对项目进行编译,但经过和一些开发者交流,目前主流开发方式还是用macOS开发,部署到Ubuntu服务器上。

项目初始化

我们可以通过SwiftPackageManager来初始化一个项目。

1
2
mkdir MySwiftServer
vi Package.swift

以上,新建一个MySwiftServer文件夹,用Vim新建一个Package.swift文件,这个文件你可以理解为CocoaPod中的Podfile

Package.swift中输入以下内容。

1
2
3
4
5
6
7
8
9
10
11
import PackageDescription
let package = Package(
name: "MySwiftServer",
dependencies: [
.Package(
url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git",
majorVersion: 2, minor: 0
)
]
)

语法是不是挺熟悉,这段Swift代码是作为项目的配置文件,表明了我们的项目名、所需依赖和依赖的版本。

保存好该文件,回到终端,执行swift build,第一次编译会从仓库clone所有的dependencies到本地,速度可能有点慢,好好等待就可以了。

当所有module编译完成后会提示我们warning: root package 'MySwiftServer' does not contain any sources,意思是我们还没有源代码。我们可以在项目目录下新建一个文件夹,名为Sources,用来保存源文件。在Sources目录中新建一个main.swift文件,作为程序入口,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import PerfectLib
import PerfectHTTP
import PerfectHTTPServer
let server = HTTPServer()
var routes = Routes()
routes.add(method: .get, uri: "/", handler: {
request, response in
response.setHeader(.contentType, value: "text/html")
response.appendBody(string: "<html><title>Hello</title><body>Hello World</body></html>")
response.completed()
}
)
server.addRoutes(routes)
server.serverPort = 8181
do {
try server.start()
} catch PerfectError.networkError(let err, let msg) {
print("Error Message: \(err) \(msg)")
}

这段代码首先创建了一个路由,是get方法,路径是根路径,并且返回了一段html代码,设置服务器端口为8181,然后是用一个do循环来驱动了服务器。

重新执行swift build,完成编译后,我们可以执行.build/debug/MySwiftServer来运行我们的程序,服务器会监听8181端口。打开浏览器,输入http://localhost:8181/,我们可以看到浏览器页面中显示Hello World。

项目配置

我们可以利用SPM来生成xcodeproj,执行swift package generate-xcodeproj,当提示generated: ./MySwiftServer.xcodeproj后,即可用Xcode打开项目目录下的MySwiftServer.xcodeproj文件。

在Xcode左侧navigator中,选择Project-MySwiftServer-Build Settings-Library Search Paths,添加"$(PROJECT_DIR)/**",注意要包含前后引号。

配置完成后,就可以用Xcode来写代码、Build、Run项目。

运行服务器

尝试⌘CMD+R,运行项目,console中会提示服务器已经在8181端口跑起来了。打开浏览器,输入地址http://localhost:8181/,马上可以看到页面上显示我们配置好的Hello World页面。

路由

PerfectHTTP中,有一个struct名为Routes,我们可以通过它来构建服务器的路由。

Sources目录中,创建一个名为routeHandlers.swift的文件,删除main.swift中的有关路由部分的代码,删除后的main.swift文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
import PerfectLib
import PerfectHTTP
import PerfectHTTPServer
let server = HTTPServer()
server.serverPort = 8181
do {
try server.start()
} catch PerfectError.networkError(let err, let msg) {
print("Error Message: \(err) \(msg)")
}

将刚刚我们删掉的那部分代码,粘贴到刚刚创建的routeHandlers.swift文件中。routeHandlers.swift文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import PerfectLib
import PerfectHTTP
import PerfectHTTPServer
public func signupRoutes() {
addURLRoutes()
}
func addURLRoutes() {
var routes = Routes()
routes.add(method: .get, uri: "/", handler: {
request, response in
response.setHeader(.contentType, value: "text/html")
response.appendBody(string: "<html><title>Hello</title><body>Hello World</body></html>")
response.completed()
}
}

这段代码,将刚刚添加的“Hello World”页路由放到了统一文件中进行管理,然后我们在main.swift中,记得调用signupRoutes方法。编译运行,一切正常。

上面代码中add方法最后一个参数handler是传入一个闭包,该闭包定义为public typealias RequestHandler = (HTTPRequest, HTTPResponse) -> (),所以我们可以将一个符合该类型的参数传入add方法中。修改routeHandlers.swift文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import PerfectLib
import PerfectHTTP
import PerfectHTTPServer
public func signupRoutes() {
addURLRoutes()
}
func addURLRoutes() {
var routes = Routes()
routes.add(method: .get, uri: "/", handler: helloHandler)
}
func helloHandler(request: HTTPRequest, _ response: HTTPResponse) {
response.setHeader(.contentType, value: "text/html")
response.appendBody(string: "<html><title>Hello</title><body>Hello World</body></html>")
response.completed()
}

重新运行编译,完全没问题。

MongoDB数据库

MongoDB是一种非关系型数据库,可以存储类JSON格式的BSON数据,所以深受广大开发者的喜爱,我们在此使用MongoDB举例。

对于已经使用过MongoDB的同学,可以不用看安装和配置部分。

安装

1
brew install mongodb

我们通过HomeBrew来为我们的Mac安装MongoDB,但是在El Capitain及之后版本,由于SIP的原因,可能会在安装的过程中出现权限问题,可以暂时关闭SIP功能,在安装完成后再开启,具体不表。如果遇到问题,可以谷歌一下“10.11 mac mongodb”。

配置

mkdir /data/db:创建目录/data/db,是MongoDB的默认存储目录。

chown id -u /data/db:赋权限。

mongod:开启服务

唰唰唰,一堆日志输出,这样子就可以了。

可视化工具

macOS平台上,有MongoHub可视化工具,不过在我使用过程中遇到了几次闪退,估计是很久没更新的原因吧,大家如果有比较好的工具可以告诉我哈。

数据库连接

Package.swift中,添加MongoDB依赖,如下:

1
2
3
4
5
6
7
8
9
10
import PackageDescription
let package = Package(
name: "PerfectTemplate",
targets: [],
dependencies: [
.Package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", majorVersion: 2, minor: 0),
.Package(url:"https://github.com/PerfectlySoft/Perfect-MongoDB.git", versions: Version(0,0,0)..<Version(10,0,0))
]
)

编译,在经过等待后,项目中已经有MongoDB这个module了。

routeHandlers.swift中,添加import MongoDB,并用一个字符串常量指定MongoDB服务器地址var mongoURL = "mongodb://localhost:27017"

添加一个新的路由,用来查找数据库数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func queryFullDBHandler(request: HTTPRequest, _ response: HTTPResponse) {
// 创建连接
let client = try! MongoClient(uri: mongoURL)
// 连接到具体的数据库,假设有个数据库名字叫 test
let db = client.getDatabase(name: "test")
// 定义集合
guard let collection = db.getCollection(name: "test") else {
return
}
// 在关闭连接时注意关闭顺序与启动顺序相反
defer {
collection.close()
db.close()
client.close()
}
// 执行查询
let fnd = collection.find(query: BSON())
// 初始化一个空数组用于存放结果记录集
var arr = [String]()
// "fnd" 游标是一个 MongoCursor 类型,用于遍历结果
for x in fnd! {
arr.append(x.asString)
}
// 返回一个格式化的 JSON 数组。
let returning = "{\"data\":[\(arr.joined(separator: ","))]}"
// 返回 JSON 字符串
response.appendBody(string: returning)
response.completed()
}

将该路由部署上去:

1
2
3
func addURLRoutes() {
routes.add(method: .get, uri: "/mongo", handler: queryFullDBHandler)
}

编译运行,我们在浏览器中打开http://localhost:8181/mongo发现返回一个JSON对象:{"data":[]}。接下来我们添加一个数据库写入接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func addHandler(request: HTTPRequest, _ response: HTTPResponse) {
// 创建连接
let client = try! MongoClient(uri: mongoURL)
// 连接到具体的数据库,假设有个数据库名字叫 test
let db = client.getDatabase(name: "test")
// 定义集合
guard let collection = db.getCollection(name: "test") else {
return
}
// 定义BSOM对象,从请求的body部分取JSON对象
let bson = try! BSON(json: request.postBodyString!)
// 在关闭连接时注意关闭顺序与启动顺序相反
defer {
bson.close()
collection.close()
db.close()
client.close()
}
let result = collection.save(document: bson)
response.setHeader(.contentType, value: "application/json
response.appendBody(string: request.postBodyString!)
response.completed()
})
server.addRoutes(routes)
}

现在我们借助接口调试工具(PAW/Postman等)来测试这个接口,我们编译运行服务器,在接口调试工具中,选择POST,地址http://localhost:8181/add,body部分给出一个JSON对象,比如{"text" : "test", "desc" : "description", "detail" : "detail" },然后打出请求,返回值如果是我们打出的JSON对象,说明请求正常返回了。接下来用刚才部署好的http://localhost:8181/mongo接口来验证一下我们是否真的插入了新的数据,返回结果默认是UTF8编码,如果有中文乱码的情况可以考虑下编码是否有问题。结果如下:

1
{"data":[{ "_id" : { "$oid" : "58203e113cba965b8d5616a2" }, "text" : "test", "desc" : "description", "detail" : "detail" }]}

我们成功了。

过滤器

我们在上网时如果访问到不存在的资源,会看到一个“404 NOT FOUND”页面,类似还有“403 FORBIDDEN”、“401 UNAUTHORIZED”等等,要对这些页面进行过滤,并在发生问题的时候做出一些操作,Perfect为我们提供了HTTPResponseFilterHTTPResponseFilter是一个协议,含有两个方法,本着Swift的“能用struct就不要用class”的思想,我们可以定义一个struct,遵循HTTPResponseFilter,作为我们的过滤器。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Filter404: HTTPResponseFilter {
func filterBody(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) {
callback(.continue)
}
func filterHeaders(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) {
if case .notFound = response.status {
response.bodyBytes.removeAll()
response.setBody(string: "\(response.request.path) is not found.")
response.setHeader(.contentLength, value: "\(response.bodyBytes.count)")
callback(.done)
} else {
callback(.continue)
}
}
}

大概意思就是拦截下response,如果状态值是notFound,我们就把response的body改为“Hehe …… path …… is not found.”。

然后我们在main.swift文件中,把之前写好的代码稍加改动:

1
2
3
4
5
6
7
do {
try server
.setResponseFilters([(Filter404(), .high)])
.start()
} catch PerfectError.networkError(let err, let msg) {
print("Network error thrown: \(err) \(msg)")
}

设置好我们的Filter404(),访问个不存在的资源试一试:http://localhost:8181/hehe,果然如愿以偿地显示了/hehe is not found.

类似的,我们还可以过滤其他http错误,具体可查阅HTTPResponse中的HTTPResponseStatus

何去何从

目前看来Perfect提供的API已经非常完善了,基本具备了一个Web服务器框架的常用特性。但是从目前的文档看来,Perfect的API变动还是比较频繁的,我们大可以抱着玩的态度来对待这个Swift in server。相信在Swift服务器开发官方工作组的推动下,我们不久就可以用这门年轻的语言来开发我们的服务器应用。

关于部署到服务器,我近期可以踩一踩这部分的坑,目前计划是用Ubuntu in Docker的方式来部署。有兴趣的话,可以持续关注我的博客。