★Overlapped 입출력의 의미
이전의 전송이 끝나기 전에 새로운 전송을 시작할 수 있게끔 입출력을 하는 방식을
Overlapped 입출력이라고 부른다. (중첩됬으므로...)
이런 것이 가능해지려면 일단 전송을 하는 함수(ex: send)가 비동기 방식을 취해야한다.
다시 말해서, 데이터 전송이 안끝나도 실행되자마자 리턴해야된다.
참고로 하나의 쓰레드 내에서 말하고 있는 것이다.
★Overlapped 입출력을 위한 기본 단계
1] Overlapped 소켓의 생성
지금까지와는 다르게 소켓 자체가 overlapped가 되게끔 만들어야한다.
socket()보다 더 기능이 많은 WSASocket()을 사용한다. (WSA_FLAG_OVERLAPPED 플래그)
2] 데이터의 전송
WSASend()와 WSARecv()를 사용하면 중첩된 방식의 데이터 입출력이 가능해진다.
다시 한 번 말하지만 이 함수가 Overlapped 입출력을 가능하게끔 하는 원리는
비동기 방식으로 전송을 하는 것에서부터 비롯된다. (asynchronized)
실행이 되면 바로 리턴된다.
리턴하고 바로 또 WSASend/WSARecv를 호출하면 중첩되는 것이 당연.
int WSASend(s, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags,
lpOverlapped, lpCompletionRoutine);
int WSARecv(s, lpBuffers, dwBufferCount, lpNumberOfBytesRecvd, lpFlags,
lpOverlapped, lpCompletionRoutine);
※ lpBuffers는 WSABUF 구조체 배열의 포인터다. (pg491)
※ lpNumberOfBytes~ : Pointer to the number of bytes received by this call if the
send/receive operation completes immediately.
여기서 문제가 생긴다. WSASend로 이루어진 전송에 대한 결과 정보를 어떻게 받느냐다.
첫 번째 방법은 "Event 커널 오브젝트를 이용하는 방법"이고
두 번째 방법은 "CALLBACK 함수 기반의 Completion Routines를 사용하는 방법"이다.
WSASend나 WSARecv의 여섯번째의 파라미터(lpOverlapped)가 첫 번째 방법에 쓰이고
일곱번째의 파라미터(lpCompletionRoutine)가 두 번째 방법에 쓰인다.
※※ 리눅스에서 writev, readv를 이용하여 Gather/Scatter 입출력을 쉽게 했었는데
윈도우에서도 WSASend/WSARecv의 인자로
WSABUF을 여러 개 이용하여 Gather/Scatter 입출력이 가능하다.
★첫번째 방법: Event 커널 오브젝트 기반의 Overlapped I/O
이 전에 말했듯이, WSASend나 WSARecv함수의 여섯 번째 인자를 사용하게 된다.
WSAOVERLAPPED라는 타입의 구조체를 만들어가지고 초기화시켜서 집어넣으면 된다.
실제로 WSAOVERLAPPED 구조체의 멤버를 보면 5개나 되는데
4개는 내부적으로 쓰여서 우리가 신경쓸 바 없고
나머지 1개(WSAEVENT hEvent)만 우리가 초기화시켜줘서 넣어야한다.
이렇게 해서 이제 "전송 또는 수신"과 "Event 커널 오브젝트"가 연결되었다.
이렇게 연결된 전송 또는 수신이 끝나는 시점이 그 Event 커널 오브젝트가 signaled가 되는 시점이다.
(또한 OVERLAPPED 구조체도 "전송 또는 수신"과 연결됬다고 봐도 된다. 끝나고 결과를 저장...)
※ 참고로 복습하자면, WSAEventSelect는 "소켓"과 "Event 커널 오브젝트"를 연결시키는 함수였다.
Overlapped I/O니까 n개의 전송 또는 수신이 n개의 Event 커널 오브젝트와 연결될 것이다.
n개의 전송 또는 수신은 동시다발적으로 이루어지고
Event 커널 오브젝트도 그에 맞춰서 모두 signaled로 변할 것이다.
그렇게 signaled가 된 Event 커널 오브젝트들을 뭘 어째야할까?
바로 지금까지 해왔던 듯이, WaitForMultipleEvents() 함수를 쓰는 것이다.
책에서는 그냥 똑같은 기능을 하는 WSAWaitForMultipleEvents()를 사용했다.
이제 WSAWaitForMultipleEvent()가 리턴하면
모든 overlapped 전송과 수신이 완료됬다는 것을 의미한다.
완료된 입출력의 성공/실패 여부, 입출력된 바이트 수 등에 대한 정보를 어떻게 뽑아낼까?
WSAGetOverlappedResult() 함수가 그 기능을 하는 함수다.
BOOL WSAGetOverlappedResult(s, lpOverlapped, lpcbTransfer, fWait, lpdwFlags)
- s: 입출력이 완료된 소켓의 핸들
- lpOverlapped: WSASend 혹은 WSARecv 함수 호출시 전달했던
OVERLAPPED 구조체 변수의 포인터를 전달한다.
- fWait: TRUE >> 전송 또는 수신이 끝날 때까지 블로킹
FALSE >> 바로 리턴
- lpcbTransfer, lpdwFlags는 파라미터를 이용한 일종의 리턴이다.
= 리턴: 입출력의 성공적인 완료 시 TRUE 리턴. 입출력이 현재 진행중이거나,
에러가 발생해서 입출력을 완료하지 못한 경우를 실패로 보아 FALSE 리턴.
※ WSAWaitForMultipleEvent()를 이 전에 실행했을 경우,
입출력이 현재 진행중이어서 FALSE를 리턴하는 케이스는 존재하지 않을 것이다.
또한 fWait를 TRUE로 설정하더라도 블로킹 되는 케이스는 존재지 않음.
★두번째 방법: Completion Routines 기반의 Overlapped I/O
WSASend와 WSARecv의 일곱 번째 전달 인자를 사용해야한다.
전달되는 것은 Completion Routine의 함수 포인터이다. 원형은 다음과 같이 선언한다.
void CALLBACK 이름(DWORD dwError, DWORD cbTransferred,
LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags)
- dwError: 완료된 중첩된 입출력 상태 정보. 정상종료 시에 0. 그 이외는 오류 발생을 의미.
- cbTransferred: 전송된 바이트 수
- lpOverlapped: WSASend, WSARecv 함수 호출 시 전달하는 그 구조체 변수의 포인터
- dwFlags: 중첩된 입출력 함수 호출 시 설정했던 옵션 정보
※ dwError와 cdTransferred만 요긴하게 쓰이고, 나머지 것들은 별로...
일단 CALLBACK의 이름에서 의미하는바와 같이 운영체제가 자신의 필요성에 따라 부르는 함수다.
그렇다면 이제 우리가 알아야할 것은... 이 것을 언제 부르는가?
또 이 함수를 통해서 우리는 얼마만큼의 자유를 가지고 있는가? 등이다.
일단 WSARecv에 일곱 번째 전달 인자를 사용하게 될 경우
Event 커널 오브젝트가 signaled가 되는 "조건"이
Event 커널 오브젝트 기반의 Overlapped I/O일 때(첫번째 방법)와는 완전히 다르게 된다.
signaled가 되는 조건은 CompletionRoutine이 가리키는 CALLBACK함수가
한 번 실행되어 종료된 이후이다.
그렇다면 CompletionRoutine이 가리키는 CALLBACK함수는 언제 실행될까?
바로 WSAWaitForMultipleEvents()를 실행시켰을 때부터 실행된다.
< 시나리오 >
WSARecv나 WSASend를 통하여, Event 커널 오브젝트와 CALLBACK 함수 연결
↓
WSAWaitForMultipleEvents() 호출 (Event 커널 오브젝트에 관한 정보만 파라미터로 넘김)
↓
만약 전송/수신이 아직 안끝났으면 끝날 때까지 기다린 후
(CALLBACK함수는 전송/수신이 끝나기 전에는 실행대기 상태에 빠진다 - 커널에서 알아서)
↓
CompletionRoutine이 가리키는 CALLBACK 함수 실행 시작
↓
CompletionRoutine이 가리키는 CALLBACK 함수 종료
↓
WSARecv로 CALLBACK함수와 연결시켰던 Event 커널 오브젝트가 signaled가 되는 시점
↓
WSAWaitForMultipleEvents() 종료
참고로 WSAWaitForMultipleEvents 함수를 대신해서 SleepEx 함수를 사용할 경우
I/O completion callback function(CALLBACK 함수)가 불려질 때까지
블로킹 상태에 빠진다. 이럴 경우 Event 커널 오브젝트를 생성할 필요가 없어진다.
더 깔끔해진다고나할까?
DWORD SleepEx(
DWORD dwMilliseconds, // time-out interval
BOOL bAlertable // early completion option
);
/* EXAMPLE */
void CALLBACK CompletionRoutine(DWORD error, DWORD szRecvBytes,
LPWSAOVERLAPPED lpOverlapped, DWORD flags){
if(error != 0) //에러 발생 시
ErrorHandling("CompletionRoutine error");
recvBytes = szRecvBytes; //전역 recvBytes
buf[szRecvBytes] = 0; //전역 buf[]
printf("수신한 메시지: %s \n", buf);
}