C++ Socket, IOCP

윤형·2025년 12월 26일

C++ Game Server

목록 보기
8/10
post-thumbnail

학습목표

게임서버는 주로 Window의 IOCP로 이루어지게 된다. IOCP를 이해하기 위해서는 논블로킹 소켓과 Overlapped IO의 지식이 필수기 때문에 하나씩 공부해보도록 하겠다.

  • TCP, UDP
  • 논블로킹 소켓
  • Overlapped I/O
  • epoll
  • IOCP (I/O Completion Port)

TCP

클라이언트

// 클라이언트의 의사 코드임
// 이 프로그램은 11.22.33.44 기기에서 실행된다고 가정
mainI() {
	s = socket(TCP);
    s.bind(any_port);
    s.connect("55.66.77.88:5959");
    s.send("hello");
    s.close();
}
  • TCP통신은 이러한 흐름으로 연결이 진행된다.
  • Socket을 생성한다.
  • 로컬 포트를 bind(연결)한다.
  • connect()를 해서 서버와 연결을 시도한다. 이때 블로킹 상태로 대기하다가 서버가 수락하면 리턴한다.
  • 메시지를 보낸다.
  • 종료

서버

main() {
    s = socket(TCP);
    s.bind("0.0.0.0:5959");    // 특정 포트 바인드
    s.listen();                // 연결 대기
    s2 = s.accept(); 		   // 연결
	print(getpeeraddr(s2));    // 클라이언트의 endpoint 출력해보기
    while (true) {
    	r = s2.recv(); 	       // 소켓에서 데이터를 받는다.
        if (r.length <=0 ) {
        	break;
        }
        print(r);
    }
    s2.close();
}
  • 서버도 socket을 만들고 정해진 주소에 bind를 한다.
  • listen()을 통해 데이터를 받는다. -> 의사코드라 간단히 해둔거임.
  • 연결이 되면 recv를 통해 데이터를 받고 응답을 다시 보낸다. 만약 받을 데이터가 없으면 블로킹을 한다.
  • TCP 수신을 했는데 받은 데이터 크기가 0바이트면 상대방이 tcp연결을 끝냈음을 의미한다.
  • 반면 소켓에서 연결이 갑자기 끊어지면 recv()는 음수를 반환한다. -> 이건 의사코드긴한데 암튼 이런 느낌으로 동작함.

Socket

소켓은 각각 송신 버퍼(send buffer)와 수신 버퍼(receive buffer)를 하나씩 가지고 있는다.

Send Buffer

  • Send Buffer는 byte Array이다. Send Buffer의 크기는 고정 되어있고 마음대로 크기를 변경할 수 있다.
  • Queue와 마찬가지로 FIFO 형태로 동작하게 된다. 우리가 Send를 하면 바로 보내지는 것이 아니라 일단 Send Buffer에 채워지게 된다.
  • Buffer에 채워진 데이터는 잠시 후 통신을 통해 점차 빠져나간다.
  • 만약 정해진 send 버퍼에 공간이 없다면 공간이 생길때까지 블로킹을 한다.

Receive Buffer

  • TCP는 1바이트라도 받을 수 있으면 즉시 리턴한다. 만약 버퍼에 데이터가 없으면 블로킹한다.
  • TCP는 recv버퍼가 가득차 있으면 send함수를 블로킹하지만 UDP에서는 바로 리턴이 되고 유실이 발생한다. -> TCP는 연결지향적이고 UDP는 비연결적임. 데이터 유실 발생
  • 즉, 버퍼에서 데이터를 꺼내는 속도가 더 느리다고 해서 TCP연결이 끊어지지 않는다.

  • UDP는 데이터그램이 1개라도 도착해 있으면 즉시 리턴한다.
  • 여기서 데이터그램은 일반적인 바이트단위의 데이터와는 다르게 "경계가 있는 완전한 하나의 데이터"를 의미한다. (크기 다양)
    ⚠️ UDP는 receive 버퍼의 공간이 부족하면 들어오려고 하는 데이터그램을 그냥 버린다. 따라서 send하는 함수가 블로킹 되지 않는다. (TCP와 다른 중요한 차이점)
  • 그래서 버퍼에서 데이터를 꺼내는 속도가 더 느리면 데이터그램 유실이 발생한다.

  • 그래서 UDP의 장점은 속도가 일정하다는 것이다. 패킷이 손실되면 그냥 버리니 속도가 밀리는 경우가 거의 없다.
  • 그리고 좀 더 생각해보면 디도스도 어느정도 방어가 된다. 패킷을 비정상적으로 보내도 어차피 초과분은 그냥 버리면 된다. (물론 정상적인 패킷이 버려지기 때문에 100% 방어는 아님.)
  • TCP의 경우에 패킷손실이 일어나면 재전송을 요청하고 기다리고 다시 받고 등등 손해가 크다.

참고로 소켓은 OS에서 관리하는 객체이다.

Blocking Socket

IO 작업이 완료될 때까지 스레드가 멈추는 방식이다.

char buf[1024];
recv(sock, buf, 1024, 0);  // 데이터 올 때까지 **스레드 멈춤**
  • 보통 클라이언트 - 서버가 1:1 대응일 경우에는 블로킹으로 처리하는 경우가 매우 안정적이다.
  • 하지만 게임 서버가 1:1 대응하는 경우는 없다. 따라서 주로 논블로킹 소켓을 이용하게 된다.

Non-Blocking Socket

호출시 결과를 즉시 반환하며, 프로그램이 블로킹하지 않고 진행하도록 한다. 완료 불가능 할 시 "would block"에러를 반환한다.

fcntl(sock, F_SETFL, O_NONBLOCK);  // 논블로킹 설정

char buf[1024];
int len = recv(sock, buf, 1024, 0);
if (len > 0) { process(buf); }           // 성공
else if (errno == EWOULDBLOCK) {}        // 다음에 재시도
  • 논블로킹 방식은 함수를 항상 바로 리턴하게 되는데 이때 성공
    혹은 would block을 반환하게 된다.
  • would block은 쉽게 말해서 "블로킹 걸렸어야 하는 상황인데 난 논블로킹 소켓이라 내가 처리했다." 이런 의미에 가깝다.
while(true) {
	r = s.send(dest, data);
    if(r == EWOULDBLOCLK) {
    	continue;
        // 블로킹 걸릴 상황이여서 송신을 안했다.
    }
    
    if (r == Ok) {
    	// 성공 처리
    }
    else {
    	// 실패 처리
    }
}
  • 대충 이런 느낌으로 동작한다고 보면 된다.
  • 보내거나 받을 때 상황이라면 would block이 리턴이 되면 소켓 안에서 아무일도 일어나지 않았다는 뜻이다.
  • 그런데 connect()하는 과정에서 would block이 리턴이 된다면 무슨 뜻일까?
  • 아마도 소켓안에서 무언가 일이 일어난 것이다.
  • TCP는 3-way-handshake로 연결이 이뤄지기 때문에 발생하게 되는 것이다.
  • 3wh는 보안때 잠깐 배웠는데 정리하자면 "안녕"->"누구?"->"나야" 이렇게 정보를 주고받으며 연결을 하는 것을 의미한다.
  • 즉 connect과정에서 would block이 나온다면 아마도 "누구?"(SYN-ACK)를 받는 응답을 기다리는 과정일 것이다.

🤔 그렇다면 connect을 했는데 연결이 잘 된건지 확인하려면 어떻게 하면 될까?

result = s.connect();
if (result == EWOULDBLOCK) {
	while(true) {
    	byte emptyData[0]; // 길이가 0인 배열
        result = s.send(emptyData);
        if (result == OK) {
        	// 연결 성공 처리
        }
        else if (result == ENOTCONN) {
        	// 연결이 아직 진행중
        }
        else {
        	// 연결 실패
        }
    }
}
  • 전통적인 방법으로는 간단하게 빈 데이터를 보내보는 것이다.
  • 이러면 응답이 다시 오게 되니까 이걸 보고 연결이 잘 된건지 아니면 안된건지 다시 확인하는 방법이 있다.
List<Socket> sockets;

void NonBlockSocketOperation() {
	while(true) {
    	foreach(s in sockets) {
        	(result, data) = s.receive();
            if (data.length > 0) {
            	print(data);
            }
            else if (result != EWOULDBLOCK) {
            	// 소켓 오류 처리
            }
        }
    }
}
  • 이렇게 서버 측에서는 처리하면 될것이다.
  • 하지만 이렇게 하게 되면 루프를 도는 동안 receive는 논블로킹이지만 대부분의 경우에는 Would block이 걸릴 것이다.
  • would block이 걸리게 되면 각 과정은 부하가 적지만 context switching 비용이랑 시스템콜이랑 등등하면 엄청난 CPU부하가 일어나게 될 것이다.
  • 만약 소켓이 5만개 있다고 생각해보면 항상 5만개를 다 반복 돌면 정말 미친 짓이다.
  • 과장 한스푼 넣자면 아마 99%의 소켓처리는 would block일 것이다. (receive 버퍼에 데이터가 없어서.)
  • 모든 소켓을 다 확인하지 말고 처리해야할 데이터가 있는 소켓만을 처리하면 이 문제는 해결할 수 있을 것이다.
s = socket(TCP);
s.SetNonBlocking(true);
,,,
while(true) {
	(socket, result) = s.accept();
	,,,
}
,,,

쉽게 이런식으로 논블록킹 모드로 설정하면 된다.

select()

void NonBlockSocketOperation() {
	while(true) {
    	select(sockets, 100ms);
    	foreach(s in sockets) {
        	(result, data) = s.receive();
            if (data.length > 0) {
            	print(data);
            }
            else if (result != EWOULDBLOCK) {
            	// 소켓 오류 처리
            }
        }
    }
}
  • 간단한 의사 코드를 먼저 확인해보자면 소켓의 배열과 최대 대기 시간을 인자로 받는다.
  • 처리가 가능한 소켓이 하나라고 있으면 즉시 리턴하는거고 아니라면 100밀리초까지 블로킹 하게 된다.
  • 블로킹하면서도 조건을 만족하는 상황이 생기면 즉시 리턴하는 것이다.
  • 이러면 반복문을 덜 돌리게 되면서 CPU부하가 줄어드는 개념이다.
void SelectVersion() {
    while(true) {
        fd_set readfds;
        FD_ZERO(&readfds);
        int max_fd = 0;
        
        // O(n) 준비
        foreach(s in sockets) {
            FD_SET(s.fd, &readfds);
            max_fd = max(max_fd, s.fd);
        }
        
        // 대기
        select(max_fd+1, &readfds, NULL, NULL, &timeout);
        
        // O(n) 확인
        foreach(s in sockets) {
            if (FD_ISSET(s.fd, &readfds)) {
                (result, data) = s.receive();
                if (data.length > 0) print(data);
                else if (result != EWOULDBLOCK) error(s);
            }
        }
    }
}
  • fd_set: 집합 비트맵, 이걸 이용하는 이유는 한번에 많은 소켓을 확인하기 위해서다. 비트1 개로 소켓 1개 확인. (1024개 비트)
  • FD_ZERO를 통해 0으로 처음 세팅함.
  • FD_SET을 이용해 소켓을 비트화 한다.
  • 암튼 대충 먼저 선별한 다음 반복문 돌린다는 뜻이다.
  • 윈도우와 리눅스가 거의 비슷하게 사용하는 방법이다.
  • 하지만 대충 봐도 알 수 있듯 소켓수가 많아지게 되면 비트맵에 다 표현이 안될 수 있다.
  • 단점으로는 select를 한번 수행하면 준비되지 않은 애들은 다 등록 해제가 되기 때문에 한번 select후에 다시 재등록 해줘야 한다는 점이다.
  • 또한 한번에 64개만 확인할 수 있기 때문에 소켓이 많아지면 그에 따라 여러개를 같이 관리할 수 있게 코드를 수정해야 한다.

poll()

poll()은 리눅스에서 사용가능한 방법중 하나이다.

struct pollfd {
    int      fd;       // 소켓 번호 (fd=3,7,150000 등 무제한!)
    short    events;   // 감시할 이벤트 (읽기/쓰기 등)
    short    revents;  // 발생한 이벤트 (커널이 채움)
};
struct pollfd fds[10];  // 동적 배열 (select 1024 제한 해결!)

while(true) {
    // 1. 배열 채우기 (O(n))
    for(int i = 0; i < sockets.size(); i++) {
        fds[i].fd = sockets[i].fd;
        fds[i].events = POLLIN;     // 읽기 감시
        fds[i].revents = 0;         // 리셋
    }
    
    // 2. 대기 (소켓3,7에 데이터 도착시 깨어남)
    int ready_count = poll(fds, sockets.size(), 10);  // 10ms 타임아웃
    
    // 3. 준비된 소켓만 처리
    for(int i = 0; i < sockets.size(); i++) {
        if (fds[i].revents & POLLIN) {  // 데이터 있음!
            char buf[1024];
            recv(fds[i].fd, buf, sizeof(buf), 0);
            process_packet(buf);
        }
    }
}
  • poll()방식도 미리 가능한 소켓들을 따로 모아둔 다음.
  • 준비된 애들만 처리하는 방식으로 CPU효율을 높인 방식이다.

epoll()

epoll()은 poll()의 업그레이드 버전이고 현재 가장 많이 쓰이는 방법이다. 뒷부분에서 자세히 다루도록 하겠다.

Overlapped I/O

Window에서 제공하는 비동기 IO방식이다. 논블로킹으로 I/O작업을 요청하면 바로 리턴하고 OS가 백그라운드에서 처리한 후 완료를 알려준다.

지금까지의 논블록 소켓의 장점과 단점은 다음과 같다.

  • <장점>
  • 스레드 블로킹이 없으므로 중도 취소 같은 통제가 가능하다.
  • 스레드 개수가 적어도 소켓을 여러개 다룰 수 있다.
  • 스레드 개수가 적어 연산량이 낭비되지 않는다. +) 호출 스택 메모리도 낭비 안됨.
  • <단점>
  • 소켓 IO함수가 would block을 리턴하는 경우 재시도 호출 낭비가 발생한다.
  • IO함수를 호출할 때 입력하는 데이터 블록에 대한 복사 연산을 실행한다.

예를 들어 UDP의 경우 recv버퍼에 데이터그램만큼의 공간이 없다면 would block이 발생하게 된다. 재시도를 해도 계속 없으면 계속 리턴을 할꺼고 그러면 결국 자원 낭비로 이어질 수 있다.

또한 서버측에서 recv로 데이터를 받게 되면 커널 recv버퍼에 있는 데이터를 user layer에 있는 recv 버퍼로 복사해야 하기 때문에 메모리에 중복 저장이 된다.

⚠️ 참고로 커널 버퍼 - 유저 버퍼 이렇게 나눠서 값을 저장하는건 단점이라기 보다 의도된 설계에 가깝다. 커널 버퍼에 저장되는 포인터를 메모리 안정성을 이유로 유저 버퍼에서는 값만 복사해 가는 개념이다.

😃 이러한 단점을 한번에 해결해 주는 기법이 Overlapped 인 것이다!!!

차이점

기존의 논블록 소켓은 다음과 같이 데이터를 다루게 된다.

  1. 소켓이 IO가능인 것이 있을 때 까지 기다린다.
  2. 소켓에 대해 논블록 엑세스를 한다.
  3. would block이 발생하면 그대로 두고, 그렇지 않으면 실행 결과를 리턴한다.

Overlapped IO는 조금 다르게 처리하게 된다.

  1. 소켓에 대해 Overlapped 엑세스를 건다.
  2. overlapped 엑세스가 성공했는지 확인한 후 성공하면 결과값을 얻는거고 나머지를 처리한다.

논블로킹: NIC(송신 하드웨어) -> DMA -> 커널 버퍼 -> 유저 버퍼
IOCP: NIC -> DMA -> 유저 버퍼 (바로 받음.)

void OverlappedSocketOperation() {
	var overlappedSendStatus;
    (result, length) = s.OverlapedSend(data, overlappedSendStatus);
    
    if (length > 0) {
    	// 이러면 성공한거임.
    }
    else if (result == WSA_IO_PENDING) {
    	// overlaeed I/O 진행중.
        while(true) {
        	(result, length) 
            = GetOverlappedResult(s, overlappedSendStatus);
            
            if (length > 0) {
            	// 잘 보냈다는 뜻.
            }
            else {
            	// 아직 IO pending중.
            }
        }
    }
}
  • 먼저 진행중인 상태를 저장하는 구조체를 준비한다.
  • 소켓에 대한 overlapped 전용 함수를 호출한다. (항상 즉시 리턴함.)
  • overlapped를 사용하면 즉시 리턴되지만 운영체제에서 해당 IO실행이 별도로 이루어지게 된다.
  • 동시간대에 이루어지면서 마치 중첨(overlapped)된 것처럼 동작한다.
  • Overlapped 송신이 완료 대기중이라면 Pending이라고 한다.
  • 따라서 overlapped IO 전용함수가 비동기로 하는 일이 완료 될 때까지 소켓 API에 인자로 넘긴 데이터는 변경되거나 제거하면 안된다. (처리중이기 때문)
  • overlapped status 구조체 또한 중간에 바꾸면 안됨.

Overlapped 전용 함수를 사용하면 버퍼의 메모리 블록 자체를 사용하게 된다. 즉, 커널 buffer를 거치지 않고 바로 사용자 버퍼로 오게 되는 것이다.

<일반 recv 3단계>
네트워크 ──1──> [커널 버퍼] ──2──> [사용자 버퍼] ──3──> [앱 로직]
       ↑           ↑                ↑
   원본       중간 저장!       최종 사용

<Overlapped IO>
네트워크 ────────────────> [사용자 버퍼] ──> [앱 로직]
              직접!
일반 버전 함수Overlapped I/O 전용 함수
sendWSASend
sentoWSASendTo
recvWSARecv
recvfromWSARecveFrom
connectConnectEx
acceptAcceptEx
  • 정보) WSA는 Window Socket Api의 약자임.
  • 정보) Ex는 Extended(확장)임.

추가적으로 논블록 소켓에서는 상태 확인 후에 일을 하기 때문에 리액터(reactor)라고 부르고 Overlapped는 일단 먼저 저지르고 결과를 확인하기 때문에 프로액터(proactor)패턴이라고 부른다.

reactorproactor
논블록 소켓Overlapped I/O
I/O를 시도한다. (성공 혹은 실패)I/O를 시행한다. (무조건 성공)
실패할 때는 I/O 기능을 기다린 후 재시도를 한다.I/O완료를 기다린다.
성공할 때는 상황을 종료한다.상황을 종료한다.

근데 어쨋든 Overlapped방식이나 poll방식도 결국 반복문을 돌아야 처리할 수 있어서 부하가 크다. 그래서 나온 방식이 epoll과 IOCP라는 것이다.

epoll

epoll은 소켓이 I/O 가능 상태가 되면 이를 감지해서 사용자에게 알림을 해 주는 역할을 한다. 이때 어떤 소켓이 I/O가능 상태인지 알려주게 된다.

epoll = new epoll();
foreach(s in sockets) {
	epoll.add(s, GetUserPtr(s));
}

events = epoll.wait(100ms);

foreach(event in events) {
	s = event.socket;
    // 위 epoll.add에 들어갔던 값을 얻는다.
    userPtr = event.userPtr;
    type = event.type;
    if (type == ReceiveEvent) {
    	(result, data) = s.recv();
        if (data.length > 0) {
        	//수신된 데이터를 처리한다.
            Process(userPtr, s, data);
        }
    }
}
  • 대략적으로 의사코드를 작성했다.
  • 먼저 epoll객체를 만든다.
  • 여러 소켓을 epoll에 추가한다. 추가된 소켓의 IO 가능 이벤트는 epoll로 감지할 수 있다.
  • epoll에서 이벤트를 꺼내오는 함수를 호출한다. 이 함수는 사용자가 원하는 시간까지만 블로킹 하며 그 전에 이벤트가 생기면 즉시 리턴한다.

LT vs ET

  • epoll을 사용하면 IO가능 상태의 소켓만을 루프 돌기 때문에 select보다 훨씬 효율적이다. 하지만 현실적으로 생각해보면 버퍼가 full이 되는 경우는 거의 없다. 따라서 full에서 살짝 공간이 남으면 거기를 채우고 나머지 부분은 다시 요청을 보내고 조금 넣고 다시 요청보내고....를 반복하면서 필요 이상의 루프를 돌게 될 수 있다. 이 문제를 해결하기 위해서는 Level Trigger대신 Edge Trigger를 써야 한다.

여기서 Level은 전압 자체가 있음을 의미하고 Edge는 전압의 변화과 있음을 의미하는 전자공학 용어이다.
그러면 epoll에서는 무슨 말이냐면 레벨 트리거는 "소켓이 IO가능하다."라는 의미이고, 에지 트리거는 "소켓이 IO 원래 안됐는데 이제는 된다."라는 의미이다.

여기서 소켓 통신을 예로 들어보자. 소켓 버퍼에 읽을 데이터가 있는 경우를 1, 없는 경우를 0으로 생각할 수 있다.

소켓 버퍼에 100바이트의 데이터가 도착했을 때, 소켓의 상태는 0에서 1로 변경된다. 이 때는 LT나 ET 모두 이벤트를 발생 시킨다.

이제 사용자가 소켓 버퍼에서 데이터를 읽는다고 했을 때, 100byte를 다 읽지 못하고 30byte만 읽었다고 하자. 그렇다면 여전히 소켓 버퍼에는 70byte의 데이터가 남아 있다.

이 다음 상황에서, 레벨 트리거링을 하면, 소켓 버퍼에 데이터가 남아있기 때문에 여전히 이벤트가 발생하지만, 엣지 트리거링을 하면 소켓 버퍼의 상태(데이터가 존재하는 상태)가 변하지 않았기 때문에 이벤트가 발생하지 않는다.

즉, LT에서는 남아있는 데이터를 보는게 아니라 recv버퍼에 데이터 있음만을 감지하기 때문에 처리 못했으면 넘기고 다시 반복문을 돌면서 해결한다. 하지만 ET의 경우에는 깨워진 상태에서 반복하며 남은 데이터를 처리하는 것이다.

epoll = new epoll(EPOLLET);  // ET 모드!

events = epoll.wait(100);  // CPU 0% + 게임 프레임
foreach(event in events) {
    s = event.socket;
    userPtr = event.userPtr;
    
    if(event.type & ReceiveEvent) {
        // ✅ ET 필수: 버퍼 완전 비우기!
        while(true) {
            (result, data) = s.recv();
            if(result <= 0 || EWOULDBLOCK) break;
            Process(userPtr, s, data);  // 30B + 70B 모두!
        }
    }
}
버퍼 상태: 000011110000 (1=데이터 있음)

LT (레벨 트리거): "1인 동안 계속 울림!" 🔄
   epoll_wait: "소켓3,5,6,7에 데이터! 또! 또! 또!"

ET (엣지 트리거): "상태 바뀔 때 딱 1회 울림!" 📢
   epoll_wait: "소켓3에 새 데이터 도착!" (한 번만)
  • 대충 이런 느낌이다. 레벨 트리거는 데이터가 남아있게 되면 계속 돌면서 데이터가 있다고 하면서 이벤트를 호출할 것이다.
  • 하지만 ET는 변화할때만 (0->1) 이벤트를 호출하기 때문에 1회만 하게 될 것이다.

⚠️ 하나의 epoll 인스턴스를 여러 스레드가 공유하고 있다면 이벤트가 발생하면 대기중인 모든 스레드가 깨어나는 구조이다. (Thundering Herd(천둥 떼))

IOCP (I/O Completion Port)

Window전용 고성능 IO라고 생각하면 된다. IOCP는 Overlapped IO가 완료되면 이를 감지해 사용자에게 알려주는 역할을 한다.

  • 소켓 개수가 1억개여도 여기서 IO가 완료된 것들만 IOCP를 이용해 얻을 수 있기 때문에 모든 소켓에서 루프를 돌지 않아도 된다.
iocp = new iocp(); // (1)
foreach (s in sockets) {
	iocp.add(s, GetUserPtr(s)); // (2)
    s.OverlappedReceive(data[s], receiveOverlapped[s]);
}

events = iocp.wait(100ms);

foreach(event in events) {
	// (5)
	userPtr = event.userPtr;
    ov = event.overlappedPtr;
    
    s = GetSocketFromUserPtr(userPtr);
    
    if (ov == receiveOverlapped[s]) {
    	// overlapped receive가 성공했으니 받은 데이터를 처리한다.
        Process(s, userPtr, data[s]);
        
        s.OverlappedReceive(data[s], receiveOverlapped[s]); // (6)
    }
}
  • 먼저 IOCP객체를 만들게 된다. (1)
  • iocp.add(s, GetUserPtr(s)) -> 소켓 s의 완료 이벤트를 이 iocp로 보내고 userPtr를 식별자로 붙이라는 뜻이다. (2)
  • 추가된 소켓의 IO이벤트는 iocp를 이용해 감지할 수 있다. IO를 하고 싶으면 Overlapped IO를 걸면된다. (6)
  • 이벤트가 가르키는 Overlapped status 객체와 대응하는 userPtr값을 꺼내오고 원하는 처리를 한다. (5)

IOCP 정리

  • 데이터를 받으면 바로 사용자 버퍼에 전달함.
  • 처리는 백그라운드에서 하고 함수는 바로 리턴됨.
  • 그러다가 IO처리가 끝나면 완료 큐에 올림.
  • 완료큐에 온 내용을 처리하면 됨.

이러한 IOCP의 특성을 이용해 보통 스레드풀을 만들게 된다.

profile
제가 관심있고 공부하고 싶은걸 정리하는 저만의 노트입니다.

0개의 댓글