FlyingFox是使用Swift并发构建的轻质HTTP服务器。该服务器使用非阻止BSD插座,在并发的子任务中处理每个连接。当插座被阻止没有数据时,使用共享的AsyncSocketPool暂停任务。
可以使用Swift软件包管理器安装FlyingFox。
注意: FlyingFox在Xcode 15+上需要Swift 5.9。它在iOS 13+,TVOS 13+,WatchOS 8+,MacOS 10.15+和Linux上运行。 Android和Windows 10支持是实验性的。
要使用Swift Package Manager安装,请将其添加到dependencies:软件包中的部分。Swift文件:
. package ( url : " https://github.com/swhitty/FlyingFox.git " , . upToNextMajor ( from : " 0.20.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.disconnectedtofl。然后必须再次启动服务器。
可以通过实现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 )
}还可以提供RAW 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 ( " 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 " ) // falseHTTP标头可以匹配:
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
}达尔文平台可以将JSON主体与NSPredicate匹配:
let route = HTTPRoute ( " POST * " , body : . json ( where : " food == 'fish' " ) ) { "side" : " chips " , "food" : " fish " }路由可以使用:前缀在路径或查询项目中包含命名参数。提供给此参数的任何字符串都可以匹配该路由,处理程序可以使用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 )
}路由参数可以自动提取并映射到处理程序的闭合参数。
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 )
}可以提取符合HTTPRouteParameterValue的String , Int , Double , Bool和任何类型。
HTTPResponse可以通过在响应有效载荷中提供WSHandler来将连接转换为Websocket协议。
protocol WSHandler {
func makeFrames ( for client : AsyncThrowingStream < WSFrame , Error > ) async throws -> AsyncStream < WSFrame >
} WSHandler促进了一对AsyncStream<WSFrame>包含在连接上发送的RAW WebSocket框架的交换。尽管功能强大,但通过WebSocketHTTPHandler交换消息流更加方便。
回购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附加的宏实现的。
在这里阅读更多。
在内部,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包装文件描述符,并为普通操作提供了快速接口,抛出了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并提供异步接口即可。所有异步插座均配置为FLAG 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_insockaddr_in6sockaddr_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 " ) )然后,您可以进入插座:
% nc -U Ants
示例命令行应用程序可在此处使用。
FlyingFox主要是Simon Whitty的作品。
(贡献者的完整列表)