동기(Synchronous)
여러 개의 작업을 순차적으로 진행합니다.
함수를 호출하고 함수의 결과를 호출한 쪽에서 처리합니다.
비동기(Asynchronous)
함수를 호출 했을 때 이 함수의 결과를 호출한 쪽에서 처리하지 않습니다.
블로킹(Blocking)
자신의 수행 결과가 끝날 때 까지 제어권을 갖고 있습니다.
그래서 블로킹 함수는 프로세스가 붙잡혀있습니다.
논 블로킹(Non-Blocking)
호출되면 제어권을 자신을 호출한 쪽으로 넘깁니다.
그래서 프로세스가 붙잡혀있지 않고 다른 일을 할 수 있습니다.
WSAEventSelect
윈속2 버전 이상의 핵심은 비동기 시스템이라고 합니다.
WSA는 Windows Socket Async의 약자입니다.
그래서 WSAEventSelect 모델은 비동기 입출력으로 생각할 수 있지만,
비동기 입출력 모델이 아닙니다. 여전히 동기 입출력 방식으로 소켓을 처리합니다.
그러나 입출력 함수를 안전하게 호출할 수 있는 시점을
운영체제가 알려주기때문에 단순한 입출력 방식보다
편리하게 여러개의 소켓을 처리할 수 있습니다.
운영체제에서 함수 호출의 시점을 알려주기 때문에
비동기처럼 처리할 수 있어서 WSA의 이름이 붙여졌다고합니다.
윈속의 대표적인 비동기 입출력 모델은 Overlapped I/O, IOCP 입니다.
WSAEventSelect 모델 서버 C++ 소스 코드
#include <WinSock2.h>
#include <iostream>
using namespace std;
#pragma comment(lib, "ws2_32.lib")
#define PORT 7890
#define PACKET_LENGTH 1024
int main()
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
// WSAEventSelect 모델은 이벤트 객체를 활용하여 네트워크의 이벤트 상태를 감지한다.
// WSA_MAXIMUM_WAIT_EVENTS(64) 만큼 소켓 배열을 만든다.
SOCKET hSocketArray[WSA_MAXIMUM_WAIT_EVENTS] = {};
// 이벤트 상태를 감지할 수 있는(대기 시킬) 이벤트 배열을 만든다.
WSAEVENT hSocketEventArray[WSA_MAXIMUM_WAIT_EVENTS] = {};
int iSocketCount = 0;
SOCKET hListen = socket(PF_INET, SOCK_STREAM, 0);
SOCKADDR_IN tListenAddr = {};
tListenAddr.sin_family = AF_INET;
tListenAddr.sin_port = htons(PORT);
tListenAddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(hListen, (SOCKADDR*)&tListenAddr, sizeof(tListenAddr));
listen(hListen, SOMAXCONN);
// 네트워크의 이벤트 상태를 감지할 이벤트 객체를 생성한다.
WSAEVENT hEvent = WSACreateEvent();
// 생성한 리슨 소켓과 이벤트 객체를 묶어준다.
// 세 번째 인자는 이벤트가 감지할 이벤트의 종류를 지정해주는 것이다.
// 이를 통해 리슨 소켓은 접속 요청과 접속 종료 이벤트를 감지할 수 있게 된다.
WSAEventSelect(hListen, hEvent, FD_ACCEPT | FD_CLOSE);
// 바인딩된 소켓과 이벤트의 종류가 지정된 이벤트 객체를 소켓 배열에 넣어준다.
hSocketArray[iSocketCount] = hListen;
hSocketEventArray[iSocketCount] = hEvent;
++iSocketCount;
// ioctlsocket : 소켓의 입출력 모드를 변경한다.
// socket 함수는 기본적으로 블로킹 소켓을 생성한다.
// 때문에 현재 hListen 소켓은 블로킹 소켓으로 설정되어 있으므로
// 이를 논블로킹 소켓으로 바꿔준다. (리슨 소켓을 논블로킹 소켓으로 바꿔준다)
// 하나의 프로세스에서만 잡히게 될 리슨 소켓의 입출력 상황을 감지해주는 방식을 바꿔준 것 뿐이다.
// 두 번째 인자는 소켓(hListen)이 수행할 커맨드를 전달한다.
// FIONBIO : 작업을 수행할 소켓을 논블로킹 모드로 활성화한다.
// 반환값 : 성공 0, 실패 SOCKET_ERROR(-1) 반환
// WSAGetLastError 함수를 이용해서 특정한 에러 코드도 얻을 수 있다.
u_long on = TRUE;
ioctlsocket(hListen, FIONBIO, &on);
// WSANETWORKEVENTS : 발생한 이벤트 종류를 감지해주는 구조체이다.
// 이를위해 네트워크 이벤트 종류를 저장한다.
/*
typedef struct _WSANETWORKEVENTS {
long lNetworkEvents; // 발생한 네트워크 이벤트를 저장한다.
int iErrorCode[FD_MAX_EVENTS]; // 오류가 발생했을 때 오류 정보를 저장한다.
} WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;
*/
WSANETWORKEVENTS wsaNetEvents;
while (true)
{
// WSAWaitForMultipleEvents : 발생한 이벤트를 감시한다.
// 이벤트 상태가 시그널 상태가 될 때 까지 대기한다.
// 이벤트 객체의 신호 상태를 감지하는 것이다.
// arg 1) DWORD cEvents : 대기할 이벤트 개수
// arg 2) const WSAEVENT *lphEvents : 대기할 이벤트 배열
// arg 3) BOOL fWaitAll : TRUE이면 모든 객체가 시그널 상태가 되기 전 까지 대기,
// FALSE이면 한 이벤트 객체가 신호상태이면 리턴
// arg 4) DWORD dwTimeout : 이벤트 객체 대기상태 대기 시간
// arg 5) BOOL fAlertable : 스레드 실행에 대한 입출력 완료 루틴을 설정한다. 패스.
// 반환값 : 성공(이벤트가 발생한 이벤트 객체 배열의 인덱스 값)
// 타임 아웃(WSA_WAIT_TIMEOUT : 지금은 INIFITE로 했으므로 이벤트가 발생했을 때만 리턴),
// 실패(WSA_WAIT_FAILED)
DWORD dwIndex = WSAWaitForMultipleEvents(iSocketCount, hSocketEventArray, FALSE,
WSA_INFINITE, FALSE);
if (dwIndex == WSA_WAIT_FAILED)
continue;
// 리턴 받은 이벤트 객체의 인덱스에서 WSA_WAIT_EVENT_0를 빼주면
// 몇 번째 인덱스에서 이벤트가 발생했는지 알 수 있게 된다.
dwIndex -= WSA_WAIT_EVENT_0;
// WSAEnumNetworkEvents : 최종적으로 구한 인덱스를 통해 발생한 이벤트의 종류를 알아낸다.
// arg 1) SOCKET s : 발생한 이벤트를 알고자하는 소켓
// arg 2) WSAEVENT hEventObject : 발생한 이벤트를 알고자하는 이벤트 핸들
// arg 3) LPWSANETWORKEVENTS lpNetworkEvents : 발생한 이벤트 종류 기록을 채워넣을 구조체
// 발생한 에러 또한 담길수도 있다.
// MSDN 참조
// https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsaenumnetworkevents
if (WSAEnumNetworkEvents(hSocketArray[dwIndex], hSocketEventArray[dwIndex],
&wsaNetEvents) == SOCKET_ERROR)
continue;
/* WSAEnumNetworkEvents 함수의 인자로 wsaNetEvents 구조체 포인터를 전달해서
wsaNetEvents.lNetworkEvents에 발생한 이벤트의 종류가 담겨있다.
이 이벤트 종류를 바탕으로 처리한다. */
// 클라이언트 접속 이벤트가 발생했을 때
// FD_ACCEPT 이벤트를 처리한다.
if (wsaNetEvents.lNetworkEvents & FD_ACCEPT)
{
// wsaNetEvents.iErrorCode : 여기에는 이벤트 에러가 발생했을 때 기록된다.
// 에러가 발생했을 때 lNetworkEvents의 FD_ACCEPT는 iErrorCode의 FD_ACCEPT_BIT와 대응된다.
// 그 밖에 대응되는 매크로 함수
/*
[ lNetworkEvents ] [ lNetworkEvents ]
FD_ACCEPT FD_ACCEPT_BIT
FD_READ FD_READ_BIT
FD_WRITE FD_WRITE_BIT
FD_CLOSE FD_CLOSE_BIT
FD_CONNECT FD_CONNECT_BIT
FD_OOB FD_OOB_BIT
*/
if (wsaNetEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
{
// 에러 로그를 출력하고 다음 순서를 진행한다.
cout << "Accept Error !!" << endl;
continue;
}
// 접속을 성공했으면 accept 소켓을 생성한다.
SOCKADDR_IN tAddr = {};
int iSize = sizeof(tAddr);
SOCKET hSocket = accept(hSocketArray[dwIndex], (SOCKADDR*)&tAddr, &iSize);
// 접속 실패시 모든 소켓을 종료한다.
if (hSocket == SOCKET_ERROR)
{
for (int i = 0; i < iSocketCount; ++i)
{
closesocket(hSocketArray[i]);
}
WSACleanup();
return 0;
}
cout << hSocket << " Client Connect" << endl;
// 이벤트 객체를 생성해서 접속된 클라이언트 소켓과 이벤트 객체를 묶어준다.
hEvent = WSACreateEvent();
// FD_READ : 데이터 수신이 가능하면 윈도우 메시지를 발생시키는 이벤트 종류를 지정한다.
// FD_CLOSE : 상대가 접속을 종료할 수 있는 이벤트 종류를 지정한다.
WSAEventSelect(hSocket, hEvent, FD_READ | FD_CLOSE);
hSocketArray[iSocketCount] = hSocket;
hSocketEventArray[iSocketCount] = hEvent;
++iSocketCount;
// 접속을 성공한 클라에게 send
send(hSocket, "접속 성공", 9, 0);
}
// Recv 이벤트를 검사한다.
if (wsaNetEvents.lNetworkEvents & FD_READ)
{
if (wsaNetEvents.iErrorCode[FD_READ_BIT] != 0)
{
cout << "Recv Error !!" << endl;
continue;
}
char strPacket[PACKET_LENGTH] = {};
int iLength = recv(hSocketArray[dwIndex], strPacket, PACKET_LENGTH, 0);
// 클라이언트에서 send한 Packet 메시지를 출력한다.
cout << hSocketArray[dwIndex] << " Client Message : "
<< strPacket << endl;
// 접속하고 있는 다른 유저에게도 클라에서 전송한 메시지를 보여준다.
for (int j = 0; j < iSocketCount; ++j)
{
// 리슨 소켓 무시
if (hListen == hSocketArray[j])
continue;
// 패킷 메시지를 송신한 자기 자신 무시
else if (hSocketArray[dwIndex] == hSocketArray[j])
continue;
send(hSocketArray[j], strPacket, iLength, 0);
}
}
// close 이벤트가 발생했을 경우
if (wsaNetEvents.lNetworkEvents & FD_CLOSE)
{
if (wsaNetEvents.iErrorCode[FD_CLOSE_BIT] != 0)
{
cout << "Close Error !!" << endl;
continue;
}
// 소켓을 닫는다.
closesocket(hSocketArray[dwIndex]);
// 개방되어있는 이벤트 객체 핸들을 닫는다.
WSACloseEvent(hSocketEventArray[dwIndex]);
// 배열의 뒤에 있는 소켓을 앞으로 한 칸씩 땡겨준다.
for (DWORD j = dwIndex; j < iSocketCount - 1; ++j)
{
hSocketArray[j] = hSocketArray[j + 1];
hSocketEventArray[j] = hSocketEventArray[j + 1];
}
--iSocketCount;
}
}
// 모든 소켓을 종료한다.
for (int i = 0; i < iSocketCount; ++i)
{
closesocket(hSocketArray[i]);
WSACloseEvent(hSocketEventArray[i]);
}
WSACleanup();
return 0;
}
실행 결과
클라이언트 C++ 소스 코드는 멀티 스레드, 크리티컬 섹션 포스팅에 있는
소스 코드와 동일합니다. (https://pony11.tistory.com/14?category=868582)
'윈도우즈 소켓 프로그래밍 기초' 카테고리의 다른 글
IOCP (I/O Completion Port) (0) | 2020.01.03 |
---|---|
Overlapped I/O (0) | 2019.12.29 |
I/O 멀티플렉싱 (fd_set, select) (0) | 2019.12.29 |
멀티 스레드, 크리티컬 섹션 동기화 (0) | 2019.12.29 |
TCP / UDP 통신 (0) | 2019.12.27 |