上个月有一件让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的同学,可以不用看安装和配置部分。
安装
我们通过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)
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 ]()
for x in fnd! {
arr.append(x.asString)
}
let returning = "{\"data\":[\(arr.joined(separator: " ,"))]}"
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为我们提供了HTTPResponseFilter
。HTTPResponseFilter
是一个协议,含有两个方法,本着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的方式来部署。有兴趣的话,可以持续关注我的博客。