在服务端写 Swift 是一种怎样的体验

在服务端写 Swift 是一种怎样的体验_第1张图片

作者丨郑宇琦,@提拉拉拉就是技术宅,

现任职于LinkedIn,曾就职于百度 

来源丨个人博客

https://mp.weixin.qq.com/s/z1Vy7Mdf-BfKvrIRqtB_Vg

在服务端写 Swift 是一种怎样的体验_第2张图片

不得不说,在生产中使用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 所造成的麻烦,我们可以用条件编译来很方便地解决这个问题:

#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。文章中测试了以上四种框架以及Node.js在性能上的表现,结果汇总如下:

在服务端写 Swift 是一种怎样的体验_第3张图片

在测试结果中,Perfect 以非常优秀的成绩胜出,因此笔者最终也选择了Perfect。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 ,包含了简单的 HTTP 服务器创建的过程。README 中的过程是基于 SPM 编译管理的,但在 Mac 下,我们有更好的选择 —— Xcode。

克隆官方的例子后使用build命令自动下载依赖库:

$ swift build

之后这个命令生成 Xcode 工程:

$ swift package generate-xcodeproj

接着我们就可以像开发 macOS 应用那样开发服务端了。官方在文档中提醒我们不要直接编辑这个 xcodeproj 文件。因为有时我们需要修改 Package.swift 来下载更多依赖库,这时候这个 xcodeproj 会被重新生成,之前所做的所有内容都会被覆盖。本书的 Sample 只做演示用,所以并没有这样做。读者在自己正式的项目中要注意这一点,避免以后发生麻烦。

在现在版本的 README 中,提供了一种非常炫酷的创建服务的方式,直接构造一个多维数组,填写相关信息,之后就可以直接跑起一组服务:

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 程序:

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中调用:

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"

现在我们就可以在浏览器看到结果了:

在服务端写 Swift 是一种怎样的体验_第4张图片

如果在 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,或自行搜索配置。

现在我们的服务已经在 Mac 上跑起来了,那么如何部署到 Linux 服务器中呢?例子中的 HelloWorld 直接拷贝到 Linux 服务器中 build 是直接可以通过的,这个例子太过初级,这里讲个稍微复杂一点的帮助读者理解。

在 Linux 中我们使用 SPM 管理项目。例如我们的项目包含了 Swift 主函数代码和一个纯C语言的 Framework。在 Xcode 中开发时,可以按照自己的习惯建立两个工程,主工程链接库工程生成的库,这个过程相信大家都很熟悉了。但为了 Linux 工程的管理,源文件需要放在两个独立的文件夹中。具体目录结构如下图:

在服务端写 Swift 是一种怎样的体验_第5张图片

SPM 会按照Package.swift文件来为我们生成Target,处理依赖关系。SwiftCode 模块依赖了 CCode,因此此时要将该文件改成这样:

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目录,执行代码运行:

$ ./.build/debug/SwiftCode

这样我们的服务就已经在 Linux 上跑起来了。嗯,如果代码确认没问题,需要发行 Release 版本呢?

$ swift build -c release

之后会生成./.build/release文件夹,其中包含了 Release 版的库和可执行文件。

清理工程可以使用下面这个命令:

swift build --clean
//当然,也可以粗暴一点,直接删除.build目录,效果是一样的
rm -rf .build

如果需要连带依赖库一起清理,使用下面这个命令:

swift build --clean=dist
//同样的,可以粗暴地直接手动删除目录
rm -rf .build
rm -rf Packages

链接其他第三方库如 libhdfs.so 并编译 release 版本:

$ swift build -Xlinker -lhdfs -c release

静态编译:

$ swift build -static-stdlib

其他编译器功能请参阅:

$ swift --help

小结

通过以上这个例子,我们创建了一个简单的 HTTP 服务器,并且部署到了 Linux 服务器上。由于框架是开源的,甚至可以直接修改源代码来完成例如埋点等功能。在接下来的部分,笔者将会添加一些常规的服务来进一步让大家体会 Swift 在服务端的开发体验。

基于Perfect的常规服务接入体验

一个完整的服务器只包含 HTTP 服务和文件系统功能肯定是不够的,必须有一种方法让开发者接入其他第三方服务。第三方服务的 SDK 有 Swift 版的吗?嗯,很少很少。但多数厂商都会提供C语言接口,这就可以作为开发者们的突破口,利用C语言接口接入服务。

笔者会举两个简单的例子来介绍第三方服务接入的过程:MySQL和HDFS。

MySQL接入

  
对于厂商没有提供接口的服务,Perfect 官方“造了很多轮子”来帮助开发者接入。MySQL 的接入可以直接选用官方提供的Perfect-MySQL。

编辑 Package.swift ,在依赖库中加入对Perfect-MySQL的依赖:

.Package(url:"https://github.com/PerfectlySoft/Perfect-MySQL.git", majorVersion: 2, minor: 0)

下载依赖后就可以使用了,同样来看一个简单的例子:

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.Results对象被ARC回收时释放 C++ 资源。这种方法相当于维护了一个服务端,在客户端操作类似于操作一个状态机,造成在处理查询结果上语法还是充满了 C Style ,例如需要取第N条数据时必须先调用以下方法将指针移动到目标位置然后读取,读取时也只能用类似生成器的方法读:

//移动到第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 的目录结构,假设结构如下图所示:

在服务端写 Swift 是一种怎样的体验_第6张图片

• CCode.h:Module 暴露的接口。

#include "../CWrap.h"

• HDFSType.h:针对库中的数据结构的另一层封装,在原有数据结构前加入cw_前缀定义新结构。

• CWrap.h:HDFS 接口头文件。

#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 接口封装的具体实现过程。

#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 端被调用:

let fs = cw_func_connect("目标NameNode", cw_tPort(目标端口))

事实上笔者在 C++ 层不仅是简单封装了接口,更是封装了功能。在平时的开发过程中经常也会在厂商 SDK 和自己的业务代码中间做封装,笔者将其移到了 C++ 中实现,同时也可以暴露更少的数据结构和细节。当然这一层也可以放到 Swift 中去完成,但封装数据结构的工作量会更大。

最后使用如下命令链接 HDFS 库并编译:

$ swift build -Xlinker -lhdfs

实际应用中,笔者的项目是接入了 HDFS 的。在 Swift 端调用时,因为是原生调用,因此性能和稳定性都不错。但关于 HDFS 的开发,其实还存在很多坑。在 Mac 上开发时会遇到例如CLASSPATH环境变量未定义的问题,无法Debug执行的问题等等。读者若有兴趣可以自行研究。

小结

其实 Swift 端在调用原生服务时并没有给开发者带来太多的额外工作量,其麻烦的主要来源也在各语言桥接上。目前桥接C语言还是比较方便的,其他语言的接入笔者并没有深入研究。但随着 Swift 的发展,如果将来可以普及,那么会有更多厂商推出 Swift SDK,这些问题也就可以解决了。

基于BlueSocket的Socket服务端开发体验

很多服务是基于 Socket 的,那么 Swift 的 Socket 服务端开发体验又如何呢?这里笔者推荐使用 BlueSocket。这是IBM推出的一款在 iOS、macOS 和 Linux 上通用的 Socket 库,在三个平台上通用,能在调试上带来很大的便利。

做 Socket 的应用层开发开发,开发者只要操作收、发缓冲区,即可实现数据的接收和发送。在 macOS 和 iOS 上,Socket 相关 API 存在于Darwin库中,而 Linux 上则是Glibc库中,是一组C语言接口,开发者也可以自己向内核申请 Socket 去完成业务逻辑。在这里BlueSocket 已经帮助开发者实现了 Socket 的内核调用了。

既然是调用系统库的C语言接口,那么调用逻辑肯定是跟其他语言是一样的,这一点可以让熟悉 Socket 开发的开发者直接上手。现在来看一个简单的 TCP Socket 服务端的创建过程:

//声明服务器类
internal class SocketService {
    //停止标记
    fileprivate var stopFlag = false
    //监听Socket
    fileprivate var listenSocket: Socket
    //记录客户端列表,以便退出时关闭连接
    fileprivate var clients = Dictionary()

    //开始运行服务端
    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 的数组操作方法来拆包:

//定义数据包长度
let packetSize = 4096
//声明存放数据的缓冲区,如需限制大小,可自己修改代码
var buffer = Array()
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。

//声明一个协议,可以被JSON序列化以及反序列化
public protocol JSONable {
    init?(json: JSON)
    func toJSON() -> JSON
}

//声明对象,遵守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()
        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 虽好,但完善之路任重而道远。

相关链接

[1]PerfectlySoft 官方网站 https://www.perfect.org/ 
[2]PerfectlySoft GitHub https://github.com/PerfectlySoft 
[3]Perfect 简体中文官方文档 https://www.perfect.org/docs/index_zh_CN.html 
[4]IBM BlueSocket GitHub https://github.com/IBM-Swift/BlueSocket

在服务端写 Swift 是一种怎样的体验_第7张图片

你可能感兴趣的:(在服务端写 Swift 是一种怎样的体验)