[TCP/IP 소켓 프로그래밍] (17) Overlapped IO 서버
IOCP 서버 모델을 구현하기 앞서 Overlappd IO를 수행하는 Sender와 Receiver로 Overlapped IO 서버를 구현해본다.
리눅스의 epoll과 대비되는 윈도우의 IOCP는 논블로킹 소켓을 기반으로 Overlapped IO 입출력 기능을 사용한다.
논블로킹 모드의 소켓 구성하기
SOCKET hListnSock;
int mode = 1;
//...
hListnSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WS_FLAG_OVERLAPPED);
ioctlsocket(hLisnSock, FIOBIO, &mode); // for non-blocking socket
//...
리눅스에서 epoll의 엣지 트리거 서버를 구현할 때, fcntl(파일 디스크립터, 명령어, 인수) 함수를 이용하여 소켓의 O_NONBLOCK 상태를 설정하였다. 윈도우에서도 비슷하게 ioctlsocket 함수를 통해 소켓의 속성을 변경할 수 있으며 입출력모드 FIOBIO를 변수 mode에 저장된 값으로 변경하고 있다.
논블로킹 소켓을 이용하여 입출력을 요청할 때, 버퍼에 데이터가 없어 수행할 일이 없다면, INVALID_SOCKET을 곧바로 반환하게 된다. 이는 에러 상황과 구분하면서 WSAGetLastError함수로 WSAEWOULDBLOCK이라는 상황을 인지할 수 있다.
Overlapping IO 서버
기본적인 구조는 다음과 같이 처리된다.
while(1)
{
Accept() 로직 : 클라이언트 연결을 수락한다.
Read() 로직 : 클라이언트로 부터 메세지를 수신한다. 이는 CompletionRoutine으로 등록된 함수를 호출함으로 수행.
}
ReadRoutine -> Read()에 등록된 콜백 함수로 클라이언트로부터 메세지를 수신받고 에코 서비스를 위해 Write 출력함수를 호출한다. 현재 송신이 가능하다면 Write에 등록된 WriteRoutine이 호출되고 에코 메세지를 전송할 것이다.
메인 함수 - > Read() -> ReadRoutine - > Write() -> WriteRoutine이 차례대로 호출되고 수신할 메세지와 전송할 메세지를 담을 수 있는 구조체가 필요하다. 해당 구조체에 대한 변수를 동적할당하여 서로 다른 콜백함수에서 다른 버퍼를 사용할 수 있게 한다.
- 메세지 버퍼를 위한 사용자 정의 구조체 PER_IO_DATA
#define BUF_SIZE 1024
void CALLBACK ReadRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void CALLBACK WriteRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
typedef struct
{
SOCKET hClntSock; // 소켓 핸들
char buf[BUF_SIZE]; // 수신할 메세지 버퍼
WSABUF wsaBuf; // 전송할 메세지 버퍼
} PER_IO_DATA, *LPPER_IO_DATA;
- main 함수의 서버 로직
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hListnSock, hRecvSock;
SOCKADDR_IN listnAdr, recvAdr;
LPWSAOVERLAPPED lpOver;
DWORD recvBytes;
LPPER_IO_DATA hbInfo;
DWORD mode = 1, flagInfo = 0;
int recvAdrSz;
// 윈속 라이브러리 초기화
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error");
// 중첩 IO가 되는 소켓 생성과 논블로킹 소켓으로 설정
hListnSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
ioctlsocket(hListnSock, FIONBIO, &mode);
// 서버 소켓 주소 할당
memset(&listnAdr, 0, sizeof(listnAdr));
listnAdr.sin_family = AF_INET;
listnAdr.sin_addr.s_addr = htonl(INADDR_ANY);
listnAdr.sin_port = htons(PORT);
if (bind(hListnSock, (SOCKADDR*)&listnAdr, sizeof(listnAdr)) == SOCKET_ERROR)
ErrorHandling("bind() error");
if (listen(hListnSock, 5) == SOCKET_ERROR)
ErrorHandling("listen() error");
recvAdrSz = sizeof(recvAdr);
while (1) // 계속 반복하며 체크
{
// for alertable wait state
SleepEx(100, TRUE);
// 논블로킹 소켓의 accept 함수 호출
hRecvSock = accept(hListnSock, (SOCKADDR*)&recvAdr, &recvAdrSz);
if (hRecvSock == INVALID_SOCKET)
{
// 연결 요청이 없었음
if (WSAGetLastError() == WSAEWOULDBLOCK)
continue;
else
ErrorHandling("accept() error");
}
puts("Client connected...");
lpOver = (LPWSAOVERLAPPED)malloc(sizeof(WSAOVERLAPPED));
memset(lpOver, 0, sizeof(WSAOVERLAPPED));
// 서버의 서비스 제공을 위한 메세지 버퍼
hbInfo = (LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
hbInfo->hClntSock = (DWORD)hRecvSock;
hbInfo->wsaBuf.buf = hbInfo->buf;
hbInfo->wsaBuf.len = BUF_SIZE;
// HANDLE은 void*이기 때문에 overlapped 구조체의 hEvent에 할당하여
// 관련 콜백 함수에서 WSAOVERLAPPD 구조체의 hEvent를 접근하여 우회적으로 메세지 버퍼를 사용할 수 있다.
lpOver->hEvent = (HANDLE)hbInfo;
WSARecv(hRecvSock, &hbInfo->wsaBuf, 1, &recvBytes, &flagInfo, lpOver, ReadRoutine);
}
closesocket(hRecvSock);
closesocket(hListnSock);
WSACleanup();
return 0;
}
while 문 안의 서버의 처리 로직 내용은 다음과 같다.
- 클라이언트 연결 요청이 있는 지 accept 함수로 확인한다.
만약 연결 요청이 없다면, INVALID_SOCKET이 반환되고 에러인지 구분하여 처리한다.
- 연결 요쳥이 있을 시 연결을 수락하고 에코 메세지 전송을 위한 사용자 구조체 PER_IO_DATA를 동적 할당하여 준비한다. 이 메세지 버퍼를 WSAOVERLAPPED 구조체 내부의 hEvent에 할당하여 관련 서비스 루틴 호출시 WSAOVERLAPPED구조체의 hEvent객체를 접근하여 메세지 버퍼를 우회적으로 사용할 수 있게된다.
- hEvent는 기존에 CreateEvent()로 생성한 이벤트 오브젝트를 할당하기 위한 용도로 사용되지만 현재 코드는 이벤트 발생 방식이 아니라 SleepEx 호출을 통한 현재 쓰레드의 alertable wait 상태로 돌입하여 ReadRoutine를 주기적으로 호출하고 있는 것을 구현하고 있다.
서비스 관련 루틴 함수
void CALLBACK ReadRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
LPPER_IO_DATA hbInfo = (LPPER_IO_DATA)(lpOverlapped->hEvent);
SOCKET hSock = hbInfo->hClntSock;
LPWSABUF bufInfo = &(hbInfo->wsaBuf);
DWORD sentBytes;
// 연결 종료
if (szRecvBytes == 0)
{
closesocket(hSock);
free(lpOverlapped->hEvent);
free(lpOverlapped);
puts("Client disconnected...");
}
else
{
// 에코 메세지 서비스 루틴 실행
bufInfo->len = szRecvBytes;
WSASend(hSock, bufInfo, 1, &sentBytes, 0, lpOverlapped, WriteRoutine);
}
}
void CALLBACK WriteRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
LPPER_IO_DATA hbInfo = (LPPER_IO_DATA)(lpOverlapped->hEvent);
SOCKET hSock = hbInfo->hClntSock;
LPWSABUF bufInfo = &(hbInfo->wsaBuf);
DWORD recvBytes;
DWORD flagInfo = 0;
WSARecv(hSock, bufInfo, 1, &recvBytes, &flagInfo, lpOverlapped, ReadRoutine);
}
클라이언트 코드
보낸 메세지를 한 번에 받을 수 있도록 한다.
#include "../network_header.h"
#include "../ErrorHandling.h"
#include <WinSock2.h>
#define BUF_SIZE 1024
int main(int argc, char* argv[])
{
SOCKET hSocket;
SOCKADDR_IN servAdr;
char message[BUF_SIZE];
int strLen, readLen;
// 윈속 라이브러리 초기화
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error");
hSocket = socket(PF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
ErrorHandling("socket() error");
// 서버 소켓 주소 할당
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_addr.s_addr = inet_addr(DEFAULT_NETWORK);
servAdr.sin_port = htons(PORT);
if (connect(hSocket, (struct sockaddr*)&servAdr, sizeof(servAdr)) == -1)
ErrorHandling("connect() error");
else
puts("Connected");
while (1)
{
fputs("Input mesage(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if (strcmp(message, "q\n") == 0 || strcmp(message, "Q\n") == 0)
break;
// 메세지 전송
strLen = strlen(message);
send(hSocket, message, strLen, 0);
// 메세지 수신
readLen = 0;
while (1)
{
readLen += recv(hSocket, message, BUF_SIZE - 1, 0);
if (readLen >= strLen)
break;
}
message[strLen] = 0;
printf("Message from server: %s\n", message);
}
// 소켓 종료
closesocket(hSocket);
// 윈속 라이브러리 해제
WSACleanup();
return 0;
}
IOCP 서버
Overlapped IO 서버에서 매번 aleratable wait로 설정하는 SleepEx는 불필요한 호출이다. 이는 accept호출은 main에서 담당하되, 실제 클라이언트와의 IO는 별도의 쓰레드를 생성하여 처리한다.
출처 : 열혈 TCP/IP 소켓 프로그래밍 윤성우 저