TCP/IP 소켓프로그래밍 : 윈도우 IO 모델과 IOCP
Asynchronous Notification IO 모델
동기 입출력 : send함수 호출시 전송을 시작하고, 출력버퍼에 전송이 완료되면 send함수가 반환된다. recv함수도 마찬가지로 호출 시 수신(입력버퍼->버퍼변수)시작하고 수신이완료되면 반환
비동기 입출력 : send, recv 함수는 호출시 바로 반환되고, 내부적으로 따로 데이터 입출력이 계속 이뤄져 완료한다.
입출력은 CPU를 크게 사용하지 않는데, 동기 입출력을 사용하여 입출력이 종료되기 전가지 블록상태로 만들어버리면 CPU 쉬게되어 자원에 낭비가 생길 수 있기 때문에 비동기 입출력을 사용한다.
동기 알림 : select 문 등으로 소켓 관찰 시 알림이 발생하면 함수가 반환된다. -> 반환을 통해 알림을 확인
비동기 알림 : 소켓 관찰만 시켜두고 원하는 시간에 찾아가서 알림이 있는지 확인 (WSAEventSelect)
int WSAEventSelect(SOCKET s, WSAEVENT hEventObject, long lNetworkEvnets);
- s : 관찰대상 소켓 핸들 전달
- hEventObject : s에서 lNetworkEvent에 해당하는 이벤트가 발생하면 hEventObject를 signaled로 변경한다.
- lNetworkEvents : 감시하고자 하는 이벤트 전달. FD_READ, FD_WRITE, FD_OOB, FD_ACCEPT, FD_CLOSE
WSAEVENT WSACreateEvnet(void); 두번째 인자로 전달되는 이벤트오브젝트 생성. nonsignaled+manualreset
BOOL WSACloseEvent(WSAEVENT hEvnet);
DWORD WSAWaitForMultipleEvents( cEvents, lphEvents, fWaitAll, dwTimeout, fAlertable);
- fAlertable : TRUE 전달 시 alertable wait 상태로 진입
WSAWaitForMultipleObject 함수와 동일하지만 fAlertable 인자만 추가된다.
int posInfo, startIdx, i;
...
posInfo=WSAWaitForMultipleEvents(numOfSock, hEventArray, FALSE, WSA_INFINITE, FALSE);
// 알림이 발생한 첫번째 소켓인덱스
startIdx=posInfo-WSA_WAIT_EVENT_0;
...
// 동시에 2개 이상의 알림이 발생했다면 나머지는 반복문을통해 찾아야한다.
for(i=startIdx; i<numOfSock; i++){
int sigEventIdx=WSAWaitForMultipleEvents(1, &hEvnetArray[i], TRUE, 0, FALSE);
...
}
만약 WSAWaitForMultipleEvents 함수가 auto-reset 모드의 커널오브젝트핸들을 전달받았다면, 함수가 반환되면서 전부 nonsignaled상태로 변경되기 때문에 동시에 발생한 나머지 인덱스는 찾을 수 없게된다.
int WSAEnumNetworkEvents(SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvnets);
- lpNetworkEvents : 해당 소켓에 발생한 이벤트 정보가 변수에 채워지게된다.
typedef struct _WSANETWORKEVENTS {
long lNetworkEvents;
int iErrorCode[FD_MAX_EVENTS];
}WSANETWORKEVENTS, *LPWSANETWORKEVENTS;
WSANeTWORKEVENTS netEvnets;
...
WSAEnumNetworkEvents(hSock, hEvent, &netEvnets);
if(netEvents.lNetworkEvents & FD_ACCEPT){
...
}
if(netEvents.lNetworkEvents & FD_READ){
...
}
if(netEvents.iErrorCode[FD_READ_BIT]!=0){
// FD_READ 이벤트 관련 오류 발생시 처리
}
...
Overlapped IO 모델
중첩된 입출력 모델. nonblocking 모드로 IO를 하면 입출력이 동시에 진행될 수 있다. 같은 소켓에 넌블러킹모드로 진행하는 경우에는 소켓의 입력버퍼가 하나이기 때문에 동시에 진행되지 않지만, 다른 소켓에 입출력하는 경우에는 동시에 진행된다.
넌블러킹 IO는 성능은 좋지만, read, write가 완료되었는지 확인해야하는 작업이 필요하기 때문에 구현이 힘들어진다.
모든 비동기io는 중첩시킬 수 있게된다. 윈도우가 말하는 overapped IO 모델은 확인작업에서 사용된다.
SOCKET WSASocket(af, type, protocol, lpProtocolInfo, g, dwFlags);
- lpProtocolInfo : 소켓의 특성 정보를 담고있는 WSAPROTOCOL_INFO 구조체 변수의 주소값전달. 필요없다면 NULL
- g : 예약된 파라미터. 0전달
- dwFlags : 소켓의 속성정보 전달. WSA_FLAG_OVERLLAPED 전달하면 OverlappedIO 소켓을 생성
int WSASend(s, lpBuffer, dwBufferCount, lpNumberOfByesSent, dwFlags, lpOverlapped, lpCompletionRoutine);
- s : 소켓의 핸들 전달
- lpBuffers : 전송할 데이터 정보로 이뤄진 WSABUF 구조체로 이뤄진 배열의 주소값 전달. 여러데이터 동시전송가능)
- dwBufferCount : 2번째 인자로 전달된 배열의 길이 전달
- lpNumberOfBytesSent : 전송된 바이트 수가 저장될 변수의 주소값 전달. 함수가 반환될때에도 아직 전송이 완료되지 않은 상태일 가능성이 높기 때문에 의미없는 데이터일 가능성이 높다.
- dwFlags : 데이터 전송 특성 변경 시 사용. MSG_OOB 등
아래 두 매개변수는 입출력이 완료되었을때 확인하는 용도로 사용된다. (두 방법 모두 입출력 완료 확인 가능)
- lpOverlapped : WSAOVERLAPPED 구조체의 주소값을 전달. Event오브젝트가 signaled상태가되는것을 통해 확인가능
- lpCompletionRoutine : 함수의 주소값을 전달하여 완료될때 함수호출
typederf struct __WSABUF{
u_long len; // 전송할 데이터의 크기
char FAR* buf; // 버퍼의 주소값
}WSABUF, *LPWSABUF;
typedef struct _WSAOVERLLAPED{
DWORD Internal; // 운영체제가 내부적으로 사용하는 멤버
DWORD InternalHigh;// 운영체제가 내부적으로 사용하는 멤버
DWORD Offset; // 운영체제가 내부적으로 사용하는 멤버
DWORD OffsetHigh; // 운영체제가 내부적으로 사용하는 멤버
WSAEVENT hEvent; // IO가 끝났을때 signaled로 변경될 이벤트오브젝트
} WSAOVERLAPPED, *LPWSAOVERLAPPED;
WSAEVNET event;
WSAOVERLAPPED overlapped;
WSABUF dataBuf;
char buf[BUF_SIZE]={"전송할데이터"};
int recvBytes=0;
...
event=WSACreateEvent();
memset(&overlapped, 0, sizeof(overlapped));
overapped.hEvent=event;
dataBuf.len=sizeof(buf);
dataBuf.buf=buf;
// 나중에 IO가 완료되면 overlapped.hEvent 가 signaled로 변경된다.
WSASend(hSocket, &dataBuf, 1, &recvBytes, 0, &overlapped, NULL);
...
BOOL WSAGetOverlappedResult(s, lpOverlapped, lpcbTransfer, fWait, lpdwFlags)
- s : io가 진행된 소켓 핸들 전달
- lpOverlapped : WSAOVERLAPPED 구조체를 전달하고 이 값
- lpcbTransfer : 실제 송수신된 바이트 크기를 저장할 변수
- fWait : IO 가 진행중이면 TRUE 전달 시 완료될때까지 대기, FALSE라면 FALSE를 반환하면서 함수를 빠져나온다.
- lpdwFlags : WSARecv 함수가 호출된 경우 OOB메세지인지 등을 확인하기 위함. 필요없으면 NULL 전달
WSASend 함수는 데이터 전송이 완전히 끝난경우 0을 반환하고, 소켓 오류가 발생하거나 데이터 전송이 바로 끝나지 않은경우 SOCKET_ERROR을 반환한다. WSAGetOverlappedResult 함수는 데이터 전송이 언제 끝나는지 물어보는 함수이기 때문에 WSASend 함수가 SOCKET_ERROR가 반환된 경우 실행되며, 데이터 전송이 완료되지 않아서 SOCKET_ERROR가 반환되는 경우 WSAGetLastError를 통해 WSA_IO_PENDING 오류코드를 반환받을 수 있다.
WSAGetLastError은 프로그램 실행중 오류가 발생한경우 특정 메모리공간에 오류코드가 기록되는데, 이 코드값을 가져오는 함수이다.
if(WSASend(hSocket, &dataBuf, 1, &sendBytes, 0, &overlapped, NULL) == SOCKET_ERROR){
if(WSAGetLastError == WSA_IO_PENDING){
// IO가 WSASend 한번에 끝나지 않은경우
// IO가 완료될때까지 대기 어차피 반환해도 매뉴얼리셋이라 상관없다.
WSAWAitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);
// 여기서도 4번째 인자로 대기하게할 수 있지만 보통 이렇게 합쳐서 씀
WSAGetOverlappedResult(hSocket, &overlapped, &sendBytes, FALSE, NULL);
} else {
// 진짜 소켓 에러가 발생한 경우
}
}
WSARecv 함수도 Send와 동일하게 사용된다.
Completion Routine : WSASend, WSARecv 함수의 마지막인자로 IO가 완전히 끝났을때 실행할 함수를 전달할 수 있다. 하지만 쓰레드가 다른 작업을 수행하다가 컴플리션 루틴에 방해받을 수 있기 때문에 컴플리션루틴이 실행되려면 IO가 완전히 끝난 뒤 쓰레드가 Alertable wait 상태에 놓였을때 비로소 함수를 실행한다. Alertable wait은 쓰레드가 별다른 일을 수행하지 않아서 운영체제가 전달하는 메세지의 수신이 가능한 상태를 뜻한다.
Alertalbe Wait 상태 진입에 사용되는 함수
WaitForSingleObjectEx, WaitForMultipleObjectsEx, WSAWaitForMultipleEvents, SleepEx
// overlapped IO를 진행하기 위해서는 사용하지 않더라도 6번째 인자를 0으로 초기화한뒤 전달해줘야한다.
if(WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, ComRoutine) == SOCKET_ERROR){
if(WSAGetLastError()==WSA_IO_PENDING)
puts("data receive");
}
// alertalbe wait 상태로 진입
// evObj는 사용하지 않지만 함수호출을 위해 더미를 전달한다.
idx=WSAWaitForMultipleEvents(1, &evObj, FALSE, WSA_INFINITE, TRUE)
if(idx == WAIT_IO_COMPLETION)
puts("Overlapped I/O Completed");
else
ErrorHandling("WSARecv() error");
IOCP
멀티쓰레드는 사실상 하나의 CPU에서 컨텍스트 스위칭을 통해 동시에 처리하는것처럼 보이는 방식이다. IOCP는 서버모델을 단순화해서 성능을 극대화한 모델이다.
멀티쓰레드는 웹서버와 같이 클라이언트와 데이터를 주고받는 세션시간이 짧아질수록 쓰레드가 빠르게 데이터를 주고받고 소멸되기 때문에 컨텍스트 스위칭의 시간이 짧아지고 성능이 좋아진다. 또 코드 작성이 직관적이고 편리하다. IOCP는 세션이 길수록 효과가 좋다.
SOCKET hLisnSock;
// 입출력모드 1은 nonblocking을 뜻함
int mode=1;
...
hLisnSock=WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
ioctlsocket(hLisnSock, FIONBIQ, &mode); // 입출력 모드(FIONBIO)를 mode에 저장된 값으로 변경해라
...
while(1){
SleepEx(100,TRUE); // 메인쓰레드를 alertable wait 상태로 만들기위한 함수호출
hRecvSock=accept(hLisnSock, (SOCKADDR*)&recvAdr, &recvardsz);
ifhRecvSock==INVALID_SOCKET){
// 클라이언트가 없는경우 계속 빠져나오면서 SleepEx 실행
if(WSAGetLastError()==WSAEWOULDBLOCK)
continue;
else
ErrorHandling("accept() error");
}
puts("Client connected....");
lpOvLp=(LPWSAOVERLAPPED)malloc(sizeof(WSAOVERLAPPED));
memset(lpOvLp, 0, sizeof(WSAOVERLAPPED));
hbInfo=(LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
hInfo->hClntSock=(DWORD)hRecvSock;
(hbInfo->wsaBuf).buf=hbInfo->buf;
(hbInfo->wsaBuf).len=hbInfo->BUF_SIZE;
lpOvLp->hEvent=(HANMDLE)hbInfo;
WSARecv(hRecvSock, &(hbInfo->wsaBuf), 1, &recvBytes, &flagInfo, lpOvLp, ReadCompRoutine);
}
넌블러킹 모드로 소켓을 생성하고 이 소켓을 통해 accept를 호출하면 대기하는 클라이언트가 없는경우 INVALID_SOCKET이 반환되고, WSAGetLastError으로 WSAEWOULDBLOCK가 반환된다.
클라이언트가 연결된경우 비동기 데이터 Recv 방식으로 데이터 입력이 완료되면 ReadCompRoutine을 실행시킨다.
ReadCompRoutine은 WriteCompRoutine을 실행시키고, WriteCompRoutine