본문 바로가기

프로그래밍/JAVA 프로그래밍

Java NIO의 ByteBuffer 와 Channel 클래스

Java NIO란?


jdk 1.3 이후 자바IO의 속도를 개선하기 위해 만들어진 클래스들이다. java.nio.channels 패키지는 7개의 인터페이스와 13개의 클래스로 구성되어 있다.


(1) JAVA NIO의 특징

  • 기본 데이터형용 버퍼를 클래스로 제공해 준다.
  • Character-set 인코더들과 디코더.
  • 패턴과 어울린 기능은 Perl-style 정규식들로 설정.
  • 채널, 새로운 I/O 추상화.
  • 메모리 매핑과 파일의 lock(잡금장치)를 지원해주는 인터페이스.
  • non-blocking 입출력이 가능.


(2) 채널(channel)이란?

     서버와 클라이언트간의 통신 수단이다. 파일, 네트워크소켓, 프로그램 컴포넌트간의 입출력 작업을 수행할 수 있는 개방된 연결을 의미한다.

<그림출처: http://blog.naver.com/PostView.nhn?blogId=specialcase&logNo=80092963677>


(3) 채널의 특징

     1. 읽기, 쓰기를 동시에 할 수 있다.

     2. Buffer클래스를 사용하여 데이터형에 맞는 전용 메모리 공간을 가진다.

     3. 블로킹된 스레드를 깨우거나 중단 시킬 수 있다.

     4. 채널은 비동기적으로 닫히거나 중단될 수 있다. 



(4) NIO의 Non-Blocking 입출력 메커니즘

     서버와 클라이언트간의 통신 메커니즘은 Channel클래스로, 클라이언트로 부터 들어오는 클라이언트 요청을 나누어 각 요처에 해당하는 각각의 처리자에게 보내기위한 Selector 클래스를 사용한다. 



(5) java.nio.Channels인터페이스 계층도


<그림출처: http://blog.naver.com/PostView.nhn?blogId=specialcase&logNo=80092963677>

 

- Channel                       : 모든 Channel 클래스류의 최상위 인터페이스.

- InterruptibleChannel      : 채널 입출력 중 다른 스레드에 의해 채널이 중단될 수 있다.

ReadableByteChannel   : 읽기를 위한 인터페이스이다.

- WritableByteChannel     : 쓰기를 위한 인터페이스이다.   
ScatteringByteChannel  : 모아서 쓰기 위한 인터페이스
- GatheringByteChannel  : 모아서 읽기 위한 인터페이스
ByteChannel                : 데이터 읽기/쓰기를 지원하는 인터페이스
    

(6) java.nio.channels 클래스

<그림출처: http://blog.naver.com/PostView.nhn?blogId=specialcase&logNo=80092963677>




ByteBuffer 클래스


채널의 기본 입출력 버퍼는 ByteBuffer 이다.

ByteBuffer클래스는 커널 버퍼에 직접 접근할 수 있는 DirectBuffer를 지원한다.

image0

그림출처: http://eincs.net/2009/08/java-nio-bytebuffer-channel/


 

(1) ByteBuffer 생성

ByteBuffer bb1=ByteBuffer.allocate(256);           // 일반 버퍼 생성

ByteBuffer bb1=ByteBuffer.allocateDirect(256);   // 커널버퍼에 직접 접근하는 버퍼 생성

 

(2) ByteBuffer의 네가지 위치 포인터

- position: 현재 읽기나 쓰기를 할 위치를 가르킨다. 읽기나 쓰기가 진행될때 position값도 자동 이동된다.

- limit: 현재 ByteBuffer의 에 읽기/쓰기를 할 수 있는 위치의 한계값

- capacity: ByteBuffer의 용량(항생 버퍼의 마지막을 가르킴)

- mark: 사용자가 지정한 위치.

 

Java NIO: Buffer capacity, position and limit in write and read mode.

 

(3) ByteBuffer의 읽기/쓰기

- get()과 put() 메서드를 이용하여 바이트배열에 읽기/쓰기 수행

  int bytesRead = inChannel.read(buf); //read into buffer.

  int bytesWritten = inChannel.write(buf); //read from buffer into channel.

  buf.put(127);  
  byte aByte = buf.get(); 

 

- getInt()/putInt() 와 같이 타입에 대한 읽기/쓰기 메서드도 제공됨

- order() 메섣로 빅엔디안/리틀엔디안 방식을 지정

 

 

 

(4) ByteBuffer의 주요 메서드들

- rewind() : 버퍼의 데이터를 다시 읽기 위해 position값을 0으로 설정한다.

                   데이터 삭제 X, position = 0, mark = 제거

- flip() : Buffer를 쓰기모드에서 읽기 모드로 스위칭한다. limit = position, position = 0, mark = 제거

- clear() : position을 0으로 설정하고, limit값을 capacity로 설정한다.

                데이터 삭제 X, position = 0, limit = capacity, mark = 제거

- reset() : position값을 mark로 되돌린다.

                데이터 삭제 X, position = mark, (단, mark < position 일때만 가능하면 그외에는 오류 발생)

remaining() : (limit - position) 값 리턴

position() : 현재 position 값 리턴

- position(int pos) : position 지정

limit() : 현재 limit 값 리턴

mark() : 현재 position 을 mark 로 지정

- compact() : 현재 position 부터 limit 의 사이에 있는 데이터들을 buffer의 가장 앞으로 이동시키고,

position은 데이터의 마지막 부분을 가리키고, limit = capacity , mark = 제거

(앞으로 이동시키고 남은 뒤부분 데이터들은 0으로 초기화하지 않기 때문에 쓰레기 데이터가 남음)

 

 

Channel 클래스


Buffer에 있는 내용을 송신/수신한다.

Socket을 통해 들어온 내용을 ByteBuffer에 저장하거나 Buffer의 내용으로 Packet을 생성하여 Socket을 통해 송신 하는 기능을 제공한다.

ServerSocketChannel 이나 SocketChannel 클래스는 Selector를 이용하여 Non-Blocking하게 입출력을 수행할 수 있다.

하지만 FileChannel은 Blocking만 가능하다.

 

 

ServerSocketChannel/ SocketChannel 클래스


1. Channel을 이용한 통신 처리 순서

 

 서버

 클라이언트

 s-1. open()로 ServerSocketChannel 객체 생성

 s-2. bind()로 Port Binding

 s-3. accept()로 클라이언트 연결 대기

 

 

  c-1. open()로 SocketChannel객체 생성

  c-2. connect()로 서버에 접속

 s-4. 클라이언트가 연결되면 해당 클라이언트와

        통신하는 SocketChannel 객체 반환

 

 

 s-5. SocketChannel에 read/write

 c-3. SocketChannel에 read/write

 s-6. close()

 c-4. close()

 


>> Server측 WorkFlow


 



>> Client측 WorkFlow




SocketChannel을 이용한 데이터 송수신예


(1) Opening a SocketChannel

SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://sample.com", 80));


(2) Closing a SocketChannel

socketChannel.close();   

(3) Reading from a SocketChannel

ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = socketChannel.read(buf);

 

First a Buffer is allocated. The data read from the SocketChannel is read into the Buffer.

socket에서 읽은 데이터를 ByteBuffer에 저장. read()는 읽을 바이트 수를 반환한다.

반환값이 -1 이면 end-of-stream(connection종료) 이다.

 

(4) Writing to a SocketChannel

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
    channel.write(buf);
}

 



 Selector 클래스와 SelectionKey 클래스



java.nio.channels.Selector 클래스는 SelectableChannel(채널)들을 관리하는 클래스로서 SelectionKey의 인스턴스로 관리한다. 


java.nio.channels.SelectionKey 클래스는 SelectableChannel 을 선택하는데 기준이 되는 동작(ops)과 부가적인 정보 객체(att)와 이에 연결된 Selector를 함께 표현하는 클래스로 SelectableChannel를 다루는데 있어 기본적인 정보 클래스이다.


[출처] New I/O(java.nio) |작성자 미래













SocketChannel을 이용한 Non-Blocking 모드 통신


SocketChannel을 Non-Blocking Mode로 설정하여 connect(), read(), write()를 비동기로 처리할 수 있다.

 

(1) connect()

socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://sample.com", 80));

while(! socketChannel.finishConnect() ){
    //wait, or do something else...   
}


(2) write()

Non-blocking mode의 write()에는 아무것도 쓰지 않아도 return될 수 있다. 그래서 write() 메소드를 loop안에 쓰기를 체크하도록 작성한다.


(3) read()

Non-blocking mode의  read()는 아무것도 읽지 않아도 return될 수 있다. 그래서 반환되는 int값을 체크하여 처리해야 한다.

 

 

ServerSocketChannel을 이용한 서버 예제



package kr.co.openeg;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class MainClass {

  public final static int PORT = 2345;

  public static void main(String[] args) throws Exception {

    ServerSocketChannel serverChannel = ServerSocketChannel.open();
    SocketAddress port = new InetSocketAddress(PORT);
    serverChannel.socket().bind(port);
    
    while (true) {
      System.out.println("연결 대기중.....");
      SocketChannel clientChannel = serverChannel.accept();

      String response = "Hello " + clientChannel.socket().getInetAddress() + " on port "
          + clientChannel.socket().getPort() + "\r\n";
      response += "This is " + serverChannel.socket() + " on port "
          + serverChannel.socket().getLocalPort() + "\r\n";

      byte[] data = response.getBytes("UTF-8");
      ByteBuffer buffer = ByteBuffer.wrap(data);
      while (buffer.hasRemaining())
        clientChannel.write(buffer);
      clientChannel.close();
      System.out.println("연결종료");
    }
  }
}

ServerSocketChannel과 Selectors를 이용한 서버 예제


package kr.co.openeg;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class MainClass2 {

  private static byte[] data = new byte[255];

  public static void main(String[] args) throws IOException {
    for (int i = 0; i < data.length; i++)
      data[i] = (byte) i;

    ServerSocketChannel server = ServerSocketChannel.open();
    server.configureBlocking(false);

    server.socket().bind(new InetSocketAddress(9000));
    Selector selector = Selector.open();
    server.register(selector, SelectionKey.OP_ACCEPT);

    while (true) {
      selector.select();
      Set readyKeys = selector.selectedKeys();
      Iterator iterator = readyKeys.iterator();
      while (iterator.hasNext()) {
        SelectionKey key = (SelectionKey) iterator.next();
        iterator.remove();
        if (key.isAcceptable()) {
          SocketChannel client = server.accept();
          System.out.println("Accepted connection from " + client);
          client.configureBlocking(false);
          ByteBuffer source = ByteBuffer.wrap(data);
          SelectionKey key2 = client.register(selector, SelectionKey.OP_WRITE);
          key2.attach(source);
        } else if (key.isWritable()) {
          SocketChannel client = (SocketChannel) key.channel();
          ByteBuffer output = (ByteBuffer) key.attachment();
          if (!output.hasRemaining()) {
            output.rewind();
          }
          client.write(output);
        }
        key.channel().close();
      }
    }
  }
}



SocketChannel을 이용한 클라이언트 예제



package kr.co.openeg;

import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;

public class MainClient {

	/**
	 * @param args
	 */
	private final static String HOST="localhost";
	private final static int PORT=2345;

	
	public static void main(String[] args)throws Exception {
		
		SocketChannel channel=SocketChannel.open(new InetSocketAddress(HOST,PORT));
		//channel.configureBlocking(false);
		ByteBuffer buff=ByteBuffer.allocate(100);
		buff.clear();
		channel.read(buff);
		buff.rewind();
		System.out.println(toString(buff));
		channel.close();
		
	}
	
	private static  String toString(ByteBuffer buffer) throws UnsupportedEncodingException {
	        byte[] bytes = new byte[buffer.remaining()];
	        buffer.get(bytes);
	        return new String(bytes, "UTF-8");
	}
	private static  ByteBuffer toByteBuffer(String value) throws UnsupportedEncodingException {
	     return ByteBuffer.wrap(value.getBytes("UTF-8"));
	 } 

}

자바캔의 NIO API 정리



자바 1.4의 새로운 입출력, NIO API 1부 - 버퍼와 채널

자바 1.4의 새로운 입출력, NIO API 2부 - Charset을 이용한 인코딩/디코딩처리

자바 1.4의 새로운 입출력, NIO API 3부 - 논블러킹 I/O와 Selector