FlyingFoxは、Swift Concurrencyを使用して構築された軽量のHTTPサーバーです。サーバーは、非ブロックBSDソケットを使用し、同時の子タスクで各接続を処理します。ソケットがデータなしでブロックされると、共有AsyncSocketPoolを使用してタスクが停止されます。
Swiftパッケージマネージャーを使用して、FlyingFoxをインストールできます。
注: FlyingFoxには、Xcode 15+でSwift 5.9が必要です。 iOS 13+、TVOS 13+、WatchOS 8+、MacOS 10.15+、およびLinuxで実行されます。 AndroidとWindows 10のサポートは実験的です。
Swift Package Managerを使用してインストールするには、これをdependencies:パッケージのセクション。swiftファイル:
. 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リクエストは、 AsyncStream<WSMessage>のペアが交換されるWSMessageHandlerを提供することにより、websocketにルーティングできます。
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 )
}Raw WebSocketフレームも提供できます。
複数のハンドラーをリクエストとともにグループ化し、 RoutedHTTPHandlerを使用してHTTPRouteと一致させることができます。
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ルートは、リテラルを自動的にHTTPRouteに変換することを可能にするためにExpressibleByStringLiteralです。
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 " ) // falseHTTPヘッダーは一致させることができます:
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
}ダーウィンのプラットフォームは、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 、応答ペイロード内でWSHandlerを提供することにより、WebSocketプロトコルに接続を切り替えることができます。
protocol WSHandler {
func makeFrames ( for client : AsyncThrowingStream < WSFrame , Error > ) async throws -> AsyncStream < WSFrame >
} WSHandler 、接続上に送信されたRaw Websocketフレームを含むペアAsyncStream<WSFrame>の交換を促進します。強力ですが、 WebSocketHTTPHandlerを介してメッセージのストリームを交換する方が便利です。
Repo FlyingFoxMacrosには、 HTTPHandlerを自動的に統合するためにHTTPRouteで注釈を付けることができるマクロが含まれています。
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モジュールは、これらのソケットにクロスプラットフォームの非同期インターフェイスを提供します。
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をラップし、非同期インターフェイスを提供します。すべてのASYNCソケットは、Flag 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を使用してソケットを一時停止および再開します。 Darwin Platformsとepoll(7)でkqueue(2)を抽象化し、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アドレスで使用することもでき、ソケットを介してプライベートIPCを許可します。
// only listens on Unix socket "Ants"
let server = HTTPServer ( address : . unix ( path : " Ants " ) )その後、ソケットにネットキャットできます。
% nc -U Ants
コマンドラインアプリFlyingFoxCliの例はこちらから入手できます。
FlyingFoxは主にSimon Whittyの作品です。
(貢献者の完全なリスト)