--------------------------------------------------------------------------------------------------
출처: http://blog.naver.com/ree31206/46430161
--------------------------------------------------------------------------------------------------
소켓 입출력 모델 - WSAAsyncSelect 모델
WSAAsyncSelect() 함수가 핵심적인 역활을 한다. 윈도우 메시지 형태로 소켓과 관련된 네트워크
이벤트를 처리 할 수 있다. 모든 소켓과 관련된 메시지는 하나의 윈도우 프로시저로 전달되므로
멀티스레드를 사용하지 않고도 여러 소켓을 처리할 수 있다.
[WSAAsyncSelect 모델을 이용한 소켓 입출력 절차]
1. WSAAsyncSelect() 함수를 이용하여 소켓을 위한 윈도우 메시지와 처리할 네트워크 이벤트를
등록한다. 예를 들면 소켓을 통해 데이터를 보내거나 받을수 있는 상황이 되면 특정 윈도우 메시지
로 알려달라는 내용을 등록한다.
2. 등록한 네트워크 이벤트가 발생하면 윈도우 메시지가 발생하고 윈도우 프로시저가 호출된다.
3. 윈도우 프로시저에서는 받은 메시지 종류에 따라 적절한 소켓 함수를 호출하여 처리한다.
* WSAAsyncSelect() 함수
int WSAAsyncSelect(
SOCKET s, // 처리하고자 하는 소켓
HWND hWnd, // 메시지를 받을 윈도우를 나타내는 핸들
unsigned int wMsg, // 윈도우가 받을 메시지. 소켓을 위한 메시지는 따로 정의되어
// 있지 않으므로 사용자 정의 메시지를 이용
long lEvent // 처리할 네트워크 이벤트 종류를 마스크 조합으로 나타낸다
); 성공 : 0, 실패 : SOCKET_ERROR
wMsg 를 사용하기 위해선 사용자 정의 윈도우 메시지를 등록해야 한다.
#define WM_SOCKET (WM_USER+1) WM_SOCKET 메시지를 정의한다.
WM_USER 란것은 우리가 지정해줄수 있는 숫자라고 생각하면 된다.
다른 메시지를 추가한다면 +2, +3 해서 추가하면 된다.
// 소켓 s 에 대해 FD_READ | FD_WRITE 이벤트를 등록하는 예
WSAAsyncSelect(s, hWnd, WM_SOCKET, FD_READ | FD_WRITE);
* 네트워크 이벤트 상수값
FD_ACCEPT : 클라이언트가 접속하면 윈도우 메시지를 발생시킨다. accept()
FD_READ : 데이터 수신이 가능하면 윈도우 메시지를 발생시킨다. recv(), recvfrom()
FD_WRITE : 데이터 송신이 가능하면 윈도우 메시지를 발생시킨다. send(), sendto()
FD_CLOSE : 상대가 접속을 종료하면 윈도우 메시지를 발생시킨다.
FD_CONNECT : 접속이 완료되면 윈도우 메시지를 발생시킨다.
FD_OOB : OOB 데이터가 도착하면 윈도우 메시지를 발생시킨다. recv(), recvfrom()
- WSAAsyncSelect() 함수를 사용하면 해당 소켓은 자동으로 넌블로킹 모드로 전환된다.
- accept() 함수가 리턴하는 소켓은 연결 대기 소켓과 동일한 속성을 지니게 된다
- 윈도우 메시지에 대응하여 소켓함수를 호출하면 대부분은 성공하지만
WSAEWOULDBLOCK 오류코드가 발생하는 경우도 있으므로 이를 체크해야 한다.
- 윈도우 메시지를 받았을때 적절한 소켓함수를 호출하지 않으면 다음 번에는 같은 윈도우 메세지가
발생하지 않는다. 예를 들어 FD_READ 이벤트에 대응하여 recv() 함수를 호출하지 않으면 동일한
소켓에 대한 FD_READ 이벤트는 더 발생하지 않는다. 그러므로 윈도우 메시지가 발생하면 앞의
표에 있는 대응함수를 호출해야 하며 그렇지 않을경우 애플리케이션이 직접 메시지를 발생시켜야
한다.
* 윈도우 프로시저 lParam
이식성을 위해 다음과 같이 정의된 매크로를 사용
#define WSAGETSELECTERROR(lParam) HIWORD(lParam)
#define WSAGETSELECTEVENT(lParam) LOWORD(lParam)
[ WSAAsyncSelect() 함수를 이용한 에코 서버 ]
Win32 Application 에서 그냥 하면 되지만 Win32 Console Application 으로 생성해
윈도우창 만드는 것도 구현..
#include <winsock2.h>
#include <stdlib.h>
#include <stdio.h>
#define BUFSIZE 512
#define WM_SOCKET (WM_USER+1)
struct SOCKETINFO
{
SOCKET sock;
char buf[BUFSIZE+1];
int recvbytes;
int sendbytes;
BOOL recvdelayed;
SOCKETINFO *next;
};
SOCKETINFO *SocketInfoList; // 리스트로 구현..
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
void ProcessSocketMessage(HWND, UINT, WPARAM, LPARAM);
BOOL AddSocketInfo(SOCKET sock);
SOCKETINFO *GetSocketInfo(SOCKET sock);
void RemoveSocketInfo(SOCKET sock);
void err_quit(char *msg);
void err_display(char *msg);
void err_display(int errcode);
int main(int argc, char* argv[])
{
int retval;
WNDCLASS wndclass;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hInstance = NULL;
wndclass.lpfnWndProc = (WNDPROC)WndProc;
wndclass.lpszClassName = "MyWindowClass";
wndclass.lpszMenuName = NULL;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
if(!RegisterClass(&wndclass))
return -1;
HWND hWnd = CreateWindow("MyWindowClass", "TCP 서버", WS_OVERLAPPEDWINDOW,
0, 0, 600, 300, NULL, (HMENU)NULL, NULL, NULL);
if(hWnd == NULL)
return -1;
ShowWindow(hWnd, SW_SHOWNORMAL);
UpdateWindow(hWnd);
WSADATA wsa;
if(WSAStartup(MAKEWORD(2,2), &wsa) != 0)
return -1;
SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(listen_sock == INVALID_SOCKET) err_quit("socket()");
retval = WSAAsyncSelect(listen_sock, hWnd,
WM_SOCKET, FD_ACCEPT|FD_CLOSE);
if(retval == SOCKET_ERROR) err_quit("WSAAsyncSelect()");
SOCKADDR_IN serveraddr;
ZeroMemory(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(9000);
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
retval = bind(listen_sock, (SOCKADDR *)&serveraddr, sizeof(serveraddr));
if(retval == SOCKET_ERROR) err_quit("bind()");
retval = listen(listen_sock, SOMAXCONN);
if(retval == SOCKET_ERROR) err_quit("listen()");
// 메시지 루프
MSG msg;
while(GetMessage(&msg, 0, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// 윈속 종료
WSACleanup();
return msg.wParam;
}
// 윈도우 메시지 처리
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg)
{
case WM_SOCKET: // 소켓 관련 윈도우 메시지
ProcessSocketMessage(hWnd, uMsg, wParam, lParam);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
// 소켓 관련 윈도우 메시지 처리
void ProcessSocketMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
// 데이터 통신에 사용할 변수
SOCKETINFO *ptr;
SOCKET client_sock;
SOCKADDR_IN clientaddr;
int addrlen;
int retval;
// 오류 발생 여부 확인
if(WSAGETSELECTERROR(lParam)) {
err_display(WSAGETSELECTERROR(lParam));
RemoveSocketInfo(wParam);
return;
}
// 메시지 처리
switch(WSAGETSELECTEVENT(lParam))
{
case FD_ACCEPT:
addrlen = sizeof(clientaddr);
client_sock = accept(wParam, (SOCKADDR *)&clientaddr, &addrlen);
if(client_sock == INVALID_SOCKET) {
if(WSAGetLastError() != WSAEWOULDBLOCK)
err_display("accept()");
return;
}
printf("[TCP 서버] 클라이언트 접속: IP 주소=%s, 포트 번호=%d\n",
inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
AddSocketInfo(client_sock);
retval = WSAAsyncSelect(client_sock, hWnd, WM_SOCKET, FD_READ|FD_WRITE|
FD_CLOSE);
if(retval == SOCKET_ERROR) {
err_display("WSAAsyncSelect()");
RemoveSocketInfo(client_sock);
}
break;
case FD_READ:
ptr = GetSocketInfo(wParam);
if(ptr->recvbytes > 0) {
ptr->recvdelayed = TRUE;
return;
}
// 데이터 받기
retval = recv(ptr->sock, ptr->buf, BUFSIZE, 0);
if(retval == SOCKET_ERROR) {
if(WSAGetLastError() != WSAEWOULDBLOCK) {
err_display("recv()");
RemoveSocketInfo(wParam);
}
return;
}
ptr->recvbytes = retval;
// 받은 데이터 출력
ptr->buf[retval] = '\0';
addrlen = sizeof(clientaddr);
getpeername(wParam, (SOCKADDR *)&clientaddr, &addrlen);
printf("[TCP/%s:%d] %s\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port),
ptr->buf);
case FD_WRITE:
ptr = GetSocketInfo(wParam);
if(ptr->recvbytes <= ptr->sendbytes)
return;
// 데이터 보내기
retval = send(ptr->sock, ptr->buf + ptr->sendbytes,
ptr->recvbytes - ptr->sendbytes, 0);
if(retval == SOCKET_ERROR) {
if(WSAGetLastError() != WSAEWOULDBLOCK) {
err_display("send()");
RemoveSocketInfo(wParam);
}
return;
}
ptr->sendbytes += retval;
// 받은 데이터를 모두 보냈는지 체크
if(ptr->recvbytes == ptr->sendbytes) {
ptr->recvbytes = ptr->sendbytes = 0;
if(ptr->recvdelayed) {
ptr->recvdelayed = FALSE;
PostMessage(hWnd, WM_SOCKET, wParam, FD_READ);
}
}
break;
case FD_CLOSE:
RemoveSocketInfo(wParam);
break;
}
}
// 소켓 정보 추가
BOOL AddSocketInfo(SOCKET sock)
{
SOCKETINFO *ptr = new SOCKETINFO;
if(ptr == NULL) {
printf("[오류] 메모리가 부족합니다!\n");
return FALSE;
}
ptr->sock = sock;
ptr->recvbytes = 0;
ptr->sendbytes = 0;
ptr->recvdelayed = FALSE;
ptr->next = SocketInfoList;
SocketInfoList = ptr;
return TRUE;
}
// 소켓 정보 얻기
SOCKETINFO *GetSocketInfo(SOCKET sock)
{
SOCKETINFO *ptr = SocketInfoList;
while(ptr)
{
if(ptr->sock == sock)
return ptr;
ptr = ptr->next;
}
return NULL;
}
// 소켓 정보 제거
void RemoveSocketInfo(SOCKET sock)
{
// 클라이언트 정보 얻기
SOCKADDR_IN clientaddr;
int addrlen = sizeof(clientaddr);
getpeername(sock, (SOCKADDR *)&clientaddr, &addrlen);
printf("[TCP 서버] 클라이언트 종료: IP 주소=%s, 포트 번호=%d\n",
inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
SOCKETINFO *curr = SocketInfoList;
SOCKETINFO *prev = NULL;
while(curr)
{
if(curr->sock == sock) {
if(prev)
prev->next = curr->next;
else
SocketInfoList = curr->next;
closesocket(curr->sock);
delete curr;
return;
}
prev = curr;
curr = curr->next;
}
}
// 소켓 함수 오류 출력 후 종료
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);
exit(-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);
printf("[%s] %s", 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);
printf("[오류] %s", (LPCTSTR)lpMsgBuf);
LocalFree(lpMsgBuf);
}
[소스 분석]
간단한 리스트로 클라이언트 접속이 이루어진다. 리스트의 앞부분으로 계속 추가된다.
AddSocketInfo(SOCKET sock) 함수는 클라이언트 접속시 소켓을 추가해주는 함수이다.
데이터를 초기화 하고..
ptr->recvdelayed = FALSE; // 일단 FALSE
ptr->next = SocketInfoList; // 리스트. 다음을 현재 리스트의 앞부분을 가리키게 함
SocketInfoList = ptr; // 리스트 앞부분에 현재 소켓 정보 저장
GetSocketInfo(SOCKET sock) 함수는 소켓을 검색해서 얻는 것이다.
RemoveSocketInfo(SOCKET sock) 함수는 소켓을 삭제하는 함수이다.
간단한 리스트로 구현되어 있으니 Pass ~~
- listen_sock
retval = WSAAsyncSelect(listen_sock, hWnd, WM_SOCKET, FD_ACCEPT|FD_CLOSE);
listen_sock 의 역활은 클라이언트의 접속과 종료를 얻는것이기 때문에 FD_ACCEPT | FD_CLOSE 로 설정해주면 된다.
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg)
{
case WM_SOCKET: // 소켓 관련 윈도우 메시지
ProcessSocketMessage(hWnd, uMsg, wParam, lParam);
return 0;
......
}
}
사용자 정의해준 WM_SOCKET 이벤트 발생시 처리해주는 함수를 호출..
WndProc 함수에는 최대한 간단히 해준다. 코드가 길어지기 때문에 함수 호출..
이 서버자체가 간단한 에코서버 이다 보니 ProcessSocketMessage() 함수 역시 간단하게 해석이
된다.
1. 클라이언트 접속(소켓 추가)
case FD_ACCEPT:
.......
AddSocketInfo(client_sock); // 소켓 추가(연결)
// 쓰기, 읽기, 종료 설정
retval = WSAAsyncSelect(client_sock, hWnd, WM_SOCKET, FD_READ|FD_WRITE|FD_CLOSE);
......
2. 클라이언트로부터 데이터 받기
case FD_READ:
ptr = GetSocketInfo(wParam); // 소켓 검색
if(ptr->recvbytes > 0) { // send 를 해야하므로 return 을 한다는 것이다.
// 처음엔 recvdelayed = FALSE
ptr->recvdelayed = TRUE;
return;
}
retval = recv(ptr->sock, ptr->buf, BUFSIZE, 0);
ptr->recvbytes = retval; // 값을 넣어줬으니 다음에는 WRITE
3. 클라이언트에 데이터 전송
case FD_WRITE:
ptr = GetSocketInfo(wParam);
if(ptr->recvbytes <= ptr->sendbytes)
return;
// 데이터 보내기
retval = send(ptr->sock, ptr->buf + ptr->sendbytes, ptr->recvbytes - ptr->sendbytes, 0);
.....
ptr->sendbytes += retval;
// 받은 데이터를 모두 보냈는지 체크
if(ptr->recvbytes == ptr->sendbytes) {
ptr->recvbytes = ptr->sendbytes = 0;
if(ptr->recvdelayed) {
ptr->recvdelayed = FALSE;
PostMessage(hWnd, WM_SOCKET, wParam, FD_READ);
}
}
........
뭐 간단하니.... Pass~~~ 다 보냈으면 다시 READ
[출처] 소켓 입출력 모델 - WSAAsyncSelect 모델 |작성자 달새
'IT_Programming > Network Programming' 카테고리의 다른 글
[펌] 소켓 입출력 모델 - Overlapped 모델 (0) | 2009.11.18 |
---|---|
[펌] 소켓 입출력 모델 - WSAEventSelect 모델 (0) | 2009.11.18 |
Raw Socket (0) | 2009.11.13 |
[Windows_WinSock2] 조건부 Accept & Scatter/Gather I/O (0) | 2009.11.12 |
성능 향상을 위한 소켓 제어 (0) | 2009.11.12 |