책정리/열혈 TCP,IP

22장 Overlapped IO 모델

GONII 2015. 8. 6. 18:55
  • Overlapped IO 모델의 이해

    IO(입출력)의 중첩이란?

    위 그림에서 보이듯이, 하나의 쓰레드 내에서 동시에 둘 이상의 데이털르 전송(또는 수신)함으로 인해 입출력이 중첩되는 상황을 가리켜 'IO의 중첩'이라 한다. 그리고 이러한 일이 가능하려면 호출된 입출력 함수가 바로 반환을 해야 한다. 그래야 두 번째, 세 번째 데이터 전송을 시도할 수 있기 때문이다. 결과적으로 위의 모델로 데이터를 송수신하는데 있어서 핵심이 되는 사항은 '비동기 IO'이다. 그리고 비동기 IO가 가능하려면 호출되는 입출력 함수는 넌블로킹 모드로 동작해야 한다.

    이번 Chapter에서 말하는 Overlapped IO의 포커스는 IO에 있지 않습니다

    윈도우에서 말하는 Overlapped IO의 포커스는 IO가 아닌 IO가 완료된 상황의 확인방법에 있다. 입출력을 하건, 이들이 넌블로킹 모드로 진행된다면 이후에 완료 결과를 별도로 확인해야 한다.

    Overlapped IO 소켓의 생성

#include <winsock2.h>

SOCKET WSASocket(

IN int af,

IN int type,

IN int protocol,

IN LPWSAPROTOCOL_INFOW lpProtocolInfo,

IN GROUP g,

IN DWORD dwFlags

);

  • int af

    프로토콜 체계 정보 전달

  • int type

    소켓의 데이터 전송방식에 대한 정보 전달

  • int protocol

    두 소켓 사이에 사용되는 프로토콜 정보 전달

  • LPWSAPROTOCOL_INFOW lpProtocolInfo

    생성되는 소켓의 특성 정보를 담고 있는 WSAPROTOCOL_INFO 구조체 변수의 주소 값 전달, 필요 없는 경우 NULL 전달

  • GROUP g

    함수의 확장을 위해서 예약되어 있느느 매개변수, 0전달

  • DWORD dwFlags

    소켓의 속성정보 전달

네 번째 다섯번째 매개변수는 지금 하는 일과 관계가 없으니 각각 NULL과 0을 전달하고, 마지막 매개변수는 WSA_FLAG_OVERLAPPED를 전달해서 생성되는 소켓에 Overlapped IO가 가능한 속성을 부여하자. 이렇게 되면 Overlapped IO가 가능한 넌블로킹 모드의 소켓이 생성된다.

WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

Overlapped IO를 진행하는 WSASend 함수

Overlapped IO 속성이 부여된 소켓의 생성 이후 진행되는 두 소켓간의(서버, 클라이언트간의) 연결 과정은 일반 소켓의 연결과정과 차이가 없다. 그러나 데이터의 입출력에 사용되는 함수는 달라야 한다.

#include <winsock2.h>

 

int WSASend(

IN SOCKET s,

__in_ecount(dwBufferCount) LPWSABUF lpBuffers,

IN DWORD dwBufferCount,

__out_opt LPDWORD lpNumberOfBytesSent,

IN DWORD dwFlags,

__in_opt LPWSAOVERLAPPED lpOverlapped,

__in_opt LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine

);

  • SOCKET s

    소켓의 핸들 전달, Overlapped IO 속성이 부여된 소켓의 핸들 전달 시 Overlapped IO 모델로 출력 진행

  • LPWSABUF lpBuffers

    전송할 데이터 정보를 지니는 WSABUF 구조체 변수들로 이뤄진 배열의 주소 값 전달

  • DWORD dwBufferCount

    두 번재 인자로 전달된 배열의 길이정보 전달

  • LPDWORD lpNumberOfBytesSent

    전송된 바이트 수가 저장될 변수의 주소 값 전달

  • DWORD dwFlags

    함수의 데이터 전송특성을 변경하는 경우에 사용(ex:MSG_OOB)

  • LPWSAOVERLAPPED lpOverlapped

    WSAOVERLAPPED 구조체 변수의 주소 값 전달, Event 오브젝트를 사용해서 데이터 전송의 완료를 확인하는 경우에 사용되는 매개변수

  • LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine

    Completion Routine 이라는 함수의 주소 값 전달, 이를 통해서도 데이터 전송의 완료를 확인할 수 있다.

typedef struct _WSABUF

{

ULONG len;                // 전송할 데이터의 크기

__field_bcount(len) CHAR FAR *buf; // 버퍼의 주소값

} WSABUF, FAR * LPWSABUF;

위 함수를 이용해서 데이터를 전송할 때는 다음의 형태로 코드를 구성해야 한다.

WSAEVENT event;
WSAOVERLAPPED overlapped;

WSABUF dataBuf;

char buf[BUF_SIZE] = {"전송할 데이터"};

int recvBytes = 0;

...

event = WSACreateEvvent();

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

overlapped.hEvent = event;

dataBuf.len = sizeof(buf);

dataBuf.buf = buf;

WSASend(hSocket, &dataBuf, 1, &recvBytes, 0, &overlapped, NULL);

...

여섯 번째 인자로 전달된 WSAOVERLAPPED 구조체는 다음과 같다

typedef struct _OVERLAPPED {

ULONG_PTR Internal;

ULONG_PTR InternalHigh;

union {

struct {

DWORD Offset;

DWORD OffsetHigh;

};

PVOID Pointer;

};

HANDLE hEvent;

} OVERLAPPED, *LPOVERLAPPED;

이 중에서 멤버 Internal, InternalHigh는 Overlapped IO가 진행되는 과정에서 운영체제 내부적으로 사용되는 멤버이고, Offset, OffsetHigh 역시 사용이 예약되어 있는 멤버이다. 실제로 관심을 두어야 할 멤버 변수는 hEvent가 전부이다.

만약 lpOverlapped에 NULL이 전달되면, WSASend 함수의 첫 번재 인자로 전달된 핸들의 소켓은 블로킹 모드로 동작하는 일반적은 소켓으로 간주된다.

WSASend 함수호출을 통해 동시에 둘 이상의 영역으로 데이터를 전송하는 경우에는 여섯 번째 인자로 전달되는 WSAOVERLAPPED 구조체 변수를 각각 별도로 구성해야 한다.

WSASend 함수와 관련해서 한가지 더!

WSASend 함수라고 해서 무조건 함수의 반환과 데이터의 전송완료 시간이 불일치 하는 것은 아니다. 출력 버퍼가 비어있고, 전송하는 데이터의 크기가 많다면 함수호출과 동시에 데이터의 전송이 완료될 수도 있다. 그리고 이러한 경우에는 WSASend함수가 0을 반환하고 매개변수 lpNumberOfBytesSent로 전달된 주소의 변수에는 실제 전송된 데이터의 크기가 저장된다.

반면 호출된 WSASend 함수가 반환을 한 다음에도 계속해서 데이터의 전송이 이뤄지는 상황이라면 WSASend 함수는 SOCKET_ERROR를 반환하고, WSAGetLastError 함수호출을 통해 확인 가능한 오류코드로는 WSA_IO_PENDING이 등록된다.

다음 함수호출을 통해 실제 전송된 데이터의 크기를 확인해야 한다.

#include <winsock2.h>

BOOL WSAGetOverlappedResult(

IN SOCKET s,

IN LPWSAOVERLAPPED lpOverlapped,

OUT LPDWORD lpcbTransfer,

IN BOOL fWait,

OUT LPDWORD lpdwFlags

);

  • SOCKET s

    Overlapped IO가 진행된 소켓의 핸들

  • LPWSAOVERLAPPED lpOverlapped

    Overlapped IO 진행 시 전달한 WSAOVERLAPPED 구조체 변수의 주소 값 전달

  • LPDWORD lpcbTransfer

    실제 송수신된 바이트 크기를 저장할 변수의 주소 값 전달

  • BOOL fWait

    여전히 IO가 진행중인 상황의 경우, TRUE전달 시 IO가 완료될 때까지 대기하게 되고, FALSE전달 시 FALSE를 반환하면서 함수를 빠져나온다.

  • LPDWORD lpdwFlags

    WSARecv함수가 호출된 경우, 부수적인 정보(수신된 메시지가 OOB메시지 인자와 같은)를 얻기 위해 사용된다. 불필요하면 NULL

Overlapped IO를 진행하는 WSARecv 함수

#include <winsock2.h>

int WSARecv(

IN SOCKET s,

__in_ecount(dwBufferCount) __out_data_source(NETWORK) LPWSABUF lpBuffers,

IN DWORD dwBufferCount,

__out_opt LPDWORD lpNumberOfBytesRecvd,

IN OUT LPDWORD lpFlags,

__in_opt LPWSAOVERLAPPED lpOverlapped,

__in_opt LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine

);
// 성공 : 0
// 실패 : SOCKET_ERROR

  • SOCKET s

    Overlapped IO속성이 부여된 소켓의 핸들

  • LPWSABUF lpBuffers

    수신된 데이터 정보가 저장될 버퍼의 정보를 지니는 WSABUF 구조체 배열의 주소 값

  • DWORD dwBufferCount

    두 번째 인자로 전달된 배열의 길이 정보

  • LPDWORD lpNumberOfBytesRecvd

    수신된 데이터의 크기정보가 저장될 변수의 주소값 전달

  • LPDWORD lpFlags

    전송특성과 관련된 정보를 지정하거나 수신하는 경우에 사용

  • LPWSAOVERLAPPED lpOverlapped

    WSAOVERLAPPED 구조체 변수의 주소 값 전달

  • LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine

    Completion Routine이라는 함수의 주소 값 전달

  • Overlapped IO에서의 입출력 완료의 확인

    Overlapped IO에서 입출력의 완료 및 결과를 확인하는 방법에는 두 가지가 있다. 그 두가지는 다음과 같다

    • WSASend, WSARecv 함수의 여섯 번째 매개변수 활용 방법, Event 오브젝트 기반
    • WSASend, WSARecv함수의 일곱 번째 매개변수 활용 방법, Completion Routine 기반

    Event 오브젝트 사용하기

    예제를 통해 다음 두가지 사실을 확인하기 바란다.

    • IO가 완료되면 WSAOVERLAPPED 구조체 변수가 참조하는 Event 오브젝트가 signaled 상태가 된다.
    • IO의 완료 및 결과를 확인하려면 WSAGetOverlappedResult 함수를 사용한다.
    • 예제 OverlappedSend_win.c

#include <stdio.h>

#include <stdlib.h>

#include <winsock2.h>

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

   

void errorHandling(char* msg);

   

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

{

WSADATA wsa;

SOCKET hSocket;

SOCKADDR_IN sendAddr;

   

WSABUF dataBuf;

char msg[] = "Network is Computer!";

DWORD sendbytes = 0;

   

WSAEVENT evObj;

WSAOVERLAPPED overlapped;

   

if( argc != 3 )

{

printf("Usage : %s <IP><port>\n", argv[0]);

exit(1);

}

   

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

{

errorHandling("WSAStartup() error");

}

   

hSocket = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

   

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

sendAddr.sin_family = AF_INET;

sendAddr.sin_addr.s_addr = inet_addr(argv[1]);

sendAddr.sin_port = htons(atoi(argv[2]));

   

// connect

if( connect(hSocket, (SOCKADDR*)&sendAddr, sizeof(sendAddr)) == SOCKET_ERROR )

{

errorHandling("connect() error");

}

   

// CreateEvent

evObj = WSACreateEvent();

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

overlapped.hEvent = evObj;

dataBuf.len = strlen(msg) + 1;

dataBuf.buf = msg;

   

if( WSASend(hSocket, &dataBuf, 1, &sendbytes, 0, &overlapped, NULL) == SOCKET_ERROR )

{

if( WSAGetLastError() == WSA_IO_PENDING )

{

puts("Background data send");

WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);

WSAGetOverlappedResult(hSocket, &overlapped, &sendbytes, FALSE, NULL);

}

else

{

errorHandling("WSASend() error");

}

}

   

printf("Send data size : %d\n", sendbytes);

WSACloseEvent(evObj);

closesocket(hSocket);

WSACleanup();

   

return 0;

}

   

void errorHandling(char* msg)

{

fputs(msg, stderr);

fputc('\n', stderr);

exit(1);

}

WSAGetLastError 함수는 다음과 같이 정의되어 있어서 소켓관련 함수가 호출된 이후에 발생하는 오류의 원인정보를 반환한다

#include <winsock2.h>

int WSAGetLastError(void);
// 오류 상황에 대한 상태 값(오류의 원인을 알리는 값) 반환

이 함수가 반환한 값 WSA_IO_PENDING을 통해서 WSASend 함수의 호출 결과가 오류상황이 아닌, 완료되지 않은(Pending된) 상황임을 확인할 수 있었다.

  • OverlappedRecv_win.c

#include <stdio.h>

#include <stdlib.h>

#include <winsock2.h>

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

   

#define BUF_SIZE 1024

void errorHandling(char* msg);

   

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

{

WSADATA wsaData;

SOCKET hListenSock, hRecvSock;

SOCKADDR_IN listenAddr, recvAddr;

int recvAddrSz;

WSABUF dataBuf;

WSAEVENT evObj;

WSAOVERLAPPED overlapped;

   

char buf[BUF_SIZE];

DWORD recvbytes = 0, flags = 0;

if( argc != 2 )

{

printf("Usage : %s<port>\n", argv[0]);

exit(1);

}

   

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

{

errorHandling("WSAStartup() error");

}

   

hListenSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

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

listenAddr.sin_family = AF_INET;

listenAddr.sin_addr.s_addr = htonl(INADDR_ANY);

listenAddr.sin_port = htons(atoi(argv[1]));

   

if( bind(hListenSock, (SOCKADDR*)&listenAddr, sizeof(listenAddr)) == SOCKET_ERROR )

{

errorHandling("bind() error");

}

if( listen(hListenSock, SOMAXCONN) == SOCKET_ERROR )

{

errorHandling("listen() error");

}

   

recvAddrSz = sizeof(recvAddr);

hRecvSock = accept(hListenSock, (SOCKADDR*)&recvAddr, &recvAddrSz);

   

evObj = WSACreateEvent();

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

overlapped.hEvent = evObj;

dataBuf.len = BUF_SIZE;

dataBuf.buf = buf;

   

if( WSARecv(hRecvSock, &dataBuf, 1, &recvbytes, &flags, &overlapped, NULL) == SOCKET_ERROR)

{

if( WSAGetLastError() == WSA_IO_PENDING )

{

puts("Background data receive");

WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);

WSAGetOverlappedResult(hRecvSock, &overlapped, &recvbytes, FALSE, NULL);

}

else

{

errorHandling("WSARecv() error");

}

}

printf("received message : %s\n", buf);

WSACloseEvent(evObj);

closesocket(hRecvSock);

closesocket(hListenSock);

   

WSACleanup();

return 0;

}

   

void errorHandling(char* msg)

{

fputs(msg, stderr);

fputc('\n', stderr);

exit(1);

}

Completion Routine 사용하기

WSASend, WSARecv 함수의 마지막 전달인자를 통해서 등록되는 Completion Routine이라 불리는 함수를 통해서 확인하는 방법이 있다. CR등록은 다음의 의미를 갖는다

"Pending된 IO가 완료되면, 이 함술르 호출해 달라"

IO가 완료되었을 때, 자동으로 호출될 함수를 등록하는 형태로 IO완료 이후의 작업을 처리하는 방식이 Completion Routine을 활용하는 방식이다. 그러너데 매우 중요한 작업을 진행중인 상황에서 갑자기 Completion Routine이 호출되면 프로그램의 흐름을 망칠 수 있다. 따라서 운영체제는 다음과 같이 이야기한다.

"IO를 요청한 쓰레드가 alertable wait 상태에 놓였을 때만 Completion Routine을 호출할께"

'arlertable wait 상태'라는 것은 운영체제가 전달하는 메시지의 수신을 대기하는 쓰레드의 상태를 뜻하며, 다음 함수가 호출된 상황에서 쓰레드는 alertable wait 상태가 된다.

  • WaitForSingleObjectEx
  • WaitForMultipleObjectEx
  • WSAWaitForMultipleEvents
  • SleepEx

이 함수들은 매개변수 마지막에 더 추가되어 있는데 이 매개변수에 TRUE를 전달하면 해당 쓰레드는 alertable wait상태가 된다. 따라서 IO를 진행시킨 다음에, 급한 다른 볼일들을 처리하고 나서, IO가 완료되었는지 확인하고 싶을 때 위의 함수들 중 하나를 호출하면 된다. 그러면 운영체제는 쓰레드가 alertable wait상태에 진입한 것을 인식하고, 완료된 IO가 있다면 이에 해당하는 Completion Routine을 호출해 준다. Completion Routine이 실행되면, 위 함수들은 모두 WAIT_IO_COMPLETION을 반환하면서 함수를 빠져나온다.

  • ComplRoutineRecv_win.c

#include <stdio.h>

#include <stdlib.h>

#include <winsock2.h>

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

   

#define BUF_SIZE 1024

void CALLBACK CompRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags);

void errorHandling(char* msg);

   

WSABUF dataBuf;

char buf[BUF_SIZE];

DWORD recvbytes = 0;

   

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

{

WSADATA wsaData;

SOCKET hListenSock, hRecvSock;

SOCKADDR_IN listenAddr, recvAddr;

   

WSAOVERLAPPED overlapped;

WSAEVENT evObj;

   

int idx, recvAddrSz;

DWORD flags = 0;

if( argc != 2 )

{

printf("Usage:%s<port>\n", argv[0]);

exit(1);

}

   

// WSAStartup()

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

{

errorHandling("WSAStartup() error");

}

   

// WSASocket()

hListenSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

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

listenAddr.sin_family = AF_INET;

listenAddr.sin_addr.s_addr = htonl(INADDR_ANY);

listenAddr.sin_port = htons(atoi(argv[1]));

   

if( bind(hListenSock, (SOCKADDR*)&listenAddr, sizeof(listenAddr)) == SOCKET_ERROR )

{

errorHandling("bind() error");

}

if( listen(hListenSock, SOMAXCONN) == SOCKET_ERROR )

{

errorHandling("listen() error");

}

   

// accept()

recvAddrSz = sizeof(recvAddr);

hRecvSock = accept(hListenSock, (SOCKADDR*)&recvAddr, &recvAddrSz);

if( hRecvSock == INVALID_SOCKET )

{

errorHandling("accept() error");

}

   

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

dataBuf.len = BUF_SIZE;

dataBuf.buf = buf;

evObj = WSACreateEvent();        // dummy event obj

   

// WSARecv를 통해 overlapped 모델의 completion routine 사용

if( WSARecv(hRecvSock, &dataBuf, 1, &recvbytes, &flags, &overlapped, CompRoutine) == SOCKET_ERROR )

{

// WSAGetLastError()가 WSA_IO_PENDING을 리턴하면 I/O 진행중

if( WSAGetLastError() == WSA_IO_PENDING )

{

puts("Background data receive");

}

}

   

idx = WSAWaitForMultipleEvents(1, &evObj, FALSE, WSA_INFINITE, TRUE);

// I/O 정상적인 완료

if( idx == WAIT_IO_COMPLETION )

puts("Overlapped I/O Completed");

else

errorHandling("WSARecv() error");

   

WSACloseEvent(evObj);

   

closesocket(hRecvSock);

closesocket(hListenSock);

   

WSACleanup();

   

return 0;

}

   

void CALLBACK CompRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)

{

if( dwError != 0 )

{

errorHandling("CompRoutine error");

}

else

{

recvbytes = szRecvBytes;

printf("Received message : %s\n", buf);

}

}

void errorHandling(char* msg)

{

fputs(msg, stderr);

fputc('\n', stderr);

exit(1);

}

WSARecv함수의 마지막 인자로 전달된 Completion Routine의 원형은 다음과 같다

void CALLBACK CompletionRoutine(
DWORD dwError,
DWORD cbTransferred,

LPWSAOVERLAPPED lpOverlapped,

DWORD dwFlags);

첫 번째 매개변수로는 오류 정보가, 두번째 매개변수로는 완료된 입출력 데이터의 크기 정보가 전달된다. 세번째 매개변수로는 WSASend, WSARecv 함수의 매개변수 lpOverlapped로 전달된 값이, 마지막으로 dwFlags에는 입출력 함수호출 시 전달된 특성정보 또는 0이 전달된다.

반응형