FlyingFox est un serveur HTTP léger construit en utilisant la concurrence Swift. Le serveur utilise des sockets BSD non bloquants, gérant chaque connexion dans une tâche enfant simultanée. Lorsqu'une prise est bloquée sans données, les tâches sont suspendues à l'aide de l' AsyncSocketPool partagé.
FlyingFox peut être installé en utilisant Swift Package Manager.
Remarque: FlyingFox nécessite Swift 5.9 sur Xcode 15+. Il fonctionne sur iOS 13+, TVOS 13+, Watchos 8+, MacOS 10.15+ et Linux. La prise en charge d'Android et de Windows 10 est expérimentale.
Pour installer à l'aide de Swift Package Manager, ajoutez ceci à la section des dependencies: dans votre fichier package.swift:
. package ( url : " https://github.com/swhitty/FlyingFox.git " , . upToNextMajor ( from : " 0.20.0 " ) )Démarrez le serveur en fournissant un numéro de port:
import FlyingFox
let server = HTTPServer ( port : 80 )
try await server . run ( )Le serveur s'exécute dans la tâche actuelle. Pour arrêter le serveur, annulez la tâche terminant toutes les connexions immédiatement:
let task = Task { try await server . run ( ) }
task . cancel ( )Arrêtez gracieusement le serveur une fois toutes les demandes existantes terminées, autrement fermée avec force après un délai d'expiration:
await server . stop ( timeout : 3 )Attendez que le serveur écoute et prêt pour les connexions:
try await server . waitUntilListening ( )Récupérer l'adresse d'écoute actuelle:
await server . listeningAddressRemarque: iOS raccrochera la prise d'écoute lorsqu'une application est suspendue en arrière-plan. Une fois que l'application revient au premier plan,
HTTPServer.run()détecte cela, lançantSocketError.disconnected. Le serveur doit ensuite être recommencé.
Les gestionnaires peuvent être ajoutés au serveur en implémentant HTTPHandler :
protocol HTTPHandler {
func handleRequest ( _ request : HTTPRequest ) async throws -> HTTPResponse
}Des itinéraires peuvent être ajoutés aux demandes de déléguation du serveur à un gestionnaire:
await server . appendRoute ( " /hello " , to : handler )Ils peuvent également être ajoutés aux fermetures:
await server . appendRoute ( " /hello " ) { request in
try await Task . sleep ( nanoseconds : 1_000_000_000 )
return HTTPResponse ( statusCode : . ok )
}Les demandes entrantes sont acheminées vers le gestionnaire de la première route de correspondance.
Les gestionnaires peuvent lancer HTTPUnhandledError si après avoir inspecté la demande, ils ne peuvent pas le gérer. L'itinéraire de correspondance suivant est ensuite utilisé.
Les demandes qui ne correspondent à aucun itinéraire géré reçoivent HTTP 404 .
Les demandes peuvent être acheminées vers des fichiers statiques avec FileHTTPHandler :
await server . appendRoute ( " GET /mock " , to : . file ( named : " mock.json " ) ) FileHTTPHandler renverra HTTP 404 si le fichier n'existe pas.
Les demandes peuvent être acheminées vers des fichiers statiques dans un répertoire avec DirectoryHTTPHandler :
await server . appendRoute ( " GET /mock/* " , to : . directory ( subPath : " Stubs " , serverPath : " mock " ) )
// GET /mock/fish/index.html ----> Stubs/fish/index.html DirectoryHTTPHandler renverra HTTP 404 si un fichier n'existe pas.
Les demandes peuvent être proxées via une URL de base:
await server . appendRoute ( " GET * " , to : . proxy ( via : " https://pie.dev " ) )
// GET /get?fish=chips ----> GET https://pie.dev/get?fish=chipsLes demandes peuvent être redirigées vers une URL:
await server . appendRoute ( " GET /fish/* " , to : . redirect ( to : " https://pie.dev/get " ) )
// GET /fish/chips ---> HTTP 301
// Location: https://pie.dev/get Les demandes peuvent être acheminées vers un WebSocket en fournissant un WSMessageHandler où une paire d' AsyncStream<WSMessage> est échangée:
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 )
}Des cadres Web WebSocket peuvent également être fournis.
Plusieurs gestionnaires peuvent être regroupés avec des demandes et appariés avec HTTPRoute à l'aide de RoutedHTTPHandler .
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 est lancé lorsqu'il n'est pas en mesure de traiter la demande avec l'un de ses gestionnaires enregistrés.
HTTPRoute est conçu pour être apparié pour HTTPRequest , permettant d'identifier les demandes par certaines ou toutes ses propriétés.
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 Les itinéraires sont ExpressibleByStringLiteral permettant aux littéraux de convertir automatiquement en HTTPRoute :
let route : HTTPRoute = " /hello/world "Les itinéraires peuvent inclure une méthode spécifique contre:
let route = HTTPRoute ( " GET /hello/world " )
route ~= HTTPRequest ( method : . GET , path : " /hello/world " ) // true
route ~= HTTPRequest ( method : . POST , path : " /hello/world " ) // falseIls peuvent également utiliser les jilèges dans le chemin:
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 " ) // falseLes itinéraires peuvent inclure des paramètres qui correspondent comme des caractères génériques permettant aux gestionnaires d'extraire la valeur de la demande.
let route = HTTPRoute ( " GET /hello/:beast/world " )
let beast = request . routeParameters [ " beast " ]Les jilèges traînants correspondent à tous les composants de chemin de traîne:
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 " ) // trueLes articles de requête spécifiques peuvent être appariés:
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 " ) // falseLes valeurs des éléments de requête peuvent inclure les jilèges:
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 " ) // falseLes en-têtes HTTP peuvent être égalés:
let route = HTTPRoute ( " * " , headers : [ . contentType : " application/json " ] )
route ~= HTTPRequest ( headers : [ . contentType : " application/json " ] ) // true
route ~= HTTPRequest ( headers : [ . contentType : " application/xml " ] ) // falseLes valeurs d'en-tête peuvent être des caractères génériques:
let route = HTTPRoute ( " * " , headers : [ . authorization : " * " ] )
route ~= HTTPRequest ( headers : [ . authorization : " abc " ] ) // true
route ~= HTTPRequest ( headers : [ . authorization : " xyz " ] ) // true
route ~= HTTPRequest ( headers : [ : ] ) // falseDes modèles corporels peuvent être créés pour correspondre aux données du corps de la demande:
public protocol HTTPBodyPattern : Sendable {
func evaluate ( _ body : Data ) -> Bool
} Les plates-formes Darwin peuvent correspondre à un corps JSON avec un NSPredicate :
let route = HTTPRoute ( " POST * " , body : . json ( where : " food == 'fish' " ) ) { "side" : " chips " , "food" : " fish " } Les itinéraires peuvent inclure des paramètres nommés dans un élément de chemin ou de requête en utilisant : préfixe. Toute chaîne fournie à ce paramètre correspondra à l'itinéraire, les gestionnaires peuvent accéder à la valeur de la chaîne à l'aide 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 )
}Les paramètres d'itinéraire peuvent être extraits et mappés automatiquement aux paramètres de fermeture des gestionnaires.
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 )
}La demande peut être éventuellement incluse.
handler . appendRoute ( " GET /creature/:name?type=:beast " ) { ( request : HTTPRequest , name : String , beast : Beast ) -> HTTPResponse in
return HTTPResponse ( statusCode : . ok )
} String , Int , Double , Bool et tout type conforme à HTTPRouteParameterValue peut être extrait.
HTTPResponse peut basculer la connexion au protocole WebSocket en produisant un WSHandler dans la charge utile de réponse.
protocol WSHandler {
func makeFrames ( for client : AsyncThrowingStream < WSFrame , Error > ) async throws -> AsyncStream < WSFrame >
} WSHandler facilite l'échange d'une paire AsyncStream<WSFrame> contenant les trames WebSocket brutes envoyées sur la connexion. Bien que puissant, il est plus pratique d'échanger des flux de messages via WebSocketHTTPHandler .
Le repo FlyingFoxMacros contient des macros qui peuvent être annotées avec HTTPRoute pour synthétiser automatiquement un 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 ( )Les annotations sont implémentées via des macros joints SE-0389.
Lisez la suite ici.
En interne, FlyingFox utilise un emballage mince autour des prises BSD standard. Le module FlyingSocks fournit une interface asynchrone à plate-forme transversale à ces sockets;
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 enveloppe un descripteur de fichiers et fournit une interface rapide aux opérations communes, lançant SocketError au lieu de renvoyer des codes d'erreur.
public enum SocketError : LocalizedError {
case blocked
case disconnected
case unsupportedAddress
case failed ( type : String , errno : Int32 , message : String )
} Lorsque les données ne sont pas disponibles pour une prise et que l'errNo EWOULDBLOCK est renvoyé, alors SocketError.blocked est lancé.
AsyncSocket enveloppe simplement une Socket et fournit une interface asynchrone. Toutes les prises asynchrones sont configurées avec le drapeau O_NONBLOCK , attrapant SocketError.blocked , puis suspendue la tâche actuelle à l'aide d'un AsyncSocketPool . Lorsque les données deviennent disponibles, la tâche est reprise et AsyncSocket réessayera l'opération.
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> est le pool par défaut utilisé dans HTTPServer . Il suspend et reprend des sockets en utilisant son EventQueue générique en fonction de la plate-forme. Résumé kqueue(2) Sur les plates-formes Darwin et epoll(7) sur Linux, le pool utilise des événements de noyau sans avoir à interroger en continu les descripteurs de fichiers d'attente.
Windows utilise une file d'attente soutenue par une boucle continue de poll(2) / Task.yield() pour vérifier toutes les prises en attente de données à un intervalle fourni.
Le cluster de structures sockaddr est regroupé via la conformité à SocketAddress
sockaddr_insockaddr_in6sockaddr_un Cela permet à HTTPServer de démarrer avec l'une de ces adresses configurées:
// only listens on localhost 8080
let server = HTTPServer ( address : . loopback ( port : 8080 ) )Il peut également être utilisé avec des adresses Unix-domaine, permettant une IPC privée sur une prise:
// only listens on Unix socket "Ants"
let server = HTTPServer ( address : . unix ( path : " Ants " ) )Vous pouvez ensuite Netcat sur la prise:
% nc -U Ants
Un exemple de l'application de ligne de commande FlyingFoxcli est disponible ici.
Flyingfox est principalement le travail de Simon Whitty.
(Liste complète des contributeurs)