2. TCP 넌블로킹 채널
넌블로킹 방식의 특징
- 자바는 블로킹 방식으로 인한 스레드 증가의 해결책으로 스레드 풀을 사용하였고, 다른 해결책으로 넌블로킹 방식을 지원하고 있다.
- 넌블로킹 방식은 connect(), accept(), read(), write() 메소드에서 블로킹이 없다.
- 블로킹되지 않고 바로 리턴하기 때문에 while를 통해서 계속적으로 connect(), accept(), read(), write() 메소드 실행하고 있어야함
그러나 이러한 방법은 CPU를 과도하게 소비하는 문제점이 있다.
- 이를 해결하기 위해 넌블로킹은 이벤트 리스너 역할을 하는 셀렉터(Selector)를 사용한다.(변화가 생기면 감지한다)
- 넌블로킹 채널에 Selector를 등록해 놓으면 클라이언트의 연결 요청이 들어오거나 데이터가 도착할 경우, 채널은 Selector에 통보한다.
- Selector는 통보한 채널들을 선택해서 작업 스레드가 accept() 또는 read()메소드를 실행해서 즉시 작업을 처리하도록 한다.
※Selector는 멀티 채널의 작업을 싱글 스레드에서 처리할 수 있도록 해주는 멀티플렉서(multiplexor)역할을 한다.
1. 채널은 Selector에 자신을 동록할때 작업 유형을 키(SelectionKey)로 생성하고, 관심키셋에 저장시킨다.
2. 클라이언트가 처리 요청
3. Selector는 관심키셋에 등록된 키 중에서 작업 처리 준비가 된 키를 선택된 키셋에 별도로 저장한다.
4. 작업 스레드가 선택된 키셋에 있는 키를 하나씩 꺼내어 키와 연관된 채널 작업을 처리하게 된다.
- 작업 스레드가 선택된 키셋에 있는 모든 키를 처리하게 되면 선택된 키셋은 비워지고, Selector는 다시 관심키셋에 작업 처리 준비가 된 키들을 선택해서 선택된 키셋을 치운다.
- 작업 스레드를 스레드풀로 사용하여 작업 스레드 수를 증가 시킬 수 있다.
◆ 셀렉터 생성과 등록
- Selector는 정석 메소드 open()메소드를 호출하여 생성한다.
셀렉터 생성 방법
try{
Selector =selector = Selector.open();
}catch(IOException e){ // open()메소드는 IOException 예외를 발생 시킬 수있다.
- Selector에 등록할 수 있는 채널은 SelectableChannel의 하위 채널만 가능
- ServerSocketChannel, SocketChannel은 SelectableChannel의 하위 클래스
※ 주의점 : 채널이 넌블로킹으로 설정된 것만 가능
셀렉터 등록 전 서버 소켓 채널, 소켓 채널 넌블로킹 설정
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //서버 소켓 채널 생성
serverSocketChannel.configureBlocking(false); //넌블로킹 설정
SocketChannel socketChannel = SocketChannel.open(); //소켓 채널 생성
socketChannel.configureBlocking(false); //넌블로킹 설정
셀럭터 등록 - register()메소드를 이용하여 Selector에 등록
SelectionKey selectionKey = serverSocketChannel.register(Selector sel, int ops);
SelectionKey selectionKey = socketChannel.register(Selector sel, int ops);
- 첫 번째 매개값 : Selector
- 두 번째 매개값 : 채널의 작업 유형
두 번째 매개값 - 채널의 작업 유형별 SelectionKey의 상수들
- register()는 채널과 작업 유형 정보를 담고 있는 SelectionKey를 생성하고 Selector의 관심키셋에 저장한 후 해당 SelectionKey리턴
ex. ServiceSocketChannel의 register()
SelectionKey selectionKey = serverSocketCannel.register(selector, SelectionKey.OP_ACCEPT);
//ServerSocketChannel은 클라이언트 연결 수락 작업을 하므로 OP_ACCEPT 지정
ex. SocketChannel의 register()
SelectionKey selectionKey = SocketCannel.register(selector, SelectionKey.OP_CONNECT); // 서버 연결
SelectionKey selectionKey = SocketCannel.register(selector, SelectionKey.OP_READ); //읽기 작업
SelectionKey selectionKey = SocketCannel.register(selector, SelectionKey.OP_WRITE); //쓰기 작업
※주의점 : 동일한 SocketChannel로 두 가지 이상의 작업 유형을 등록할 수 없다. 즉 register()를 두번 이상 호출 불가
- 채널의 작업 유형이 변경되면 이미 생성된 SelectionKey를 수정해야함
- SelectionKey는 작업 유형 변경, 첨부 객체 저장, 채널 등록 취소 등을 할 때 사용
그러나 별도로 관리할 필요없다 -> 채널이 Selector를 등록하면 채널의 KeyFor()메소드로 SelectionKey를 언제 든지 가져옴
채널로 SelectionKey 가져오는 방법
SelectionKey key = socketChannel.keyFor(selector);
◆ 선택된 키셋
- Selector를 구동하려면 select()메소드를 호출해야 한다.
- select()는 관심키셋에 저장된 SelectionKey로부터 작업 처리 준비가 되었다는 통보가 올 때까지 대기(블로킹)한다.
- 작업 처리 준비가 되었다는 뜻 : 소켓 채널의 read() 사용하려면 상대측에 소켓 채널의 write()를 해야함
- 최소한 하나의 SelectionKey로부터 작업 처리 준비가 되었다는 통보가 오면 리턴.
- 리턴값은 통보를 해온 SelectionKey의 수
select() 메소드 종류
※ 이 메소드는 블로킹되므로 별도의 작업 스레드를 생성하여 실행한다.
select()메소드가 리턴하는 경우
1. 채널이 작업 처리 준비가 되었다는 통보를 할때
2. Selector의 wakeup()메소드를 호출할때
3. select()를 호출한 스레드가 인터럽트할때
- SocketChannel은 상황에 따라서 읽기 작업과 쓰기 작업을 번갈아가며 작업 유형을 변경할 수 있다.
- 작업 유형이 변경되면 SelectionKey의 작업 유형을 interestOps()메소드로 변경해야 작업 스레드가 올바르게 채널 작업을 할 수 있다.
ex. SelectionKey의 작업 유형을 OP_WRITE로 변경하는 코드
selectionKey.interestOps(SelectionKey.OP_WRITE);
selector.wakeup();
- 작업 유형이 변경되면 Selector의 wakeup()메소드를 호출해서 블로킹되어 있던 select()를 즉시 리턴하고, 변경된 작업 유형을 감지하도록 select()를 다시 실행해야 한다.
ex. select()메소드가 1 이상의 값을 리턴 시, selectedKeys()메소드로 작업 처리 준비된 SelectionKey들을 Set컬렉션으로 얻음
int keyCount = selector.select();
if(KeyCount>0){
Set<SelectionKey> selectionKeys = selector.selectedKeys();
}
채널 작업 처리
- 작업 스레드가 해야할 일은 선택된 키셋에 Selectionkey를 하나씩 꺼내어 작업 유형별로 채널 작업을 처리하는 것.
- SelectionKey의 어떤 작업 유형인지 알아내는 방법은 SelectionKey의 다음 메소드 중에서 어떤 것이 true를 리턴하는 조사하면 됨
리턴타입 메소드명(매개 변수) 설명
boolean isAcceptable() 작업 유형이 OP_ACCEPT인 경우
boolean isConnectable() 작업 유형이 OP_CONNECT인 경우
boolean isReadable() 작업 유형이 OP_READ인 경우
boolean isWritable() 작업 유형이 OP_WRITE인 경우
ex.
Thread thread = new Thread(){
@Override
public void run(){
while(true){
try{
int keyCount = selector.select(); //작업 처리 준비된 키 감지
if(keyCount ==0){continue;} //키가 없을 경우 루프 처음으로 돌아감
Set<SelectionKey> selectedKeys = selector.selectedKeys(); //선택된 키셋 Set컬렉션으로 얻기
iterator<SelectionKey> iterator = selectedKeys.iterator();
while(iterator.hasNext()){
SelectionKey selectionKey = iterator.next(); // 키를 하나씩 꺼내옴
if(selectionKey.isAcceptable()){ //연결 수락 작업 처리}
else if(selectionKey.isReadable()) { //읽기 작업처리}
else if(selectionKey.isWritable()) { //쓰기 작업처리}
iterator.remove(); // 선택된 키셋에서 처리 완료된 SelectionKey를 제거
} ....
thread.start();
- 작업 스레드가 채널 작업을 처리하려면 채널 객체가 필요한데, SelectionKey의 channel()메소드를 호출하면 얻을 수 있다.
//OP_ACCEPT일 경우
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
//OP_CONNECT,OP_READ,OP_WRITE 일 경우
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
- 작업 스레드가 채널 작업을 처리하다 보면 채널 객체 이외에 다른 객체가 필요할 수도 있다.
- 이러한 객체는 SelectionKey에 첨부해 두고 사용 가능.
SelectionKey의 attach()메소드 : SelectionKey에 다른 객체를 첨부함
SelectionKey의 attachment()메소드 : 첨부된 객체를 얻는데 사용
ex. Client 객체를 SelectionKey에 첨부하고 얻어내는 코드
[객체 첨부하기]
Client client = new Client(socketChannel);
SelectionKey selectionKey = socketChannel.register(selector, SelectionKey.OP_READ);
selectionKey.attach(client);
[첨부된 객체 얻기] - 새로운 Thread
if(selectionKey.isReadable()){
Client client = (Client)selectionKey.attachment();
'👨🏫Study > JAVA' 카테고리의 다른 글
[JAVA] Java EE (0) | 2022.04.11 |
---|---|
[JAVA] 19. NIO 기반 입출력 및 네트워킹(6) (0) | 2022.04.06 |
[JAVA] 19. NIO 기반 입출력 및 네트워킹(4) (0) | 2022.04.06 |
[JAVA] 19. NIO 기반 입출력 및 네트워킹(3) (0) | 2022.04.06 |
[JAVA] 19. NIO 기반 입출력 및 네트워킹(2) (0) | 2022.04.06 |
댓글