본문 바로가기
카테고리 없음

[NETWORK] 네트워크 기본 - 2

by simongs 2020. 5. 12.

동기 vs 비동기


동기

  • 완료될 때 까지 결과를 기다리는 것

비동기

  • 완료될 때까지 기다리지 않는것
  • 호출한 쪽이 주기적으로 끝났는지 알아내거나 (Polling)
  • 호출받은 쪽이 완료되면 알려주거나 한다 (Notify, Callback)

Blocking I/O vs Non-Blocking I/O


Blocking I/O

  • 요청한 작업이 성공하거나 에러가 발생할 때 까지 응답을 돌려주지 않는다.
  • 아래 소스에서 다수의 요청을 받기 위한 모델이 request 별로 별도의 thread 생성
  • 1 request = 1 Socket = 1 Thread
  • 1 thread 별 thread stack 이 쌓이므로 메모리의 한계가 있다
  • 그 다음모델은 thread pool 을 관리하여 메모리의 누수를 막는 방법이 있다.
  • 적절한 스레드 갯수를 정해야 하는데 두가지 관점을 고려해야한다.
    • JVM GC를 고려해야한다. heap이 클수록 Stop-the-world 시간은 길어진다.
    • Context Switching의 비용은 thread 수가 늘어날 수록 커진다.
  • 그래서 Blocking방식으로는 아주 많은 수의 동시접속 사용자를 수용하기에 한계가 있다.
public class BlockingServer {
    public static void main(String[] args) throws Exception {
        BlockingServer server = new BlockingServer();
        server.run();
    }

    private void run() throws IOException {
        ServerSocket server = new ServerSocket(8888);
        System.out.println("접속 대기중");

        while (true) {
            Socket sock = server.accept();
            System.out.println("클라이언트 연결됨");

            OutputStream out = sock.getOutputStream();
            InputStream in = sock.getInputStream();

            while (true) {
                try {
                    // blocking method, 운영체제의 송신버퍼에 전송할 데이터를 기록한다. 
                    // 이때 송신버퍼의 남은 크기가 write 메소드에서 기록한 데이터의 크기보다 작다면 
                    // 송신버퍼가 비워질 때까지 블로킹된다.
                    int request = in.read();
                    out.write(request);
                }
                catch (IOException e) {
                    break;
                }
            }
        }
    }
}

Non-Blocking I/O

  • 요청한 작업의 성공여부와 상관없이 바로 결과를 돌려주는 것
  • read() 호출 시 RETURN 되는 수는 읽은 Byte 수이다. 0이면 읽은 값 없음.
  • 일종의 이벤트 기반 프로그래밍이다.
  • Non-Blocking 소캣의 Selector 를 활용한 I/O 이벤트 감시
public class NonBlockingServer {
    private Map<SocketChannel, List<byte[]>> keepDataTrack = new HashMap<>();
    private ByteBuffer buffer = ByteBuffer.allocate(2 * 1024);

    private void startEchoServer() {
       try (
          // Selector는 자신에게 등록된 채널에 변경 사항이 발생했는지 확인한다.
          // 변경사항이 발생한 채널에 대한 접근을 가능하게 한다.
          Selector selector = Selector.open();

          // blocking 소켓의 ServerSocket에 대응하는 Non-Blocking 소켓클래스
          ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()
        ) {

          if ((serverSocketChannel.isOpen()) && (selector.isOpen())) {
             serverSocketChannel.configureBlocking(false); // default value is true. 비동기로 하려면 세팅필요
             serverSocketChannel.bind(new InetSocketAddress(8888)); // port binding

             // ServerSocketChannel 에 Selector 를 등록한다.
             // Selector가 감지할 이벤트는 연결 요청에 해당하는 Accept() Operation 이다.
             serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
             System.out.println("접속 대기중");

             while (true) {
                // Selector 에 등록된 채널에서 변경사항이 있는지 검사한다.
                // Selector 에 아무런 I/O 이밴트도 발생하지 않으면 스레드는 이 부분에서 블로킹 된다.
                // I/O 이벤트가 발생하지 않았을 때 블로킹을 피하고 싶다면 selectNow()를 사용하면 된다.
                selector.select();

                // Selector 에 등록돤 채널 중에서 I/O 이벤트가 발생한 채널들의 목록을 조회한다.
                Iterator<SelectionKey> keys = selector.selectedKeys().iterator();

                while (keys.hasNext()) {
                   SelectionKey key = (SelectionKey) keys.next();

                   // I/O 이벤트가 발생한 채널에서 동일한 이벤트가 감지되는 것을 방지하기 위해 제거한다.
                   keys.remove();

                   if (!key.isValid()) {
                      continue;
                   }

                   // 연결요청
                   if (key.isAcceptable()) {
                      this.acceptOP(key, selector);
                   }

                   // 데이터 수신
                   else if (key.isReadable()) {
                      this.readOP(key);
                   }

                   // 데이터 쓰기 가능
                   else if (key.isWritable()) {
                      this.writeOP(key);
                   }
                }
             }
          }
          else {
             System.out.println("서버 소캣을 생성하지 못했습니다.");
          }
       }
       catch (IOException ex) {
          System.err.println(ex);
       }
    }

    private void acceptOP(SelectionKey key, Selector selector) throws IOException {
       ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
       // 클라이언트의 연결을 수락하고 연결된 소켓 채널을 가져온다.
       SocketChannel socketChannel = serverChannel.accept();
       socketChannel.configureBlocking(false);

       System.out.println("클라이언트 연결됨 : " + socketChannel.getRemoteAddress());

       keepDataTrack.put(socketChannel, new ArrayList<byte[]>());

       // 클라이언트 소켓 채널을 Selector에 등록하여 I/O 이벤트를 감시한다.
       socketChannel.register(selector, SelectionKey.OP_READ);
    }

    private void readOP(SelectionKey key) {
       try {
          SocketChannel socketChannel = (SocketChannel) key.channel();
          buffer.clear();
          int numRead = -1;
          try {
             numRead = socketChannel.read(buffer);
          }
          catch (IOException e) {
             System.err.println("데이터 읽기 에러!");
          }

          if (numRead == -1) {
             this.keepDataTrack.remove(socketChannel);
             System.out.println("클라이언트 연결 종료 : "
                                + socketChannel.getRemoteAddress());
             socketChannel.close();
             key.cancel();
             return;
          }

          byte[] data = new byte[numRead];
          System.arraycopy(buffer.array(), 0, data, 0, numRead);
          System.out.println(new String(data, "UTF-8")
                              + " from " + socketChannel.getRemoteAddress());

          doEchoJob(key, data);
       }
       catch (IOException ex) {
          System.err.println(ex);
       }
    }

    private void writeOP(SelectionKey key) throws IOException {
       SocketChannel socketChannel = (SocketChannel) key.channel();

       List<byte[]> channelData = keepDataTrack.get(socketChannel);
       Iterator<byte[]> its = channelData.iterator();

       while (its.hasNext()) {
          byte[] it = its.next();
          its.remove();
          socketChannel.write(ByteBuffer.wrap(it));
       }

       key.interestOps(SelectionKey.OP_READ);
    }

    private void doEchoJob(SelectionKey key, byte[] data) {
       SocketChannel socketChannel = (SocketChannel) key.channel();
       List<byte[]> channelData = keepDataTrack.get(socketChannel);
       channelData.add(data);

       key.interestOps(SelectionKey.OP_WRITE);
    }

    public static void main(String[] args) {
       NonBlockingServer main = new NonBlockingServer();
       main.startEchoServer();
    }
 }

Event-Driven 프로그래밍


  • 이벤트 기반 프로그래밍은 전통적으로 사용자 UI가 있는 프로그램에 많이 사용되었다.
    • 마우스 클릭 이벤트, 윈도우 창 리사이즈 이벤트, 마우스 오버 이벤트 등등..
  • Javascript, iOS 앱, Android앱, 데스크탑 어플리케이션 주로 클라이언트 단의 프로그래밍에서 등장하는 패턴
  • NodeJS는 비동기 싱글스레드이다. 비동기식 API와 동기식 API가 혼재되어 있다. (API에 명시됨)
  • 이벤트 기반 프로그래밍은 추상화 레벨이 중요하다. abstract <-> detail 의 간격 조절 중요

소켓 연결 순서


소켓이란 데이터 송수신을 위한 네트워크 추상화 단위이다. 일반적으로 네트워크 프로그래밍에서 소켓은 IP주소와 포트를 가지고 있으며 양방향 네트워크 통신이 가능한 객체이다.

소켓에 데이터를 기록하거나 읽으려면 소켓에 연결된 소켓 채널(NIO) 이나 스트림(Old Blocking I.O)를 사용해야 한다. 클라이언트 어플리케이션이 소켓에 연결된 스트림에 데이터를 기록하면 소켓이 해당 데이터를 인터넷으로 연결된 서버로 전송한다.

소켓의 연결순서 도식화

Reference


댓글