![](https://www.notion.so/images/page-cover/rijksmuseum_jansz_1649.jpg)
https://github.com/apple/swift-nio
iOS 앱 내에 로컬 프록시 서버를 구축하기 위해 방법을 찾아보던 도중, 서버 개발을 위한 SwiftNIO라는 애플의 프레임워크를 알 게 되었습니다.
사용 방법을 위해 README를 읽고 번역해보았습니다. 오역이 있을 수 있습니다.
Swift로 서버를 구축해보고자 하시는 분들께 도움이 되었으면 합니다.
SwiftNIO는 유지 관리 가능한 고성능 프로토콜 서버 및 클라이언트의 신속한 개발을 위한 크로스 플랫폼 비동기, 이벤트 드리븐 네트워크 어플리케이션 프레임워크.
Netty와 비슷하지만, Swift용으로 작성됨.
개요
- SwiftNIO는 기본적으로 Swift에서 고성능 네트워킹 어플리케이션을 구축하기 위한 도구.
- 특히 thread-per-connect 동시성 모델이 비효율적이거나 지원 불가능한 사례를 대상으로 한다.
- 이는 HTTP 서버와 같이, 상대적으로 utilization이 낮은 연결을 많이 사용하는 서버를 구축할 때 나타나는 일반적인 제한 사항
- thread-per-connect: 네트워크 연결 마다, 스레드를 생성하는 방식What is the difference between thread per connection vs thread per request?Can you please explain the two methodologies which has been implemented in various servlet implementations: Thread per connection Thread per request Which of the above two strategies scales bette...
https://stackoverflow.com/questions/15217524/what-is-the-difference-between-thread-per-connection-vs-thread-per-request
- thread-per-connect: 네트워크 연결 마다, 스레드를 생성하는 방식
- 이를 위해 SwfitNIO는 non-blocking-I/O를 광범위하게 사용
- SwiftNIO는 웹 프레임워크와 같은 고수준의 솔루션을 제공하는 것이 목표가 아니다.
- 대신 SwiftNIO는 이러한 상위 수준 애플리케이션을 위한 하위 수준의 빌딩 블록을 제공하는 게 목표
기본 아키텍처
SwiftNIO의 기본 빌딩 블록은 다음 8가지 타입의 객체가 있다.
- EventLoopGroup: 프로토콜
- EventLoop: 프로토콜
- Channel: 프로토콜
- ChannelHandler: 프로토콜
- Bootstrap: 몇가지 연관된 구조체
- ByteBuffer: 구조체
- EventLoopFuture: 제네릭 클래스
- EventLoopPromise: 제네릭 구조체
EventLoop와 EventLoopGroups
- SwiftNIO의 기본적인 I/O 원시는 EventLoop
- event loop는 이벤트(data received와 같이 보통 I/O 와 관련된 이벤트)가 발생하기를 기다린 다음, 발생 시 일종의 콜백을 실행하는 객체
- 거의 모든 SwiftNIO 애플리케이션에는 상대적으로 적은 수의 event loop가 존재.
- 일반적으로 애플리케이션이 사용하려는 CPU 코어 당 한두 개만 사용
- 일반적으로 event loop는 애플리케이션의 전체 수명 동안 실행되며 이벤트를 전달하는 끝없는 루프에서 spinning
- event loop들은 event loop groups로 수집된다.
- 이러한 그룹은 이벤트 루프를 중심으로, 작업을 분배하는 메커니즘을 제공
- 예를 들어, 인바운드 연결을 수신할 때, listen 소켓은 하나의 이벤트 루프에 등록
- 하지만 우리는, 해당 listten 소켓에서 허용되는 모든 연결이 동일한 event loop에 등록되는 것을 원하지 않음
- 왜냐하면 그렇게 되면 다른 event loop가 비워져 있음에도 하나의 event loop에 잠재적으로 과부하가 걸릴 수 있기 때문에
- 이러한 이유로 event loop groups는 여러 event loop에 load를 분산시키는 기능을 제공
- 현재 SwiftNIO는 하나의 EventLoopGroup(MultiThreadedEventLoopGroup)과 두개의 EventLoop 구현(SelectableEventLoop, EmbeddedEventLoop)이 존재.
- 프로덕션 애플리케이션의 경우 POSIX 라이브러리를 사용해서 여러 스레드를 생성하고 각 스레드에 하나씩 배치하는 MultiThreadedEventLoopGroup이 존재
- SelectableEventLoop는 file descriptors 로부터 I/O 이벤트를 관리하거나, work를 전송하는 작업을 수행하는 selector를 사용하는 event loop이다.
- 이러한 EventLoop와 EventLoopGroups는 NIOPosix 모듈을 통해 제공.
- 추가적으로 주로 테스팅 목적의 dummy event loop인 EmbeddedEventLoop도 존재 (NIOEmbedded 모듈을 통해 제공)
- EventLoop들에는 여러가지 중요한 속성이 존재
- 가장 중요한 건 SwiftNIO 애플리케이션에서 모든 작업이 수행되는 방식.
- EventLoop 객체들은 거의 다른 SwiftNIO 애플리케이션의 객체들을 소유하고, 이 실행 모델을 이해하는 것은 매우 중요함
Channels, Channel Handlers, Channel Pipelines, and Channel Contexts
- EventLoop들이 SwiftNIO의 동작에 매우 중요하지만, 대부분 사용자는 EventLoopPromise들을 생성하고 작업을 예약하도록 요청하는 것 외에는 실질적으로 상호작용하지는 않는다.
- SwiftNIO 어플리케이션 대부분의 유저는 Channels와 ChannelHandlers와 상호작용하는데 대부분의 시간을 할애할 것.
- 유저와 상호작용 하는 거의 모든 SwiftNIO 프로그램의 file descriptor는 단일 Channel과 관련된다.
- Channel은 이러한 file descriptor를 소유하고, 그것의 생명주기를 관리하는 책임이 있다.
- 또한 그 file descriptor의 Inbound와 outbound 이벤트를 처리할 책임도 있다:
- even loop에 file descriptor에 해당하는 이벤트가 있을 때 마다 해당 file descriptor를 소유한 Channel에 알릴 것이다.
- Channel 그것들 자신은 유용하지 않다. 드물게, socket에서 받거나 보낸 데이터로 아무것도 원하지 않는 애플리케이션이 존재한다.
- 그래서 Channel의 또다른 중요한 부분은 ChannelPipeline이다.
- ChannelPipeline은 ChannelHandler라고 불리는, Channel의 이벤트를 처리하는 객체의 시퀀스다.
- ChannelHandler들은 진행하면서, 순서대로 하나에서 다른 하나로 이러한 이벤트들을 전환하고 변화시키면서 처리한다.
- 이것들을 데이터 처리 파이프라인으로 생각할 수 있다. 그래서 이름이 ChannelPipeline이다.
- 모든 ChannelHandler들은 Inbound와 Outbound 핸들러 이거나, 둘다 이다.
- Inbound 핸들러들은 inbound 이벤트를 처리한다.
- inbound 이벤트는 socket으로부터 데이터를 읽거나
- socket close나 를 원격 peer들에 의해 발생한 이벤트의 다른 종류들 읽거나 하는 이벤트들을 말한다.
- Outbound 핸들러들은 outbound 이벤트를 처리한다.
- writes나, 접속 시도, local socket close같은 이벤트를 말한다.
- 각 핸들러는 event들을 순서대로 처리한다.
- 예를 들어, write 이벤트 들이 pipeline의 뒤에서부터 앞으로 전달되는 반면, read 이벤트들은 한번에 한 handler씩, pipeline의 앞부터 뒤로 전달된다.
- 각 handler는 언제든지 적절한 방향으로 다음 handler로 전달될 inbound 혹은 outbound 이벤트를 생성할 수 있다.
- 이를 통해 handler는 read를 분할하고, write를 통합하고, 연결 시도를 지연하고, 일반적으로 이벤트의 임의 전환을 수행할 수 있다.
- 일반적으로 ChannelHandler는 재사용 가능성이 높은 구성 요소로 설계되었다.
- 이 말은, 한 특정 데이터 변환의 수행하면서, 가능한 한 작게 디자인 되도록 의도했다는 것이다.
- 이는 handler를 새롭고 유연한 방식으로 함께 구성할 수 있어 코드 재사용 및 캡슐화에 도움이 된다.
- ChannelHandler는 ChannelHandlerContext를 사용하여, 자신이 ChannelPipeline에 있는 위치를 추적 할 수 있다.
- 이 객체들은 pipeline에서 이전과 다음 channel handler의 참조를 포함해서, 항상 ChannelHandler가 pipeline에서 유지되는 동안 event를 방출하는 것을 보장한다.
- SwiftNIO에는 HTTP 파싱과 같은 기능적으로 유용한 ChannelHandler가 많이 적재되어 있다.
- 추가적으로, 고성능 어플리케이션은 Context Switching을 동반하는 문제를 방지하는 데 도움이 되므로, 최대한 많은 로직을 Channelhandler에서 제공하는 것을 원할 것 입니다.
- SwiftNIO는 몇몇 Channel 구현을 적재하고 있습니다.
- 특히 inbound 연결을 accept하는 ServerSocketChannel, TCP 연결을 위한 Channel인 SocketChannel, UDP 소켓을 위한 DatagramChannel을 적재하고 있습니다.
- 이러한 모든 것들은 EmbddedChannel을 제공하는 NIOPosix 모듈을 통해 제공됩니다.
A Note on Blocking
- ChannelPipeline에 관하여 중요한 것 중 하나는 그들은 thread-safe 하다는 것.
- 이것은 SwiftNIO 어플리케이션을 작성하는 데 매우 중요하고, 이로 인해 synchronize의 없이 ChannelHandler 들을 더 간단하게 작성할 수 있게 한다.
- 그러나 이는 모든 ChannelPipeline의 코드가 EventLoop로 같은 스레드에 보내지는 걸로 이뤄진다.
- 이 말은, 일반적으로, ChannelHandler는 background 스레드로 보내지 않는 blocking code를 호출하면 안된다.
- 만약, ChannelHandler가 어떠한 이유로 block 된다면, 모든 부모 EventLoop에 붙은 Channel들은 blocking 호출이 완료될 때 까지 진행할 수 없게 된다.
- 이는 일반적으로 SwiftNIO 어플리케이션을 작성하면서 고려할 사항.
- 만약 코드를 blocking 스타일로 작성하는게 유용하다면, pipeline에서 작업을 마치면 다른 스레드로 작업을 전송시키는 게 좋다.
Bootstrap
- EventLoop들에 Channel을 직접 설정하고, 등록하는게 가능하지만, 일반적으로 이 작업을 처리하기 위해서는 고수준의 추상화를 사용하는 게 더 유용하다.
- 이러한 이유로, SwiftNIO는 channel들의 생성을 선형으로 연결하는 목적의 몇가지 Bootstrap 객체를 적재하고 있다.
- 일부 BootStrap 객체는 또한 TCP 연결 시도를 위한 Happy Eyeballs 지원과 같은 다른 기능도 제공한다.
- 현재 SwiftNIO는 NIOPosix 모듈 안에 3가지 Bootstrap 객체를 적재하고 있다:
- channel listening을 위한 ServerBootstrap
- TCP 채널 클라이언트를 위한 ClientBootstrap
- UDP 채널을 위한 DatagramBootstrap
ByteBuffer
- SwiftNIO 어플리케이션 내의 작업의 다수는 Byte 단위의 버퍼를 뒤섞는 것.
- 적어도, Byte단위의 버퍼 형태로 네트워크와 data를 주고 받는다.
- 그렇기에, SwiftNIO 어플리케이션이 수행하는 작업 종류의 최적화된 고성능 데이터 구조를 가지는 게 중요하다.
- 이러한 이유로, SwiftNIO는 SwiftNIO 어플리케이션 대부분의 핵심 building 블록을 형성하는 빠른 copy-on-right bytebuffer인 ByteBuffer를 제공한다.
- 이 타입은 NIOCore 모듈로 제공된다
- ByteBuffer는 몇가지 유용한 기능을 제공하고, 추가적으로 unsafe 모드에서 사용할 수 있는 몇가지 hook들을 제공한다.
- 이렇게 하면 메모리 정확성 문제까지, 응용 프로그램을 여는 비용으로 성능이 향상되는지 확인하는 경계를 허문다.
- 일반적으로, 항상 safe mode에서 ByteBuffer를 사용하는 것이 매우 권장된다.
Promises and Futures
- 동시성과 동기 코드를 작성하는 것의 주된 차이점은, 즉시 모든 action들이 완료되지 않을 것이라는 것.
- 예를 들어, channel에 데이터를 작성할 때, Event loop가 즉시 해당 write를 네트워크로 flush하지 못할 수 있다.
- 이러한 이유로, SwiftNIO는 EventLoopPromise<T>와 EventLoopFuture<T>를 제공하여 비동기적으로 완료 되는 작업을 관리한다.
- 이러한 타입은 NIOCore 모듈에서 제공한다.
- EventLoopFuture<T>는 미래에 특정 시점에 채워질 함수의 리턴값을 위한 컨테이너이다.
- 각 EventLoopFuture는 상응하는 결과가 넣어질 객체인 EventLoopPromise<T>을 가진다.
- promise가 성공하면, future가 채워진다.
- future를 poll하여 완료 시점을 탐지해야 한다면, 상당히 비효율적일 것이므로, EventLoopFuture<T>는 관리되는 callback들을 가질 수 있도록 디자인 되었다.
- 기본적으로, 결과가 가능해질 때, 실행될 미래의 callback을 중단할 수 있다.
- EventLoopFuture<T>는 이러한 callback이 처음에 promise를 생성한 event loop에서 항상 실행되도록 스케줄링을 신중하게 조정해서 event loop future 콜백을 중심으로 너무 많은 동기화를 필요로 하지 않도록 한다.
- 고려해야 할 또다른 주제는 닫히는 작업으로 전달된 promise가 Channel에서의 closeFuture과 어떻게 다르게 동작하는지다.
- 예를 들어, 닫히는 작업으로 전달된 promise는 Channel이 닫힌 이후, ChannelPipeline이 완전히 지워지기 전이지만, 전달된 promise는 성공할 것이다.
- 이를 통해, 필요한 경우 channelpipeline이 완전히 지워지기 전에 조치를 취할 수 있다.
- 추가적인 조치 없이 channel이 닫히고, channelpipeline까지 지워질 때까지 기다리려면 closeFuture가 succeed할 때까지 기다리는 것이 더 나은 방법일 것이다.
Uploaded by N2T