2020년 1월 8일 수요일

[Java] Java 소켓 통신 (NIO)


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은 일반 스트림이기 때문에 사용하기 편한 형식으로 변환해 사용할 수 있다.

  서버 측도 어렵진 않다. 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 패키지는 다음과 같은 주요 기능을 제공한다.
 • 채널: NIO 버퍼에서 다른 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() 메서드로 구성된다.


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.");
    }
}


[Python] Python에서 언더스코어('_')가 갖는 의미.

Python을 쓰다보면 메서드나 변수의 앞과 뒤에 '_'나 '__'를 붙이는 경우를 자주 보게 된다. 값을 무시하는 용도로 쓰는 '_'에 대한 건 스킵하고, 이 글에선 이 두가지 경우에 대해서만 다루기로 한다..


1. Single Underscore
  '_' 를 변수나 메서드 앞에 붙이는 건 단지 Python에서 쓰이는 이름 짓기 규칙일 뿐이다. 이는 특정 변수나 메서드가 내부 용임을 다른 프로그래머에게 알려주는 힌트로서 존재한다. 다만 '*' 임포트를 사용할 경우 모듈에서 정의된 메서드를 private 처럼 동작하게 해주는데, '*' 임포트 방식은 알다시피 권장되는 방식이 아니다.

# my_module.py
def _internal_func():
    return 42

>>> from my_module import *
>>> _internal_func()
NameError: "name '_internal_func' is not defined"

>>> import my_module
>>> my_module._internal_func()
42

  '_' 가 변수의 끝에 붙는 경우는 변수의 이름 충돌을 피하기 위해 쓰이는 Python의 컨벤션이다.


2. Double Underscore, 던더
  '__'와 같이 언더스코어 두 개를 변수나 메서드 앞에 붙이는 경우가 있다. 이는 Python 인터프리터 동작을 바꾸어주므로 단순 컨벤션이 아니다. 이러한 이중 언더스코어 prefix는 Python 인터프리터가 속성 이름을 다시 생성하게 해준다.(맹글링이라 한다.) 속성 이름 앞에 클래스 이름을 붙여주는 것인데 서브 클래스와의 이름 충돌을 피하게 해주어 서브 클래스에서 변수가 대체되지 않도록 보호해준다.
(private을 위한 장치라 생각할 수도 있긴 하지만 부수적인 효과일 뿐이고 실제 목적은 그러한 것이 아니다.)

  마지막으로 '__'를 변수나 메서드 앞 뒤로 붙이는 경우가 있다. 이 경우엔 맹글링이 적용되지 않는다. 이는 특별한 문법적 기능이나 특별한 기능을 제공하기 위한 장치로 'magic' 또는 'special' 메서드라 불리운다. 대표적으로 '__init__' 이라든가 '__str__' 등이 있다.

2020년 1월 6일 월요일

[Java] 디렉터리 및 파일 삭제

아래의 코드는 사용자가 지정한 디렉터리 밑을 돌아다니면서 파일과 디렉터리를 삭제하는 코드다.
for 문 안에서 삭제하지 말아야 하는 패턴 같은걸 추가하면 더 안전하게 사용할 수 있다.

private void deleteFiles(String path) {
    File file = new File(path);
    if (file.exists() && file.isDirectory()) {
        File fileList[] = file.listFiles();
        if (fileList == null) {
            return;
        }
        
        for (int i = 0, length = fileList.length; i < length; i++) {
            if (fileList[i].isDirectory()) {
                deleteFiles(fileList[i].getAbsolutePath());
                
                fileList[i].delete(); // delete sub directory.
            } else if (fileList[i].isFile()) {
                fileList[i].delete();
            }
        }
        
        file.delete(); // delete main directory.
    }
}