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

Ch 22. Overlapped IO 모델

by minjunkim.dev 2020. 8. 15.

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


# 챕터 21에서 비동기로 처리되었던 것은 IO가 아닌 Notification(알림) 이었음.

이번 챕터에서는 IO를 비동기로 처리하는 방법에 대해서 설명함.

 

# IO(입출력)의 중첩(Overlapped IO)

- 하나의 쓰레드 내에서 동시에 둘 이상의 영역으로 데이터를 송수신함으로 인해, 입출력이 중첩되는 상황

- 이것이 가능하려면 입출력 함수가 바로 반환되어야 함, 즉 비동기 IO가 가능하여야 함

=> 따라서, 호출되는 입출력 함수가 넌-블로킹 모드로 동작해야 함

 

# 윈도우에서 말하는 Overlapped IO

1) 비동기 IO을 통해 한 쓰레드 내에서 여러 IO가 중첩되는 모델

2) IO가 완료된 상황에서 완료결과를 확인하는 방법

=> 두 가지를 모두 포함한 것을 의미


# Overlapped IO 소켓의 생성

- Overlapped IO에 적합한 소켓을 생성해야 함

#include <winsock2.h>

SOCKET WSASocket(
int af, int type, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo,
GROUP g, DWORD dwFlags);
// 성공시 소켓의 핸들, 실패시 INVALID_SOCKET 반환

- af : 프로토콜 체계 정보 전달

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

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

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

- g : 함수의 확장을 위해서 예약되어 있는 매개변수, 따라서 0 전달

- dwFlags : 소켓의 속성정보 전달

 

- 4, 5번째 매개변수는 지금 우리가 하려는 일과 관계가 없으니 각각 NULL과 0을 전달하자.

- 마지막 매개변수에는 WSA_FLAG_OVERLAPPED를 전달해서 생성되는 소켓에 Overlapped IO가 가능한 속성을 부여하자.

e.g.

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

# Overlapped IO를 진행하는 WSASend 함수

- 소켓 생성 이후에 진행되는 서버/클라이언트간의 연결과정은 일반 소켓과 차이가 없으나,

데이터의 입출력에 사용되는 함수는 달라야 함

#include <winsock2.h>

int WSASend(
SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
// 성공시 0, 실패시 SOCKET_ERROR 반환

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

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

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

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

- dwFlags : 함수의 데이터 전송특성을 변경하는 경우에 사용, 예로 MSG_OOB를 전달하면 OOB 모드 데이터 전송

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

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

 

typedef struct __WSABUF
{
    u_long len; // 전송할 데이터의 크기
    char FAR* buf; // 버퍼의 주소값
} WSABUF, *LPWSABUF;

- 데이터 전송시 코드의 구성 형태

WSAEVENT event;
WSAOVERLAPPED overlapped;
WSABUF dataBuf;
char buf[BUF_SIZE]={"전송할 데이터"};
int recvBytes=0;
....
/* manual-reset모드 + non-signaled 상태의 Event 오브젝트 생성 */
event=WSACreateEvent();

/* 모든 비트 0으로 초기화 */
memset(&overlapped, 0, sizeof(overlapped));

overlapped.hEvent=event;

/* 전송할 데이터 정보 초기화 */
dataBuf.len=sizeof(buf);
dataBuf.buf=buf;

/* 데이터 전송 */
WSASend(hSocket,&dataBuf,1,&recvBytes,0,&overlapped,NULL);
....

 

typedef struct _WSAOVERLAPPED
{
    DWORD Internal;
    DWORD InternalHigh;
    DWORD Offset;
    DWORD OffsetHigh;
    WSAEVENT hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;

 

- Overlapped IO를 진행하려면, WSASend 함수의 매개변수 lpOverlapped에는 항상 NULL이 아닌, 유효한 구조체 변수의 주소값을 전달해야 한다. NULL이 전달되면, WSASend 함수의 첫번째 매개변수로 전달된 핸들의 소켓은 블로킹 모드로 동작하는 일반적인 소켓으로 간주된다.

 

- WSASend 함수호출을 통해서 동시에 둘 이상의 영역으로 데이터를 전송하는 경우에는 여섯번째 인자로 전달되는 WSAOVERLAPPED 구조체 변수를 "각각 별도로" 구성해야 한다. => WSAOVERLAPPED 구조체 변수가 Overlapped IO의 진행과정에서 운영체제에 의해 참조되기 때문이다.


# WSASend 함수와 관련한 추가적인 내용

- WSASend 함수의 네번째 인자 lpNumberOfBytesSent 는 전송된 데이터의 크기를 저장한다고 하였는데, WSASend 함수는 호출되자마자 반환한다. 그런데 어떻게 전송된 데이터의 크기가 저장될 수 있을까?

- WSASend 함수라고 해서 무조건 함수의 반환과 데이터의 전송완료 시간이 불일치 하는 것은 아니다.

1) 출력버퍼가 비어있고, 전송하는 데이터의 크기가 크지 않으면, 함수호출과 동시에 출력버퍼로 데이터의 전송이 완료될 수도 있음 => lpNumberOfBytesSent 에 전송된 데이터 크기가 저장되고, 0을 반환

2) 호출된 WSASend 함수가 반환된 이후에도 계속해서 데이터의 전송이 이뤄지는 상황

=> WSASend 함수는 SOCKET_ERROR를 반환하고, WSAGetLastError 함수호출을 통해서 확인 가능한 오류코드로는 WSA_IO_PENDING이 등록됨 => 이 경우에는 다음 함수 호출을 통해 실제 전송된 데이터의 크기를 확인해야 함

#include <winsock2.h>

BOOL WSAGetOverlappedResult(
SOCKET s, LPWSAOVERLAPPED lpOverlapped, LPDWORD lpcbTransfer,
BOOL fWait, LPDWORD lpdwFlags);
// 성공시 TRUE, 실패시 FALSE 반환

- s : Overlapped IO가 진행된 소켓의 핸들

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

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

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

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

 

- 위 함수는 데이터의 전송결과 뿐만 아니라, 데이터 수신결과의 확인에도 사용되는 함수


# Overlapped IO를 진행하는 WSARecv 함수

- WSASend 함수와 기능적으로 전송하느냐 수신하느냐에 대한 차이만 있고 나머지는 동일함

#include <winsock2.h>

int WSARecv(
SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
// 성공시 0, 실패시 SOCKET_ERROR 반환

- s : Overlapped IO 속성이 부여된 소켓의 핸들 전달

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

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

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

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

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

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

 

# Gather/Scatter IO

- 여러 버퍼에 존재하는 데이터를 모아서 한번에 전송 : Gather 출력

- 수신된 데이터를 여러 버퍼에 나눠서 저장 : Scatter 입력

- 챕터 13에서 소개한 writev & readv 함수는 Gather/Scatter IO 기능을 지니는 함수들임

그러나 이 함수들은 윈도우에 정의되어 있지 않음.

하지만 WSASend & WSARecv 함수를 사용하면 이러한 일들이 가능


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

1) WSASend, WSARecv 함수의 여섯번째 매개변수 활용 방법, Event 오브젝트 기반

2) WSASend, WSARecv 함수의 일곱번째 매개변수 활용 방법, Completion Routine 기반

 

1. Event 오브젝트 사용하기

1) IO가 완료되면 WSAOVERLAPPED 구조체 변수가 참조하는 Event 오브젝트가 signaled 상태가 된다.

2) IO의 완료 및 결과를 확인하려면 WSAGetOverlappedResult 함수를 사용한다.

- WSASend & WSARecv의 반환값이 SOCKET_ERROR을 반환하지 않으면 데이터 송수신이 완료된 상황

- WSASend & WSARecv의 반환값이 SOCKET_ERROR이고, WSAGetLastError 함수의 반환값이 WSA_IO_PENDING인 경우는

데이터의 송수신이 완료되지는 않았지만 계속해서 진행중인 상태 

- 데이터 송수신이 완료되면 Event 오브젝트는 signaled 상태가 되므로,

데이터 송수신 완료를 WSAWaitForMultipleEvents 함수호출을 통해 기다릴 수 있음

#include <winsock2.h>

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

 

2. Completion Routine 사용하기

- Completion Routine(이하 CR)의 등록 == "Pending(IO가 완료되지 않은 상황)된 IO가 완료되면, 함수를 호출해 달라!"

- 즉, IO가 완료되었을 때 자동으로 호출될 함수를 등록하는 형태로 IO 완료 이후의 작업을 처리하는 방식이

CR을 활용하는 방식

- 매우 중요한 작업을 진행중인 상황에서 갑자기 CR이 호출되면 프로그램의 흐름을 망칠 수 있으므로,

운영체제는 IO를 요청한 쓰레드가 alertable wait 상태에 놓여있을 때만 CR을 호출한다.

- "alertable wait 상태" : 운영체제가 전달하는 메시지의 수신을 대기하는 쓰레드의 상태.

다음 함수가 호출된 상황에서 쓰레드는 alertable wait 상태가 됨.

1) WaitForSingleObjectEx

2) WaitFOrMultipleObjectsEx

3) WSAWaitForMultipleEvents

4) SleepEx

- IO를 진행시킨 다음, 급한 일들을 처리하고 나서 IO가 완료되었는지를 확인하고자 한다면, 위의 함수들 중 하나를 호출하면 된다. 그러면 운영체제는 쓰레드가 alertable wait 상태에 진입한 것을 인식하고, 완료된 IO가 있다면 이에 해당하는 CR을 호출해준다.

- CR이 실행되면, 위 함수들은 모두 WAIT_IO_COMPLETION을 반환(IO의 정상완료를 의미)하면서 함수를 빠져나오고, 그 다음부터 실행을 이어나간다.

- WSASend & WSARecv 함수의 마지막 인자로 전달되는 CR의 원형

void CALLBACK CompeltionRoutine(
DWORD dwError, DWORD cbTransferred,
LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags);

- dwError : 오류정보가 전달됨(정상종료시 0 전달)

- cbTransferred : 완료된 입출력 데이터의 크기정보가 전달됨

- lpOverlapped : WSASend & WSARecv 함수의 매개변수 lpOverlapped로 전달된 값이 전달됨

- dwFlags : 입출력 함수 호출시 전달된 특성정보 또는 0이 전달됨

- CALLBACK : 함수의 호출규약 중 하나, CR을 정의하는 경우에 반드시 삽입


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