본문 바로가기
Programming/열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)

Ch 23. IOCP(Input Output Completion Port)

by minjunkim.dev 2020. 8. 16.

    모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!


# select와 같은 전통적인 IO 모델의 한계극복을 목적으로, 운영체제 레벨(커널 레벨)에서 성능을 향상시킨 IO 모델이 운영체제 별로 등장(리눅스의 epoll, 윈도우의 IOCP, 등...)

 

# 하드웨어의 성능이나, 할당된 대역폭이 충분한 상황에서 서버의 응답시간이나, 동시접속자 수에 문제가 발생하면

1) 비효율적인 IO의 구성 또는 비효율적인 CPU의 활용

2) 데이터베이스의 설계내용과 쿼리(Query)의 구성

정도만 수정해도, 대부분의 문제는 해결된다고 한다.

 

# 넌-블로킹 모드의 소켓 구성하기

- 윈도우에서는 다음의 함수호출을 통해 넌-블로킹 모드로 소켓의 속성을 변경함

SOCKET hLisnSock;
int mode=1;
....
hLisnSock=WSASocket(PF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);
ioctsocket(hLisnSock,FIONBIO,&mode); // for non-blocking socket
....

- ioctsocket 함수는 소켓의 IO를 컨트롤하는 함수

- FIONBIO 는 소켓의 입출력 모드를 변경하는 옵션

- 세번째 인자로 전달된 주소값의 변수에 0이 저장되어 있으면 블로킹 모드로, 1이 저장되어 있으면 넌-블로킹 모드로 소켓의 입출력은 변경

- 속성이 넌-블로킹 모드로 변경되면, 넌-블로킹 모드로 입출력되는 것 이외에도

1) 클라이언트의 연결요청이 존재하지 않는 상태에서 accept 함수가 호출되면 INVALID_SOCKET이 곧바로 반환된다. 그리고 이어서 WSAGetLastError 함수를 호출하면 WSAEWOULDBLOCK가 반환된다.

=> 넌-블로킹 입출력 소켓을 대상으로 accept 함수를 호출해서 INVALID_SOCKET이 반환되면, WSAGetLastError 함수를 호출해서 INVALID_SOCKET이 반환된 이유를 확인하고, 그에 적절한 처리를 해야함.

2) accept 함수호출을 통해서 새로 생성되는 소켓 역시 넌-블로킹 속성을 지닌다.


# Overlapped IO만 가지고 에코 서버 구현하기

 

CmplRouEchoServ_win.c

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#define BUF_SIZE 1024

void CALLBACK ReadCompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void CALLBACK WriteCompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void ErrorHandling(char* message);

typedef struct
{
    SOCKET hClntSock; // 소켓의 핸들
    char buf[BUF_SIZE]; // 버퍼
    WSABUF wsaBuf; // 버퍼관련 정보
} PER_IO_DATA, *LPPER_IO_DATA;
/*
구조체 변수에 담겨있는 정보만 참조해도 데이터의 송수신이 가능하도록 구조체를 정의
*/


int main(int argc, char* argv[])
{
    WSADATA wsaData;
    SOCKET hLisnSock, hRecvSock;
    SOCKADDR_IN lisnAdr, recvAdr;
    LPWSAOVERLAPPED lpOvLp;
    DWORD recvBytes;
    LPPER_IO_DATA hbInfo;
    int mode=1, recvAdrSz, flagInfo=0;
    
    if(argc!=2) // 실행파일의 경로/PORT번호를 인자로 받아야 함
    {
        printf("Usage: %s <port> \n", argv[0]);
        exit(EXIT_FAILURE);
    }
    
    if(WSAStartup(MAKEWORD(2,2), &wsaData)!=0) // 윈속 라이브리러 초기화
        ErrorHandling("WSAStartup() error");
    
    /* 소켓은 처음 생성되면 블로킹 모드이므로 넌-블로킹 모드로 변환 */
    hLisnSock=WSASocket(PF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);
    ioctsocket(hLisnSock,FIONBIO,&mode); // for non-blocking mode socket
    
    /* 서버 주소정보 초기화 */
    memset(&lisnAdr,0,sizeof(lisnAdr));
    lisnAdr.sin_family=AF_INET;
    lisnAdr.sin_addr.s_addr=htonl(INADDR_ANY);
    lisnAdr.sin_port=htons(atoi(argv[1]));
    
    /* 서버 주소정보를 기반으로 주소할당 */
    if(bind(hLisnSock,(SOCKADDR*)&lisnAdr,sizeof(lisnAdr))==SOCKET_ERROR)
        ErrorHandling("bind() error");
    
    /* 서버(리스닝) 소켓이 되며, 연결요청 대기큐 생성 */
    if(listen(hLisnSock, 5)==SOCKET_ERROR)
        ErrorHandling("listen() error");
    
    recvAdrSz=sizeof(recvAdr);
    while(1)
    {
    	/* 이 시점에 완료된 IO가 있다면 CR을 수행 */
        SleepEx(100, TRUE); // for alertable wait state
        
        /*
        클라이언트의 연결요청을 수락 
        단, 소켓이 넌-블로킹 모드이므로 클라이언트의 연결요청이 없다면
        바로 INVALID_SOCKET을 반환(블로킹 되지 않음)
        이 때는 WSAGetLastError 함수를 호출하여
        오류원인을 확인하고 이에 적절한 처리를 해야함
        클라이언트의 연결요청이 있었다면
        새로운 소켓을 "넌-블로킹 모드"로 생성
        */
        hServSock=accept(hLisnSock,(SOCKADDR*)&recvAdr,&recvAdrSz);
        if(hServSock==INVALID_SOCKET)
        {
            if(WSAGetLastError()==WSAEWOULDBLOCK)
                continue;
            else
                ErrorHandling("accept() error")
        }
        
        puts("Client connected...");
        
        /*
        WSARecv 함수호출 시, 인자로 OVERLAPPED 구조체 변수의 주소 값을 전달해야 하는데,
        이 때 한번의 함수호출 당 WSAOVERLAPPED 구조체 변수를
        "각각 별도로" 구성해서 전달해야 하기 때문에, 동적할당을 진행
        */
        lpOvLp=(LPWSAOVERLAPPED)malloc(sizeof(WSAOVERLAPPED));
        memset(lpOvLp,0,sizeof(WSAOVERLAPPED));
        
        hbInfo=(LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
        hbInfo->hClntSock=(DWORD)hRecvSock;
        (hbInfo->wsaBuf).buf=hbInfo->buf;
        (hbInfo->wsaBuf).len=BUF_SIZE;
        
        lpOvLp->hEvent=(HANDLE)hbInfo;
        /*
        CR 기반의 Overlapped IO에서는 Event 오브젝트가 불필요하므로
        hEvent에 필요한 다른 정보를 채워도 된다.
        */
        
        /*
        CR 기반의 Overlapped IO 실행
        CR로 ReadCompRoutine 함수를 등록함
        */
        WSARecv(hRecvSock,&(hbInfo->wsaBuf),
        1, &recvBytes, &flagInfo, lpOvLp, ReadCompRoutine);
    }
    
    closesocket(hRecvSock); // accept에 의해 생성된 소켓 소멸
    closesocket(hLisnSock); // 서버(리스닝) 소켓 소멸
    WSACleanup(); // 윈속 라이브러리 해제
    return 0;
}

void CALLBACK ReadCompRoutine(
DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
	/* 이 함수가 호출되었다는 것은
    클라이언트로부터의 데이터 수신이 완료되었음을 의미
    따라서, 클라이언트에게 이 데이터를 다시 에코해야 함
    */
	
    /*
    WSARecv 함수 호출시 전달했던
    LPWSAOVERLAPPED가 가리키는 구조체변수에
    저장해두었던 정보를 여기서 활용
    */
    LPPER_IO_DATA hbInfo=(LPPER_IO_DATA)(lpOverlapped->hEvent);
    SOCKET hSock=hbInfo->hClntSock;
    LPWSABUF bufInfo=&(hbInfo->wsaBuf);
    DWORD sendBytes;
    
    if(szRecvBytes==0) // 클라이언트의 EOF 전달(연결종료 요청)
    {
        closesocket(hSock);
        free(lpOverlapped->hEvent);
        puts("Client disconnected...");
    }
    else // echo
    {
        bufInfo->len=szRecvBytes;
        
        /*
        CR 기반의 Overlapped IO 실행
        CR로 WriteCompRoutine 함수를 등록함
        */
        WSASend(hSock,bufInfo,1,&sendBytes,0,lpOverlapped,WriteCompRoutine);
    }
}

void CALLBACK WriteCompRoutine(
DWORD dwError, DWORD szSendBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
    /*
    에코 메시지가 전송된 이후에 이 함수가 호출됨
    */
    
    LPPER_IO_DATA hbInfo=(LPPER_IO_DATA)(lpOverlapped->hEvent);
    SOCKET hSock=hbInfo->hClntSock;
    LPWSABUF bufInfo=&(hbInfo->wasBuf);
    DWORD recvBytes;
    int flagInfo=0;
    
    /*
    다시 데이터를 수신해야 하므로
    CR 기반의 Overlapped IO 실행
    CR로 ReadCompRoutine 함수를 등록함
    */
    WSARecv(hSock, bufInfo, 1, &recvBytes, &flagInfo, lpOverlapped, ReadCompRoutine);
}

void ErrorHandling(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(EXIT_FAILURE);
}

- 위 예제의 동작원리

1) 클라이언트가 서버에 연결되면, WSARecv 함수를 호출하면서 넌-블로킹 모드로 데이터가 수신되게 하고, 수신이 완료되면 ReadCompRoutine 함수가 호출되게 한다.

2) ReadCompRoutine 함수가 호출되면 WSASend 함수를 호출하면서 넌-블로킹 모드로 데이터가 송신되게 하고, 송신이 완료되면 WritecompRoutine 함수가 호출되게 한다.

3) 그런데 이렇게 해서 호출된 WriteCompRoutine 함수는 다시 WSARecv 함수를 호출하면서 넌-블로킹 모드로 데이터의 수신을 기다린다.

(반복 ...)

 

- 입출력 완료시 자동으로 호출되는 CR(Completion Routine) 내부로 클라이언트 정보(소켓과 버퍼, 여기서 PER_IO_DATA 구조체)를 전달하기 위해 WSAOVERLAPPED 구조체의 멤버 hEvent를 사용함.


# 클라이언트의 재 구현

 

StableEchoClnt_win.c

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#define BUF_SIZE 1024

void ErrorHandling(char* message);

int main(int argc, char* argv[])
{
    WSADATA wsaData;
    SOCKET hSocket;
    SOCKADDR_IN servAdr;
    char message[BUF_SIZE];
    int strLen, readLen;
    
    if(argc!=3) // 실행파일의 경로/IP/PORT번호를 인자로 받아야 함
    {
        printf("Usage: %s <IP> <port> \n",argv[0]);
        exit(EXIT_FAILURE);
    }
    
    if(WSAStartup(MAKEWORD(2,2), &wsaData)!=0) // 윈속 라이브버리 초기화
        ErrorHandling("WSAStartup() error");
    
    /* 소켓은 처음 생성되면 블로킹 모드이므로 넌-블로킹 모드로 변환 */
    hSocket=socket(PF_INET,SOCK_STREAM,0);
    if(hSocket==INVALID_SOCKET)
        ErrorHandling("socket() error");
    
    /* 서버 주소정보 초기화 */
    memset(&servAdr,0,sizeof(servAdr));
    servAdr.sin_family=AF_INET;
    servAdr.sin_addr.s_addr=htonl(INADDR_ANY);
    servAdr.sin_port=htons(atoi(argv[1]));
    
    /* 서버 주소정보를 기반으로 연결요청 */
    if(connect(hSocket,(SOCKADDR*)&servAdr,sizeof(servAdr))==SOCKET_ERROR)
        ErrorHandling("connect() error");
    else
        puts("Connected...");
    
    while(1)
    {
        /* 클라이언트가 서버로 전송할 문자열 입력 */
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);
        
        /* 클라이언트 종료 조건 */
        if(!strcmp(message,"Q\n")||!strcmp(message,"q\n"))
            break;
        
        strLen=strlen(message);
        
        /*
        입력받은 문자열을 서버로 전송(널문자는 제외)
        */
        send(hSocket,message,strLen,0);
        
        readLen=0;
        /*
        TCP는 데이터의 경계가 없으므로,
        전송한 데이터를 서버로부터 다시 전부 수신할 때까지 반복
        */
        while(1)
        {
            readLen+=recv(hSocket,&message[readLen],BUF_SIZE-1,0);
            if(readLen>=strlen)
                break;
        }
        message[strLen]='\0'; // C-str 문자열을 만들기 위해 널문자 추가
        printf("Message from server: %s", message);
    }
    
    closesocket(hSocket); // 클라이언트 소켓 소멸
    WSACleanup(); // 윈속 라이브러리 해제
    return 0;
}

void ErrorHandling(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(EXIT_FAILURE);
}

# 위에서 구현한 Overlapped IO 기반의 에코 서버의 단점

- 클라이언트의 연결요청에 대한 처리를 위한 넌블로킹 모드의 accept 함수호출

- alertable wait 상태로의 진입을 위한 SleepEx 함수호출

=> 이 두 함수를 번갈아가며 반복호출 되는 것이 성능에 영향을 미칠 수 있음

=> 이렇게 구성할 수 밖에 없는 이유는 둘 중 하나만 호출할 수 있는 상황이 아니기 때문

따라서,

=> accept 함수의 호출은 main 쓰레드(main 함수 내에서) 처리하도록 하고,

별도의 쓰레드를 추가로 하나 생성해서 클라이언트와의 입출력을 따로 담당하게 하자!

=> 이것이 IOCP에서 제안하는 서버의 구현 모델이다.

즉, IOCP에서는 IO를 전담하는 쓰레드를 별도로 생성하고,

이 쓰레드가 모든 클라이언트를 대상으로 IO를 진행한다.

 

# IOCP를 관찰할 때 추가적인 쓰레드 생성에 중점을 두지말고

1) 입출력이 넌-블로킹 모드로 동작하는가?

2) 넌-블로킹 모드로 진행된 입출력의 완료는 어떻게 확인하는가?

=> 모든 IO 모델을 비롯해, IOCP까지 위 두가지 관점에서 특성이 나뉨.


# Completion Port의 생성

- IOCP에서는 완료된 IO의 정보가 Completion Port 오브젝트(이하 CP 오브젝트)라는 커널 오브젝트에 등록됨.

- 그러나 이 등록은 자동적으로 되는 것이 아니라 요청의 과정이 선행되어야 함

("소켓과 CP 오브젝트와의 연결요청"이 필요)

- IOCP 모델의 서버 구현을 위해서는,

1) CP 오브젝트의 생성

2) CP 오브젝트와 소켓의 연결

이 진행되어야 함.

- 이 때, 소켓은 반드시 Overlapped 속성이 부여된 소켓

- 위 두가지 일은 하나의 함수를 통해서 이루어짐

#include <windows.h>

HANDLE CreateIoCompletionPort(
HANDLE FileHandle, HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads);
);
// 성공시 CP 오브젝트의 핸들, 실패시 NULL 반환

- FileHandle : CP 오브젝트 생성시에는 INVALID_HANDLE_VALUE를 전달

- ExistingCompletionPort : CP 오브젝트 생성시에는 NULL 전달

- CompletionKey : CP 오브젝트 생성시에는 0 전달

- NumberOfConcurrentThreads : CP 오브젝트에 할당되어 완료된 IO를 처리할 쓰레드의 수를 전달, 예를 들어 2가 전달되면 CP 오브젝트에 할당되어 동시 실행 가능한 쓰레드의 수는 최대 2개로 제한된다. 그리고 이 인자에 0이 전달되면 시스템의 CPU 개수가 동시 실행 가능한 쓰레드의 최대수로 지정된다.

 

- 위 함수를 CP 오브젝트의 생성을 목적으로 호출할 때는 마지막 매개변수만 의미를 가짐

 

# Completion Port 오브젝트와 소켓의 연결

- CP 오브젝트가 생성되었다면, 이를 소켓과 연결시켜야 함

- 그래야 완료된 소켓의 IO 정보가 CP 오브젝트에 등록됨

#include <windows.h>

HANDLE CreateIoCompletionPort(
HANDLE FileHandle, HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads);
);
// 성공시 CP 오브젝트의 핸들, 실패시 NULL 반환

- FileHandle : CP 오브젝트에 연결할 소켓의 핸들 전달

- ExistingCompletionPort : 소켓과 연결할 CP 오브젝트의 핸들 전달

- CompletionKey : 완료된 IO 관련 정보의 전달을 위한 매개변수

- NumberOfConcurrentThreads : 어떠한 값을 전달하건, 이 함수의 두번째 매개변수가 NULL이 아니면 그냥 무시됨

 

- 위 함수가 호출된 이후부터는 해당 소켓을 대상으로 진행된 IO가 완료되면, 이에 대한 정보가 해당 CP 오브젝트에 등록된다.

 

# Completion Port의 완료된 IO 확인과 쓰레드의 IO처리

- CP에 등록되는 완료된 IO의 확인방법

#include <windows.h>

BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort, LPDWORD lpNumberOfBytes, PULONG_PTR lpCompletionKey,
LPOVERLAPPED* lpOverlapped, DWROD dwmilliseconds);
);
// 성공시 TRUE, 실패시 FALSE 반환

- CompletionPort : 완료된 IO 정보가 등록되어 있는 CP 오브젝트의 핸들 전달

- lpNumberOfBytes : 입출력 과정에서 송수신 된 데이터의 크기정보를 저장할 변수의 주소값 전달

- lpCompeletionKey : CreateloCompletionPort 함수의 세번째 인자로 전달된 값의 저장을 위한 변수의 주소값 전달

=>

- lpOverlapped : WSASend, WSARecv 함수호출시 전달하는 OVERLAPPED 구조체 변수(WSAOVERLAPPED)의 주소값이 저장될, 변수의 주소값 전달 

- dwMilliseconds : 타임아웃 정보전달, 여기서 지정한 시간이 완료되면 FALSE를 반환하면서 함수를 빠져나가며, INFINITE를 전달하면 완료된 IO가 CP 오브젝트에 등록될 때까지 블로킹 상태에 있게 된다.

 

- 이 함수는 IOCP의 완료된 IO 처리를 담당하는 쓰레드가 호출해야 함

- 우리가 쓰레드를 직접 생성하여 입출력함수를 호출해 IO를 담당하게끔 해야하며,

이 쓰레드가 IO의 완료를 위해서 GetQueuedCompletionStatus를 호출하는 것임

- 성능을 최대로 이끌어 낼 CP 오브젝트에 할당할 쓰레드의 적정 개수를 정확히 파악하고 싶다면, 오로지 실험적 결과를 통해서만 확인이 가능하며, MSDN에서는 적정 개수를 "컴퓨터의 CPU(코어가 아님) 수"라고 설명하고 있다.


# IOCP 기반의 에코 서버의 구현

 

IOCPEchoServ_win.c

#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <winsock2.h>
#include <windows.h>

#define BUF_SIZE 100
#define READ 3
#define WRITE 5

/* 클라이언트와 연결된 소켓정보를 담기 위한 구조체 */
typedef struct // socket info
{
    SOCKET hClntSock;
    SOCKADDR_IN clntAdr;
} PER_HANDLE_DATA, *LPPER_HANDLE_DATA;

/*
IO에 사용되는 버퍼와 Overlapped IO에 반드시 필요한
OVERLAPPED 구조체 변수를 담아서 구조체를 정의
"구조체 변수의 주소값은 구조체 첫번째 멤버의 주소값과 일치함을 기억"
*/
typedef struct // buffer info
{
    OVERLAPPED overlapped;
    WSABUF wsaBuf;
    char buffer[BUF_SIZE];
    int rmMode; // READ or WRITE
} PER_IO_DATA, *LPPER_IO_DATA;

DWORD WINAPI EchoThreadMain(LPVOID CompletionPortIO);
void ErrorHandling(char* message);

int main(int argc, char* argv[])
{
    WSADATA wsaData;
    HANDLE hComPort;
    SYSTEM_INFO sysInfo;
    LPPER_IO_DATA ioInfo;
    LPPER_HANDLE_DATA handleInfo;

    SOCKET hServSock;
    SOCKADDR_IN servAdr;
    int recvBytes, i, flags=0;

    if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) // 윈속 라이브러리 초기화
        ErrorHandling("WSAStartup() error");
    
    /* CP 오브젝트 생성 */
    hComPort=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);

    GetSystemInfo(&sysInfo); // 현재 실행중인 시스템 정보를 얻기

    /*
    컴퓨터 내 CPU 수만큼 쓰레드를 생성 및 실행
    각 쓰레드에 CP 오브젝트의 핸들 전달
    각 쓰레드는 이 핸들을 통해 CP 오브젝트에 접근가능
    */
    for(i=0;i<sysInfo.dwNumberOfProcessors;++i)
        _beginthreadex(NULL,0,EchoThreadMain,(LPVOID)hComport,0,NULL);

    /*
    Overlapped IO가 가능한 속성 부여
    소켓은 블로킹 모드
    */
    hServSock=WSASocket(PF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);

    /* 서버 주소정보 초기화 */
    memset(&servAdr,0,sizeof(servAdr));
    servAdr.sin_family=AF_INET;
    servAdr.sin_addr.s_addr=htonl(INADDR_ANY);
    servAdr.sin_port=htons(atoi(argv[1]));

    /* 서버 주소정보를 기반으로 주소할당 */
    bind(hServSock,(SOCKADDR*)&servAdr,sizeof(servAdr));

    /* 서버(리스닝)소켓이 됨 */
    listen(hServSock,5);

    while(1)
    {
        SOCKET hClntSock;
        SOCKADDR_IN clntAdr;
        int addrLen=sizeof(clntAdr);

        /*
        클라이언트의 연결요청을 수락 
        클라이언트와의 송수신을 위해 새로운 소켓 생성
        */
        hClntSock=accept(hServSock,(SOCKADDR*)&clntAdr,&addrLen);
        handleInfo=(LPPER_HANDLE_DATA)malloc(sizeof(PER_HANDLE_DATA));
        handleInfo->hClntSock=hClntSock; // accept 함수로 호출된 소켓의 핸들을 구조체 안에 저장

        /*
        accept 함수호출로 생성된 소켓의 주소정보를 
        handleInfo->clntAdr로 복사
        */
        memcpy(&(handlerInfo->clntAdr),&clntAdr,addrLen);

        /*
        생성된 소켓과 CP 오브젝트와 연결
        세번째 인자로 전달된
        클라이언트와의 연결을 위해 생성된 소켓에 대한 정보는
        GetQueuedCompletionStatus 함수를 호출하는
        CP 오브젝트에 할당된 쓰레드에서 반환받게 됨
        */
        CreateIoCompletionPort((HANDLE)hClntSock,hComPort,(DWORD)handleInfo,0);

        /*
        WSARecv 함수호출에 필요한
        OVERLAPPED 구조체 변수, WASBUF 구조체 변수, 버퍼를 한번에 마련.
        단, OVERLAPPED 구조체 변수는
        매번 다른 구조체변수의 주소값을 함수에 전달해야하므로 동적할당.
        */
        ioInfo=(LPPER_IO_DATA)malloc(sizeof(sizeof(PER_IO_DATA)));
        memset(&(ioInfo->overlapped),0,sizeof(OVERLAPPED));
        ioInfo->wasBuf.len=BUF_SIZE;
        ioInfo->wasBuf.buf=ioInfo->buffer;

        /*
        IOCP는 입력의완료/출력의완료 를 구분해주지 않고,
        완료되었다는 사실만 인식시켜주므로,
        입력을 진행한 것인지 출력을 진행한 것인지 따로 기록해두어야 함
        */
        ioInfo->rwMode=READ;

        /*
        Overlapped IO 실행 
        CR은 등록하지 않음
        여섯번째 인자로 전달한 OVERLAPPED 구조체 변수 정보는
        GetQueuedCompletionStatus 함수를 호출하는
        CP 오브젝트에 할당된 쓰레드에서 반환받게 됨
        */
        WSARecv(handleInfo->hClntSock,&(ioInfo->wsaBuf),
        1,&recvBytes,&flags,&(ioInfo->overlapped),NULL);

    }
    closesocket(hServSock);
    WSACleanup();
    return 0;
}

/* 
함수 내에서 GetQueuedCompletionStatus 함수를 호출하므로
이 함수를 main으로 호출하는 쓰레드를
"CP 오브젝트에 할당된 쓰레드"라고 함
*/
DWORD WINAPI EchoThreadMain(LPVOID CompletionPortIO)
{
    HANDLE hComPort=(HANDLE)CompletionPortIO; // CP 오브젝트의 핸들
    SOCKET sock;
    DWORD bytesTrans;
    LPPER_HANDLE_DATA handleInfo;
    LAPPER_IO_DATA ioInfo;
    DWORD flags=0;

    while(1)
    {
        /*
        완료된 IO의 확인
        IO가 완료되고 이에 대한 정보가 CP 오브젝트에 등록되었을 때 반환(인자가 INFINITE 이므로)
        반환시 세번째,네번째 인자로 얻는 정보가 무엇인지 정확히 알아야 함
        (CreateloCompletionPort 함수와
        WSARecv 함수에 전달한 인자 정보를
        이 때 반환받아서 사용할 수 있음)
        */
        GetQueuedCompletionStatus(hComPort,&bytesTrans,
        (LPWORD)&handleInfo, (LPOVERLAPPED*)&ioInfo, INFINITE);

        sock=handleInfo->hClntSock; // IO가 완료된 소켓의 핸들

        if(ioInfo->rwMode==READ) // 완료된 IO가 데이터 수신이라면
        {
            puts("message received!");
            if(bytesTrans==0) // 클라이언트의 EOF 전송시
            {
                closesocket(sock); // 생성한 소켓 소멸

                /*
                이 소켓에 대한 IOCP를 위하여 생성한
                동적할당 메모리 해제
                */
                free(handleInfo); free(ioInfo);
                continue;
            }


            /* 서버가 수신한 메시지를 클라이언트에게 재전송 */

            /* OVERLAPPED 구조체 변수 정보 초기화 */
            memset(&(ioInfo->overlapped),0,sizeof(OVERLAPPED));
            ioInfo->wsaBuf.len=bytesTrans; // 수신한 데이터크기만큼
            ioInfo->rwMode=WRITE; // 데이터 전송이므로 

            WSASend(sock,&(ioInfo->wasBuf),
            1,NULL,0,&(ioInfo->overlapped),NULL);


            /*
            클라이언트로 메시지 재전송 이후,
            클라이언트로부터 다시 메시지를 수신
            */
            ioInfo=(LAPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));

            memset(&(ioInfo->overlapped),0,sizeof(OVERLAPPED));
            ioInfo->wsaBuf.len=BUF_SIZE;
            ioInfo->wsaBuf.buf=ioInfo->buffer;
            ioInfo->rwMode=READ;

            WSARecv(sock,&(ioInfo->wsaBuf),
            1,NULL,&flags,&(ioInfo->overlapped),NULL);

        }
        else // 완료된 IO가 데이터 송신이라면
        {
            puts("message sent!");

            /*
            이 소켓에 대한 IOCP를 위하여 생성한
            동적할당 메모리 해제
            */
            free(ioInfo);
        }
    }
    return 0;
}

void ErrorHandling(char* message)
{
    fputs(message, stderr);
    fputc('\n',stderr);
    exit(EXIT_FAILURE);
}

- StableEchoClnt_win.c 와 함께 실행하면 된다.


# IOCP가 성능이 좀 더 나오는 이유

- 코드 레벨에서 select와 비교해보면,

1) 넌-블로킹 방식으로 IO가 진행되기 때문에, IO 작업으로 인한 시간의 지연이 발생하지 않는다.

(select는 일반적으로 IO가 완료될 때까지 블로킹 상태에 있어 다른 작업을 하기 어렵다.)

2) IO가 완료된 핸들을 찾기 위해서 반복문을 구성할 필요가 없다.

(select는 IO 완료를 확인하기 위해서는 관심대상 소켓 전부를 일일히 확인해야만 한다.)

3) IO의 진행대상인 소켓의 핸들을 배열에 저장해 놓고, 관리할 필요가 없다.

(select 함수는 관심대상의 핸들을 저장해 놓고 관리해놓아야 한다. fd_set형 변수를 기억하자.)

4) IO의 처리를 위한 쓰레드의 수를 조절할 수 있다. 따라서 실험적 결과를 토대로 적절한 쓰레드의 수를 지정할 수 있다.


[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어