Chapter 22 - Overlapped IO 모델
Chapter 22 - Overlapped IO 모델
22-1 : Overlapped IO 모델의 이해
IO의 중첩 : 하나의 쓰레드 내에서 동시에 둘 이상의 영역으로 데이터를 송수신함으로써 입출력이 중첩되는 상황
- 입출력 함수가 호출 즉시 반환해야함 - 비동기 IO가 필요
- 따라서 입출력 함수는 논블로킹 모드로 동작해야함
Overlapped IO
에서는 입출력 자체보다 입출력이 완료된 상황의 확인이 중요
Overlapped IO
에 적합한 소켓을 생성하는데는 WSASocket()
함수를 사용
#include <winsock2.h>
SOCKET WSASocket(int af, int type, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWORD dwFlags);
// 성공시 소켓 핸들, 실패시 INVALID_SOCKET 반환
af
: 프로토콜 체계 정보type
: 소켓의 데이터 전송방식protocol
: 두 소켓 사이에 사용되는 프로토콜 정보lpProtocolInfo
: 생성되는 소켓의 특성을 정의하는WSAPROTOCOL_INFO
구조체 변수의 주소값g
: 소켓 그룹 ID, 0 입력시 그룹 작업이 수행되지 않음dwFlags
: 소켓의 추가 속성정보,WSA_FLAG_OVERLAPPED
전달시Overlapped IO
를 지원하는 소켓을 생성
Overlapped IO
에서 두 소켓간의 연결은 기존과 동일, 데이터의 입출력에 차이가 존재
Overlapped IO
에서 데이터 송신에는 WSASend()
함수를 사용
#include <winsock2.h>
int WSASend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
// 성공시 0, 실패시 SOCKET_ERROR 반환
typedef struct __WSABUF
{
u_long len; // 전송할 데이터 크기
char FAR *buf; // 버퍼 주소값
} WSABUF, *LPWSABUF;
typedef struct _WSAOVERLAPPED
{
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
WSAEVENT hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;
lpBuffers
: 전송할 데이터 정보를 가진WSABUF
구조체 배열의 주소값dwBufferCount
:lpBUffers
로 전달된 배열의 길이lpNumberOfBytesSent
: 전송될 바이트 수가 저장될 변수의 주소값dwFlags
: 함수의 데이터 전송특성 지정lpOverlapped
:WSAOVERLAPPED
구조체 변수의 주소값,Event
오브젝트를 사용해 데이터 전송의 완료를 확인하는데 사용lpCompletionRoutine
: 데이터 전송의 완료를 확인하기 위한Completion Routine
함수의 주소값 전달
Overlapped IO
를 진행하기 위해서는 WSASend()
함수의 매개변수 lpOverlapped
의 값이 항상 유효한 구조체 변수의 주소값이여야함
또한 WSASend()
함수를 통해 동시에 둘 이상의 영역으로 데이터를 전송하는 경우 lpOverlapped
구조체 변수를 각각 별도로 구성해야함
WSASend()
함수를 사용한 데이터 전송 예시
WSAEVENT event;
WSAOVERLAPPED overlapped;
WSABUF dataBuf;
char buf[BUF_SIZE] = {"data to send"}
int recvBytes = 0;
...
event = WSACreateEvent();
memset(&overlapped, 0, sizeof(overlapped)); // 모든 비트 0으로 초기화
overlapped.hEvent = event;
dataBuf.len = sizeof(buf);
dataBUf.buf = buf;
WSASend(hSocket, &dataBuf, 1, &recvBytes, 0, &overlapped, NULL);
...
전송하는 데이터의 크기가 크지 않은 경우 함수호출과 동시에 데이터 전송이 완료되어 WSASend()
함수는 0을 반환하고 lpNumberOfByteSent
로 실제 전송된 데이터의 크기정보가 저장됨
반면 계속해서 데이터의 전송이 이루어진다면 WSASend()
함수는 SOCKET_ERROR
를 반환하고 WSAGetLastError()
함수를 통해 확인가능한 오류코드에 WSA_IO_PENDING
이 등록되며, WSAGetOverlappedResult()
함수를 통해 실제 전송된 데이터 크기를 확인해야함
#include <winsock2.h>
BOOL WSAGetOverlappedResult(SOCEKT s, LPWSAOVERLAPPED lpOverlapped, LPDWORD lpcbTransfer, BOOL fWait, LPDWORD lpdwFlags);
// 성공시 TRUE, 실패시 FALSE 반환
lpcbTransfer
: 실제 송수신된 바이트 크기를 저장할 변수의 주소값fWait
: 여전히 IO가 진행중인 경우 - TRUE일시 완료될 때까지 대기, FALSE일시 즉시 FALSE를 반환lpdwFlags
:WSARecv()
함수 호출시 부수적인 정보를 얻기 위해 사용, 불필요할시 NULL
Overlapped IO
에서 데이터 수신에는 WSARecv()
함수를 사용
#include <winsock2.h>
int WSARecv(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
// 성공시 0, 실패시 SOCKET_ERROR 반환
여러 데이터를 모아서 한번에 전송하고 수신된 데이터를 여러 버퍼에 나눠서 저장하는 것을 Gather/Scatter IO
라고 함
리눅스에서는 writev()
, readv()
함수가 이를 지원하고 윈도우에서는 WSASend()
, WSARecv()
함수를 사용할 수 있음
22-2 : Overlapped IO에서의 입출력 완료의 확인
Event
오브젝트 기반 : lpOVerlapped
활용
CompetionRoutine
기반 : lpCompletionRoutine
활용
Event
오브젝트 활용 Sender 예시
// OverlappedSend_win.cpp
#include <iostream>
#include <winsock2.h>
using namespace std;
void Errorhandling(char *msg);
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN sendAdr;
WSABUF dataBuf;
char msg[] = "Network is Computer!";
int sendBytes = 0;
WSAEVENT evObj;
WSAOVERLAPPED overlapped;
if(argc != 3)
{
cout << "Usage : " << argv[0] << " <IP> <port>\n";
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error");
hSocket = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
memset(&sendAdr, 0, sizeof(sendAdr));
sendAdr.sin_family = AF_INET;
sendAdr.sin_addr.s_addr = inet_addr(argv[1]);
sendAdr.sin_port = htons(atoi(argv[2]));
if (::connect(hSocket, (SOCKADDR*)&sendAdr, sizeof(sendAdr)) == SOCKET_ERROR)
ErrorHandling("connect() error");
evObj = WSACreateEvent();
memset(&overlapped, 0, sizeof(overlapped));
overlapped.hEvent = evObj;
dataBuf.len = strlen(msg) + 1;
dataBuf.buf = msg;
if (WSASend(hSocket, &dataBuf, 1, &sendBytes, 0, &overlapped, NULL) == SOCKET_ERROR)
{
if (WSAGetLastError() == WSA_IO_PENDING)
{
cout << "Background data send\n";
WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);
WSAGetOverlappedResult(hSocket, &overlapped, &sendBytes, FALSE, NULL);
}
else
ErrorHandling("WSASend() error");
}
cout << "Send data size : " << sendBytes << endl;
WSACloseEvent(evObj);
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
- IO가 완료되면
WSAOVERLAPPED
구조체 변수가 참조하는Event
오브젝트가signaled
상태가 됨 - IO의 완료 및 결과 확인을 위해
WSAGetOverlappedResult()
함수를 사용
Event
오브젝트 활용 Receiver 예시
// OverlappedRecv_win.cpp
#include <iostream>
#include <winsock2.h>
using namespace std;
#define BUF_SIZE 1024
void Errorhandling(char *msg);
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hLisnSock, hRecvSock;
SOCKADDR_IN lisnAdr, recvAdr;
int recvAdrSz;
WSABUF dataBuf;
WSAEVENT evObj;
WSAOVERLAPPED overlapped;
char buf[BUF_SIZE];
int recvBytes = 0, flags = 0;
if(argc != 2)
{
cout << "Usage : " << argv[0] << " <port>\n";
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error");
hLisnSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
memset(&lisnAdr, 0, sizeof(lisnAdr));
lisnAdr.sin_family = AF_INET;
lisnAdr.sin_addr.s_addr = htonl(INADDR_ANY);
lisnAdr.sin_port = htons(atoi(argv[1]));
if (::bind(hLisnSock, (SOCKADDR*)&lisnAdr, sizeof(lisnAdr)) == SOCKET_ERROR)
ErrorHandling("bind() error");
if (::listen(hLisnSock, 5) == SOCKET_ERROR)
ErrorHandling("listen() error");
recvAdrSz = sizeof(recvAdr);
hRecvSock = accept(hLisnSock, (SOCKADDR*)&recvAdr, &recvAdrSz);
evObj = WSACreateEvent();
memset(&overlapped, 0, sizeof(overlapped));
overlapped.hEvent = evObj;
dataBuf.len = BUF_SIZE;
dataBuf.buf = msg;
if (WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, NULL) == SOCKET_ERROR)
{
if (WSAGetLastError() == WSA_IO_PENDING)
{
cout << "Background data receive\n";
WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);
WSAGetOverlappedResult(hRecvSock, &overlapped, &recvBytes, FALSE, NULL);
}
else
ErrorHandling("WSARecv() error");
}
cout << "Received message : " << buf << endl;
WSACloseEvent(evObj);
closesocket(hRecvSock);
closesocket(hLisnSock);
WSACleanup();
return 0;
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
Completion Routine
방식 : Pending됭 IO 완료시 자동으로 호출된 함수를 등록
- 단, IO를 요청한 쓰레드가
alertable wait
상태에 놓여있을 때만 루틴을 호출함 WaitForSingleObjectEx()
,WaitForMultipleObjectsEx()
,WSAWaitForMultipleEvents()
,SleepEx()
함수 호출시 쓰레드가alertable wait
상태가 됨Competion Routine
이 실행되면 위 함수들은WAIT_IO_COMPLETION
을 반환하면서 함수를 빠져나옴
Competion Routine
활용 Receiver 예시
// CmplRoutineRecv_win.cpp
#include <iostream>
#include <winsock2.h>
using namespace std;
#define BUF_SIZE 1024
void CALLBACK CompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void ErrorHandling(char *msg);
WSABUF dataBuf;
char buf[BUF_SIZE];
int recvBytes = 0;
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hLisnSock, hRecvSock;
SOCKADDR_IN lisnAdr, recvAdr;
WSAEVENT evObj;
WSAOVERLAPPED overlapped;
int recvAdrSz, flags = 0;
if(argc != 2)
{
cout << "Usage : " << argv[0] << " <port>\n";
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error");
hLisnSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
memset(&lisnAdr, 0, sizeof(lisnAdr));
lisnAdr.sin_family = AF_INET;
lisnAdr.sin_addr.s_addr = htonl(INADDR_ANY);
lisnAdr.sin_port = htons(atoi(argv[1]));
if (::bind(hLisnSock, (SOCKADDR*)&lisnAdr, sizeof(lisnAdr)) == SOCKET_ERROR)
ErrorHandling("bind() error");
if (::listen(hLisnSock, 5) == SOCKET_ERROR)
ErrorHandling("listen() error");
recvAdrSz = sizeof(recvAdr);
hRecvSock = accept(hLisnSock, (SOCKADDR*)&recvAdr, &recvAdrSz);
if (hRecvSock == INVALID_SOCKET)
Errorhandling("accept() error");
evObj = WSACreateEvent(); // Dummy event object
memset(&overlapped, 0, sizeof(overlapped));
dataBuf.len = BUF_SIZE;
dataBuf.buf = msg;
if (WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, CompRoutine) == SOCKET_ERROR)
{
if (WSAGetLastError() == WSA_IO_PENDING)
cout << "Background data receive\n";
}
idx = WSAWaitForMultipleEvents(1, &evObj, FALSE, WSA_INFINITE, TRUE);
if (idx == WAIT_IO_COMPLETION)
cout << "Overlapped I/O Completed\n";
else // if error occurred!
ErrorHandling("WSARecv() error");
WSACloseEvent(evObj);
closesocket(hRecvSock);
closesocket(hLisnSock);
WSACleanup();
return 0;
}
void CALLBACK CompRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVErLAPPED lpOverlapped, DWORD flags)
{
if (dwError != 0)
ErrorHandling("CompRoutine error");
else
{
recvBytes = szRecvBytes;
cout << "Received message : " << buf << endl;
}
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
내용 확인문제
-
Asnchronous(비동기)
Notification IO
모델과Overlapped IO
모델 사이에서 비동기로 처리되는 영역이 어떻게 차이가 나는지 설명해보자.Notification IO
모델은 IO 관련 이벤트의 발생을 알리는 알림을 비동기로 처리하고,Overlapped IO
모델은 IO가 완료된 상황을 확인하는 과정을 비동기로 처리함 -
논-블로킹 IO, 비동기 IO 그리고
Overlapped IO
의 관계를 하나의 문장으로 연결해서 설명해보자.비동기 IO는 IO가 완료된 상황을 확인하는 과정이 비동기로 처리됨을 의미하며, 이를 위해선 IO가 논블로킹 모드로 동작해야하며 이를 바탕으로 IO가 중첩된 형태인
Overlapped IO
가 가능 - 다음 코드의 일부를 보면서 문제점이 있다면 어떠한 문제가 있는지 지적해보자. 그리고 이에대한 해결책도 함께 제시해보자.
while (1) { hRecvSock = accept(hLisnSock, (SOCKADDR*)&recvAdr, &recvAdrSz); evObj = WSACreateEvent(); memset(&overlapped, 0, sizeof(overlapped)); overlapped.hEvent = evObj; dataBuf.len = BUF_SIZE; dataBuf.buf = buf; WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, NULL); }
참고로, 위의 코드가 완벽하진 않지만 이 코드만 가지고도 충분히 발견할 수 있는 구조적인 문제점이 있다.
반복문을 통해 호출되는
WSARecv()
함수의OVERLAPPED
구조체가 모두 동일한 변수인 것이 문제
OVERLAPPED
구조체 변수를 동적할당하여 해결할 수 있음 -
WSASend
함수호출 이후에 IO가 Pending된 상황과 그렇지 않은 상황을 확인하는 방법에 대해서 소스코드 수준에서 설명해보자.WSASend()
함수가SOCKET_ERROR
를 반환하고WSAGetLastError()
함수가WSA_IO_PENDING
을 반환하는 경우 -
쓰레드의
alertable wait
상태는 어떠한 상태를 의미하는가? 그리고 쓰레드를 이 상태가 되도록 하는 함수들 중 두가지만 말해보자.쓰레드가 운영체제가 전달하는 메시지의 수신을 대기하는 상태로
Completion Routine
을 호출해도 문제가 없음
WaitForSingleObjectEx()
,WaitForMultipleObjectsEx()
,WSAWaitForMultipleEvents()
,SleepEx()