全棧Swifter:用Perfect框架開發服務器端

JamalErx 8年前發布 | 8K 次閱讀 Swift Apple Swift開發

上個月有一件讓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/

 

 本文由用戶 JamalErx 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!