java.nio 패키지


참고로 nio는 New I/O의 약자. ( non-blocking의 약자인줄 알았는데 아님. )

기존의 java.io 패키지를 대체하기 위해 새롭게 나온 패키지다.

과거 Java I/O는 C/C++과 비교해 매우 많이 느리다는 평가가 많았는데, 이유는 두 가지다.

  1. 커널 버퍼를 직접적으로 핸들링할 수 없다.
  2. blocking I/O다.

 

 

 

기존의 io 패키지에서는 데이터가 흘러가는 통로를 stream이라는 용어를 써서 표현했고,

새로운 nio 패키지에서는 데이터가 흘러가는 통로를 channel이라는 용어를 써서 표현한다.

 

Stream VS Channel

Stream의 특징은 다음과 같다.

  • 읽기/쓰기를 할때 InputStreamOutputStream으로 구분해서 사용했다. ( 단방향 )
  • Stream으로 흘러다니는 데이터의 단위는 기본적으로 Byte 단위다. 데이터를 담기 위해 byte 또는 byte[] 타입을 사용한다. 
  • 읽기/쓰기 작업시 blocking 된다.

 

Channel의 특징은 다음과 같다.

  • 양방향이기 때문에 SocketChannel, FileChannel 등의 객체 하나로 input/output 둘다 가능하다.
  • ByteBuffer 객체를 통해서만 데이터를 읽고 쓸 수 있다. Channel에서 데이터를 읽어오려면 ByteBuffer 객체를 사용해야한다.
  • 읽기/쓰기 작업시 non-blocking 모드로 사용이 가능하다. ( blocking / non-blocking 둘 중 선택해서 사용. )

 

 

 

blocking 방식에서의 서버/클라이언트


서버 예제

public class Server {

    ServerSocketChannel serverSocketChannel = null;

    public static void main(String[] args) {
        
        // ServerSocketChannel을 생성하고, 몇 가지 설정을 한다.
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(true);
        serverSocketChannel.bind(new InetSocketAddress(10000));
        
        while (true) {
            // 블로킹 방식으로 설정했기 때문에, accept() 메서드에서 연결을 받을 때까지 블락된다.
            SocketChannel socketChannel = serverSocketChannel.accept();
            System.out.println("Connected : " + socketChannel.getRemoteAddress());
            
            // 연결된 클라이언트와 입/출력하기.
            Charset charset = Charset.forName("UTF-8");
            
            ByteBuffer byteBuffer = ByteBuffer.allocate(128);
            socketChannel.read(byteBuffer);
            byteBuffer.flip();
            System.out.println("Received Data : " + charset.decode(byteBuffer).toString());
            
            byteBuffer = charset.encode("Hello, My Client !");
            socketChannel.write(byteBuffer);
            System.out.println("Sending Success");
        }
    }
}

 

클라이언트 예제

public class Client {

    SocketChannel socketChannel = null;

    public static void main(String[] args) {
        
        // SocketChannel을 생성하고, 몇 가지 설정을 한다.
        socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(true);
        
        // 서버에 연결하기.
        socketChannel.connect(new InetSocketChannel("localhost", 10000));
        
        // 마찬가지로, 연결된 서버와 입/출력해보기.
        Charset charset = Charset.forName("UTF-8");
        
        ByteBuffer byteBuffer = charset.encode("Hello, Server !");
        socketChannel.write(byteBuffer);
        System.out.println("Sending Success");
        
        byteBuffer = ByteBuffer.allocate(128);
        socketChannel.read(byteBuffer);
        System.out.println("Received : " + charset.decode(byteBuffer).toString());
        
        // 서버와 볼일이 끝났으면, 소켓 닫기.
        if (socketChannel.isOpen()) {
            socketChannel.close();
        }
    }
}

 

 

 

non-blocking 방식에서의 서버/클라이언트


위에서 예를 든 blocking 방식에서는 accept() 메서드를 호출하는 순간 언제 연결될지 모르는 클라이언트를 주구장창 기다려야하고,

read() 메서드 또한 연결된 클라이언트로부터 언제 전송될지 모르는 데이터를 하염없이 기다려야한다.

따라서, 다수의 클라이언트가 걸어오는 연결 요청을 감당하기 위해서 ServerSocketChannel, SocketChannel 하나당 하나의 스레드가 할당된다.

연결된 클라이언트 하나당 스레드 1개가 소모되는 식인데, 다수의 클라이언트 연결 요청에 대비해 스레드 풀을 사용한다.

 

non-blocking 방식에서는 accept(), read() 등의 메서드를 호출하여도 블락이 되지 않는다.

non-blocking 방식이란거 자체가 비동기로 처리하겠다는 뜻이고, 비동기는 이벤트가 들어오면 콜백 함수를 실행시키는 방법으로 구현한다.

당연히 이벤트가 발생했는지 확인하는 이벤트 리스너가 있어야겠지 ?

이를 위해 Selector라는 이벤트 리스너를 제공한다.

non-blocking 모드의 채널에 Selector를 등록해놓으면, 채널은 클라이언트의 연결 요청이 들어왔을 때 Selector에 알려준다.

 

Selector는 멀티 채널의 작업을 싱글 스레드에서 처리할 수 있도록 멀티플렉서(multiplexor)도 제공한다.

 

non-blocking 서버 예제

public class Server {

    private Selector selector = null;
    private List<SelectionKey> room = new ArrayList<>();

    public void initServer() throws IOException {
        selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(10000));

        // 채널에 Selector를 등록한다.
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }

    public void startServer() throws Exception {
        while (true) {
            // 발생한 이벤트가 있는지 확인한다.
            selector.select();

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

            for (SelectionKey key : selectionKeySet) {
                if (key.isAcceptable()) {
                    accept(key);
                }
                else if (key.isReadable()) {
                    read(key);
                }
            }
        }
    }

    private void accept(SelectionKey key) throws Exception {
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();

        SocketChannel socketChannel = serverSocketChannel.accept();

        if (null == socketChannel) {
            return;
        }

        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);

        room.add(socketChannel); // 연결된 클라이언트 추가하기.
        System.out.println(socketChannel.toString() + "클라이언트가 접속했습니다.");
    }

    private void read(SelectionKey key) {
        Charset charset = Charset.forName("UTF-8");
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024); // buffer 생성

        try (SocketChannel socketChannel = (SocketChannel) key.channel()) {
            socketChannel.read(byteBuffer); // 클라이언트 소켓으로부터 데이터를 읽음
        } catch (IOException ex) {
            room.remove(socketChannel);
            log.error(e.getMessage());
        }

        System.out.println("Received Data : " + charset.decode(byteBuffer).toString());
        byteBuffer.clear();
    }
}

 

 

 

참고자료


[1] https://www.slideshare.net/allnewangel/java-nio-23150417

[2] https://homoefficio.github.io/

[3] http://eincs.com/2009/08/java-nio-bytebuffer-channel-file/

 

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

[Java] JVM (Java Virture Machine)  (0) 2020.04.19
[Java] try-with-resources로 쉽게 자원해제하기  (0) 2020.04.17
[Java] Atomic Type  (0) 2020.04.13
[Java] volatile 키워드  (0) 2020.04.13
[Java] synchronized 키워드  (0) 2020.04.13