FlyingFox 是一个使用 Swift 并发 构建的轻量级 HTTP 服务器。该服务器使用非阻塞 BSD 套接字,在并发子 Task 中处理每个连接。当套接字被阻塞且没有数据时,任务会使用共享的 AsyncSocketPool
挂起。
可以使用 Swift Package Manager 安装 FlyingFox。
注意: FlyingFox 需要 Xcode 14.3+ 上的 Swift 5.8。它运行在 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.14.0"))
通过提供端口号来启动服务器
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
将请求路由到静态文件
await server.appendRoute("GET /mock", to: .file(named: "mock.json"))
如果文件不存在,FileHTTPHandler
将返回 HTTP 404
。
可以使用 DirectoryHTTPHandler
将请求路由到目录中的静态文件
await server.appendRoute("GET /mock/*", to: .directory(subPath: "Stubs", serverPath: "mock"))
// GET /mock/fish/index.html ----> Stubs/fish/index.html
如果文件不存在,DirectoryHTTPHandler
将返回 HTTP 404
。
可以通过基本 URL 代理请求
await server.appendRoute("GET *", to: .proxy(via: "https://pie.dev"))
// GET /get?fish=chips ----> GET https://pie.dev/get?fish=chips
可以将请求重定向到 URL
await server.appendRoute("GET /fish/*", to: .redirect(to: "https://pie.dev/get"))
// GET /fish/chips ---> HTTP 301
// Location: https://pie.dev/get
可以通过提供 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
将多个处理器与请求分组,并根据 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"}
通过在响应有效负载中提供 WSHandler
,HTTPResponse
可以将连接切换到 WebSocket 协议。
protocol WSHandler {
func makeFrames(for client: AsyncThrowingStream<WSFrame, Error>) async throws -> AsyncStream<WSFrame>
}
WSHandler
有助于交换一对 AsyncStream<WSFrame>
,其中包含通过连接发送的原始 websocket 帧。 虽然功能强大,但通过 WebSocketHTTPHandler
交换消息流更加方便。
分支 preview/macro
包含一个实验性预览实现,其中处理器可以使用路由注释函数
@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.start()
注释通过 SE-0389 Attached Macros 实现,该宏在 Swift 5.9 及更高版本中可用。
在此处阅读更多信息 here。
在内部,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
包装一个文件描述符,并为常用操作提供 Swift 接口,抛出 SocketError
而不是返回错误代码。
public enum SocketError: LocalizedError {
case blocked
case disconnected
case unsupportedAddress
case failed(type: String, errno: Int32, message: String)
}
当套接字没有数据可用并且返回 EWOULDBLOCK
errno 时,则抛出 SocketError.blocked
。
AsyncSocket
只是简单地包装了一个 Socket
并提供了一个异步接口。 所有异步套接字都配置了标志 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()
支持的队列,以指定的间隔检查所有等待数据的套接字。
sockaddr
结构集群通过符合 SocketAddress
进行分组
sockaddr_in
sockaddr_in6
sockaddr_un
这允许使用任何这些配置的地址启动 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
示例命令行应用程序 FlyingFoxCLI 可在此处获取 here。
FlyingFox 主要是 Simon Whitty 的作品。
(贡献者完整列表)