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的作品。
(貢獻者的完整列表)