네트워크 프로그래밍을 하다 보면 "블로킹"과 "논블로킹"이라는 개념을 접하게 됩니다. 특히 소켓 프로그래밍에서 이 두 개념을 알고 가는게 중요한 것 같아서 이 글에서는 블로킹과 논블로킹을 정리해보려고 합니다.
블로킹과 논블로킹 소켓은 각각의 상황에 따라 장단점이 존재합니다. 네트워크 프로그래밍에서 이 두 개념을 잘 이해하고 활용하면, 더 안정적이고 효율적인 애플리케이션을 설계할 수 있습니다.
블로킹(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는 이벤트 루프 기반으로 동작하므로, 많은 클라이언트를 처리할 때도 상대적으로 적은 수의 스레드를 사용할 수 있
단점:
- 프로그래밍이 더 복잡해집니다. 데이터가 준비되지 않은 상황을 고려해야 하며, 이벤트 기반 프로그래밍 모델을 사용해야 할 수도 있음
- 소켓 상태를 지속적으로 확인하거나 적절히 대기하는 로직이 필요할 수 있음
'시스템 > 시스템 프로그래밍' 카테고리의 다른 글
TCP/IP Socket 통신 프로그래밍 (0) | 2024.09.02 |
---|