FlyingFox هو خادم HTTP خفيف الوزن مصنوع باستخدام Swift Concurrency. يستخدم الخادم مآخذ 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 الخاص بك:
. 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> يحتوي على إطارات WebSocket Raw المرسلة عبر الاتصال. على الرغم من أنها قوية ، إلا أنها أكثر ملاءمة لتبادل تدفقات الرسائل عبر 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 واجهة متزامنة منصة منصة لهذه المقابس ؛
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 )
} عندما تكون البيانات غير متوفرة للمقبس ويتم إرجاع erno EWOULDBLOCK ، ثم يتم إلقاء SocketError.blocked .
AsyncSocket يلف ببساطة Socket ويوفر واجهة Async. يتم تكوين جميع مآخذ 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 العامة اعتمادًا على النظام الأساسي. بعد استخلاص kqueue(2) على منصات Darwin و epoll(7) على Linux ، يستخدم المسبح أحداث kernel دون الحاجة إلى استطلاع واصفات ملف الانتظار بشكل مستمر.
يستخدم 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-Nomain ، مما يسمح لـ IPC الخاص على مقبس:
// only listens on Unix socket "Ants"
let server = HTTPServer ( address : . unix ( path : " Ants " ) )يمكنك بعد ذلك netcat إلى المقبس:
% nc -U Ants
تطبيق سطر الأوامر FlyingFoxcli متاح هنا.
Flyingfox هو في المقام الأول عمل سيمون وايت.
(قائمة كاملة من المساهمين)