Build Codecov Platforms Swift 6.0

简介

FlyingFox 是一个轻量级的 HTTP 服务器,使用 Swift 并发 构建。该服务器使用非阻塞 BSD 套接字,并在并发子 Task 中处理每个连接。当套接字被阻塞且没有数据时,任务会使用共享的 AsyncSocketPool 挂起。

安装

可以使用 Swift Package Manager 安装 FlyingFox。

注意: FlyingFox 需要 Xcode 15+ 上的 Swift 5.9+。它运行在 iOS 13+、tvOS 13+、watchOS 8+、macOS 10.15+ 和 Linux 上。Android 和 Windows 10 支持为实验性功能。

要使用 Swift Package Manager 安装,请将此添加到您的 Package.swift 文件中的 dependencies: 部分

.package(url: "https://github.com/swhitty/FlyingFox.git", .upToNextMajor(from: "0.21.0"))

用法

通过提供端口号来启动服务器

import FlyingFox

let server = HTTPServer(port: 80)
try await server.run()

服务器在当前任务中运行。要停止服务器,请取消任务,立即终止所有连接

let task = Task { try await server.run() }
task.cancel()

在所有现有请求完成后优雅地关闭服务器,否则在超时后强制关闭

await server.stop(timeout: 3)

等待直到服务器正在监听并准备好接受连接

try await server.waitUntilListening()

检索当前监听地址

await server.listeningAddress

注意:当应用程序在后台挂起时,iOS 将挂断监听套接字。一旦应用程序返回前台,HTTPServer.run() 会检测到这一点,并抛出 SocketError.disconnected 错误。然后必须再次启动服务器。

处理器 (Handlers)

可以通过实现 HTTPHandler 向服务器添加处理器 (Handlers)

protocol HTTPHandler {
  func handleRequest(_ request: HTTPRequest) async throws -> HTTPResponse
}

可以将路由 (Routes) 添加到服务器,将请求委托给处理器 (handler)

await server.appendRoute("/hello", to: handler)

它们也可以添加到闭包中

await server.appendRoute("/hello") { request in
  try await Task.sleep(nanoseconds: 1_000_000_000)
  return HTTPResponse(statusCode: .ok)
}

传入的请求被路由到第一个匹配路由的处理器 (handler)。

如果处理器 (Handlers) 在检查请求后无法处理,则可以抛出 HTTPUnhandledError。然后将使用下一个匹配的路由。

不匹配任何已处理路由的请求将收到 HTTP 404 错误。

FileHTTPHandler (文件 HTTP 处理器)

可以使用 FileHTTPHandler 将请求路由到静态文件

await server.appendRoute("GET /mock", to: .file(named: "mock.json"))

如果文件不存在,FileHTTPHandler 将返回 HTTP 404 错误。

DirectoryHTTPHandler (目录 HTTP 处理器)

可以使用 DirectoryHTTPHandler 将请求路由到目录中的静态文件

await server.appendRoute("GET /mock/*", to: .directory(subPath: "Stubs", serverPath: "mock"))
// GET /mock/fish/index.html  ---->  Stubs/fish/index.html

如果文件不存在,DirectoryHTTPHandler 将返回 HTTP 404 错误。

ProxyHTTPHandler (代理 HTTP 处理器)

请求可以通过基本 URL 进行代理

await server.appendRoute("GET *", to: .proxy(via: "https://pie.dev"))
// GET /get?fish=chips  ---->  GET https://pie.dev/get?fish=chips

RedirectHTTPHandler (重定向 HTTP 处理器)

请求可以重定向到 URL

await server.appendRoute("GET /fish/*", to: .redirect(to: "https://pie.dev/get"))
// GET /fish/chips  --->  HTTP 301
//                        Location: https://pie.dev/get

WebSocketHTTPHandler (WebSocket HTTP 处理器)

可以通过提供 WSMessageHandler 将请求路由到 WebSocket,其中交换一对 AsyncStream<WSMessage>

await server.appendRoute("GET /socket", to: .webSocket(EchoWSMessageHandler()))

protocol WSMessageHandler {
  func makeMessages(for client: AsyncStream<WSMessage>) async throws -> AsyncStream<WSMessage>
}

enum WSMessage {
  case text(String)
  case data(Data)
}

原始 WebSocket 帧也可以提供

RoutedHTTPHandler (路由 HTTP 处理器)

可以使用 RoutedHTTPHandler 将多个处理器 (handlers) 与请求分组,并根据 HTTPRoute 进行匹配。

var routes = RoutedHTTPHandler()
routes.appendRoute("GET /fish/chips", to: .file(named: "chips.json"))
routes.appendRoute("GET /fish/mushy_peas", to: .file(named: "mushy_peas.json"))
await server.appendRoute(for: "GET /fish/*", to: routes)

当无法使用其任何注册的处理器 (handlers) 处理请求时,将抛出 HTTPUnhandledError

路由 (Routes)

HTTPRoute 旨在针对 HTTPRequest 进行模式匹配,允许通过其部分或全部属性识别请求。

let route = HTTPRoute("/hello/world")

route ~= HTTPRequest(method: .GET, path: "/hello/world") // true
route ~= HTTPRequest(method: .POST, path: "/hello/world") // true
route ~= HTTPRequest(method: .GET, path: "/hello/") // false

路由 (Routes) 是 ExpressibleByStringLiteral,允许将字面量自动转换为 HTTPRoute

let route: HTTPRoute = "/hello/world"

路由 (Routes) 可以包含要匹配的特定方法

let route = HTTPRoute("GET /hello/world")

route ~= HTTPRequest(method: .GET, path: "/hello/world") // true
route ~= HTTPRequest(method: .POST, path: "/hello/world") // false

它们也可以在路径中使用通配符

let route = HTTPRoute("GET /hello/*/world")

route ~= HTTPRequest(method: .GET, path: "/hello/fish/world") // true
route ~= HTTPRequest(method: .GET, path: "/hello/dog/world") // true
route ~= HTTPRequest(method: .GET, path: "/hello/fish/sea") // false

路由 (Routes) 可以包含参数,这些参数像通配符一样匹配,允许处理器 (handlers) 从请求中提取值。

let route = HTTPRoute("GET /hello/:beast/world")

let beast = request.routeParameters["beast"]

尾部通配符匹配所有尾部路径组件

let route = HTTPRoute("/hello/*")

route ~= HTTPRequest(method: .GET, path: "/hello/fish/world") // true
route ~= HTTPRequest(method: .GET, path: "/hello/dog/world") // true
route ~= HTTPRequest(method: .POST, path: "/hello/fish/deep/blue/sea") // true

可以匹配特定的查询项

let route = HTTPRoute("/hello?time=morning")

route ~= HTTPRequest(method: .GET, path: "/hello?time=morning") // true
route ~= HTTPRequest(method: .GET, path: "/hello?count=one&time=morning") // true
route ~= HTTPRequest(method: .GET, path: "/hello") // false
route ~= HTTPRequest(method: .GET, path: "/hello?time=afternoon") // false

查询项值可以包含通配符

let route = HTTPRoute("/hello?time=*")

route ~= HTTPRequest(method: .GET, path: "/hello?time=morning") // true
route ~= HTTPRequest(method: .GET, path: "/hello?time=afternoon") // true
route ~= HTTPRequest(method: .GET, path: "/hello") // false

可以匹配 HTTP 标头

let route = HTTPRoute("*", headers: [.contentType: "application/json"])

route ~= HTTPRequest(headers: [.contentType: "application/json"]) // true
route ~= HTTPRequest(headers: [.contentType: "application/xml"]) // false

标头值可以是通配符

let route = HTTPRoute("*", headers: [.authorization: "*"])

route ~= HTTPRequest(headers: [.authorization: "abc"]) // true
route ~= HTTPRequest(headers: [.authorization: "xyz"]) // true
route ~= HTTPRequest(headers: [:]) // false

可以创建正文模式来匹配请求正文数据

public protocol HTTPBodyPattern: Sendable {
  func evaluate(_ body: Data) -> Bool
}

Darwin 平台可以使用 NSPredicate 模式匹配 JSON 正文

let route = HTTPRoute("POST *", body: .json(where: "food == 'fish'"))
{"side": "chips", "food": "fish"}

路由参数 (Route Parameters)

路由 (Routes) 可以在路径或查询项中使用带有 : 前缀的命名参数。提供给此参数的任何字符串都将匹配路由,处理器 (handlers) 可以使用 request.routeParameters 访问字符串的值。

handler.appendRoute("GET /creature/:name?type=:beast") { request in
  let name = request.routeParameters["name"]
  let beast = request.routeParameters["beast"]
  return HTTPResponse(statusCode: .ok)
}

路由参数可以自动提取并映射到处理器 (handlers) 的闭包参数。

enum Beast: String, HTTPRouteParameterValue {
  case fish
  case dog
}

handler.appendRoute("GET /creature/:name?type=:beast") { (name: String, beast: Beast) -> HTTPResponse in
  return HTTPResponse(statusCode: .ok)
}

请求可以可选地包含在内。

handler.appendRoute("GET /creature/:name?type=:beast") { (request: HTTPRequest, name: String, beast: Beast) -> HTTPResponse in
  return HTTPResponse(statusCode: .ok)
}

可以提取 StringIntDoubleBool 以及任何符合 HTTPRouteParameterValue 的类型。

WebSockets

HTTPResponse 可以通过在响应有效负载中提供 WSHandler,将连接切换到 WebSocket 协议。

protocol WSHandler {
  func makeFrames(for client: AsyncThrowingStream<WSFrame, Error>) async throws -> AsyncStream<WSFrame>
}

WSHandler 方便交换一对 AsyncStream<WSFrame>,其中包含通过连接发送的原始 WebSocket 帧。虽然功能强大,但通过 WebSocketHTTPHandler 交换消息流更方便。

宏 (Macros)

仓库 FlyingFoxMacros 包含可以使用 HTTPRoute 注释的宏,以自动合成 HTTPHandler

import FlyingFox
import FlyingFoxMacros

@HTTPHandler
struct MyHandler {

  @HTTPRoute("/ping")
  func ping() { }

  @HTTPRoute("/pong")
  func getPong(_ request: HTTPRequest) -> HTTPResponse {
    HTTPResponse(statusCode: .accepted)
  }

  @JSONRoute("POST /account")
  func createAccount(body: AccountRequest) -> AccountResponse {
    AccountResponse(id: UUID(), balance: body.balance)
  }
}

let server = HTTPServer(port: 80, handler: MyHandler())
try await server.run()

注释通过 SE-0389 附加宏 实现。

在此处阅读更多信息:here

FlyingSocks

在内部,FlyingFox 使用标准 BSD 套接字的薄封装。FlyingSocks 模块为这些套接字提供跨平台异步接口;

import FlyingSocks

let socket = try await AsyncSocket.connected(to: .inet(ip4: "192.168.0.100", port: 80))
try await socket.write(Data([0x01, 0x02, 0x03]))
try socket.close()

Socket (套接字)

Socket 封装了文件描述符,并为常见操作提供了 Swift 接口,抛出 SocketError 而不是返回错误代码。

public enum SocketError: LocalizedError {
  case blocked
  case disconnected
  case unsupportedAddress
  case failed(type: String, errno: Int32, message: String)
}

当套接字没有数据可用并且返回 EWOULDBLOCK 错误码时,将抛出 SocketError.blocked

AsyncSocket (异步套接字)

AsyncSocket 只是封装了 Socket 并提供异步接口。所有异步套接字都配置了 O_NONBLOCK 标志,捕获 SocketError.blocked,然后使用 AsyncSocketPool 挂起当前任务。当数据可用时,任务将恢复,AsyncSocket 将重试操作。

AsyncSocketPool (异步套接字池)

protocol AsyncSocketPool {
  func prepare() async throws
  func run() async throws

  // Suspends current task until a socket is ready to read and/or write
  func suspendSocket(_ socket: Socket, untilReadyFor events: Socket.Events) async throws
}

SocketPool (套接字池)

SocketPool<Queue>HTTPServer 中使用的默认池。它使用其通用 EventQueue 挂起和恢复套接字,具体取决于平台。在 Darwin 平台上抽象 kqueue(2),在 Linux 上抽象 epoll(7),该池使用内核事件,而无需持续轮询等待的文件描述符。

Windows 使用由 poll(2) / Task.yield() 持续循环支持的队列,以指定的间隔检查所有等待数据的套接字。

SocketAddress (套接字地址)

sockaddr 结构簇通过符合 SocketAddress 进行分组

这允许使用任何这些配置的地址启动 HTTPServer

// only listens on localhost 8080
let server = HTTPServer(address: .loopback(port: 8080))

它也可以与 UNIX 域 地址一起使用,允许通过套接字进行私有 IPC

// only listens on Unix socket "Ants"
let server = HTTPServer(address: .unix(path: "Ants"))

然后你可以 netcat 到套接字

% nc -U Ants

命令行应用 (Command line app)

示例命令行应用 FlyingFoxCLI 可在此处获得。

鸣谢

FlyingFox 主要由 Simon Whitty 完成。

(贡献者完整列表)