O Flyingfox é um servidor HTTP leve construído usando a Swift Concorrência. O servidor usa soquetes BSD não bloqueadores, manipulando cada conexão em uma tarefa simultânea para crianças. Quando um soquete é bloqueado sem dados, as tarefas são suspensas usando o AsyncSocketPool compartilhado.
O Flyingfox pode ser instalado usando o Swift Package Manager.
Nota: O FlyingFox requer Swift 5.9 no Xcode 15+. Ele é executado no iOS 13+, TvOS 13+, WatchOS 8+, MacOS 10.15+ e Linux. O suporte Android e Windows 10 é experimental.
Para instalar usando o Swift Package Manager, adicione isso às dependencies: Seção no seu arquivo package.swift:
. package ( url : " https://github.com/swhitty/FlyingFox.git " , . upToNextMajor ( from : " 0.20.0 " ) )Inicie o servidor fornecendo um número de porta:
import FlyingFox
let server = HTTPServer ( port : 80 )
try await server . run ( )O servidor é executado na tarefa atual. Para interromper o servidor, cancele a tarefa que termina todas as conexões imediatamente:
let task = Task { try await server . run ( ) }
task . cancel ( )Desligue graciosamente o servidor depois de todas as solicitações existentes concluídas, de outra forma, fechando com força após um tempo limite:
await server . stop ( timeout : 3 )Aguarde até que o servidor esteja ouvindo e pronto para conexões:
try await server . waitUntilListening ( )Recupere o endereço de escuta atual:
await server . listeningAddressNOTA: O iOS vai pendurar o soquete de escuta quando um aplicativo for suspenso em segundo plano. Depois que o aplicativo retornar ao primeiro plano,
HTTPServer.run()detecta isso, lançandoSocketError.disconnected. O servidor deve ser iniciado mais uma vez.
Os manipuladores podem ser adicionados ao servidor implementando HTTPHandler :
protocol HTTPHandler {
func handleRequest ( _ request : HTTPRequest ) async throws -> HTTPResponse
}As rotas podem ser adicionadas às solicitações de delegação do servidor a um manipulador:
await server . appendRoute ( " /hello " , to : handler )Eles também podem ser adicionados ao fechamento:
await server . appendRoute ( " /hello " ) { request in
try await Task . sleep ( nanoseconds : 1_000_000_000 )
return HTTPResponse ( statusCode : . ok )
}As solicitações recebidas são roteadas para o manipulador da primeira rota correspondente.
Os manipuladores podem jogar HTTPUnhandledError se, depois de inspecionar a solicitação, eles não podem lidar com isso. A próxima rota correspondente é usada.
Solicitações que não correspondem a nenhuma rota manipulada Receba HTTP 404 .
As solicitações podem ser roteadas para arquivos estáticos com FileHTTPHandler :
await server . appendRoute ( " GET /mock " , to : . file ( named : " mock.json " ) ) FileHTTPHandler retornará HTTP 404 se o arquivo não existir.
As solicitações podem ser roteadas para arquivos estáticos em um diretório com DirectoryHTTPHandler :
await server . appendRoute ( " GET /mock/* " , to : . directory ( subPath : " Stubs " , serverPath : " mock " ) )
// GET /mock/fish/index.html ----> Stubs/fish/index.html DirectoryHTTPHandler retornará HTTP 404 se um arquivo não existir.
Os pedidos podem ser proxiados por meio de um URL base:
await server . appendRoute ( " GET * " , to : . proxy ( via : " https://pie.dev " ) )
// GET /get?fish=chips ----> GET https://pie.dev/get?fish=chipsOs pedidos podem ser redirecionados para um URL:
await server . appendRoute ( " GET /fish/* " , to : . redirect ( to : " https://pie.dev/get " ) )
// GET /fish/chips ---> HTTP 301
// Location: https://pie.dev/get As solicitações podem ser roteadas para um WebSocket, fornecendo um WSMessageHandler , onde são trocados um 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 )
}Os quadros de websocket bruto também podem ser fornecidos.
Vários manipuladores podem ser agrupados com solicitações e correspondidos com HTTPRoute usando 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 é jogado quando não consegue lidar com a solicitação com nenhum de seus manipuladores registrados.
HTTPRoute foi projetado para ser o padrão correspondente ao HTTPRequest , permitindo que as solicitações sejam identificadas por algumas ou todas as suas propriedades.
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 As rotas são ExpressibleByStringLiteral permitindo que os literais sejam automaticamente convertidos em HTTPRoute :
let route : HTTPRoute = " /hello/world "As rotas podem incluir um método específico para combinar com:
let route = HTTPRoute ( " GET /hello/world " )
route ~= HTTPRequest ( method : . GET , path : " /hello/world " ) // true
route ~= HTTPRequest ( method : . POST , path : " /hello/world " ) // falseEles também podem usar curingas dentro do caminho:
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 " ) // falseAs rotas podem incluir parâmetros que correspondem a curingas, permitindo que os manipuladores extraem o valor da solicitação.
let route = HTTPRoute ( " GET /hello/:beast/world " )
let beast = request . routeParameters [ " beast " ]Os curingas à direita correspondem a todos os componentes do caminho à direita:
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 " ) // trueItens de consulta específicos podem ser correspondidos:
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 " ) // falseOs valores dos itens de consulta podem incluir curingas:
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 " ) // falseOs cabeçalhos HTTP podem ser correspondidos:
let route = HTTPRoute ( " * " , headers : [ . contentType : " application/json " ] )
route ~= HTTPRequest ( headers : [ . contentType : " application/json " ] ) // true
route ~= HTTPRequest ( headers : [ . contentType : " application/xml " ] ) // falseOs valores do cabeçalho podem ser curingas:
let route = HTTPRoute ( " * " , headers : [ . authorization : " * " ] )
route ~= HTTPRequest ( headers : [ . authorization : " abc " ] ) // true
route ~= HTTPRequest ( headers : [ . authorization : " xyz " ] ) // true
route ~= HTTPRequest ( headers : [ : ] ) // falseOs padrões corporais podem ser criados para corresponder aos dados do corpo da solicitação:
public protocol HTTPBodyPattern : Sendable {
func evaluate ( _ body : Data ) -> Bool
} As plataformas de Darwin podem padronizar um corpo JSON com um NSPredicate :
let route = HTTPRoute ( " POST * " , body : . json ( where : " food == 'fish' " ) ) { "side" : " chips " , "food" : " fish " } As rotas podem incluir parâmetros nomeados em um item de caminho ou consulta usando o : prefixo. Qualquer sequência fornecida a este parâmetro corresponderá à rota, os manipuladores podem acessar o valor da string 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 )
}Os parâmetros de rota podem ser extraídos automaticamente e mapeados para os parâmetros de fechamento dos manipuladores.
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 )
}A solicitação pode ser opcionalmente incluída.
handler . appendRoute ( " GET /creature/:name?type=:beast " ) { ( request : HTTPRequest , name : String , beast : Beast ) -> HTTPResponse in
return HTTPResponse ( statusCode : . ok )
} String , Int , Double , Bool e qualquer tipo que esteja em conformidade com HTTPRouteParameterValue podem ser extraídos.
HTTPResponse pode alternar a conexão com o protocolo Websocket, provocando um WSHandler dentro da carga útil da resposta.
protocol WSHandler {
func makeFrames ( for client : AsyncThrowingStream < WSFrame , Error > ) async throws -> AsyncStream < WSFrame >
} WSHandler facilita a troca de um par AsyncStream<WSFrame> contendo os quadros de websocket bruto enviados pela conexão. Embora poderoso, é mais conveniente trocar fluxos de mensagens via WebSocketHTTPHandler .
O Repo FlyingFoxMacros contém macros que podem ser anotados com HTTPRoute para sintetizar automaticamente um 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 ( )As anotações são implementadas via Macros Anexado Se-0389.
Leia mais aqui.
Internamente, o Flyingfox usa um invólucro fino em torno de soquetes BSD padrão. O módulo FlyingSocks fornece uma interface assíncrona de plataforma cruzada para esses soquetes;
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 envolve um descritor de arquivo e fornece uma interface rápida para operações comuns, jogando SocketError em vez de retornar códigos de erro.
public enum SocketError : LocalizedError {
case blocked
case disconnected
case unsupportedAddress
case failed ( type : String , errno : Int32 , message : String )
} Quando os dados não estão disponíveis para um soquete e o ERRNO EWOULDBLOCK é retornado, SocketError.blocked será lançado.
AsyncSocket simplesmente envolve um Socket e fornece uma interface assíncrona. Todos os soquetes assíncronos são configurados com o flag O_NONBLOCK , capturando SocketError.blocked e, em seguida, suspendendo a tarefa atual usando um AsyncSocketPool . Quando os dados estão disponíveis, a tarefa é retomada e AsyncSocket novamente novamente a operação.
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> é o pool padrão usado no HTTPServer . Ele suspende e retomar os soquetes usando seu EventQueue genérico, dependendo da plataforma. Abstraindo kqueue(2) nas plataformas Darwin e epoll(7) no Linux, o pool usa eventos do kernel sem a necessidade de pesquisar continuamente os descritores de arquivo em espera.
O Windows usa uma fila apoiada por um loop contínuo da poll(2) / Task.yield() para verificar todos os soquetes aguardando dados em um intervalo fornecido.
O agrupamento de estruturas sockaddr é agrupado por conformidade com SocketAddress
sockaddr_insockaddr_in6sockaddr_un Isso permite que HTTPServer seja iniciado com qualquer um desses endereços configurados:
// only listens on localhost 8080
let server = HTTPServer ( address : . loopback ( port : 8080 ) )Ele também pode ser usado com endereços UNIX no domínio, permitindo o IPC privado sobre um soquete:
// only listens on Unix socket "Ants"
let server = HTTPServer ( address : . unix ( path : " Ants " ) )Você pode então netcat para o soquete:
% nc -U Ants
Um exemplo de aplicativo de linha de comando flyingfoxcli está disponível aqui.
O Flyingfox é principalmente obra de Simon Whitty.
(Lista completa de colaboradores)