在服务端写 Swift 是一种怎样的体验
发布于:2018-06-21 22:32,阅读数:3532,点赞数:7
> 本文为本人原创并首发于2017年3月发布于《iOS成长之路 2017春》中。 > > 由于时间关系,部分内容在当前版本的工具着或已失效,本文仅供参考。 不得不说,在生产中使用Swift开发服务端是一次大胆的尝试。是其发展速度和活跃程度给了我足够的信心,而其开发速度和运行效率又给了我足够的动力。在实际项目中以 MySQL 和 HDFS 作为支撑,应用了包含 Web 服务、JSON 接口服务、以及基于 Socket 的文件传输和视频直播流服务在内的业务。这些服务最终都运行在 Linux 服务器上,实践的结果让人满意。因此笔者非常期待 Swift 能在服务端市场中能大显身手。 ## 溯源 ### Swift在Linux上的表现 北京时间 2015 年 6 月 9 日凌晨的 WWDC 大会上,发布 Swift 2.0 的同时宣布 Swift 即将**开源**,开源内容包括**编译器**和**标准库**,并支持 Linux。开源和跨平台给语言带来了更宽的发展通道。 那么在 Linux 中 Swift 表现到底如何呢? 其实早期的在 iOS 上使用 Swift 的开发者可能会发现,iOS 和macOS 中的 Swift 并没有脱离 Objective-C。很多 SDK 中的 Swift 类其实仅仅是 Objective-C 类的**封装**。例如在开发应用的时候不可避免的要使用`UIViewController`或者`NSViewController`这些标准框架类,这些类存在于 UIKit 等标准库中。虽然 App 主体使用了 Swift 开发,但这些标准库仍然是旧的 Objective-C 库,造成的结果就是这些 Swift 代码仍然跑在 Objective-C runtime 中。这常常让人觉得所谓的纯 Swift 项目一点都不纯,写 Swift 简直多此一举,还不如直接操作 Objective-C 呢。 但是在 Linux 中,纯 Swift 可以真的是**纯 Swift**。Apple 这些年在软件产品上一直背负着沉重的历史负担,大量的库都使用 Objective-C 编写,运行在 Objective-C runtime 中。为了不使原本的功能失效,Apple 在把所有的库重新实现一遍之前是无法撤掉 runtime 的。而在 Linux 中,Apple 没有任何负担,可以直接使用新的库。包括 Foundation 在内的系统库已经被打造成为了C语言和 Swift 编写的库,它不再运行在 Objective-C runtime 上了。此时调用函数将在**编译时被链接**,代码的性能会得到提高,而原本的 runtime 的黑魔法也都将失效。 经过笔者这段时间对 Linux 上的 Swift 的体验,它基本上可以满足日常项目的需要。虽然有些 API 还未被实现,但总能找到替代的办法。其运行也是稳定的,至少笔者手上的数据是零崩溃。目前语言还在快速发展中,问题会逐渐被修复,库函数也会逐渐被完善,因此笔者对 Linux 上的 Swift 还是充满信心的。 ### Swift开发服务端的资质 按照目前 Swift 的情况是具有在 Linux 上开发**完整**服务端软件能力的。 Linux 中使用`Swift Package Manager(SPM)` 来构建项目,SPM 以文件夹**目录**为项目分支结构,以`Module`为单位构建项目。其中,SPM 支持 C/C++ Module ,同时也支持 stl 等标准 C++ 库,这为我们提供了无限的扩展可能。 操作系统本身提供了大量C语言接口可供调用。Swift 可以原生访问系统的库,同样是以 Module 的形式。C语言的数据结构会被自动桥接为`struct`,直接使用。当然从语法上来讲,最佳的办法是自己再包装一层 Module,毕竟在 Swift 中操作的是`String`而不是`char *`,是`Data`而不是`unsigned char *`。 只要是提供 C/C++ 接口的各类服务,都可以集成进 Swift 项目中。不论是 MySQL,还是 Hadoop,它们都提供了C语言的库,如果想要在 Swift 的服务端中调用,直接写一个 C Module 开放接口即可。由于 Swift 并非纯面向对象语言,因此在这里开放接口甚至都不需要用类来封装。具体的做法将在后面的内容中提到。 One more thing,Swift 可以很简单地调用系统命令行。熟悉 Cocoa 的读者应该知道`NSTask`这个类可以调用系统命令,同样的我们也可以用这个在 Linux 中调用系统命令行。这个类在 Swift 中改成了`Process`,在 Linux 中这个类叫`Task`,使用方法相同。既然可以调用系统命令行,那么我们就可以直接执行 shell 脚本来更多的事了。考虑到 macOS 和 Linux 中使用不同的 API 所造成的麻烦,我们可以用**条件编译**来很方便地解决这个问题: ```c++ #if os(Linux) //在Linux中编译的代码 #else //在macOS中编译的代码 #end ``` ### Swift开发服务端的优点 既然 Swift 拥有了开发完整服务端软件的能力,那么就可以用它开发服务端了。可是现在 .Net、Java 等各大服务端框架已经非常成熟,如果用 Swift 开发,到底有哪些好处呢? 首先是代码层面: - 编译后为原生代码,静态链接,运行效率高。 - Swift 在字符串、集合等数据的业务处理上有着较好的性能和极简的语法。 - 仍然可以使用如闭包、GCD 等开发者熟悉的东西,为开发带来极大便利。 - 内存管理仍然采用引用计数,及时回收,避免了例如 C++ 忘记手动回收造成泄露和 Java 延时回收难以 Debug 的弊端。 - Swift 目前已经开源,正处于快速迭代的成长期,社区非常活跃,具有潜力。 项目层面: - 支持纯C/C++语言 Module,无限的接入扩展性。 - SPM 支持直接从 GitHub 同步代码,方便管理。 - Swift 开发的服务端和 Swift 开发的 iOS、macOS 客户端可以共用同一套模型源文件,非常方便。 - 目前已有相对成熟的框架以及各类工具库可供使用,正在快速成长。 现在的 iOS 招聘信息里普遍都会考虑 Swift,有很多新项目已经采用 Swift 开发出来了。在客户端上,大家正在逐渐接受 Swift。那么在服务端,Swift 是否也可以占有一席之地呢?这个问题就留给时间回答吧。 ## 实践 ### Swift服务端框架 目前 Swift 的后端框架主要有`Perfect`、`Vapor`、`Kitura`和`Zewo`等。前段时间掘金上有一篇文章在几个维度对比了几个框架的性能:[不服跑个分 - 顶级 Swift 服务端框架对决 Node.js](https://gold.xitu.io/entry/57e296af0bd1d000570ee3b4)。 文章中测试了以上四种框架以及`Node.js`在性能上的表现,结果汇总如下:  在测试结果中,Perfect 以非常优秀的成绩胜出,因此笔者最终也选择了Perfect。[PerfectlySoft](https://github.com/PerfectlySoft) 公司一直保持着比较高产的状态,同时也是一家非常亲中国的公司。不仅有简体中文的文档,还有官方微信账号。也有不少同行们收到了公司发来的中文邮件。 在实际的开发体验中,Perfect 的表现值得肯定。内存占用很低,官方提供的库也非常全面。从基本的 HTTP 服务器(`Perfect-HTTPServer`),多线程库(`Perfect-Threading`),日志库(`Perfect-Logger`),到各类数据库链接库(`Perfect-MySQL`, `Perfect-MongoDB`等),甚至到Hadoop(`WebHDFS`, `MapReduce`等功能)和邮件库(`Perfect-SMTP`)都提供了。就在写文的今天,官方在服务器助手中新增了在 Mac 上交叉编译,一键测试 Linux 版的功能。这些更新每周都会在官方微信中推送,而且是简体中文的文章,这对中国开发者来说是极为友好的。关于详细的开发体验将在下文提到。 由于其他框架笔者接触的不多,因此就此带过。但也并不是说分数低其他框架就不好,例如 Vapor 的路由写法更简单一点,MVC 的结构更舒服一点,读者可以自由选择。另外对测试结果好奇的读者可以自行测试,测试代码都在原文链接之中。 ### 基于Perfect的HTTP Server开发体验 如何使用 Perfect 开发服务端呢?这里笔者将分为**开发过程**和**部署过程**进行介绍。 #### 开发过程 程序最终是跑在 Linux 机器里的,但依然可以继续在 Mac 中开发和调试,最终再移植到 Linux 中进行编译运行。PerfectlySoft 提供了例子 [PerfectTemplate](https://github.com/PerfectlySoft/PerfectTemplate) ,包含了简单的 HTTP 服务器创建的过程。README 中的过程是基于 SPM 编译管理的,但在 Mac 下,我们有更好的选择 —— Xcode。 克隆官方的例子后使用`build`命令自动下载依赖库: ```bash $ swift build ``` 之后这个命令生成 Xcode 工程: ```bash $ swift package generate-xcodeproj ``` 接着我们就可以像开发 macOS 应用那样开发服务端了。官方在文档中提醒我们不要直接编辑这个 xcodeproj 文件。因为有时我们需要修改 Package.swift 来下载更多依赖库,这时候这个 xcodeproj 会被重新生成,之前所做的所有内容都会被覆盖。本书的 Sample 只做演示用,所以并没有这样做。读者在自己正式的项目中要注意这一点,避免以后发生麻烦。 在现在版本的 README 中,提供了一种非常炫酷的创建服务的方式,直接构造一个多维数组,填写相关信息,之后就可以直接跑起一组服务: ```swift import PerfectLib import PerfectHTTP import PerfectHTTPServer let port1 = 8080, port2 = 8181 let confData = [ "servers": [ [ "name":"localhost", "port":port1, "routes":[ //... ], "filters":[ //... ] ] ] ] do { // Launch the servers based on the configuration data. try HTTPServer.launch(configurationData: confData) } catch { fatalError("\(error)") // fatal error launching one of the servers } ``` 由于这种方式十分简洁干练但不利于读者理解内部实现的结构,笔者会使用原来的接口创建方法来讲。 在 Perfect 中创建 HTTP 服务极为简单,在路由的创建过程中,开发者只需要提供了一个处理该路由的回调函数,其余大量的工作 Perfect 已经帮我们完成了。当 Perfect 服务器接收到一个HTTP请求后,服务器首先会将请求交给当前已经注册了的过滤器。这些过滤器可能会修改或者重定向请求,转发给别的路由。当过滤器处理完成后服务端会寻找有没有相应的回调,如果有则会交给回调函数进行处理,如果没有则会返回 404 。来直接看下面的例子,完成一个最简单的 HTTP Server 程序: ```swift import Foundation import PerfectLib import PerfectHTTP import PerfectHTTPServer open class MyServer { fileprivate var server: HTTPServer internal init(root: String, port: UInt16) { //构造 HTTPServer 对象 server = HTTPServer.init() //构造路由对象,这只是个容器,现在这里面并没有内容 var routes = Routes.init() //配置路由,添加URL以及回调函数 configure(routes: &routes) //将路由添加进服务 server.addRoutes(routes) //设置端口和根目录 server.serverPort = port server.documentRoot = root } //配置路由函数 fileprivate func configure(routes: inout Routes) { //添加接口,路径为/,方法为GET,回调函数为闭包 routes.add(method: .get, uri: "/", handler: { request, response in //取得url中的参数,类型是`[(String, String)]`,遍历即可 let param = request.params() //返回数据头 response.setHeader(.contentType, value: "text/html") //返回数据体 response.appendBody(string: "Hello World") //返回 response.completed() }) } //开始服务 open func start() { do { try self.server.start() } catch PerfectError.networkError(let err, let msg) { print("Network error thrown: \(err) \(msg)") } catch { print("Network unknow error") } } } ``` 包含一个 URL 接口的 HTTP Server 就写完了,最终在`main.swift`中调用: ```swift import Foundation let myServer = MyServer.init(root: "Your Path To Root", port: 8080) myServer.start() ``` 此时可以看到控制台输出: ``` [INFO] Starting HTTP server on 0.0.0.0:8080 with document root "Your Path To Root" ``` 现在我们就可以在浏览器看到结果了:  如果在 Response 中返回页面 HTML 代码,那么这就是个 Web 服务器。如果在 Response 中返回 JSON/XML 字符串,那么这就是个业务接口,可以给手机 App 使用。 HelloWorld 固然简单,实际情况往往没那么简单。倘若要建个站点,必然包含了大量`css`、`JavaScript`以及图片等静态资源,这些静态资源如果需要开发者手动加入路由显然不现实。所以在`PerfectHTTP`中提供了`StaticFileHandlerd`模块帮我们实现了静态资源的处理。 若不修改任何配置,则在最初设置的 root 目录下所有的文件都是可以被直接下载的。描述文件 URL 的这些请求会被方法`handleRequest`处理,在路由设置不存在的情况下会访问本地文件,若文件存在则会直接发送文件。如果读者需要建立一个静态 Web 站点,甚至可以一个路由都不配置,直接运行一个空的 HTTPServer,这些静态资源会被直接开放在网络中,访问者可以直接根据 URL 访问不同的页面。当然,也可以手动映射静态资源,利用通配符将一个本地目录映射到一个 URL 上,具体实现过程可以参考官方文档,在此不多介绍。 另外官方提供的`File`模块也能以**数据流**的形式很便利地操作文件。在页面方面,Perfect 同样支持`Mustache`模板功能。官方提供的`Perfect-Mustache`库提供了相关的功能支撑。这是一个非常好用的页面模板引擎,一个很好的页面中动态元素替换的解决方案。在页面 HTML 代码中嵌入例如`{{var}}`的占位字段,在 Swift 代码中提供一个字典,将页面中的占位符替换成字典中的值。详细的使用过程可以参阅官方文档,这个库可以独立于服务器使用。 总之,官方提供了大量的工具库且正在以非常高的活跃度继续开发,大大便利开发者们。 #### 部署过程 Linux 上的 Swift 环境配置在此不做介绍,具体配置过程可使用 PerfectlySoft 提供的安装脚本:[Perfect-Ubuntu](http://www.enumsblog.com/post?pid=16009),或自行搜索配置。 现在我们的服务已经在 Mac 上跑起来了,那么如何部署到 Linux 服务器中呢?例子中的 HelloWorld 直接拷贝到 Linux 服务器中 build 是直接可以通过的,这个例子太过初级,这里讲个稍微复杂一点的帮助读者理解。 在 Linux 中我们使用 SPM 管理项目。例如我们的项目包含了 Swift 主函数代码和一个纯C语言的 Framework。在 Xcode 中开发时,可以按照自己的习惯建立两个工程,主工程链接库工程生成的库,这个过程相信大家都很熟悉了。但为了 Linux 工程的管理,源文件需要放在两个独立的文件夹中。具体目录结构如下图:  SPM 会按照`Package.swift`文件来为我们生成`Target`,处理依赖关系。SwiftCode 模块依赖了 CCode,因此此时要将该文件改成这样: ```swift import PackageDescription let package = Package( name: "PerfectTemplate", targets: [Target(name: "SwiftCode", dependencies:["CCode"])], dependencies: [ .Package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", majorVersion: 2, minor: 0), ] ) ``` 这时候在 Linux 中 build 该项目时,CCode Module 会被单独编译成`CCode.so`供主模块调用。无论是混编还是纯 Swift 项目也可以这样处理,当然,别想着混编 Objective-C 了,这儿已经没有 Objective-C runtime了。按照这样的目录结构在 Linux 中 build,将会生成隐藏的`.build`目录,执行代码运行: ````bash $ ./.build/debug/SwiftCode ```` 这样我们的服务就已经在 Linux 上跑起来了。嗯,如果代码确认没问题,需要发行 Release 版本呢? ```bash $ swift build -c release ``` 之后会生成`./.build/release`文件夹,其中包含了 Release 版的库和可执行文件。 清理工程可以使用下面这个命令: ```bash swift build --clean //当然,也可以粗暴一点,直接删除.build目录,效果是一样的 rm -rf .build ``` 如果需要连带依赖库一起清理,使用下面这个命令: ```bash swift build --clean=dist //同样的,可以粗暴地直接手动删除目录 rm -rf .build rm -rf Packages ``` 链接其他第三方库如 libhdfs.so 并编译 release 版本: ```bash $ swift build -Xlinker -lhdfs -c release ``` 静态编译: ```bash $ swift build -static-stdlib ``` 其他编译器功能请参阅: ```bash $ swift --help ``` #### 小结 通过以上这个例子,我们创建了一个简单的 HTTP 服务器,并且部署到了 Linux 服务器上。由于框架是开源的,甚至可以直接修改源代码来完成例如埋点等功能。在接下来的部分,笔者将会添加一些常规的服务来进一步让大家体会 Swift 在服务端的开发体验。 ### 基于Perfect的常规服务接入体验 一个完整的服务器只包含 HTTP 服务和文件系统功能肯定是不够的,必须有一种方法让开发者接入其他第三方服务。第三方服务的 SDK 有 Swift 版的吗?嗯,很少很少。但多数厂商都会提供C语言接口,这就可以作为开发者们的突破口,利用C语言接口接入服务。 笔者会举两个简单的例子来介绍第三方服务接入的过程:**MySQL**和**HDFS**。 #### MySQL接入 对于厂商没有提供接口的服务,Perfect 官方“造了很多轮子”来帮助开发者接入。MySQL 的接入可以直接选用官方提供的`Perfect-MySQL`。 编辑 Package.swift ,在依赖库中加入对`Perfect-MySQL`的依赖: ```swift .Package(url:"https://github.com/PerfectlySoft/Perfect-MySQL.git", majorVersion: 2, minor: 0) ``` 下载依赖后就可以使用了,同样来看一个简单的例子: ```swift import Foundation import PerfectLib import PerfectHTTP import MySQL //定义在Linux中的数据库参数 #if os(Linux) let testHost = "数据库IP" let testUser = "数据库登录名" let testPassword = "数据库密码" let testSchema = "Schema名" #else //定义在其他平台的数据库参数参数 #endif internal class MyDB { //构造一个库中的MySQL对象 fileprivate let mysql = MySQL.init() internal init?() { //设置客户端字符集,这是非常必要的操作,否则所有中文可能都会变成问号 guard mysql.setOption(.MYSQL_SET_CHARSET_NAME, "utf8mb4") else { return nil } //连接数据库 guard mysql.connect(host: testHost, user: testUser, password: testPassword) else { return nil } } //执行SQL语句并返回结果 @discardableResult internal func query(_ s: String) -> [[String?]]? { guard mysql.selectDatabase(named: testSchema), mysql.query(statement: s) else { return nil } let results = mysql.storeResults() var resultArray = [[String?]]() while let row = results?.next() { resultArray.append(row) } return resultArray } } ``` 一个简单的类就可以帮助开发者操作数据库了,使用时直接调用`query`方法即可。 `Perfect-MySQL`基于C接口实现,其默认链接的库为`libmysqlclient.so`。官方为开发者做了一次中间封装,处理了大量指针操作,让语法更符合 Swift 标准。库中查询语句的结果会返回一个`MySQL.Results`对象,其包含了查询结果的记录数、字段数等方法可供调用。可能是为了保证性能,官方并未把查询结果全部取出来做 Swift 数据结构封装。数据仍然存在于 C++ 层,以指针`UnsafeMutablePointer<MYSQL_RES>`进行操作,当`MySQL.Results`对象被`ARC`回收时释放 C++ 资源。这种方法相当于维护了一个服务端,在客户端操作类似于操作一个状态机,造成在处理查询结果上语法还是充满了 C Style ,例如需要取第N条数据时必须先调用以下方法将指针移动到目标位置然后读取,读取时也只能用类似生成器的方法读: ```swift //移动到第N条记录 public func dataSeek(_ offset: UInt) //读取这条内容,指针自动后移到下一条记录处 public func next() -> Element? ``` 需要注意的是,这个库默认链接的是`libmysqlclient.so`,在库内部调用`mysql_real_connect`可能会出现线程安全问题。因此多线程调用`connect`方法时需要处理好线程问题。 #### HDFS接入 这是一个典型的调用C接口接入的服务。 HDFS 是一个运行在 Java 上的分布式文件系统,它提供了C接口可供开发者调用。在使用之前需要开发者配置相关环境以及在本机编译 Hadoop 的 Native 库,这个过程不在讨论范围之内。编译完成后会得到一系列 Hadoop 的库,在本文中只选用 HDFS 库,即 libhdfs.so。 在上文中提到了如何利用 SPM 建立 C Module,而C Module可以被自动桥接到 Swift。但不幸的是用于描述文件信息的结构体`hdfsFileInfo`中的变量在 Swift 中是无法被访问到的,因此在数据结构上需要在 C++ 层重新做一下封装。做完封装后将接口封装为纯C语言接口即可供 Swift 端调用。 这里需要一点C语言基础,由于接口数量众多,本文中仅举例连接 HDFS 的函数封装过程。根据 SPM 管理 Module 的目录结构,假设结构如下图所示:  - `CCode.h`:Module 暴露的接口。 ```c++ #include "../CWrap.h" ``` - `HDFSType.h`:针对库中的数据结构的另一层封装,在原有数据结构前加入`cw_`前缀定义新结构。 - `CWrap.h`:HDFS 接口头文件。 ```c++ #include "HDFSType.h" #ifdef __cplusplus extern "C" { cw_hdfsFS cw_func_connect(const char* nn, cw_tPort port); #endif #ifdef __cplusplus } #endif ``` `CWrap.cpp`:HDFS 接口封装的具体实现过程。 ```c++ #include "CWrap.h" #include "HDFS头文件路径/hdfs.h" cw_hdfsFS cw_func_connect(const char* nn, cw_tPort port) { hdfsBuilder * builder = hdfsNewBuilder(); hdfsBuilderSetNameNode(builder, nn); hdfsBuilderSetNameNodePort(builder, port); hdfsBuilderConfSetStr(builder, "dfs.support.append", "true"); hdfsBuilderConfSetStr(builder, "dfs.replication", "1"); return (cw_hdfsFS)hdfsBuilderConnect(builder); } ``` 最终,方法`cw_func_connect`能在 Swift 端被调用: ```swift let fs = cw_func_connect("目标NameNode", cw_tPort(目标端口)) ``` 事实上笔者在 C++ 层不仅是简单封装了接口,更是封装了功能。在平时的开发过程中经常也会在厂商 SDK 和自己的业务代码中间做封装,笔者将其移到了 C++ 中实现,同时也可以暴露更少的数据结构和细节。当然这一层也可以放到 Swift 中去完成,但封装数据结构的工作量会更大。 最后使用如下命令链接 HDFS 库并编译: ```bash $ swift build -Xlinker -lhdfs ``` 实际应用中,笔者的项目是接入了 HDFS 的。在 Swift 端调用时,因为是原生调用,因此性能和稳定性都不错。但关于 HDFS 的开发,其实还存在很多坑。在 Mac 上开发时会遇到例如`CLASSPATH`环境变量未定义的问题,无法`Debug执行`的问题等等。读者若有兴趣可以自行研究。 #### 小结 其实 Swift 端在调用原生服务时并没有给开发者带来太多的额外工作量,其麻烦的主要来源也在各语言桥接上。目前桥接C语言还是比较方便的,其他语言的接入笔者并没有深入研究。但随着 Swift 的发展,如果将来可以普及,那么会有更多厂商推出 Swift SDK,这些问题也就可以解决了。 ### 基于BlueSocket的Socket服务端开发体验 很多服务是基于 Socket 的,那么 Swift 的 Socket 服务端开发体验又如何呢?这里笔者推荐使用 [BlueSocket](https://github.com/IBM-Swift/BlueSocket)。这是IBM推出的一款在 iOS、macOS 和 Linux 上通用的 Socket 库,在三个平台上通用,能在调试上带来很大的便利。 做 Socket 的应用层开发开发,开发者只要操作收、发缓冲区,即可实现数据的接收和发送。在 macOS 和 iOS 上,Socket 相关 API 存在于`Darwin`库中,而 Linux 上则是`Glibc`库中,是一组C语言接口,开发者也可以自己向内核申请 Socket 去完成业务逻辑。在这里BlueSocket 已经帮助开发者实现了 Socket 的内核调用了。 既然是调用系统库的C语言接口,那么调用逻辑肯定是跟其他语言是一样的,这一点可以让熟悉 Socket 开发的开发者直接上手。现在来看一个简单的 TCP Socket 服务端的创建过程: ```swift //声明服务器类 internal class SocketService { //停止标记 fileprivate var stopFlag = false //监听Socket fileprivate var listenSocket: Socket //记录客户端列表,以便退出时关闭连接 fileprivate var clients = Dictionary<Int32, Socket>() //开始运行服务端 internal func start(port: Int) throws { //创建一个Socket self.listenSocket = try Socket.create() //开始监听 try listenSocket.listen(on: port) //死循环监听客户端的连接 while !stopFlag { //当有客户端连接时添加到客户端列表 let client = try listenSocket.acceptClientConnection() self.add(client: client) } } //停止时关闭客户端所有连接 internal func stop() { stopFlag = true for socket in clients.values { socket.close() } listenSocket.close() } //添加客户端方法 fileprivate func add(client: Socket) { DispatchQueue.global().async { do { //登记客户端 self.clients[client.socketfd] = client //死循环读取数据 while !self.stopFlag { var tmpBuffer = Data.init() //读取接收缓冲区数据,如果没有数据,线程会等在这里 let readSize = try client.read(into: &tmpBuffer) //如果长度为0,表示连接断开 if readSize == 0 { break } else { //处理缓冲区数据 } } //注销客户端的登记 self.clients[client.socketfd] = nil } catch { //处理异常 } } } } ``` 从代码中可以看出,每当一个客户机申请连接时,GCD 都会获取一条线程来跑。理论上一个客户端独占一条线程,这和其他语言的开发是一样的,但不一样的是,Swift 使用的 GCD 和闭包在这里能极大提高代码可读性。`read`方法会将缓冲区数据写出来交给开发者处理。TCP 的数据是基于流的,包与包之间没有固定的分隔,所有包粘在一起。这里如果需要拆固定长度的包,Swift 有一种很好的方式。 代码中定期调用`read`方法,它会将缓冲区里**所有**的内容都写出来。这个内容的长度是不确定的,取决于发送方和网络状况,以及这段代码中处理数据部分的耗时。在实际开发中开发者会定义自己的数据包,可能是有意义的间隔符,也有可能是固定长度。笔者在这里举一个固定长度包的例子,利用 Swift 的数组操作方法来拆包: ```swift //定义数据包长度 let packetSize = 4096 //声明存放数据的缓冲区,如需限制大小,可自己修改代码 var buffer = Array<UInt8>() while !self.stopFlag { //临时缓冲区 var tmpBuffer = Data.init() let readSize = try client.read(into: &tmpBuffer) if readSize == 0 { break } else { //将临时缓冲区的数据追加到上面定义的缓冲区里 buffer += tmpBuffer.bytes //当数据足够长时 while buffer.count >= packetSize { //获取包长度的数据 let packetBytes = Array(buffer.dropLast(buffer.count - self.packetSize)) //删去取出的部分 buffer.removeFirst(packetSize) //处理数据 } } } ``` 以上代码能实现一个缓冲区队列来按顺序取所需要的数据包,代码简单且有效。但需要注意的是这里的`dropLast`和`removeFirst`都是时间复杂度`O(n)`的方法。但作为 Socket 缓冲区,这个数组并不会很大,因此这里对性能的影响很小。但如果服务端对性能极为敏感且缓冲区有可能会很大的时候,这里最好采用常规**翻转栈**的方法设计队列,这里不再涉及。 在设计模式上例如处理数据部分,可以使用`Delegate`来将数据包回调给委托方实现,这样一个通用的定长数据包的 SocketServer 工具类就这样简单地实现了。关于客户端的实现,同样极为简单,开发者可以参考上面给出的 BlueSocket 的链接,查看官方的 README 文档以及历程,笔者在这里不再贴出代码了。 #### 小结 使用 BlueSocket 能让开发者在很短的时间内创建自己的 Socket 服务器。无论是做即时通信,文件传输还是直播,Swift 都能轻松胜任。正如本文开头提到的,Swift 开发服务端时,GCD 等工具和极简的语法能给开发者带来极大便利。同时也是个正在快速发展的语言,将来会有更多这样优秀的工具库的诞生,让开发过程更加高效。 #### Swift服务端和iOS客户端同时进行的开发体验 使用 Swift 做服务端一个很大的优势就在于,可以和客户端共用一套模型源文件。在服务端传送一个对象到客户端是业务开发中经常遇到的需求,数据发送时开发者有很多方法,转成数据二进制流、XML字符串、JSON字符串、ProtoBuf等发送,当客户端收到时再转模型。如果开发者使用 Swift 开发,那么这套代码只需要写一次,且以后再修改需求加字段,也不会出现服务端改了客户端忘改了的现象了。 笔者在这里举一个简单的使用 JSON 字符串进行传输的模型,解析库使用的是 [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)。 ```swift //声明一个协议,可以被JSON序列化以及反序列化 public protocol JSONable { init?(json: JSON) func toJSON() -> JSON } ``` ```swift //声明对象,遵守JSON序列化协议 open class MyObject: JSONable { open var mName: String! public required init?(json: JSON) { guard let mName = json["mName"].string else { return nil } self.mName = mName } open func toJSON() -> JSON { var dict = Dictionary<String, Any>() dict["mName"] = mName return JSON.init(dict) } } ``` 建完模型以后,这个源文件在两端都是可以用的,服务端和客户端同时引用这个源文件。当需求发生变更,需要加入字段时,服务端开发者直接修改这个源文件,由于客户端引用的也是同一个文件,因此客户端那边也被修改了。如果服务端需要在对象里加入一个验证算法,例如根据当前时间计算出一个 Key 来验证,那么直接在这个源文件里写就可以了,客户端开发者根本不需要关心这个算法是什么,叫什么,放在哪,因为服务端的哥们已经全部完成了。当对象被序列化调用`toJSON`方法时,这个方法已经被服务端开发者重写了,因此新的参数已经被加入序列了。因此这些与客户端业务毫无关系的代码,甚至可以做到不需要客户端开发者的参与就可以完成。 另外关于跨平台的问题,与 Cocoa 无关的 Swift 代码,理论上是可以在三个平台中通用的。笔者在前段时间完成了包括文件流和内存流的操作库,这些代码在一行未改的情况下在三个平台都可以直接使用,因此极大方便了“现造的轮子”在项目中的快速推进。 综上,在和 iOS 和 macOS 客户端协作开发的情况下,Swift 有着天生的优势,为开发提供便利。 ## 尾声 ### 问题和不足 在 macOS 上正常工作的代码在 Linux 上并不总能正常工作。笔者在开发过程中也遇到过许多问题。主要的问题归纳起来可以总结为以下几点: - **未实现的 API**:一些较为常用的 API 未被实现,典型的有`String`的`init(contentsOf url: URL)`,`FileManager`的`default`属性等等。不过这个问题随着时间的推进,最终都将被解决。 - **API 执行结果不一致**:体现为 macOS 上工作良好的代码在 Linux 上罢工。例如从`Data`构造`String`时若遇到`\0`字符,在 macOS 上会忽略这个字符,而在 Linux 上字符串会直接截断。这些问题可能由于底层实现不同所导致的。 - **API Bug**:表现为 macOS 上工作良好的代码在 Linux 出现非代码逻辑错误的问题。例如同时在 N 条使用`DispatchQueue.global()`获得的线程里使用`Data`的`init(contentsOf url: URL)`时直接崩溃报错`falat error`的问题。 大多数情况下发生以上情况都能找到替代的 API 来实现。例如上面提到的`Data`的`init(contentsOf url: URL)`时报错的问题可以使用`URLSession`来解决,`DateFormatter`时区错乱的问题可以使用`TimeZone`的`init(secondsFromGMT: Int)`来手动设置时差秒数。但这些都是临时的解决方案。笔者认为只有当原生的 API 足够可靠时,才会吸引更多的开发者加入阵营。 ### 总结和体会 本文主要讲了如何使用 Swift 语言开发简单的 HTTP 和 Socket 服务器,以及如何连接 MySQL 数据库和 HDFS,并最终部署到 Linux 服务器上。所用到的框架为`Perfect`和`BlueSocket`,以及和`iOS`客户端协作开发时的体验。 使用 Swift 开发服务端,开发时的体验是极好的。干净的文件结构,没有各项杂乱的环境配置,因此工程也极少出问题。在 Xcode 中开发时由于也是笔者比较熟悉的环境,可以使用 GCD 等熟悉的工具,因此开发过程是非常舒服的。最终部署时也非常简单,运行情况也比较好。Swift 诞生时间虽然不长,但是网络上资料已经比较多了,不再是以前的满眼 HelloWorld 了,Perfect 官方更是提供了简体中文的文档,因此现在想要深入学习 Swift 的话时机是非常合适的。对于目前存在的问题,相信随着时间的推移,会逐步得到解决。 Swift 虽好,但完善之路任重而道远。 ## 相关链接 - [PerfectlySoft 官方网站](http://perfect.org) - [PerfectlySoft GitHub](https://github.com/PerfectlySoft) - [Perfect 简体中文官方文档](http://perfect.org/docs/index_zh_CN.html) - [IBM BlueSocket GitHub](https://github.com/IBM-Swift/BlueSocket) - [笔者博客中的 Perfect TAG](http://enumsblog.com/list?tag=Perfect)
评论:2条
1楼:2019-01-17 14:19:12
苏-ios:
围观大神。。
2楼:2019-05-18 15:32:58
糖炒栗子:
围观大神