FlightFox - это легкий HTTP -сервер, созданный с использованием Swift Anturrency. Сервер использует не блокирующие розетки BSD, обрабатывая каждое соединение в одновременной дочерней задаче. Когда розетка блокируется без данных, задачи приостановлены с использованием общего AsyncSocketPool .
FlyingFox может быть установлен с помощью Swift Package Manager.
ПРИМЕЧАНИЕ: FlyingFox требует Swift 5.9 на XCode 15+. Он работает на iOS 13+, TVOS 13+, WatchOS 8+, MacOS 10.15+ и Linux. Поддержка Android и Windows 10 экспериментальная.
Чтобы установить, используя Swift Package Manager, добавьте это в dependencies: раздел в вашем пакете. Свифт -файл:
. 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.disconnected. Затем сервер должен быть запущен еще раз.
Хандлеры могут быть добавлены на сервер, реализуя 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 Запросы могут быть направлены на WebSocket, предоставляя WSMessageHandler , где обменены парой 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 )
}Сырые кадры WebSocket также могут быть предоставлены.
Несколько обработчиков могут быть сгруппированы с запросами и сопоставлены с HTTPRoute с использованием 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 бросается, когда он не может обрабатывать запрос с любым из его зарегистрированных обработчиков.
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 " ) // falseЗаголовки HTTP могут быть сопоставлены:
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
} Платформы Darwin могут соответствовать телу 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 )
} String , Int , Double , Bool и любой тип, который соответствует HTTPRouteParameterValue можно извлечь.
HTTPResponse может переключить соединение на протокол WebSocket, обеспечивая WSHandler в рамках полезной нагрузки.
protocol WSHandler {
func makeFrames ( for client : AsyncThrowingStream < WSFrame , Error > ) async throws -> AsyncStream < WSFrame >
} WSHandler облегчает обмен парным AsyncStream<WSFrame> содержащим рамы Raw WebSocket, отправленные через соединение. Несмотря на мощные, более удобно обмениваться потоками сообщений через WebSocketHTTPHandler .
Repo 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 обеспечивает кросс -платформный интерфейс Async для этих розеток;
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 и обеспечивает асинхровый интерфейс. Все асинхронные розетки настроены с флагом 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 в зависимости от платформы. Аннотация kqueue(2) на платформах Darwin и epoll(7) на Linux, пул использует события ядра без необходимости постоянного опроса дескрипторов файлов ожидания.
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, позволяя частному МПК через гнездо:
// only listens on Unix socket "Ants"
let server = HTTPServer ( address : . unix ( path : " Ants " ) )Затем вы можете поступить на розетку:
% nc -U Ants
Пример приложения командной строки FlyingFoxCli доступен здесь.
Flyingfox - это прежде всего работа Саймона Уитти.
(Полный список участников)