IOCP Server
✨ 시작하기 전에
이번에 IOCP를 활용해 아무 클라이언트한테 수신을 받는 그대로 전송하는 에코서버(Echo Server)를
구현해 볼 것이다. 이전 글만으로 IOCP를 완전히 이해하는 것은 불가능하기 때문에 이 시리즈에서 천천히
IOCP를 활용해 볼 예정이다.
1. 오버랩 구조체를 상속받기
// 완료된 비동기 함수가 무엇인지 식별하기 위한 열거형
enum class EventType
{
Send,
Recv
}
// 확장된 오버랩 구조체
struct AsyncEvent : OVERLAPPED
{
AsyncEvent(EventType eventType) : eventType(eventType)
{
// 오버랩 구조체의 내용물 비우기
ZeroMemory(this, sizeof(OVERLAPPED));
}
EventType eventType;
}
위 코드는 비동기 함수(send, recv,...)의 종류를 판별하기 위해 AsyncEvent라는 이름으로 OVERLAPPED구조체를 상속받아 확장시키고, 이 오버랩 결과가 무슨 종류인지 알기 위해 EventType
열거형을 멤버로 넣어주었다.
2. WorkerThread 정의하기
WSABUF wsaBuf; // main과 WorkerThread에서 사용하기 위한 전역변수
void WorkerThread(HANDLE cp) {
AsyncEvent* asyncEvent = NULL; // 오버랩 결과물
SOCKET* sock = NULL; // CP에 등록할 때 소켓과 함께 Completion Key로 넣어줬던 소켓
DWORD transferredBytes = 0, flags = 0; // 수신/송신한 바이트 수, 플래그
while (true) {
// 비동기 함수가 끝날때까지 대기
if (GetQueuedCompletionStatus(cp, &transferredBytes, reinterpret_cast<PULONG_PTR>(&sock), reinterpret_cast<LPOVERLAPPED*>(&asyncEvent), INFINITE))
{
switch (asyncEvent->eventType)
{
case EventType::Send: // WSASend가 완료되었을 때
cout << "Sent " << transferredBytes << " bytes.\n";
// 받았으니 다시 수신하기
asyncEvent->eventType = EventType::Recv;
if (SOCKET_ERROR == WSARecv(*sock, &wsaBuf, 1, &transferredBytes, &flags, asyncEvent, NULL))
{
const auto err = WSAGetLastError();
if (err != WSA_IO_PENDING)
throw runtime_error("WSARecv()");
}
break;
case EventType::Recv: // WSARecv가 완료되었을 때
cout << "Received " << transferredBytes << " bytes.\n";
this_thread::sleep_for(500ms); // 쉴틈없이 send, recv 동작하는거 방지
// 받았으면 받은거 그대로 전송
wsaBuf.len = transferredBytes;
asyncEvent->eventType = EventType::Send;
if (SOCKET_ERROR == WSASend(*sock, &wsaBuf, 1, &transferredBytes, flags, asyncEvent, NULL))
{
const auto err = WSAGetLastError();
if (err != WSA_IO_PENDING)
throw runtime_error("WSASend()");
}
break;
}
}
}
}
일단 기본적인 WorkerThread의 모습은 이렇게 되어있다. 이 WorkerThread는 recv 받았으면 그 데이터 그대로 send, 그리고 다시 recv준비하며 에코서버의 다시 보내기 기능을 구현해 준다.
3. 소켓 생성 & Listen
int main() {
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
throw runtime_error("WSAStartup()");
auto listenSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if(listenSock == INVALID_SOCKET)
throw runtime_error("socket()");
SOCKADDR_IN addr;
ZeroMemory(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(SOCKET_ERROR == bind(listenSock, reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)))
throw runtime_error("bind()");
if(SOCKET_ERROR == listen(listenSock, SOMAXCONN))
throw runtime_error("listen()");
...
WSACleanup();
return 0;
}
우리가 소켓 프로그래밍을 공부하며 자주 보던 그 코드다. 다른 특징이 있다면 애러가 생긴다면 std::runtime_error를 throw한다. ... 부분은 IOCP에 사용될 추가적인 코드가 들어간다.
4. CP생성 & WorkerThread 생성, 그리고 메인 루프
...
// CP 핸들 만들기
HANDLE cp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, NULL);
if(cp == INVALID_HANDLE_VALUE)
throw runtime_error("CreateIoCompletionPort()");
// CP에 listenSock 등록시키고, Completion Key로 자신의 소켓 등록
CreateIoCompletionPort(reinterpret_cast<HANDLE>(listenSock), cp, reinterpret_cast<ULONG_PTR>(&listenSock), NULL);
// WorkerThread 객체 생성
thread worker(WorkerThread, cp);
// 버퍼 등록
char buf[128] = "";
wsaBuf.buf = buf;
wsaBuf.len = 128;
while (true) {
// 클라이언트 Accept
auto clientSock = accept(listenSock, NULL, NULL);
if (clientSock != INVALID_SOCKET) {
cout << "Connected\n";
// 접속한 클라이언트도 CP에 등록
CreateIoCompletionPort(reinterpret_cast<HANDLE>(clientSock), cp, reinterpret_cast<ULONG_PTR>(&clientSock), NULL);
// WSARecv에 Recv이벤트를 담아서 호출
AsyncEvent recvEvent(EventType::Recv);
DWORD dw = 0, flags = 0;
if (SOCKET_ERROR == WSARecv(clientSock, &wsaBuf, 1, &dw, &flags, &recvEvent, NULL))
{
const auto err = WSAGetLastError();
if (err != WSA_IO_PENDING)
throw runtime_error("WSARecv()");
}
}
}
worker.join(); // `std::thread`은 끝에서 join해줘야 한다.
...
위 코드는 CP핸들을 만들어주고 리슨소켓과 클라이언트 소켓 모두 CP에 등록해준 뒤, Accept 된 소켓에 recv를 걸어주는 코드다. WorkerThread를 끝에서 join해주는 이유는 WorkerThread를 직접적으로 join하면 애러가 나는 문제가 있다.
이 코드를 모두 작성하고 돌려보면 데이터를 수신하면 그 데이터 그대로 송신하는 서버가 완성될 것이다.
테스트는 이 서버를 실행하고 telnet 클라이언트를 이용해 테스트 해보면 된다.
마무리
이론상은 이렇게 글로 작성했지만, 보지 않고 한번 직접 한번 실습해 보면 애러가 나거나 원하는 대로 작동하지 않을 것이다. 하지만 하나하나 고쳐가다 보면 IOCP의 동작 원리를 이해하기 쉬워질 것이다.
'network' 카테고리의 다른 글
[Network] HTTP 프로토콜 분석 (0) | 2024.11.10 |
---|---|
[Network] 차세대 IO 모델 (0) | 2024.09.11 |
[Network] 01. IOCP란 무엇일까? (2) | 2023.10.30 |