개발 블로그

소켓의 동작 방식: 블로킹 vs 논블로킹 본문

시스템/시스템 프로그래밍

소켓의 동작 방식: 블로킹 vs 논블로킹

갹둥 2024. 9. 2. 22:01

 네트워크 프로그래밍을 하다 보면 "블로킹"과 "논블로킹"이라는 개념을 접하게 됩니다. 특히 소켓 프로그래밍에서 이 두 개념을 알고 가는게 중요한 것 같아서 이 글에서는 블로킹과 논블로킹을 정리해보려고 합니다. 

 

블로킹과 논블로킹 소켓은 각각의 상황에 따라 장단점이 존재합니다. 네트워크 프로그래밍에서 이 두 개념을 잘 이해하고 활용하면, 더 안정적이고 효율적인 애플리케이션을 설계할 수 있습니다. 

 

 

블로킹(Blocking)

  • 요청한 작업이 성공하거나 에러가 발생하기 전까지는 응답을 돌려주지 않는 것
  • 이전 게시물에서 C 코드에서 socket() 함수로 생성된 소켓은 기본적으로 블로킹 모드임. 즉, read()나 write() 호출 시, 데이터가 준비될 때까지 함수 호출이 블로킹입니다.
    int n = read(socket_fd, buffer, sizeof(buffer));
  • Java에서 Socket, ServerSocket으로 생성한 소켓은 블로킹 소켓입니다.
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class SimpleServer {
    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(8080);
            System.out.println("Server is listening on port 8080");

            while (true) {
                Socket clientSocket = serverSocket.accept(); // 클라이언트의 연결 요청 수락
                System.out.println("Client connected");

                InputStream input = clientSocket.getInputStream();
                OutputStream output = clientSocket.getOutputStream();

                int data = input.read();
                System.out.println("Received data: " + data);

                output.write(data);

                clientSocket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

(자바 코드에 대한 자세한 설명은 다음 게시물에...)

 

장점:

  • 구현이 간단하고 직관적
  • 프로그래밍 로직이 이해하기 쉬움

단점:

  • 특정 작업이 완료될 때까지 전체 프로그램이 대기해야 하므로 효율성이 떨어질 수 있음
  • 특히 다중 클라이언트를 처리하는 서버의 경우, 하나의 클라이언트가 데이터를 보내지 않으면 다른 클라이언트의 요청도 처리되지 않을 수 있음
  • cf) 보완책: 각 클라이언트의 요청을 별도의 스레드(혹은 프로세스)로 처리하면, 블로킹 소켓을 사용하면서도 다중 클라이언트를 처리할 수 있음 +) 비동기 I/O

 

 

 

논블로킹(Non-Blocking)

  • 요청한 작업의 성공 여부와 상관없이 바로 결과를 돌려주는 것
  • 요청의 응답으로 성공 여부 판단
  • 데이터가 아직 준비되지 않았다면 오류 코드나 특정 상태를 반환
  • C에서는 아래와 같이 구현 가능 
  • fcntl(socket_fd, F_SETFL, O_NONBLOCK); // 소켓을 논블로킹 모드로 설정 char buffer[1024]; int n = read(socket_fd, buffer, sizeof(buffer)); if (n < 0 && errno == EWOULDBLOCK) { // 데이터가 아직 준비되지 않음. 다른 작업을 수행할 수 있습니다. }
  • Java에서는 JDK 1.3까지는 블로킹 방식의 I/O만 지원했지만, JDK 1.4 이후부터 NIO라는 논블로킹 방식의 I/O API가 추가되었습니다.
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.ServerSocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NonBlockingServer {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open(); // 셀렉터를 열어 여러 채널의 이벤트를 감시
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new java.net.InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false); // 서버 소켓 채널을 논블로킹 모드로 설정
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 서버 소켓 채널을 셀렉터에 등록하고 연결 수락 이벤트를 감시

        while (true) {
            selector.select(); // 블로킹 호출, I/O 이벤트를 기다립니다
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                keyIterator.remove();

                if (key.isAcceptable()) {
                    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = ssc.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(256);
                    int numRead = socketChannel.read(buffer);
                    if (numRead == -1) {
                        socketChannel.close();
                    } else {
                        String received = new String(buffer.array()).trim();
                        System.out.println("Received: " + received);
                    }
                }
            }
        }
    }
}

 

(자바 코드에 대한 자세한 설명은 다음 게시물에...)

 

장점:

  • 특정 작업이 완료되지 않았더라도 다른 작업을 수행할 수 있어 효율성이 높음
  • 특히 다중 클라이언트를 처리하는 서버의 경우, 하나의 클라이언트가 데이터를 보내지 않더라도 다른 클라이언트의 요청을 처리할 수 있음
  • 논블로킹 I/O는 이벤트 루프 기반으로 동작하므로, 많은 클라이언트를 처리할 때도 상대적으로 적은 수의 스레드를 사용할 수 있

단점:

  • 프로그래밍이 더 복잡해집니다. 데이터가 준비되지 않은 상황을 고려해야 하며, 이벤트 기반 프로그래밍 모델을 사용해야 할 수도 있음
  • 소켓 상태를 지속적으로 확인하거나 적절히 대기하는 로직이 필요할 수 있음