FlyingSocks 0.15.0

FlyingSocks 0.15.0

Simon Whitty维护。



  • 作者:
  • Simon Whitty

Build Codecov Platforms Swift 5.8 License Twitter

介绍

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

安装

可以使用Swift Package Manager安装FlyingFox。

注意: FlyingFox需要Swift 5.5和Xcode 13.2+。它在iOS 13+、tvOS 13+、macOS 10.15+和Linux上运行。Windows 10支持是实验性的。

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

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

使用方法

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

import FlyingFox

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

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

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

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

await server.stop(timeout: 3)

等待服务器开始监听并准备接收连接

try await server.waitUntilListening()

检索当前监听地址

await server.listeningAddress

注意:当应用在后台挂起时,iOS 会挂起监听套接字。一旦应用返回前台,HTTPServer.start() 会检测到这一点,抛出 SocketError.disconnected 异常。此时必须重新启动服务器。

处理程序

可以通过实现 HTTPHandler 将处理程序添加到服务器中

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

可以将路由添加到服务器,并将请求委派给处理程序

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)
}

传入的请求将被路由到第一个匹配的路由的处理程序。

如果处理程序在检查请求后无法处理它,它们可以抛出 HTTPUnhandledError。然后使用下一个匹配的路由。

与任何处理路由不匹配的请求将收到 HTTP 404

FileHTTPHandler

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

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

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

DirectoryHTTPHandler

可以使用 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

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

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

RedirectHTTPHandler

可以将请求重定向到URL。

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

WebSocketHTTPHandler

通过提供WSMessageHandler,可以提供一个对AsyncStream<WSMessage>对的交换,以将请求路由到websocket。

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

可以使用RoutedHTTPHandler将多个处理器组合起来,并将请求与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)

当无法用任何已注册的处理器处理请求时,将引发HTTPUnhandledError

路由器

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

路由器支持ExpressibleByStringLiteral,允许字面量自动转换为HTTPRoute

let route: HTTPRoute = "/hello/world"

路由器可以包含一个特定方法以进行匹配

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

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

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"}

WebSocket

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

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

WSHandler促进了一对包含原始WebSocket帧的AsyncStream<WSFrame>的交换。虽然功能强大,但通过WebSocketHTTPHandler交换消息流更为方便。

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()

套接字

套接字包装了一个文件描述符,并提供了一个 Swift 接口来访问常见操作,通过抛出 SocketError 而不是错误代码来处理异常。

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

当一个套接字的数据不可用时,如果返回了 EWOULDBLOCK 错误号,则抛出 SocketError.blocked

异步套接字

异步套接字简单地包装了一个 套接字,并提供了一个异步接口。所有异步套接字都配置了 O_NONBLOCK 标志,捕获 SocketError.blocked 然后使用 AsyncSocketPool 暂停当前任务。当数据变为可用时,任务将继续,AsyncSocket 将重试操作。

异步套接字池

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<Queue>HTTPServer 内部使用的基本池。它根据平台使用泛型 EventQueue 暂停和恢复套接字。在 Darwin 平台上抽象 kqueue(2),在 Linux 上抽象 epoll(7),池使用内核事件而无需连续轮询等待的文件描述符。

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

SocketAddress

sockaddr 结构体簇通过 SocketAddress 的规范进行分组

  • sockaddr_in
  • sockaddr_in6
  • sockaddr_un

这使得 HTTPServer 可以使用任何一个配置的地址来启动

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

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

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

然后您可以使用 netcat 操作该套接字

% nc -U Ants

命令行应用

示例命令行应用 FlyingFoxCLI 在此处可用 这里

致谢

FlyingFox 主要由 Simon Whitty 完成。

(贡献者完整列表)