全棧Swifter:用Perfect框架開發服務器端
上個月有一件讓Swifter興奮的事情:蘋果官方啟動了Swift語言服務器端開發工作組。這意味著官方正式表態,Swift進軍服務器端開發。
前幾天我參加了iDev全平臺蘋果開發者大會,會上楊暉老師帶領我們對Swift的服務器端開發前景做了分析,對Swift語言目前的幾大服務器端開發框架進行了剖析,說實話收獲并不多,因為我之前也在持續關注這一塊的信息,但對我的啟發是非常大的,這直接導致會后幾天每天都心里癢,想寫點什么。
于是我就寫了點什么:joy:
我寫了啥
我的畢業設計正好需要一個服務器端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來初始化一個項目。
mkdir MySwiftServer
vi Package.swift
以上,新建一個 MySwiftServer 文件夾,用Vim新建一個 Package.swift 文件,這個文件你可以理解為CocoaPod中的 Podfile 。
在 Package.swift 中輸入以下內容。
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 文件,作為程序入口,代碼如下:
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 文件內容如下:
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 文件內容如下:
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 文件如下:
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的同學,可以不用看安裝和配置部分。
安裝
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依賴,如下:
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" 。
添加一個新的路由,用來查找數據庫數據:
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()
}
將該路由部署上去:
func addURLRoutes() {
routes.add(method: .get, uri: "/mongo", handler: queryFullDBHandler)
}
編譯運行,我們在瀏覽器中打開 http://localhost:8181/mongo 發現返回一個JSON對象: {"data":[]} 。接下來我們添加一個數據庫寫入接口:
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編碼,如果有中文亂碼的情況可以考慮下編碼是否有問題。結果如下:
{"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 ,作為我們的過濾器。代碼如下:
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 文件中,把之前寫好的代碼稍加改動:
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的方式來部署。有興趣的話,可以持續關注我的博客。
來自:http://blog.talisk.cn/blog/2016/11/08/Perfect-Swifter/