Perfect & swift-server/http

1,497 阅读6分钟

原文高清传送门:ichuan.me/4-test

简评

如果事先没有了解过 swift,可以先到 swift.org 管窥一二,server-side project 作为 swift 的应用领域之一,也是其未来重点发力的地方。

本文主要探讨的是 swift 服务端框架 Perfect 和国庆前夕 swift-server 小组发布的 http 服务端基础框架 api,此两者的架构分析和异同点比较。

首先两者的定位是不一样的,Perfect 是一套成熟的服务端框架,其自给自足的周边已经相当完善了,开发者可基于它快速搭建一套 web 服务,可以直接类比 Node.js 服务端框架 koa / express,它的目标用户是 web 服务的开发者。swift-server/http 的定位则是实现一组基础的 HTTP 服务 API,上层 web 应用框架可基于它来搭建,可以说它的目标用户是 web 应用框架,比如 PerfectX :)

以上两张图分别代表了 Perfectswift-server/http 的层次或者调用架构,当然这是以我个人的视角(经验)来分析的,或者会有理解出问题的地方,还请各位看官雅正。我先聊一下对这两者的认识吧。

Perfect

Perfect 似乎天生就带有中式基因,API 文档标配中英版本,这对于国内的开发者而言,多少拉近了些距离。而且 PerfectlySoft 这个组织有着不错的运营和推广,团队华人成员RockfordWei,就是其中一名核心开发。

Perfect 目前发展的挺快,官网一直在更新其周边。
其基础库和衍生的扩展库(Perfect 全家桶)发展迅猛,(静态)路由、模板、过滤器、安全、认证、数据库、websocket、IPV6/http2/https、TensorFlow 支持等等。基础库实现都是跨平台的,历史原因也保留了自己的多线程、队列实现(目前 swift-corelibs-libdispatch 已经实现了跨平台)。

在上图中,其基础架构和调用流程是比较简单的:

  1. server 完成初始化,加载路由;
  2. 可选的设置 request/response 过滤器;
  3. server.start 中完成 socket bind/listen/accept;
  4. 每个收到的 c_socket,启用全局队列中的线程来 handlerConnection 处理;
  5. (仅讨论 HTTP1.1) handlerConnection 分别调用 readRequest/runRequest,此两者是核心;
  6. readRequest 中从 socket 读取数据,成功则进入下一步,失败则会启用 NetEvent 开始 I/O 多路复用 epoll/kevent,直到数据可读取,再次读取成功则进入下一步;
  7. readRequest 从 socket 数据读取成功,会触发 http_parser 设置好的各种回调,这些回调会把 request 的完整信息 parse 出来暂存下来。至此,读取阶段结束;
  8. runRequest 主要处理 request 过滤器(如果有)和路由;
  9. 在路由中最后会调用 response.complete,触发 completeCallback,随即调用 flush 写出数据到 socket,分别为 pushHeaderpushBody,这两者又会调用 response 的过滤(如果有),最后真正写出。与读取同样,写出若失败则会给到 NetEvent epoll/kevent,直到可写再次写出。

以上即为 Perfect 的请求处理流程,结构上是比较清晰的(相对于 swift-server/http)。

这里面有不少细节处理的比较精细,比如 NetEvent 仅在需要时才创建等。

吹毛求疵的说,第 4 步有点莫名的接连两次启用了异步线程来处理 c_socket,私以为没有必要。第二个就是过滤器的设定有点疑问,.high/.medium/.low 既然是通过级别来划组,高级别如果处理了,同级别的跳过,低级别还能处理的设定就有点奇怪了。

总的来说,Perfect 使用上比较方便,特别是在 macOS 环境上,配合 IDE 调试也很方便。一般的服务,我都会尽力尝试用它来替代 Nodejs。

swift-server/http

应该是国庆假期的时候,偶然发现了这个库。听说是 server-group 官方发布,于是见猎心喜,马上就对着源码对比看了看。在评价之前,对着上图,我们先走一遍流程:

  1. server.init
  2. server.start,socket bind/listen,基本上这个时候主线程的逻辑就到这里了,接着 Runloop.run 到天荒地老了;
  3. 异步线程跑一段 loop:accept,round-robin 选择该 socket 读写使用的 queue,构造 parser/listener。

    parser 是一个稍显重量的类,主要负责数据(request)的分析、解构,数据分析的同时驱动后续流程(handler 的处理),listener 则主要负责 socket 的基本管理,比如 close/read/write;

  4. 接着在 acceptQueue 中执行 listener.process 逻辑。这里 acceptQueue 的最大并发量表示了最多能同时处理多少客户端的链接请求;
  5. process 通过 DispatchSourceRead 设定 event_callback,在可读事件回调中执行 parser.readStream这里可以对比 Perfect 流程的第 6 步
  6. readStream 做的事情和 Perfect 中 readRequest 基本一致,但是它除了解构组装 request 数据之外,同时负责驱动 handler 调用。这是这个框架最特立独行的地方,至少从这里开始就和 Perfect 走向截然不同的道路了;
  7. 在 readStream 中 bodyReceived / messageCompletion 阶段,会调用 handler 逻辑,一般我们会调用 response.writeHeader / response.writeBody,这里会直接写出到套接字。流程结束。

整个流程相比 Perfect 显得不那么有结构性,从第 5 步开始笔锋斗转,readStream 操作直接驱动后续所有流程,这中间少了很多可以操作的空间。

这里有几个值得注意的点:

  1. 显然,swift-server/http 能力非常单一,流程衔接紧张,上层框架所有能介入的地方,只有一处,即一开始的 handler,这个 handler 必然要包含所有的处理,包括过滤器,路由等等。似乎它只处理了 request 数据读取和分析解构;
  2. 相较 Perfect 的 I/O 事件机制 epoll/kevent,swift-server/http 使用 DispatchSource 来获取可读可写事件,写法上更显纯 swift 风格一些,虽然底层上可能也是使用 epoll/kevent 机制。但是有点奇怪的地方在于,它在数据读取阶段使用了 DispatchSourceRead,但是写出阶段却是直写,未通过 DispatchSourceWrite,这应该是一个明显的 bug,因为 socket 在 send 时会因为窗口等原因,发送是有可能失败的(需等待缓冲区),这里会直接 close 掉导致数据写出失败。

总评

swift-server/http 作为一个刚刚发起的项目,后续还会有很多更新。考虑到其背景特殊,未来基于它而实现的 web 应用框架或许也不会少。
私以为,类似 Perfect 这样已经相当成熟的 web 框架应该不大会转投到 swift-server/http 阵营,毕竟是和 Perfect-Net/Perfect-HTTP/Perfect-HTTPServer 等核心模块直接冲突的,而且当下看来,Perfect 在基础功能和扩展功能的组合结构性上会更合理一点。

当然了,我也可能被打脸,万一呢。哈哈。

题外话

swift-server/which_is_the_fastest 这个项目中测试的得分可能稍显牵强,因为 swift-server/http 本身的功能就比较少,没有路由、过滤等消耗,得分相比其他 web 应用框架自然会较高。

PS: 我怎么感觉自己是 swift-server/http 黑,逃)