Flyingfox ist ein leichter HTTP -Server, der mit Swift -Parallelität erstellt wurde. Der Server verwendet nicht blockierende BSD -Sockets und behandelt jede Verbindung in einer gleichzeitigen untergeordneten Aufgabe. Wenn eine Steckdose ohne Daten blockiert wird, werden die Aufgaben unter Verwendung des gemeinsam genutzten AsyncSocketPool suspendiert.
FlyingFox kann mit Swift Paket Manager installiert werden.
Hinweis: FlyingFox benötigt Swift 5.9 auf Xcode 15+. Es läuft auf iOS 13+, TVOS 13+, WatchOS 8+, MacOS 10.15+ und Linux. Die Unterstützung von Android und Windows 10 ist experimentell.
Um mit Swift Package Manager zu installieren, fügen Sie dies den dependencies: Abschnitt in Ihrem Paket.Swift -Datei:
. package ( url : " https://github.com/swhitty/FlyingFox.git " , . upToNextMajor ( from : " 0.20.0 " ) )Starten Sie den Server, indem Sie eine Portnummer bereitstellen:
import FlyingFox
let server = HTTPServer ( port : 80 )
try await server . run ( )Der Server wird innerhalb der aktuellen Aufgabe ausgeführt. Um den Server zu stoppen, stornieren Sie die Aufgabe, alle Verbindungen unmittelbar zu beenden:
let task = Task { try await server . run ( ) }
task . cancel ( )Nach Abschluss aller vorhandenen Anfragen ordnungsgemäß herunterfahren, um nach einem Auszeits einen gewaltsamen Schluss zu erhalten:
await server . stop ( timeout : 3 )Warten Sie, bis der Server zuhört und für Verbindungen bereit ist:
try await server . waitUntilListening ( )Abrufen Sie die aktuelle Höradresse ab:
await server . listeningAddressHinweis: IOS hängt die Hörstecke auf, wenn eine App im Hintergrund suspendiert ist. Sobald die App in den Vordergrund zurückkehrt, erkennt
HTTPServer.run()dies und werfenSocketError.disconnected. Der Server muss dann noch einmal gestartet werden.
Handler können zum Server hinzugefügt werden, indem HTTPHandler implementiert wird:
protocol HTTPHandler {
func handleRequest ( _ request : HTTPRequest ) async throws -> HTTPResponse
}Routen können zum Server -Delegieren von Anforderungen an einen Handler hinzugefügt werden:
await server . appendRoute ( " /hello " , to : handler )Sie können auch zu Schließungen hinzugefügt werden:
await server . appendRoute ( " /hello " ) { request in
try await Task . sleep ( nanoseconds : 1_000_000_000 )
return HTTPResponse ( statusCode : . ok )
}Eingehende Anfragen werden an den Handler der ersten passenden Route weitergeleitet.
Handler können HTTPUnhandledError werfen, wenn sie nach der Inspektion der Anfrage nicht damit umgehen können. Die nächste passende Route wird dann verwendet.
Anfragen, die nicht mit einer gehandhabten Route übereinstimmen, erhalten HTTP 404 .
Anfragen können mit FileHTTPHandler in statische Dateien weitergeleitet werden:
await server . appendRoute ( " GET /mock " , to : . file ( named : " mock.json " ) ) FileHTTPHandler gibt HTTP 404 zurück, wenn die Datei nicht vorhanden ist.
Anfragen können in ein Verzeichnis mit DirectoryHTTPHandler in statische Dateien weitergeleitet werden:
await server . appendRoute ( " GET /mock/* " , to : . directory ( subPath : " Stubs " , serverPath : " mock " ) )
// GET /mock/fish/index.html ----> Stubs/fish/index.html DirectoryHTTPHandler gibt HTTP 404 zurück, wenn keine Datei vorliegt.
Anfragen können über eine Basis -URL ausgewiesen werden:
await server . appendRoute ( " GET * " , to : . proxy ( via : " https://pie.dev " ) )
// GET /get?fish=chips ----> GET https://pie.dev/get?fish=chipsAnfragen können zu einer URL weitergeleitet werden:
await server . appendRoute ( " GET /fish/* " , to : . redirect ( to : " https://pie.dev/get " ) )
// GET /fish/chips ---> HTTP 301
// Location: https://pie.dev/get Anfragen können in ein WebSocket weitergeleitet werden, indem ein WSMessageHandler bereitgestellt wird, bei dem ein Paar AsyncStream<WSMessage> ausgetauscht wird:
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 )
}Rohes Websocket -Frames können ebenfalls bereitgestellt werden.
Mehrere Handler können mit Anfragen gruppiert und gegen HTTPRoute mit RoutedHTTPHandler abgestimmt werden.
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 wird geworfen, wenn es die Anfrage mit einem seiner registrierten Handler nicht bearbeiten kann.
HTTPRoute ist als Muster konzipiert, das mit HTTPRequest übereinstimmt, sodass Anfragen von einigen oder allen Eigenschaften identifiziert werden können.
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 Routen sind ExpressibleByStringLiteral sodass Literale automatisch in HTTPRoute konvertiert werden können:
let route : HTTPRoute = " /hello/world "Routen können eine bestimmte Methode enthalten, mit der sie übereinstimmen können:
let route = HTTPRoute ( " GET /hello/world " )
route ~= HTTPRequest ( method : . GET , path : " /hello/world " ) // true
route ~= HTTPRequest ( method : . POST , path : " /hello/world " ) // falseSie können auch Wildcards innerhalb des Weges verwenden:
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 " ) // falseRouten können Parameter enthalten, die wie Platzhalter übereinstimmen, die es Handlern ermöglichen, den Wert aus der Anforderung zu extrahieren.
let route = HTTPRoute ( " GET /hello/:beast/world " )
let beast = request . routeParameters [ " beast " ]Nachfolgende Wildcards entsprechen allen nachlaufenden Pfadkomponenten:
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 " ) // trueSpezifische Abfragelemente können übereinstimmen:
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 " ) // falseAbfragelementwerte können Platzhalter enthalten:
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 -Header können übereinstimmen:
let route = HTTPRoute ( " * " , headers : [ . contentType : " application/json " ] )
route ~= HTTPRequest ( headers : [ . contentType : " application/json " ] ) // true
route ~= HTTPRequest ( headers : [ . contentType : " application/xml " ] ) // falseHeaderwerte können Platzhalter sein:
let route = HTTPRoute ( " * " , headers : [ . authorization : " * " ] )
route ~= HTTPRequest ( headers : [ . authorization : " abc " ] ) // true
route ~= HTTPRequest ( headers : [ . authorization : " xyz " ] ) // true
route ~= HTTPRequest ( headers : [ : ] ) // falseKörpermuster können erstellt werden, um den Anforderungskörperdaten zu entsprechen:
public protocol HTTPBodyPattern : Sendable {
func evaluate ( _ body : Data ) -> Bool
} Darwin -Plattformen können einen JSON -Körper mit einem NSPredicate entsprechen:
let route = HTTPRoute ( " POST * " , body : . json ( where : " food == 'fish' " ) ) { "side" : " chips " , "food" : " fish " } Routen können benannte Parameter in einem Pfad oder Abfragelement mit dem : Präfix enthalten. Jede an diesem Parameter gelieferte Zeichenfolge entspricht der Route. Handler können auf den Wert der request.routeParameters zugreifen.
handler . appendRoute ( " GET /creature/:name?type=:beast " ) { request in
let name = request . routeParameters [ " name " ]
let beast = request . routeParameters [ " beast " ]
return HTTPResponse ( statusCode : . ok )
}Routenparameter können automatisch extrahiert und auf Verschlussparameter von Handlern abgebildet werden.
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 )
}Die Anfrage kann optional enthalten sein.
handler . appendRoute ( " GET /creature/:name?type=:beast " ) { ( request : HTTPRequest , name : String , beast : Beast ) -> HTTPResponse in
return HTTPResponse ( statusCode : . ok )
} String , Int , Double , Bool und jeder Typ, der sich an HTTPRouteParameterValue entspricht, kann extrahiert werden.
HTTPResponse kann die Verbindung zum WebSocket -Protokoll wechseln, indem Sie einen WSHandler innerhalb der Antwortnutzlast produzieren.
protocol WSHandler {
func makeFrames ( for client : AsyncThrowingStream < WSFrame , Error > ) async throws -> AsyncStream < WSFrame >
} WSHandler ermöglicht den Austausch eines Paares AsyncStream<WSFrame> das die über die Verbindung gesendeten rohen WebSocket -Frames enthält. Obwohl es leistungsfähig ist, ist es bequemer, über WebSocketHTTPHandler -Streams von Nachrichten auszutauschen.
Das Repo FlyingFoxMacros enthält Makros, die mit HTTPRoute kommentiert werden können, um einen HTTPHandler automatisch zu syntieren.
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 ( )Die Annotationen werden über SE-0389 beigefügte Makros implementiert.
Lesen Sie hier mehr.
Intern verwendet Flyingfox eine dünne Wrapper um Standard -BSD -Sockeln. Das FlyingSocks -Modul bietet eine asynchrische Kreuzungsplattform zu diesen Steckdosen.
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 wickelt einen Dateideskriptor und bietet eine schnelle Schnittstelle zu allgemeinen Operationen, wobei SocketError anstelle von zurückgegebenen Fehlercodes geworfen wird.
public enum SocketError : LocalizedError {
case blocked
case disconnected
case unsupportedAddress
case failed ( type : String , errno : Int32 , message : String )
} Wenn Daten für einen Socket nicht verfügbar sind und das EWOULDBLOCK -Errno zurückgegeben wird, wird SocketError.blocked geworfen.
AsyncSocket hüllt einfach eine Socket und bietet eine asynchronisierte Schnittstelle. Alle asynchronen Sockets sind mit dem Flag O_NONBLOCK konfiguriert, fangen SocketError.blocked und speichern die aktuelle Aufgabe mithilfe eines AsyncSocketPool . Wenn Daten verfügbar werden, wird die Aufgabe wieder aufgenommen und AsyncSocket wird der Betrieb wiederholt.
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> ist der Standardpool, der in HTTPServer verwendet wird. In Abhängigkeit von der Plattform wird die Steckdosen mithilfe seines generischen EventQueue ausgesetzt und fortgesetzt. Zusammenfassung kqueue(2) auf Darwin -Plattformen und epoll(7) unter Linux verwendet der Pool Kernel -Ereignisse, ohne die Wartendateideskriptoren kontinuierlich zu befragen.
Windows verwendet eine Warteschlange, die durch eine kontinuierliche poll(2) / Task.yield() unterstützt wird.
Der sockaddr -Struktionscluster wird durch Konformität mit SocketAddress gruppiert
sockaddr_insockaddr_in6sockaddr_un Auf diese Weise kann HTTPServer mit einer dieser konfigurierten Adressen gestartet werden:
// only listens on localhost 8080
let server = HTTPServer ( address : . loopback ( port : 8080 ) )Es kann auch mit UNIX-Domain-Adressen verwendet werden, sodass ein privates IPC über einem Socket ermöglicht wird:
// only listens on Unix socket "Ants"
let server = HTTPServer ( address : . unix ( path : " Ants " ) )Sie können dann die Socket netcat:
% nc -U Ants
Eine Beispiel -Befehlszeilen -App Flyingfoxcli ist hier verfügbar.
Flyingfox ist in erster Linie die Arbeit von Simon Whitty.
(Vollständige Liste der Mitwirkenden)