1. 요약
1.1 Java는 blocking 소켓과 non-blocking 소켓을 제공한다.
1.2 Blocking 소켓은 java.net 패키지에서 제공되고 쓰기 쉽다. 서버 입장에서 보면 멀티-쓰레딩 구조인데 동시 접속자가 적을 것으로 예상되는 소규모의 애플리케이션에선 이걸 써도 무방하다.
1.3 Non-blocking 버전은 java 1.4 부터 지원되었고, java.nio 패키지에서 제공된다. NIO(New I/O?)라고 부른다. Blocking 소켓에 비해 적은 비용으로 많은 클라이언트의 요청을 처리할 수 있다.
1.4 NIO의 ByteBuffer은 사용법이 좀 까다로워서 사용상 주의가 필요하다. Non-blocking 소켓을 써야만 하는 상황이라면 Netty framework이 더 나은 선택지일 수 있다. Netty는 조금 더 나은 성능에 더 쓰기 편한 'ByteBuf'를 제공한다.
2. Original Socket (Blocking)
(클라이언트) 소켓 인스턴스 생성은 아래와 같이 간단하다. 블로킹 방식으로 동작하기 때문에 클라이언트 입장에서 사용하기가 쉽다.
소켓 인스턴스가 서버에 연결되면 서버에 입력, 출력 스트림을 얻을 수 있다. 입력 스트림은 서버에서 데이터를 읽는 데 쓰이고, 출력 스트림은 서버에 데이터를 쓰는데 이용된다.
InputStream와 OutputStream은 일반 스트림이기 때문에 사용하기 편한 형식으로 변환해 사용할 수 있다.
InputStream와 OutputStream은 일반 스트림이기 때문에 사용하기 편한 형식으로 변환해 사용할 수 있다.
서버 측도 어렵진 않다. port를 선택해 listen 하고, accept()를 호출하면 클라이언트 연결이 될 때까지 블로킹 된다. 클라이언트가 서버에 연결되면 accept() 메서드는 서버가 클라이언트와 통신할 수 있는 소켓을 반환한다. 이는 클라이언트에 사용된 것과 같은 소켓 클래스이므로 마찬가지 방식으로 데이터를 읽고 쓸 수 있다.
서버는 클라이언트와 다르게 여러 클라이언트와 통신할 필요가 있으므로 accept()는 while 문 안에 놓고, 연결된 클라이언트가 있으면 이에 대한 핸들러는 별도의 쓰레드에서 동작시키는 구조로 로직을 작성하게 된다. 문제는 클라이언트가 많아질 때이다. 늘어나는 쓰레드는 서버에 부담을 준다. 물론 일반적인 규모의 프로그램에서는 굳이 non-blocking 소켓을 쓸 필요도 없고, 문제없이 동작할 것이다.
public class SocketServer extends Thread { private ServerSocket serverSocket; private int port; private volatile boolean running = false; public SocketServer(int port) { this.port = port; } public void startServer() { try { serverSocket = new ServerSocket(port); this.start(); } catch (IOException e) { e.printStackTrace(); } } public void stopServer() { running = false; this.interrupt(); } @Override public void run() { running = true; while (running) { try { System.out.println("Listening for a connection"); Socket socket = serverSocket.accept(); System.out.println("Client connected."); RequestHandler requestHandler = new RequestHandler(socket); requestHandler.start(); } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) { int port = 12345; SocketServer server = new SocketServer(port); server.startServer(); // Shutdown in 1 minute. try { Thread.sleep(60000); } catch(Exception e) { e.printStackTrace(); } server.stopServer(); } } class RequestHandler extends Thread { private Socket socket; RequestHandler(Socket socket) { this.socket = socket; } @Override public void run() { try { BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter out = new PrintWriter(socket.getOutputStream()); String line = in.readLine(); while(line != null && line.length() > 0) { out.println("Received: " + line); // Echo message out.flush(); line = in.readLine(); } in.close(); out.close(); socket.close(); } catch(Exception e) { e.printStackTrace(); } } }
2. New IO (NIO, non-blocking socket)
non-blocking 버전으로 Java 1.4 이후로 도입되었다. java.nio 패키지는 다음과 같은 주요 기능을 제공한다.
• 버퍼: 간단한 작업 집합으로 인터페이스 된 연속된 메모리 블록을 나타낸다.
• non-blocking I/O: 파일, 소켓과 같은 일반적인 I/O 소스에 '채널'을 연결해주는 역할을 한다.
Java에서 non-blocking 방식으로 통신하려면 우선 목적지(destination)에 대한 '채널'을 연 다음 '버퍼'를 이용해 서로 데이터를 주고 받아야 한다. 버퍼(java.nio.Buffer)엔 읽기/쓰기에 대한 위치(position), 버퍼의 고정 크기(capacity), 버퍼에 쓸 수 있는 데이터의 양(limit) 등의 속성이 있다. 주요 메서드론 clear(), flip(), rewind(), compact(), wrap() 등이 있는데 생각보다 까다로운 편. (Netty에선 쓰기 편하게 정리되어있다.)
NIO에선 AsynchronousServerSocketChannel이 ServerSocket의 역할을 대신한다. 채널을 열기 위한 open() 메서드가 있고, 특정 포트와 연결하기 위한 bind() 메서드가 있다. 그리고 클라이언트의 연결을 허용하는 accept() 메서드로 구성된다.
• 버퍼: 간단한 작업 집합으로 인터페이스 된 연속된 메모리 블록을 나타낸다.
• non-blocking I/O: 파일, 소켓과 같은 일반적인 I/O 소스에 '채널'을 연결해주는 역할을 한다.
Java에서 non-blocking 방식으로 통신하려면 우선 목적지(destination)에 대한 '채널'을 연 다음 '버퍼'를 이용해 서로 데이터를 주고 받아야 한다. 버퍼(java.nio.Buffer)엔 읽기/쓰기에 대한 위치(position), 버퍼의 고정 크기(capacity), 버퍼에 쓸 수 있는 데이터의 양(limit) 등의 속성이 있다. 주요 메서드론 clear(), flip(), rewind(), compact(), wrap() 등이 있는데 생각보다 까다로운 편. (Netty에선 쓰기 편하게 정리되어있다.)
NIO에선 AsynchronousServerSocketChannel이 ServerSocket의 역할을 대신한다. 채널을 열기 위한 open() 메서드가 있고, 특정 포트와 연결하기 위한 bind() 메서드가 있다. 그리고 클라이언트의 연결을 허용하는 accept() 메서드로 구성된다.
public class NioSocketServer { public NioSocketServer() { try { final AsynchronousServerSocketChannel listener = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(12345)); listener.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() { @Override public void completed(AsynchronousSocketChannel ch, Void att) { listener.accept(null, this); // Accept the next connection. ch.write(ByteBuffer.wrap("Hello world\n".getBytes())); // Send hello message. ByteBuffer byteBuffer = ByteBuffer.allocate(4096); // Create read buffer. try { boolean running = true; int bytesRead = ch.read(byteBuffer).get(20, TimeUnit.SECONDS); // Read, timeout 20sec. while (bytesRead != -1 && running) { if (byteBuffer.position() > 2) { byteBuffer.flip(); // Ready to read. // Buffer data to string. byte[] lineBytes = new byte[bytesRead]; byteBuffer.get(lineBytes, 0, bytesRead); String line = new String(lineBytes); System.out.println("ECHO: " + line); ch.write(ByteBuffer.wrap(line.getBytes())); // ECHO byteBuffer.clear(); bytesRead = ch.read(byteBuffer).get(20, TimeUnit.SECONDS); } else { running = false; } } } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } catch (TimeoutException e) { ch.write(ByteBuffer.wrap("Good Bye\n".getBytes())); // Send timeout message. } try { if (ch.isOpen()) { ch.close(); } } catch (IOException e1) { e1.printStackTrace(); } } @Override public void failed(Throwable exc, Void att) { // } }); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { new NioSocketServer(); System.out.println("Started."); try { Thread.sleep(60 * 1000); } catch(Exception e) { e.printStackTrace(); } System.out.println("Aborted."); } }