Java NIO는 기존 IO 패키지를 개선하기 위해 나온 패키지다.
그럼 어떤것을 개선 하였을까?
위 그림은 Java에서 IO를 처리하는 전체적인 구조를 보여주는 그림이다.
C/C++처럼 직접 메모리를 관리하고 운영체제 수준의 시스템 콜을 직접 사용 할 수 없기에 커널 버퍼에서 JVM내의 Buffer로 한번 더 데이터를 옮겨주는 과정이 생기면서 발생되는 문제점인 JVM 내부 버퍼로 복사 시 발생하는 CPU연산, GC관리, IO요청에 대한 스레드 블록이 발생하게 되는 현상때문에 효율이 좋지 못한점을 개선하기 위해 나온 패키지이다.
1. Buffer
커널에 관리되는 시스템메모리를 직접 사용 할 수 있는 Buffer 클래스
구분 | Direct Buffer | Non Direct Buffer |
---|---|---|
사용공간 | OS 메모리 | JVM 힙 메모리 |
버퍼 생성 속도 | 느리다 | 빠르다 |
버퍼의 크기 | 크다 | 작다 |
IO 성능 | 높다 | 낮다 |
Use Case | 한번 생성하고 재사용 시 | 빈번하게 생성할 때 |
InputStream input = ...;
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
reader.readLine(); // 라인을 읽을때까지 블록이 됨
....
Java IO 다이어 그램
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
/**
* 데이터가 지정한 버퍼 크기보다 크다면
* 버퍼의 데이터가 남아있는지 여러번 검사하는 로직이 필요
**/
while(!bufferfull(bytesRead))
bytesRead = inChannel.read(buffer);
Java NIO 다이어 그램
구분 | IO | NIO |
---|---|---|
입출력방식 | Stream | Channel |
버퍼방식 | Non-buffer | Buffer |
비동기방식 | X | O |
Blocking/Non-Blocking | Blocking | 둘다지원 |
Use Case | 연결 클라이언트가 적고 IO가 큰 경우 | 연결 클라이언트가 많고 IO가 작은 경우 |
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는 파일 관련된 부분이 가장 크게 개선되었지만 이번엔 다루지 않겠다.
좋은 글 감사합니다 BTS