책정리/윈도우 네트워크 프로그래밍

10장 소켓 입출력 모델(II)

GONII 2015. 7. 17. 13:23
  • Overlapped 모델(1)

    Overlapped 모델은 지금까지 배운 다른 소켓 입출력 모델과는 근본적으로 다른 입출력 방식으로, 고성능을 제공한다. 또한 동작 방식에 따라 크게 두 종류의 작성 방법을 지원한다.

    동작 원리

    원래 Overlapped 입출력 방식은 윈도우 운영체제에서 고성능 파일 입출력을 위해 제공하는데, 이를 소켓 입출력에서도 사용할 수 있게 만든 것이 Overlapped 모델이다.

    위의 그림은 동기 입출력(synchronous I/O)라고 부른다. 애플리케이션은 입출력 함수를 호출한 후 입출력 작업이 끝날 때까지 대기한다. 입출력 작업이 끝나면 입출력 함수는 리턴하고 애플리케이션은 입출력 결과를 처리하거나 다른 작업을 진행할 수 있다. Select, WSAAsyncSelect, WSAEventSelect 모델은 모두 동기 입출력 방식으로 소켓 입출력을 처리한다. 단, 입출력 함수를 안전하게 호출할 수 있는 시점을 운영체제가 알려주기 때문에 단순한 동기 입출력 방식보다 편리하게 여러 소켓을 처리할 수 있는 것이다. 이와 같이 운영체제가 함수 호출 시점을 알려주는 개념을 비동기 통지(asynchronous notification)라고 부른다. 요약하면 Select, WSAAsyncSelect, WSAEventSelect 모델은 동기 입출력과 비동기 통지를 결합한 형태라 할 수 있다.

    위 그림은 비동기 입출력(asynchronous I/O)또는 중첩 입출력(overlapped I/O)라고 부른다. 애플리케이션은 입출력 함수를 호출한 후 입출력 작업의 완료 여부와 무관하게 다른 작업을 진행할 수 있다. 입출력 작업이 끝나면 운영체제는 작업 완료를 애플리케이션에 알려준다. 이때 애플리케이션은 다른 작업을 중단하고 입출력 결과를 처리하면 된다. Overlapped와 Completion Port 모델은 모두 비동기 입출력 방식으로 소켓 입출력을 처리한다. 비동기 입출력 방식에서는 입출력 완료를 운영체제가 알려주는 개념이 반드시 필요하므로 비동기 통지도 사용한다고 볼 수 있다. 요약하면 Overlapped, Completion Port 모델은 비동기 입출력과 비동기 통지를 결합한 형태라고 할 수 있다.

       

    Overlapped 모델을 사용하는 소켓 애플리케이션은 다음과 같이 세 단계의 공통 절차를 따른다.

    • 비동기 입출력을 지원하는 소켓을 생성한다.

      socket() 함수로 생성산 소켓은 기본적으로 비동기 입출력을 지원한다.

    • 비동기 입출력을 지원하는 소켓 함수를 호출 한다.

      총 13개의 함수가 있다.

AcceptEx(), ConnectEx(), DisconnectEx(), TransmitFile(), TransmitPackets(), WSAIoctl(), WSANSPIoctl(), WSAProviderConfigChange(), WSARecv(), WSARecvFrom(), WSARecvMsg(), WSASend(), WSASendTo()

  • 운영체제는 소켓 입출력 작업 완료를 애플리케이션에 알려주고(=비동기 통지), 애플리케이션은 입출력 결과를 처리한다.

   

Overlapped 모델은 운영체제의 비동기 통지 방식에 따라 두 종류로 구분한다. 둘 중 어떤 것을 선택하는가에 따라 애플리케이션의 구조는 달라진다.

Overlapped 모델의 종류

종류

설명

Overlapped 모델(1)

소켓 입출력 작업이 완료되면, 운영체제는 애플리케이션이 등록한 이벤트 객체를 신호 상태로 바꾼다. 애플리케이션은 이벤트 객체를 관찰함으로써 작업 완료 사실을 감지할 수 있다.

Overlapped 모델(2)

소켓 입출력 작업이 완료되면, 운영체제는 애플리케이션이 등록한 함수를 자동으로 호출한다. 일반적으로 운영체제가 호출하는 애플리케이션 함수를 콜백 함수(callback function)라 부르는데, 특별히 Overlapped 모델에서는 완료 루틴(completion routine)이라 칭한다.

소켓 애플리케이션에서 Overlapped 모델을 사용하는 주된 이유는 데이터를 보내고 받는 작업을 효율적으로 처리하기 위해서다. 이때 사용하는 핵심 함수인 WSASend()와 WSARecv()를 살펴보도록 하자.

int WSASend(

SOCKET s,

LPWSABUF lpBuffers,

DWORD dwBufferCount,

LPDWORD lpNumberOfBytesSent,

DWORD dwFlags,

LPWSAOVERLAPPED lpOverlapped,

LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine

);

int WSARecv(

SOCKET s,

LPWSABUF lpBuffers,

DWORD dwBufferCount,

LPDWORD lpNumberOfBytesRecvd,

DWORD dwFlags,

LPWSAOVERLAPPED lpOverlapped,

LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine

);

  • s

    비동기 입출력을 할 소켓이다.

  • lpBuffers

    WSABUF 구조체 배열의 시작 주소다. 각각의 배열 원소(WSABUF 타입)는 버퍼의 시작주소와 길이(바이트 단위)를 담고 있다.

typedef struct __WSABUF {

u_long len; // 길이(바이트 단위)

char FAR* buf; // 버퍼 시작 주소

} WSABUF, *LPWSABUF;

  • dwBufferCount

    WSABUF 구조체 배열의 원소 개수

  • lpNumberOfBytesSent, lpNumberOfBytesRecvd

    DWORD 형 변수 주소값으로, 함수 호출이 성공하면 이 변수에 보내거나 받은 바이트 수가 저장된다.

  • dwFlags, lpFlags

    각각 send() 와 recv() 함수의 마지막 인자와 동일한 역할을 한다.

  • lpOverlapped

    WSAOVERLAPPED 구조체 변수 주소값이다. 이 구조체는 비동기 입출력을 위한 정보를 운영체제에 전달하거나, 운영체제가 비동기 입출력 결과를 애플리케이션에 전달할 때 사용한다. WSAOVERLAPPED 구조체 변수 중 처음 네 개는 운영체제가 내부적으로만 사용한다. 마지막 변수인 hEvent는 이벤트 객체 핸들값으로, Overlapped 모델(1)에서만 사용한다. 입출력 작업이 완료되면 hEvent가 가리키는 이벤트 객체는 신호 상태가 된다.

typedef struct _WSAOVERLAPPED {

DWORD Internal;

DWORD InternalHigh;

DWORD Offset;

DWORD OffsetHigh;

WSAEVENT hEvent;

} WSAOVERLAPPED, *LPWSAOVERLAPPED;

  • lpCompletionRoutine

    입출력 작업이 완료되면 운영체제가 자동으로 호출할 완료 루틴(콜백 함수)의 주소값이다.

WSASend()와 WSARecv() 함수의 특징은 다음과 같다.

  • Scatter / Gather 입출력을 지원한다.

    송신측에서 WSABUF 구조체를 사용하면, 여러 개의 버퍼에 저장된 데이터를 모아서(gather) 보낼 수 있다.

// 송신측 코드

buf1[128];

buf2[256];

WSABUF wsabuf[2];

wsabuf[0].buf = buf1;

wsabuf[0].len = 128;

wsabuf[1].buf = buf2;

wsabuf[1].len = 256;

WSASend(sock, wsabuf, 2, ...);

수신측에서도 역시 WSABUF 구조체를 사용하면, 받은 데이터를 여러 개의 버퍼에 흩뜨려(scatter) 저장할 수 있다.

// 수신측 코드

buf1[128];

buf2[256];

WSABUF wsabuf[2];

wsabuf[0].buf = buf1;

wsabuf[0].len = 128;

wsabuf[1].buf = buf2;

wsabuf[1].len = 256;

WSARecv(sock, wsabuf, 2, ...);

  • 마지막 두 인자에 모두 NULL 값을 사용하면 동기 함수로 동작한다.
  • Overlapped 모델(1)에서는 WSAOVERLAPPED 구조체의 hEvent 변수를, Overlapped 모델(2)에서는 lpCompletionRoutine 인자를 사용한다.

    단, lpCompletionRoutine 인자의 우선 순위가 높으므로 이 값이 NULL이 아니면 WSAOVERLAPPED 구조체의 hEvent 변수는 사용되지 않는다.

지금까지 살펴본 내용은 Overlapped 모델에 공통적으로 적용된다. Overlapped 모델(1)을 이용한 소켓 입출력 방법 절차는 다음과 같다.

  • 비동기 입출력을 지원하는 소켓을 생성한다. 이때 WSACreateEvent() 함수를 호출하여 대응하는 이벤트 객체도 같이 생성한다.
  • 비동기 입출력을 지원하는 소켓 함수를 호출한다. 이때 WSAOVERLAPPED 구조체의 hEvent 변수에 이벤트 객체 핸들값을 넣어서 전달한다. 비동기 입출력 작업이 곧바로 완료되지 않으면, 소켓 함수는 오류를 리턴하고 오류 코드는 WSA_IO_PENDING으로 설정된다. 나중에 비동기 입출력 작업이 완료되면, 운영체제는 이벤트 객체를 신호 상태로 만들어 이 사실을 애플리케이션에 알린다.
  • WSAWaitForMultipleEvents() 함수를 호출하여 이벤트 객체가 신호 상태가 되기를 기다린다.
  • 비동기 입출력 작업이 완료하여 WSAWaitForMultipleEvents() 함수가 리턴하면 WSAGetOverlappedResult() 함수를 호출하여 비동기 입출력 결과를 확인하고 데이터를 처리한다.
  • 새로운 소켓을 생성하면 1 ~ 4를, 그렇지 않으면 2 ~ 4를 반복한다.

WSAGetOverlappedResult() 함수의 원형은 다음과 같다

BOOL WSAGetOverlappedResult(

SOCKET s,

LPWSAOVERLAPPED lpOverlapped,

LPDWORD lpcbTransfer,

BOOL fWait,

LPDWORD lpdwFlags

);

  • s

    비동기 입출력 함수 호출에 사용한 소켓을 다시 넣는다

  • lpOverlapped

    비동기 입출력 함수 호출에 사용한 WSAOVERLAPPED 구조체 변수의 주소값을 다시 넣는다.

  • lpcbTransfer

    DWORD 형 변수 주소값을 전달하면, 이 변수가 가리키는 영역에 전송 바이트 수가 저장된다.

  • fWait

    비동기 입출력 작업이 끝날 때까지 대기하려면 TRUE, 그렇지 않으면 FALSE를 사용한다. WSAWaitForMultipleEvents() 함수를 이전에 호출했다면, 비동기 입출력 작업 완료 사실을 이미 알고 있으므로 FALSE를 사용하면 된다.

  • lpdwFlags

    DWORD형 변수 주소값을 전달하면, 비동기 입출력 작업과 관련된 부가적인 정보가 저장된다. 이 값은 거의 사용하지 않으므로 무시해도 된다.

[실습] Overlapped 모델(1)을 이용한 TCP 서버

서버와 클라이언트의 동작은 다음과 같다

  • 서버 : 클라이언트가 보낸 데이터를 받아(WSARecv), 이를 문자열로 간주하여 무조건 화면에 출력한다. 그리고 받은 데이터를 변경 없이 다시 클라이언트에 보낸다(WSASend)
  • 클라이언트 : 사용자가 키보드로 입력한 문자열을 서버에 보낸다(WSASend). 서버가 받은 데이터를 그대로 되돌려 보내면, 클라이언트는 이를 받아(WSARecv) 화면에 출력한다.

코드 분석

  • Overlapped 모델(2)

    Overlapped 모델(2)는 완료 루틴을 통해 비동기 입출력 결과를 처리한다. 여기서 완료 루틴(completion routine)이란 애플리케이션이 정의한 일종의 콜백 함수로, 운영체제가 적절한 시점에 자동으로 호출하도록 되어 있다.

    동작 원리

    Overlapped 모델(2)를 사용하기 위해서는 비동기 입출력 시작부터 완료 루틴 호출까지의 과정을 이해해야 한다.

    각 단계별 동작을 요약하면 다음과 같다

    • 비동기 입출력 함수를 호출함으로써 운영체제에 입출력 작업을 요청한다.
    • 해당 스레드는 곧바로 alertable wait 상태에 진입한다. alertable wait 상태란 비동기 입출력을 위한 특별한 대기 상태로, 비동기 입출력 함수를 호출한 스레드가 이 상태에 있어야만 완료 루틴이 호출될 수 있다. 스레드를 alertable wait 상태로 만드는 함수는 다양한다. 몇 가지 예를 들면 WaitForSingleObjectEx(), WaitForMultipleObjectEx(), SleepEx(), WSAWaitForMultipleEvents() 등이 있다. 마지막 함수인 WSAWaitForMultipleEvents()의 마지막 인자에 TRUE를 사용하면 해당 스레드는 alertable wait 상태가 된다.
    • 비동기 입출력 작업이 완료되면, 운영체제는 스레드의 APC큐에 결과를 저장한다. 여기서 APC큐(Asynchronous Procedure Call Queue)란 비동기 입출력 결과 저장을 위해 운영체제가 각 스레드마다 할당하는 메모리 영역이다.
    • 비동기 입출력 함수를 호출한 스레드가 alertable wait 상태에 있으면, 운영체제는 APC큐에 저장된 정보(완료 루틴의 주소)를 참조하여 완료 루틴을 호출한다. 완료 루틴 내부에서는 데이터를 처리한 후 다시 비동기 입출력 함수를 호출할 수 있다.
    • APC 큐에 저장된 정보를 토대로 모든 완료 루틴 호출이 끝나면, 스레드는 alertable wait상태에서 빠져나온다. 이 스레드가 비동기 입출력 결과를 계속 처리하려면 다시 alertable wait 상태에 진입해야 한다.

    Overlapped모델(2)의 실행 순서를 보면 처음 시작할 때는 다음과 같은 순서로 실행된다.

    완료 루틴 내부에서 새로운 비동기 입출력을 시작하면 다음과 같은 순서로 실행된다.

    지금까지 소개한 내용은 완료 루틴을 이용한 일반적인 비동기 입출력 과정으로 Overlapped 소켓 입출력 모델에도 그대로 적용된다. Overlapped 모델(2)를 이용한 소켓 입출력 절차는 다음과 같다

    • 비동기 입출력을 지원하는 소켓을 생성한다
    • 비동기 입출력 함수를 호출한다. 이때 완료 루틴의 시작 주소를 함수 인자로 전달한다. 비동기 입출력 작업이 곧바로 완료되지 않으면, 소켓 함수는 오류를 리턴하고 오류 코드는 WSA_IO_PENDING으로 설정된다.
    • 비동기 입출력 함수를 호출한 스레드를 alertable wait 상태로 만든다. WaitForSignleObjectEx(), WiatForMultipleObjectsEx(), SleepEx(), WSAWaitForMultipleEvents() 등의 함수 중에서 적절한 것을 선택하여 사용하면 된다.
    • 비동기 입출력 작업이 완료되면, 운영체제는 완료 루틴을 호출한다. 완료 루틴에서는 비동기 입출력 결과를 확인하고 데이터를 처리한다.
    • 완료 루틴 호출이 모두 끝나면, 스레드는 alertable wait 상태에서 빠져나온다
    • 새로운 소켓을 생성하면 1 ~ 5, 그렇지 않으면 2 ~ 5 를 반복한다

    완료 루틴의 형태는 다음과 같다

void CALLBACK CompletionRoutine(

DWORD dwError,

DWORD cbTransferred,

LPWSAOVERLAPPED lpOverlapped,

DWORD dwFlags

);

  • dwError

    비동기 입출력 결과를 나타낸다. 오류가 발생하면 이 값은 0이 아닌 값이 된다.

  • cbTransferred

    전송 바이트 수를 나타낸다. 통신 상대가 접속을 종료하면 이 값은 0이 된다.

  • lpOverlapped

    비동기 입출력 함수 호출 시 넘겨준 WSAOVERLAPPED 구조체의 주소값이 이 인자를 통해 다시 애플리케이션에 넘어온다. Overlapped 모델(2)에서는 이벤트 객체를 사용하지 않으므로, WSAOVERLAPPED 구조체를 완료 루틴 내부에서 직접 사용할 일은 거의 없다.

  • dwFlags

    항상 0이므로 적어도 현재까지는 사용하지 않는다.

[실습] Overlapped 모델(2)을 이용한 TCP 서버, 코드 분석

서버 : 클라이언트가 보낸 데이터를 받아(WSARecv), 이를 문자열로 간주하여 무조건 화면에 출력한다. 그리고 받은 데이터를 변경 없이 다시 클라이언트에 보낸다(WSASend)

클라이언트 : 사용자가 키보드로 입력한 문자열을 서버로 보낸다(WSASend) 서버가 받은 데이터를 그대로 되돌려 보내면 클라이언트는 이를 받아(WSARecv) 화면에 출력한다

헤더파일, 전역변수, 함수 원형 선언

#include <winsock2.h>

#include <stdlib.h>

#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")

   

#define BUFSIZE 512

   

// 소켓 정보 저장을 위한 구조체

struct SOCKETINFO

{

WSAOVERLAPPED overlapped;

SOCKET sock;

char buf[BUFSIZE+1];

int recvbytes;

int sendbytes;

WSABUF wsabuf;

};

   

SOCKET client_sock;

   

// 소켓 입출력 함수

DWORD WINAPI WorkerThread(LPVOID arg);

void CALLBACK CompletionRoutine(

DWORD dwError, DWORD cbTransferred,

LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags);

   

// 오류 출력 함수

void err_quit(char* msg);

void err_display(char* msg);

void err_display(int errcode);

8 - 16행 : 소켓 정보 저장을 위한 구조체다. 각 소켓마다 WSAOVERLAPPED 구조체, 애플리케이션 버퍼, 송수신 바이트 수, WSABUF구조체 정보를 유지한다.

18행 : accept() 함수의 리턴값을 저장한 변수, 두 스레드에서 접근하므로 전역 변수로 선언하였다.

메인 함수

int main(int argc, char* argv[])

{

int retval;

   

// 윈속 초기화

WSADATA wsa;

if( WSAStartup(MAKEWORD(2,2), &wsa) != 0 )

return -1;

   

// socket()

SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);

if(listen_sock == INVALID_SOCKET)

err_quit("socket()");

   

//bind()

SOCKADDR_IN serveraddr;

ZeroMemory(&serveraddr, sizeof(serveraddr));

serveraddr.sin_family = AF_INET;

serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);

serveraddr.sin_port = htons(9000);

retval = bind(listen_sock, (SOCKADDR*)&serveraddr, sizeof(serveraddr));

if(retval == SOCKET_ERROR)

err_quit("bind()");

   

// listen()

retval = listen(listen_sock, SOMAXCONN);

if( retval == SOCKET_ERROR )

err_quit("listen()");

   

// 이벤트 객체 생성

HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);

if( hEvent == NULL )

return -1;

   

// 스레드 생성

DWORD ThreadId;

HANDLE hThread = CreateThread(NULL, 0, WorkerThread, (LPVOID)hEvent, 0, &ThreadId);

if( hThread == NULL ) return -1;

CloseHandle(hThread);

   

while(1)

{

// accept()

client_sock = accept(listen_sock, NULL, NULL);

if(client_sock == INVALID_SOCKET)

{

err_display("accept()");

continue;

}

if(!SetEvent(hEvent))

break;

}

   

// 윈속 종료

WSACleanup();

return 0;

}

58 - 59행 : 이벤트 객체를 생성한다. 이 이벤트 객체는 alertable wait 상태인 스레드를 깨우는 용도로 사용된다.(75, 93행)

62 - 66행 : 스레드 생성할 때 이벤트 객체 핸들값을 인자로 전달한다. 이 스레드는 alertable wait 상태가 됨으로써, 비동기 입출력이 완료하면 완료 루틴이 호출되도록 한다.

70 - 75행 : 클라이언트가 접속할 때마다 이벤트 객체를 신호 상태로 만들어(75행) alertable wait상태인 스레드를 깨운다.(93행)

스레드 함수

// 스레드 함수

DWORD WINAPI WorkerThread(LPVOID arg)

{

HANDLE hEvent = (HANDLE)arg;

int retval;

   

while(1)

{

while(1)

{

// alertable wait

DWORD result = WaitForSingleObjectEx(hEvent, INFINITE, TRUE);

if(result == WAIT_OBJECT_0)

break;

   

if(result != WAIT_IO_COMPLETION)

return -1;

}

   

// 접속한 클라이언트 정보 출력

SOCKADDR_IN clientaddr;

int addrlen = sizeof(clientaddr);

getpeername(client_sock, (SOCKADDR*)&clientaddr, &addrlen);

printf("[TCP서버] 클라이언트 접속: IP주소=%s, 포트번호=%d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

   

// 소켓 정보 구조체 할당과 초기화

SOCKETINFO* ptr = new SOCKETINFO;

if(ptr == NULL)

{

printf("[오류] 메모리가 부족합니다.!\n");

return -1;

}

   

ZeroMemory(&(ptr->overlapped), sizeof(ptr->overlapped));

ptr->sock = client_sock;

ptr->recvbytes = 0;

ptr->sendbytes = 0;

ptr->wsabuf.buf = ptr->buf;

ptr->wsabuf.len = BUFSIZE;

   

// 비동기 입출력 시작

DWORD recvbytes;

DWORD flags = 0;

   

retval = WSARecv(

ptr->sock,

&(ptr->wsabuf),

1,

&recvbytes,

&flags,

&(ptr->overlapped),

CompletionRoutine

);

   

if(retval == SOCKET_ERROR)

{

if(WSAGetLastError() != WSA_IO_PENDING)

{

err_display("WSARecv()");

return -1;

}

}

}

return 0;

}

86행 : 스레드 함수 인자로 전달된 이벤트 객체 핸들값을 저장해둔다.

90 - 95행 : WaitForSingleObjectEx() 함수를 호출하여 alertable wait상태에 진입한다. WaitForSingleObjectEx()함수가 리턴하여 alertable wait 상태에서 벗어나면 리턴값을 확인한다. 리턴값이 WAIT_OBJECT_0이면 새로운 클라이언트가 접속한 경우므로 루프를 벗어난다. 리턴값이 WAIT_IO_COMPLETION이면 비동기 입출력 작업과 이에 따른 완료 루틴 호출이 끝난 경우므로 다시 alertable wait 상태에 진입한다.

98 - 102행 : 접속한 클라이언트 정보를 화면에 출력한다.

105 - 115행 : 소켓 정보 구조체를 할당하고 초기화한다.

118 - 127행 : WSARecv() 함수를 호출하여 비동기 입출력을 시작한다.

완료 루틴

void CALLBACK CompletionRoutine(

DWORD dwError, DWORD cbTransferred,

LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags)

{

int retval;

   

// 클라이언트 정보 얻기

SOCKETINFO* ptr = (SOCKETINFO*)lpOverlapped;

SOCKADDR_IN clientaddr;

int addrlen = sizeof(clientaddr);

getpeername(ptr->sock, (SOCKADDR*)&clientaddr, &addrlen);

   

// 비동기 입출력 결과 확인

if(dwError != 0 || cbTransferred == 0)

{

if(dwError != 0)

err_display(dwError);

closesocket(ptr->sock);

printf("[TCP서버]클라이언트 종료: IP주소=%s, 포트번호=%d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

delete ptr;

return;

}

   

// 데이터 전송량 갱신

if(ptr->recvbytes == 0)

{

ptr->recvbytes = cbTransferred;

ptr->sendbytes = 0;

// 받은 데이터 출력

ptr->buf[ptr->recvbytes] = '\0';

printf("[TCP%s:%d]%s\n",inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port), ptr->buf);

}

else

{

ptr->sendbytes += cbTransferred;

}

   

if(ptr->recvbytes > ptr->sendbytes)

{

// 데이터 보내기

ZeroMemory(&(ptr->overlapped), sizeof(ptr->overlapped));

ptr->wsabuf.buf = ptr->buf + ptr->sendbytes;

ptr->wsabuf.len = ptr->recvbytes - ptr->sendbytes;

   

DWORD sendbytes;

retval = WSASend(ptr->sock, &(ptr->wsabuf), 1, &sendbytes, 0, &(ptr->overlapped), CompletionRoutine);

if(retval == SOCKET_ERROR)

{

if(WSAGetLastError() != WSA_IO_PENDING)

{

err_display("WSASend()");

return;

}

}

}

else

{

ptr->recvbytes = 0;

   

// 데이터 받기

ZeroMemory(&(ptr->overlapped), sizeof(ptr->overlapped));

ptr->wsabuf.buf = ptr->buf;

ptr->wsabuf.len = BUFSIZE;

   

DWORD recvbytes;

DWORD flags = 0;

retval = WSARecv(ptr->sock, &(ptr->wsabuf), 1, &recvbytes, &flags, &(ptr->overlapped), CompletionRoutine);

if( retval == SOCKET_ERROR )

{

if(WSAGetLastError() != WSA_IO_PENDING)

{

err_display("WSARecv()");

return;

}

}

}

}

141 - 144행 : 화면 출력을 위해 클라이언트 정보를 얻어 저장해둔다.

147 - 154행 : 비동기 입출력 결과를 확인한다. 오류가 발생했거나 클라이언트가 정상 종료한 경우라면, 소켓 정보를 제거한다.(152행)

157 - 167행 : 데이터 전송량을 갱신한다. 소켓 정보 구조체를 참조하면, 받은 데이터인지 혹은 보낸 데이터인지 알 수 있다.

169 - 184행 : 보낸 데이터가 받은 데이터보다 적으면, 아직 보내지 못한 데이터를 보낸다. WSASend() 함수는 비동기적으로 동작하므로 실제로 보낸 데이터 수는 다음 번에 완료 루틴이 호출되면 확인할 수 있다.(166행)

185 - 204행 : 소켓 정보 중 받은 데이터 수를 초기화한 후(186행), 데이터를 받는다. WSARecv() 함수는 비동기적으로 동작하므로 실제로 받은 데이터 수는 다음 번에 완료 루틴이 호출되면 확인 할 수 있다.(158행)

  • Completion Port 모델

    동작 원리

    Completion Port 모델의 핵심은 입출력 완료 포트(I/O completion port)라 부르는 윈도우 운영체제가 제공하는 구조를 이해하고 활용하는 것이다. 입출력 완료 포트는 비동기 입출력 결과와 이 결과를 처리할 스레드에 대한 정보를 담고 있는 구조로 Overlapped 모델(2)에서 소개한 APC 큐와 비슷한 개념이다. APC 큐와의 차이점을 항목별로 정리하면 다음과 같다.

    • 생성과 파괴

      APC큐는 스레드마다 자동으로 생성되고 파괴된다. 입출력 완료포트는 CreateIoCompletionPort()함수를 호출하여 생성하고, CloseHandle() 함수를 호출하여 파괴한다.

    • 접근 제약

      APC 큐에 저장된 결과는 APC 큐를 소유한 스레드만 확인할 수 있지만, 입출력 완료 포트에는 이러한 제약이 없다. 대개 입출력 완료 포트를 접근하는 스레드를 별도로 두는데, 이를 작업자 스레드(worker thread)라 부른다. 이상적인 작업자 스레드 개수는 CPU 개수와 같게 하는 것이지만, 몇 가지 이유로 인해 CPU개수*n개를 생성한다. n의 값을 정하는 특별한 규칙은 없으며, 애플리케이션의 특성에 따라 결정해야 한다.

    • 비동기 입출력 처리 방법

      APC 큐에 저장된 결과를 처리하려면 해당 스레드는 alertable wait 상태에 진입해야 한다. 입출력 완료 포트에 저장된 결과를 처리하려면 작업자 스레드는 GetQueuedCompletionStatus()함수를 호출해야 한다.

    Completion Port 모델을 사용하기 위해서는 비동기 입출력 시작부터 완료까지의 과정을 이해해야 한다. 다음은 Completion Port 모델을 이용한 입출력 과정을 보여준다.

    각 단계별 동작을 요약하면 다음과 같다. 단, 입출력 완료 포트와 작업자 스레드는 미리 생성해둔 것으로 간주한다.

    • 애플리케이션을 구성하는 임의의 스레드에서 비동기 입출력 함수를 호출함으로써 운영체제에 입출력 작업을 요청한다.
    • 모든 작업자 스레드는 GetQueuedCompletionStatus() 함수를 호출하여 입출력 완료 포트를 감시한다. 완료된 비동기 입출력 작업이 아직 없다면 모든 작업자 스레드는 대기 상태가 된다. 이때 대기 중인 작업자 스레드 목록은 입출력 완료 포트 내부에 저장된다.
    • 비동기 입출력 작업이 완료되면 운영체제는 입출력 완료 포트에 결과를 저장한다. 이때 저장된 정보를 입출력 완료 패킷(I/O completion packet)이라 부른다.
    • 운영체제는 입출력 완료 포트에 저장된 작업자 스레드 목록에서 하나를 선택하여 깨운다. 대기 상태에서 깨어난 작업자 스레드는 비동기 입출력 결과를 처리한다. 이후 작업자 스레드는 필요에 따라 다시 비동기 입출력 함수를 호출할 수 있다.

    Completion Port 모델을 이용한 소켓 입출력 절차를 살펴보면 다음과 같다

    • CreateIoCompletionPort() 함수를 호출하여 입출력 완료 포트를 생성한다.
    • CPU 개수에 비례하여 작업자 스레드를 생성한다. 모든 작업자 스레드는 GetQueuedCompletionStatus() 함수를 호출하여 대기 상태가 된다.
    • 비동기 입출력으르 지원하는 소켓을 생성한다. 이 소켓에 대한 비동기 입출력 결과가 입출력 완료 포트에 저장되려면 CreateIoCompletionPort() 함수를 호출하여 소켓과 입출력 완료 포트를 연결해야 한다.
    • 비동기 입출력 함수를 호출한다. 비동기 입출력 작업이 곧바로 완료되지 않으면, 소켓 함수는 오류를 리턴하고 오류 코드는 WSA_IO_PENDING으로 설정된다.
    • 비동기 입출력 작업이 완료되면 운영체제는 입출력 완료 포트에 결과를 저장하고 대기 중인 스레드 하나를 깨운다. 대기 상태에서 깨어난 작업자 스레드는 비동기 입출력 결과를 처리한다.
    • 새로운 소켓을 생성하면 3 ~ 5를, 그렇지 않으면 4 ~ 5를 반복한다.

    입출력 완료 포트 생성

    CreateIoCompletionPort() 함수는 두가지 역할을 한다. 하나는 입출력 완료 포트를 새로 생성하는 것이고, 하나는 소켓과 입출력 완료 포트를 연결하는 것이다. 소켓과 입출력 완료 포트를 연결하면 이 소켓에 대한 비동기 입출력 결과가 입출력 완료 포트에 저장된다.

HANDLE CreateIoCompletionPort(

HANDLE FileHandle,

HANDLE ExistingCompletionPort,

ULONG_PTR CompletionKey,

DWORD NumberOfConcurrentThreads

);

// 성공 : 입출력 완료 포트 핸들

// 실패 : NULL

  • HANDLE FileHandle

    입출력 완료 포트와 연결할 파일 핸들이다. 소켓 프로그래밍에서는 소켓 디스크립터를 넣어 주면 된다. 새로운 입출력 완료 포트를 생성할 때는 INVALID_HANDLE_VALUE 값을 사용해도 된다.

  • HANDLE ExistingCompletionPort

    파일 또는 소켓과 연결할 입출력 완료 포트 핸들이다. 이 값이 NULL이면 새로운 입출력 완료 포트를 생성한다.

  • ULONG_PTR CompletionKey

    입출력 완료 패킷(I/O completion packet)에 들어갈 부가적인 정보로 32비트 값을 줄 수 있다. 입출력 완료 패킷은 비동기 입출력 작업이 완료할 때마다 생성되어 입출력 완료 포트에 저장되는 정보다.

  • DWORD NumberOfConcurrentThreads

    동시에 실행할 수 있는 작업자 스레드의 개수다. 0을 사용하면 자동으로 CPU 개수와 같은 수로 설정된다. 운영체제는 실행중인 작업자 스레드 개수가 여기서 설정한 값을 넘지 않도록 해준다.

입출력 완료 포트를 생성하는 예는 아래와 같다.

HANDLE hcp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
if( hcp == NULL ) return -1;

기존의 소켓과 입출력 완료 포트를 연결하는 코드는 다음과 같다

SOCKET sock;
...
HANDLE hResult = CreateIoCompletionPort((HANDLE)sock, hcp, (DWORD)sock, 0);

비동기 입출력 결과 확인

작업자 스레드는 GetQueuedCompletionStatus() 함수를 호출함으로써 입출력 완료 포트에 입출력 완료 패킷이 들어올 때까지 대기한다. 입출력 완료 패킷이 입출력 완료 포트에 들어오면 운영체제는 실행중인 작업자 스레드의 개수를 체크한다. 이 값이 CreateIoCompletionPort() 함수의 네 번째 인자로 설정한 값보다 작다면, 대기 상태인 작업자 스레드를 깨워서 입출력 완료 패킷을 처리하도록 한다.

BOOL GetQueuedCompletionStatus(

HANDLE CompletionPort,

LPDWORD lpNumberOfBytesTransferred,

PULONG_PTR lpCompletionKey,

LPOVERLAPPED *lpOverlapped,

DWORD dwMilliseconds

);

// 성공 : 0이 아닌 값

// 실패 : 0

  • HANDLE CompletionPort

    입출력 완료 포트의 핸들이다.

  • LPDWORD lpNumberOfBytesTransferred

    DWORD 타입 변수 주소값을 넣으면 비동기 입출력 작업으로 전송된 바이트 수가 여기에 저장된다.

  • PULONG_PTR lpCompletionKey

    DWORD 타입 변수 주소값을 넣으면 CreateIoCompletionPort() 함수 호출 시 전달한 세 번째 인자(32비트)가 여기에 저장된다.

  • LPOVERLAPPED *lpOverlapped

    OVERLAPPED 타입 포인터의 주소값을 넣으면 비동기 입출력 함수 호출 시 전달한 OVERLAPPED 구조체의 주소값이 여기에 저장된다.

  • DWORD dwMilliseconds

    작업자 스레드가 대기할 시간을 밀리초(milliseconds) 단위로 지정한다. INFINITE 값을 사용하면 입출력 완료 패킷이 생성되어 운영체제가 자신을 깨울 때까지 무한히 대기한다.

애플리케이션이 작업자 스레드에 특별한 사실을 알리기 위해 직접 입출력 완료 패킷을 생성할 수도 있다. 이때 사용하는 함수는 PostQueuedCompletionStatus()다. 각 인자의 의미는 GetQueuedCompletionStatus() 함수와 비슷하다.

BOOL PostQueuedCompletionStatus(

HANDLE CompletionPort,

DWORD dwNumberOfBytesTransferred,

ULONG_PTR dwCompletionKey,

LPOVERLAPPED lpOverlapped

);

[실습] Completion Port 모델을 이용한 TCP 서버

서버 : 클라이언트가 보낸 데이터를 받아, 이를 문자열로 간주하여 무조건 화면에 출력한다. 그리고 받은 데이터를 변경 없이 다시 클라이언트에 보낸다.

클라이언트 : 사용자가 키보드로 입력한 문자열을 서버에 보낸다. 서버가 받은 데이터를 그대로 되돌려 보내면 클라이언트는 이를 받아 화면에 출력한다.

코드 분석

헤더 파일, 전역 변수, 함수 원형 선언

#include <winsock2.h>

#include <stdlib.h>

#include <stdio.h>

   

#pragma comment(lib, "ws2_32.lib")

   

#define BUFSIZE 512

   

// 소켓 정보 저장을 위한 구조체

struct SOCKETINFO

{

OVERLAPPED overlapped;

SOCKET sock;

char buf[BUFSIZE-1];

int recvbytes;

int sendbytes;

WSABUF wsabuf;

};

   

// 소켓 입출력 함수

DWORD WINAPI WorkerThread(LPVOID arg);

// 오류 출력 함수

void err_quit(char* msg);

void err_display(char* msg);

메인 함수

int _tmain(int argc, _TCHAR* argv[])

{

int retval;

   

// 윈속 초기화

WSADATA wsa;

if( WSAStartup(MAKEWORD(2,2), &wsa) != 0 )

{

return -1;

}

   

// 입출력 완료 포트 생성

HANDLE hcp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

if( hcp == NULL )

{

return -1;

}

   

// CPU 개수 확인

SYSTEM_INFO si;

GetSystemInfo(&si);

   

// (CPU개수 * 2)개의 작업자 스레드 생성

HANDLE hThread;

DWORD threadId;

for( int i = 0 ; i < (int)si.dwNumberOfProcessors * 2 ; i++ )

{

hThread = CreateThread(NULL, 0, WorkerThread, hcp, 0, &threadId);

if( hThread == NULL )

{

return -1;

}

CloseHandle(hThread);

}

   

// socket()

SOCKET listenSock = socket(AF_INET, SOCK_STREAM, 0);

if( listenSock == INVALID_SOCKET )

{

err_quit("socket()");

}

   

// bind()

SOCKADDR_IN serverAddr;

memset(&serverAddr, 0, sizeof(serverAddr));

serverAddr.sin_family = AF_INET;

serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);

serverAddr.sin_port = htons(9000);

   

retval = bind(listenSock, (SOCKADDR*)&serverAddr, sizeof(serverAddr));

if( retval == SOCKET_ERROR )

err_quit("bind()");

   

// listen()

retval = listen(listenSock, SOMAXCONN);

if( retval == SOCKET_ERROR )

{

err_quit("listen()");

}

   

//

while(1)

{

// accept()

SOCKADDR_IN clientAddr;

int addrLen = sizeof(clientAddr);

SOCKET clientSock = accept(listenSock, (SOCKADDR*)&clientAddr, &addrLen);

if( clientSock == INVALID_SOCKET )

{

err_display("accept()");

continue;

}

printf("[TCP서버]클라이언트 접속:IP주소=%s, 포트번호=%d\n",

inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

   

// 소켓과 입출력 완료 포트 연결

HANDLE hResult = CreateIoCompletionPort((HANDLE)clientSock, hcp, (DWORD)clientSock, 0);

if( hResult == NULL )

return -1;

   

// 소켓 정보 구조체 할당

SOCKETINFO *ptr = new SOCKETINFO;

if( ptr == NULL )

{

printf("[err]memory not enough");

break;

}

memset(&ptr->overlapped, 0, sizeof(ptr->overlapped));

ptr->sock = clientSock;

ptr->recvbytes = 0;

ptr->sendbytes = 0;

ptr->wsabuf.buf = ptr->buf;

ptr->wsabuf.len = BUFSIZE;

   

// 비동기 입출력 시작

DWORD recvbytes;

DWORD flags = 0;

retval = WSARecv(

clientSock,

&(ptr->wsabuf),

1,

&recvbytes,

&flags,

&(ptr->overlapped),

NULL);

if( retval == SOCKET_ERROR )

{

if(WSAGetLastError() != ERROR_IO_PENDING )

{

err_display("WSARecv()");

}

continue;

}

}

   

WSACleanup();

return 0;

}

34 ~ 36행 : 입출력 완료 포트를 생성한다.

39 ~ 40행 : CPU개수를 얻는다. CPU개수에 비례하여 작업자 스레드를 생성하기 위해서다

43 ~ 49행 : (CPU개수*2)개의 작업자 스레드를 생성한다. 이때 스레드 함수 인자로 입출력 완료 포트 핸들값을 전달한다.

81 ~ 83행 : accept() 함수가 리턴한 소켓과 입출력 완료 포트를 연결한다.

86 ~ 96행 : 소켓 정보 구조체를 할당하여 초기화한다.

99 ~ 108행 : WSARecv() 함수를 호출하여 비동기 입출력을 시작한다.

스레드 함수

// 소켓 입출력 함수

DWORD WINAPI WorkerThread(LPVOID arg)

{

HANDLE hcp = (HANDLE)arg;

int retval;

   

while(1)

{

// 비동기 입출력 완료 기다리기

DWORD cbTransferred;

SOCKET clientSock;

SOCKETINFO* ptr;

retval = GetQueuedCompletionStatus(

hcp,

&cbTransferred,

(LPDWORD)&clientSock,

(LPOVERLAPPED*)&ptr,

INFINITE);

   

// 클라이언트 정보 얻기

SOCKADDR_IN clientAddr;

int addrLen = sizeof(clientAddr);

getpeername(ptr->sock, (SOCKADDR*)&clientAddr, &addrLen);

   

// 비동기 입출력 결과 확인

if( retval == 0 || cbTransferred == 0 )

{

if( retval == 0 )

{

DWORD temp1, temp2;

WSAGetOverlappedResult(ptr->sock, &(ptr->overlapped), &temp1, FALSE, &temp2);

err_display("WSAGetOverlappedResult()");

}

closesocket(ptr->sock);

printf("[TCP서버]클라이언트종료:IP주소=%s, 포트번호=%d\n",

inet_ntoa(clientAddr.sin_addr),

ntohs(clientAddr.sin_port));

delete ptr;

continue;

}

   

// 데이터 전송량 갱신

if( ptr->recvbytes == 0 )

{

ptr->recvbytes = cbTransferred;

ptr->sendbytes = 0;

// 받은 데이터 출력

ptr->buf[ptr->recvbytes] = '\0';

printf("[TCP%s:%d] %s\n",

inet_ntoa(clientAddr.sin_addr),

ntohs(clientAddr.sin_port),

ptr->buf);

}

else

{

ptr->sendbytes += cbTransferred;

}

   

if( ptr->recvbytes > ptr->sendbytes )

{

// 데이터 보내기

memset(&(ptr->overlapped), 0, sizeof(ptr->overlapped));

ptr->wsabuf.buf = ptr->buf + ptr->sendbytes;

ptr->wsabuf.len = ptr->recvbytes - ptr->sendbytes;

   

DWORD sendbytes;

retval = WSASend(

ptr->sock,

&(ptr->wsabuf),

1,

&sendbytes,

0,

&(ptr->overlapped),

NULL);

if( retval == SOCKET_ERROR )

{

if( WSAGetLastError() != WSA_IO_PENDING )

{

err_display("WSASend()");

}

continue;

}

}

else

{

ptr->recvbytes = 0;

   

// 데이터 받기

memset(&(ptr->overlapped), 0, sizeof(ptr->overlapped));

ptr->wsabuf.buf = ptr->buf;

ptr->wsabuf.len = BUFSIZE;

   

DWORD recvbytes;

DWORD flags = 0;

retval = WSARecv(

ptr->sock,

&(ptr->wsabuf),

1,

&recvbytes,

&flags,

&(ptr->overlapped),

NULL);

if( retval == SOCKET_ERROR )

{

if(WSAGetLastError() != WSA_IO_PENDING)

{

err_display("WSARecv()");

}

continue;

}

}

}

return 0;

}

118행 : 스레드 함수 인자로 전달된 입출력 완료 포트 핸들값을 저장해둔다.

123 ~ 127행 : GetQueuedCompletionStatus() 함수를 호출하여 비동기 입출력이 완료되기를 기다린다.

130 ~ 132행 : 화면 출력을 위해 클라이언트 정보를 얻어 저장해둔다.

135 ~ 147행 : 비동기 입출력 결과를 확인한다. GetQueuedCompletionStatus() 함수에서 오류가 발생했다면(136행), GetLastError() 함수로 오류 코드를 얻을 수 있다. 그러나 이 값은 일반적인 윈도우 API 오류 코드이므로 WSAGetOverlappedResult() 함수를 호출하여 올바른 소켓 오류 코드를 생성한 후 err_display() 함수로 오류 문자열을 출력한다.(137~140행) 오류 처리가 끝나면 소켓을 닫고(142행), 소켓 정보를 제거한다(145행)

150 ~ 160행 : 데이터 전송량을 갱신한다. 소켓 정보 구조체를 참조하면 받은 데이터인지 보낸 데이터인지 알 수 있다.

162 ~ 177행 : 보낸 데이터가 받은 데이터보다 적으면, 아직 보내지 못한 데이터를 보낸다. WSASend() 함수는 비동기적으로 동작하므로 실제로 보낸 데이터 수는 다음 번에 루프를 돌 때 확인할 수 있다.(159행)

178 ~ 196행 : 소켓 정보 중 받은 데이터 수를 초기화한 후(179행), 데이터를 받는다. WSARecv() 함수는 비동기적으로 동작하므로 실제로 받은 데이터 수는 다음 번에 루프를 돌 때 확인할 수 있다.(151행)

오류 처리 함수

// 오류 출력 함수

void err_quit(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);

LocalFree(lpMsgBuf);

exit(-1);

}

void err_display(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

printf("[%s] %s", msg, (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

  • 소켓 입출력 모델 요약

    각 모델의 장점을 정리하면 다음과 같다

모델

장점

단점

Select 모델

모든 윈도우 버전은 물론, 유닉스에서도 사용할 수 있으므로 이식성이 높다.

하위 호환성을 위해 존재하며, 성능은 여섯 가지 모델 중 가정 떨어진다. 64개 이상의 소켓을 처리하려면 여러 개의 스레드를 사용해야 한다.

WSAAsyncSelect 모델

소켓 이벤트를 윈도우 메시지 형태로 처리하므로 GUI 애플리케이션과 잘 결합할 수 있다.

하나의 윈도우 프로시저에서 일반 윈도우 메시지와 소켓 메시지를 처리해야 하므로 성능 저하 요인이 된다.

WSAEventSelect 모델

Select모델과 WSAAsyncSelect 모델의 특성을 혼합한 형태로, 비교적 뛰어난 성능을 제공하면서 윈도우를 필요로 하지 않는다.

64개 이상의 소켓을 처리하려면 여러 개의 스레드를 사용해야 한다.

Overlapped 모델(1)

비동기 입출력을 통해 뛰어난 성능을 제공한다.

64개 이상의 소켓을 처리하려면 여러 개의 스레드를 사용해야 한다.

Overlapped 모델(2)

비동기 입출력을 통해 뛰어난 성능을 제공한다.

모든 비동기 소켓 함수에 대해 완료 루틴을 사용할 수 있는 것은 아니다.

Completion Port 모델

비동기 입출력과 완료 포트를 통해 가장 뛰어난 성능을 제공한다.

가장 단순한 소켓 입출력 방식(블로킹 소켓+스레드)과 비교하면 코딩이 복잡하지만 성능면에서 특별한 단점은 없다. 윈도우 NT계열에서만 사용할 수 있다.

9장에서 소개한 이상적인 소켓 입출력 모델에 요구되는 사항을 각 소켓 입출력 모델과 비교해보자.

  • 소켓 함수 호출 시 블로킹을 최소화한다.

    여섯 가지 모델 모두 만족한다.

  • 입출력 작업을 다른 작업과 병행한다.

    비동기 입출력 방식을 사용하는 Overlapped모델(1), Overlapped모델(2), Completion Port 모델만 만족한다.

  • 스레드 개수를 최소화한다.

    여섯 가지 모델 모두 어느 정도 만족한다. 그러나 소켓의 개수가 늘어나면 WSAAAsyncSelect모델, Overlapped모델(2), Completion Port모델을 제외하고는 모두 스레드를 추가로 생성해야 한다. WSAAsyncSelect 모델은 스레드 개수가 늘어나면 성능이 상당히 떨어지므로 실질적으로는 Overlapped모델(2)와 Completion Port모델만이 이 조건을 만족한다. Completion Port 모델은 CPU 개수에 비례하여 작업자 스레드를 생성할 수 있으므로 가장 이상적이다.

  • 유저 모드와 커널 모드 전환 횟수와 데이터 복사를 최소화한다.

    비동기 입출력 방식을 사용하는 Overlapped모델(1), Overlapped모델(2), Completion Port모델이 이 조건을 만족한다. 비동기 입출력을 할 때 송신 버퍼나 수신 버퍼가 가득차면, 윈도우 운영체제는 애플리케이션 버퍼를 잠근 후(lock), 이 메모리 영역을 직접 접근한다. 따라서 유저 영역<->커널 영역 복사가 불필요하며 모드 전환없이 입출력 작업이 곧바로 이루어지므로 효율적이다.

OveerLapped모델(1)을 이용한 소켓 입출력 절차

  • 비동기 입출력을 지원하는 소켓을 생성한다. 이때 WSACreateEvent() 함수를 호출하여 대응하는 이벤트 객체도 같이 생성한다.
  • 비동기 입출력을 지원하는 소켓 함수를 호출한다. 이때 WSAOVERLAPPED 구조체의 hEvent 변수에 이벤트 객체 핸들값을 넣어서 전달한다. 비동기 입출력 작업이 곧바로 완료되지 않으면 소켓 함수는 오류를 리턴하고 오류 코드는 WSA_IO_PENDING으로 설정된다. 나중에 비동기 입출력 작업이 완료되면 운영체제는 이벤트 객체를 신호 상태로 만들어 이 사실을 애플리케이션에게 알린다.
  • WSAWaitForMultipleEvents() 함수를 호출하여 이벤트 객체가 신호 상태가 되기를 기다린다.
  • 비동기 입출력 작업이 완료하여 WSAWaitForMultipleEvents() 함수가 리턴하면 WSAGetOverlappedResult() 함수를 호출하여 비동기 입출력 결과를 확인하고 데이터를 처리한다.
  • 새로운 소켓을 생성하면1~4, 그렇지 않으면 2~4를 반복한다.

Overlapped모델(2)를 이용한 소켓 입출력 절차

  • 비동기 입출력을 지원하는 소켓을 생성한다.
  • 비동기 입출력 함수를 호출한다. 이때 완료 루틴의 시작 주소를 함수 인자로 전달한다. 비동기 입출력 작업이 곧바로 완료되지 않으면 소켓 함수는 오류를 리턴하고 오류 코드는 WSA_IO_PENDING으로 설정된다.
  • 비동기 입출력 함수를 호출한 스레드를 alertable wait 상태로 만든다. 앞에서 소개한 WaitForSingleObjectEx(), WaitForMultipleObjectsEx(), SleepEx(), WSAWaitForMultipleEvents() 등의 함수 중에서 적절한 것을 선택하여 사용하면 된다.
  • 비동기 입출력 작업이 완료되면 운영체제는 완료 루틴을 호출한다. 완료 루틴에서는 비동기 입출력 결과를 확인하고 데이터를 처리한다.
  • 완료 루틴 호출이 모두 끝나면 스레드는 alertablee wait 상태에서 빠져나온다.
  • 새로운 소켓을 생성하면 1~5, 그렇지 않으면 2~5를 반복한다.

Completion Port모델을 이용한 소켓 입출력 절차

  • CreateIoCompletionPort() 함수를 호출하여 입출력 완료 포트를 생성한다.
  • CPU 개수에 비례하여 작업자 스레드를 생성한다. 모든 작업자 스레드는 GetQueuedCompletionStatus() 함ㅁ수를 호출하여 대기 상태가 된다.
  • 비동기 입출력을 지원하는 소켓을 생성한다. 이 소켓에 대한 비동기 입출력 결과를 입출력 완료 포트에 저장하려면 CreateIoCompletionPort() 함수를 호출하여 소켓과 입출력 완료 포트를 연결해야 한다.
  • 비동기 입출력 함수를 호출한다. 비동기 입출력 작업이 곧바로 완료되지 않으면 소켓 함수는 오류를 리턴하고 오류코드는 WSA_IO_PENDING으로 설정된다.
  • 비동기 입출력 작업이 완료되면 운영체제는 입출력 완료 포트에 결과를 저장하고 대기중인 스레드 하나를 깨운다. 대기 상태에서 깨어난 작업자 스레드는 비동기 입출력 결과를 처리한다.
  • 새로운 소켓을 생성하면 3~5, 그렇지 않으면 4~5를 반복한다
반응형

'책정리 > 윈도우 네트워크 프로그래밍' 카테고리의 다른 글

9장 소켓 입출력 모델(I)  (0) 2015.06.23
7장 소켓 옵션  (0) 2015.04.23
6장 UDP 서버/클라이언트  (0) 2015.04.20
5장 멀티스레드  (0) 2015.04.20
4장 TCP 서버/클라이언트 구조  (0) 2015.04.13