全栈Swifter:一、Perfect框架初识

因为一直是在做移动端的开发,在工作中总会遇到比较坑的后端。
之前就遇到过一个让人抓狂的后端开发人员,本来只需一句代码解析的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对象,说明请求正常返回了,如下图:

PAW工具

图中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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容