WSAAsyncSelect란?
기존의 Select 모델을 윈도우 메시지 형태로 변화한 모델이라 생각하면 됨.
동작원리
1. WSAAsyncSelect() 함수를 이용하여 소켓을 위한 윈도우 메세지와 처리할 네트워크 이벤트를 등록한다.
ex) #define WM_SOCKET WM_USER + 1
WSAAsyncSelect(clientSocket, hWnd, WM_SOCKET, FD_READ | FD_WRITE | FD_CLOSE);
2. 등록한 네트워크 이벤트가 발생하면 윈도우 메세지가 발생하고 윈도우 메세지 프로시저가 호출된다.
3. 윈도우 메세지 프로시저에서는 받은 메세지의 종류에 따라 적절한 소켓 함수를 호출하여 처리한다.
함수 구조
int WSAAsyncSelect(
SOCKET s, // 설정하고 하는 소켓
HWND hWnd, // 메세지 전달 처리를 위한 윈도우 핸들러
unsigned int uMsg, // 확인할 윈도우 메세지
long lEvent // 네트워크 이벤트 설정
);
네트워크 이벤트 | 의미 |
FD_ACCEPT | 클라이언트가 접속하면 윈도우 메세지를 발생시킨다. |
FD_READ | 데이터 수신이 가능하면.. |
FD_WRITE | 데이터 송신이 가능하면.. |
FD_CLOSE | 상대가 접속을 종료하면.. |
FD_CONNECT | 접속이 완료되면.. |
FD_OOB | OOB 데이터가 도착하면 |
WSAAsyncSelect 함수의 유의 사항
1. WSAAsyncSelect 함수를 호출하면 해당 소켓은 자동으로 논블로킹 모드로 전환된다.
2. select 모드에서 소켓 함수당 읽기 셋 또는 쓰기 셋의 구분이 동일하다
3. 윈도우 메세지에 대응하여 소켓 함수를 호출하면 대부분 성공하지만, WSAEWOULDBLOCK 오류가 발생하는 경우도 있다. 꼭 체크하자
4. 윈도우 메세지를 받았을때 적절한 소켓 함수 처리를 안하면, 다음번 메세지는 발생하지 않는다.
예제 소스
[유의사항]
예제 소스를 작성하실때 비쥬얼스튜디오에서 제공하는 Win32 프로젝트를 사용하시면 됩니다.
유니코드 기반이 아니므로, 프로젝트 설정시 언어체계 부분을 유니코드가 아닌 설정안함으로 변경하시길 바랍니다.
[Server.cpp]
#include <winsock2.h>
#include "Server.h"
#include <list>
#include <algorithm>
#define WM_SOCKET WM_USER + 1
#define MAX_BUFFER_SIZE 512
#define MAX_CONNECT 1024
// 소켓 정보 저장을 위한 구조체
typedef struct
{
SOCKET sock; // 소켓
char recvBuffer[MAX_BUFFER_SIZE + 1]; // 받기 버퍼
char sendBuffer[MAX_BUFFER_SIZE + 1]; // 쓰기 버퍼
int recvBytes; // 받은 데이터 크기
int sendBytes; // 쓰기 데이터 크기
} SOCKET_INFO;
typedef std::list<SOCKET_INFO*> SOCKET_INFO_LIST;
SOCKET_INFO_LIST g_aFreeSocketInfo; // 사용되지 않은 소켓 정보 리스트
SOCKET_INFO_LIST g_aActiveSocketInfo; // 사용하고 있는 소켓 정보 리스트
SOCKET g_ListenSocket = NULL; // 서버 리슨 소켓
HWND g_hWndDisplay = NULL; // 출력을 위한 윈도우
BOOL g_bStartServer = FALSE; // 서버 시작 확인 플래그
void err_quit(char* msg); // 에러 출력 후 종료 함수
void err_display(char* msg); // 에러 출력 함수
void err_display(int errCode); // 에러 코드를 통한 출력 함수
void DisplayText(char* fmt, ...); // 에러 출력 윈도우에 표현하기 위한 함수
BOOL AddActiveSocketInfo(SOCKET clientSocket); // 접속 소켓정보 추가 함수
void RemoveActiveSocketInfo(SOCKET clientSocket); // 접속해제 소켓 정보 삭제 함수
SOCKET_INFO* GetActiveSocketInfo(SOCKET clientSocket); // 현재 접속되어 있는 소켓 정보 얻는 함수
void ServerInit(HWND hWnd)
{
int retValue;
// 출력을 위한 자식 윈도우를 하나 만든다.
HINSTANCE hInst = (HINSTANCE)GetWindowLong(hWnd, GWL_HINSTANCE);
g_hWndDisplay = CreateWindow(
"edit", NULL,
WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL | ES_AUTOHSCROLL | ES_AUTOVSCROLL | ES_MULTILINE | ES_READONLY,
0, 0, 0, 0, hWnd, 0, hInst, NULL);
if(g_hWndDisplay == NULL)
{
MessageBox(NULL, "출력용 윈도우 생성 실패", "error", MB_ICONERROR | MB_OK);
PostQuitMessage(-1);
return;
}
// 윈속 초기화
WSADATA wsa;
if(WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
{
PostQuitMessage(-1);
return;
}
// 소켓 생성
g_ListenSocket = socket(AF_INET, SOCK_STREAM, 0);
if(g_ListenSocket == INVALID_SOCKET)
{
err_quit("socket()");
return;
}
// WSAASyncSelect()
retValue = WSAAsyncSelect(g_ListenSocket, hWnd, WM_SOCKET, FD_ACCEPT | FD_CLOSE);
if(retValue == SOCKET_ERROR)
{
err_quit("WSAASyncSelect()");
return;
}
// bind()
SOCKADDR_IN serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(5001);
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
retValue = bind(g_ListenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
if(retValue == SOCKET_ERROR)
{
err_quit("bind()");
return;
}
// listen()
retValue = listen(g_ListenSocket, SOMAXCONN);
if(retValue == SOCKET_ERROR)
{
err_quit("listen()");
return;
}
// 사용하지 않는 소켓 정보 메모리 할당
for(int i = 0; i < MAX_CONNECT; ++i)
{
SOCKET_INFO* pInfo = new SOCKET_INFO;
memset(pInfo, 0, sizeof(SOCKET_INFO));
g_aFreeSocketInfo.push_back(pInfo);
}
g_bStartServer = TRUE; // 서버 시작 확인
}
void ServerShutdown()
{
if(g_bStartServer == FALSE)
return;
// 접속되어진 리스트를 메모리 해제한다.
// 접속되어진 클라이언트가 있으므로 소켓도 해제한다.
for(SOCKET_INFO_LIST::iterator i = g_aActiveSocketInfo.begin();
i != g_aActiveSocketInfo.end(); ++i)
{
closesocket((*i)->sock);
delete (*i);
}
// 사용하지는 않는 할당 정보에 대해 해제한다.
for(SOCKET_INFO_LIST::iterator i = g_aFreeSocketInfo.begin();
i != g_aFreeSocketInfo.end(); ++i)
{
delete (*i);
}
g_aActiveSocketInfo.clear();
g_aFreeSocketInfo.clear();
// 리슨 소켓을 닫는다.
closesocket(g_ListenSocket);
// 윈속종료
WSACleanup();
g_bStartServer = FALSE; // 서버 종료 확인
}
LRESULT CALLBACK ServerMessageProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg)
{
case WM_SIZE :
// 출력하려는 윈도우에 대한 출력 위치값 변경
MoveWindow(g_hWndDisplay, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
break;
case WM_SOCKET :
{
int retValue;
// 오류 발생 여부 확인
if(WSAGETSELECTERROR(lParam))
{
err_display(WSAGETSELECTERROR(wParam));
RemoveActiveSocketInfo(wParam);
return 0;
}
// 각 상황별 메세지 처리
switch(WSAGETSELECTEVENT(lParam))
{
case FD_ACCEPT : // 클라이언트가 접속 되었을 시
{
SOCKADDR_IN clientAddr;
int addrLength = sizeof(clientAddr);
SOCKET clientSocket = accept(g_ListenSocket, (SOCKADDR*)&clientAddr, &addrLength);
if(clientSocket == INVALID_SOCKET)
{
if(WSAGetLastError() != WSAEWOULDBLOCK)
{
err_display("accept()");
break;
}
return 0;
}
DisplayText("[TCP 서버] 클라이언트 접속 : IP 주소 = %s, 포트번호 = %d\r\n",
inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
// 접속된 클라이언트도 WSAAsyncSelect를 통해 설정해 준다.
retValue = WSAAsyncSelect(clientSocket, hWnd, WM_SOCKET, FD_READ | FD_WRITE | FD_CLOSE);
if(retValue == SOCKET_ERROR)
{
err_display("WSAAsyncSelect()");
closesocket(clientSocket);
return 0;
}
// 활성된 소켓정보 리스트에 추가한다.
AddActiveSocketInfo(clientSocket);
}
break;
case FD_READ : // 패킷 도착
{
// wParam 즉 소켓 값을 통해서 해당 소켓 정보를 찾는다.
SOCKET_INFO* pSocketInfo = GetActiveSocketInfo(wParam);
// 데이터를 읽는다.
retValue = recv(pSocketInfo->sock, pSocketInfo->recvBuffer, MAX_BUFFER_SIZE, 0);
if(retValue == SOCKET_ERROR)
{
if(WSAGetLastError() != WSAEWOULDBLOCK)
{
err_display("recv()");
RemoveActiveSocketInfo(wParam);
return 0;
}
}
pSocketInfo->recvBytes = retValue;
pSocketInfo->recvBuffer[retValue] = '\0';
SOCKADDR_IN socketAddr;
int nAddrLength = sizeof(socketAddr);
getpeername(pSocketInfo->sock, (SOCKADDR*)&socketAddr, &nAddrLength);
DisplayText("[TCP/%s:%d] %s\r\n", inet_ntoa(socketAddr.sin_addr),
ntohs(socketAddr.sin_port), pSocketInfo->recvBuffer);
// 쓰기 버퍼에 값을 전달한다. (에코서버이므로)
memcpy(pSocketInfo->sendBuffer, pSocketInfo->recvBuffer, retValue);
pSocketInfo->sendBuffer[retValue] = '\0';
pSocketInfo->sendBytes = retValue;
// 쓰기 버퍼에 값이 있으므로, 보내기를 수행하도록 메세지를 전달한다.
PostMessage(hWnd, WM_SOCKET, wParam, FD_WRITE);
}
break;
case FD_WRITE : // 패킷 보내기
{
// wParam 즉 소켓 값을 통해서 해당 소켓 정보를 찾는다.
SOCKET_INFO* pSocketInfo = GetActiveSocketInfo(wParam);
if(pSocketInfo->sendBuffer <= 0)
return 0;
// 데이터를 보낸다.
retValue = send(pSocketInfo->sock, pSocketInfo->sendBuffer, pSocketInfo->sendBytes, 0);
if(retValue == SOCKET_ERROR)
{
if(WSAGetLastError() != WSAEWOULDBLOCK)
{
err_display("send()");
RemoveActiveSocketInfo(wParam);
return 0;
}
}
pSocketInfo->sendBytes = 0;
}
break;
case FD_CLOSE : // 접속 해제
RemoveActiveSocketInfo(wParam);
return 0;
}
}
break;
}
return 0;
}
BOOL AddActiveSocketInfo(SOCKET clientSocket)
{
if(g_aFreeSocketInfo.empty())
{
DisplayText("[오류] 소켓 정보를 추가할 수 없습니다.\r\n");
return FALSE;
}
// 사용하지 않는 소켓 정보를 하나 꺼낸다.
SOCKET_INFO* pSocketInfo = (*g_aFreeSocketInfo.begin());
g_aFreeSocketInfo.pop_front();
pSocketInfo->sock = clientSocket;
pSocketInfo->recvBytes = 0;
pSocketInfo->sendBytes = 0;
// 사용하는 소켓 정보 리스트의 관리로 넣는다.
g_aActiveSocketInfo.push_back(pSocketInfo);
return TRUE;
}
void RemoveActiveSocketInfo(SOCKET clientSocket)
{
SOCKADDR_IN socketAddr;
int nAddrLength = sizeof(socketAddr);
getpeername(clientSocket, (SOCKADDR*)&socketAddr, &nAddrLength);
DisplayText("[TCP 서버] 클라이언트 종료: IP 주소 = %s, 포트번호 = %d",
inet_ntoa(socketAddr.sin_addr), ntohs(socketAddr.sin_port));
// 사용하고 있는 소켓정보 리스트에서 조건에 맞는 소켓정보를 찾아
// 접속을 해제하고, 사용하고 있는 소켓정보 리스트에서 삭제, 사용하지 않는 소켓정보 리스트에 추가하여
// 다음번 사용을 다시 할 수 있도록 한다.
for(SOCKET_INFO_LIST::iterator i = g_aActiveSocketInfo.begin();
i != g_aActiveSocketInfo.end(); ++i)
{
if(clientSocket == (*i)->sock)
{
SOCKET_INFO* pSocketInfo = (*i);
closesocket(pSocketInfo->sock);
g_aActiveSocketInfo.erase(i);
g_aFreeSocketInfo.push_back(pSocketInfo);
DisplayText(" (%d/%d)\r\n", g_aActiveSocketInfo.size(), g_aFreeSocketInfo.size());
return;
}
}
}
SOCKET_INFO* GetActiveSocketInfo(SOCKET clientSocket)
{
// 사용하고 있는 소켓 정보들에서 조건에 만족하는 소켓 정보를 얻는다.
for(SOCKET_INFO_LIST::iterator i = g_aActiveSocketInfo.begin();
i != g_aActiveSocketInfo.end(); ++i)
{
if(clientSocket == (*i)->sock)
return (*i);
}
return NULL;
}
void err_quit(char* msg)
{
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, WSAGetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf, 0, NULL);
MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);
LocalFree(lpMsgBuf);
PostQuitMessage(-1);
}
void err_display(char* msg)
{
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, WSAGetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf, 0, NULL);
DisplayText("[%s] %s\r\n", msg, (LPCTSTR)lpMsgBuf);
LocalFree(lpMsgBuf);
}
void err_display(int errCode)
{
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, errCode,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf, 0, NULL);
DisplayText("[오류] %s", (LPCTSTR)lpMsgBuf);
LocalFree(lpMsgBuf);
}
void DisplayText(char* fmt, ...)
{
// printf와 같은 출력과, 가변적인 파라메터를 처리하는 루틴
// va_list, va_start, vsprintf 등 MSDN을 참고하기 바람
va_list arg;
va_start(arg, fmt);
char buf[1024];
vsprintf(buf, fmt, arg);
int nLength = GetWindowTextLength(g_hWndDisplay);
SendMessage(g_hWndDisplay, EM_SETSEL, nLength, nLength);
SendMessage(g_hWndDisplay, EM_REPLACESEL, FALSE, (LPARAM)buf);
}
[Server.h]
// 서버 초기화 함수
void ServerInit(HWND hWnd);
// 서버 종료 함수
void ServerShutdown();
// 서버 메세지 처리 함수
LRESULT CALLBACK ServerMessageProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
[기존 윈도우 메세지 함수 처리 내부]
case WM_CREATE :
ServerInit(hWnd);
break;
default:
ServerMessageProc(hWnd, message, wParam, lParam);
return DefWindowProc(hWnd, message, wParam, lParam);
[출처] WSAAsyncSelect (정문수 개인 카페) |작성자 유빈아빠
댓글 없음:
댓글 쓰기