java.nio.channels.Selectors에서 입출력을 하고 있는 Socket의 집합 상태를 확인하기 위해 이벤트 통지 API를 사용하는데 그렇기 때문에 클라이언트마다 스레드를 생성할 필요도 없고 필요할 때마다 스레드에게 통지해주기 때문에 비동기적 통신 구조를 갖는다.
다시 말해 하나의 thread가 IO 결과를 지켜보는 것을 담당하고 특정 채널로 그 결과를 추후에 전달하는 구조이다
서버는 bind를 통해 자신의 OS 포트에 연결 후 accept()를 통해 클라이언트 요청을 기다림 (blocking)
클라이언트 또한 bind를 통해 자신의 OS 포트에 연결, connect()를 이용하여 서버측에 연결
서버는 요청을 받은 뒤에 새로운 SocketChannel 생성
이후 상호 통신
서버 앞단의 Selector가 요청에 따라서 알맞는 Channel을 반환
이 하나의 쓰레드를 selector라고 하고
유일하게 blocking되는 쓰레드이다
selector는 Reactor 패턴의 구현체이다
selector는 어느 channel set이 IO event를 가지고 있는지 알려준다.
Selector.select()는 IO 이벤트가 발생한 채널 set을 리턴
return 할 channel이 없다면 계속 block된다.
하나 이상의 채널을 셀렉터에 등록하고 select() 메서드를 호출해서 등록된 채널 중 이벤트 준비가 완료된 하나의 채널이 생길때까지 block됨
Selector와 Channel 간의 관계를 표현해주는 객체
ServerSocketChannel에 selector가 등록되면 key를 준다
Selector가 제공한 Selection Key를 이용해 Reactor는 채널에서 발생하는 IO이벤트로 수행할 작업을 선택
적은 수의 스레드로 더 많은 연결을 처리할 수 있으므로 메모리 관리와 컨텍스트 전환에 따르는 오버헤드 감소
입출력을 처리하지 않을 때는 스레드를 다른 작업에 활용할 수 있다
1. selector라는 이벤트 thread가 socket channel 들과 연결을 맺는다
2. 읽고 쓰는 과정에서 socket이 준비 완료되면 selector에게 이벤트를 알린다
3. selector가 이벤트를 리슨해서 관련 thread에게 알린다
4. thread가 관련 데이터를 버퍼에 쓰거나 버퍼에서 읽는다
논블로킹 네트워크 연결은 작업 완료를 기다릴 필요가 없게 해준다. 완전 비동기 입출력은 이 특징을 바탕으로 한 단계 더 나아간다. 비동기 메서드는 즉시 반환하며 작업이 완료되면 직접 또는 나중에 이를 통지한다.
셀렉터는 적은 수의 스레드로 여러 연결에서 이벤트를 모니터링할 수 있게 해준다.
논블로킹 입출력을 이용하면 블로킹 입출력 방식을 이용할 때보다 더 많은 이벤트를 훨씬 빠르고 경제적으로 처리할 수 있다. 이것은 네트워킹 관점에서 우리가 구축하려는 시스템의 핵심이며, 앞으로 알아보겠지만 네티 설계의 핵심이기도 하다.
Netty는 Reactor 모델의 구현이며 NIO 네트워크 기반 프레임워크
더 나은 처리량, 더 낮은 대기시간, 자원 소비량 감소, 불필요한 메모리 복사 최소화
Netty는 기본적으로 Selector를 멀티플랙서로, Eventloop를 리피터로, PipeLine을 이벤트 프로세서로 사용한다.
Tomcat은 요청 당 하나의 스레드가 동작하는데 netty는 이벤트를 받는 스레드와 다수의 worker스레드로 동작한다
따라서 인바운드와 아웃바운드시 thread 간 추가적인 컨텍스트 스위칭이 불필요하며 이벤트를 처리하는 ChannelHandler간의 동기화가 필요없다
PipeLine에서 책임 체인 모델을 사용하기에 버퍼가 최적화되어 성능이 크게 향상되었다.
ServerSocket
, Socket
ServerSocketChannel
, SocketChannel
Netty의 socket mode나 thread 설정을 쉽게 할 수 있고 Eventhandler도 설정해줄 수 있다.
Netty는 크게 다음과 같은 Component들을 통해 데이터를 처리한다.
기본 입출력작업은 Socket 클래스를 이용
이벤트 루프는 연결의 수명주기 중 발생하는 이벤트를 처리하는 Netty의 핵심 추상화를 정의
Netty의 모든 입출력 작업은 비동기적이다. 이를 위해 Netty는 ChannelFuture를 제공하며, 이 인터페이스의 addListener() 메서드는 작업이 완료되면(성공 여부와 관계없이) 알림을 받을 ChannelFutureListener 하나를 등록한다.
인바운드와 아웃바운드 데이터의 처리에 적용되는 모든 Application 논리의 컨테이너 역할
ChannelHandler의 메서드가 네트워크 이벤트에 의해 트리거
Channel을 통해 오가는 인바운드와 아웃바운드 이벤트를 가로채는 Channelhandler 인스턴스의 체인
ChannelPipeline은 ChannelHandler 체인을 위한 컨테이너를 제공하며, 체인 상에서 인바운드와 아웃바운드 이벤트를 전파하는 API를 정의한다. Channel이 생성되면 여기에 자동으로 자체적인 ChannelPipeline이 할당된다.
Netty는 ChannelInboundHandler와 ChannelOutboundHandler의 구현을 구분하며, 핸들러 간의 데이터 전달이 동일한 방향으로 수행되도록 보장
netty에서는 한 connection 안에서 발생하는 inbound, outbound 데이터 흐름을 핸들링할 수 있는 Handler들이 체인 형식으로 파이프라인을 형성하고 있다.
이때 한 connection에서 발생하는 다양한 이벤트들을 하나의 Eventloop 즉 하나의 쓰레드가 처리하는데 이때 이벤트는 각 handler들의 액션을 말한다. 예를 들어 어떤 Handler가 데이터를 넘겨받는 것도 이벤트고, handler 내에 buffer에 데이터를 쓰는 것도 이벤트이다.
여러 쓰레드에서 handler의 이벤트를 처리하게 한다면 thread-safe 해야하고 로직 순서가 보장되지 않는 문제가 있어서 보통 하나의 thread connection을 사용한다.
이벤트를 실행하기 위한 무한루프 스레드
이벤트 큐에 이벤트를 등록하고 이벤트 루프가 큐에 접근하여 처리
이벤트 루프가 다중 스레드면 한 큐를 여러 스레드에 공유해서 쓰면 이벤트 실행순서 불일치 문제가 발생하여 이벤트 큐를 공유하지 않고 이벤트 루프 쓰레드 내부에 둔다
네티의 NioEventLoop 자체는 SingleThreadEventExecutor이므로 NioEventLoop의 시작은 실제로 NioEventLoop에 바인딩 된 로컬 java 스레드의 시작이다.
인스턴스의 실행 메서드는 NioEventLoop에 의해 실행되도록 예약된 작업 대기열에 작업을 추가한다
일반적으로 이벤트 루프는
1. Channel로부터 발생한 Event를 받아 IO
2. taskQueue에 들어있는 Task를 처리하는 non IO
두 가지 작업을 담당
startegy에는 다음 단계에서 어떤 프로세스로 실행되어야 하는지 결정하는데 세 가지 종류가 있다
select
: blockingIO가 실행되어야 할 때, 실행해야 할 task는 없기때문에 대기 상태
continue
: IO loop가 blocking IO를 실행하지 않고 다시 실행되어야할 때
busy_wait
: blocking 없이 새로운 event를 Polling
hasTask가 true면 selectSupplier를 호출해서 event count를 얻어온다
hasTasks가 false면 처리할 task가 없기때문에 blocking상태로 event를 기다린다
네티는 Java NIO의 Selector.select를 지속적으로 호출하는데 사용되는 Selector 스레드가 있다. 그리고 현재 준비된 IO이벤트가 있는지 쿼리한다.
1. selector.open()을 통해 selector를 열고
2. 채널을 선택기에 등록하고 모니터링해야하는 이벤트를 설정한다 (관심 설정)
3. 아래 과정을 반복
1) 스레드 실행 루프 : SingleThreadEventExecutor.this.run() 호출 (NioEventLoop이 구현)
run 이 실행되면 shutdown 시그널을 받기 전까지 무한 loop를 돈다
run 메서드의 첫번째 단계: hasTasks()로 현재 작업 대기열에 작업이 있는지 확인
작업 대기열이 비어있지 않은 경우 selector.selectNow() 메서드 호출
selector.selectNow() : 현재 준비된 IO이벤트가 있으면 반환, 없으면 0
select() : selector.select(timeoutMilis) 호출, 해당 호출은 스레드를 block
hasTask가 true인 경우 전달받은 selectSupplier를 호출해서 event Count를 얻어온다
hasTask가 false인 경우에는 blocking 상태로 event를 기다린다
이 select 메서드에서 selector.select(timeoutMilis)가 호출되면 이 호출은 현재 스레드를 차단하고 timeoutMilis는 차단 타임아웃을 의미한다. hasTasks() 가 true면 selectNow()메서드는 현재 스레드를 블로킹하지 않는다
hasTasks() -> false면 oldWakenUp 호출시에 현재 스레드를 블로킹
작업 대기열에 작업이 없으면 네티는 IO 준비 이벤트를 블로킹하고 기다린다
작업이 있으면 non-blocking selectNow() 메서드를 호출해서 taskQueue의 작업을 처리한다.
https://hbase.tistory.com/116
https://narup.tistory.com/118
https://velog.io/@monami/Netty
https://effectivesquid.tistory.com/65
https://robin00q.tistory.com/81?category=978531
https://sightstudio.tistory.com/15
https://woooongs.tistory.com/73?category=1009976