FlyingFox 는 Swift 동시성을 사용하여 구축 된 가벼운 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를 사용하여 설치하려면 Package.swift 파일의 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 wsmessagehandler를 제공하여 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 )
}원시 웹 소켓 프레임도 제공 할 수 있습니다.
여러 핸들러는 요청으로 그룹화하고 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 경로는 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 " ) // 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
} Darwin 플랫폼은 JSON 본체와 NSPredicate 와 일치 할 수 있습니다.
let route = HTTPRoute ( " POST * " , body : . json ( where : " food == 'fish' " ) ) { "side" : " chips " , "food" : " fish " } 경로에는 : prefix를 사용하여 경로 또는 쿼리 항목 내에 명명 된 매개 변수가 포함될 수 있습니다. 이 매개 변수에 제공되는 모든 문자열은 경로와 일치하며 핸들러는 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 연결을 통해 전송 된 원시 웹 소켓 프레임을 포함하는 쌍의 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 감싸고 비동기 인터페이스를 제공합니다. 모든 비동기 소켓은 플래그 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 플랫폼 및 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 " ) )그런 다음 소켓으로 Netcat을 할 수 있습니다.
% nc -U Ants
예제 명령 줄 앱 FlyingFoxCli가 여기에서 제공됩니다.
Flyingfox는 주로 Simon Whitty의 작품입니다.
(기고자의 전체 목록)