자바

자바 NIO TCP 블로킹 채널

알통몬_ 2017. 4. 18. 09:41
반응형


안녕하세요 알통몬입니다.

공감 및 댓글은 포스팅 하는데 아주아주 큰 힘이 됩니다!!

포스팅 내용이 찾아주신 분들께 도움이 되길 바라며

더 깔끔하고 좋은 포스팅을 만들어 나가겠습니다^^

 


TCP 블로킹 채널 :

NIO 를 이용해 TCP 서버와 클라이언트 애플리케이션을 개발하려면 블로킹, 넌블로킹, 비동기 구현 방식 중 하나를

골라야 합니다. 이 결정에 따라 구현이 완전히 달라집니다.

복잡해진 부분이 없지 않지만, 네트워크 입출력의 성능과 효율성 면에서 선택 폭이 넓어졌기 때문에

최적의 네크워크를 개발할 수 있게 되었씁니다.


서버 소켓 채널과 소켓 채널의 용도 :

NIO 에서 TCP 네트워크 통신을 위해 사용하는 채널

- java.nio.channels.ServerSocketChannel 은 클라이언트 SocketChannel 의 연결 요청을 수락하고

통신용 SocketChannel 을 생성합니다.

- java.nio.channels.SocketChannel 


위의 두 채널은 IO의 ServerSocket 과 Socket에 대응되는 클래스입니다.

IO 가 버퍼를 지원하지 않고 블로킹 입출력 방식만 지원한다면 위의 두 채널은

버퍼를 이용하고 블로킹 방식, 넌블로킹 방식을 모두 지원합니다.


서버 소켓 채널의 생성과 연결 수락 :

서버 개발을 위해서 ServerSocketChannel 객체를 얻어야 합니다.

ServerSocketChannel 은 open() 정적 메소드로 생성하고 블로킹 방식으로 동작시키기려면

configureBlocking(true) 를 호출하면 됩니다.

기본적으로 블로킹 방식으로 동작하지만 넌블로킹 방식과 구분해주기 위해 명시적으로

선언합니다. 포트에 바인딩을 하려면 bind()가 호출되어야 합니다.

매개 값으로는 포트 정보를 가진 InetSocketAddress 객체를 매개 값으로 주면 됩니다.

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.configureBlocking(true);

serverSocketChannel.bind(new InetSocketAddress(5001));


포트 바인딩까지 다 했다면 ServerSocketChannel 은 클라이언트 연결 수락을 위해 accept()를 실행해야 합니다.

accept()는 클라이언트가 연결을 요청하기 전까지 계속 블로킹되므로 UI / 이벤트를 처리하는 스레드에서는

accept()를 호출하지 않도록 해야 합니다. 클라이언트가 연결 요청을 하면 accept()는 클라이언트와

통신할 SocketChannel 을 만들고 리턴합니다.

SocketChannel socketChannel = serverSocketChannel.accept();

연결된 클라이언트의 IP 와 포트 정보를 알고 싶다면 SocketChannel 의 getRempteAddress()를 호출해

SocketAddress 를 얻으면 됩니다. 실제 리턴되는 것은 InetSocketAddress 인스턴스입니다.

타입 변환 방법

InetSocketAddress socketAddress = (InetSocketAddress) socketChannel.getRemoteAddress();

연결 수락 예제)

import java.io.IOException;

import java.net.InetSocketAddress;

import java.nio.channels.ServerSocketChannel;

import java.nio.channels.SocketChannel;


public class Example {

 public static void main(String[] args) {

  ServerSocketChannel serverSocketChannel = null;

  try {

   serverSocketChannel = ServerSocketChannel.open();

   serverSocketChannel.configureBlocking(true);  

   serverSocketChannel.bind(new InetSocketAddress(5001));

   while(true) {

    System.out.println( "[[연결 기다림]]");

    SocketChannel socketChannel = serverSocketChannel.accept();

    InetSocketAddress inetSocketAddress = (InetSocketAddress) socketChannel.getRemoteAddress();

    System.out.println("[[연결 수락함]] " + inetSocketAddress.getHostName());

   }

  } catch(Exception e) {}

  

  if(serverSocketChannel.isOpen()) {

   try {

    serverSocketChannel.close();

   } catch (IOException e1) {}

  }

 }

}



소켓 채널의 생성과 연결 요청 :

클라이언트가 서버에 연결 요청을 할 때에는 java.nio.channel.SocketChannel 을 사용합니다.

SocketChannel은 open() 정적 메소드로 생성하고, ServerSocketChannel 과 마찬가지로

넌블로킹 방식과 구분해주기 위해 명시적으로 configureBlocking(true)를 호출해줍니다.

서버에 연결 요청은 connect() 를 호출하여 하고, 서버 IP 와 포트 정보를 가진 InetSocketAddress 객체를

매개 값으로 주면됩니다. connect() 는 연결이 완료될 때까지 블로킹되고 연결이 완료되면 리턴합니다.

로컬 PC 의 5001 포트에 바인딩된 서버에 연결 요청하는 코드는 아래와 같습니다.

SocketChannel socketChannel = SocketChannel.open();

socketChannel.configureBlocking(true);

socketChannel.connect(new InetSocketAddress("localhost", 5001)); 

connect()는 클라이언트가 연결 요청을 하기 전까지 블로킹되기 때문에 UI 나 이벤트를 처리하는 스레드에서는

connect()를 호출하지 않도록 해야 합니다.

연결 후 클라이언트 프로그램을 종료하거나 필요에 따라 연결을 끊고 싶다면 socketChannel.close()를 호출하면 됩니다.


연결 요청 예제)

import java.io.IOException;

import java.net.InetSocketAddress;

import java.nio.channels.SocketChannel;


public class Example {

 public static void main(String[] args) {

  SocketChannel socketChannel = null;

  try {

   socketChannel = SocketChannel.open();

   socketChannel.configureBlocking(true);

   System.out.println( "[연결 요청]");

   socketChannel.connect(new InetSocketAddress("localhost", 5001));

   System.out.println( "[연결 성공]");

  } catch(Exception e) {}

  

  if(socketChannel.isOpen()) {

   try {

    socketChannel.close();

   } catch (IOException e1) {}

  } 

 }


소켓 채널의 데이터 통신 :

클라이언트가 연결 요청을 하고 서버가 수락을 하게 되면 양쪽 SocketChannel 객체의 read() 와 write()를 호출해

데이터 통신을 할 수 있습니다. 모두 버퍼를 이용하기 때문에 버퍼로 읽거나 쓰는 작업을 해야 합니다.


SocketChannel 의 write()를 사용해 문자를 보내는 코드는

Charset charset = Charset.forName("UTF-8");

ByteBuffer byteBuffer = charset.encode("안녕하세요.");

socketChannel.write(byteBuffer); 


SocketChannel 의 read()를 사용해 문자를 받는 코드는

ByteBuffer byteBuffer = ByteBuffer.allocate(100);

int byteCount = socketChannel.read(byteBuffer);

byteBuffer.flip();

Charset charset = Charset.forName("UTF-8");

String message = charset.decode(byteBuffer).toString();


클라이언트 측 예제

 import java.io.IOException;

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.channels.SocketChannel;

import java.nio.charset.Charset;


public class ClientExample {

 public static void main(String[] args) {

  SocketChannel socketChannel = null;

  try {

   socketChannel = SocketChannel.open();

   socketChannel.configureBlocking(true);

   System.out.println( "[[연결 요청]]");

   socketChannel.connect(new InetSocketAddress("localhost", 5001));

   System.out.println( "[[연결 성공]]");

   

   ByteBuffer byteBuffer = null;

   Charset charset = Charset.forName("UTF-8");

   

   byteBuffer = charset.encode("Hello Server");

   socketChannel.write(byteBuffer);

   System.out.println( "[[데이터 보내기 성공]]");

   

   byteBuffer = ByteBuffer.allocate(100);

   int byteCount = socketChannel.read(byteBuffer);

   byteBuffer.flip();

   String message = charset.decode(byteBuffer).toString();

   System.out.println("[[데이터 받기 성공]]: " + message);

  } catch(Exception e) {}

  

  if(socketChannel.isOpen()) {

   try {

    socketChannel.close();

   } catch (IOException e1) {}

  } 

 }

 

}


서버 측 예제

 import java.io.IOException;

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.channels.ServerSocketChannel;

import java.nio.channels.SocketChannel;

import java.nio.charset.Charset;


public class ServerExample {

 public static void main(String[] args) {

  ServerSocketChannel serverSocketChannel = null;

  try {

   serverSocketChannel = ServerSocketChannel.open();

   serverSocketChannel.configureBlocking(true);  

   serverSocketChannel.bind(new InetSocketAddress(5001));

   while(true) {

    System.out.println( "[[연결 기다림]]");

    SocketChannel socketChannel = serverSocketChannel.accept();

    InetSocketAddress isa = (InetSocketAddress) socketChannel.getRemoteAddress();

    System.out.println("[[연결 수락함]] " + isa.getHostName());

    

    ByteBuffer byteBuffer = null;

    Charset charset = Charset.forName("UTF-8");

    

    byteBuffer = ByteBuffer.allocate(100);

    int byteCount = socketChannel.read(byteBuffer);

    byteBuffer.flip();

    String message = charset.decode(byteBuffer).toString();

    System.out.println("[[데이터 받기 성공]]: " + message);    

    

    byteBuffer = charset.encode("Hello Client");

    socketChannel.write(byteBuffer);

    System.out.println( "[[데이터 보내기 성공]]");

   }

  } catch(Exception e) {}

  

  if(serverSocketChannel.isOpen()) {

   try {

    serverSocketChannel.close();

   } catch (IOException e1) {}

  }

 }

}


데이터를 받기 위해 read()를 호출하면 상대방이 데이터를 보내기 전까지 블로킹 됩니다.

read() 가 블로킹이 해제되고 리턴되는 경우

2번과 3번의 경우에 예외 처리를 해서 SocketChannel을 닫기 위해 close() 호출해야 합니다.

try {

    //2번 경우

   int byteCount = socketChannel.read(byteBuffer);


   // 3번 경우

   if(readByteCount == -1) {

      throw new IOException();

   }

} catch (Exception e) {

      try{ socketChannel.close();} catch(Exception e2) { }

}


이상입니다.

반응형