이 모델은 이전의 Select 계열의 모델보다 더 한방에 이해하기 어려운 것 같다.
제대로 이해하고 쓰는 것이 아니라, 잊어먹기 전에 최대한 메모해 둔다는 느낌으로 작성한다.
Overlapped 모델엔 이벤트 기반과 콜백 기반 2가지 방식이 있다.
이벤트 기반에 대해서 먼저 작성한다.
1. Overlapped 모델의 개념
Overlapped 모델은 기본적으로 비동기와 논 블로킹이 조합된 형태의 모델이다.
반면에 이전의 Select 계열 모델들은 논 블로킹이었지만 비동기는 아니었다.
Ovelapped 모델은 아래와 같이 동작한다.
- Overlapped 모델은 미리 함수를 걸어(실행) 둔다.
WSARecv, WSASend - 함수가 성공했는지 확인
2-1. 성공했다면 결과를 가져와서 처리
2-2. 실패했다면 그 이유를 확인
의 흐름을 가진다.
미리 함수를 걸어둔다는 것은 이전의 Select 모델을 생각해 보면 이해가 가능하다.
함수가 완전히 실행할 준비가 됐는지 확인을 하는 것과,
미리 실행하는 것은 상당히 다르다.
이를 Reactor Pattern과 Proactor Pattern으로 나누어 부를 수 있다.
- Reactor Pattern
뒤늦게 처리한다는 의미로, 일반적인 논 블로킹 소켓이라 볼 수 있다.
소켓의 상태를 확인 후 「뒤늦게」 recv나 send를 호출한다. - Proactor Pattern
미리라는 의미로 볼 수 있다. Overlapped 모델이 이에 속한다.
1.1. WSAOVERLAPPED의 구조
typedef struct _OVERLAPPED {
ULONG_PTR Internal;
ULONG_PTR InternalHigh;
union {
struct {
DWORD Offset;
DWORD OffsetHigh;
} DUMMYSTRUCTNAME;
PVOID Pointer;
} DUMMYUNIONNAME;
HANDLE hEvent;
} OVERLAPPED, *LPOVERLAPPED;
뭐가 참 많은데 맨 아래 핸들 말고는 우리는 몰라도 상관없다.
윗동네는 OS가 알아서 지지고 볶고 할 것이기 때문에,
우린 이벤트 핸들만 갖고 놀면 된다.
2. WSASend, WSARecv
Overlapped 모델에 사용되는 비동기 입출력 함수이다.
먼저 이들의 구조에 대해 알아보자.
WSASend
#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
WSASend(
_In_ SOCKET s,
_In_reads_(dwBufferCount) LPWSABUF lpBuffers,
_In_ DWORD dwBufferCount,
_Out_opt_ LPDWORD lpNumberOfBytesSent,
_In_ DWORD dwFlags,
_Inout_opt_ LPWSAOVERLAPPED lpOverlapped,
_In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
#endif /* INCL_WINSOCK_API_PROTOTYPES */
WSARecv
#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
WSARecv(
_In_ SOCKET s,
_In_reads_(dwBufferCount) __out_data_source(NETWORK) LPWSABUF lpBuffers,
_In_ DWORD dwBufferCount,
_Out_opt_ LPDWORD lpNumberOfBytesRecvd,
_Inout_ LPDWORD lpFlags,
_Inout_opt_ LPWSAOVERLAPPED lpOverlapped,
_In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
#endif /* INCL_WINSOCK_API_PROTOTYPES */
둘의 차이가 거의 없다는 것을 알 수 있다.
차이는 WSASend는 flags에 대해 DWORD를 그대로 받고 WSARecv는 DWORD의 포인터를 받는다는 차이 정도이다.
CompletionRoutine은 나중에 콜백 기반 모델에서 사용할 예정이다.
3. 이벤트 기반 Overlapped 모델
먼저 클라이언트와 연결해 보자.
그전에 Session 구조체에 수정이 필요하다.
기존 구조체에 Overlapped 대응을 위한 값을 추가해 줘야 한다.
struct Session
{
WSAOVERLAPPED overlapped = {}; <-- 이 녀석!
SOCKET socket = INVALID_SOCKET;
char recvBuffer[BUFSIZE] = {};
int32 recvBytes = 0;
int32 sendBytes = 0;
};
WSAOVERLAPPED overlapped를 추가했다.
while(true)
{
SOCKADDR_IN clientAddr;
int32 addrLen = sizeof(clientAddr);
SOCKET clientSocket;
while(true)
{
clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
if (clientSocket != INVALID_SOCKET)
break;
if (::WSAGetLastError() == WSAEWOULDBLOCK)
continue;
return 0;
}
Session session = Session{ clientSocket };
WSAEVENT wsaEvent = ::WSACreateEvent();
session.overlapped.hEvent = wsaEvent;
cout << "Client Connected!" << endl;
while(true)
{
// 입력 처리
}
늘 하던 대로 accept를 해 주고
통지를 받기 위한 wsaEvent 핸들을 하나 만들어 준다.
그리고 아까 Session 구조체에 추가한 overlapped의 hEvent에 핸들을 넣어준다.
이제 저 while문을 채울 것이다.
while(true)
{
WSABUF wsaBuf;
wsaBuf.buf = session.recvBuffer;
wsaBuf.len = BUFSIZE;
DWORD recvLen = 0;
DWORD flags = 0;
if(::WSARecv(clientSocket, &wsaBuf, 1, &recvLen, &flags, &session.overlapped, nullptr) == SOCKET_ERROR)
{
if (::WSAGetLastError() == WSA_IO_PENDING)
{
// Pending
::WSAWaitForMultipleEvents(1, &wsaEvent, TRUE, WSA_INFINITE, FALSE);
::WSAGetOverlappedResult(session.socket, &session.overlapped, &recvLen, FALSE, &flags);
}
}
cout << "Data Recv Len = " << recvLen << endl;
cout << "Data = " << session.recvBuffer << endl;
}
::closesocket(session.socket);
::WSACloseEvent(wsaEvent);
저 WSABUF라는 친구는 단순하다.
typedef struct _WSABUF {
ULONG len; /* the length of the buffer */
_Field_size_bytes_(len) CHAR FAR *buf; /* the pointer to the buffer */
} WSABUF, FAR * LPWSABUF;
정말 단순히 길이와 char형의 버퍼만을 가지고 있다.
지금은 세션이 하나뿐이라 WSABUF를 굳이 배열형태로 만들지 않았지만,
여러 세션을 처리해야 할 때는 배열형으로 각 세션마다 버퍼를 던져줘야만 한다.
저 버퍼의 내용을 여러 소켓이 공유하는 것 자체에 문제가 있을뿐더러,
동작하는 중에 저 버퍼의 내용이 오염되면 제대로 된 결과를 받을 수 없기 때문이다.
미리 함수를 걸어 놨지만, 버퍼에 내용이 없어 처리하지 못할 수도 있다.
그때 WSA_IO_PENDING이 발생하고 우린 이를 캐치해 기다릴 수 있다.
이벤트 기반이기 때문에 이전에 봤던 ::WSAWaitForMultipleEvents()를 사용해 기다리고,
::WSAGetOverlappedResult()를 실행해 결과를 받아온다.
이 함수의 구조를 보자.
#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
BOOL
WSAAPI
WSAGetOverlappedResult(
_In_ SOCKET s,
_In_ LPWSAOVERLAPPED lpOverlapped,
_Out_ LPDWORD lpcbTransfer,
_In_ BOOL fWait,
_Out_ LPDWORD lpdwFlags
);
#endif /* INCL_WINSOCK_API_PROTOTYPES */
소켓은 소켓.
overlapped 포인터를 받고,
수신데이터의 Length의 포인터를 넘겨주고,
함수가 다른 보류 중 작업이 완료될 때까지 기다릴지에 대한 여부. 지금은 필요 없으므로 FALSE.
플래그도 지금 필요가 없으므로 0을 넘겨준다.
자 이렇게 모든 문제가 없이 함수가 다 실행됐을 때 아래의 결과를 뱉어준다.
3.1. 결과
데이터가 잘 전송됐다.
4. 콜백 함수 기반 Overlapped 모델
먼저 콜백 기반의 모델의 동작을 확인해 보자.
- 비동기 IO를 지원하는 소켓 생성
- 비동기 IO 함수 호출
이때 완료 루틴의 시작 주소를 넘겨준다. - 작업이 바로 완료되지 않으면, WSA_IO_PENDING 에러 반환
- 비동기 IO 함수를 호출한 스레드를 Alertable Wait 상태로 만든다.
ex) WaitForSingleObjectEx, WaitForMultipleObjectsEx, SleepEx, WSAWAitForMultipleEvents - 비동기 IO가 완료되면, OS는 완료 루틴을 호출
- 완료 루틴 호출이 완료되면, 스레드는 Alertable Wait 상태에서 빠져나온다.
예전에 본 bAlertable이라는 부울 파라미터를 활용한다고 유추해 볼 수 있겠다.
그리고 이전에 잠시 그 존재를 확인했던 Completion Routine의 구조에 대해 알아보자.
typedef
void
(CALLBACK * LPWSAOVERLAPPED_COMPLETION_ROUTINE)(
IN DWORD dwError,
IN DWORD cbTransferred,
IN LPWSAOVERLAPPED lpOverlapped,
IN DWORD dwFlags
);
#if(_WIN32_WINNT >= 0x0501)
- dwError
오류 발생 시 0이 아닌 값을 가진다. - cbTrasnferred
전송된 바이트 수를 가진다. - lpOverlapped
비동기 IO 함수 호출 시 넘겨준 WSAOVERLAPPED 구조체의 주소값 - dwFlags
여기선 사용하지 않을 것이기 때문에 0으로 밀어둘 것이다.
일단 콜백 함수 기반이기 때문에 콜백 함수를 만들어 주어야 한다.
이번엔 아래와 같이 작성했다.
void CALLBACK RecvCallback(DWORD error, DWORD recvLen, LPWSAOVERLAPPED overlapped, DWORD flags)
{
// 구조체에서 overlapped를 가장 위로 올려놨기 때문에
// 캐스팅이 가능
Session* session = (Session*)overlapped;
cout << "Data Recv Len Callback = " << recvLen << endl;
cout << "Data Recv Data Callback = " << session->recvBuffer << endl;
// TODO : 에코 서버를 만든다면 WSASend()
}
아까 Session 구조체를 수정할 때 overlapped를 가장 위로 올려놓았던 것을 기억하자.
그러므로 Sessions 구조체의 메모리 시작점은 overlapped의 주소를 가리키게 된다.
따라서 그냥 Session으로 캐스팅해도 다른 데이터들은 건드리지 않고 overlapped만 받아올 수 있다.
이벤트 기반 모델에 비해 코드 상의 차이가 매우 크다고는 할 수 없다.
if (::WSARecv(clientSocket, &wsaBuf, 1, &recvLen, &flags, &session.overlapped, RecvCallback) == SOCKET_ERROR)
{
if (::WSAGetLastError() == WSA_IO_PENDING)
{
// Pending
// Alertable Wait
// 스레드별이기 때문에 효율성의 문제가 있을 수 있음
::SleepEx(INFINITE, TRUE);
//::WSAWaitForMultipleEvents(1, &wsaEvent, TRUE, WSA_INFINITE, TRUE);
}
else
{
// TODO : 문제 있는 상황
break;
}
}
else
{
cout << "Data Recv Len = " << recvLen << endl;
}
기존 nullptr로 밀어두던 CompletionRoutine에 콜백 함수를 넘겨줬다.
여기서 ::SleppEx() 함수가 새로이 등장했다.
WINBASEAPI
DWORD
WINAPI
SleepEx(
_In_ DWORD dwMilliseconds,
_In_ BOOL bAlertable
);
대기시간은 무한으로 밀어주고, TRUE로 해 주어 Alertable 상태로 빠지게 해 준다.
이제 호출될 콜백 함수가 있는지 판별하고 뭔가 있을 때까지 계속 대기할 것이다.
여기서 APC에 대해 정말 얕게 알아보자.
APC큐라는 것이 스레드마다 생긴다.
저 큐에 콜백 함수들을 때려 넣어둔다.
여기서 SleepEx() 같은 함수들을 통해 Alertable 상태로 넘어가면,
APC큐에 처리할 수 있는 뭔가가 있다고 했을 때, 그것들을 싸그리 처리하고 완료 루틴으로 넘어가게 된다.
큐에 하나라도 남아있다면 빠져나올 수 없다.
이제 Alertable 상태를 빠져나와서 스레드에 있는 나머지 코드들을 실행하게 되는 것이다.
4.1. 결과
여하튼 잘 실행되는지 결과를 보자.
콜백으로 넘어가서 결과를 잘 뱉어주는 것을 확인할 수 있다.
5. 다른 모델과의 비교
여기까지 4가지의 모델에 대해 알아보았다.
간략하게 장단을 비교해 보자.
- Select 모델
- 장점 : 윈도우/리눅스 공통. 크로스 플랫폼이라는 것이 큰 장점이라 할 수 있다.
- 단점 : 성능이 매우 낮다. 64개 제한도 치명적이다. - WSAEventSelect 모델
- 장점 : Select 모델에 비해 비교적 뛰어난 성능 <- 장점일까?
- 단점 : 64개 제한은 여전히 치명적이다. - Overlapped (이벤트)
- 장점 : 성능은 좋다고 할 수 있다.
- 단점 : 이벤트 기반이기 때문에 여전히 치명적인 64개 제한을 가진다. - Overlapped (콜백)
- 장점 : 이 중에서 성능은 꽤 봐줄 만하다.
- 단점 : 모든 비동기 소켓 함수에서 사용이 가능하진 않다. (accept) / 빈번한 Alertable Wait로 인한 성능 저하
accept에서 왜 사용이 불가능 한지는 아래의 함수의 구조를 참고해 알아보자.
BOOL AcceptEx(
[in] SOCKET sListenSocket,
[in] SOCKET sAcceptSocket,
[in] PVOID lpOutputBuffer,
[in] DWORD dwReceiveDataLength,
[in] DWORD dwLocalAddressLength,
[in] DWORD dwRemoteAddressLength,
[out] LPDWORD lpdwBytesReceived,
[in] LPOVERLAPPED lpOverlapped
);
이는 AcceptEx() 함수의 구조인데, 어디를 봐도 콜백 루틴 걸어줄 곳이 없다는 것을 알 수 있다.
그냥 써먹을 수가 없다는 소리.
그리고 Alertable Wait로 인한 성능 저하에 대해선 이렇게 볼 수도 있다.
APC큐는 스레드 별로 있다고 했다.
1스레드의 APC큐엔 할 일이 많고 나머지 9개 스레드가 할 일이 없이 논다고 해도,
1스레드의 일 처리를 다른 스레드가 도와줄 수 없다.
하나만 일하고 나머지는 노는 매우 비효율적인 상태가 일어날 수도 있다.
하지만 이런 문제들은 IOCP ( Completion Port ) 모델에서 많은 부분이 개선되거나 해결될 수 있다.
6. 마무리
다음엔 Completion Port 모델에 대해 공부하고, 이에 더 나아가,
IOCP를 기반한 네트워크 라이브러리를 만들어 보는 형태로 공부해 갈 것이다.
지금까지의 모델들보다 확실히 어렵겠지만 즐겁게 공부해 보도록 해야겠다.
7. Reference
'Study > C++ & C#' 카테고리의 다른 글
[C++] Socket util 클래스 작성 (0) | 2023.06.20 |
---|---|
[C++] Completion Port Model (0) | 2023.06.17 |
[C++] WSAEventSelect Model (0) | 2023.06.14 |
[C++] Select Model (0) | 2023.06.12 |
[C++] STL : Vector (0) | 2023.04.18 |