WSAAsyncSelect 모델은 WSAAsyncSelect()
함수가 핵심 역할을 한다는 뜻에서 붙인 이름이다.
WSAAsyncSelect 모델을 사용하면 소켓과 관련된 네트워크 이벤트를 윈도우 메세지 형태로 받게 된다. 아래 그림 같이 모든 소켓과 관련된 메세지가 한 윈도우, 즉, 한 윈도우 프로시저에 전달되므로 멀티스레드를 사용하지 않고도 여러 소켓을 처리할 수 있다.
WSAAsyncSelect 모델을 사용하면 소켓 함수 호출이 성공할 수 있는 시점을 윈도우 메세지 수신으로 알 수 있다. 따라서 소켓 함수 호출 시 조건이 만족되지 않아 생기는 문제를 해결할 수 있다.
WSAAsyncSelect 모델을 이용한 소켓 입출력 절차는 다음과 같다.
WSAAsyncSelect()
함수를 호출하여 소켓 이벤트를 알려줄 윈도우 메세지와 관심있는 네트워크 이벤트를 등록한다. 예를 들면, 소켓으로 데이터를 받을 수 있는 상황이 되면 (WM_USER + 1)
로 정의된 윈도우 메세지로 알려달라고 등록한다.WSAAsyncSelect()
함수의 원형은 다음과 같다.
// 성공: 0, 실패: SOCKET_ERROR
int WSAAsyncSelect(
SOCKET s,
HWND hWnd,
unsigned int wMsg,
long lEvent
);
WM_USER + x
형태의 사용자 정의 메세지를 이용하면 된다.네트워크 이벤트 | 의미 |
---|---|
FD_ACCEPT | 접속한 클라이언트가 있다. |
FD_READ | 데이터 수신이 가능하다. |
FD_WRITE | 데이터 송신이 가능하다. |
FD_CLOSE | 상대가 접속을 종료했다. |
FD_CONNECT | 통신을 위한 연결 절차가 끝났다. |
FD_OOB | OOB(Out-Of-Band) 데이터가 도착했다. |
다음 코드는 소켓 s에 대해 FD_READ
와 FD_WRITE
이벤트를 등록하는 예를 보여준다.
#define WM_SOCKET (WM_USER+1) // 사용자 정의 윈도우 메세지
...
WSAAsyncSelect(s, hWnd, WM_SOCKET, FD_READ|FD_WRITE);
WSAAsyncSelect()
함수 사용 시 유의할 점은 다음과 같다.
WSAAsyncSelect()
함수를 호출하면 해당 소켓은 자동으로 넌블로킹 모드로 전환된다. 블로킹 소켓은 윈도우 메세지 루프를 정지시킬 가능성이 있기 때문에 WSAAsyncSelect 모델에서는 넌블로킹 소켓만 사용하게 되어 있다.accept()
함수가 리턴하는 소켓은 연결 대기 소켓과 동일한 속성을 갖게 된다. 연결 대기 소켓은 직접 데이터 송수신을 하지 않으므로 FD_READ
, FD_WRITE
이벤트를 처리하지 않는다. 반면 accept()
함수가 리턴하는 소켓은 FD_READ
, FD_WRITE
이벤트를 처리해야 하므로, 다시 WSAAsyncSelect()
함수를 호출하여 관심있는 이벤트를 등록해야 한다.WSAEWOULDBLOCK
오류 코드가 발생하는 경우가 드물게 있다.FD_READ
이벤트에 대응하여 recv()
함수를 호출하지 않으면, 동일 소켓에 대한 FD_READ
이벤트는 다시 발생하지 않는다. 따라서 윈도우 메세지가 발생하면 아래 표에 표시한 대응 함수를 호출해야 하며, 그렇지 않을 경우 응용 프로그램이 나중에 직접 메세지를 발생시켜야 한다.응용 프로그램이 직접 메세지를 발생시킨다는 것은
PostMessage()
API 함수를 사용해 자신의 윈도우 메세지 큐에 직접 메세지를 넣는다는 뜻이다.
네트워크 이벤트 | 대응 함수 |
---|---|
FD_ACCEPT | accept() |
FD_READ | recv() , recvfrom() |
FD_WRITE | send() , sendto() |
FD_CLOSE | 없음 |
FD_CONNECT | 없음 |
FD_OOB | recv() , recvfrom() |
네트워크 이벤트 발생 시 윈도우 프로시저에는 다음과 같이 총 4개의 인자를 통해 데이터를 전달받는다.
LPESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
...
}
WSAAsyncSelect()
함수 호출 시 등록했던 사용자 정의 메세지SOCKET
으로 형변환하여 소켓 함수 호출에 그대로 사용하면 된다.WSAGetLastError()
함수로 오류 코드를 알아낼 수 없다. 이식성(portability)을 위해 다음과 같이 정의된 매크로를 사용하는 것이 좋다.#define WSAGETSELECTERROR(lParam) HIWORD(lParam)
#define WSAGETSELECTEVENT(lParam) LOWORD(lParam)
아래 링크는 WSAAsyncSelect 모델을 이용한 TCP 서버이다.
https://github.com/LEEBONGHAK/TCP-IP_window_socket/tree/main/Chapter10/WSAAsyncSelect
실행 결과는 논블로킹 소켓과 Select 모델을 적용시킨 결과와 같다.
스레드를 한 개만 사용한다는 점과 넌블로킹 소켓을 사용하지만 CPU 사용률이 매우 낮다는 점은 동일하다. 다만 차이점은 WSAAsyncSelect 모델 적용 서버는 윈도우가 존재한다는 점이다.
WSAAsyncSelect 모델도 Select 모델처럼 소켓과 관련된 이벤트를 알려줄 뿐 소켓 정보를 관리해주지는 않는다. 따라서 각 소켓에 필요한 정보(응용 프로그램 버퍼, 송수신 바이트 정보 등)를 관리하는 기능은 응용 프로그램이 구현해야 한다.
Select 모델에서는 최대 FD_SETSIZE(64)
개의 소켓을 처리할 수 있으므로 배열을 사용했지만, WSAAsyncSelect 모델은 이런 제약이 없으므로 작성된 서버는 연결 리스트(linked list)를 사용하였다.
위 그림은 소켓 정보를 관리하기 위한 구조다. SocketInfoList
변수는 연결 리스트의 시작점을 나타내는 SOCKETINFO
형 포인터다. 새로운 소켓이 생성되면 아래 그림처럼 SOCKETINFO
구조체를 동적으로 할당하고, 연결 리스트 맨 앞에 삽입한다.
소켓 정보를 삭제할 때는 아래 그림처럼 앞쪽에 있는 포인터를 조작해 연결 리스트를 유지한다.
이제 핵심 코드를 분석할 것이다.
소켓 정보 저장을 위한 구조체와 변수는 다음과 같다.
008 #define WM_SOCKET (WM_USER+1)
009
010 // 소켓 정보 저장을 위한 구조체와 변수
011 typedef struct socketinfo
012 {
013 SOCKET sock;
014 char buf[BUFSIZE + 1];
015 int recvbytes;
016 int sendbytes;
017 BOOL recvdelayed;
018 SOCKETINFO *next;
019 } SOCKETINFO;
020
021 SOCKETINFO *SocketInfoList;
SOCKETINFO
구조체다. 데이터를 받은 후 끝에 널 문자('\0')를 추가하기 위해 BUFSIZE + 1
길이인 바이트 배열을 선언했다. recvbytes
, sendbytes
는 각각 받은 바이트 수와 보낸 바이트 수를 유지하기 위한 변수다. recvdelayed
변수는 FD_READ
메세지를 받았지만 대응 함수인 recv()
함수를 호출하지 않은 경우 TRUE
로 설정할 것이다. next
변수는 SOCKETINFO
구조체를 단일 연결 리스트(singly-linked list)로 관리하기 위해 필요하다.SocketInfoList
는 단일 연결 리스트의 시작점이다.037 int main(int argc, char **argv)
038 {
040 int retval;
041
042 // 윈도우 클랙스 등록
043 WNDCLASS wndclas;
044 wndclass.style = CS_HREDRAW | CS_VREDRAW;
045 wndclass.lpfnWndProc = WndProc;
046 wndclass.cbClsExtra = 0;
047 wndclass.cbWndExtra = 0;
048 wndclass.hInstance = NULL;
049 wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
050 wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
051 wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
052 wndclass.lpszMenuName = NULL;
053 wndclass.lpszClassName = "MyWndClass";
054 if (!RegisterClass(&wndclass))
055 return 1;
056
057 // 윈도우 생성
058 WHND hWnd = CreateWindow("MyWndClass", "TCP 서버", WS_OVERLAPPEDWINDOW, 0, 0, 600, 200, NULL, NULL, NULL, NULL);
059 if (hWnd == NULL)
060 return 1;
061 ShowWindow(hWnd, SW_SHOWNORMAL);
062 UpdateWindow(hWnd);
063
064 // 윈속 초기화
065 WSADATA wsa;
066 if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
067 return 1;
068
069 // socket()
070 SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);
071 if (listen_sock == INVALID_SOCKET)
072 err_quit("socket()");
073
074 // bind()
075 SOCKADDR_IN serveraddr;
076 ZeroMemory(&serveraddr, sizeof(serveraddr));
077 serveraddr.sin_family = AF_INET;
078 serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
079 serveraddr.sin_port = htons(SERVERPORT);
080 retval = bind(listen_sock, (SOCKADDR *)&serveraddr, sizeof(serveraddr));
081 if (retval == SOCKET_ERROR)
082 err_quit("bind()");
083
084 // listen()
085 retval = listen(listen_sock, SOMAXCONN);
086 if (retval == SOCKET_ERROR)
087 err_quit("listen()");
088
089 // WSAAsyncSelect()
090 retval = WSAAsyncSelect(listen_sock, hWnd, WM_SOCKET, FD_ACCEPT|FD_CLOSE);
091 if (retval == SOCKET_ERROR)
092 err_quit("WSAAsyncSelect()");
093
094 // 메세지 루프
095 MSG msg;
096 // GetMessage()는 메세지 큐에 메세지가 존재한다면 가져와서 MSG 구조체에 그 값을 저장하고 TRUE를 반환한다. 그런데 만약 읽은 메세지가 WM_QUIT이면 FALSE를 리턴한다.
097 while (GetMessage(&msg, 0, 0) > 0)
098 {
099 // TranslateMessage()은 GetMessage()으로부터 전달받은 메시지가 WM_KEYDOWN 인지, 눌려진 키가 문자키인지 검사해보고 조건이 맞을 경우 WM_CHAR 메시지를 만들어 message queue에 덧붙이는 역할을 한다. 문자 입력이 아닐 경우는 아무 일도 하지 않는다.
100 // 키보드 드라이버에 의해 ASCII 문자로 매핑된 키에 대해서만 WM_CHAR 메시지를 생성한다.
101 TranslateMessage(&msg);
102 // message queue에 덧붙여진 메시지는 DispatchMessage()에 의해 WndProc()으로 전달된다.
103 DispatchMessage(&msg);
104 }
105
106 // 윈속 종료
107 WSACleanup();
108 return msg.wParam;
109 }
FD_ACCEPT
와 FD_CLOSE
이벤트만 등록한다.110 // 윈도우 메세지 처리
111 LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
112 {
113 switch (uMsg)
114 {
115 case WM_SOCKET: // 소켓 관련 윈도우 메세지
116 ProcessSocketMessage(hWnd, uMsg, wParam, lParam);
117 return 0;
118 case WM_DESTROY:
119 PostQuitMessage(0);
120 return 0;
121 }
122 return DefWindowProc(hWnd, uMsg, wParam, lParam);
123 }
WM_SOCKET
메세지가 발생하면 ProcessSocketMessage()
함수에 전달하여 처리한다. 윈도우 메세지를 처리하는 코드가 길어질 경우에는 이와 같이 별도의 사용자 정의 함수에서 처리하는 것이 좋다.125 // 소켓 관련 윈도우 메세지 처리
126 void ProcessSocketMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
127 {
128 // 데이터 통신에 사용할 변수
129 SOCKETINFO *ptr;
130 SOCKET client_sock;
131 SOCKADDR_IN clientaddr;
132 int addrlen, retval;
133
134 // 오류 발생 여부 확인
135 if (WSAGETSELECTERROR(lParam))
136 {
137 err_display(WSAGETSELECTERROR(lParam));
138 RemoveSocketInfo(wParam);
139 return ;
140 }
WSAGETSELECTERROR()
매크로 함수로 lParam
변수의 상위 16비트를 검사한다. 오류가 발생했다면 err_display()
함수로 오류 코드ㅡ레 대응하는 문자열을 출력한 후, RemoveSocketInfo()
함수로 소켓을 닫고 소켓 정보를 제거한다.오류가 발생하지 않았다면 WSAGETSELECTEVENT()
매크로 함수로 네트워크 이벤트를 조사해야v한다.
142 // 메세지 처리
143 switch (WSAGETSELECTEVENT(lParam))
144 {
145 case FD_ACCEPT:
146 addrlen = sizeof(clientaddr);
147 client_sock = accept(wParam, (SOCKADDR *) &clientaddr, &addlen);
148 if (client_sock == INVALID_SOCKET)
149 {
150 err_display("accept()");
151 return ;
152 }
153 printf("\n[TCP 서버] 클라이언트 접속: IP 주소=%s, 포트 번호=%d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientadd.sin_port));
154 AddSocketInfo(client_sock);
155 retval = WSAAsyncSelect(client_sock, hWnd, WM_SOCKET, FD_READ|FD_WRITE|FD_CLOSE);
156 if (retval == SOCKET_ERROR)
157 {
158 err_display("WSAAsyncSelect()");
159 RemoveSocketInfo(client_sock);
160 }
161 break;
FD_ACCEPT
이벤트가 발생하면 accept()
함수를 호출하고 리턴 값을 확인하여 오류를 처리한다.AddSocketInfo()
함수를 호출해 소켓 정보를 추가한다.accept()
함수가 리턴한 소켓에 대해 WSAAsyncSelect()
함수를 호출함으로써 관심이 있는 이벤트를 다시 등록한다. 이 소켓은 데이터 전송에 사용할 것이므로 FD_READ
, FD_WRITE
, FD_CLOSE
이벤트를 등록한다. 이 과정에서 오류가 발생하면 RemoveSocketInfo()
함수를 호출해 소켓을 닫고 소켓 정보를 제거한다.162 case FD_READ:
163 ptr = GetSocketInfo(wParam);
164 if (ptr->recvbytes > 0)
165 {
166 ptr->recvdelayed = TRUE;
167 return ;
168 }
169 // 데이터 받기
170 retval = recv(ptr->sock, ptr->buf, BUFSIZE, 0);
171 if (retval == SOCKET_ERROR)
172 {
173 err_display("recv()");
174 RemoveSocketInfo(wParam);
175 return ;
176 }
177 ptr->recvbytes = retval;
178 // 받은 데이터 출력
179 ptr->buf[retval] = '\0';
180 addrlen = sizeof(clientaddr);
181 getpeername(wParam, (SOCKADDR *) &clientaddr, &addrlen);
182 printf("[TCP/%s: %d] %s\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port), ptr->buf);
FD_READ
이벤트가 발생하면 해당 소켓(wParam
에 저장되어 있다.)에 대응ㅇ하는 소켓 정보 구조체를 GetSocketInfo()
함수로 얻는다.recv()
를 호출하지 않고 곧바로 리턴한다. 단, recvdelayed
변수를 TRUE
로 설정하여 이 사실을 기록해둔다. 대응함수를 호출하지 않았으므로 받은 데이터가 있더라도 FD_READ
이벤트는 다시 발생하지 않는다. 따라서 나중에 PostMessage()
API 함수를 사용하여 응용 프로그램이 직접 FD_READ
이벤트를 발생시켜야 한다.(203행)recv()
함수를 호출해 데이터를 읽는다. 오류가 발생했다면 RemoveSocketInfo()
함수를 호출해 소켓을 닫고 소켓 정보를 제거한다.recv()
함수 호출이 성공했다면, 받은 바이트 수(recvbytes
)을 갱신한다.FD_READ
이벤트를 처리한 후 곧바로 FD_WRITE
이벤트를 처리한다.(break 문이 없다는 것을 유의한다.)183 case FD_WRITE:
184 ptr = GetSocketInfo(wParam);
185 if (ptr->recvbytes <= ptr->sendbytes)
186 return ;
187 // 데이터 보내기
188 retval = send(ptr->sock, ptr->buf + ptr->sendbytes, ptr->recvbytes - ptr->sendbytes, 0);
189 if (retval == SOCKET_ERROR)
190 {
191 err_display("send()");
192 RemoveSocketInfo(wParam);
193 return ;
194 }
195 ptr->sendbytes += retval;
196 // 받은 데이터를 모두 보냈는지 체크
197 if (recv->recvbytes == ptr->sendbytes)
198 {
199 ptr->recvbytes = ptr->sendbytes = 0;
200 if (ptr->recvdelayed)
201 {
202 ptr->recvdelayed = FALSE;
203 PostMessage(hWnd, WM_SOCKET, wParam, FD_READ);
204 }
205 }
206 break;
207 case FD_CLOSE:
208 RemoveSocketInfo(wParam);
209 break ;
210 }
211 }
FD_WRITE
이벤트가 발생하면 해당 소켓(wParams
에 저장되어 있다.)에 대응하는 소켓 정보 구조체를 GetSocketInfo()
함수로 얻는다.send()
함수를 호출하여 데이터를 보낸다. 오류가 발생했다면 RemoveSocketInfo()
함수를 호출해 소켓을 닫고 소켓 정보를 제거한다.send()
함수 호출이 성공했으면, 보낸 바이트 수(sendbytes
)를 갱신한다.FD_READ
이벤트에 대한 대응함수를 호출하지 않았다면 PostMessage()
API 함수를 호출해 강제로 발생시킨다. 이렇게 하면 다음에 FD_READ
이벤트가 발생하므로 이전에 도착했지만 처리하지 못한 데이터를 읽을 수 있다.FD_CLOSE
이벤트가 발생하면 정상 종료를 의미하므로 RemoveSocketInfo()
함수를 호출해 소켓을 닫고 소켓 정보를 제거한다.참고 자료
김성우 저, "TCP/IP 윈도우 소켓 프로그래밍", 한빛아카데미, 2018