Chapter 23 - IOCP(Input Output Completion Port)

23-1 : Overlapped IO를 기반으로 IOCP 이해하기

리눅스의 epoll과 윈도우의 IOCP 사이의 절대적 비교우위는 존재하지 않음
대신 하드웨어의 성능 / 할당된 대역폭이 충분한 상황에서 응답시간 / 동시접속자 수에 문제가 발생할 경우 아래 사항들을 먼저 고려

  • 비효율적인 IO의 구성 또는 비효율적인 CPU의 활용
  • 데이터베이스의 설계내용과 쿼리(Query)의 구성

윈도우에서는 ioctlsocket() 함수를 통해 논블로킹 모드로 소켓의 속성을 변경할 수 있음

#include <winsock2.h>

int WSAAPI ioctlsocket(SOCEKT s, long cmd, u_long *argp);
// 성공시 0, 실패시 SOCKET_ERROR 반환
  • cmd : 소켓의 IO방식을 지정
  • cmd를 입출력 모드를 뜻하는 FIONBIO로, argp를 0이 아닌 값으로 두면 논블로킹 모드로 변경됨
    • 위와같이 변경시 추가로 클라이언트의 연결요청이 존재하지 않는 상태에서 accept() 함수 호출시 곧바로 INVALID_SOCKET을 반환
    • 이어서 WSAGetLastError() 함수 호출시 WSAEWOULDBLCOK을 반환
    • accept() 함수의 호출로 새로 생성되는 소켓 또한 논블로킹 속성을 지님

Overlapped IO 기반의 서버구현에는 반드시 논블로킹 소켓이 필요
Overlapped IO를 사용한 에코 서버 예시

// CmplRouEchoServ_win.cpp

#include <iostream>
#include <winsock2.h>

using namespace std;

#define BUF_SIZE 1024
void CALLBACK	ReadCompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void CALLBACK	WriteCompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void			ErrorHandling(char *message);

typedef struct
{
	SOCEKT	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)
	{
		cout << "Usage : " << argv[0] << " <port>\n";
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error");

	hLisnSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
	ioctlsocket(hLisnSock, FIONBIO, &mode);

	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)
	{
		SleepEx(100, TRUE);	// for alertable wait state
		hRecvSock = accept(hLisnSock, (SOCKADDR*)&recvAdr, &recvAdrSz);
		if (hRecvSock == INVALID_SOCKET)
		{
			if (WSAGetLastError() == WSAEWOULDBLOCK)
				continue;
			else
				ErrorHandling("accept() error");
		}
		cout << "Client connected....." << endl;

		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;
		WSARecv(hRecvSock, &(hbInfo->wsaBuf), 1, &recvBytes, &flagInfo, lpOvLp, ReadCompRoutine);
	}
	closesocket(hRecvSock);
	closesocket(hLisnSock);
	WSACleanup();
	return 0;
}

void CALLBACK	ReadCompRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
	LPPER_IO_DATA	hbInfo = (LPPER_IO_DATA)(lpOverlapped->hEvent);
	SOCEKT			hSock = hbInfo->hClntSock;
	LPWSABUF		bufInfo = &(hbInfo->wsaBuf);
	DWORD			sentBytes;

	if (szRecvBytes == 0)
	{
		closesocket(hSock);
		free(lpOverlapped->hEvent);
		free(lpOverlapped);
		cout << "Client disconnected.....\n";
	}
	else	// echo
	{
		bufInfo->len = szRecvBytes;
		WSASend(hSock, bufInfo, 1, &sentBytes, 0, lpOverlapped, WriteCompRoutine);
	}
}

void CALLBACK	WriteCompRoutine(DWORD dwError, DWORD szSendBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
	LPPER_IO_DATA	hbInfo = (LPPER_IO_DATA)(lpOverlapped->hEvent);
	SOCEKT			hSock = hbInfo->hClntSock;
	LPWSABUF		bufInfo = &(hbInfo->wsaBuf);
	DWORD			recvBytes;
	int				flagInfo = 0;

	WSARecv(hSock, bufInfo, 1, &recvBytes, &flagInfo, lpOverlapped, ReadCompRoutine);
}

void	ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
  • 클라이언트가 연결되면 WSARecv() 함수를 호출하면서 논블로킹 모드로 데이터 수신, 수신 완료시 ReadCompRoutine() 함수가 호출됨
  • ReadCompRoutine() 함수 호출시 WSASend() 함수를 호출하면서 논블로킹 모드로 데이터 송신, 송신 완료시 WriteCompRoutine() 함수 호출
  • 호출된 WriteCompRoutine() 함수는 다시 WSARecv() 함수를 호출하면서 논블로킹 모드로 데이터 수신을 기다림

소켓은 처음 생성시 블로킹 모드이기 때문에 ioctlsocket() 함수를 통해 논블로킹 모드로 전환
Completion Routine 기반 Overlapped IO에서는 Event 오브젝트가 불필요하기 때문에 hEvent에 다른 정보를 채울 수 있음
따라서 입출력 완료시 자동으로 호출되는 Completion Routine 내부로 클라이언트 정보(소켓 / 버퍼)를 전달하기 위해 WSAOVERLAPPED 구조체의 멤버 hEvent를 사용


약간의 문제점을 해결한 에코 클라이언트 예시

// StableEchoClnt_win.cpp

#include <iostream>
#include <winsock2.h>

using namespace std;

#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)
	{
		cout << "Usage : " << argv[0] << " <IP> <port>\n";
		exit(1);
	}

	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 = inet_addr(argv[1]);
	servAdr.sin_port = htons(atoi(argv[2]));

	if (::connect(hSocket, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
		ErrorHandling("connect() error");
	else
		cout << "Connected.................\n";

	while (1)
	{
		cout << "Input message(Q to quit) : ";
		cin.getline(message, BUF_SIZE);

		if (!strcmp(message, "q") || !strcmp(message, "Q"))
			break;

		strLen = strlen(message);
		send(hSocket, message, strLen, 0);
		readLen = 0;
		while (1)
		{
			readLen += recv(hSocket, &message[readLen], BUF_SIZE - 1, 0);
			if (readLen >= strLen)
				break;
		}
		message[strLen] = 0;
		cout << "Message from server : " << message << endl;
	}
	closesocket(hSocket);
	WSACleanup();
	return 0;
}

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


Overlapped IO 모델 에코 서버의 경우 논블로킹 모드의 accept() 함수와 alertable wait 상태 진입을 위한 SleepEx() 함수의 반복 호출이 성능에 영향을 미칠 수 있음
이를 해결하기 위해 accept() 함수 호출은 메인 쓰레드가 처리하도록 하고, 별도로 쓰레드를 하나 생성하여 클라이언트와의 입출력을 담당하게 할 수 있음
이것이 IOCP에서 제안하는 서버의 구현 모델임


23-2 : IOCP의 단계적 구현

IOCP에서는 완료된 IO의 정보가 Completion Port 오브젝트라는 커널 오브젝트에 등록됨
단, 이를 위해서는 소켓(반드시 Overlapped 속성이 부여되어있어야함)과 CP 오브젝트와의 연결 요청이 필요
CP 오브젝트의 생성에는 CreateIoCompletionPort() 함수를 사용

#include <windows.h>

HANDLE CreateIoCompletionPort(HANDLE FileHandle, HANDLE ExistingCompletionPort, ULONG_PTR CompletionKey, DWORD NumberOfConcurretThreads);
// 성공시 CP 오브젝트의 핸들, 실패시 NULL 반환
  • FileHandle : CP 오브젝트 생성시 INVALID_HANDLE_VALUE
  • ExistingCompletionPort : CP 오브젝트 생성시 NULL
  • CompletionKey : CP 오브젝트 생성시 0
  • NumberOfConcurrentThreads : CP 오브젝트에 할당되어 완료된 IO를 처리할 쓰레드 수, 0 전달시 시스템의 CPU가 동시 실행 가능한 최대 쓰레드 수로 설정

CP 오브젝트를 소켓에 연결시킬 때도 CreateIoCompletionPort() 함수를 사용하며, 이 때는 매개변수의 사용이 달라짐

  • FileHandle : CP 오브젝트에 연결할 소켓의 핸들
  • ExistingCompletionPort : 소켓과 연결할 CP 오브젝트의 핸들
  • CompletionKey : 완료된 IO 관련 정보의 전달을 위한 매개변수
  • NumberOfConcurrentThreads : ExistingCompletionPort가 NULL이 아니면 무시됨


CP에 등록되는 완료된 IO의 확인에는 GetQueuedCompletionStatus() 함수를 사용

#include <windows.h>

BOOL GetQueuedCompletionStatus(HANDLE CompletionPort, LPDWORD lpNumberOfBytes, PULONG_PTR lpCompletionKey, LPOVERLAPPED* lpOverlapped, DWORD dwMilliseconds);
// 성공시 TRUE, 실패시 FALSE 반환
  • CompletionPort : 완료된 IO 정보가 등록되어있는 CP 오브젝트 핸들
  • lpNumberOfBytes : 입출력 과정에서 송수신된 데이터 크기정보를 저장할 변수의 주소값
  • lpCompletionKey : CreateIoCompletionPort() 함수의 CompetionKey 값의 저장을 위한 변수의 주소값
  • lpOverlapped : WSASend() / WSARecv() 함수 호출시 OVERLAPPED 구조체 변수의 주소값이 저장될 변수의 주소값
  • dwMilliseconds : 타임아웃, 지정한 시간 완료시 즉시 FALSE 반환 / INFINITE로 지정시 완료된 IO가 CP 오브젝트에 등록될 때까지 블로킹 상태 유지


IOCP 기반 에코 서버 구현

// IOCPEchoServ_win.cpp
// 클라이언트는 이전의 StableEchoClnt_win.cpp를 사용

#include <iostream>
#include <process.h>
#include <windows.h>
#include <winsock2.h>

using namespace std;

#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;

typedef struct	// buffer info
{
	OVERLAPPED	overlapped;
	WSABUF		wsaBuf;
	char		buffer[BUF_SIZE];
	int			rwMode;	// 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, flags = 0;

	if(argc != 2)
	{
		cout << "Usage : " << argv[0] << " <port>\n";
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error");

	hComPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
	GetSystemInfo(&sysInfo);
	for (int i = 0; i < sysInfo.dwNumberOfProcessors; i++)
		_beginthreadex(NULL, 0, EchoThreadMain, (LPVOID)hComPort, 0, NULL);
	
	hServSock = WSASocket(AF_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]));

	if (::bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
		ErrorHandling("bind() error");

	if (::listen(hServSock, 5) == SOCKET_ERROR)
		ErrorHandling("listen() error");

	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;
		memcpy(&(handleInfo->clntAdr), &clntAdr, addrLen);

		CreateIoCompletionPort((HANDLE)hClntSock, hComPort, (DWORD)handleInfo, 0);

		ioInfo = (LPPER_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(handleInfo->hClntSock, &(ioInfo->wsaBuf), 1, &recvBytes, &flags, &(ioInfo->overlapped), NULL);
	}
	return 0;
}

DWORD WINAPI	EchoThreadMain(LPVOID pComPort)
{
	HANDLE				hComPort = (HANDLE)pComPort;
	SOCkET				sock;
	DWORD				bytesTrans;
	LPPER_HANDLE_DATA	handleInfo;
	LPPER_IO_DATA		ioInfo;
	DWORD				flags = 0;

	while (1)
	{
		GetQueuedCompletionStatus(hComPort, &bytesTrans, (LPDWORD)&handleInfo, (LPOVERLAPPED*)&ioInfo, INFINITE);
		sock = handleInfo->hClntSock;

		if (ioInfo->rwMode == READ)
		{
			cout << "message received!\n";
			if (bytestrans == 0)	// EOF 전송시
			{
				closesocket(sock);
				free(handleInfo);
				free(ioInfo);
				continue;
			}

			memset(&(ioINfo->overlapped), 0, sizeof(OVERLAPPED));
			ioInfo->wsaBuf.len = bytesTrans;
			ioInfo->rwMode = WRITE;
			WSASend(sock, &(ioInfo->wsaBuf), 1, NULL, 0, &(ioInfo->overlapped), NULL);

			ioInfo = (LPPER_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
		{
			cout << "message sent!\n";
			free(ioInfo);
		}
	}
	return 0;
}

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


IOCP가 성능이 좀더 나은 이유

  • 논블로킹 방식으로 IO가 진행되기 때문에 IO 작업으로 인한 시간지연이 발생하지 않음
  • IO가 완료된 핸들을 찾기 위한 반복문을 구성할 필요가 없음
  • IO의 진행대상인 소켓 핸들을 배열에 저장하여 관리할 필요가 없음
  • IO의 처리를 위한 쓰레드의 수를 조절할 수 있음

내용 확인문제

  1. Completion Port 오브젝트에는 하나 이상의 쓰레드가 할당되어서 입출력을 처리하게된다. 그렇다면 Completion Port 오브젝트에 할당될 쓰레드는 어떻게 생성되며, 또 할당의 방법은 무엇인지 소스코드 레벨에서 설명해보자.

    Completion Port 오브젝트에 할당할 쓰레드는 프로그래머가 직접 생성해주어야하며, 해당 쓰레드내에서 GetQueuedCompletionPort() 함수를 호출하는 것이 해당 오브젝트에 할당함을 의미함

  2. CreateIoCompletionPort 함수는 다른 함수들과 달리 두 가지의 기능을 제공한다. 그렇다면 이 두 가지 기능은 각각 무엇인가?

    Completion Port 오브젝트의 생성, 오브젝트와 소켓과의 연결

  3. Completion Port 오브젝트와 소켓의 연결이 의미하는 바는 무엇인가? 그리고 연결의 과정은 어떻게 진행해야 하는가?

    IO 작업의 완료 여부를 관찰할 소켓이 Completion Port 오브젝트에 등록되었음을 뜻함
    CreateIoCompletionPort() 함수를 사용하여 연결

  4. IOCP와 관련된 다음 문장들 중에서 옳지 않은 것을 모두 고르면?
    • 최소한의 스레드로 다수의 IO를 처리할 수 있는 구조이기 때문에 쓰레드의 컨텍스트 스위칭으로 인한 성능의 저하를 막을 수 있다. (O)
    • IO가 진행중인 상태에서 서버는 IO의 완료를 기다리지 않고, 다른 일을 진행할 수 있기 때문에 CPU를 효율적으로 사용할 수 있는 구조이다. (O)
    • IO가 완료되었을 때, 이와 관련된 Completion Routine이 자동으로 호출되기 때문에 IO의 완료를 대기하기 위해서 별도의 함수를 호출할 필요가 없다. (X)
    • IOCP는 윈도우 이외의 다른 운영체제에서도 제공되는 기능이기 때문에 호환성에 있어서도 높은 점수를 줄 수 있다.(X)
  5. 다음 문장들 중에서 IOCP에 할당할 적정 쓰레드의 수를 결정하는 방법으로 적절하면 O, 적절치 않으면 X를 표시해보자.
    • 일반적인 선택은 CPU의 수와 동일한 수의 쓰레드를 할당하는 것이다. (O)
    • 가장 좋은 방법은 여건이 허락하는 범위 내에서 실험적인 결과를 통해 쓰레드의 수를 결정하는 것이다. (O)
    • 할당할 쓰레드의 수는 여유있게 선택하는 것이 좋다. 예를 들어서 한 개의 쓰레드로 충분한 상황에서는 여유있게 세 개 정도의 쓰레드를 IOCP에 할당하는 것이 좋다. (X)
  6. 이번 Chapter에서 설명한 IOCP 모델을 바탕으로 채팅 서버를 구현해보자. 이 채팅서버는 Chapter 20에서 소개한 채팅 클라이언트인 예제 chat_clnt_win.cpp와 함께 동작이 가능해야한다. 참고로 본문에서 제시한 IOCP 예제는 하나의 사례일 뿐이니, 이 틀에 꼭 맞추려고 노력하지 않아도 된다. 이 틀에 완벽히 맞추려다보면 오히려 구현이 어렵게 느껴질 수 있다.

     // IOCPchatserv_win.cpp
     // 클라이언트는 이전의 chat_clnt_win.cpp를 사용
    
     #include <iostream>
     #include <process.h>
     #include <windows.h>
     #include <winsock2.h>
    
     using namespace std;
    
     #define BUF_SIZE	100
     #define MAX_CLNT	256
    
     typedef struct	// socket info
     {
         SOCKET		hClntSock;
         SOCKADDR_IN	clntAdr;
     } PER_HANDLE_DATA, *LPPER_HANDLE_DATA;
    
     typedef struct	// buffer info
     {
         OVERLAPPED	overlapped;
         WSABUF		wsaBuf;
         char		buffer[BUF_SIZE];
     } PER_IO_DATA, *LPPER_IO_DATA;
    
     DWORD WINAPI	EchoThreadMain(LPVOID CompletionPortIO);
     void			ErrorHandling(char *message);
    
     int		clntCnt = 0;
     SOCKET	clntSocks[MAX_CLNT];
    
     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, flags = 0;
    
    
         if(argc != 2)
         {
             cout << "Usage : " << argv[0] << " <port>\n";
             exit(1);
         }
    
         if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
             ErrorHandling("WSAStartup() error");
    
         hComPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
         GetSystemInfo(&sysInfo);
         for (int i = 0; i < sysInfo.dwNumberOfProcessors; i++)
             _beginthreadex(NULL, 0, EchoThreadMain, (LPVOID)hComPort, 0, NULL);
    		
         hServSock = WSASocket(AF_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]));
    
         if (::bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
             ErrorHandling("bind() error");
    
         if (::listen(hServSock, 5) == SOCKET_ERROR)
             ErrorHandling("listen() error");
    
         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;
             memcpy(&(handleInfo->clntAdr), &clntAdr, addrLen);
    
             clntSocks[clntCnt++] = hClntSock;
    
             CreateIoCompletionPort((HANDLE)hClntSock, hComPort, (DWORD)handleInfo, 0);
    
             ioInfo = (LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
             memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));
             ioInfo->wsaBuf.len = BUF_SIZE;
             ioInfo->wsaBuf.buf = ioInfo->buffer;
             WSARecv(handleInfo->hClntSock, &(ioInfo->wsaBuf), 1, &recvBytes, &flags, &(ioInfo->overlapped), NULL);
         }
         return 0;
     }
    
     DWORD WINAPI	EchoThreadMain(LPVOID pComPort)
     {
         HANDLE				hComPort = (HANDLE)pComPort;
         SOCkET				sock;
         DWORD				bytesTrans;
         LPPER_HANDLE_DATA	handleInfo;
         LPPER_IO_DATA		ioInfo;
         DWORD				flags = 0;
    
         while (1)
         {
             GetQueuedCompletionStatus(hComPort, &bytesTrans, (LPDWORD)&handleInfo, (LPOVERLAPPED*)&ioInfo, INFINITE);
             sock = handleInfo->hClntSock;
    
             cout << "message received!\n";
             if (bytestrans == 0)	// EOF 전송시
             {
                 for (int i = 0; i < clntCnt; i++)
                 {
                     if (sock == clntSocks[i])
                     {
                         while (i++ < clntCnt - 1)
                             clntSocks[i] = clntSocks[i + 1];
                     }
                 }
                 clntCnt--;
                 closesocket(sock);
                 free(handleInfo);
                 free(ioInfo);
                 continue;
             }
    
             for (int i = 0; i < clntCnt; i++)
                 send(clntSocks[i], ioInfo->buffer, bytesTrans, 0);
    
             memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));
             WSARecv(sock, &(ioInfo->wsaBuf), 1, NULL, &flags, &(ioInfo->overlapped), NULL);
         }
         return 0;
     }
    
     void	ErrorHandling(char *message)
     {
         fputs(message, stderr);
         fputc('\n', stderr);
         exit(1);
     }