Hello, Freakin world!

[채팅앱][서버] EventLoop 스레드 설계 본문

Toy Project/채팅 앱 만들기

[채팅앱][서버] EventLoop 스레드 설계

johnna_endure 2020. 2. 21. 20:33

이 부분은 꽤 골치아픈 부분이었다.

 

우선 아래 사진을 한번 보자.

이벤트 루프 스레드를 만든 이유는 위의 그림처럼 selecter(셀렉터의 객체)의 select() 메서드를 반복 수행하기 위함이다. 

 

(참고 : select() 의 역할은 실행할 수 있는 이벤트가 최소한 하나가 있을 때까지 대기하는 일종의 blockingQueue와 비슷함.)

 

가장 고민됐던 부분은 selector 객체를 어디서 관리하느냐는 문제였다.

고민하기에 앞서, selector 객체는 전역적으로 사용되기 때문에 반드시 공개되어져야 한다.

 

두 가지 선택지가 있다.

1. 이벤트 루프 스레드 내부에서 생성하고 스레드를 시작하기 전에 selector 객체를 초기화해 외부에 공개한다.

2. 외부에서 selector 객체를 관리하고 이벤트 루프 스레드 생성 시에 주입한다.

 

(참고 : 셀렉터 내부의 이벤트 큐는 스레드 세이프하다. javadoc의 Selector 참고.)

 

 

결론적으로 2번을 선택했다. 

1번은 셀렉터 관리 책임이 이벤트 루프 스레드에 전가되므로 별로다.

 

그런데 2번을 구현하다보면 blocking 메서드로 인한 어떤 이슈를 맞이하게 된다.

 

이슈에 대해 설명하기에 앞서 아래는 구현코드다.

import ...

public class Main {
    public static void main(String[] args) throws IOException {
        SocketAddress socketAddress = new InetSocketAddress("localhost", 9000);
        ServerChannelManager manager = new ServerChannelManager(socketAddress);
        ServerSocketChannel serverSocketChannel = manager.getBindedChannel();
        Selector selector = Selector.open();

        EventLoop eventLoop = new EventLoop(selector);
        eventLoop.start();

        //이벤트 등록
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        selector.wakeup();
    }
}

 

import ...

public class EventLoop extends Thread{
    Selector selector;
    EventHandler eventHandler = new EventHandler();
    Logger logger = Logger.getLogger(EventLoop.class.getCanonicalName());

    public EventLoop(Selector selector) {
        this.selector = selector;
    }

    @Override
    public void run() {
        logger.entering("EventLoop", "run");
        while(true) {
            try {
                selector.select();
                logger.info("[서버][이벤트루프] : select() 호출됨.");
            } catch (IOException e) {
                e.printStackTrace();
            }

            Set<SelectionKey> selectionKeySet = selector.selectedKeys();
            selectionKeySet.stream()
                    .filter(key -> key.isAcceptable())
                    .forEach(key -> eventHandler.connect(key));
        }
    }
}

 

import ...

public class EventHandler {

    Logger logger = Logger.getLogger(EventHandler.class.getCanonicalName());

    public void connect(SelectionKey key) {
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
        try {
            SocketChannel socketChannel = serverSocketChannel.accept();
            logger.info("[서버][이벤트핸들러] accept 호출됨.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

EventHandler와 EventLoop 코드를 자세히 살펴볼 필요는 없다.  

그저 '아~ 이런 코드가 있구나' 정도로 넘어가자. 

 

애를 먹었던 부분은 Main 코드 중 이벤트 등록 시점을 달리하면서 나타나는 문제였다.

EventLoop 스레드를 시작하기 전에 selector에 이벤트를 등록한 경우에는 문제없이 작동했지만,

스레드를 시작하고나서 selector에 등록되는 이벤트는 감지하지 못했다.

그 이유는 이벤트 스레드가 select() 메서드로 block되어 있어서 이 변화를 갱신하지 못하기 때문이다.

 

다시 말해서 이벤트큐가 비어있는 상태에서 이벤트 루프 스레드가 block되어 있는 경우엔 이벤트를 이벤트 큐에 보내고

명시적으로 wakeup() 이라는 메서드를 호출해줘야 한다. (자세한 건 select() 의 javadoc을 읽어보자.)

 

후~ 얼추 다중 클라이언트와 연결이 가능한 nio 서버가 탄생했다.

다음은 다시 클라이언트로 돌아가 이 서버를 켜놓고 테스트할 수 있는 단위 테스트를 작성해보자.

 

 

 

 

 

 

 

 

Comments