소켓 입출력 모델(socket I/O model)은 다수의 소켓을 관리하고 소켓에 대한 입출력을 처리하는 일관된 방식을 뜻한다.
소켓 입출력 모델을 사용하면 프로그래밍 복잡도는 높아지지만 시스템 자원을 적게 사용하면서도 다수의 클라이언트를 효율적으로 처리하는 서버를 만들 수 있다. 여기에서는 소켓 입출력 모델을 학습하기 위한 예비 지식으로 소켓의 동작 모드를 알아보고, 윈도우 운영체제가 제공하는 다양한 소켓 입출력 모델의 종류와 특징을 개략적으로 살펴볼 것이다.
소켓은 소켓 함수 호출 시 동작하는 방식에 따라 블로킹(blocking)과 넌블로킹(nonblocking) 소켓으로 구분하고 이를 소켓 모드(socket mode)라 부른다.
블로킹 소켓(blocking socket)은 소켓 함수 호출 시 조건이 만족되지 않으면 함수가 리턴하지 않고 스레드 실행이 정지한다. 조건이 만족되면 소켓 함수가 리턴하면서 정지된 스레드가 깨어나 실행을 재개한다.
주요 소켓 함수와 리턴 조건을 요약하면 아래 표와 같다. 조건이 만족되지 않으면 소켓 함수가 리턴하지 않으므로, 응용 프로그램이 별도의 스레드를 사용하지 않는 한 다른 작업을 할 수 없다.
소켓 함수 | 리턴 조건 |
---|---|
accept() | 접속한 클라이언트가 있을 때 |
connect() | 서버에 접속을 성공했을 때 |
send() , sendto() | 응용 프로그램이 전송을 요청한 데이터를 소켓 송신 버퍼에 모두 복사했을 때 |
recv() , recvfrom() | 소켓 수신 버퍼에 도착한 데이터가 1바이트 이상 있고, 이를 응용 프로그램이 제공한 버퍼에 복사했을 때 |
SO_SNDTIMEO
,SO_RECVTIMEO
소켓 옵션을 이용하면, 조건이 만족되지 않더라도send()
,sendto()
,recv()
,recvfrom()
함수가 일정 시간이 지나면 리턴하게 할 수 있다.
넌블로킹 소켓(nonblocking socket)은 소켓 함수 호출 시 조건이 만족되지 않더라도 함수가 리턴하므로 스레드가 중단 없이 다음 코드를 수행한다. 예를 들면, 접속한 클라이언트가 없어도 accept()
함수가 리턴하고, 소켓 수신 버퍼에도 도착한 데이터가 없어도 recv()
함수가 리턴한다.
socket()
함수는 기본적으로 블로킹 소켓을 생성한다. 넌블로킹 소켓이 필요하면 다음과 같이 ioctlsocket()
함수를 호출해 소켓 모드를 변경해야 한다.
// 블로킹 소켓 생성
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET)
err_quit("socket()");
// 넌블로킹 소켓 생성
u_long on = 1;
retval = ioctlsocket(sock, FIONBIO, &on);
if (retval == SOCKET_ERROR)
err_quit("ioctlsocket()");
넌블로킹 소켓에 대해 소켓 함수를 호출할 때 조건이 만족되지 않으면 소켓 함수는 오류를 리턴한다. 이때는 WSAGetLastError()
함수를 이용해 오류 코드를 확인해야 한다. 대개 오류 코드는 WSAEWOULDBLOCK
인데, 이는 조건이 만족되지 않았음을 나타내므로 나중에 다시 소켓 함수를 호출하면 된다.
아래 링크는 넌블로킹 소켓을 사용하여 구현한 TCP 서버이다.
https://github.com/LEEBONGHAK/TCP-IP_window_socket/tree/main/Chapter10/NonblockingTCPServer
실행 결과 블로킹 TCP서버와 같다. 다만, CPU 사용률은 차이가 크가 NonblockTCPServer 예제를 실행한 상태에서 Windows 작업 관리자를 띄워보면 CPU 사용률이 거의 100%에 가까운 것을 볼 수 있다. 이는 접속한 클라이언트가 없어도 accept()
함수가 리턴하여 goto문에 의해 계속 수행되기 때문이다.
넌블로킹 소켓의 특징을 정리하면 다음과 같다.
장/단점 | 특징 |
---|---|
장점 | - 소켓 함수 호출 시 항상 리턴하므로 조건이 만족되지 않아 스레드가 오랜 시간 정지하는 상황, 즉 교착 상태(deadlock)가 생기지 않는다. - 멀티스레드를 사용하지 않고도 여러 소켓에 대해 돌아가면서 입출력을 처리할 수 있다. 필요하다면 중간에 소켓과 직접 관계가 없는 다른 작업을 할 수도 있다. |
단점 | - 소켓 함수를 호출할 때마다 WSAEWOULDBLOCK 과 같은 오류 코드를 확인하고 처리해야 하므로 프로그램 구조가 복잡해진다.- 블로킹 소켓을 사용한 경우보다 CPU 사용률이 높다. |
소켓 입출력 모델에 기반한 서버를 작서하기 앞서 기존의 서버 작성 모델을 살펴볼 것이다. 소켓 입출력 모델을 기반하지 않은 일반적인 TCP 혹은 UDP 서버는 반복 서버와 병행 서버로 분류할 수 있다.
반복 서버(iterative server)는 여러 클라이언트를 한 번에 하나씩 처리한다. 스레드(thread) 1개만으로 구현된 서버로 다음과 같은 특징을 갖는다.
장/단점 | 특징 |
---|---|
장점 | - 스레드 1개만으로 구현하므로 시스템 자원 소모가 적다. |
단점 | - 한 클라이언트의 처리 시간이 길어지면 다른 클아이언트의 대기 시간이 길어진다. |
위의 특징 때문에 반복 서버는 UDP 서버를 작성할 때 적합하다. UDP는 TCP와 달리 논리적 연결 개념을 사용하지 않으므로, 반복 서버 방식으로 다수의 클라이언트를 동시에 서비스할 수 있다.
병행 서버(concurrent server)는 여러 클라이언트를 동시에 처리한다. 멀티스레드로 구현된 서버로 병행 서버의 특징은 다음과 같다.
장/단점 | 특징 |
---|---|
장점 | - 한 클라이언트의 처리 시간이 길어지더라도 다른 클라이언트에 영향을 주지 않는다. |
단점 | - 스레드를 여러 개 생성하여 구현하므로 시스템 자원 소모가 많다. |
이러한 특징 때문에 병행 서버는 TCP 서버를 작성할 때 적합하다. TCP는 UDP와 달리 논리적 연결 개념을 사용하므로 병행 서버 방식을 사용하지 않고는 다수의 클라이언트를 동시에 서비스하기 어렵다. 하지만 TCP 서버가 각 클라이언트와 아주 짧은 시간 동안만 통신하고, 동시에 접속하는 클라이언트 수가 제한되어 있다면 반복 서버 방식으로 작성해도 된다.
바람직한 소켓 입출력 모델은 반복 서버와 병행 서버의 장점을 모두 갖추면서 각각의 단점을 해결할 형태가 될 것이다. 유한한 자원(CPU, 메모리 등)을 갖춘 시스템에서 실행되는 서버가 제공해야 하는 이상적인 기능은 다음과 같다.
이상적인 기능을 제공하는 서버를 구현하기 위해 소켓 입출력 모델에 요구되는 사항을 정리해보면 다음과 같다.
WSAEWOULDBLOCK
과 같은 오류 코드가 발생하는 경우가 없어야 한다는 뜻이다.윈도우 운영 체제에서 제공하는 소켓 입출력 모델은 아래 표와 같다.
소켓 입출력 모델 | 윈도우 CE | 윈도우(클라이언트 버전) | 윈도우(서버 버전) |
---|---|---|---|
Select | CE 1.0 이상 | 윈도우 95 이상 | 윈도우 NT 이상 |
WSAAsyncSelect | x | 윈도우 95 이상 | 윈도우 NT 이상 |
WSAEventSelect | CE .NET 4.0 이상 | 윈도우 95 이상 | 윈도우 NT 3.51 이상 |
Overlapped | CE .NET 4.0 이상 | 윈도우 95 이상 | 윈도우 NT 3.511 이상 |
Completion Port | x | 윈도우 NT 3.5 이상(윈도우 95/98/Me 제외) | 윈도우 NT 3.5 이상(윈도우 95/98/Me 제외) |
참고 자료
김성우 저, "TCP/IP 윈도우 소켓 프로그래밍", 한빛아카데미, 2018