Java NIO에 대해 알아보자 (1/2)

hj·2021년 5월 11일
3

NIO(New Input Output)

Java NIO는 기존 IO 패키지를 개선하기 위해 나온 패키지다.

그럼 어떤것을 개선 하였을까?
위 그림은 Java에서 IO를 처리하는 전체적인 구조를 보여주는 그림이다.

C/C++처럼 직접 메모리를 관리하고 운영체제 수준의 시스템 콜을 직접 사용 할 수 없기에 커널 버퍼에서 JVM내의 Buffer로 한번 더 데이터를 옮겨주는 과정이 생기면서 발생되는 문제점인 JVM 내부 버퍼로 복사 시 발생하는 CPU연산, GC관리, IO요청에 대한 스레드 블록이 발생하게 되는 현상때문에 효율이 좋지 못한점을 개선하기 위해 나온 패키지이다.

NIO의 핵심 용어

     1. Buffer
         커널에 관리되는 시스템메모리를 직접 사용 할 수 있는 Buffer 클래스

구분 Direct Buffer Non Direct Buffer
사용공간 OS 메모리 JVM 힙 메모리
버퍼 생성 속도 느리다 빠르다
버퍼의 크기 크다 작다
IO 성능 높다 낮다
Use Case 한번 생성하고 재사용 시 빈번하게 생성할 때

     2. Channel
읽기/쓰기를 하나씩 할 수 있는 단방향, 둘다 가능한 양방향을 지원하는 입출력 클래스 네이티브 IO, Scatter/Gather 구현으로 효율 적인 IO 처리 지원
* Scatter/Gather :하나의 데이터 스트림에서 여러개의 버퍼로 한번의 시스템 콜로 읽거나 쓰는 방법

     3. Selector
클라이언트 하나당 스레드 하나를 생성해 처리하기 때문에 스레드가 많이 생성 될 수록 급격한 성능저하를 가졌던 단점을 개선하여 하나의 스레드로 여러 채널을 관리하고 처리할 수 있는 클래스 (Reactor 패턴 활용) * Reactor 패턴 :이벤트에 반응하는 객체를 만들고 이벤트가 발생하면 해당 객체가 반응하여 해당 이벤트에 맞는 핸들러와 매핑시켜서 처리하는 구조

리눅스환경에선 2.6 이상 부터는 SelectorProvider를 통해 epoll을 지원한다. 이전은 poll

그래서 NIO? IO?

데이터 처리
IO는 Stream을 통해 데이터를 읽습니다.
   InputStream input = ...;
   BufferedReader reader = new BufferedReader(new InputStreamReader(input));
    reader.readLine(); // 라인을 읽을때까지 블록이 됨
    ....
Java IO 다이어 그램

NIO는 Channel을 통해 데이터를 읽습니다.
	ByteBuffer buffer = ByteBuffer.allocate(48);
    int bytesRead = inChannel.read(buffer);
    /**
    *	데이터가 지정한 버퍼 크기보다 크다면
    *	버퍼의 데이터가 남아있는지 여러번 검사하는 로직이 필요
    **/
    while(!bufferfull(bytesRead))
    	bytesRead = inChannel.read(buffer);
Java NIO 다이어 그램
NIO는 기본적으로 Buffer을 사용하기에 Stream으로 데이터를 주고 받는 IO에 비해 사전작업(버퍼할당, 처리)에 소요되는 비용이 크다

정리

구분 IO NIO
입출력방식 Stream Channel
버퍼방식 Non-buffer Buffer
비동기방식 X O
Blocking/Non-Blocking Blocking 둘다지원
Use Case 연결 클라이언트가 적고 IO가 큰 경우 연결 클라이언트가 많고 IO가 작은 경우

간단한 NIO Server 코드

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Iterator;

public class NioServer {
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private int port = 5999;

    NioServer() {}

    NioServer(int port) {
        this.port = port;
    }

    private void init() throws IOException {
        selector = Selector.open();
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false); // non-block
        serverSocketChannel.socket().bind(new InetSocketAddress("localhost", port));
    }

    private void accept(SelectionKey key) throws IOException {
        ServerSocketChannel sChannel = (ServerSocketChannel) key.channel();
        SocketChannel socketChannel = sChannel.accept();
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
    }

    private void readMsg(SelectionKey key) throws IOException {
        ByteBuffer buf = ByteBuffer.allocateDirect(1024);
        StringBuffer sbuffer = new StringBuffer();
        SocketChannel socketChannel = (SocketChannel) key.channel();

        socketChannel.configureBlocking(false);
        socketChannel.read(buf);
        buf.flip();

        Charset charset = Charset.forName("UTF-8");
        CharsetDecoder decoder = charset.newDecoder();
        CharBuffer charBuffer = decoder.decode(buf);

        sbuffer = new StringBuffer(charBuffer.toString());
        socketChannel.register(selector, SelectionKey.OP_WRITE, sbuffer);
    }

    private void sendMsg(SelectionKey key) throws IOException {
        StringBuffer sbuffer = new StringBuffer();

        SocketChannel socketChannel = (SocketChannel) key.channel();
        socketChannel.configureBlocking(false);
        sbuffer = (StringBuffer) key.attachment();
        socketChannel.write(ByteBuffer.wrap(sbuffer.toString().getBytes("UTF-8")));
        socketChannel.register(selector, SelectionKey.OP_READ, sbuffer);
    }

    public void startServer() throws IOException {
        init();
        SelectionKey acceptKey  = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        /** SelectionKey.OP_ACCEPT        ServerSocketChannel의 연결 수락 작업
        *   SelectionKey.OP_CONNECT       SocketChannel의 서버 연결 작업
        *   SelectionKey.OP_READ          SocketChannel의 데이터 읽기 작업
        *   SelectionKey.OP_WRITE         SocketChannel의 데이터 쓰기 작업
         **/
        while(acceptKey.selector().select() > 0) {  
            Iterator keyIter = selector.selectedKeys().iterator();

            while(keyIter.hasNext()) {
                SelectionKey selectionKey = (SelectionKey) keyIter.next();
                keyIter.remove();

                if(selectionKey.isAcceptable()) {
                    accept(selectionKey);
                }

                if(selectionKey.isReadable()) {
                    readMsg(selectionKey);
                }

                if(selectionKey.isWritable()) {
                    sendMsg(selectionKey);
                }
            }
        }


    }

    public static void main(String[] args) {
        NioServer nioServer = new NioServer();
        try {
            nioServer.startServer();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }}

다만 위 코드에서 문제가 발생할 것인데

그것은 selector().select() 부분에서 block을 걸고 있기때문이다.

문서를 참고해보면 Selector에서 채널을 등록할 때마다 생성되는 key와 동기화가 되고 select된 key가 작업이 완료될때까지 다른 key들은 기다리게 된다.

해당 문제를 해결하려면 selector().wakeup() 메서드를 활용해 제어 할 수 있다.

NIO2가 Java 1.7에서 등장했다.
이에 따라 비동기를 지원하는 새롭게 등장한 클래스가 있다.
다음글에서 알아보자.

ps. 참고로 NIO2는 파일 관련된 부분이 가장 크게 개선되었지만 이번엔 다루지 않겠다.

* 참고자료

profile
Something Interesting

6개의 댓글

comment-user-thumbnail
2021년 5월 12일

좋은 글 감사합니다 BTS

답글 달기
comment-user-thumbnail
2021년 5월 31일

하트 구걸하지마세요. NCT

1개의 답글
comment-user-thumbnail
2022년 3월 22일

깔끔한 정리감사해요! 댓글남기려고 가입했네용 ㅎㅎ

답글 달기
comment-user-thumbnail
2023년 3월 7일

new io -> (nonblocking io = nio)......

답글 달기
comment-user-thumbnail
2023년 4월 12일

심플서버 짜면서 공부하니까 너무 재밌네요~~ 감사합니다.

답글 달기