因为一直是在做移动端的开发,在工作中总会遇到比较坑的后端。
之前就遇到过一个让人抓狂的后端开发人员,本来只需一句代码解析的response,在对接他的API时发现需要上百行代码去解析,那时我心里就埋下了走向后端的种子,我要写出最美的API。
自从Swift开源之后一直想尝试用Swift写服务端,在官方Swift3.0发布和经过许多优秀团队对Swift的贡献,Swift逐渐稳定。在今天成熟的条件下,我要做的就是用由加拿大团队开发的Perfect服务器框架,写一个简单的API。
开发环境
本demo环境:
- macOS Sierra 10.12.5
- Xcode 8.3.2
- paw 3.1
项目初始化
我们可以通过SwiftPackageManager来初始化一个项目。打开终端:
HO-2:~ HO$ mkdir SwiftServerDemo
HO-2:~ HO$ cd SwiftServerDemo
HO-2:SwiftServerDemo HO$ vi Package.swift
以上,新建一个SwiftServerDemo文件夹,用Vim新建一个Package.swift文件,这个文件你可以理解为CocoaPod中的Podfile。
在Package.swift中输入以下内容。
import PackageDescription
let package = Package(
name: "SwiftServerDemo",
dependencies: [
.Package(
url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git",
majorVersion: 2, minor: 0
)
]
)
语法是不是挺熟悉,这段Swift代码是作为项目的配置文件,表明了我们的项目名、所需依赖和依赖的版本。
保存好该文件,回到终端,执行swift build。
HO-2:SwiftServerDemo HO$ swift build
第一次编译会从仓库clone所有的dependencies到本地,速度可能有点慢,好好等待就可以了,最终终端如下:
2017-06-28 10:41:53.408 xcodebuild[1991:95616] [MT] DVTPlugInManager: Required plug-in compatibility UUID DFFB3951-EB0A-4C09-9DAC-5F2D28CC839C for KSImageNamed.ideplugin (com.ksuther.KSImageNamed) not present
2017-06-28 10:41:53.517 xcodebuild[1991:95616] [MT] PluginLoading: Required plug-in compatibility UUID DFFB3951-EB0A-4C09-9DAC-5F2D28CC839C for plug-in at path '~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/VVDocumenter-Xcode.xcplugin' not present in DVTPlugInCompatibilityUUIDs
2017-06-28 10:41:53.517 xcodebuild[1991:95616] [MT] PluginLoading: Required plug-in compatibility UUID DFFB3951-EB0A-4C09-9DAC-5F2D28CC839C for plug-in at path '~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/KSImageNamed.xcplugin' not present in DVTPlugInCompatibilityUUIDs
2017-06-28 10:41:53.518 xcodebuild[1991:95616] [MT] PluginLoading: Required plug-in compatibility UUID DFFB3951-EB0A-4C09-9DAC-5F2D28CC839C for plug-in at path '~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/ActivatePowerMode.xcplugin' not present in DVTPlugInCompatibilityUUIDs
Fetching https://github.com/PerfectlySoft/Perfect-HTTPServer.git
Fetching https://github.com/PerfectlySoft/Perfect-HTTP.git
Fetching https://github.com/PerfectlySoft/PerfectLib.git
Fetching https://github.com/PerfectlySoft/Perfect-Net.git
Fetching https://github.com/PerfectlySoft/Perfect-COpenSSL.git
Fetching https://github.com/PerfectlySoft/Perfect-Thread.git
Cloning https://github.com/PerfectlySoft/Perfect-HTTP.git
Resolving https://github.com/PerfectlySoft/Perfect-HTTP.git at 2.0.7
Cloning https://github.com/PerfectlySoft/Perfect-COpenSSL.git
Resolving https://github.com/PerfectlySoft/Perfect-COpenSSL.git at 2.0.4
Cloning https://github.com/PerfectlySoft/Perfect-HTTPServer.git
Resolving https://github.com/PerfectlySoft/Perfect-HTTPServer.git at 2.0.9
Cloning https://github.com/PerfectlySoft/Perfect-Net.git
Resolving https://github.com/PerfectlySoft/Perfect-Net.git at 2.0.5
Cloning https://github.com/PerfectlySoft/Perfect-Thread.git
Resolving https://github.com/PerfectlySoft/Perfect-Thread.git at 2.0.12
Cloning https://github.com/PerfectlySoft/PerfectLib.git
Resolving https://github.com/PerfectlySoft/PerfectLib.git at 2.0.10
warning: module 'SwiftServerDemo' does not contain any sources.
warning: module 'SwiftServerDemo' does not contain any sources.
当所有module编译完成后会提示我们warning: module 'SwiftServerDemo' does not contain any sources.,意思是我们还没有源代码。
我们可以在项目目录下新建一个文件夹,名为Sources,用来保存源文件。
HO-2:SwiftServerDemo HO$ mkdir Sources
在Sources目录中新建一个main.swift文件,作为程序入口,代码如下:
HO-2:SwiftServerDemo HO$ cd Sources
HO-2:Sources HO$ vi main.swift
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 Swift</body></html>")
response.completed()
}
)
server.addRoutes(routes)
server.serverPort = 8080
do {
try server.start()
} catch PerfectError.networkError(let err, let msg) {
print("Error Message: \(err) \(msg)")
}
这段代码首先创建了一个路由,是get方法,路径是根路径,并且返回了一段html代码,设置服务器端口为8080,然后是用一个do循环来驱动了服务器。
重新执行swift build
HO-2:Sources HO$ swift build
巴拉巴拉一堆日志输出....像下面这样子就可以了
上面还有很多很多...
efault value#>
Compile Swift Module 'SwiftServerDemo' (1 sources)
Linking /Users/HO/SwiftServerDemo/.build/debug/SwiftServerDemo
HO-2:Sources HO$
完成编译后,我们可以执行.build/debug/SwiftServerDemo来运行我们的程序,服务器会监听8080端口。
HO-2:Sources HO$ cd
HO-2:~ HO$ cd SwiftServerDemo
HO-2:SwiftServerDemo HO$ .build/debug/SwiftServerDemo
[INFO] Starting HTTP server on 0.0.0.0:8080
打开浏览器,输入http://localhost:8080/
我们可以看到浏览器页面中显示:Hello Swift
项目配置
我们可以利用SPM来生成xcodeproj,执行swift package generate-xcodeproj,当提示generated: ./SwiftServerDemo.xcodeproj后,即可用Xcode打开项目目录下的SwiftServerDemo.xcodeproj文件。
HO-2:~ HO$ cd SwiftServerDemo
HO-2:SwiftServerDemo HO$ swift package generate-xcodeproj
generated: ./SwiftServerDemo.xcodeproj
在Xcode左侧navigator中,选择Project-MySwiftServer-Build Settings-Library Search Paths,添加"$(PROJECT_DIR)/**",注意要包含前后引号。
配置完成后,就可以用Xcode来写代码、Build、Run项目。
运行服务器
尝试⌘CMD+R,运行项目,console中会提示服务器已经在8080端口跑起来了。打开浏览器,输入地址http://localhost:8080/ ,马上可以看到页面上显示我们配置好的Hello Swift页面。
分离路由
在PerfectHTTP中,有一个struct名为Routes,我们可以通过它来构建服务器的路由。
在Sources目录中,创建一个名为routeHandlers.swift的文件
HO-2:~ HO$ cd /Users/HO/SwiftServerDemo/Sources
HO-2:Sources HO$ vi routeHandlers..swift
routeHandlers.swift 内容如下:
import PerfectLib
import PerfectHTTP
import PerfectHTTPServer
回到终端build一下:
HO-2:Sources HO$ swift build
日志如下表示成功:
Compile Swift Module 'SwiftServerDemo' (2 sources)
Linking /Users/HO/SwiftServerDemo/.build/debug/SwiftServerDemo
用SPM来生成新的xcodeproj,执行swift package generate-xcodeproj,当提示generated: ./SwiftServerDemo.xcodeproj后即可用Xcode打开项目目录下的SwiftServerDemo.xcodeproj文件,此时工程结构如下:
HO-2:SwiftServerDemo HO$ swift package generate-xcodeproj
generated: ./SwiftServerDemo.xcodeproj
用xcode打开工程,删除main.swift中的有关路由部分的代码,删除后的main.swift文件内容如下:
import PerfectLib
import PerfectHTTP
import PerfectHTTPServer
let server = HTTPServer()
server.addRoutes(signupRoutes())
server.serverPort = 8080
do {
try server.start()
} catch PerfectError.networkError(let err, let msg) {
print("Error Message: \(err) \(msg)")
}
将刚刚我们删掉的那部分代码,粘贴到刚刚创建的routeHandlers.swift文件中,然后适当的修改。routeHandlers.swift文件内容如下:
import PerfectLib
import PerfectHTTP
import PerfectHTTPServer
public func signupRoutes() -> Routes{
return addURLRoutes()
}
func addURLRoutes() -> Routes {
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 Swift</body></html>")
response.completed()
})
return routes
}
这段代码,将刚刚添加的“Hello Swift”页路由放到了统一文件中进行管理。编译运行,没毛病。
上面代码中add方法最后一个参数handler是传入一个闭包,该闭包定义为public typealias RequestHandler = (HTTPRequest, HTTPResponse) -> (),所以我们可以将一个符合该类型的参数传入add方法中。修改routeHandlers.swift文件如下:
import PerfectLib
import PerfectHTTP
import PerfectHTTPServer
public func signupRoutes() -> Routes{
return addURLRoutes()
}
func addURLRoutes() -> Routes {
var routes = Routes()
routes.add(method: .get, uri: "/", handler: helloHandler)
return routes
}
func helloHandler(request: HTTPRequest, _ response: HTTPResponse) {
response.setHeader(.contentType, value: "text/html")
response.appendBody(string: "<html><title>Hello</title><body>Hello Swift</body></html>")
response.completed()
}
重新运行编译,完全没毛病。
MongoDB数据库
MongoDB是一种非关系型数据库,可以存储类JSON格式的BSON数据,所以深受广大开发者的喜爱,我们在此使用MongoDB举例。
对于已经使用过MongoDB的同学,可以不用看安装和配置部分。
安装Homebrew:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
安装mongoldb:
brew install mongodb
要安装mongo-c:
brew install mongo-c-driver
到这里我们我们完成了mongodb 的安装,如遇到权限问题,具体不表,可以自行谷歌或者查看我之前python文集中有关mongodb的安装传送门。
数据库连接
使用vim打开Package.swift:
HO-2:SwiftServerDemo HO$ vi Package.swift
在Package.swift中,添加MongoDB依赖如下:
import PackageDescription
let package = Package(
name: "SwiftServerDemo",
dependencies: [
.Package(
url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git",
majorVersion: 2, minor: 0
),
.Package(
url: "https://github.com/PerfectlySoft/Perfect-MongoDB.git",
majorVersion: 2
)
]
)
回到终端build:
HO-2:SwiftServerDemo HO$ swift build
巴拉巴拉一堆日志输出....像下面这样子就表示mongodb依赖完成了。
Fetching https://github.com/PerfectlySoft/Perfect-MongoDB.git
Fetching https://github.com/PerfectlySoft/Perfect-mongo-c.git
Cloning https://github.com/PerfectlySoft/Perfect-MongoDB.git
Resolving https://github.com/PerfectlySoft/Perfect-MongoDB.git at 2.0.8
Cloning https://github.com/PerfectlySoft/Perfect-mongo-c.git
Resolving https://github.com/PerfectlySoft/Perfect-mongo-c.git at 2.0.0
Compile Swift Module 'MongoDB' (7 sources)
/Users/HO/SwiftServerDemo/.build/checkouts/Perfect-MongoDB.git--8154448075697536604/Sources/MongoDB/MongoCollection.swift:457:13: warning: 'mongoc_collection_save' is deprecated
let res = mongoc_collection_save(ptr, sdoc, nil, &error)
^
/Users/HO/SwiftServerDemo/.build/checkouts/Perfect-MongoDB.git--8154448075697536604/Sources/MongoDB/MongoCollection.swift:580:16: warning: 'mongoc_collection_find' is deprecated
let cursor = mongoc_collection_find(ptr, flags.queryFlags, UInt32(skip), UInt32(limit), UInt32(batchSize), qdoc, fields?.doc, nil)
^
/Users/HO/SwiftServerDemo/.build/checkouts/Perfect-MongoDB.git--8154448075697536604/Sources/MongoDB/MongoGridFS.swift:401:15: warning: 'mongoc_gridfs_find' is deprecated
plist = mongoc_gridfs_find(handle, &query)
^
/Users/HO/SwiftServerDemo/.build/checkouts/Perfect-MongoDB.git--8154448075697536604/Sources/MongoDB/MongoGridFS.swift:403:15: warning: 'mongoc_gridfs_find' is deprecated
plist = mongoc_gridfs_find(handle, filter?.doc)
^
Compile Swift Module 'SwiftServerDemo' (2 sources)
Linking ./.build/debug/SwiftServerDemo
然后再使用SPM重新生成一份xcode文件:
HO-2:SwiftServerDemo HO$ swift package generate-xcodeproj
generated: ./SwiftServerDemo.xcodeproj
用xcode打开工程,在routeHandlers.swift中,添加import MongoDB,并用一个字符串常量指定MongoDB服务器地址var mongoURL = "mongodb://localhost:27017"。
添加一个新的路由,用来查找数据库数据:
//添加一个新的路由,用来查找数据库数据
func queryFullDBHandler(request: HTTPRequest, _ response: HTTPResponse) {
//创建连接
let client = try! MongoClient(uri: mongoURL)
//连接到具体的数据库,假设有个数据库名字叫 SwiftServerDemoTest
let db = client.getDatabase(name: "SwiftServerDemoTest")
//定义集合
guard let collection = db.getCollection(name: "SwiftServerDemoTest") 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()
}
将该路由部署上去:
func addURLRoutes() -> Routes {
var routes = Routes()
routes.add(method: .get, uri: "/mongo", handler: queryFullDBHandler(request:_:))
return routes
}
编译运行,我们在浏览器中打开http://localhost:8080/mongo
发现返回一个JSON对象:{"data":[]}。
接下来我们添加一个数据库写入接口:
//添加一个新的路由,用来添加数据写入接口
func addHandler(request: HTTPRequest, _ response: HTTPResponse) {
//创建连接
let client = try! MongoClient(uri: mongoURL)
//连接到具体的数据库,假设有个数据库名字叫 SwiftServerDemoTest
let db = client.getDatabase(name: "SwiftServerDemoTest")
//定义集合
guard let collection = db.getCollection(name: "SwiftServerDemoTest") else { return }
//定义BSON对象,从请求的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()
}
将该路由部署上去:
func addURLRoutes() -> Routes {
var routes = Routes()
routes.add(method: .post, uri: "/mongo/add", handler: addHandler(request:_:))
return routes
}
编译运行,没毛病!!
现在我们借助接口调试工具PAW来测试这个接口。
在接口调试工具中,选择POST,地址http://localhost:8080/mongo/add, body部分给出一个JSON对象,比如{"name" : "CRonaldo", "birth" : "1985-2-5", "nationality" : "Portugal","number":"7" },然后打出请求,返回值如果是我们打出的JSON对象,说明请求正常返回了,如下图:
图中1:请求method 请求 地址
图中2:请求body
图中3:返回json
接下来用刚才部署好的http://localhost:8080/mongo 接口来验证一下我们是否真的插入了新的数据,返回结果默认是UTF8编码,如果有中文乱码的情况可以考虑下编码是否有问题。结果如下:
{"data":[{ "_id" : { "$oid" : "59536b6a4c6063a74f4eee42" }, "name" : "C Ronaldo", "birth" : "1985-2-5", "nationality" : "Portugal", "number" : "7" },{ "_id" : { "$oid" : "59536bac4c6063a82e70c822" }, "name" : "C Ronaldo", "birth" : "1985-2-5", "nationality" : "Portugal", "number" : "7" },{ "_id" : { "$oid" : "59536baf4c6063a82e70c824" }, "name" : "C Ronaldo", "birth" : "1985-2-5", "nationality" : "Portugal", "number" : "7" }]}
没毛病,到这里我们就成功了。。。。
过滤器
我们在上网时如果访问到不存在的资源,会看到一个“404 NOT FOUND”页面,类似还有“403 FORBIDDEN”、“401 UNAUTHORIZED”等等,要对这些页面进行过滤,并在发生问题的时候做出一些操作,Perfect为我们提供了HTTPResponseFilter。HTTPResponseFilter是一个协议,含有两个方法,本着Swift的“能用struct就不要用class”的思想,我们可以定义一个struct,遵循HTTPResponseFilter,作为我们的过滤器。代码如下:
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改为“ …… path …… is not found.”。
然后我们在main.swift文件中,把之前写好的代码稍加改动:
import PerfectLib
import PerfectHTTP
import PerfectHTTPServer
let server = HTTPServer()
server.addRoutes(signupRoutes())
server.serverPort = 8080
do {
try server
.setResponseFilters([(Filter404(),.high)])
.start()
} catch PerfectError.networkError(let err, let msg) {
print("Network Error Message: \(err) \(msg)")
}
类似的,我们还可以过滤其他http错误,具体可查阅HTTPResponse中的HTTPResponseStatus。