본문 바로가기
Old Posts/Java

Java NIO - 네트워크 입출력 (SocketChannel, ServerSocketChannel, DatagramChannel)

by A6K 2021. 1. 5.

Java NIO를 이용해서 네트워크 입출력을 구현할 수도 있다. 파일 입출력과 함께 핵심적인 기능이다. Java NIO에서 제공하는 네트워크 관련 채널은 다음과 같다.

  • SocketChannel (TCP 클라이언트)
  • ServerSocketChannel (TCP 서버)
  • DatagramChannel (UDP 통신)

1. SocketChannel 

Java NIO는 SocketChannel을 통해 TCP 연결을 지원한다. SocketChannel을 만드는 방법에는 두 가지가 있다.

  • connect() 메소드로 서버 연결
  • accept() 로 클라이언트 연결
SocketChannel channel = SocketChannel.open();

channel.connect(new InetSocketAddress("localhost", 8080));

다른 채널들처럼 SocketChannel도 open() 메소드를 이용해서 열어준다. 그리고 connect() 메소드를 이용해 서버 소켓에 연결한다. 그러면 TCP 연결이 맺어지게 된다. (다 사용했으면 마지막엔 close()로 닫아줘야한다)

데이터 읽기

ByteBuffer buffer = ByteBuffer.allocate(64);

int bytesRead = channel.read(buffer);

버퍼를 할당하고 소켓에서 데이터를 읽을 수 있다. read() 메소드의 파라미터로 데이터를 읽어서 저장할 버퍼를 넣어주면, 데이터를 읽고, 읽은 데이터의 양을 리턴한다. 만약 -1 이 리턴되었다면 서버와의 연결이 종료되었다는 의미다.

데이터 쓰기

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

ByteBuffer buffer = ByteBuffer.allocate(128);
buffer.clear();
buffer.put(data.getBytes());

buffer.flip();

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

write()를 수행해서 소켓에 데이터를 쓸 때 얼마나 데이터가 쓰여질지 모른다. 때문에 buffer.hasRemaining() 을 반복적으로 수행하면서 모든 데이터가 쓰여질 때까지 반복적으로 실행해야한다.

SocketChannel - 논블로킹 모드

SocketChannel은 AbstractSelectableChannel을 상속하고 있다. 따라서 논블로킹 모드로 사용할 수 있다. 

socketChannel.configureBlocking(false);

SocketChannel의 connect(), read(), write() 메소드를 논블로킹으로 실행할 수 있다.

channel.configureBlocking(false);
channel.connect(new InetSocketAddress("localhost", 8080));

while(! channel.finishConnect() ){
    //채널에 연산 수행   
}

클라이언트가 서버로의 접속을 맺을 때 논블로킹 모드로 수행할 수 있다. connect() 메소드를 실행하면 TCP 연결이 맺어질때까지 기다리지 않고 바로 리턴한다. 이 후, finishConnect() 메소드를 이용해서 연결이 맺어졌는지 비동기적으로 확인할 수 있다.

read()와 write() 메소드 역시 논블로킹 모드로 실행할 수 있다. 대신 아무것도 읽지 않거나 아무것도 쓰지 않은 채로 리턴하게 될 수 있다. 이 경우 read(), write() 메소드의 리턴값이 0이 될 것이다. 논블로킹 모드로 입출력할 때에는 리턴 값을 잘 확인해서 얼마나 입출력이 되었는지를 확인하자.

주의할 점은 SocketChannel을 논블로킹으로 설정할 경우 Selector와 잘 동작하지 않는다는 점이다.

 

2. ServerSocketChannel

Java NIO는 ServerSocketChannel을 이용해서 서버 소켓을 열 수 있다. 서버 소켓은 클라이언트의 연결을 기다렸다가 연결이 맺어지면 위에서 봤던 SocketChannel 객체를 리턴해준다.

ServerSocketChannel serverChannel = ServerSocketChannel.open();

serverChannel.socket().bind(new InetSocketAddress(1234));

while(true){
    SocketChannel clientChannel = clientChannel.accept();

    // 클라이언트 요청 처리
}

우선 open() 메소드를 이용해서 ServerSocketChannel을 연다. 서버 소켓에 포트를 바인딩한다. 이 프로그램이 실행되는 호스트의 1234번 포트를 사용하게 된다.

accept()

포트 바인딩이 끝났으면 accept() 메소드를 호출해서 새로운 클라이언트 연결을 기다린다. 클라이언트가 SocketChannel의 connect() 메소드를 이용해서 서버에 연결을 맺으면, 서버의 accept() 메소드는 그 클라이언트와의 연결을 SocketChannel 객체로 만들어서 리턴한다. accept() 메소드는 새로운 클라이언트의 연결이 도착할 때까지 블럭된다.

서버의 경우 일회성 프로그램이 아닌 데몬으로 만드는 경우가 많기 때문에 while-loop을 이용해 반복적으로 클라이언트 연결을 받고, 클라이언트 요청을 처리하도록 구현한다.

ServerSocketChannel 역시 논블로킹 모드로 설정할 수 있다. 이 경우 accept() 메소드는 블럭되지 않고 null 값을 바로 리턴한다.

3. DatagramChannel

Java NIO는 DatagramChannel을 통해서 UDP 프로토콜을 사용하는 네트워크 통신을 지원한다.

DatagramChannel channel = DatagramChannel.open();

channel.socket().bind(new InetSocketAddress(1234));

UDP에는 커넥션이라는 개념이 없다. 그냥 소켓이 있고 주소가 바인딩되어 있을 뿐이다. 데이터는 상대에게 전달 될 수도 있고 아닐 수도 있다. 순서도 보장되지 않는다.

데이터 읽기

연결이 없기 때문에 소켓에 도착한 데이터를 그저 읽을 뿐이다.

ByteBuffer buffer = ByteBuffer.allocate(64);
buffer.clear();

channel.receive(buffer);

DatagramChannel은 receive() 메소드를 이용해 데이터를 읽어들일 수 있다. 만약 버퍼의 사이즈보다 더 많은 양의 데이터가 채널에 도착한 경우 버퍼보다 큰 데이터는 조용히 버려진다.

데이터 쓰기

send() 메소드를 이용해서 데이터 전송도 가능하다.

String data = "new string data";

ByteBuffer buffer = ByteBuffer.allocate(64);
buffer.clear();
buffer.put(data.getBytes());
buf.flip();

int bytesSent = datagramChannel.send(buf, new InetSocketAddress("localhost", 8080);

UDP에서는 커넥션이 없다. 때문에 send() 메소드에 목적지 주소를 입력해줘야한다. 보내는 주소가 유효하지 않은 주소라고 해도 별다른일은 일어나지 않는다. 그냥 전송이 실패한다. 그거시 UDP니까.

우리는 프로그래머. DRY 원칙을 지켜야한다. Do not Repeat Yourself. 같은 소켓으로 전송할 때 반복적으로 같은 주소를 입력해야하는데, connect() 메소드를 이용해서 생력할 수 있다.

datagramChannel.connect(new InetSocketAddress("localhost", 8080));

int bytesRead = datagramChannel.read(buffer);

// 혹은

int bytesWritten = datagramChannel.write(buffer);

이름은 connect 이지만 연결의 개념은 없다. 대신 read(), write() 메소드를 이용해서 읽을 때, connect() 메소드로 설정해놓은 UDP 소켓과 데이터를 주고 받게 된다.

당연한 말이지만 UDP를 이용해서 통신하기 때문에 메시지가 전송되었음을 보장해주지는 않는다. 데이터의 송수신 순서도 보장되지 않는다.

댓글