본문 바로가기
Programming/열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)

Ch 21. 내용 확인문제

by minjunkim.dev 2020. 8. 15.

    모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!


01. 동기 입출력과 비동기 입출력이 무엇인지, send & recv 함수를 기준으로 설명해보자. 그리고 동기 입출력의 단점은 무엇이고 이것이 비동기 입출력을 통해서 어떻게 해결이 되는지도 설명하자.

 

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

 

- 동기식 입출력 : 입출력 함수의 반환시점과 데이터 송수신의 완료시점이 일치하는 경우

1) send 함수를 호출하면 데이터 전송 시작

2) 출력버퍼로 데이터 전송이 완료되면 send 함수 반환

3) recv 함수를 호출하면 데이터 수신 시작

4) 원하는만큼 데이터 수신을 하면 recv 함수 반환

 

- 그렇다면 비동기 입출력이 의미하는 바는 무엇인가?

: 입출력 함수의 반환시점과 데이터 송수신의 완료시점이 일치하지 않는 경우

1) send 함수를 호출하면 데이터 전송이 시작되고, send 함수는 반환됨

2) 이후에 출력버퍼로 데이터 전송이 완료됨

3) recv 함수를 호출하면 데이터 수신이 시작되고, recv 함수는 반환됨

4) 이후에 원하는만큼 데이터 수신이 완료됨

 

# 동기화된 입출력의 단점과 비동기의 해결책

- 동기 입출력의 단점 : 입출력이 진행되는 동안 호출된 함수가 반환을 하지 않으니, 다른 일을 할 수가 없음

- 비동기 입출력 : 데이터의 송수신 완료에 상관없이, 호출된 함수가 반환을 하므로 다른 일을 진행할 수 있음

- 비동기 방식은 동기 방식에 비해 보다 효율적으로 CPU를 활용하는 모델이 됨,

이를 가리켜 동기 방식의 단점을 극복한 모델이라 하는 것


02. 모든 경우에 있어서 비동기 입출력이 최선의 선택이 될 수는 없다. 그렇다면 비동기 입출력의 단점은 무엇인가? 그리고 어떠한 경우에 동기 입출력이 좋은 선택이 될 수 있겠는가? 이에 대한 답을 내리기 위해서 비동기 입출력 관련 소스코드를 참고하기 바라며, 쓰레드와 관련해서도 의견을 제시하기 바란다.

- 비동기 입출력의 특징은 입출력의 완료를 이후에 확인해야 한다는 것이다. 이는 서버의 서비스 형태가 매우 간단하고, 응답에 필요한 데이터의 크기가 작은 경우에는 불편하게 느껴질 수 있다.

 

- 클라이언트 하나당 쓰레드를 하나씩 생성하는 서버 모델에서는 굳이 비동기 입출력의 필요성이 없다.


03. select 방식과 관련된 다음 설명이 맞으면 O, 틀리면 X를 표시해보자.

 

- select 방식은 호출된 함수의 반환을 통해서 IO 관련 이벤트의 발생을 알리니, Nontification IO 모델이라 할 수 있다. O

 

- select 방식은 IO 관련 이벤트의 발생시점과 호출된 함수의 반환시점이 일치하기 때문에 비동기 모델이 아니다.

O

 

- WSAEventSelect 함수는 select 방식의 비동기 모델이라 할 수 있다. IO 관련 이벤트의 발생을 비동기의 형태로 알리기 때문이다. O


04. select 함수를 이용하는 방식과 WSAEventSelect 함수를 이용하는 방식의 차이점을 소스코드의 관점에서 설명해보자.

 

- select 함수

1) 관찰의 대상이 되는 핸들 정보를 함수호출 시마다 매번 전달해야만 함

2) 이벤트가 발생할 때까지 블로킹 상태

 

- WSAEventSelect 함수

1) 관찰 대상의 정보가 운영체제에 등록되고, 운영체제가 이를 기록해두기 때문에 관찰의 대상이 되는 핸들의 정보를 매번 전달할 필요가 없음(리눅스의 epoll과 동일)

2) 함수호출 후 바로 반환하기 때문에, 이벤트가 발생할 때까지 블로킹 상태에 있지 않음

이후에 별도의 과정을 거쳐서 이벤트의 발생유무를 확인하면 됨


05. 챕터 17에서 소개한 epoll은 엣지 트리거 모드와 레벨 트리거 모드로 동작한다. 그렇다면 이 중에서 비동기 입출력이 잘 어울리는 모드는 무엇인가? 그리고 그 이유는 또 무엇인가? 이와 관련해서 포괄적인 답을 해보자.

 

- 비동기 입출력이 잘 어울리는 모드는 "엣지 트리거 모드"

 

- 엣지 트리거 모드는 입력버퍼에 데이터가 남아있다고 해서 매번 이벤트를 등록하지는 않는다. 즉, 엣지 트리거 모드에서는 비동기로 입출력을 진행할 경우, 이미 입출력이 진행중인 상황에 대해서는 이벤트를 등록하지 않기 때문에, 새로 등록되는 이벤트에 대해서만 신경을 쓰면 된다.

 

- 레벨 트리거 모드에서는 입력버퍼에 데이터가 남아있는 상황이면 계속해서 이를 이벤트로 등록한다. 따라서 레벨 트리거 모드에서 비동기로 입출력을 진행할 경우, 새로 발생한 입출력과 처리중인 입출력의 구분이 불가능하기 때문에 코드의 구현이 어렵다.


06. 리눅스의 epoll 역시 비동기 입출력 모델이라 할 수 있다. 그렇다면 이를 비동기 입출력 모델이라 할 수 있는 이유에 대해서 설명 해보자.

 

- epoll 방식도 이벤트의 관찰 대상을 등록하는 과정(epoll_ctl 함수호출)과 이벤트의 발생을 확인하는 과정(epoll_wait 함수호출)이 별도의 함수로 분리되어 있다. 따라서 이벤트가 발생한 이후에 원하는 시점에서 이벤트의 발생유무를 확인하고, 이에 대한 처리를 할 수 있다.


07. WSAWaitForMultipleEvents 함수가 관찰할 수 있는 최대 핸들의 수는 어떻게 확인이 가능한가? 이의 확인을 위한 코드를 작성해서, 이 값을 실제로 확인 해보자.

 

- 매크로 형태로 정의되어 있는 상수 WSA_MAXIMUM_WAIT_EVENTS 값을 확인하면 된다.


08. 비동기 Notification IO 모델에서 Event 오브젝트가 manual-reset 모드이어야 하는 이유를 설명 해보자.

 

step 1) 이벤트의 발생유무를 확인하기 위해서 WSAWaitForMultipleEvents 함수가 호출

=> 이때 이벤트가 발생한 이벤트 커널 오브젝트는 signaled 상태임

 

step 2) 실제 이벤트의 발생대상을 찾기 위해서 WSAWaitForMultipleEvents 반복하여 호출하는데,

manual-reset 모드가 아니면, step 1)에서의 함수 호출 반환 이후, 자동으로 non-signaled 상태로 변경되기 때문에

여기서 WSAWaitForMultipleEvents 함수를 호출해도 이벤트의 발생대상을 찾을 수 없음


09. 이번 챕터에서 설명한 비동기 Notification IO 모델을 바탕으로 채팅 서버를 구현 해보자. 이 채팅 서버는 챕터 20에서 소개한 채팅 클라이언트인 예제 chat_clnt_win.c와 함께 동작이 가능해야 한다.

 

# chat_clnt_win.c

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

#define BUF_SIZE 100
#define NAME_SIZE 20

/* 쓰레드의 main 함수, 입출력 루틴 분할 */
unsigned WINAPI SendMsg(void * arg);
unsigned WINAPI RecvMsg(void * arg);
void ErrorHandling(char * msg);

char name[NAME_SIZE]="[DEFAULT]";
char msg[BUF_SIZE];

int main(int argc, char * argv[])
{
    WSADATA wsaData;
    SOCKET hSock;
    SOCKADDR_IN servAdr;
    HANDLE hSndThread, hRcvThread;
    
    if(argc!=4)// 실행파일의 경로/IP/PORT번호/채팅닉네임 을 입력으로 받아야 함
    {
        printf("Usage: %s <IP> <port> <name> \n", argv[0]);
        exit(EXIT_FAILURE);
    }
    
    if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) // 윈속 라이브러리 초기화
    	ErrorHandling("WSAStartup() error!");
        
    sprintf(name,"[%s]",argv[3]);
    hSock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성
    
    /* 서버 주소정보 초기화 */
    memset(&servAdr,0,sizeof(servAdr));
    servAdr.sin_family=AF_INET;
    servAdr.sin_addr.s_addr=inet_addr(argv[1]);
    servAdr.sin_port=htons(atoi(argv[2]));
    
    /* 서버로 연결요청(진정한 클라이언트 소켓이 됨) */
    if(connect(hSock,(SOCKADDR*)&servAdr,sizeof(servAdr))==SOCKET_ERROR)
        ErrorHandling("connect() error");
    
    /* 쓰레드 생성 및 실행, 입출력 루틴 분할 */
    hSndThread=(HANDLE)_beginthreadex(NULL,0,Sendmsg,(void*)&hSock,0,NULL);
    hRcvThread=(HANDLE)_beginthreadex(NULL,0,Recvmsg,(void*)&hSock,0,NULL);
    
    /* 각 쓰레드가 종료되어 signaled 상태가 될때까지 블로킹 */
    WaitForSingleObject(hSndThread,INFINITE);
    WaitForSingleObject(hSndThread,INFINITE);
    
    closesocket(hSock); // 클라이언트 소켓 소멸
    WSACleanup(); // 윈속 라이브러리 해제
    return 0;
}

unsigned WINAPI SendMsg(void * arg) // send thread main
{
    SOCKET hSock=*((SOCKET*)arg)
    char nameMsg=[NAME_SIZE+BUF_SIZE];
    
    while(1)
    {
        fgets(msg,BUF_SIZE,stdin);
        if(!strcmp(msg,"q\n")||!strcmp(msg,"Q\n"))
        {
            closesocket(hSock); // 클라이언트 소켓 종료
            exit(EXIT_SUCCESS);
        }
        sprintf(nameMsg,"%s %s",name,msg);
        send(hSock,nameMsg,strlen(nameMsg),0); // 채팅닉네임과 채팅내용을 채팅서버로 송신
    }
}

unsigned WINAPI RecvMsg(void * arg) // read thread main
{
    SOCKET hSock=*((SOCKET*)arg)
    char nameMsg=[NAME_SIZE+BUF_SIZE];
    int strLen;
    
    while(1)
    {
        /* 서버로부터 온 데이터를 수신 */
        strLen=recv(hSock,nameMsg,NAME_SIZE+BUF_SIZE-1,0);
        if(strLen==-1)
            return -1;
        nameMsg[strLen]='\0'; // C-str 문자열을 만들기 위해 널문자 추가
        fputs(nameMsg,stdout); // 클라이언트의 표준출력에 문자열 출력
    }
}

void ErrorHandling(char * msg)
{
    fputs(msg, stdout);
    fputc('\n', stdout);
    exit(EXIT_FAILURE);
}

 

# AsynNotiCahtServ_win.c

#include <stdio.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 100

/* 클라이언트로부터 수신한 메시지를 전체 클라이언트에게 송신 */
void SendMsg(SOCKET clntSocks[], int clntCnt, char * msg, int len);

void CompressSockets(SOCKET hSockArr[], int idx, int total);
void CompressEvents(WSAEVENT hEventArr[], int idx, int total);
void ErrorHandling(char *msg);

int main(int argc, char *argv[])
{
	WSADATA wsaData;
	SOCKET hServSock, hClntSock;
	SOCKADDR_IN servAdr, clntAdr;

	SOCKET hSockArr[WSA_MAXIMUM_WAIT_EVENTS]; 
	WSAEVENT hEventArr[WSA_MAXIMUM_WAIT_EVENTS];
	WSAEVENT newEvent;
	WSANETWORKEVENTS netEvents;

	int numOfClntSock=0;
	int strLen, i;
	int posInfo, startIdx;
	int clntAdrLen;
	char msg[BUF_SIZE];
	
	if(argc!=2) // 실행파일의 경로/PORT번호 를 입력으로 받아야 함
        {
		printf("Usage: %s <port>\n", argv[0]);
		exit(EXIT_FAILURE);
	}
    
	if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) // 윈속 라이브러리 초기화
		ErrorHandling("WSAStartup() error!");

	hServSock=socket(PF_INET, SOCK_STREAM, 0); // TCP 소켓 생성
    
    /* 서버 주소정보 초기화 */
    	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]));
   
	/* 서버 주소정보를 기반으로 주소할당 */
	if(bind(hServSock, (SOCKADDR*) &servAdr, sizeof(servAdr))==SOCKET_ERROR)
		ErrorHandling("bind() error");

	/* 서버소켓(리스닝소켓)이 됨 */
	if(listen(hServSock, 5)==SOCKET_ERROR)
		ErrorHandling("listen() error");
	
    /* manual-reset모드,non-signaled상태의 Event 오브젝트 생성 */
	newEvent=WSACreateEvent();
    
    /* 해당 소켓의 이벤트 발생여부를 관찰할 것을 명령 */
	if(WSAEventSelect(hServSock, newEvent, FD_ACCEPT)==SOCKET_ERROR)
		ErrorHandling("WSAEventSelect() error");

	/* 소켓의 핸들과 이와 연결된 Event 커널 오브젝트의 핸들을 대응하여 저장 */
	hSockArr[numOfClntSock]=hServSock;
	hEventArr[numOfClntSock]=newEvent;
    
	++numOfClntSock; // 이벤트 발생여부 관찰대상이 된 소켓의 개수 증가

	while(1)
	{
    	/*
        첫번째로 이벤트가 발생한(signaled 상태가 된)
        Event 커널 오브젝트 관련정보 반환
        */
            posInfo=WSAWaitForMultipleEvents(
					numOfClntSock, hEventArr, FALSE, WSA_INFINITE, FALSE);
		
            startIdx=posInfo-WSA_WAIT_EVENT_0; // 인덱스 계산

            for(i=startIdx; i<numOfClntSock; i++)
            {
        	/* 각 Event 커널 오브젝트에 대해 이벤트가 발생했는지(signaled 상태인지) 확인 */
                 int sigEventIdx=
              WSAWaitForMultipleEvents(1, &hEventArr[i], TRUE, 0, FALSE);
                
                if((sigEventIdx==WSA_WAIT_FAILED || sigEventIdx==WSA_WAIT_TIMEOUT))
                  continue;
                else // 이벤트가 발생했다면
                {
                    sigEventIdx=i; // 해당되는 소켓과 커널 오브젝트의 핸들을 얻기 위한 인덱스

                    /* signaled 상태가 된 원인(이벤트의 종류)을 확인 */
                    WSAEnumNetworkEvents(
                    hSockArr[sigEventIdx], hEventArr[sigEventIdx], &netEvents);

                    if(netEvents.lNetworkEvents & FD_ACCEPT) // 클라이언트의 연결요청 시
                    {
                        /*
                        연결요청한 클라이언트의 연결요청을 수락
                        이를 위해 새로운 소켓을 생성
                        */
                        clntAdrLen=sizeof(clntAdr);
                        hClntSock=accept(
                        hSockArr[sigEventIdx], (SOCKADDR*)&clntAdr, &clntAdrLen);

                        /* manual-reset모드/non-signaled상태의 Event 오브젝트 생성 */
                        newEvent=WSACreateEvent();

                        /*
                        생성된 소켓의 이벤트 발생여부의 관찰을 명령
                        이벤트의 종류는 수신할 데이터가 존재 or 연결의 종료가 요청
                        이벤트가 발생하면 커널 오브젝트는 signaled 상태로 변경됨
                        */
                        WSAEventSelect(hClntSock, newEvent, FD_READ|FD_CLOSE);

                        /* 소켓의 핸들과 이와 연결된 Event 커널 오브젝트의 핸들을 대응하여 저장 */
                        hEventArr[numOfClntSock]=newEvent;
                        hSockArr[numOfClntSock]=hClntSock;

                        ++numOfClntSock; // 이벤트 발생여부 관찰대상의 소켓의 개수 증가
                        puts("connected new client...");
                    }

                    if(netEvents.lNetworkEvents & FD_READ) // 클라이언트로부터 수신할 데이터가 있을 시
                    {
                        /* 클라이언트로부터의 데이터 수신 */
                        strLen=recv(hSockArr[sigEventIdx], msg, sizeof(msg), 0);

                        /* 수신한 데이터를 접속한 모든 클라이언트에게 전달 */
                        SendMsg(hSockArr, numOfClntSock, msg, strLen);
                    }

                    if(netEvents.lNetworkEvents & FD_CLOSE) // 클라이언트가 연결종료를 요청했을 시
                    {
                        /*
                        클라이언트와의 송수신을 위해 생성된 소켓과
                        이와 연결된 Event 커널 오브젝트 소멸
                        */
                        WSACloseEvent(hEventArr[sigEventIdx]);
                        closesocket(hSockArr[sigEventIdx]);

                        --numOfClntSock; // 이벤트 발생여부 관찰대상의 소켓의 개수 감소

                        /*
                        관찰대상에서 제외된 소켓과
                        이와 연결된 Event 커널 오브젝트의 핸들을 배열에서 삭제
                        */
                        CompressSockets(hSockArr, sigEventIdx, numOfClntSock);
                        CompressEvents(hEventArr, sigEventIdx, numOfClntSock);
                    }
                }
           }
       }
       WSACleanup(); // 윈속 라이브러리 해제
       return 0;
}

void SendMsg(SOCKET clntSocks[], int clntCnt, char * msg, int len)   // send to all
{
	int i;
	for(i=0; i<clntCnt; i++)
		send(clntSocks[i], msg, len, 0);
}
void CompressSockets(SOCKET hSockArr[], int idx, int total)
{
	int i;
	for(i=idx; i<total; i++)
		hSockArr[i]=hSockArr[i+1];
}
void CompressEvents(WSAEVENT hEventArr[], int idx, int total)
{
	int i;
	for(i=idx; i<total; i++)
		hEventArr[i]=hEventArr[i+1];
}
void ErrorHandling(char *msg)
{	
	fputs(msg, stderr);
	fputc('\n', stderr);
	exit(EXIT_FAILURE);
}

 

[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어