X : 소켓 입출력 모델 I
TCP/IP 윈도우 소켓 프로그래밍 을 읽고 정리한 문서입니다 ;)
소켓 입출력 모델 개요
소켓 입출력 모델은 다수의 소켓을 관리하고 소켓 입출력을 처리하는 일관된 방식을 의미한다.
프로그래밍 복잡도는 높아지지만 시스템 자원을 적게 사용하면서도 다수의 클라이언트를 효율적으로 처리하는 서버를 만들 수 있게 된다.
소켓 모드의 종류
블로킹 소켓
- 소켓 함수 호출 시 조건이 만족되지 않으면 함수가 리턴하지 않고 스레드 실행이 정지된다.
논블로킹 소켓
소켓 함수 호출 시 조건이 만족되지 않더라도 리턴하고 다음 코드를 수행한다.
조건이 만일 만족되지 않는다면
WSAEWOULDBLOCK
코드를 리턴하므로 확인해야 한다.ioctlsocket()
함수를 호출하여 소켓 모드를 변경해야 논블로킹으로 동작한다.int ioctlsocket( SOCKET sock, // 대상 소켓 long cmd, // 소켓에 수행할 동작 u_long* argp // 0이라면 블로킹 모드, 1이라면 논블로킹 모드 );
논블로킹 코드 스니펫
// listen_socket 을 논블로킹 소켓을 변경
u_long on = 1;
int result = ioctlsocket(listen_socket, FIONBIO, &on);
if (result == SOCKET_ERROR) {
// handle error
}
SOCKET client_sock;
SOCKADDR_IN clientaddr;
int addrlen = sizeof(clientaddr);
char buf[BUFSIZE + 1];
// accept 예시
while(1) {
ACCEPT_AGAIN:
client_sock = accept(listen_sock, (SOCKADDR *)&clientaddr, &addrlen);
if (client_sock == INVALID_SOCKET) {
if (WSAGetLastError() == WSAEWOULDBLOCK) {
GOTO ACCEPT_AGAIN;
}
// handle error
}
...
}
논블로킹 코드는 스레드가 오랜 시간 정지하는 상황이 생기지 않으며, 멀티스레드를 사용하지 않고도 여러 소켓에 대해 돌아가면서 입출력을 처리할 수 있게 된다.
단, 함수 호출 결과를 WSAEWOULDBLOCK
과 항상 비교해야 하며, CPU 사용률은 높아지게 된다.
서버 작성 모델 종류
- 반복 서버
- 여러 클라이언트를 한 번에 하나씩 처리한다.
- 스레드 한 개만으로 구현하므로 시스템 자원 소모가 적다
- 한 클라이언트의 처리 시간이 길어지면 다른 클라이언트 대기 시간이 길어진다.
- UDP 서버를 작성할 때 적합하다.
- 병행 서버
- 여러 클라이언트를 동시에 처리한다.
- 한 클라이언트의 처리 시간이 길어지더라도 다른 클라이언트에겐 영향이 없다.
- 스레드를 여러 개 생성하므로 시스템 자원 소모가 많다.
- TCP 서버를 작성할 때 적합하다.
이상적인 소켓 입출력 모델
반복 서버와 병행 서버의 장점을 모두 갖추면서 각각의 단점을 해결한 형태가 이상적일 것이다.
- 가능한 많은 클라이언트가 접속할 수 있다.
- 서버는 각 클라이언트의 요청에 빠르게 반응하며, 고속으로 데이터를 전송한다.
- 시스템 자원 사용량을 최소화한다.
이러한 요구 사항을 맞춘 서버를 구현하기 위해 소켓 입출력 모델에 요구되는 사항은 다음과 같다.
- 소켓 함수 호출 시 블로킹을 최소화한다.
- CPU 사용률을 최소로 하되, 논블로킹 소켓을 사용한 소켓 함수 호출이 항상 성공해야 한다.
- 스레드 개수를 일정 수준으로 유지한다.
- CPU 개수에 비례하여 스레드를 생성하고 입출력을 처리할 수 있도록 한다.
- CPU 명령 수행과 입출력 작업을 병행한다.
- 하드웨어와 CPU는 진정한 병렬 동작이 가능하므로, CPU 명령 수행과 소켓 입출력을 병행시킬 수 있어야 한다.
- 유저 모드와 커널 모드 전환 횟수를 최소화한다.
- 응용프로그램은 유저 모드와 커널 모드를 끊임없이 오가며 실행된다.
- 많은 CPU 사이클이 소모되므로 가능한 커널모드 전환을 줄여야 한다.
SELECT 모델
Select 모델은 select()
함수가 핵심 역할을 한다는 뜻에서 붙인 이름이고, 이를 이용하면 소켓 모드와 관계 없이 여러 소켓을 하나의 스레드로 처리할 수 있다.
Select 모델을 사용하면 소켓 함수 호출이 성공할 수 있는 시점을 미리 알 수 있다. 즉, 다음과 같다.
- 블로킹 소켓에서 소켓 함수 호출 시 블로킹되는 상황을 막을 수 있다.
- 논블로킹 소켓에서
WSAEWOULDBLOCK
을 예방하여 다시 호출할 필요가 없게끔 한다.
이를 구현하기 위해서는, 소켓 세트 (Socket Set) 세 개를 준비해야 한다.
예를 들면, 어떤 소켓에 대해 recv()
함수를 호출하고 싶다면 읽기 셋에 넣고, send()
를 호출하고 싶다면 쓰기 셋에 넣으면 된다.
이후 select()
함수를 호출하면, 소켓 셋에 포함된 소켓이 입출력을 위한 준비가 될 때까지 대기한다. 이후, 적어도 하나의 소켓이 준비되면 리턴한다.
이 때 소켓 셋에는 입출력이 가능한 소켓만 남고 나머지는 모두 제거된다. 따라서 셋에 남아있는 소켓들을 순회하며 적합한 작업을 처리하는 식이다.
소켓 셋을 통해 소켓 함수를 성공적으로 호출할 수 있는 시점을 알아낼 수 있고, 일부의 경우 소켓 함수의 호출 결과를 알아낼 수 있다.
- 읽기 세트
- 함수 호출 시점
accept()
함수를 성공적으로 호출할 수 있다.- 소켓 수신 버퍼에 도착한 데이터가 있으므로
recv()
호출이 가능하다. - TCP 연결이 종료되었으므로
recv()
호출이 가능하다.
- 함수 호출 시점
- 쓰기 세트
- 함수 호출 시점
- 소켓 송신 버퍼의 여유 공간이 충분하므로
send()
호출이 가능하다.
- 소켓 송신 버퍼의 여유 공간이 충분하므로
- 함수 호출 결과
- 논블로킹 소켓을 사용한
connect()
함수 호출이 성공했다.
- 논블로킹 소켓을 사용한
- 함수 호출 시점
- 예외 세트
- 함수 호출 시점
- OOB 데이터가 도착했기에
recv()
호출이 가능하다.
- OOB 데이터가 도착했기에
- 함수 호출 결과
- 논블로킹 소켓을 사용한
connect()
함수 호출이 실패했다.
- 논블로킹 소켓을 사용한
- 함수 호출 시점
int select(
int nfds, // 윈도우에서는 무시
fd_set *readfds, // 읽기 세트
fd_set *writefds, // 쓰기 세트
fd_set *exceptfds, // 예외 세트
const struct timeval *timeout // select() 함수가 리턴할 타임아웃 설정
);
select()
함수를 사용한 소켓 입출력 절차는 다음과 같다.
- 소켓 셋을 비운다.
- 소켓 셋에 소켓을 넣는다. 넣을 수 있는 최대 소켓 개수는
FD_SETSIZE
로 정의되어 있다. select()
함수를 호출한다. 타임아웃이 NULL이라면select()
함수는 조건이 만족하는 소켓이 있을 때까지 리턴하지 않는다.select()
함수가 리턴하면 소켓 셋에 남아있는 모든 소켓에 대해 적절한 함수 호출을 한다.- 위 절차를 반복한다.
소켓 세트에 대한 조작을 도와주는 다양한 매크로 함수 또한 존재한다.
FD_ZERO(fd_set *set)
: 세트를 비운다 (초기화)FD_SET(SOCKET s, fd_set *set)
: 세트에 소켓 s를 넣는다.FD_CLR(SOCKET s, fd_set *set)
: 세트에서 소켓 s를 제거한다.FD_ISSET(SOCKET s, fd_set *set)
: 세트에 소켓 s가 들어있는지 확인한다.
WSAAsyncSelect 모델
WSAAsyncSelect 모델은 WSAAsyncSelect()
함수가 핵심 역할을 한다. 이를 이용하면 소켓과 관련된 네트워크 이벤트를 윈도우 메시지 형태로 받게 된다.
즉, 한 윈도우 프로시저에 전달되므로 멀티스레드를 사용하지 않고도 여러 소켓을 처리할 수 있다.
이 모델을 사용하면 소켓 함수 호출이 성공할 수 있는 시점을 윈도우 메시지 수신으로 알 수 있다.
WSAAsyncSelect()
함수를 호출하여 소켓 이벤트를 알려줄 윈도우 메시지와 관심 있는 네트워크 이벤트를 등록한다.- 등록한 네트워크 이벤트가 발생하면, 윈도우 메시지가 발생하여 윈도우 프로시저가 호출된다.
- 윈도우 프로시저에서 받은 메시지의 종류에 따라 적절한 소켓 함수를 호출하여 처리한다.
int WSAAsyncSelect(
SOCKET s,
HWND hWnd,
unsigned int wMsg,
long lEvent
);
SOCKET s
: 네트워크 이벤트를 처리하고자 하는 소켓HWND hWnd
: 네트워크 이벤트가 발생했을 때 메시지를 받을 윈도우의 핸들unsigned int wMsg
: 네트워크 이벤트가 발생하면 윈도우가 받을 메시지. 직접WM_USER+x
형태의 사용자 메시지를 정의해서 사용하게 된다.long lEvent
: 관심 있는 네트워크 이벤트의 비트 조합
WSAAsyncSelect()
함수를 호출하면 해당 소켓은 자동으로 논블로킹 모드로 전환된다.
accept()
가 반환하는 소켓은 연결대기 소켓과 동일한 속성을 가지지만, 직접 WSAAsyncSelect()
함수를 호출하여 관심 있는 이벤트를 등록해야 한다.
또한, 이 경우 WSAEWOULDBLOCK
오류 코드가 발생하는 경우가 있을 수 있다.
윈도우 메시지를 받았을 때, 적절한 소켓 함수를 호출하지 않으면 다음 번에는 같은 윈도우 메시지가 발생하지 않게 된다. 따라서 소켓 함수 호출을 하거나 직접 해당 메시지를 발생시켜야 한다.
네트워크 이벤트 발생 시 윈도우 프로시저에 전달되는 내용은 다음과 같다.
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
HWND hWnd
: 메시지가 발생한 윈도우의 핸들UINT uMsg
:WSAAsyncSelect()
호출 시에 등록했던 사용자 정의 메시지WPARAM wParam
: 네트워크 이벤트가 발생한 소켓LPARAM lParam
: 하위 16비트는 발생한 네트워크 이벤트이고, 상위 16비트는 오류 코드이다.
이 모델 또한 Select모델과 마찬가지로 소켓과 관련된 이벤트를 알려줄 뿐 소켓 정보를 관리해주지는 않기에, 각 소켓에 필요한 정보를 관리하는 기능은 응용 프로그램이 구현해야 한다.
Select모델에서는 FD_SETSIZE개의 소켓을 처리할 수 있다는 제약이 있었지만, WSAAsyncSelect모델은 이러한 제약이 없다.
WSAEventSelect 모델
WSAEventSelect모델은 WSAEventSelect()
함수가 핵심 역할을 한다. 이는 소켓과 관련된 네트워크 이벤트를 이벤트 객체를 통해 감지한다. 이벤트 객체를 소켓당 하나씩 생성하고 이벤트 객체들을 관찰하면 멀티스레드를 사용하지 않고도 여러 소켓을 처리할 수 있다.
소켓 함수 호출이 성공할 수 있는 시점을 이벤트 객체를 통해 알 수 있기에, 소켓에 대해 이벤트 객체를 짝지어두는 절차가 팔요하다.
- 소켓을 생성할 때마다
WSACreateEvent()
함수를 이용해 이벤트 객체를 생성한다. WSAEventSelect()
함수를 이용해 소켓과 이벤트 객체를 짝지음과 동시에, 처리할 네트워크 이벤트를 등록한다.WSAWaitForMultipleEvents()
함수를 호출해 이벤트 객체가 신호 상태가 되기를 기다린다. 네트워크 이벤트가 발생하면 해당 소켓과 연결된 이벤트가 신호 상태가 될 것이다.WSAEnumNetworkEvents()
함수를 호출해 발생한 네트워크 이벤트를 알아내고 이를 처리한다.
WSACreateEvent()
함수는 이벤트 객체를 생성하므로, 사용을 마친 이벤트 객체는 WSACloseEvent()
함수를 호출해 제거해야 한다.
이벤트 객체의 상태를 변화시키는 함수로 WSASetEvent()
, WSAResetEvent()
또한 사용 가능하다.
WSAEventSelect()
함수는 소켓과 이벤트 객체를 짝지음과 동시에, 처리할 네트워크 이벤트를 등록한다.
int WSAEventSelect(
SOCKET s,
WSAEVENT hEventObject,
long lNetworkEvents
);
WSAEVENT hEventObject
: 소켓과 연관시킬 이벤트 객체의 핸들이다.long lNetworkEvents
: 관심 있는 네트워크 이벤트를 비트 마스킹하여 등록한다.
WSAEventSelect()
함수를 호출하면 해당 소켓은 자동으로 넌블로킹 모드로 전환된다.
accept()
함수가 리턴하는 소켓은 연결 대기 소켓과 동일한 설정을 가지지만, WSAEventSelect()
함수를 호출하여 관심 있는 이벤트를 직접 등록해주어야 한다.
네트워크 이벤트에 대응하여 소켓 함수를 호출할 때 WSAEWOULDBLOCK
오류 코드가 발생하는 경우가 드물게 있다.
네트워크 이벤트 발생 시 적절한 소켓 함수를 호출하지 않으면, 다음 번에는 같은 네트워크 이벤트가 발생하지 않는다. 따라서, 대응 함수를 늦게라도 호출할 수 있도록 해야 한다.
WSAWaitForMultipleEvents()
함수는 여러 이벤트 객체를 동시에 관찰할 수 있는 기능을 제공한다. 이는 WaitForMultipleObjects()
함수와 사용법이 비슷하다. 그렇기에 한 번에 감시 가능한 이벤트의 최댓값은 WSA_MAXIMUM_WAIT_EVENTS
로서 이는 64이다.
구체적인 네트워크 이벤트를 알아내기 위해서는 WSAEnumNetworkEvents()
함수를 이용하여 비트마스킹된 네트워크 이벤트와 오류 메시지를 받아올 수 있다.
이 모델 또한 Select모델과 마찬가지로 소켓 정보를 응용프로그램이 직접 관리해야 한다. 동시에 처리할 수 있는 소켓의 개수가 64개로 제한되어 있다는 점에서 Select
모델과 같다.
요약
- 소켓 입출력 모델
- 다수의 소켓을 관리하고 소켓에 대한 입출력을 처리하는 일관된 방식
- 소켓 입출력 모델을 사용하면 프로그래밍 복잡도는 높아지지만, 시스템 자원을 적게 사용하면서 다수의 클라이언트를 효율적으로 처리하는 서버를 만들 수 있다.
- 두 종류의 소켓 모드
- 블로킹 소켓 : 소켓 함수 호출 시 조건이 만족되지 않으면 함수가 리턴되지 않고 스레드 실행은 정지된다. 조건이 만족될 때 스레드가 재개되며 소켓 함수가 리턴된다.
- 논블로킹 소켓 : 소켓 함수 호출 시 조건이 만족되지 않더라도 함수가 리턴되므로 스레드 실행이 중단되지 않는다.
ioctlsocket()
함수 호출을 통해 모드 전환이 가능하다.
- Select모델을 이용한 소켓 입출력 절차
- 소켓 셋을 준비하여 초기화하고 각 소켓을 적당한 셋에 넣는다.
select()
함수를 호출하면 소켓 셋에 포함된 소켓이 입출력이 가능할 때까지 대기된다.select()
함수가 리턴하면 소켓 셋에 존재하는 소켓들에 적절한 소켓 함수를 호출한다.
- WSAAsyncSelect모델을 이용한 소켓 입출력 절차
WSAAsyncSelect()
함수를 호출하여 소켓 이벤트를 알려줄 윈도우 메시지와 관심있는 네트워크 이벤트를 등록한다.- 등록한 네트워크 이벤트가 발생하면 윈도우 메시지가 발생하여 윈도우 프로시저가 호출된다.
- 윈도우 프로시저에서 받은 메시지의 종류에 따라 적절한 소켓 함수를 호출한다.
- WSAEventSelect모델을 이용한 소켓 입출력 절차
- 소켓을 생성할 때마다
WSACreateEvent()
함수를 이용해 이벤트 객체를 생성한다. WSAEventSelect()
함수를 이용해 소켓과 이벤트 객체를 짝짓고, 처리할 네트워크 이벤트를 등록한다.- 해당 네트워크 이벤트가 발생하면 이벤트가 신호 상태가 되므로,
WSAWaitForMultipleEvents()
함수를 호출하여 이를 감지할 수 있다. WSAEnumNetworkEvents()
함수를 호출해 해당 소켓에 발생한 네트워크 이벤트를 알아내고 적절한 소켓 함수를 호출한다.
- 소켓을 생성할 때마다
'공부한 이야기 > 윈도우 소켓 프로그래밍' 카테고리의 다른 글
XI : 소켓 입출력 모델 II (0) | 2023.04.29 |
---|---|
IX : GUI 소켓 응용 프로그램 (0) | 2023.04.29 |
VIII : 소켓 옵션 (0) | 2023.04.29 |
VII : UDP 서버-클라이언트 (0) | 2023.04.29 |
VI : 멀티스레드 (0) | 2023.04.29 |