Flyingfox es un servidor HTTP ligero construido con la concurrencia Swift. El servidor utiliza enchufes BSD que no tienen bloqueo, manejando cada conexión en una tarea infantil concurrente. Cuando un socket se bloquea sin datos, las tareas se suspenden utilizando AsyncSocketPool compartido.
Flyingfox se puede instalar utilizando Swift Package Manager.
Nota: Flyingfox requiere Swift 5.9 en Xcode 15+. Se ejecuta en iOS 13+, TVOS 13+, WatchOS 8+, MacOS 10.15+ y Linux. El soporte de Android y Windows 10 es experimental.
Para instalar con Swift Package Manager, agregue esto a las dependencies: sección en su archivo paquete.swift:
. package ( url : " https://github.com/swhitty/FlyingFox.git " , . upToNextMajor ( from : " 0.20.0 " ) )Inicie el servidor proporcionando un número de puerto:
import FlyingFox
let server = HTTPServer ( port : 80 )
try await server . run ( )El servidor se ejecuta dentro de la tarea actual. Para detener el servidor, cancele la tarea que termina todas las conexiones inmediatamente:
let task = Task { try await server . run ( ) }
task . cancel ( )Apague con gracia el servidor después de que se complete todas las solicitudes existentes, de lo contrario, cerrada con fuerza después de un tiempo de espera:
await server . stop ( timeout : 3 )Espere hasta que el servidor esté escuchando y listo para las conexiones:
try await server . waitUntilListening ( )Recupere la dirección de escucha actual:
await server . listeningAddressNota: iOS colgará el enchufe de escucha cuando una aplicación esté suspendida en segundo plano. Una vez que la aplicación vuelve al primer plano,
HTTPServer.run()detecta esto, lanzandoSocketError.disconnected. El servidor debe iniciarse una vez más.
Los manejadores se pueden agregar al servidor implementando HTTPHandler :
protocol HTTPHandler {
func handleRequest ( _ request : HTTPRequest ) async throws -> HTTPResponse
}Las rutas se pueden agregar a las solicitudes de delegación del servidor a un controlador:
await server . appendRoute ( " /hello " , to : handler )También se pueden agregar a los cierres:
await server . appendRoute ( " /hello " ) { request in
try await Task . sleep ( nanoseconds : 1_000_000_000 )
return HTTPResponse ( statusCode : . ok )
}Las solicitudes entrantes se enrutan al controlador de la primera ruta coincidente.
Los manejadores pueden lanzar HTTPUnhandledError si después de inspeccionar la solicitud, no pueden manejarla. Luego se usa la siguiente ruta coincidente.
Las solicitudes que no coinciden con ninguna ruta manejada reciben HTTP 404 .
Las solicitudes se pueden enrutar a archivos estáticos con FileHTTPHandler :
await server . appendRoute ( " GET /mock " , to : . file ( named : " mock.json " ) ) FileHTTPHandler devolverá HTTP 404 si el archivo no existe.
Las solicitudes se pueden enrutar a archivos estáticos dentro de un directorio con DirectoryHTTPHandler :
await server . appendRoute ( " GET /mock/* " , to : . directory ( subPath : " Stubs " , serverPath : " mock " ) )
// GET /mock/fish/index.html ----> Stubs/fish/index.html DirectoryHTTPHandler devolverá HTTP 404 si no existe un archivo.
Las solicitudes se pueden representar a través de una URL base:
await server . appendRoute ( " GET * " , to : . proxy ( via : " https://pie.dev " ) )
// GET /get?fish=chips ----> GET https://pie.dev/get?fish=chipsLas solicitudes se pueden redirigir a una URL:
await server . appendRoute ( " GET /fish/* " , to : . redirect ( to : " https://pie.dev/get " ) )
// GET /fish/chips ---> HTTP 301
// Location: https://pie.dev/get Las solicitudes se pueden enrutar a un WebSocket proporcionando un WSMessageHandler donde se intercambian un par de 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 )
}También se pueden proporcionar marcos de WebSocket en bruto.
Se pueden agrupar múltiples manejadores con solicitudes y coincidir con HTTPRoute utilizando 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 se arroja cuando no puede manejar la solicitud con ninguno de sus manejadores registrados.
HTTPRoute está diseñado para ser un patrón coincidente con HTTPRequest , lo que permite identificar las solicitudes por algunas o todas sus propiedades.
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 Las rutas son ExpressibleByStringLiteral permitiendo que los literales se conviertan automáticamente en HTTPRoute :
let route : HTTPRoute = " /hello/world "Las rutas pueden incluir un método específico para que coincida con:
let route = HTTPRoute ( " GET /hello/world " )
route ~= HTTPRequest ( method : . GET , path : " /hello/world " ) // true
route ~= HTTPRequest ( method : . POST , path : " /hello/world " ) // falseTambién pueden usar comodines dentro del camino:
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 " ) // falseLas rutas pueden incluir parámetros que coinciden como comodines que permiten a los manejadores extraer el valor de la solicitud.
let route = HTTPRoute ( " GET /hello/:beast/world " )
let beast = request . routeParameters [ " beast " ]Los comodines que los comodines coinciden con todos los componentes de trayectoria de final:
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 " ) // trueSe pueden combinar elementos de consulta específicos:
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 " ) // falseLos valores del elemento de consulta pueden incluir comodines:
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 " ) // falseLos encabezados HTTP se pueden combinar:
let route = HTTPRoute ( " * " , headers : [ . contentType : " application/json " ] )
route ~= HTTPRequest ( headers : [ . contentType : " application/json " ] ) // true
route ~= HTTPRequest ( headers : [ . contentType : " application/xml " ] ) // falseLos valores del encabezado pueden ser comodines:
let route = HTTPRoute ( " * " , headers : [ . authorization : " * " ] )
route ~= HTTPRequest ( headers : [ . authorization : " abc " ] ) // true
route ~= HTTPRequest ( headers : [ . authorization : " xyz " ] ) // true
route ~= HTTPRequest ( headers : [ : ] ) // falseLos patrones de cuerpo se pueden crear para que coincidan con los datos del cuerpo de la solicitud:
public protocol HTTPBodyPattern : Sendable {
func evaluate ( _ body : Data ) -> Bool
} Las plataformas Darwin pueden coincidir con un cuerpo JSON con un NSPredicate :
let route = HTTPRoute ( " POST * " , body : . json ( where : " food == 'fish' " ) ) { "side" : " chips " , "food" : " fish " } Las rutas pueden incluir parámetros con nombre dentro de una ruta o elemento de consulta utilizando : Prefix. Cualquier cadena suministrada a este parámetro coincidirá con la ruta, los manejadores pueden acceder al valor de la cadena usando 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 )
}Los parámetros de ruta se pueden extraer automáticamente y asignarse a los parámetros de cierre de los controladores.
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 solicitud se puede incluir opcionalmente.
handler . appendRoute ( " GET /creature/:name?type=:beast " ) { ( request : HTTPRequest , name : String , beast : Beast ) -> HTTPResponse in
return HTTPResponse ( statusCode : . ok )
} Se puede extraer String , Int , Double , Bool y cualquier tipo que se ajuste a HTTPRouteParameterValue .
HTTPResponse puede cambiar la conexión al protocolo WebSocket probando un WSHandler dentro de la carga útil de respuesta.
protocol WSHandler {
func makeFrames ( for client : AsyncThrowingStream < WSFrame , Error > ) async throws -> AsyncStream < WSFrame >
} WSHandler facilita el intercambio de un par AsyncStream<WSFrame> que contiene los marcos de WebSocket en bruto enviados a través de la conexión. Si bien es potente, es más conveniente intercambiar flujos de mensajes a través de WebSocketHTTPHandler .
El Repo FlyingFoxMacros contiene macros que se pueden anotar con HTTPRoute para sintisear automáticamente 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 ( )Las anotaciones se implementan a través de Macros adjuntas SE-0389.
Lea más aquí.
Internamente, Flyingfox utiliza un envoltorio delgado alrededor de los enchufes BSD estándar. El módulo FlyingSocks proporciona una interfaz asíncrata de plataforma cruzada a estos 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 envuelve un descriptor de archivo y proporciona una interfaz rápida a las operaciones comunes, lanzando SocketError en lugar de devolver los códigos de error.
public enum SocketError : LocalizedError {
case blocked
case disconnected
case unsupportedAddress
case failed ( type : String , errno : Int32 , message : String )
} Cuando los datos no están disponibles para un enchufe y se devuelve el error EWOULDBLOCK , entonces se lanza SocketError.blocked .
AsyncSocket simplemente envuelve un Socket y proporciona una interfaz Async. Todos los enchufes de Async están configurados con el indicador O_NONBLOCK , capturando SocketError.blocked y luego suspendiendo la tarea actual utilizando un AsyncSocketPool . Cuando los datos están disponibles, se reanuda la tarea y AsyncSocket volverá a intentar la operación.
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> es el grupo predeterminado utilizado en HTTPServer . Suspende y reanude los enchufes utilizando su EventQueue genéricos dependiendo de la plataforma. Abrazando kqueue(2) en las plataformas Darwin y epoll(7) en Linux, el grupo utiliza eventos del núcleo sin la necesidad de sondear continuamente los descriptores de archivos de espera.
Windows utiliza una cola respaldada por un bucle continuo de poll(2) / Task.yield() para verificar todos los enchufes que esperan datos a un intervalo suministrado.
El clúster de estructuras sockaddr se agrupa mediante conformidad con SocketAddress
sockaddr_insockaddr_in6sockaddr_un Esto permite que HTTPServer se inicie con cualquiera de estas direcciones configuradas:
// only listens on localhost 8080
let server = HTTPServer ( address : . loopback ( port : 8080 ) )También se puede usar con direcciones de dominio Unix, lo que permite IPC privado sobre un socket:
// only listens on Unix socket "Ants"
let server = HTTPServer ( address : . unix ( path : " Ants " ) )Luego puede netcat al socket:
% nc -U Ants
Una aplicación de línea de comandos de ejemplo FlyingFoxCli está disponible aquí.
Flyingfox es principalmente el trabajo de Simon Whitty.
(Lista completa de contribuyentes)