본문 바로가기
👨‍🏫Study/JAVA

[JAVA] 19. NIO 기반 입출력 및 네트워킹(5)

by 코푸는 개발자 2022. 4. 6.
728x90

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();
728x90

댓글