[게임서버프로그래밍#3] Overlapped I/O
2024. 6. 4. 20:05ㆍ개발 서적 리뷰/게임서버 프로그래머 책

이 글에서는 Windows 환경에서 고성능 네트워크 통신을 위해 사용되는
Overlapped I/O를 설명한다.
기존 비동기 방식의 한계를 보완하며, CPU 낭비를 줄이고 I/O 효율을 높이는 방법과
주의사항까지 구체적으로 다룬다.
비동기 처리 방식의 문제점
기존 비동기 처리(select + socket + thread)에서는 다음과 같은 문제점이 존재한다.
- 메모리 복사 비용:
send() 같은 socket 내부 함수 호출 시 메모리 복사가 발생한다. - 불필요한 재시도 호출:
송수신 버퍼에 1바이트라도 여유가 있으면 send()나 recv()를 호출하려 하지만, 실제로 데이터를 보내거나 받을 수 없는 경우가 발생한다. - CPU 낭비:
데이터를 보낼 수 없는 상황에서도 API 호출을 반복하면서 CPU 자원을 낭비하게 된다.
예시
- 송신 버퍼에 1바이트 여유가 있지만, 5바이트짜리 메시지를 보내려 할 경우:
호출은 하지만 데이터는 송신되지 않음 → 불필요한 CPU 낭비 발생
Overlapped I/O란 무엇인가
정의:
Overlapped I/O는 운영체제에게 I/O 요청을 맡기고, 작업 완료 시 알림을 받는 구조이다. 직접 재시도하지 않고, 이벤트가 발생하면 처리하는 방식이다.
장점
- 메모리 복사 절감:
WSASend() 등을 사용하면 메모리 복사 비용을 줄일 수 있다. - 재시도 호출 최소화:
소켓이 가능해질 때까지 기다렸다가, 한 번에 결과를 처리할 수 있다.
Overlapped I/O 처리 흐름
- WSASend()나 WSARecv() 같은 Overlapped 함수를 호출한다.
- 작업이 완료될 때까지 기다리지 않고, 백그라운드에서 작업이 진행된다.
- 작업 완료 시 운영체제가 알람을 보낸다.
- 알람을 받아 결과를 확인한다.
* 주의사항
- Overlapped I/O에서는 전달한 버퍼를 백그라운드에서 사용한다.
(따라서, I/O 완료 전까지 해당 버퍼 데이터를 수정하거나 삭제하면 안 된다)
Overlapped 전용 주요 함수
| 함수 | 설명 |
| WSASend() | 비동기 송신 요청 |
| WSASendTo() | 비동기 송신 요청(주소 지정) |
| WSARecv() | 비동기 수신 요청 |
| WSARecvFrom() | 비동기 수신 요청(주소 수신) |
| ConnectEx() | 비동기 연결 설정 |
| AcceptEx() | 비동기 연결 수락 |
I/O 완료 알림 처리 방법
방법 1: 이벤트 객체 사용
- WSACreateEvent()로 이벤트 객체를 생성하고,
WSAWaitForMultipleEvents()로 완료 여부를 감시한다.
방법 2: 콜백 함수 사용
- Overlapped 구조체에 콜백 함수를 설정하고,
완료 시 콜백이 자동 호출되게 한다.
완료 확인:
→ WSAGetOverlappedResult()를 호출하여 결과를 가져온다.
간단한 클라이언트/서버 구현 예시
1. 클라이언트 흐름
- WSASocket()으로 소켓 생성 (WSA_FLAG_OVERLAPPED 플래그 사용)
- WSASend()로 메시지 송신
- 이벤트 알람 수신 후 WSAGetOverlappedResult()로 완료 확인
2. 서버 흐름
- WSASocket()으로 리스닝 소켓 생성
- AcceptEx()로 클라이언트 연결 비동기 수락
- WSARecv()로 데이터 비동기 수신
* 주의
- 클라이언트 연결 종료 처리한다
- 완료된 I/O 작업을 스레드로 넘겨 효율적으로 처리해야 한다
- 다중 클라이언트 요청을 처리할 때 race condition에 주의해야 한다
Overlapped I/O와 IOCP 관계
Overlapped I/O는 단독으로 사용할 수도 있지만,
IOCP (I/O Completion Port) 와 결합하면 다중 I/O를 매우 효율적으로 처리할 수 있다.
- Overlapped I/O: 비동기 작업을 요청하는 기술
- IOCP: 완료된 I/O 작업을 효과적으로 관리하고 스레드에 분배하는 시스템
👉 Overlapped I/O는 IOCP 기반 서버를 만들기 위한 핵심 기술이다.
CLIENT 구현
#include <iostream>
#include <winsock2.h>
#include <MSWSock.h>
#include <string>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
class Winsock
{
public:
Winsock()
{
WSADATA wsa;
int result = WSAStartup(MAKEWORD(2, 2), &wsa);
if (result != 0)
{
throw std::runtime_error("WSAStartup failed with error : " + std::to_string(result));
}
}
~Winsock()
{
WSACleanup();
}
};
class ClientSocket
{
public:
ClientSocket()
{
//socket() => PF_INET 은 프로토콜 패밀리(pf) 약자
//sendaddr => AF_INET 은 주소 패밀리(af) 약자
sock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
memset(&sendAddr, 0, sizeof(sendAddr));
sendAddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &sendAddr.sin_addr);
sendAddr.sin_port = htons(atoi("27015"));
if (::connect(sock, (SOCKADDR*)&sendAddr, sizeof(sendAddr)) == SOCKET_ERROR)
{
throw std::runtime_error("connect failed with error : " + std::to_string(WSAGetLastError()));
}
evObj = WSACreateEvent();
memset(&overlapped, 0, sizeof(overlapped));
overlapped.hEvent = evObj;
char msg[] = "Hello ovelapped I/O hello";
int sendBytes = 0;
dataBuf.len = strlen(msg) + 1;
dataBuf.buf = msg;
if (WSASend(sock, &dataBuf, 1, (LPDWORD)&sendBytes, 0, &overlapped, NULL) == SOCKET_ERROR)
{
if (WSAGetLastError() == WSA_IO_PENDING)
{
std::cout << "Background data send" << std::endl;
WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);
WSAGetOverlappedResult(sock, &overlapped, (LPDWORD)&sendBytes, FALSE, NULL);
}
else
{
throw std::runtime_error("WSASend() Error");
}
}
std::cout << "Send data size : " << sendBytes << std::endl;
}
~ClientSocket()
{
WSACloseEvent(evObj);
closesocket(sock);
}
public:
WSAOVERLAPPED overlapped;
WSAEVENT evObj;
WSABUF dataBuf;
SOCKET sock;
SOCKADDR_IN sendAddr;
};
int main()
{
Winsock wsa;
ClientSocket sock_;
return 0;
}
Server 구현
#include <iostream>
#include <winsock2.h>
#include <string>
#include <ws2tcpip.h>
#include <chrono>
#include <thread>
#pragma comment(lib, "ws2_32.lib")
#define WSAWAITTIMEOUT 0x00000102
class Winsock
{
public:
Winsock()
{
WSADATA wsa;
int result = WSAStartup(MAKEWORD(2, 2), &wsa);
if (result != 0 )
{
throw std::runtime_error("WSAStartup failed with error : " + std::to_string(result));
}
}
~Winsock()
{
WSACleanup();
}
};
class ServerSock
{
public:
ServerSock()
{
serverSocket = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
memset(&listenAddr, 0, sizeof(listenAddr));
listenAddr.sin_family = AF_INET;
listenAddr.sin_addr.s_addr = htonl(INADDR_ANY);
listenAddr.sin_port = htons(atoi("27015"));
if (bind(serverSocket, (SOCKADDR*)&listenAddr, sizeof(listenAddr)) == SOCKET_ERROR)
{
throw std::runtime_error("bind error : " + std::to_string(WSAGetLastError()));
}
if (listen(serverSocket, 5) == SOCKET_ERROR)
{
throw std::runtime_error("listen error : " + std::to_string(WSAGetLastError()));
}
acceptAddrSize = sizeof(acceptAddr);
acceptSocket = accept(serverSocket, (SOCKADDR*)&acceptAddr, &acceptAddrSize);
evObj = WSACreateEvent();
memset(&overlapped, 0, sizeof(overlapped));
overlapped.hEvent = evObj;
dataBuf.len = 8192;
dataBuf.buf = buf;
}
void receiveData()
{
while (true)
{
if (WSARecv(acceptSocket, &dataBuf, 1, (LPDWORD)&recvBytes, (LPDWORD)&flags, &overlapped, NULL) == SOCKET_ERROR) {
if (WSAGetLastError() == WSA_IO_PENDING) {
std::cout << "Background data receive\n";
std::chrono::milliseconds timeout(1000); // 1 second timeout
DWORD dwMilliseconds = timeout.count();
int waitResult = WSAWaitForMultipleEvents(1, &evObj, TRUE, dwMilliseconds, FALSE);
if (waitResult == WSAWAITTIMEOUT) {
std::cout << "Receive operation timed out\n";
}
else {
WSAGetOverlappedResult(acceptSocket, &overlapped, (LPDWORD)&recvBytes, FALSE, NULL);
}
}
else {
throw std::runtime_error("WSARecv() error");
}
}
else
{
std::cout << "Received " << recvBytes << " bytes: " << buf << std::endl;
memset(buf, 0, sizeof(buf));
recvBytes = 0;
//break; // 1번만 응답 대기
}
}
}
void startReceiveThread()
{
std::thread receiveThread(&ServerSock::receiveData, this);
receiveThread.join();
//receiveThead.detach();
}
~ServerSock()
{
WSACloseEvent(evObj);
closesocket(serverSocket);
closesocket(acceptSocket);
}
public:
SOCKET serverSocket, acceptSocket;
SOCKADDR_IN listenAddr, acceptAddr;
int acceptAddrSize;
WSABUF dataBuf;
WSAEVENT evObj;
WSAOVERLAPPED overlapped;
char buf[8192] = "";
int recvBytes = 0, flags = 0;
};
int main()
{
Winsock wsa;
ServerSock serverSocket;
serverSocket.startReceiveThread();
}
다음 글 예고
다음 글에서는 Client-Server 구조를 실습하기 위해 "기초적인 소켓 프로그래밍(1편: 기본 연결과 통신)"을 다룬다.
비동기 소켓 설정(ioctlsocket, select 사용법)까지 함께 알아본다.
'개발 서적 리뷰 > 게임서버 프로그래머 책' 카테고리의 다른 글
| [게임서버프로그래밍#2] RDBMS와 NoSQL (4) | 2024.06.13 |
|---|---|
| [게임서버프로그래밍#4] Client-Server 기본 연결과 통신 (0) | 2024.06.08 |
| [게임서버프로그래밍#5] 멀티스레드 Client-Server 구현 (feat: mutex, chrono) (1) | 2024.06.02 |
| Socket의 기본 정리 ( feat, Blocking / Non-Blocking ) (0) | 2024.05.30 |