FlyingFox เป็นเซิร์ฟเวอร์ HTTP ที่มีน้ำหนักเบาสร้างขึ้นโดยใช้ Swift พร้อมกัน เซิร์ฟเวอร์ใช้ซ็อกเก็ต 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.swift file:
. 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
} แพลตฟอร์มดาร์วินสามารถลวดลายตรงกับร่าง 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> ที่มีเฟรม 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 ใช้ wrapper บาง ๆ รอบซ็อกเก็ต 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 และให้อินเทอร์เฟซ async ซ็อกเก็ต async ทั้งหมดได้รับการกำหนดค่าด้วยธง 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) บนแพลตฟอร์มดาร์วินและ 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 ซึ่งช่วยให้ IPC ส่วนตัวผ่านซ็อกเก็ต:
// only listens on Unix socket "Ants"
let server = HTTPServer ( address : . unix ( path : " Ants " ) )จากนั้นคุณสามารถ netcat ไปที่ซ็อกเก็ต:
% nc -U Ants
ตัวอย่างแอพบรรทัดคำสั่ง FlyingFoxCli มีให้ที่นี่
Flyingfox เป็นงานของ Simon Whitty เป็นหลัก
(รายชื่อผู้มีส่วนร่วมทั้งหมด)