FlyingFox adalah server HTTP ringan yang dibangun menggunakan konkurensi Swift. Server menggunakan soket BSD yang tidak memblokir, menangani setiap koneksi dalam tugas anak bersamaan. Ketika soket diblokir tanpa data, tugas ditangguhkan menggunakan AsyncSocketPool bersama.
FlyingFox dapat diinstal dengan menggunakan Swift Package Manager.
Catatan: FlyingFox membutuhkan Swift 5.9 pada Xcode 15+. Ini berjalan di iOS 13+, TVOS 13+, WatchOS 8+, MacOS 10.15+ dan Linux. Dukungan Android dan Windows 10 adalah eksperimental.
Untuk menginstal menggunakan Swift Package Manager, tambahkan ini ke bagian dependencies: bagian dalam file paket Anda:
. package ( url : " https://github.com/swhitty/FlyingFox.git " , . upToNextMajor ( from : " 0.20.0 " ) )Mulai server dengan memberikan nomor port:
import FlyingFox
let server = HTTPServer ( port : 80 )
try await server . run ( )Server berjalan dalam tugas saat ini. Untuk menghentikan server, membatalkan tugas yang mengakhiri semua koneksi dengan segera:
let task = Task { try await server . run ( ) }
task . cancel ( )Dengan anggun mematikan server setelah semua permintaan yang ada selesai, sebaliknya ditutup dengan paksa setelah batas waktu:
await server . stop ( timeout : 3 )Tunggu sampai server mendengarkan dan siap untuk koneksi:
try await server . waitUntilListening ( )Ambil alamat mendengarkan saat ini:
await server . listeningAddressCatatan: iOS akan menggantung soket mendengarkan saat aplikasi ditangguhkan di latar belakang. Setelah aplikasi kembali ke latar depan,
HTTPServer.run()mendeteksi ini, melemparSocketError.disconnected. Server kemudian harus dimulai sekali lagi.
Penangan dapat ditambahkan ke server dengan mengimplementasikan HTTPHandler :
protocol HTTPHandler {
func handleRequest ( _ request : HTTPRequest ) async throws -> HTTPResponse
}Rute dapat ditambahkan ke permintaan delegasi server ke penangan:
await server . appendRoute ( " /hello " , to : handler )Mereka juga dapat ditambahkan ke penutupan:
await server . appendRoute ( " /hello " ) { request in
try await Task . sleep ( nanoseconds : 1_000_000_000 )
return HTTPResponse ( statusCode : . ok )
}Permintaan yang masuk dialihkan ke pawang rute pencocokan pertama.
Penangan dapat melempar HTTPUnhandledError jika setelah memeriksa permintaan, mereka tidak dapat mengatasinya. Rute pencocokan berikutnya kemudian digunakan.
Permintaan yang tidak cocok dengan rute yang ditangani menerima HTTP 404 .
Permintaan dapat dialihkan ke file statis dengan FileHTTPHandler :
await server . appendRoute ( " GET /mock " , to : . file ( named : " mock.json " ) ) FileHTTPHandler akan mengembalikan HTTP 404 jika file tidak ada.
Permintaan dapat dialihkan ke file statis dalam direktori dengan DirectoryHTTPHandler :
await server . appendRoute ( " GET /mock/* " , to : . directory ( subPath : " Stubs " , serverPath : " mock " ) )
// GET /mock/fish/index.html ----> Stubs/fish/index.html DirectoryHTTPHandler akan mengembalikan HTTP 404 jika file tidak ada.
Permintaan dapat diproksi melalui URL dasar:
await server . appendRoute ( " GET * " , to : . proxy ( via : " https://pie.dev " ) )
// GET /get?fish=chips ----> GET https://pie.dev/get?fish=chipsPermintaan dapat dialihkan ke URL:
await server . appendRoute ( " GET /fish/* " , to : . redirect ( to : " https://pie.dev/get " ) )
// GET /fish/chips ---> HTTP 301
// Location: https://pie.dev/get Permintaan dapat dialihkan ke Websocket dengan menyediakan WSMessageHandler di mana sepasang AsyncStream<WSMessage> dipertukarkan:
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 )
}Bingkai Websocket mentah juga dapat disediakan.
Beberapa penangan dapat dikelompokkan dengan permintaan dan dicocokkan dengan HTTPRoute menggunakan 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 dilemparkan ketika tidak dapat menangani permintaan dengan penangan terdaftarnya.
HTTPRoute dirancang agar pola cocok dengan HTTPRequest , memungkinkan permintaan diidentifikasi oleh beberapa atau semua propertinya.
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 Rute dapat ExpressibleByStringLiteral memungkinkan literal untuk secara otomatis dikonversi ke HTTPRoute :
let route : HTTPRoute = " /hello/world "Rute dapat mencakup metode tertentu untuk dicocokkan dengan:
let route = HTTPRoute ( " GET /hello/world " )
route ~= HTTPRequest ( method : . GET , path : " /hello/world " ) // true
route ~= HTTPRequest ( method : . POST , path : " /hello/world " ) // falseMereka juga dapat menggunakan wildcard di jalan:
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 " ) // falseRute dapat mencakup parameter yang cocok seperti wildcard yang memungkinkan penangan untuk mengekstrak nilai dari permintaan.
let route = HTTPRoute ( " GET /hello/:beast/world " )
let beast = request . routeParameters [ " beast " ]Trailing Wildcard cocok dengan semua komponen jalur trailing:
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 " ) // trueItem kueri spesifik dapat dicocokkan:
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 " ) // falseNilai item kueri dapat mencakup wildcard:
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 " ) // falseHeader HTTP dapat dicocokkan:
let route = HTTPRoute ( " * " , headers : [ . contentType : " application/json " ] )
route ~= HTTPRequest ( headers : [ . contentType : " application/json " ] ) // true
route ~= HTTPRequest ( headers : [ . contentType : " application/xml " ] ) // falseNilai header bisa menjadi wildcard:
let route = HTTPRoute ( " * " , headers : [ . authorization : " * " ] )
route ~= HTTPRequest ( headers : [ . authorization : " abc " ] ) // true
route ~= HTTPRequest ( headers : [ . authorization : " xyz " ] ) // true
route ~= HTTPRequest ( headers : [ : ] ) // falsePola tubuh dapat dibuat agar sesuai dengan data Badan Permintaan:
public protocol HTTPBodyPattern : Sendable {
func evaluate ( _ body : Data ) -> Bool
} Platform Darwin dapat mencocokkan tubuh JSON dengan NSPredicate :
let route = HTTPRoute ( " POST * " , body : . json ( where : " food == 'fish' " ) ) { "side" : " chips " , "food" : " fish " } Rute dapat mencakup parameter yang disebutkan dalam jalur atau item kueri menggunakan : awalan. Setiap string yang dipasok ke parameter ini akan cocok dengan rute, penangan dapat mengakses nilai string menggunakan 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 )
}Parameter rute dapat diekstraksi dan dipetakan secara otomatis untuk menutup parameter penangan.
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 )
}Permintaan dapat disertakan secara opsional.
handler . appendRoute ( " GET /creature/:name?type=:beast " ) { ( request : HTTPRequest , name : String , beast : Beast ) -> HTTPResponse in
return HTTPResponse ( statusCode : . ok )
} String , Int , Double , Bool dan jenis apa pun yang sesuai dengan HTTPRouteParameterValue dapat diekstraksi.
HTTPResponse dapat mengalihkan koneksi ke protokol WebSocket dengan menyediakan WSHandler dalam muatan respons.
protocol WSHandler {
func makeFrames ( for client : AsyncThrowingStream < WSFrame , Error > ) async throws -> AsyncStream < WSFrame >
} WSHandler memfasilitasi pertukaran sepasang AsyncStream<WSFrame> yang berisi bingkai Websocket mentah yang dikirim melalui koneksi. Meskipun kuat, lebih nyaman untuk bertukar aliran pesan melalui WebSocketHTTPHandler .
Repo FlyingFoxMacros berisi makro yang dapat dianotasi dengan HTTPRoute untuk secara otomatis mensintesis 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 ( )Anotasi diimplementasikan melalui makro terpasang SE-0389.
Baca lebih lanjut di sini.
Secara internal, FlyingFox menggunakan pembungkus tipis di sekitar soket BSD standar. Modul FlyingSocks menyediakan antarmuka async lintas platform ke soket -soket ini;
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 membungkus deskriptor file dan menyediakan antarmuka cepat untuk operasi umum, melempar SocketError alih -alih mengembalikan kode kesalahan.
public enum SocketError : LocalizedError {
case blocked
case disconnected
case unsupportedAddress
case failed ( type : String , errno : Int32 , message : String )
} Ketika data tidak tersedia untuk soket dan kesalahan EWOULDBLOCK dikembalikan, lalu SocketError.blocked dilemparkan.
AsyncSocket hanya membungkus Socket dan menyediakan antarmuka async. Semua soket async dikonfigurasi dengan bendera O_NONBLOCK , menangkap SocketError.blocked dan kemudian menangguhkan tugas saat ini menggunakan AsyncSocketPool . Ketika data tersedia, tugas dilanjutkan dan AsyncSocket akan mencoba lagi operasi.
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> adalah kolam default yang digunakan dalam HTTPServer . Ini menangguhkan dan melanjutkan soket menggunakan EventQueue generik tergantung pada platform. Abstracting kqueue(2) Pada platform Darwin dan epoll(7) di Linux, kumpulan menggunakan peristiwa kernel tanpa perlu terus -menerus polling deskriptor file menunggu.
Windows menggunakan antrian yang didukung oleh loop poll(2) / Task.yield() yang berkelanjutan untuk memeriksa semua soket yang menunggu data pada interval yang disediakan.
Cluster struktur sockaddr dikelompokkan melalui kesesuaian dengan SocketAddress
sockaddr_insockaddr_in6sockaddr_un Ini memungkinkan HTTPServer dimulai dengan alamat yang dikonfigurasi ini:
// only listens on localhost 8080
let server = HTTPServer ( address : . loopback ( port : 8080 ) )Ini juga dapat digunakan dengan alamat domain unix, memungkinkan IPC pribadi di atas soket:
// only listens on Unix socket "Ants"
let server = HTTPServer ( address : . unix ( path : " Ants " ) )Anda kemudian dapat netcat ke soket:
% nc -U Ants
Contoh aplikasi baris perintah flyingfoxcli tersedia di sini.
Flyingfox terutama merupakan karya Simon Whitty.
(Daftar lengkap kontributor)