2015년 1월 20일 화요일

네트워크 프로그래밍 - 도서 추천












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

24강은 지금까지 배운 것을 토대로 HTTP 서버를 제작하는 강이다.
그야말로 마무으리!!

24-1: HTTP(Hypertext Transfer Protocol)의 개요

-웹(Web) 서버의 이해-

"HTTP을 기반으로 웹페이지에 해당하는 파일을 클라이언트에게 전송하는 역할의 서버"

-HTTP-

1. 클라이언트가 서버에 데이터 요청
2. 서버가 데이터 응답
3. 서버가 연결 종료

HTTP는 클라이언트의 상태정보를 유지하지 않는다.
그래서 HTTP를 상태가 존재하지 않는  Stateless 프로토콜이라 한다.

-요청 메세지(Request Message)의 구성-

요청라인, 메시지 헤더, 공백라인, 메시지 몸체로 구성되어 있다.

요청라인 - 요청방식에 대한 정보가 삽입(GET/POST)
메시지 헤더 - 요청에 사용된(응답 받을) 브라우저 정보, 사용자 인증 정보 등
공백라인 - 몸체와 메시지의 헤더 사이를 구분하기 위해 삽입
메시지 몸체 - 클라이언트가 웹서버에게 전송할 데이터(POST 방식에서만 삽입)

-응답 메세지(Response Message)의 구성-

상태 라인, 메시지 헤더, 공백라인, 메시지 몸체로 구성되어 있다.

상태라인 - 클라이언트의 요청에 대한 결과 정보
메시지 헤더 - 전송되는 데이터의 타입 및 길이정보 등
공백라인 - 몸체와 메시지 헤더 사이를 구분하기 위해 삽입
메시지 몸체 - 클라이언트가 요청한 파일의 데이터

24-2: 매우 간단한 웹 서버의 구현

이 부분은 그냥 예제다. 직접 해보는 것이 가장 좋다 ㅎㅎ;
역시 소스 코드를 다운 받아 해보자.
소스 코드 다운 받기

휴~~ 이로써 TCP/IP 소켓 프로그래밍이 끝났다.
얇은 책(551페이지)이라 금방 끝날줄 알았는데....
역시 처음해보는 분야는 습득 시간이 긴거 같다.
24강이면 하루에 1강씩 해서 한달이면 끝날줄 알았는데 ㅎㅎ;;
다음에는 네트워크 프로그래밍 관련해서 몇 가지 도서 추천을 할려고 한다.
(윤성우 열혈 TCP/IP 소켓 프로그래밍 마지막 강에서도 도서 추천을 한다)

막연했던 소켓 프로그래밍의 맛보기라도 하게 해준 이 책에게 감사하다.
소켓 프로그래밍은 학교에서도 수강을 안했었기에 전혀 개념이 없었다
(수강 신청할 껄 ㅡㅜ)
소켓 프로그래밍을 처음 공부하는 분들은

윤성우 열혈 TCP/IP 소켓 프로그래밍 책을 적극 추천한다.
책 사러 가기

그럼 이만~

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);
}


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

2015년 1월 16일 금요일

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

21강에서 알아본 비동기는 "알림"이었다.
전송이 완료되면 완료 되었다고 알려주는 것이였다.
22강에서는 IO를 비동기로 처리하는 방법을 설명한다.

22-1 :  Overlapped IO 모델의 이해

-IO(입출력)의 중첩이란?-

하나의 쓰레드 내에서 동시에 둘 이상의영역으로 데이터를 전송(또는 수신)함으로 인해서,
입출력이 중첩되는 상황을 가리켜 "IO의 중첩"이라 한다.
이러한 일이 가능하려면 호출된 입출력 함수가 바로 반환을 해야한다.
비동기 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 : 소켓의 속성정보 전달

-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 : 함수의 데이터 전송특성을 변경하는 경우에 사용
- lpOverlapped : WSAOVERLAPPED 구조체 변수의 주소 갑 전달,
                       이벤트 오브젝트를 사용해서
                       데이터 전송의 완료를 확인하는 경우에 사용되는 매개변수
- lpCompletionRoutine : Completion Routine 이라는 함수의 주소 값 전달,
                                   이를 통해서도 데이터 전송의 완료를 확인할 수 있다.

주의할 점이 있다. Overlapped IO를 진행하려면 WSASend 함수의 매개변수 lpOverlapped에는 항상 NULL이 아닌, 유효한 구조체 변수의 주소 값을 전달해야 한다.
만약에 lpOverlapped에 NULL 이 전달되면, 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 함수가 호출된 경우, 부수적인 정보를 얻기 위해 사용된다.
                   불필요하면 NULL 를 전달한다.

-Overlapped IO를 진행하는 WSARecv 함수-

#include <winsock2.h>
int WSARecv ( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
                     LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags,
                     LPWSAOVERLAPPED lpOverlapped,
                     LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
-> 성공 시 0, 실패시 SOCKET_ERROR 반환

- s : Overlapped IO 속성이 부여된 소켓의 핸들 전달
- lpBuffers : 수신된 데이터 정보가 저장될 버퍼의 정보를 지니는
                   WSABUF 구조체 배열의 주소 값 전달
- dwBufferCount : 두 번째 인자로 전달된 배열의 길이정보 전달
- lpNumberOfBytesRecvd : 수신된 데이터의 크기정보가 저장될 변수의 주소 값 전달
- lpFlags : 전송특성과 관련된 정보를 지정하거나 수신하는 경우에 사용된다.
- lpOverlapped : WSAOBERLAPPED 구조체 변수의 주소 값 전달
- lpCompletionRoutine : Completion Routine 이라는 함수의 주소 값 전달

22-1 : Overlapped IO에서의 입출력 완료의 확인

입출력의 완료/결과를 확인하는 방법에는 두 가지가 있다.
(1)WSASend, WSARecv 함수의 6번째 매개 변수 활용방법, 이벤트 오브젝트 기반
(2)WSASend, WSARecv 함수의 7번째 매개 변수 활용방법, Completion Routine기반

-이벤트 오브젝트 사용하기-

-IO가 완료되면 WSAOVERLAPPED 구조체 변수가 참조하는 이벤트 오브젝트가 signaled 상태가 된다.
-IO의 완료 및 결과를 확인하려면 WSAGetOverlappedResult 함수를 사용한다.

-Completion Routine 사용하기-

역시 소스를 여기에 기재해야하는데 공간이 부족하니 다운 받아서 확인해보는 것이 좋다.
그리고 윈도우는 뭔 이름이 이리 긴지 모르겠다 ㅡㅡ

소스 코드 다운 받기

2015년 1월 13일 화요일

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

21강은 한동안 이해가 안가서 멘붕이었다 ㅡㅡ;
하지만 2번 3번 보니 이해가 가더라....포기하지 않는 것이 제일 중요한 것 같다.

21-1  : 비동기 Notification IO 모델의 이해


-동기(Synchronous)와 비동기(Asynchronous)에 대한 이해-


send 함수가 호출되면 데이터의 전송이 완료된 후에야 반환이 이루어졌다.
함수의 반환이 이루어져야 전송의 완료를 확인하는 것이다.
이런식이다 보니 반환을 기둘리는 CPU 입장에서는 쓸데없이 노는 시간(?)이 생긴 것이다.
이것을 극복하고자 나온 것이 비동기 방식이다.
한마디로 CPU가 쉬는 시간을 주지 않겠다는 것이다.(노동착취 ㅡ_ㅡ)

비동기를 대략적으로 설명하자면 이렇다.

1. send를 호출해서 데이터 전송을 시작한다.
2. 전송이 완료가 되던 말던 CPU는 딴 일을 한다.
3. 전송을 "관찰"하는 함수를 호출한다.
4. 전송 완료가 되면 "관찰하던 함수"가 "send 함수"에게 완료되었다고 알려준다.
5. 관찰하던 함수 종료, send 함수 종료

IO의 상태변화를 알리는 것을 약간 고급스럽게(?) Notification 이라고 한다.

21-2 : 비동기 Notification IO 모델의 이해와 구현


소켓을 대상으로 이벤트 발생여부의 관찰을 명령할 때 사용하는 함수다.

#include <winsock2.h>
int WSAEventSelect(SOCKET s, WSAEVENT hEventObject, long lNetworkEvents);
-> 성공시 0 , 실패시 SOCKET_ERROR 반환

-s : 관찰 대상인 소켓의 핸들 전달
-hEventObject : 이벤트 발생유무의 확인을 위한 이벤트 오브젝트 핸들 전달
-lNetworkEvents : 감시하고자 하는 이벤트의 유형 정보 전달

WSAEventSelect 함수는 매개변수 s에 전달된 핸들의 소켓에서,
세 번째 인자에 전달된 이벤트 중 하나가 발생하면,
두 번째 인자에 전달된 핸들의 커널 오브젝트를 signaled 상태로 바꾸는 함수다.

세 번째 인자에 들어가는 이벤트의 종류를 살펴보자.

FD_READ : 수신할 데이터가 존재하는가?
FD_WRITE : 블로킹 없이 데이터 전송이 가능한가?
FD_OOB : Out-of-band 데이터가 수신되었는가?
FD_ACCEPT : 연결요청이 있었는가?
FD_CLOSE : 연결의 종료가 요청되었는가?

참고로 WSAEventSelect 함수 호출을 통해서 전달된 소켓의 정보는 OS에 등록되고,
이렇게 등록된 소켓에 대해서는  WSAEventSelect 함수의 재호출이 불필요하다.

-manual-reset 모드 Event 오브젝트의 또 다른 생성방법-


이전에는 CreateEvent 함수를 이용해서 이벤트 오브젝트를 생성했다.
 CreateEvent는 이벤트 오브젝트를 생성할 때,
auto-reset 모드와 manual-reset 모드를 선택해서 생성할 수 있는 함수였다.
그러나 여기서 필요한 것은 "manual-reset 모드이면서, non-signaled 상태"인
이벤트 오브젝트다. 이 때 필요한 함수를 보자.

#include <winsock2.h>
WSAEVENT WSACreateEvent(void);
-> 성공시 이벤트 오브젝트 핸들, 실패시 WSA_INVALID_EVENT 반환

위의 함수로 생성된 이벤트 오브젝트 "종료"를 위한 함수는 다음과 같다

#include <winsock2.h>
WSAEVENT WSACloseEvent(WSAEVENT hEvent);
-> 성공시 true, 실패시 false 반환

-이벤트 발생유무의 확인-


WSAEventSelect 함수호출 "이후"를 고민해야한다. "이벤트 발생유무의 확인"을 위해서는
이벤트 오브젝트를 확인해야 한다. 이때 사용하는 함수다.

#include <winsock2.h>
DWORD WSAWaitForMultipleEvents (
      DWORD cEvents, const WSAEVENT* lphEvents,
      BOOL fWaitAll, DWORD dwTimeout, BOOL fAlertable);
-> 성공시 이벤트 발생 오브젝트 관련정보, 실패시 WSA_INVALID_EVENT 반환

-cEvents : signaled 상태로의 전이여부를 확인할 Event 오브젝트의 개수 정보 전달
-lphEvents : Event 오브젝트의 핸들을 저장하고 있는 배열의 주소 값 전달
-fWaitAll : true 전달시 모든 이벤트 오브젝트가 signaled 상태일 때 반환, false 전달시 하나만 signaled 상태가 되어도 반환
-dwTimeout : 1/1000 초 단위로 타임아웃 지정, WSA_INFINITE 전달시 signaled 상태가 될 때까지 반환하지 않는다.
-fAlertable : true 전달시 alertable wait 상태로의 진입

-반환값 : 반환된 정수 값에서 상수 값 WSA_WAIT_EVENT_0을 빼면,
              두 번째 매개변수로 전달된 배열을 기준으로,
              signaled 상태가 된 이벤트 오브젝트의 핸들이 저장된 인덱스가 계산된다.
              만약에 둘 이상의 이벤트 오브젝트가 signaled 상태로 전이되었다면,
              그 중 작은 인덱스 값이 계산된다.
              그리고 타임아웃이 발생하면 WAIT_TIMEOUT 이 반환된다.

위 함수는 소켓의 이벤트 발생에 의해서 이벤트 오브젝트가 signaled 상태가 되어야 반환하는 함수이므로 소켓의 이벤트 발생여부를 확인하기에 좋은 함수다.

-이벤트 종류의 구분-


WSAWaitForMultipleEvents 함수를 통해서 signaled 상태로 전이 된 이벤트 오브젝트까지 알아낼 수 있게 되었으니 이 상태가 된 원인을 확인해야 한다.

#include <winsock2.h>
int WSAEnumNetworkEvents (
          SOCKET s, WSAEVENT hEventObject,
             LPWSANETWORKEVENTS lpNetworkEvents);
-> 성공시 0, 실패시 SOCKET_ERROR 반환

- s : 이벤트가 발생한 소켓의 핸들 전달
- hEventObject : 소켓과 연결된 signaled 상태인 이벤트 오브젝트의 핸들 전달
- lpNetworkEvents : 발생한 이벤트의 유형정보와 오류 정보로 채워질
                              WSANETWORKEVENTS 구조체 변수의 주소 전달

위의 함수를 사용한 소스를 봐야 하는데.....
좀 길어서 여기에 기재하기가 힘들다 ㅡㅜ
역시 소스를 다운 받아 해보는 것이 좋겠다.
소스 코드 다운 받기

2015년 1월 9일 금요일

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

20강은 윈도우에서의 쓰레드 동기화를 알아볼려고 한다.
이번 강은 함수가 많이 나와서 함수 소개를 주로 하겠다 ㄷㄷ

20-1 : 동기화 기법의 분류와 critical_section 동기화


윈도우에는 2가지 연산 방식 프로그램 실행방식이 있는데. 유저모드, 커널모드가 있다.

-유저모드 : 응용 프로그램이 실행되는 기본모드, 물리적인 영역으로의 접근이 허용되지 않으며 접근할 수 있는 메모리의 영역에도 제한이 따른다.
-커널모드 : 운영체제가 실행될 때의 모드로, 메모리 뿐만 아니라, 하드웨어의 접근에도 제한이 따르지 않는다.

하지만 유저모드에서 커널모드로 전환해야 하는 경우도 있다.
바로 쓰레드를 커널에게 요청할 때다.

문제는 모드를 자주 바꾸면 성능이 떨어진다는 것이다.
그래서 유저모드에서 할 수 있는 쓰레드 동기화라면
모드를 바꾸지 않고 하는 것이 더 이득이다.

먼저 유저모드의 동기화를 알아보자. 바로 "critical_section 동기화"다 .

critical_section를 줄여서 cs 오브젝트라고 표현하겠다.(절대 치기 귀찮아서 이러는게 아니다)

cs오브젝트의 초기화 및 소멸에 쓰이는 함수를 보자.

#include <windows.h>
void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection); (초기화)
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection); (소멸)

- lpCriticalSection :Inin.... 함수에서는 cs 오브젝트의 주소 값 전달, 
                            Del.. 함수에서는 해제할 cs오브젝트 주소값 전달

DeleteCriticalSection 함수는 cs 오브젝트를 소멸하는 함수가 아니다.
cs 오브젝트가 사용하던 리소스를 소멸시키는 함수다.

다음은 cs오브젝트 열쇠의 획득 및 반납에 대한 함수를 보자.
#include <windows.h>
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection); (획득)
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection); (반납)

- lpCriticalSection : 획득(소유) 및 반납할 cs오브젝트의 주소 값 전달

위 함수를 이용한 소스를 보자.
#include <stdio.h>
#include <windows.h>
#include <process.h>

#define NUM_THREAD 50
unsigned WINAPI threadInc(void * arg);
unsigned WINAPI threadDes(void * arg);

long long num=0;
//cs 오브젝트 변수 설정
CRITICAL_SECTION cs;

int main(int argc, char *argv[])
{
HANDLE tHandles[NUM_THREAD];
int i;
// cs 오브젝트 초기화
InitializeCriticalSection(&cs);
//쓰레드 생성후 핸들값에 넣기
for(i=0; i<NUM_THREAD; i++)
{
if(i%2)
   tHandles[i]=(HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
else
   tHandles[i]=(HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
}
//둘 이상의 커널 오브젝트를 대상으로 상태 확인
WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
//리소스 해제
DeleteCriticalSection(&cs);
printf("result: %lld \n", num);
//윈도우는 메인함수가 끝나면 쓰레드도 같이 종료된다.
return 0;
}

unsigned WINAPI threadInc(void * arg)
{
int i;
//임계영역 설정
EnterCriticalSection(&cs);
for(i=0; i<50000000; i++)
num+=1;
LeaveCriticalSection(&cs);

return 0;
}
unsigned WINAPI threadDes(void * arg)
{
int i;
//임계영역 설정
EnterCriticalSection(&cs);
for(i=0; i<50000000; i++)
num-=1;
LeaveCriticalSection(&cs);

return 0;
}

20 - 2: 커널모드 동기화 기법


커널 모드는 유저 모드 보다 많은 동기화 기법을 지원한다.
대표적인 뮤텍스, 세마포어, 이벤트 기법을 알아보자.

1. 뮤텍스(Mutex) 오브젝트 기반 동기화 


뮤텍스 오브젝트 생성 함수

#include <windows.h>
HANDLE CreatMutex (LPSECURITY_ATTRIBUTES lpMutexAttributes, 
                             BOOL bInitialOwner, LPCTSTR lpName);
-> 성공시 생성된 뮤텍스 오브젝트의 핸들, 실패시 NULL 반환

-lpMutexAttributes : 보안관련 특성 정보의 전달, 디폴트 보안 설정을 위해서 NULL 전달
-bInitialOwner : true 전달시, 생성되는 뮤텍스 오브젝트는 이함수를 호출한 쓰레드의 소유가 되면서 non-signaled 상태가 된다. false 전달시 생성되는 뮤텍스 오브젝트는 소유자가 존재하지 않으며, signaled 상태로 생성된다.
-ipName : 뮤텍스 오브젝트에 이름을 부여할 때 사용된다. NULL을 전달하면 이름 없는 뮤텍스 오브젝트가 생성된다.

뮤텍스 소멸 함수

#include <windows.h>
BOOL CloseHandle(HANDLE hObject);
-> 성공시 true, 실패시 false 반환

-hObject : 소멸하고자 하는 커널 오브젝트의 핸들 전달

뮤택스 반납 함수

#include <windows.h>
BOOL ReleaseMutex(HANDLE hMutex);
-> 성공시 true, 실패시 false 반환

- hMutex : 반납할, 다시 말해서 소유를 해제할 뮤텍스 오브젝트의 핸들 전달

2. 세마포어 오브젝트 기반 동기화


세마포어 오브젝트 생성 함수

#include <windows.h>
HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, 
                      LONG linitialCount, LONG lMaximumCount, LPCTSTR lpName);
-> 성공시 생성된 세마포어 오브젝트 핸들, 실패시, NULL 반환

-lpSemaphoreAttributes :보안관련 정보의 전달, 디폴트 보안설정을 위해서 NULL 전달
-linitialCount : 세마포어의 초기값 설정, 매개변수 lMaximumCount에 전달된 값보다 크면 안되고 , 0 이상이여야 함
-linitialCount : 최대 세마포어 값을 지정한다. 1을 지정하면 세마포어 값이 0, 또는 1이 되는 바이너리 세마포어가 구성된다.
-lpName : 세마포어 오브젝트에 이름을 부여할 때 사용한다. NULL을 전달하면 이름없는 세마포어 오브젝트가 된다.

세마포어 오브젝트 반납 함수

#include <windows.h>
Bool ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount, 
                                     LPLONG lpPreviousCount);
-> 성공시 TRUE, 실패시 FALSE 반환

-hSemaphore : 반납할 세마포어 오브젝트 핸들 전달
-lReleaseCount : 반납은 세마포어 값의 증가를 의미하는데, 이 매개변수를 통해서 증가되는 값의 크기를 지정할 수 있다. 그리고 이로 인해서 세마포어의 최대 값을 넘어서게 되면, 값은 증가하지 않고 fales 가 반환된다.
-lpPreviousCount : 변경 이전의 세마포어 값 저장을 위한 변수의 주소 값 전달, 불필요하다면 NULL 전달

3. 이벤트 오브젝트 기반 동기화


이벤트 오브젝트 생성에 사용되는 함수

#include<windows.h>
HANDLE CreateEvent (LPSECURITY_ATTRIBUTES lpEventAttributes, 
                         BOOL bManualReset, BOOL bInitialState, LPCTSTR lpName);
-> 성공시 생성된 이벤트 오브젝트의 핸들, 실패시 NULL 반환
-lpEventAttributes : 보안 관련 정보의 전달, 디폴트 보안설정을 위해서 NULL 전달
-bManualReset : true 전달시 manual-reset 모드 이벤트, false 전달시 auto-reset 모드 이벤트 오브젝트 생성
- bInitialState : true 전달시 singnaled 상태의 이벤트 오브젝트 생성, false 전달시 non-signaled 상태의 이벤트 오브젝트 생성
-lpName : 이벤트 오브젝트에 이름을 부여할 때 사용된다. NULL을 전달하면 이름없는 이벤트 오브젝트가 생성된다.

명시적으로 오브젝트 상대를 변경하는 함수

#include <windows.h>
BOOL ResetEvent(HANDLE hEvent); //non-signaled
BOOL setEvent(HANDLE hEvent); //signaled
-> 성공시 true, 실패시 false 반환

아오 몸이 안좋으니깐 이거 치는 것도 꽤 힘들다 ㅠㅠ
윈도우 함수는 왜이리 대소문자로 왔다갔다해야하는지,........
이름이 너무 긴게 많아!!

원래는 여기 소스도 올려서 확인해봐야 하는데
내 몸상태가 영 나쁘니....
소스 코드를 직접 다운 받아 해보자 /엉엉/
소스 코드 다운 받기

2015년 1월 8일 목요일

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

어느덧 파트3까지 왔다. 파트 3부터는 윈도우 기반 프로그래밍이다.
19강은 윈도우에서의 쓰레드 사용이다.

19-1 : 커널 오브젝트


커널 오브젝트란???
운영체제가 만드는 리소스의 종류는 다양하다.
이런 리소스를 관리하는 방식도 다양하다.
리소스마다 유지해야 하는 정보가 다르니,
이 데이터 블록 형태는 리소스마다 차이가 있다.
이 데이터 블록을 커널 오브젝트라고 한다.

커널 오브젝트의 소유자는 운영체제다.
커널의 오브젝트의 생성, 관리 그리고 소멸 시점을 결정하는 것까지
모두 운영체제의 몫이다.

19-2 : 윈도우 기반의 쓰레드 생성


프로세스와 쓰레드의 관계

main 함수의 호출주체는 쓰레드다.
각종 운영체제들도 운영체제 레벨에서 쓰레드를 지원한다.
따라서 쓰레드를 별도로 생성하지 않는 프로그램은 "단일 쓰레드 모델의 프로그램"
그리고 쓰레드를 별도로 생성하는 방식의 프로그램은 "멀티 쓰레드 모델의 프로그램"
이라고 한다.

윈도우에서 쓰레드 생성방법

1. 이 함수가 호출되면
2. 쓰레드가 생성되고
3. os는 관리를 위해 커널 오브젝트를 생성한다
4. 이 커널 오브젝트의 구분자 역할을 하는, 정수로 표현되는 "핸들"을 반환한다.

#include <windows.h>
HANDLE CreateThread(
         LPSECURITY_ATTRIBUTES lpThreadAttributes,
         SIZE_T dwStackSize,
         LPTHREAD_START_ROUTINE lpStartAddress,
         LPVOID lpParameter,
         DWORD dwCreationFlags,
         LPDWORD lpThreadId
);
-> 성공시 쓰레드 핸들, 실패시 NULL 반환

- lpThreadAttributes : 쓰레드의 보안관련 정보전달, 디폴트 보안설정을 위해서 NULL 전달
- dwStackSize : 쓰레드에게 할당할 스택의 크기를 전달, 0 전달하면 디폴트 크기의 스택 생성
- lpStartAddress : 쓰레드 main 함수 정보 전달
- lpParameter : 쓰레드의 main 함수호출 시 전달할 인자정보 전달.
- dwCreationFlags : 쓰레드 생성 이후의 행동을 결정, 0을 전달하면 생성과 동시에 실행 가능한 상태가 된다.
- lpThreadId : 쓰레드 ID의 저장을 위한 변수의 주소 값 전달.

뭔가 굉장히 복잡해 보이는데 lpStartAddress , lpParameter 만 신경 쓰고,
나머지는 0 또는 NULL을 전달하면 된다.

C/C++ 표준함수를 호출하려면 CreateThread 함수호출을 사용하면 안된다.
왜냐하면 안정적으로 동작하지 않기 때문이다.
쓰레드에 안전한 C 표준함수의 호출을 위한 쓰레드 생성 함수를 소개한다.

#include <process.h>
uintptr_t _beginthreadex (
           void *security,
           unsigned stack_size,
           unsigned (*start_address)(void*),
           void *arglist,
           unsigned initflag,
           unsigned *thrdaddr
);
-> 성공시 쓰레드 핸들, 실패시 0반환

CreateThread 함수와 비교해 보면, 거의 비슷하다.
매개변수의 수도 같고, 의미와 순서도 같다. 단지 이름과 자료형만 조금 다를 뿐이다.
참고로 uintptr_t 는 64비트로 표현되는 unsigned 정수 자료형이다.

소스를 보자.

#include <stdio.h>
#include <windows.h>
#include <process.h>    /* _beginthreadex, _endthreadex */

unsigned WINAPI ThreadFunc(void *arg);

int main(int argc, char *argv[])
{
HANDLE hThread;
unsigned threadID;
int param=5;
//쓰레드의 main 함수로  ThreadFunc를, 그리고 ThreadFunc에 변수 param의 주소값이 전달 하면서 쓰레드의 생성을 요구하고 있다.
hThread=(HANDLE)_beginthreadex(NULL, 0, ThreadFunc, (void*)&param, 0, &threadID);
if(hThread==0)
{
puts("_beginthreadex() error");
return -1;
}
//3초간의 대기시간을 갖는다.
Sleep(3000);
puts("end of main");
return 0;
}

//WINAPI 는 _beginthreadex  함수가 요구하는 호출규약을 지키기 위해 삽입한 것이다.
unsigned WINAPI ThreadFunc(void *arg)
{
int i;
int cnt=*((int*)arg);
for(i=0; i<cnt; i++)
{
Sleep(1000);  puts("running thread");  
}
return 0;
}

main 함수의 반환으로 인해 프로세스가 종료되면, 그 안에 담겨 있는 모든 쓰레드들도 함께 종료된다.

19-3 : 커널 오브젝트의 두 가지 상태


커널 오브젝트의 상태, 그리고 상태의 확인을 운영체제가 하는데
커널 오브젝트의 signaled, non-signaled  상태는 boolean 형 변수 하나로 표현한다.

이 때 쓰는 함수를 보자.

#include <windows.h>
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
-> 성공시 이벤트 정보, 실패 시 WAIT_FAILED 반환
-hHandle : 상태 확인의 대상이 되는 커널 오브젝트의 핸들을 전달.
-dwMilliseconds : 1/1000초 단위로 타임아웃을 지정, 인자로 INFINITE 전달시, 커널 오브젝트가 signaled 상태가 되기 전에는 반환하지 않는다.
-반환값 : signaled 상태로 인한 반환시, WAIT_OBJECT_0 반환, 타임아웃으로 인한 반환시 WAIT_TIMEOUT 반환.

위 함수는 이벤트 발생에 의해서 반환되면,
해당 커널 오브젝트를 다시 non-signaled 상태로 되돌리기도 한다.
그리고 이렇게 다시 non-signaled 상태가 되는
커널 오브젝트를 가리켜 "auto-reset 모드" 커널 오브젝트라 하고,
자동으로 non-signaled 상태가 되지 않는 커널 오브젝트를 가리켜
"manual-reset 모드"커널 오브젝트라 한다.

다음 함수는 둘 이상의 커널 오브젝트를 대상으로 상태를 확인하는 경우에 필요한 함수다.

#include <windows.h>
DWORD WaitForMultipleObjects(
                   DWORD nCount, const HANDLE* lpHandles, BOOL bWaitAll, 
                   DWORD dwMilliseconds);
-> 성공시 이벤트 정보, 실패시 WAIT_FAILED 반환
-nCount : 검사할 커널 오브젝트의 수 전달
-lpHandles :  핸들정보를 담고 있는 배열의 주소 값 전달
-bWaitAll : true 전달시, 모든 검사 대상이 signaled 상태가 되어야 반환, 
                false 전달시, 검사대상 중 하나라도 signaled 상태가 되면 반환
-dwMilliseconds : 1/1000 초 단위로 타임아웃 지정, 인자로 INFINITE 전달시, 커널 오브젝트가 signaled 상태가 되기 전에는 반환하지 않는다. 

새벽 3시 넘어서 글을 쓰다 보니 졸려서 비몽사몽이다 ㅡㅡ;;
윈도우 함수들은 왜이리 이상한지 모르겠다.

역시 예제를 통해서 확인해보는 것이 좋다.
소스 다운 받기

2015년 1월 7일 수요일

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

파트2의 마지막 강인 18강이다.(이런 18...;;)
2015년이 되니 하고 싶은 것도 많고 할일도 벌려놔서 소켓 프로그래밍 공부가 더뎌지고 있다 ;;
소켓 프로그래밍도 몇강 남지 않았다.
이번 1월 안에 끝내보자~ ^^
18강은 멀티쓰레드 기반의 서버 구현이다.
전에 멀티프로세스는 해봤지만 쓰레드는 처음이다.

18-1 : 쓰레드의 이론적 이해


쓰레드의 등장 배경은 멀티프로세스의 단점으로 인해 등장했다.

-멀티 프로세스의 단점-
1.프로세스의 생성이라는 과부하 걸리는 작업과정이 필요하다
2. 두 프로세스 사이에서의 데이터 교환을 위해서는 별도의 IPC 기법을 적용해야한다.

이런 단점을 극복하기 위해 쓰레드가 등장했다.

-쓰레드의 장점-
1. 쓰레드의 생성 및 컨텍스 스위칭(프로세서가 실행되기 위해 메모리에 올리고 내리는 방식)은 프로세스의 생성 및 컨텍스 스위칭보다 빠르다
2. 쓰레드 사이에서의 데이터 교환에는 특별한 기법이 필요치 않다.

쓰레드와 프로세스의 차이점
이건 말보다는 그림으로 보는 것이 이해가 빨라서 그림을 첨부한다.
(출처 - 윤성우 열혈 TCP/IP 소켓 프로그래밍)


위 그림은 프로세서의 메모리 상태이다.
프로세서는 전체를 통으로 복사해서 사용하기에 메모리가 저리 각자 할당된다.
그러다 보니 메모리 용량이 커지고, 메모리에 올렸다 내렸다 하는 과정
(고급스럽게 컨텍스 스위칭 이라고 한다.)이 느려진다.



그래서 쓰레드에서는 위의 그림같이 힙과 데이터 영역은 공유하고, 스택만 독립했다.
스택 영역은 작은 메모리를 차지하기에 컨텍스 스위칭을 할 때 부담이 적다.
그래서 빠른 것이다.

18-2 : 쓰레드의 생성 및 실행


쓰레드의 생성과 실행 흐름의 구성

쓰레드는 별도의 실행흐름을 갖기 때문에 쓰레드만의 main 함수를 별도로 지정해야한다.
그리고, 별도의 실행흐름을 형성해 줄 것을 os에게 요청해야 하는데 그 함수는 다음과 같다.

#include <pthread.h>
int pthread_create (pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void*), void *restrict arg);
-> 성공시 0, 실패시 0 이외의 값 반환

-thread : 생성할 쓰레드의 ID 저장을 위한 변수의 주소 값 전달, 참고로 쓰레드는 프로세스와 마찬가지로 쓰레드의 구분을 위한 ID가 부여된다.
-attr : 쓰레드의 부여할 특성 정보의 전달을 위한 매개변수, NULL 전달시 기본적인 특성의 쓰레드가 생성된다.
-start_routine : 쓰레드의 main 함수 역할을 하는, 별도 실행흐름의 시작이 되는 함수의 주소 값(함수 포인터) 전달.
-arg : 세 번째 인자를 통해 등록된 함수가 호출될 때 전달할 인자의 정보를 담고 있는 변수의 주소 값 전달.

보기엔 무지하게 복잡하다 (나만 그리 느끼는거?) 소스를 보며 파악해보자.

#include <stdio.h>
#include <pthread.h>
void* thread_main(void *arg);

int main(int argc, char *argv[])
{
pthread_t t_id;
int thread_param=5;

//thread_main 함수의 호출을 시작으로 별도의 실행흐름을 구성하는 스레드의 생성을 요청
if(pthread_create(&t_id, NULL, thread_main, (void*)&thread_param)!=0)
{
puts("pthread_create() error");
return -1;
};
//main함수의 실행을 10초간 중지시킨다. 프로세스의 종료시기를 늦추기 위함  
sleep(10);  puts("end of main");
return 0;
}
//매개변수 arg로 전달되는 것은 pthread_create 함수의 네 번째 전달 인자다.
void* thread_main(void *arg)
{
int i;
int cnt=*((int*)arg);
for(i=0; i<cnt; i++)
{
sleep(1);  puts("running thread");
}
return NULL;
}

쓰레드 관련코드 컴파일 할 때는 -lpthread 옵션을 추가해서 해야한다.

예) /tcpip# gcc thread1.c -o tr1 -lpthread

하지만 위의 소스처럼 sleep를 이용해서 흐름을 제어하는 경우는 거의 없다.
언제 쓰레드가 종료될지 예측한다는 것은 어렵기 때문이다.
그래서 쓰레드의 흐름을 조절하는 함수를 사용해야 한다.

#include <pthread.h>
int pthread_join(pthread_t thread, void **status);
-> 성공시 0, 실패 시 0 이외의 값 반환
-thread : 이 매개변수에 전달되는 ID의 쓰레드가 종료될 때까지 함수는 반환하지 않는다.
-status : 쓰레드의 main 함수가 반환하는 값이 저장될 포인터 변수의 주소 값을 전달한다.

소스로 확인해보자.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//쓰레드를 쓰기 위해 선언
#include <pthread.h>
void* thread_main(void *arg);

int main(int argc, char *argv[])
{
pthread_t t_id;
int thread_param=5;
void * thr_ret;
//쓰레드 생성
if(pthread_create(&t_id, NULL, thread_main, (void*)&thread_param)!=0)
{
puts("pthread_create() error");
return -1;
};
//쓰레드가 종료될 때까지 대기 시킨다.
if(pthread_join(t_id, &thr_ret)!=0)
{
puts("pthread_join() error");
return -1;
};

printf("Thread return message: %s \n", (char*)thr_ret);
free(thr_ret);
return 0;
}

void* thread_main(void *arg)
{
int i;
int cnt=*((int*)arg);
char * msg=(char *)malloc(sizeof(char)*50);
strcpy(msg, "Hello, I'am thread~ \n");

for(i=0; i<cnt; i++)
{
sleep(1);  puts("running thread");
}
return (void*)msg;
}

이번에는 쓰레드를 둘 이상을 생성해서 쓰는 것을 알아보려고 한다.
쓰레드를 2개 이상 사용하는 경우 주의할 점이 있다.
쓰레드 2개가 서로 영향을 끼치지 않는 경우는 상관이 없지만.
만약 데이터를 공유해서 값을 바꿔야 하는 경우에는 예기치 못하게 스탭이 꼬여서(?) 엉뚱한 값으로 변할 수 있다.
기본적으로 쓰레드에 안전한 함수는 표준 함수들이다.
하지만 사용자가 만든 함수나 쓰레드의 영향을 받는 데이터 계산 부분은 불안전한 부분이다.
이런 불안전한 부분을 고급스럽게(?) 임계영역이라고 한다.
일반적으로 쓰레드에 안전한 형태로 재 구현된 함수의 이름에는 _r이 붙는다.(리눅스기준)
_r이 붙은 함수를 사용할 땐 컴파일 시 -D_REENTRANT 의 옵션을 추가해야한다.

예) gcc -D_REENTRANT mythread.c -o mthread -lpthread

또 다른 소스를 보자.
이 소스는 두개의 쓰레드를 생성해서 하나는 1~5를 더하고,
다른 하나는 6~10까지 더하는 것이다.

#include <stdio.h>
#include <pthread.h>
void * thread_summation(void * arg);
//이 전역변수를 이용해서 데이터를 공유해가며 계산할 것이다.
int sum=0;

int main(int argc, char *argv[])
{
//쓰레드 2개를 넣을 변수
pthread_t id_t1, id_t2;
int range1[]={1, 5};
int range2[]={6, 10};
//쓰레드 2개 생성
pthread_create(&id_t1, NULL, thread_summation, (void *)range1);
pthread_create(&id_t2, NULL, thread_summation, (void *)range2);
//쓰레드 2개가 종료될 때까지 기다림
pthread_join(id_t1, NULL);
pthread_join(id_t2, NULL);
printf("result: %d \n", sum);
return 0;
}

void * thread_summation(void * arg)
{
int start=((int*)arg)[0];
int end=((int*)arg)[1];
//값을 받아 계산한다.
while(start<=end)
{
sum+=start;
start++;
}
return NULL;
}

실행을 해보면 그나마(?) 잘 계산이 된다.
그럼 더 잘 꼬이도록 짝수, 홀수 만큼 1씩 더했다 뺏다를 해보겠다.
소스를 보자.

#include <stdio.h>
#include <windows.h>
#include <process.h>
#define NUM_THREAD 100

void * thread_inc(void * arg);
void * thread_des(void * arg);
long long num=0;

int main(int argc, char *argv[])
{
HANDLE thread_id[NUM_THREAD];
int i;

printf("sizeof long long: %d \n", sizeof(long long));
for(i=0; i<NUM_THREAD; i++)
{
if(i%2) //짝수일 때 실행
pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
else    //홀수일 때 실행
pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
}

for(i=0; i<NUM_THREAD; i++)
pthread_join(thread_id[i], NULL);

printf("result: %lld \n", num);
return 0;
}

void * thread_inc(void * arg)
{
int i;
for(i=0; i<50000000; i++)
num+=1;
return NULL;
}
void * thread_des(void * arg)
{
int i;
for(i=0; i<50000000; i++)
num-=1;
return NULL;
}

제대로 잘 돌아갔다면 답은 0이 나와야 한다.
실행해보면 재미있는 결과를 알 수 있다. ㅡ.ㅡ
왜 이런 비극(?)이 일어났을까??

18-3 : 쓰레드의 문제점과 임계영역


위에 잠깐 설명했던 임계영역을 서로 마구 침범(?) 했기 때문에 문제가 발생한 것이다.
이런 일이 일어나는 이유는 cpu 때문이다.
cpu는 항상 바쁘다. 그러다 보니 어떤 것이 들어오면 순차적으로 처리하는 것이 아닌
엄청 빠른 시간으로 왔다갔다하며 실행을 하는 것이다.
그러다 보면 당연 쓰레드 같은 것도 스텝이 꼬여서 실행시키는 것이다.
문제가 발생하는 상황을 위의 소스에서 세가지 형태로 나눠서 정리할 수 있다.

1. 두 쓰레드가 동시에 thread_inc 함수를 실행하는 경우
2. 두 쓰레드가 동시에 thread_des 함수를 실행하는 경우
3. 두 쓰레드가 각각 thread_inc,  thread_des  함수를 동시에 실행하는 경우
이런 비극(?)을 방지하는 법을 쓰레드 동기화 라고 한다.

18-4 : 쓰레드 동기화


쓰레드 동기화는 쓰레드의 접근순서 때문에 발생하는 문제점의 해결책을 뜻한다.
동기화가 필요한 상황은 두가지로 생각해볼 수 있다.
1. 동일한 메모리 영역으로의 동시 접근이 발생한 경우
2. 동일한 메모리 영역에 접근하는 쓰레드의 실행 순서를 지정해야 하는 상황

동기화 기법은 뮤텍스, 세마포어 방식이 있다.

뮤텍스(Mutual Exclusion의 줄임말로 쓰레드의 동시 접근을 허용하지 않는다는 의미다
뮤텍스 생성/소멸 함수를 보자

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); (생성)
int pthrread_mutrx_destroy(pthread_mutex_t *mutex); (소멸)
-> 성공 시 0, 실패 시 0이외의 값 반환

-mutex : 뮤텍스 생성시에는 뮤텍스의 참조 값 저장을 위한 변수의 주소 값 전달
소멸시에는 소멸하고자 하는 뮤텍스의 참조 값을 저장하고 있는 변수의 주소 값 전달
-attr : 생성하는 뮤텍스의 특성정보를 담고 있는 변수의 주소값 전달, 별도의 특성을 저장하지 않으면 NULL 전달

이번에는 임계영역에 쓰레드 한개만 들어올 수 있도록 잠금/열림 함수를 소개한다.

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
-> 성공시 0, 실패시 0이외의 값 반환

소스를 보고 판단해보자.

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREAD 100

void * thread_inc(void * arg);
void * thread_des(void * arg);

long long num=0;
//전역 변수로 설정
pthread_mutex_t mutex;

int main(int argc, char *argv[])
{
pthread_t thread_id[NUM_THREAD];
int i;
//뮤텍스 생성
pthread_mutex_init(&mutex, NULL);

for(i=0; i<NUM_THREAD; i++)
{
//쓰레드 생성
if(i%2)
pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
else
pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
}

for(i=0; i<NUM_THREAD; i++)
pthread_join(thread_id[i], NULL);

printf("result: %lld \n", num);
//뮤텍스 소멸
pthread_mutex_destroy(&mutex);
return 0;
}

void * thread_inc(void * arg)
{
int i;
//임계영역을 감쌌다.
pthread_mutex_lock(&mutex);
for(i=0; i<50000000; i++)
num+=1;
pthread_mutex_unlock(&mutex);
return NULL;
}
void * thread_des(void * arg)
{
int i;
for(i=0; i<50000000; i++)
{
//임계영역을 감쌌다.
pthread_mutex_lock(&mutex);
num-=1;
pthread_mutex_unlock(&mutex);
}
return NULL;
}

이번에는 세마포어 생성/소멸 함수를 보자.

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
-> 성공시 0, 실패시 0 이외의 값 반환

-sem : 세마포어 생성시에는 세마포어의 참조 값 저장을 위한 변수의 주소 값 전달, 
소멸시에는 소멸하고자 하는 세마포어의 참조 값을 저장하고 있는 변수의 주소 값 전달
-pshared : 0 이외의 값 전달 시, 둘 이상의 프로세스에 의해 접근 가능한 세마포어 생성, 0 전달시 하나의 프로세스 내에서만 접근 가능한 세마포어 생성. 동기화가 목적이니 0 전달
-value : 생성되는 세마포어의 초기 값 지정

임계영역을 묶는 함수를 보자

#include <semaphore.h>
int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
-> 성공시 0, 실패시 0 이외의 값 반환

-sem 세마포어의 참조 값을 저장하고 있는 변수의 주소 값 전달, 
sem_post에 전달되면 세마포어의 값은 하나 증가, 
sem_wait 에 전달되면 세마포어의 값은 하나 감소

역시 위의 함수를 이용한 소스를 보자.

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

void * read(void * arg);
void * accu(void * arg);
static sem_t sem_one;
static sem_t sem_two;
static int num;

int main(int argc, char *argv[])
{
pthread_t id_t1, id_t2;
//세마포어 두 개 생성
sem_init(&sem_one, 0, 0);
sem_init(&sem_two, 0, 1);
//쓰레드 2개 생성
pthread_create(&id_t1, NULL, read, NULL);
pthread_create(&id_t2, NULL, accu, NULL);

pthread_join(id_t1, NULL);
pthread_join(id_t2, NULL);

sem_destroy(&sem_one);
sem_destroy(&sem_two);
return 0;
}

void * read(void * arg)
{
int i;
for(i=0; i<5; i++)
{
fputs("Input num: ", stdout);
//임계영역 설정
sem_wait(&sem_two);
scanf("%d", &num);
sem_post(&sem_one);
}
return NULL;
}
void * accu(void * arg)
{
int sum=0, i;
for(i=0; i<5; i++)
{
//임계영역 설정
sem_wait(&sem_one);
sum+=num;
sem_post(&sem_two);
}
printf("Result: %d \n", sum);
return NULL;
}

18-5 : 쓰레드의 소멸과 멀티쓰레드 기반의 다중접속 서버의 구현


이제는 쓰레드를 소멸 시키는 법도 알아야 한다.
쓰레드를 소멸하는 방법은 2 가지가 있다.(근데 책에는 3가지라고 제목이 되어있네?!)
1. pthread_join 함수의 호출
2. pthread_detach 함수의 호출

1번은 우리가 앞에서 몇번 쓴 함수다. 하지만 이 함수는 쓰레드가 종료될 때까지 대기 상태에 놓이게 된다는 것이다. 효율성에 문제가 있다.

그래서 주로 2번 방식을 쓴다고 한다.

#include <pthread.h>
int pthread_detach(pthread_t thread);
-> 성공시 0, 실패시 0 이외의 값 반환
-thread 종료와 동시에 소멸시킬 쓰레드의 ID 정보 전달

위 함수를 호출했다고 즉시, 종료되거나 블로킹 상태에 놓이지는 않는다. 
하지만 메모리의 소멸을 유도할 수 있다.

이번 강은 무지하게 길었다 -_-;; 지금도 엄청 줄이고 줄인 것이다.
역시 직접 소스를 다운 받아 해보는 것이 더욱 중요하다.

2015년 1월 4일 일요일

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

TCP/IP는 내가 처음으로 해보는 공부라 그런지 영 더디게 강이 지나가곤 한다.
학교 다닐 때 리눅스 좀 해봤어야 했는데 ㅡㅜ
물론 집에서 개인적으로 깔아보고 이것저것 만져보다가
게임도 안되고, 재미도 없어서 깔았다 지웠다를 반복했었다.
그러다 회사에서 갑자기 하게 되어 무지 고생했던 기억이 있다.
그때는 어찌어찌 복사 붙이기 신공으로 넘겼지만 아찔했던 기억이 있다.
2015년 새해가 밝았으니 다시 기운 차리고 해보고자 한다.

이번 17강은 select 보다 나은 epoll 이라는 것을 배우고자 한다.
그리고 내용이 길어서 함수랑 약간의 설명만 하겠다.
소스는 가장 아래 소스 받기 링크에서 받아서 하길 바란다 -_-;

17-1 : epoll 의 이해와 활용


select 는 오래 전에 개발된 멀티플랙싱 기법이라고 한다.
이 기법은 접속자 수가 100명만 넘어가도 버벅대는 기법이다.

select 기반의 IO 멀티플렉싱이 느린이유
- select 함수호출 이후에 항상 등장하는, 모든 파일 디스크립터를 대상으로 하는 반복문
-select 함수를 호출할 때마다 인자로 매번 전달해야 하는 관찰 대상에 대한 정보들

select 함수를 호출할 때마다 관찰 대상에 대한 정보를 매번 운영체제에게 전달해야 한다.
이것을 극복하고자 epoll 이라는 것이 생겼다.

-상태 변화의 확인을 위한, 전체 파일 디스크립터를 대상으로 하는 반복문이 필요 없다.
-select 함수에 대응하는 epoll wait 함수호출 시, 관찰대상의 정보를 매번 전달할 필요가 없다.

epoll 함수는 3개가 있다.
-epoll_create : epoll 파일 디스크립터 저장소 생성
-epoll_ctl : 저장소에 파일 디스크립터 등록 및 삭제
-epoll_wait : select 함수와 마찬가지로 파일 디스크립터의 변화를 대기한다.

epoll 함수는 리눅스 커널 2.5.44 이상에서 부터 쓸 수 있다.

리눅스 커널 버전 확인 명령어다.

cat /proc/sys/kernel/osrelease

각 함수를 자세히 보자.
epoll_create

#include <sys/epoll.h>
int epoll_create(int size);
-> 성공 시 epoll 파일 디스크립터, 실패시 -1 반환
-size : epoll 인스턴스의 크기 정보.

위 함수 호출시 생성되는 파일 디스크립터의 저장소를 가리켜 'epoll 인스턴스'라 한다.
그리고 매개 변수로 통해서 전달되는 값은 epoll 인스턴스 크기를 결정하는 정보로 사용된다. 하지만 이 값은 리눅스 커널 2.6.8 이후에는 무시된다.
그 전 버전에서는 그냥 리눅스 보고 이렇게 할당해줘라고 요청을 하는 것이지만
실제는 커널 맘대로 정한다(-_-);

epoll_ctl

#incldude <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
-> 성공시 0, 실패시 -1 반환
-epfd : 관찰 대상을 기록할 epoll 인스턴스의 파일 디스크립터
-op : 관찰 대상의 추가, 삭제 또는 변경여부 지정
-fd : 등록할 관찰 대상의 파일 디스크립터
-event : 관찰 대상의 관찰 이벤트 유형

살짝 복잡해 보이지만 예를 들면 쉽다.

epoll_ctl(A, EPOLL_CTL_ADD, B, C);

"인스턴스 A에, 파일 디스크립터 B를 등록하되, C를 통해 전달된 이벤트의 관찰을 목적으로 등록하라"

epoll_ctl(A, EPOLL_CTL_DEL, B, NULL);

"epoll 인스턴스 A에서 파일 디스크립터 B를 삭제한다."

epoll_ctl 에서 4번째 인자인 event의 유형을 정리한다

-EPOLLIN : 수신할 뎅이터가 존재하는 상황
-EPOLLOUT : 출력버퍼가 비워져서 당장 데이터를 전송할 수 있는 상황
-EPOLLPRI : OOB 데이터가 수신된 상황
-EPOLLRDHUP : 연결이 종료되거나 Half-close 가 진행된 상황, 이는 엣지 트리거 방식에서 유용하게 사용될 수 있다.
-EPOLLERR : 에러가 발생한 상황
-EPOLLET : 이벤트의 감지를 엣지 트리거 방식으로 동작 시킨다.
-EPOLLONESHOT : 이벤트가 한번 감지 되면, 해당 파일 디스크립터에서는 더이상 이벤트를 발생시키지 않는다. 따라서  epoll_ctl 함수의 두번 째 인자로 EPOLL_CTL_MOD를 전달해서 이벤트를 재설정 해야한다.

epoll_wait

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
-> 성공시 이벤트가 발생한 파일 디스크립터의 수, 실패시 -1 반환
-epfd : 이벤트 발생의 관찰영역인 epoll 인스턴스의 파일 디스크립터
-events : 이벤트가 발생한 파일 디스크립터가 채워질 버퍼의 주소 값
-maxevents : 두번째 인자로 전달된 주소 값의 버펄 등록 가능한 최대 이벤트 수
-timeout : 1/1000초 단위의 대기 시간, -1 전달시, 이벤트가 발생할 때까지 무한 대기

17-2 : 레벨 트리거, 엣지 트리거


레벨 트리거와 엣지 트리거의 차이는 이벤트가 발생하는 시점이다.

"레벨 트리거는 입력 버퍼에 데이터가 남아있는 동안 계속 해서 이벤트가 등록 된다."
"엣지 트리거는 입력 버퍼로 데이터가 수신된 상황에서 딱 한번만 이벤트가 등록 된다."

엣지 트리거 기반의 서버 구현을 위해서는 두가지를 알아야 한다.

-변수 errno를 이용한 오류의 원인을 확인하는 방법
-넌_블록킹(Non-blocking) IO를 위한 소켓의 특성을 변경하는 방법

일반적으로 리눅스에서 제공하는 소켓 관련 함수는 -1을 반환함으로써 오류의 발생을 알린다. 오류의 발생을 인식은 할 수는 있으나 오류의 원인을 정확히 확인할 수 없다.
그래서 추가적인 정보 제공을 위해 int errno 라는 변수를 전역으로 선언해 놓고 있다.

이 변수를 확인해서 어떤 오류인지 추측하는 것이다.

이번에는 소켓을 넌-블록킹 모드로 변경하는 법을 알아보자.

#include <fcntl.h>
int fcntl(int filedes, int cmd, .....);
-> 성공시 매개변수 cmd에 따른 값, 실패시 -1 반환
-filedes : 특성 변경의 대상이 되는 파일의 파일 디스크립터 전달
-cmd : 함수 호출의 목적에 해당하는 정보 전달

위 함수는 가변인자의 형태로 정의되어 있다. 
소켓을 넌-블록킹 모드로 변경하기 위해서는 다음의 두 문장을 실행하면 된다.

int flag =  fctl(fd, F_GETFL, 0);
fctl(fd, F_SETFL, flag | O_NONBLOCK);

이번 강은 내가 몸이 안좋아서 그런지 내용이 긴건지 유난히 머리에 안들어왔다.

역시 소스 코드를 다운 받아서 직접 해보는 것이 이해가 빠를 것이다.
소스 코드 받기

2015년 1월 2일 금요일

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

2015년이다~ 아아 원래 2014년 안에 다 끝내고 1월은 조직응용기사 실기 공부할려고 했는데........;;

그래도 막 공부할 순 없으니 차근차근 공부하면서 자격증 준비도 병행하려고 한다.
10점차로 떨어졌으니 이번에는 붙겠지.....

하여튼 이번 강은 "입출력 스트림의 분리에 대한 나머지 이야기"다

16-1 : 입쳑 스트림과 출력 스트림의 분리


첫번째 분리 - TCP의 입출력 루틴 분할(fork 함수 호출)
두번째 분리 - FILE 포인터를 이용한 분리(fdopen 함수 호출)

첫번째 분리의 이점
-입력루틴(코드)과 출력루틴의 독립을 통한 구현의 편의성 증대
-입력에 상관없이 출력이 가능하게 함으로 속도 향상

두번째 분리의 이점
-FILE 포인터는 읽기 모드와 쓰기모드를 구분해야 해서 구현의 편의성 증대
-입력 버처와 출력 버퍼를 구분함으로 인한 버퍼링 기능의 향상

fdopen 함수 호출 기반의 Half-close 기반의 EOF 전달 방법은 조금 다르다

소스를 보자.(일부만 볼 것이다.)

bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr));
listen(serv_sock, 5);
clnt_adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);
//읽기모드(r), 쓰기모드(w)의 FILE 포인터를 생성한다.
readfp=fdopen(clnt_sock, "r");
writefp=fdopen(clnt_sock, "w");
//클라이언트로 문자열 데이터를 전송하고 fflush 함수호출을 통해 전송을 마무리 한다.
fputs("FROM SERVER: Hi~ client? \n", writefp);
fputs("I love all of the world \n", writefp);
fputs("You are awesome! \n", writefp);
fflush(writefp);
//FILE 포인터를 대상으로 fclose 함수를 호출한다. 소켓을 종료시키면 EOF가 전송된다.
fclose(writefp); //쓰기모드 종료
fgets(buf, sizeof(buf), readfp); fputs(buf, stdout); 
fclose(readfp); //읽기 모드 종료
return 0;

fclose 함수가 호출되면 EOF가 전달은 된다. 하지만 위의 소스를 적용시켜서 실행해보면
마지막 문자열을 수신하지 못하고 종료되어 버린다.

16-2 : 파일 디스크립터의 복사와 Half-close

위의 소스에서 쓰기 모드와 읽기 모드를 따로따로 종료했는데 완전 종료가 된 이유는
파일 디스크립터가 1개였기 때문이다.
파일 디스크립터가 종료되면 소켓도 같이 종료가 된다.

쓰기 모드를 종료하는 시점에 연결되어 있던 파일 디스크립터도 종료되면서
소켓도 종료되어서, 뒤에 남은 읽기 모드의 메세지가 전송되지 않고 종료되어 버린 것이다.

이것을 해결하기 위해서 파일 디스크립터를 복사해서 해결한다.

파일 디스크립터의 복사
단순한 복사가 되면 의미가 없고, "동일한 파일 또는 소켓의 접근을 위한" 또다른 파일 디스크립터의 생성이다.

파일 디스크립터의 복사 함수를 보자.

#include <unistd.h>

int dup(int fildes);
int dup2(int fildes, int fildes2);
-> 성공시 복사된 파일 디스크립터, 실패시 -1 반환
-fildes : 복사할 파일 디스크립터 전달
-fildes2 : 명시적으로 지정할 파일 디스크립터의 정수 값 전달

위 함수를 사용한 소스를 보자.
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
int cfd1, cfd2;
char str1[]="Hi~ \n";
char str2[]="It's nice day~ \n";
//파일 디스크립터 1 복사
cfd1=dup(1);
//복사한 1을 재복사 하고 7할당
cfd2=dup2(cfd1, 7);
//복사 확인
printf("fd1=%d, fd2=%d \n", cfd1, cfd2);
write(cfd1, str1, sizeof(str1));
write(cfd2, str2, sizeof(str2));

close(cfd1); //파일 디스크립터 1 종료(복사본)
close(cfd2); //파일 디스크립터 7 종료
write(1, str1, sizeof(str1));
close(1); //파일 디스크립터 1 종료(원본)
write(1, str2, sizeof(str2)); // 이 출력은 실행 안됨
return 0;
}

소스를 분석 해보면 어찌 사용하는지 알 수 있을 것이다.

그럼 위의 것을 통합해서 소스에 적용해보자.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024

int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
FILE * readfp;
FILE * writefp;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
char buf[BUF_SIZE]={0,};

serv_sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr));
listen(serv_sock, 5);
clnt_adr_sz=sizeof(clnt_adr); 
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);
//fdopen 함수호출을 통해 FILE 포인터를 생성
readfp=fdopen(clnt_sock, "r");
writefp=fdopen(dup(clnt_sock), "w");
fputs("FROM SERVER: Hi~ client? \n", writefp);
fputs("I love all of the world \n", writefp);
fputs("You are awesome! \n", writefp);
fflush(writefp);
//fileno 함수 호출시 반환되는 파일 디스크립터를 대상으로 shutdown 함수 호출
//half-close가 진행되어 클라이언트로 EOF가 전달 된다.
//shutdown 함수가 호출되면 복사된 파일 디스크립터의 수에 상관 없이 
//half-close가 진행되고 이 과정에서 EOF가 전달된다.
shutdown(fileno(writefp), SHUT_WR);
fclose(writefp);
fgets(buf, sizeof(buf), readfp); fputs(buf, stdout); 
fclose(readfp);
return 0;
}

-실행 결과-
swyoon@my_linux:~/tcpip$ gcc sep_serv2.c -o serv2
swyoon@my_linux:~/tcpip$ ./serv2 9190
FROM CLIENT: Thank you! 

복사된 파일 디스크립터의 수에 상관 없이 
EOF의 전송을 동반하는 half-close 를 진행하기 위해서는 
shutdown 함수를 호출해야 한다.

이번 강도 끝났다. 역시 직접 소스를 다운 받아서 직접 해보는 것이 도움이 된다.


다음 17강은 select 보다 나은 epoll을 알아볼 것이다.