Overlapped I/O : 비동기 입출력
하나의 스레드에서 둘 이상의 패킷 데이터를 통신용 소켓에 송수신합니다.
그래서 "여러 개의 소켓에 입출력이 중첩된 상황" 이라고 해서
Overlapped I/O는 중첩된 입출력이라는 뜻으로 보면 될 것 같습니다.
그렇다면 입출력 함수의 입장에서 보았을 때, 즉각적인 반환이 요구됩니다.
이를 가능하게 하려면 입출력 자체를 비동기 방식으로 처리합니다.
이는 윈속에서 OVERLAPPED 구조체와 이에 따른 입출력 함수를 통해
하나 또는 그 이상의 입출력 요청을 한 번에 처리할 수 있습니다.
구조체에 가장 아래에 있는 HANDLE hEvent는 핸들을 저장합니다.
첫번째 ULONG_PTR Internal과 두번째 ULONG_PTR IternalHigh는
운영체제에서 내부적으로 사용하는 것으로 사용하기 전에 초기화만 해주면 됩니다.
이들은 운영체에서 입출력에 대한 길이를 저장할 때 사용된다고 합니다.
DWORD Offset은 패킷 데이터 입출력의 시작 위치(0)를 나타냅니다.
여기서 DWORD OffsetHigh도 그렇고, 윈속에서 제공되는 다른 구조체에서도
변수 이름 뒤에 High가 붙어있는 것은 나중에 버전업을 위해 미리 준비된 변수라고 하네요.
Overlapped I/O는 블로킹 소켓을 사용합니다.
WSAEventSelect 입출력 모델에서 ioctlsocket API로
논블로킹 소켓 모드로 전환했던 작업을 여기에서는 하지 않습니다.
Overlapped I/O가 블로킹 소켓을 사용하더라도,
WSASend, WSARecv와 같은 비동기 함수를 사용하기 때문에
블로킹 방식으로 처리하지 않게 됩니다.
Overlapped I/O 서버 C++ 소스 코드
(소스 코드에서는 Complete Routine 대신 이벤트 객체로 대체했습니다)
#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);
// WSASocket
// socket 함수의 확장된 형태이다. Overlapped I/O를 소켓에 적용하기 위해서는
// WSASocket 함수를 통해서 소켓을 생성한다.
// arg 4) LPWSAPROTOCOL_INFOW lpProtocolInfo : WSAPROTOCOL_INFO 구조체 포인터.
// 여기에서는 쓰지 않으므로 nullptr.
// arg 5) GROUP g : 소켓 그룹의 식별자.
// arg 6) DWORD dwFlags : 소켓의 속성을 지정한다.
// WSA_FLAG_OVERLAPPE : 생성할 소켓에 Overlapped의 속성을 갖도록 지정한다.
SOCKET hListen = WSASocket(PF_INET, SOCK_STREAM, 0, nullptr, 0, WSA_FLAG_OVERLAPPED);
SOCKADDR_IN tListenAddr = {};
tListenAddr.sin_family = AF_INET;
tListenAddr.sin_port = htons(PORT);
tListenAddr.sin_addr.s_addr = htonl(INADDR_ANY);
// bind
bind(hListen, (SOCKADDR*)&tListenAddr, sizeof(tListenAddr));
// listen
listen(hListen, SOMAXCONN);
// accept
SOCKADDR_IN tClntAddr = {};
int iSize = sizeof(tClntAddr);
SOCKET hSocket = accept(hListen, (SOCKADDR*)&tClntAddr, &iSize);
// 논시그널 모드인 이벤트 객체를 생성한다.
// 이벤트 핸들 오브젝트의 시그널 상태를 Overlapped 구조체에 알린다.
// 만약 소켓의 입출력 작업이 완료되면 OS는 애플리케이션이 등록한 이벤트 객체의
// 신호 상태를 바꾸고 애플리케이션은 이를 바탕으로 이벤트 객체를 관찰함으로써
// 작업 완료를 감지할 수 있게 된다. (때문에, 여기서 Complete Routine을 사용하지 않았음)
WSAEVENT hEvent = WSACreateEvent();
char Packet[PACKET_LENGTH] = {};
// 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;
*/
// 소켓의 버퍼 정보를 담고있는 구조체이다.
// 비동기 입출력 함수인 WSASend, WSARecv는 이 구조체에 들어있는 버퍼의 정보를 사용한다.
WSABUF wsaBuf;
// WSABUF구조체에 전송할 버퍼의 종류와 사이즈를 넣어준다.
wsaBuf.len = PACKET_LENGTH - 1; // 패킷 사이즈 설정
wsaBuf.buf = Packet; // 패킷 종류 설정
// OVERLAPPED 구조체 생성
WSAOVERLAPPED tOverlapped = {};
// OVERLAPPED 구조체에 위에서 생성한 Event 객체를 등록한다.
// WSASend, WSARecv 비동기 입출력 함수에서 이 구조체 등록된 이벤트를 통해
// 현재 완료된 입출력의 상태 정보를 감지할 수 있게 된다.
tOverlapped.hEvent = hEvent;
DWORD dwRecvBytes = 0;
DWORD dwFlag = 0;
/*
// WSARecv : 접속된 상대방의 소켓으로부터 전송된 패킷 데이터를 받는다.
int WSAAPI WSARecv(
SOCKET s, // 접속된 소켓 지정
LPWSABUF lpBuffers, // WSABUF 구조체 버퍼 포인터
DWORD dwBufferCount, // WSABUF 버퍼 카운트
LPDWORD lpNumberOfBytesRecvd, // Recv 완료 후 함수의 호출로 읽어낸 데이터의 바이트 수
LPDWORD lpFlags, // 입출력 Flag 설정. 이 플래그를 통해서 WSARecv가 구체적으로 동작하게 될
// 작업을 명시할 수 있다. (MSG_PEEK, MSG_OBB, MSG_PARTIAL)
LPWSAOVERLAPPED lpOverlapped, // OVERLAPPED 구조체 포인터
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 컴플리트 루틴(여기서는 안씀)
);
*/
// WSARecv 함수는 에러가 발생하지 않았다면 바로 0을 반환한다. (SOCKET_ERROR : -1)
if (WSARecv(hSocket, &wsaBuf, 1, &dwRecvBytes, &dwFlag, &tOverlapped, nullptr) == SOCKET_ERROR)
{
// 어떤 종류의 에러인지 알아야한다.
// 진짜 에러인지, 아니면 아직 보낸 데이터를 못받은건지 알야아한다.
// 에러 코드가 WSA_IO_PENDING이면 중첩 연산이 성공적으로 진행된 것이므로
// 이게 아니라면 소켓을 닫아준다.
if (WSAGetLastError() != WSA_IO_PENDING)
{
closesocket(hSocket);
closesocket(hListen);
WSACleanup();
return 0;
}
}
cout << "데이터 수신중" << endl;
// WSAWaitForMultipleEvents
// 지정된 이벤트 객체의 한 개 또는 모두가 신호를 받은 상태였을 때나
// 타임아웃으로 지정한 시간이 지났을 때 반환되는 함수이다.
// 이를 바탕으로 이벤트 객체가 시그널 상태가 되는 것을 기다릴 수 있게 된다.
/*
The WSAWaitForMultipleEvents function returns
when one or all of the specified event objects are
in the signaled state, when the time-out interval expires,
or when an I/O completion routine has executed.
*/
/*
DWORD WSAAPI WSAWaitForMultipleEvents(
DWORD cEvents, // 이벤트 객체 핸들 갯수
const WSAEVENT *lphEvents, // 이벤트 객체 핸들 포인터
BOOL fWaitAll, // 기다림의 형태를 명시,
// TRUE : lphEvents 배열에 있는 모든 이벤트 객체가 같은 시간에 신호를 받을 때 반환 됨.
// FALSE : 함수의 반환으로 일어난 상태를 지정하고 싶을 때 사용.
DWORD dwTimeout, // 타임아웃 간격 명시. 단위는 m/sec(1/1000초)
BOOL fAlertable // 컴플리트 루틴 반환 설정 여부
// TRUE : 완료루틴이 실행되고 반환됨.
// FALSE : 완료루틴은 함수가 반환될 때 실행되지 않음.
);
*/
WSAWaitForMultipleEvents(1, &hEvent, TRUE, WSA_INFINITE, FALSE);
// WSAGetOverlappedResult
// 오버랩 연산의 결과를 반환한다.
// arg 3) LPDWORD lpcbTransfer : 실제로 송수신된 바이트의 개수를 받는 포인터
// arg 4) BOOL fWait : 오버랩 연산 완료의 대기설정 여부
// TRUE : 오버랩 I/O가 완료될 때 까지 대기
// FALSE : FALSE를 설정하면 FALSE를 반환하면서 함수를 빠져나온다.
// 그리고 WSAGetLastError 함수는 WSA_IO_INCOMPLETE 에러 코드를 반환한다.
// arg 5) LPDWORD lpdwFlags : 작업 완료 상태를 가진 플래그를 수신할 포인터
WSAGetOverlappedResult(hSocket, &tOverlapped, &dwRecvBytes, FALSE, nullptr);
cout << "Recv Byte : " << dwRecvBytes << endl;
cout << "Message : " << Packet << endl;
closesocket(hSocket);
closesocket(hListen);
WSACleanup();
return 0;
}
Overlapped I/O 클라이언트 C++ 소스 코드
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <WinSock2.h>
#include <iostream>
using namespace std;
#pragma comment(lib, "ws2_32.lib")
#define PORT 7890
#define PACKET_LENGTH 1024
#define SERVER_IP "127.0.0.1"
int main()
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
SOCKET hSocket = WSASocket(PF_INET, SOCK_STREAM, 0, nullptr, 0, WSA_FLAG_OVERLAPPED);
SOCKADDR_IN tAddr = {};
tAddr.sin_family = AF_INET;
tAddr.sin_port = htons(PORT);
tAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
// connect
connect(hSocket, (SOCKADDR*)&tAddr, sizeof(tAddr));
WSAEVENT hEvent = WSACreateEvent();
char Packet[PACKET_LENGTH] = {};
strcpy_s(Packet, "비동기 입출력 테스트");
WSABUF wsaBuf;
wsaBuf.len = strlen(Packet);
wsaBuf.buf = Packet;
WSAOVERLAPPED tOverlapped = {};
tOverlapped.hEvent = hEvent;
DWORD dwSendBytes = 0;
DWORD dwFlag = 0;
// WSASend
// 접속한 상대방 소켓에게 지정한 패킷 데이터를 전송한다. (매개변수는 WSARecv와 비슷함)
if (WSASend(hSocket, &wsaBuf, 1, &dwSendBytes, dwFlag, &tOverlapped, nullptr) == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
closesocket(hSocket);
WSACleanup();
return 0;
}
}
cout << "데이터 전송중" << endl;
// 이벤트 객체가 시그널 상태가 될 때 까지 기다린다.
WSAWaitForMultipleEvents(1, &hEvent, TRUE, WSA_INFINITE, FALSE);
// 오버랩 연산의 결과를 받는다.
WSAGetOverlappedResult(hSocket, &tOverlapped, &dwSendBytes, FALSE, nullptr);
cout << "Send Bytes : " << dwSendBytes << endl;
cout << "Message : " << Packet << endl;
closesocket(hSocket);
WSACleanup();
return 0;
}
실행 결과
'윈도우즈 소켓 프로그래밍 기초' 카테고리의 다른 글
IOCP (I/O Completion Port) (0) | 2020.01.03 |
---|---|
WSAEventSelect 입출력 모델 (0) | 2019.12.29 |
I/O 멀티플렉싱 (fd_set, select) (0) | 2019.12.29 |
멀티 스레드, 크리티컬 섹션 동기화 (0) | 2019.12.29 |
TCP / UDP 통신 (0) | 2019.12.27 |