2015년 1월 20일 화요일

TCP/IP 소켓 프로그래밍 23강

휴~ 드디어 TCP/IP 공부를 마쳤다. (그래봐야 이제 첫 걸음 땐 것 ㅎㅎ;)
IT 계열은 평생 공부해야할 분야인 것 같다.
이제야 조금 네트워크 프로그래밍을 시작한 단계다.
21,22 강이 바로 이 23강을 이해하기 위한 기본이였다.
23강은 IOCP(Input Output Completion Port) 이다.

23-1: Overlapped IO를 기반으로 IOCP 이해하기


-넌-블로킹 모드의 소켓 구성하기-


윈도우에서는 다음과 같이 넌-블로킹 소켓의 속성을 변경한다.

 SOCKET hLisnSock;
int mode = 1;
......................
hLisnSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0,
                                     WSA_FLAG_OVERLAPPED);
 ioctlsocket(hLisnSock, FIONBIO, &mode); //넌-블로킹 소켓

핸들 hLisnSock 이 참조하는 소켓의 입출력모드(FIONBIO)를 변수 mode에 저장된 값의 형태로 변경한다.

속성이 넌-블로킹 모드로 변경되면 다음과 같은 특징을 지니게 된다

-클라이언트의 연결 요청이 존재하지 않는 상태에서 accept 함수가 호출되면 INVALID_SOCKET 이 곧바로 반환된다.
-그리고 이어서 WSAGetLastError 함수를 호출하면 WSAEWOULDBLOCK 가 반환된다.
-accept 함수호출을 통해서 새로 생성되는 소켓 역시 넌-블로킹 속성을 지닌다.

23-2 : IOCP의 단계적 구현


-Completion Port 의 생성-


IOCP 모델의 서버 구현을 위해서는 두가지 일을 진행해야 한다.

-Completion Port 오브젝트의 생성
-Completion Port 오브젝트와 소켓의 연결

이때 소켓은 반드시 Overlapped 속성이 부여된 소켓이어야 한다.
CP 오브젝트의 생성 함수다.

#include <windows.h>
HANDLE CreateIoCompletionPort (
              HANDLE FileHandle, HANDLE ExistingCompletionPort, 
              ULONG_PTR CompletionKey, DWORD NumberOfConcurrentThreads);
-> 성공시 CP 오브젝트의 핸들, 실패시 NULL 반환

-FileHandle : CP 오브젝트 생성시에는 INVALID_HANDLE_VALUE 를 전달
-ExistingCompletionPort : CP 오브젝트 생성시에는 NULL 전달.
-CompletionKey : CP 오브젝트 생성시에는 0 전달.
-NumberOfConcurrentThreads : 완료된 IO를 처리할 쓰레드의 수를 전달 

위 함수를 CP 오브젝트의 생성 목적으로 호출할 때는 마지막 매개변수만이 의미를 갖는다.
만약 CP 오브젝트에 할당되어 IO를 처리할 쓰레드의 수를 2개로 지정할 때
다음과 같이 구성하면 된다.

HANDLE hCpObject;
......................
hCpObject = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 2);

-Completion Port 오브젝트와 소켓의 연결-


CP 오브젝트가 생성되었다면, 소켓과 연결시켜야 한다.
그래야 완료된 소켓의 IO 정보가 CP오브젝트에 등록된다.
여기서 위에 쓰인 함수가 또 쓰인다(매개변수에 들어가는 값만 바뀐다.)

#include <windows.h>
HANDLE CreateIoCompletionPort (
              HANDLE FileHandle, HANDLE ExistingCompletionPort, 
              ULONG_PTR CompletionKey, DWORD NumberOfConcurrentThreads);
-> 성공시 CP 오브젝트의 핸들, 실패시 NULL 반환

-FileHandle : CP 오브젝트에 연결할 소켓의 핸들 전달
-ExistingCompletionPort : 소켓과 연결할 CP 오브젝트의 핸들 전달
-CompletionKey : 완료된 IO 관련 정보의 전달을 위한 매개변수
-NumberOfConcurrentThreads : 어떠한 값을 전달해도 두 번째 매개변수가 NULL이 아니면 무시된다.

-Completion Port 의 완료된 IO 확인과 쓰레드의 IO 처리-


CP에 등록되는 완료된 IO를 확인하는 함수다.

#include <windows.h>
BOOL GetQueuedCompletionStatus ( HANDLE CompletionPort, 
                   LPDWORD lpNumberOfBytes, PULONG_PTR lpCompletionKey, 
                   LPOVERLAPPED* lpOverlapped, DWORD dwMilliseconds);
-> 성공시 TRUE, 실패시 FALSE 반환

-CompletionPort : 완료된 IO 정보가 등록되어 있는 CP 오브젝트의 핸들 전달
-lpNumberOfBytes : 입출력 과정에서 송수신 된 데이터의 크기 정보를 저장할 변수의 주소 값 전달
-lpCompletionKey : CreateloCompletionPort 함수의 세번째 인자로 전달된 값의 저장을 위한 변수의 주소 값 전달
-lpOverlapped : WSASend, WSARecv 함수호출 시 전달하는 OVERLAPPED 구조체 변수의 주소 값이 저장될, 변수의 주소 값 전달
-dwMilliseconds : 타임아웃 정보전달, 지정한 시간이 완료되면 FALSE를 반환하면서 함수를 빠져나가며, INFINITE를 전달하면 완료된 IO가 CP오브젝트에 등록될 때까지 블로킹 상태에 있게 된다.

이제 위 함수를 이용한 소스를 보자.

#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <winsock2.h>
#include <windows.h>

#define BUF_SIZE 100
#define READ 3
#define WRITE 5

//클라이언트와 연결된 소켓 정보를 담기 위한 구조체
typedef struct    
{
SOCKET hClntSock;
SOCKADDR_IN clntAdr;
} PER_HANDLE_DATA, *LPPER_HANDLE_DATA;

//OVERLAPPED 구조체 변수를 담아서 구조체를 정의
typedef struct    // buffer info
{
OVERLAPPED overlapped;
WSABUF wsaBuf;
char buffer[BUF_SIZE];
int rwMode;    // READ or WRITE
} PER_IO_DATA, *LPPER_IO_DATA;

DWORD WINAPI EchoThreadMain(LPVOID CompletionPortIO);
void ErrorHandling(char *message);

int main(int argc, char* argv[])
{
WSADATA wsaData;
HANDLE hComPort;
SYSTEM_INFO sysInfo;
LPPER_IO_DATA ioInfo;
LPPER_HANDLE_DATA handleInfo;

SOCKET hServSock;
SOCKADDR_IN servAdr;
int recvBytes, i, flags=0;

if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error!");
//CP 오브젝트 생성
//마지막 인자가 0이니, 코어의 수만큼 쓰레드가 CP 오브젝트에 할당 가능
hComPort=CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

//현재 실행중인 시스템 정보를 얻기 위해서 GetSystemInfo 함수를 호출
GetSystemInfo(&sysInfo);

//CPU의 수 만큼 반복해서 쓰레드를 생성하고
//CP 오브젝트에 핸들을 전달한다. 
for(i=0; i<sysInfo.dwNumberOfProcessors; i++)
_beginthreadex(NULL, 0, EchoThreadMain, (LPVOID)hComPort, 0, NULL);

hServSock=WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family=AF_INET;
servAdr.sin_addr.s_addr=htonl(INADDR_ANY);
servAdr.sin_port=htons(atoi(argv[1]));

bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr));
listen(hServSock, 5);

while(1)
{
SOCKET hClntSock;
SOCKADDR_IN clntAdr;
int addrLen=sizeof(clntAdr);

hClntSock=accept(hServSock, (SOCKADDR*)&clntAdr, &addrLen);

//LPPER_HANDLE_DATA 구조체 변수를 동적 할당 
//클라이언트와 연결된 소켓, 그리고 클리라언트의 주소정보를 담고 있다.
handleInfo=(LPPER_HANDLE_DATA)malloc(sizeof(PER_HANDLE_DATA));
handleInfo->hClntSock=hClntSock;
memcpy(&(handleInfo->clntAdr), &clntAdr, addrLen);

//CP 오브젝트와 생성된 소켓을 연결하고 있다.
CreateIoCompletionPort((HANDLE)hClntSock, hComPort, (DWORD)handleInfo, 0);

//LPPER_IO_DATA 구조체 변수를 동적 할당하였다. 
//따라서 WSARecv 함수호출에 필요한 OVERLAPPED 구조체 변수와 
//WSABUF 구조체 변수, 그리고 버퍼까지 한번에 마련되었다.
ioInfo=(LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));
ioInfo->wsaBuf.len=BUF_SIZE;
ioInfo->wsaBuf.buf=ioInfo->buffer;
//IOCP는 기본적으로 입력의 완료와 출력의 완료를 구분 지어주지 않는다. 
//입력이건 출력이건 완료되었다는 사실만 인식시켜준다.
//그래서 입력을 진행한 것인지, 출력을 진행한 것인지 정보를 별도로 기록해줘야 한다.
ioInfo->rwMode=READ;

WSARecv(handleInfo->hClntSock, &(ioInfo->wsaBuf),
1, &recvBytes, &flags, &(ioInfo->overlapped), NULL);
}
return 0;
}

//쓰레드에 의해 실행되는 함수다. 
DWORD WINAPI EchoThreadMain(LPVOID pComPort)
{
HANDLE hComPort=(HANDLE)pComPort;
SOCKET sock;
DWORD bytesTrans;
LPPER_HANDLE_DATA handleInfo;
LPPER_IO_DATA ioInfo;
DWORD flags=0;

while(1)
{
//IO가 완료되고, 이에 대한 정보가 등록되었을 때 반환한다.
GetQueuedCompletionStatus(hComPort, &bytesTrans, 
(LPDWORD)&handleInfo, (LPOVERLAPPED*)&ioInfo, INFINITE);

sock=handleInfo->hClntSock;

//포인터 ioInfo에 저장된 값은 OVERLAPPED 구조체 변수의 주소 값이지만, 
//PER_IO_DATA 구조체 변수의 주소 값이기도 하다. 
//멤버 rwMode에 저장된 값의 확인을 통해서 입력의 완료인지 출력의 완료인지 확인한다.
if(ioInfo->rwMode==READ)
{
puts("message received!");
if(bytesTrans==0)    // EOF 전송시
{
closesocket(sock);
free(handleInfo); free(ioInfo);
continue;
}

//서버가 수신한 메시지를 클라이언트에게 재전송하는 과정을 보이고 있다.
memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));
ioInfo->wsaBuf.len=bytesTrans;
ioInfo->rwMode=WRITE;
WSASend(sock, &(ioInfo->wsaBuf), 
1, NULL, 0, &(ioInfo->overlapped), NULL);

//메시지 재전송 이후에 클라이언트가 전송하는 메시지의 수신과정을 보이고 있다.
ioInfo=(LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));
ioInfo->wsaBuf.len=BUF_SIZE;
ioInfo->wsaBuf.buf=ioInfo->buffer;
ioInfo->rwMode=READ;
WSARecv(sock, &(ioInfo->wsaBuf), 
1, NULL, &flags, &(ioInfo->overlapped), NULL);
}
//완료된 IO가 출력한 경우에 실행되는 else 영역이다.
else
{
puts("message sent!");
free(ioInfo);
}
}
return 0;
}

void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}


원래 내용이 훨씬 많지만 정말 간단하게 요약해서 정리한 것이다.
역시 소스 코드를 다운 받아 해보는 것이 가장 좋다.
소스 코드 다운 받기

댓글 없음:

댓글 쓰기