专栏名称: CSDN
CSDN精彩内容每日推荐。我们关注IT产品研发背后的那些人、技术和故事。
目录
相关文章推荐
新浪科技  ·  转发微博-20250207185308 ·  12 小时前  
新浪科技  ·  #OpenAI思维链并非原生#【#GPTo3 ... ·  19 小时前  
新浪科技  ·  【#光线传媒源于哪吒2营收约至10.10亿# ... ·  2 天前  
新浪科技  ·  【#小米SU7Ultra实车到店##小米SU ... ·  2 天前  
新浪科技  ·  【#雷军给员工发开年红包#,#小米两款Ult ... ·  2 天前  
51好读  ›  专栏  ›  CSDN

使用 Server-Side Swift 开发 RESTful API

CSDN  · 公众号  · 科技媒体  · 2017-02-14 11:25

正文

作者简介: 何轶琛,去哪儿网 iOS 开发工程师,四年多 iOS 应用开发经验,在去哪儿网实践了 Realm、Cocoapods、React Native 等一些好用、有用的技术,目前主要精力在 Swift 上。
责编: 唐小引 ,技术之路,共同进步。欢迎技术投稿、给文章纠错,请发送邮件至 [email protected]
声明: 本文为 《程序员》 原创文章,未经允许请勿转载,更多精彩文章请订阅 2017 年《程序员》

【导语】Swift 自发布以来就备受众多 Apple 开发者关注,但由于 API 尚不稳定,系统没有内置 Framework 导致 App 包增大等问题,使得线上主力使用的公司还很少,不少客户端开发者都还没有机会使用 Swift 进行开发。等到 2015 年 12 月 Swift 开源并正式支持 Linux 系统,广大 Apple 开发者迎来了更广泛的开发场景,可以用它来进行服务端开发。不到一年时间各种 Server-Side Swift Web Framework 相继问世,其中以 Kitura、Perfect、Vapor、Zewo 最为成熟。

文章正式开始前,我们先对当前几款主流框架进行了解与对比。

Kitura 是 IBM 推出的框架,使用 IBM Cloud Tools for Swift 管理组件依赖,并支持部署代码到 IBM 的云服务 Bluemix。另外还有一个在线 Swift 编码网站,可以看作是线上 GUI 版本的 Swift REPL,开发者可以直接在 Web 上编写代码并查看输出。Kitura 整个产品从代码编写到部署全部包揽,提供了完整的生态环境。

Perfect 拥有 GitHub 上最多的 Star,各种功能组件和数据库连接工具也最为齐全。近期推出的 Perfect Assistant 是运行在 macOS 上的管理工具,同样支持组件依赖管理,自动化代码部署(支持 AWS、Azure),并通过调用本地 Docker 的方式实现了在 macOS 上编译 Linux 产物的功能。

Vapor 以其友好的文档和 Pure Swift 代码实现著称,其 HTTP Parser 是使用 Swift 编写实现,而不像 Kitura 和 Perfect 是用 CHTTPParser 封装,这对最终的服务性能有很大影响。Vapor 还开发了命令行工具对 SPM 进行封装,好处是开发体验更好,但提高了学习成本。另外 Vapor 比较早就做了 ORM 工具 Fluent,整体感觉十分技术范、小清新。

Zewo 是一系列开源组件的集合平台,特点是使用 libmill 实现了类似 Go 的协程功能,模块化的设计也不同于其他的框架。

这些框架在各具特色的前提下都有高性能、易扩展等优点。正好部门内部有一个信息管理平台项目,需求很简单,只要有基本的增删改查就行,于是不用麻烦后端同学排期,可以自己来开发,也算是提前实践 Swift,积累经验。

正式开发是在 2016 年 8 月,彼时 Swift 3 尚未发布,Beta 版本的 Toolchain 每周都在更新,框架也在积极跟进发布支持最新版本的 Toolchain。技术选型期间我先后尝试了 Kitura、Vapor 和 Perfect。Kitura 的整套产品耦合太紧密,用起来比较重,对于轻量级小项目并不合适。Vapor 一开始用起来很愉快,但写到数据库连接工具时一直无法连接成功,再加上当时还在 Beta 版本,问题不少也被弃用。最后,使用 Perfect 完成了项目研发。接下来,本文将着重介绍如何使用 Perfect 完成一套 RESTful API 的开发,希望能够对大家进行 Swift Server 端开发有所裨益。

在编写代码前,要先了解目前开源的 Swift 项目包括了 Swift、SPM 以及配套的编译调试工具,在核心库方面有 libdispatch、Foundation、XCTest 这三个项目。在客户端开发中,Foundation 是最常用的工具库,它提供了一系列国际化、系统无关的 API。服务端项目增加 Foundation 支持可以统一开发体验和复用客户端代码,尤其是和系统无关的 API 可以大大增加可移植性,本属于 Swift 3.0 的组成部分但至今并没有开发完成,原因在于 Foundation 中用到了一些 Objective-C Runtime 的代码,而这部分代码并不在开源范围之内。于是在开发中需要用到 Foundation 库时就会碰到不少问题。

环境配置

macOS 上依然是 Xcode 搞定一切事情,Linux 上目前只支持两个版本的 Ubuntu,所以推荐使用 Docker 搭建 Swift 编译和执行环境,这样可以支持所有 Linux 系统,也方便在 Swift 快速迭代时及时更新 Toolchain。代码都用 SPM 管理,在实际使用中还是有些问题,比如不支持 MySQL 5.7,创建工程文件配置时漏掉了编译设置,寻找公共代码库路径在不同操作系统上没有适配等。

开发中使用了两个第三方库 SwiftLog 和 MySQL-Swift。SwiftLog 支持自定义日志级别和增量写入的日志文件,并使用了喵神的 Rainbow 在 Linux 环境下输出彩色日志。MySQL-Swift 支持 MySQL 连接池复用,可以提高访问数据库的性能。

部署环境使用 CentOS 作为宿主机,开启两个 Docker 实例分别运行 Perfect 和 MySQL,两个 Docker 实例通过 link 方式实现通信。使用 link 参数运行 Docker 实例,主 Docker 的 hosts 文件会增加从 Docker 的 host 信息,从而达到通过 Docker 别名进行通信的效果。初始化 MySQL 容器时可以将数据库文件路径设置到虚拟卷中,再使用 crontab 执行定期任务运行 AutoMySQLBackup 来备份数据。

编码

先初始化工程,使用 swift package init 新建工程,此时会生成 Package.swift 配置文件和源代码、测试代码目录。在 Package.swift 中添加 Perfect、SwiftLog、MySQL-Swift 后执行 Swift Build 即可拉取所有依赖代码。此时的目录并不包含 Xcode 工程文件,需要再执行命令 swift package generate-xcodeproj 生成,工程中各个依赖库的配置都通过自身的 Package.swift 配置文件读取。
SPM 不仅是去中心化的包管理器,它还可以编译出可执行文件,我们甚至能够直接在服务端编写代码并编译运行。如果说在使用 Objective-C 开发时用 Xcodebuild 开发自定义打包工作流,那么开发 Server-Side Swift 就需要使用 SPM 在服务端实现编译、打包等流程。虽然在服务端目前还有些兼容问题,但 SPM 作为 Swift 的组成部分,一直在快速改进与提高。

然后,添加 API 路由,我们先添加路由配置,后面再将所有配置一起设置到 Server 对象上。Perfect 的路由 API 设计参考了 Express,只需要设置 HTTP 请求类型、路由地址和对应的处理函数即可,支持使用通配符与参数,用起来还算简洁。我们可以先设置基础 API 路径,再通过 Routes().add 方法添加自定义路由,这样所有的路由都被添加到指定 API 路径下。最后将路由对象输出一下,这样开启服务时会将所有注册的路由输出到日志。

func makeURLRoutes() -> Routes {    var routes = Routes()    var apiRoutes = Routes(baseUri: "/api")    var api = Routes()
    api.add(method: .get, uri: "/staff/{id}", handler: fetchStaffById)
    apiRoutes.add(routes: api)
    routes.add(routes: apiRoutes)
    SLogWarning("\(routes.navigator.description)")    return routes
}

Swift 没有 define 关键字,但可以通过 typealias 自定义类型名称,比如 Perfect 里的路由处理回调函数就被定义为 public typealias RequestHandler = (HTTPRequest, HTTPResponse) -> ()。从定义可以看到,响应函数有两个参数,HTTPRequest 包含了客户端请求的所有信息,包括 HTTP headers 和 content data。HTTPResponse 包括 HTTP headers、body 以及 HTTP Status,这些属性可以在函数中赋值并返回给客户端。

接着来实现路由函数。先将请求参数拿到,将参数进行错误处理后调用数据库工具方法获取信息,并根据获取数据的成败做对应的处理,最后返回 JSON 格式的结果。

在路由处理函数最后都需要调用 response.completed()方法通知 Server 回调处理完成,由于函数有几个结束点,在参数错误或者获取数据的时候都可能抛出异常并提前结束函数,所以需要在好几个地方执行 response.completed()方法。Swift 和 Golang 一样拥有 defer 关键字,我们可以在函数中使用它来完成资源回收或这种需要多处调用的代码。

defer {
    response.completed()
}

进行数据库请求和生成 JSON 返回值的操作都有可能抛出异常,这样在 do-catch 中会抛出两种类型的异常,我们可以使用 switch-as 语法针对不同类型的异常进行处理。

do {    let staffData = try StaffDataBaseUtil.sharedInstance.searchStaffByID(idString)    try response.setBody(json: jsonBody(errorCode: returnCode, data: ["staff": staffData]))
} catch let error {  switch error {    case let error as QueryError:        // 数据库请求出错
    default:        // 服务端出错
  }
}

Perfect 并没有使用 SwiftyJSON 之类的第三方库,而是自己实现了很好用的 JSON 扩展,对常用的数据类型增加了 JSON 序列化和反序列化方法,Swift 的 Extension 在这里得到了充分的使用。

extension Dictionary: JSONConvertible {    /// Convert a Dictionary into JSON text.
    public func jsonEncodedString() throws -> String {        var s = "{"
        var first = true
        for (k, v) in self {
            guard let strKey = k as? String else {                throw JSONConversionError.invalidKey(k)
            }            if !first {
                s.append(",")
            } else {
                first = false
            }
            s.append(try strKey.jsonEncodedString())
            s.append(":")
            s.append(try jsonEncodedStringWorkAround(v))
        }
        s.append("}")        return s
    }
}

数据获取需要进行数据库请求,我们使用 MySQL-Swift 作为数据库连接工具,为的是使用连接池复用连接,但也给自己挖了个坑。在 Mac 上开发时都没有问题,但在服务器端就编译失败,后来发现有一部分的代码还不支持 Linux,原来是时区的问题。在连接数据库时需要传入一个 config,包括数据库地址、端口、密码等,其中就有时区配置。在从数据库获取数据时,如果字段中有日期类型,就会将获取的绝对时间转换为对应时区的时间字符串。在日期类型数据的处理部分用到了 NSTimezone,但这一类型在 Linux 上没有实现,于是编译失败。修复的方式很简单,使用 CFTimezone 传递信息就好,但 CFTimezone 返回的类型是 CFString,于是又要针对 macOS 和 Linux 实现不同的 CFString 转换到 String 的代码。如果 Swift 有一套跨平台的 Foundation 就不会出现这个问题了。

MySQL-Swift 底层使用的是 CMySQL 库进行连接。根据 options 初始化数据库连接,将连接保存到连接池中,这样后续的数据库操作不需要再次重新连接数据库。再对每个连接对象添加是否正在使用的标记,如果当前连接池中的所有连接都在使用当中,则再次新增一个数据库连接对象进行操作。可以手动设置连接池的最大连接数,当连接池中的连接数到达最大连接数时,后续的请求将会抛出获取数据库连接失败异常。
MySQL-Swift 将查询方法封装的十分优雅,工具类在初始化时候根据提前设置好的 config 生成连接池,再调用 pool.execute 方法,并传入查询执行的闭包就行。这里用到了高阶函数和范型,语法简洁又安全。使用的时候不需要关心数据库连接对象的创建和状态,只需要直接使用闭包里传进来的 Connection 连接对象进行查询即可。

private init() {
    pool = ConnectionPool(options: DBConfigOption)
}let result: [Staff] = try pool.execute { conn in
    try conn.query(querySQL)
}

对于同名不同返回值的函数,Swift 会针对代码中返回值的类型推断进行分析,确定最终运行时调用哪一个函数。上面 conn.query 函数就会根据不同的返回值执行不同的函数,所以在编写上面代码的时候一定要显示的声明返回的是 Staff 数组,不然函数返回的结果就会不同。另外在 query 的实现中可以看到,最后的不同返回结果是从同一个函数返回的 tuple 中拿到的,这样的代码阅读起来很有效率。

public func query"">(_ query: String, _ args: [QueryParameter] = []) throws -> ([T], QueryStatus) {    return try self.query(query: queryString)
}public func query"">(_ query: String, _ args: [QueryParameter] = []) throws -> [T] {    let (rows, _) = try self.query(query, args) as ([T], QueryStatus)    return rows
}public func query(_ query: String, _ args: [QueryParameter] = []) throws -> QueryStatus {    let (_, status) = try self.query(query, args) as ([EmptyRowResult], QueryStatus)    return status
}

拿到数据后可以将结果转换为自定义的数据类型。这里会将数据库查询结果解码为定义了默认值的结构体,并提供序列化方法给路由处理函数。

static func decodeRow(r: QueryRowResult) throws -> Staff {    return try Staff(
        id: r 0,
        name: r "name",
        department: r "department",
        timestamp: r "timestamp"
    )
}

在开发完成后的测试中发现返回数据的时间戳总是差几个小时,但是直接查看数据库里的数据,时间戳又是正确的,于是一步步从获取数据到返回 response 调试看看哪里出的问题,最后发现是 Swift 时区没有自动设置为和系统相同,而一直是 UTC 时间,在生成时间戳文案的时候就出错了。

所以在使用 Dateformatter 的时候需要手动设置时区,这也是 Foundation 的一个坑。

func resultDic() -> Dictionarystring, any=""> {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
    dateFormatter.timeZone = TimeZone(identifier: "Asia/Shanghai")
    let dateString = dateFormatter.string(from: timestamp.date())    return Dictionarystring, any="">.init(dictionaryLiteral:
        ("id", id),
        ("name", name),
        ("department", department),
        ("timestamp", dateString)
    )
}string,>string,>

在拿到数据后,可以将 HTTP Response 包装一下,在每个返回结果中都包括错误码,数据和时间戳这三个字段,并增加错误码对应的错误提示。这样不用在每个路由处理函数的最后都手动写一次。







请到「今天看啥」查看全文