全栈Swifter:用Perfect框架开发服务器端
JamalErx
8年前
<p>上个月有一件让Swifter兴奋的事情:苹果官方启动了Swift语言服务器端开发工作组。这意味着官方正式表态,Swift进军服务器端开发。</p> <p>前几天我参加了iDev全平台苹果开发者大会,会上杨晖老师带领我们对Swift的服务器端开发前景做了分析,对Swift语言目前的几大服务器端开发框架进行了剖析,说实话收获并不多,因为我之前也在持续关注这一块的信息,但对我的启发是非常大的,这直接导致会后几天每天都心里痒,想写点什么。</p> <p>于是我就写了点什么:joy:</p> <h2><strong>我写了啥</strong></h2> <p>我的毕业设计正好需要一个服务器端API,之前用Node.js写了一点,不过还没做完,这次正好趁着这个机会,用Swift来写一写,岂不是美哉美哉。</p> <p>这次我用最新的Perfect 2.0.2框架,最新的Xcode 8.1(实测,8.0编译时有bug),进行开发。至于为什么在众多Swift服务器端框架中挑中了Perfect,无他,星多而已。</p> <p>我不保证你在后续版本中,可以继续使用我介绍的API,而且官方虽然提供了中文文档,还有其他一些周边工具、中间件的详尽文档,但有相当一部分的内容不符合最新版本Perfect的API。所以踩坑还需后来人。扯的太多了,下面上干货。</p> <h2><strong>开始干货</strong></h2> <h2><strong>开发环境</strong></h2> <p>首先你要有个macOS来进行开发,并保证安装了最新的Xcode 8.1。Ubuntu确实可以安装Swift,并对项目进行编译,但经过和一些开发者交流,目前主流开发方式还是用macOS开发,部署到Ubuntu服务器上。</p> <h2><strong>项目初始化</strong></h2> <p>我们可以通过SwiftPackageManager来初始化一个项目。</p> <pre> <code class="language-swift">mkdir MySwiftServer vi Package.swift </code></pre> <p>以上,新建一个 MySwiftServer 文件夹,用Vim新建一个 Package.swift 文件,这个文件你可以理解为CocoaPod中的 Podfile 。</p> <p>在 Package.swift 中输入以下内容。</p> <pre> <code class="language-swift">import PackageDescription let package = Package( name: "MySwiftServer", dependencies: [ .Package( url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", majorVersion: 2, minor: 0 ) ] ) </code></pre> <p>语法是不是挺熟悉,这段Swift代码是作为项目的配置文件,表明了我们的项目名、所需依赖和依赖的版本。</p> <p>保存好该文件,回到终端,执行 swift build ,第一次编译会从仓库clone所有的dependencies到本地,速度可能有点慢,好好等待就可以了。</p> <p>当所有module编译完成后会提示我们 warning: root package 'MySwiftServer' does not contain any sources ,意思是我们还没有源代码。我们可以在项目目录下新建一个文件夹,名为 Sources ,用来保存源文件。在 Sources 目录中新建一个 main.swift 文件,作为程序入口,代码如下:</p> <pre> <code class="language-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)") } </code></pre> <p>这段代码首先创建了一个路由,是get方法,路径是根路径,并且返回了一段html代码,设置服务器端口为8181,然后是用一个do循环来驱动了服务器。</p> <p>重新执行 swift build ,完成编译后,我们可以执行 .build/debug/MySwiftServer 来运行我们的程序,服务器会监听8181端口。打开浏览器,输入 http://localhost:8181/,我们可以看到浏览器页面中显示Hello World。</p> <h2><strong>项目配置</strong></h2> <p>我们可以利用SPM来生成xcodeproj,执行 swift package generate-xcodeproj ,当提示 generated: ./MySwiftServer.xcodeproj 后,即可用Xcode打开项目目录下的MySwiftServer.xcodeproj文件。</p> <p>在Xcode左侧navigator中,选择Project-MySwiftServer-Build Settings-Library Search Paths,添加 "$(PROJECT_DIR)/**" ,注意要包含前后引号。</p> <p>配置完成后,就可以用Xcode来写代码、Build、Run项目。</p> <h2><strong>运行服务器</strong></h2> <p>尝试⌘CMD+R,运行项目,console中会提示服务器已经在8181端口跑起来了。打开浏览器,输入地址 http://localhost:8181/ ,马上可以看到页面上显示我们配置好的Hello World页面。</p> <h2><strong>路由</strong></h2> <p>在 PerfectHTTP 中,有一个struct名为 Routes ,我们可以通过它来构建服务器的路由。</p> <p>在 Sources 目录中,创建一个名为 routeHandlers.swift 的文件,删除 main.swift 中的有关路由部分的代码,删除后的 main.swift 文件内容如下:</p> <pre> <code class="language-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)") } </code></pre> <p>将刚刚我们删掉的那部分代码,粘贴到刚刚创建的 routeHandlers.swift 文件中。 routeHandlers.swift 文件内容如下:</p> <pre> <code class="language-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() } } </code></pre> <p>这段代码,将刚刚添加的“Hello World”页路由放到了统一文件中进行管理,然后我们在 main.swift 中,记得调用 signupRoutes 方法。编译运行,一切正常。</p> <p>上面代码中 add 方法最后一个参数 handler 是传入一个闭包,该闭包定义为 public typealias RequestHandler = (HTTPRequest, HTTPResponse) -> () ,所以我们可以将一个符合该类型的参数传入 add 方法中。修改 routeHandlers.swift 文件如下:</p> <pre> <code class="language-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() } </code></pre> <p>重新运行编译,完全没问题。</p> <h2><strong>MongoDB数据库</strong></h2> <p>MongoDB是一种非关系型数据库,可以存储类JSON格式的BSON数据,所以深受广大开发者的喜爱,我们在此使用MongoDB举例。</p> <p>对于已经使用过MongoDB的同学,可以不用看安装和配置部分。</p> <h3><strong>安装</strong></h3> <pre> <code class="language-swift">brew install mongodb </code></pre> <p>我们通过HomeBrew来为我们的Mac安装MongoDB,但是在El Capitain及之后版本,由于SIP的原因,可能会在安装的过程中出现权限问题,可以暂时关闭SIP功能,在安装完成后再开启,具体不表。如果遇到问题,可以谷歌一下“10.11 mac mongodb”。</p> <h3><strong>配置</strong></h3> <p>mkdir /data/db :创建目录 /data/db ,是MongoDB的默认存储目录。</p> <p>chown id -u /data/db :赋权限。</p> <p>mongod :开启服务</p> <p>唰唰唰,一堆日志输出,这样子就可以了。</p> <h3><strong>可视化工具</strong></h3> <p>macOS平台上,有MongoHub可视化工具,不过在我使用过程中遇到了几次闪退,估计是很久没更新的原因吧,大家如果有比较好的工具可以告诉我哈。</p> <h3><strong>数据库连接</strong></h3> <p>在 Package.swift 中,添加MongoDB依赖,如下:</p> <pre> <code class="language-swift">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)) ] ) </code></pre> <p>编译,在经过等待后,项目中已经有MongoDB这个module了。</p> <p>在 routeHandlers.swift 中,添加 import MongoDB ,并用一个字符串常量指定MongoDB服务器地址 var mongoURL = "mongodb://localhost:27017" 。</p> <p>添加一个新的路由,用来查找数据库数据:</p> <pre> <code class="language-swift">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() } </code></pre> <p>将该路由部署上去:</p> <pre> <code class="language-swift">func addURLRoutes() { routes.add(method: .get, uri: "/mongo", handler: queryFullDBHandler) } </code></pre> <p>编译运行,我们在浏览器中打开 http://localhost:8181/mongo 发现返回一个JSON对象: {"data":[]} 。接下来我们添加一个数据库写入接口:</p> <pre> <code class="language-swift">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) } </code></pre> <p>现在我们借助接口调试工具(PAW/Postman等)来测试这个接口,我们编译运行服务器,在接口调试工具中,选择POST,地址 http://localhost:8181/add ,body部分给出一个JSON对象,比如 {"text" : "test", "desc" : "description", "detail" : "detail" } ,然后打出请求,返回值如果是我们打出的JSON对象,说明请求正常返回了。接下来用刚才部署好的 http://localhost:8181/mongo 接口来验证一下我们是否真的插入了新的数据,返回结果默认是UTF8编码,如果有中文乱码的情况可以考虑下编码是否有问题。结果如下:</p> <pre> <code class="language-swift">{"data":[{ "_id" : { "$oid" : "58203e113cba965b8d5616a2" }, "text" : "test", "desc" : "description", "detail" : "detail" }]} </code></pre> <p>我们成功了。</p> <h2><strong>过滤器</strong></h2> <p>我们在上网时如果访问到不存在的资源,会看到一个“404 NOT FOUND”页面,类似还有“403 FORBIDDEN”、“401 UNAUTHORIZED”等等,要对这些页面进行过滤,并在发生问题的时候做出一些操作,Perfect为我们提供了 HTTPResponseFilter 。 HTTPResponseFilter 是一个协议,含有两个方法,本着Swift的“能用struct就不要用class”的思想,我们可以定义一个struct,遵循 HTTPResponseFilter ,作为我们的过滤器。代码如下:</p> <pre> <code class="language-swift">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) } } } </code></pre> <p>大概意思就是拦截下response,如果状态值是 notFound ,我们就把response的body改为“Hehe …… path …… is not found.”。</p> <p>然后我们在 main.swift 文件中,把之前写好的代码稍加改动:</p> <pre> <code class="language-swift">do { try server .setResponseFilters([(Filter404(), .high)]) .start() } catch PerfectError.networkError(let err, let msg) { print("Network error thrown: \(err) \(msg)") } </code></pre> <p>设置好我们的 Filter404() ,访问个不存在的资源试一试: http://localhost:8181/hehe ,果然如愿以偿地显示了 /hehe is not found.</p> <p>类似的,我们还可以过滤其他http错误,具体可查阅 HTTPResponse 中的 HTTPResponseStatus 。</p> <h2><strong>何去何从</strong></h2> <p>目前看来Perfect提供的API已经非常完善了,基本具备了一个Web服务器框架的常用特性。但是从目前的文档看来,Perfect的API变动还是比较频繁的,我们大可以抱着玩的态度来对待这个Swift in server。相信在Swift服务器开发官方工作组的推动下,我们不久就可以用这门年轻的语言来开发我们的服务器应用。</p> <p>关于部署到服务器,我近期可以踩一踩这部分的坑,目前计划是用Ubuntu in Docker的方式来部署。有兴趣的话,可以持续关注我的博客。</p> <p> </p> <p>来自:http://blog.talisk.cn/blog/2016/11/08/Perfect-Swifter/</p> <p> </p>