IOCP 모델 개요

이미지 1: IOCP 시스템 흐름

  1. 비동기 I/O 시작

    • 애플리케이션에서 I/O 작업(데이터 수신 또는 전송)을 시작합니다.
    • 이 작업은 비동기로 처리되므로 호출한 쓰레드는 작업이 끝나기 전에 바로 다음 작업으로 돌아갑니다.
  2. 비동기 I/O 완료

    • Windows 커널에서 비동기 작업이 완료되면 IOCP 큐(Completion Port Queue)에 작업 결과를 추가합니다.
    • 이 큐는 스레드풀(Thread Pool)과 통신하며 효율적으로 작업을 배분합니다.
  3. 스레드가 큐에서 작업 가져오기

    • GetQueuedCompletionStatus 함수는 IOCP 큐에서 완료된 작업을 가져옵니다.
    • 스레드가 CPU 코어 수에 비례하여 동작하며, 한 번에 여러 작업을 병렬로 처리할 수 있어 성능이 향상됩니다.

이미지 2: IOCP 내부 흐름

  1. Device List

    • IOCP에 등록된 소켓(Socket)과 CompletionKey가 저장됩니다.
    • 프로그래머는 CreateIoCompletionPort를 통해 소켓과 Completion Port를 바인딩합니다.
  2. I/O Queue

    • 비동기 작업이 완료되면 I/O 결과가 이 큐에 쌓입니다.
    • 각 작업은 소켓과 WSAOVERLAPPED 구조체를 포함하며, 스레드가 결과를 처리할 준비가 되어 있습니다.
  3. Thread Pool

    • 큐에 쌓인 작업을 가져와 처리합니다.
    • 주요 함수:
      • CreateIoCompletionPort: IOCP 객체를 생성하고 소켓을 등록.
      • WSARecvWSASend: 비동기 데이터 송수신.
      • GetQueuedCompletionStatus: 큐에서 완료된 작업을 가져와 처리.

코드 예제 분석

1. 주요 구조체

  1. Session 구조체

    • 클라이언트 연결을 관리하는 데이터 구조입니다.
    • 각 클라이언트의 소켓과 데이터 버퍼를 저장합니다.
    struct Session {
        SOCKET socket = INVALID_SOCKET;  // 클라이언트 소켓
        char recvBuffer[BUFSIZE] = {};   // 수신 버퍼
        int32 recvBytes = 0;             // 수신 데이터 크기
    };
  2. OverlappedEx 구조체

    • WSAOVERLAPPED를 확장하여 작업 유형(Read, Write 등)을 추가로 저장합니다.
    struct OverlappedEx {
        WSAOVERLAPPED overlapped = {};   // 기본 오버랩 구조체
        int32 type = 0;                 // 작업 유형 (READ, WRITE 등)
    };

2. 워커 스레드 함수

void WorkerThreadMain(HANDLE iocpHandle)
{
    while (true)
    {
        DWORD bytesTransferred = 0;       // 전송된 바이트 수
        Session* session = nullptr;       // 클라이언트 세션
        OverlappedEx* overlappedEx = nullptr;

        BOOL ret = ::GetQueuedCompletionStatus(
            iocpHandle, &bytesTransferred,
            (ULONG_PTR*)&session,
            (LPOVERLAPPED*)&overlappedEx, INFINITE);
  • GetQueuedCompletionStatus:
    • IOCP 큐에서 완료된 작업을 가져옵니다.
    • 매개변수:
      • iocpHandle: IOCP 핸들.
      • bytesTransferred: 완료된 작업에서 전송된 데이터 크기.
      • session: 작업과 연관된 클라이언트 세션.
      • overlappedEx: 작업에 사용된 오버랩 구조체.
        if (ret == FALSE || bytesTransferred == 0)
        {
            // 연결 끊김 처리
            continue;
        }

        assert(overlappedEx->type == IO_TYPE::READ);

        cout << "Recv Data Len = " << bytesTransferred << endl;
        cout << "Recv Data IOCP = " << session->recvBuffer << endl;
  • 오류 처리 및 로그 출력:
    • 작업 실패(ret == FALSE) 또는 데이터 없음(bytesTransferred == 0) 시 연결이 끊긴 것으로 간주합니다.
    • 수신된 데이터 크기와 내용을 출력합니다.
        WSABUF wsaBuf;
        wsaBuf.buf = session->recvBuffer;
        wsaBuf.len = BUFSIZE;

        DWORD recvLen = 0;
        DWORD flags = 0;

        ::WSARecv(session->socket, &wsaBuf, 1, &recvLen, &flags, &overlappedEx->overlapped, NULL);
    }
}
  • 다음 작업 준비:
    • 클라이언트 세션과 연결된 소켓에서 다음 데이터를 비동기로 읽습니다.
    • WSARecv: 비동기 데이터 수신을 요청합니다.

3. 메인 함수 (서버 초기화)

int main()
{
    SocketUtils::Init(); // Winsock 초기화
  • 소켓 생성 및 바인딩:

      SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);
      SocketUtils::BindAnyAddress(listenSocket, 7777);
      SocketUtils::Listen(listenSocket);
    • 리슨 소켓 생성 후 포트 7777에 바인딩 및 리슨 상태로 설정.
  • IOCP 생성:

      HANDLE iocpHandle = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
    • IOCP 객체 생성. 이후 클라이언트 소켓을 이 객체에 등록합니다.
  • 워커 스레드 실행:

      for (int32 i = 0; i < 5; i++)
          GThreadManager->Launch([=]() { WorkerThreadMain(iocpHandle); });
    • CPU 코어 수에 맞게 5개의 워커 스레드를 실행합니다.
  • 클라이언트 수락 루프:

      while (true)
      {
          SOCKADDR_IN clientAddr;
          int32 addrLen = sizeof(clientAddr);
    
          SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
          if (clientSocket == INVALID_SOCKET)
              return 0;
    
          Session* session = new Session();
          session->socket = clientSocket;
          sessionManager.push_back(session);
    
          ::CreateIoCompletionPort((HANDLE)clientSocket, iocpHandle, (ULONG_PTR)session, 0);
    • 클라이언트 연결을 수락하고 새로운 세션을 생성한 뒤 IOCP에 등록합니다.
  • 비동기 작업 시작:

          WSABUF wsaBuf;
          wsaBuf.buf = session->recvBuffer;
          wsaBuf.len = BUFSIZE;
    
          OverlappedEx* overlappedEx = new OverlappedEx();
          overlappedEx->type = IO_TYPE::READ;
    
          DWORD recvLen = 0;
          DWORD flags = 0;
          ::WSARecv(clientSocket, &wsaBuf, 1, &recvLen, &flags, &overlappedEx->overlapped, NULL);
      }
- IOCP에 등록된 클라이언트 소켓에서 데이터를 비동기로 읽기 시작합니다.

---

### 클라이언트 코드 설명

#### **소켓 연결 및 데이터 전송**

1. **클라이언트 소켓 생성 및 비동기 설정**:
   ```cpp
   SOCKET clientSocket = ::socket(AF_INET, SOCK_STREAM, 0);
   u_long on = 1;
   ::ioctlsocket(clientSocket, FIONBIO, &on);
  1. 서버 연결:

    while (true)
    {
        if (::connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
        {
            if (::WSAGetLastError() == WSAEWOULDBLOCK)
                continue;
            if (::WSAGetLastError() == WSAEISCONN)
                break;
        }
    }
  2. 데이터 전송:

    while (true)
    {
        char sendBuffer[100] = "Hello I am Client!";
        ::send(clientSocket, sendBuffer, sendLen, 0);
    }

profile
李家네_공부방

0개의 댓글