Selector

Programming/Java NIO 2015. 9. 9. 11:52

Selector는 하나 이상의 NIO 채널을 검사하고 준비되어 있는 채널(예를 들어 읽기, 기록)을 결정하는 Java NIO 컴포넌트다. 이 방법은 단일 스레드가 다중 채널과 다중 네트워크 연결을 관리할 수 있다.

Why Use a Selector?


다중 채널 관리에 있어 단일 스레드를 사용하는 것의 장점은 채널을 관리하는 데에 있어 더 적은 스레드들이 필요하다는 것이다. 실제로, 모든 채널을 단 하나의 스레드로 처리 가능하다. 운영체제에서 스레드간의 전환 비용이 높을 뿐만 아니라, 각 스레드는 일부 자원(메모리)을 차지한다. 그러므로, 스레드를 적게 사용할 수록 좋은 것이다.


하지만 현대 사회의 운영체제와 CPU는 멀티태스킹에 있어 점점 더 좋아지고 있으므로, 멀티스레딩의 오버헤드는 시간이 지남에 따라 점점 더 작아질 것이라는 것을 명심하길 바란다. 사실, CPU가 다중 코어를 갖고 있다면 멀티태스킹 하지 않음으로써 CPU 전력을 낭비할 수도 있다. 어쨋든, 이러한 디자인 토론은 다른 텍스트에 해당한다. Selector를 사용하여 단일 스레드에서 다중 채널을 관리하는 것을 말하는 것으로 충분하다.


다음은 하나의 스레드에서 Selector를 사용하여 3개의 Channel들을 처리하는 그림이다:

Java NIO: Selectors

Java NIO: 하나의 스레드에서 셀렉터를 사용하여 3개의 채널을 처리

Creating a Selector


아래와 같이 Selector.open() 메소드를 호출하여 Selector를 생성한다:

Selector selector = Selector.open();

Registering Channels with the Selector


Selector와 Channel의 사용을 위해 Selector와 Channel의 등록을 해야한다. 아래와 같이 SelectableChannel.register() 메소드를 사용하여 실행된다:

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ); 

Channel은 Selector와 사용될 수 있도록 비블럭킹 모드가 되어야 한다. 이는 FileChannel의 비블럭킹 모드로 전환이 되지 않기 때문에 Selector와 FileChannel을 사용할 수 없음을 의미한다. 반면 소켓 채널은 잘 작동한다.


register() 메소드의 두번째 매개변수에 주목하기 바란다. 이는 Selector를 통하여 관심있어하는 채널에 수신하는 "관심 집합"을 의미한다. 네가지 이벤트를 들을 수 있다:

  1. Connect
  2. Accept
  3. Read
  4. Write

"이벤트 발생" 채널은 "준비"라는 이벤트라고도 불린다. 그래서, 한 채널이 또다른 서버에 성공적으로 접속이 되는 것을 "연결 준비"라고 한다. 서버 소켓 채널이 연결이 들어오는 것을 동의하는 것을 "수락" 준비라고 한다. 한 채널이 데이터를 읽기 위한 준비가 되어있는 것을 "읽기" 준비라고 한다. 그곳에 데이터를 기록하기 위한 채널이 준비하는 것을 "기록" 준비라고 한다.


이 네개의 이벤트들은 네개의 SelectionKey 상수로 보여진다:

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

만약 하나 이상의 이벤트에 관심이 있다면, 아래와 같이 OR 상수를 함께 사용한다:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;    

필자는 이 문서 아래에 관심있는 집합들에 대해 돌아볼 것이다.

SelectionKey's


이전 섹션에서 보았듯이,  Selector의 register() 메소드로 Channel을 등록하면 SelectionKey 객체를 반환한다. 이 SelectionKey 객체는 일부 관심있는 속성들을 포함한다:

  • The interest set
  • The ready set
  • The Channel
  • The Selector
  • An attached object (optional)

이러한 속성들은 아래에 설명 할 것이다.

Interest Set


관심 집합은 "Registering Channels with the Selector" 섹션에 설명된 것처럼, "선택한" 것의 관심있는 이벤트들의 집합이다. 아래와 같이 SelectionKey를 통해 관심 집합을 읽고 기록할 수 있다:

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;    

볼 수 있듯이, 특정 이벤트가 관심 집합에 있을 경우 주어진 SelectionKey 상수와 AND로 관심 집합을 찾을 수 있다.

Ready Set


준비 집합은 채널에 대한 준비 작업의 집합이다. 주로 선택 후 준비 집합에 접근할 것이다. 선택은 다음 섹션에 설명되어 있다. 아래와 같이 준비 집합에 접근한다:

int readySet = selectionKey.readyOps();

관심 집합에서와 같은 방법으로 채널의 준비를 위한 이벤트 / 동작에 대해 테스트 할 수 있다. 또한 이러한 네 개의 메소드를 사용할 수 있는 반면, 모두 boolean을 반환 할 것이다:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

Channel + Selector


SelectionKey로부터의 채널 + 셀럭터의 접근은 간단하다. 다음 작업 방법이 있다:

Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector();    

Attaching Objects


SelectionKey의 객체에 접근할 수 있는데, 이는 주어진 채널을 인식하거나, 채널에 더 많은 정보를 접근하는데 편리하다. 예를 들어, 사용중인 채널 버퍼나 총 데이터보다 많은것을 포함하는 객체에 접근할 수도 있다. 다음 객체에 대한 접근 방법을 보여준다:

selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();

register() 메소드에서 Selector와 Channel을 등록하는 동안 이미 객체에 접근 할 수도 있다. 다음 방법과 같다:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

Selecting Channels via a Selector


Selector로 하나 이상의 채널을 등록했다면, select() 메소드 중 하나를 호출 할 수 있다. 이 메소드들은 관심있어하는(연결, 수락, 읽기 혹은 쓰기) 이벤트들이 "준비"된 채널들을 반환한다. 바꿔 말하면, 채널이 읽기 위한 준비에 관심이 있다면 select() 메소드로부터 읽기 위해 준비된 채널을 받게 될 것이다.


다음 select() 메소드들이 있다:

  • int select()
  • int select(long timeout)
  • int selectNow()

select()는 하나 이상 등록한 이벤트의 준비가 이루어질 때 까지 블록된다.


select(long timeout)는 최대 timeout 밀리초 (매개변수) 까지 블록 된다는 것을 제외하면 select()와 동일하다.


selectNow()는 전혀 블록하지 않는다. 이는 무엇이든 준비된 채널들을 즉시 반환한다.


select() 메소드에 의해 반환된 int는 얼마나 많은 채널이 준비되어 있는지를 말해준다. 즉, 마지막에 호출했던 select()로부터 준비 완료된 채널의 수이다. select()를 호출하여 1을 반환 받은 것은 하나의 채널이 준비 되었기 때문이고, 한번 더 select()를 호출하면, 한 개의 채널이 준비가 되고, 그것은 1을 다시 반환할 것이다. 만약 첫번째 채널이 준비 되었을 때 아무것도 하지 않았다면, 이제 2개의 준비된 채널을 가질 것이지만, 한 채널은 각각의 select()를 호출 간격에 준비가 되었을 것이다.

selectedKeys()


select() 메소드들 중 하나를 호출하고 그것의 반환 값이 하나 이상의 준비된 채널을 가리킬 때, 셀렉터들의 selectedKeys() 메소드를 호출하여 "선택된 키 집합"을 통해 준비된 채널들에 접근 할 수 있다. 다음 방법과 같다:

Set<SelectionKey> selectedKeys = selector.selectedKeys();     

Channel.register() 메소드로 Selector에 채널을 등록하면 SelectionKey 객체를 반환한다. 이 키는 셀렉터의 등록 채널을 대표한다. 이것은 selectedKeySet() 메소드를 통해 접근 할 수 있는 키들이다. SelectionKey로 부터 나온다.


준비된 채널들에 접근을 위해 선택된 키 집합을 반복 할 수 있다. 다음 방법과 같다:

Set<SelectionKey> selectedKeys = selector.selectedKeys();

Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

while(keyIterator.hasNext()) {
    
    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
}

선택된 키 집합에서 키들을 반복 순환한다. 각 키는 준비된 키들의 참조된 채널을 결정하기 위해 검사한다.


각 반복의 마지막에서 keyIterator.remove()를 호출하는 것에 주목하기 바란다. Selector는 키 집합에서 스스로 선택된 것으로부터 SelectionKey 객체를 삭제하지 않는다. 다음 채널이 "준비"가 되면 Selector는 그것을 선택된 키 집합에 다시 추가할 것이다.


SelectionKey.channel() 메소드로 반환된 채널은 동작이 필요한 채널(예를 들어 서버소켓채널이나 소켓채널 등)로 캐스팅 되어야만 한다.

wakeUp()


select() 메소드를 호출한 하나의 스레드는 블럭되고, 어떠한 채널도 아직 준비 되지 않았음에도 불구하고 select() 메소드를 나갈 수 있다. 이는 다른 스레드가 첫번째 스레드가 호출한 select()의 Selector에서 Selector.wakeup() 메소드를 호출하여 동작된다. 해당 스레드는 select() 내부에서 기다린 후에 즉시 반환된다.


만약 다른 스레드가 wakeup()을 호출하고 현재 select() 내부에 블럭된 스레드가 없을 경우, 다음 스레드가 select()를 호출하면 즉시 "깨어날" 것이다.

close()


Selector가 모두 종료되었을 때 close() 메소드를 호출한다. 이는 Selector를 닫고 이 Selector에 등록된 모든 SelectionKey 객체들을 새로 갱신한다. 채널들은 스스로 닫히지 않는다.

Full Selector Example


다음은 Selector를 열고, 그것으로(채널 인스턴스는 생략됨) 채널을 등록하고, 네가지(수락, 연결, 읽기, 기록) 이벤트들의 "준비"를 위한 Selector를 모니터링 하는 전체 예제이다.

Selector selector = Selector.open();

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);


while(true) {

  int readyChannels = selector.select();

  if(readyChannels == 0) continue;


  Set<SelectionKey> selectedKeys = selector.selectedKeys();

  Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

  while(keyIterator.hasNext()) {

    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
  }
}



<원문 출처>

'Programming > Java NIO' 카테고리의 다른 글

SocketChannel  (0) 2015.09.10
FileChannel  (0) 2015.09.09
Channel to Channel Transfers  (0) 2015.09.09
Scatter / Gather  (0) 2015.09.08
Buffer  (0) 2015.09.08
Posted by 레미파
,