네트워크 및 보안/윈도우즈 소켓 통신 프로그램

5. WSAEventSelect를 이용한 멀티플렉싱, 채팅 [TCP/IP 소켓 통신 프로그래밍 with 윈도우즈]

언제나휴일 2016. 4. 7. 12:37
반응형

5. WSAEventSelect를 이용한 멀티플렉싱

 

 이번에는 WSAEventSelect 이용한 멀티플렉싱을 살펴봅시다.

 

 채팅 서버는 클라이언트로부터 메시지를 수신하면 채팅 방에 접속한 모든 클라이언트에게 메시지를 전달해 주어야 합니다. 이와 같은 처리를 위해서는 채팅 방에 접속한 전체 클라이언트 정보를 기억하고 있어야 합니다.

 

 특히 채팅 서버에서는 별도의 쓰레드없이 Listen 소켓에 클라이언트의 연결 요청이 있는지와 클라이언트로부터 수신한 메시지가 있는지 확인하는 작업이 필요합니다.

 

 WSAEventSelect 모델은 특정 소켓에 특정 사건이 발생하였는지 확인하기 위한 이벤트 개체를 만들어 처리하는 모델입니다. 채팅 서버를 예를 들면 Listen 소켓에 연결 요청이 있을 때 신호 상태로 전이할 이벤트 개체를 생성합니다.


WSAEventSelect 모델 – Listen 소켓과 매핑할 이벤트 개체 생성

[그림 5.1] WSAEventSelect 모델 – Listen 소켓과 매핑할 이벤트 개체 생성

 

 그리고 반복문에서 현재까지의 이벤트 개체들 중에 하나라도 신호 상태로 전이할 때까지 대기합니다. 대기 상태가 끝나면 어느 이벤트 개체가 신호 상태인지 확인하여 해당 소켓에 준비한 사건을 처리합니다. 그리고 다시 반복합니다.


WSAEventSelect 모델

[그림 5.2] WSAEventSelect 모델

 

 

 만약 Listen 소켓과 매핑한 이벤트 개체가 신호 상태이면 클라이언트의 연결 요청을 수락합니다. 그리고 수락(accept) 함수가 반환한 송수신 소켓에 메시지 수신 혹은 소켓 닫기 이벤트가 발생할 때 신호 상태로 전이할 이벤트 개체를 생성합니다. 물론 새로 생성한 이벤트 개체는 반복문에서 대기하는 이벤트 개체 집합에 추가합니다.

 

WSAEventSelect – 클라이언트 연결 요청

[그림 5.3] WSAEventSelect – 클라이언트 연결 요청

 

 

 만약 송수신 소켓과 매핑한 이벤트 개체가 신호 상태이면 신호 상태로 전이한 이유가 메시지 수신 때문인지 소켓 닫기 때문이지 확인합니다. 메시지 수신 때문이면 recv 함수를 호출하여 메시지를 수신한 후에 채팅 방에 접속한 모든 클라이언트에게 메시지를 send 합니다. 그리고 다시 반복문의 대기 구문을 수행합니다.


 WSAEventSelect 클라이언트로부터 메시지를 수신하였을 때

 

[그림 5.4] WSAEventSelect 클라이언트로부터 메시지를 수신하였을 때

 

 그리고 송수신 소켓과 매핑한 이벤트 개체가 신호 상태이고 신호 상태로 전이한 이유가 소켓 닫기 이벤트일 때는 해당 소켓을 닫고 매핑한 이벤트 개체를 제거하고 대기하는 이벤트 개체 집합에서도 제거합니다. 그리고 다시 반복문의 대기 구문을 수행합니다.

 

5.1 채팅 서버 구현

 먼저 포트 번호와 백로그 큐 크기 및 메시지 크기를 매크로 상수로 정의합시다.

#include "common.h"

#define PORT_NUM      10200

#define BLOG_SIZE       5

#define MAX_MSG_LEN 256

 

 진입점에서는 윈속을 초기화하고 대기 소켓을 설정한 후에 이벤트 처리하는 루프를 수행합니다. 그리고 윈속을 해제합니다.

SOCKET SetTCPServer(short pnum,int blog);//대기 소켓 설정

void EventLoop(SOCKET sock);//Event Loop

int main()

{

    WSADATA wsadata;

    WSAStartup(MAKEWORD(2,2),&wsadata);//윈속 초기화      

    SOCKET sock = SetTCPServer(PORT_NUM,BLOG_SIZE);//대기 소켓 설정

    if(sock == -1){    perror("대기 소켓 오류");    }

    else{    EventLoop(sock);   }

    WSACleanup();//윈속 해제화

    return 0;

}

 

 대기 소켓을 설정하는 루틴은 차이가 없습니다.

SOCKET SetTCPServer(short pnum,int blog)

{

    SOCKET sock;

    sock = socket(AF_INET, SOCK_STREAM,IPPROTO_TCP);//소켓 생성

    if(sock == -1){    return -1;    }

 

    SOCKADDR_IN servaddr={0};//소켓 주소

    servaddr.sin_family = AF_INET;

    servaddr.sin_addr = GetDefaultMyIP();

    servaddr.sin_port = htons(PORT_NUM);

    int re = 0;

    re = bind(sock,(struct sockaddr *)&servaddr,sizeof(servaddr));//소켓 주소와 네트워크 인터페이스 결합

    if(re == -1){    return -1;    }

 

    re = listen(sock,blog);//백 로그 큐 설정

    if(re == -1){    return -1;    }

    return sock;

}

 

 소켓을 보관할 배열과 이벤트 핸들을 보관할 배열을 선언합니다. 그리고 현재 배열에 보관한 원소 개수를 기억할 변수도 선언합니다.

SOCKET  sock_base[FD_SETSIZE];

HANDLE hev_base[FD_SETSIZE];

int cnt;

 

 특정 소켓에 매핑할 네트워크 이벤트 개체를 생성하여 배열에 보관하는 함수를 정의합시다. 입력 인자로 소켓과 네트워크 이벤트 종류를 전달받습니다.

HANDLE AddNetworkEvent(SOCKET sock, long net_event)

{

 네트워크 이벤트 개체를 생성합니다.

    HANDLE hev = WSACreateEvent();

 그리고 소켓과 이벤트 개체 핸들을 배열에 보관합니다.

    sock_base[cnt] = sock;

    hev_base[cnt] = hev;

 보관한 원소 개수를 1 증가합니다.

    cnt++;

 이제 제일 중요한 작업입니다. 소켓과 해당 이벤트 개체를 매핑하는 WSAEventSelect 함수를 호출합니다. 이함수를 호출하면 해당 소켓에 설정한 네트워크 이벤트가 발생하면 이벤트 개체를 신호 상태로 전이하도록 설정합니다.

    WSAEventSelect(sock,hev,net_event);

    return hev;

}

 

 이벤트 루프 함수를 작성합시다. 여기에서는 처리할 이벤트 종류로 연결 수락과 메시지 수신, 연결 종료가 있습니다.

void AcceptProc(int index);

void ReadProc(int index);

void CloseProc(int index);

void EventLoop(SOCKET sock)

{

 제일 먼저 Listen 소켓에 클라이언트 연결 요청이 왔을 때 처리하기 위한 이벤트 개체를 추가합니다. 연결 요청에 관한 네트워크 이벤트 상수는 FD_ACCEPT입니다.

    AddNetworkEvent(sock,FD_ACCEPT);

 그리고 이벤트 처리 루프를 작성합니다.

    while(true)

    {

 이벤트 처리 루프에서는 제일 먼저 현재 이벤트 배열에 추가한 원소들 중에 신호 상태가 발생할 때까지 대기하는 작업을 수행합니다.

        int index =WSAWaitForMultipleEvents(cnt,hev_base,false,INFINITE,false);

        WSANETWORKEVENTS net_events;

 그리고 어떠한 이유로 신호상태로 바뀐 것인지 확인한다.

        WSAEnumNetworkEvents(sock_base[index],hev_base[index],&net_events);

 

 이제 네트워크 이벤트 종류에 따라 처리할 함수를 호출합니다.

        switch(net_events.lNetworkEvents)

        {

        case FD_ACCEPT: AcceptProc(index); break;

        case FD_READ: ReadProc(index); break;

        case FD_CLOSE: CloseProc(index); break;

        }

    }

 이벤트 처리 루프가 끝나면 소켓을 닫습니다. 실제로 이벤트 처리 루프는 무한 루프이므로 이 코드까지 진행하지는 않습니다.

    closesocket(sock);//소켓 닫기

}

 

 먼저 연결 요청에 관한 처리를 수행합시다.

void AcceptProc(int index)

{

    SOCKADDR_IN cliaddr={0};

    int len = sizeof(cliaddr);

 클라이언트 연결 요청을 수락합니다.

    SOCKET dosock = accept(sock_base[0],(SOCKADDR *)&cliaddr,&len);

 WSAEventSelect에서는 최대 처리할 수 있는 소켓(이벤트) 수가 정해져 있어서 필터링 작업이 필요합니다.

    if(cnt ==FD_SETSIZE)

    {

        printf("채팅 방에 꽉 차서 %s:%d 입장하지 못하였음!\n",

                inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));

        closesocket(dosock);

        return;

    }

 연결 요청을 수락하여 반환한 송수신 소켓에 메시지 수신 혹은 연결을 닫으면 신호 상태로 전이할 네트워크 이벤트를 추가합니다.

    AddNetworkEvent(dosock,FD_READ|FD_CLOSE);

 확인할 수 있게 누가 채팅 방에 입장하였는지 콘솔 화면에 출력할게요.

    printf("%s:%d 입장\n",inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));

}

 

 

 이번에는 메시지를 수신하였을 때 처리하는 함수를 작성합시다.

void ReadProc(int index)

{

 먼저 메시지를 수신합니다.

    char msg[MAX_MSG_LEN];

    recv(sock_base[index],msg,MAX_MSG_LEN,0);

 수신한 클라이언트 정보를 확인합니다.

    SOCKADDR_IN cliaddr={0};

    int len = sizeof(cliaddr);

    getpeername(sock_base[index],(SOCKADDR *)&cliaddr,&len);

 수신한 클라이언트 정보와 수신한 메시지 정보를 전송할 메시지 버퍼에 출력합니다.

    char smsg[MAX_MSG_LEN];

    sprintf(smsg,"[%s:%d]:%s",inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port),msg);

 채팅방에 접속한 모든 클라이언트에게 메시지를 전송합니다.

    for(int i = 1; i<cnt; i++)

    {

        send(sock_base[i],smsg,MAX_MSG_LEN,0);

    }

}

 

 연결 종료 처리하는 함수를 작성합시다.

void CloseProc(int index)

{

    SOCKADDR_IN cliaddr={0};

    int len = sizeof(cliaddr);

 확인하기 쉽게 채팅 방을 나간 클라이언트 정보를 출력합시다.

    getpeername(sock_base[index],(SOCKADDR *)&cliaddr,&len);

    printf("[%s:%d]  님 나감~\n",inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));       

 소켓을 닫습니다.

    closesocket(sock_base[index]);

 그리고 네트워크 이벤트 개체를 닫습니다.

    WSACloseEvent(hev_base[index]);

 소켓 배열과 네트워크 이벤트 배열에서 해당 요소를 제거합니다. 여기에서는 맨 뒤에 원소를 지울 원소가 있는 위치에 덮어 씌우는 형태로 작성하였습니다.

    cnt--;

    sock_base[index] = sock_base[cnt];

    hev_base[index] = hev_base[cnt];

 채팅 방에 남아있는 나머지 클라이언트들에게 방을 나간 클라이언트 정보를 전송합니다.

    char msg[MAX_MSG_LEN];

    sprintf(msg,"[%s:%d]님 나감~\n",inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));

    for(int i = 1; i<cnt; i++){    send(sock_base[i],msg,MAX_MSG_LEN,0);    }

}

 

#include "common.h"

#define PORT_NUM      10200

#define BLOG_SIZE       5

#define MAX_MSG_LEN 256

SOCKET SetTCPServer(short pnum,int blog);//대기 소켓 설정

void EventLoop(SOCKET sock);//Event 처리 Loop

 

int main()

{

    WSADATA wsadata;

    WSAStartup(MAKEWORD(2,2),&wsadata);//윈속 초기화           

    SOCKET sock = SetTCPServer(PORT_NUM,BLOG_SIZE);//대기 소켓 설정

    if(sock == -1){    perror("대기 소켓 오류");    }

    else{    EventLoop(sock);    }

    WSACleanup();//윈속 해제화

    return 0;

}

SOCKET SetTCPServer(short pnum,int blog)

{

    SOCKET sock;

    sock = socket(AF_INET, SOCK_STREAM,IPPROTO_TCP);//소켓 생성

    if(sock == -1){    return -1;    }

 

    SOCKADDR_IN servaddr={0};//소켓 주소

    servaddr.sin_family = AF_INET;

    servaddr.sin_addr = GetDefaultMyIP();

    servaddr.sin_port = htons(PORT_NUM);

 

    int re = 0;

    re = bind(sock,(struct sockaddr *)&servaddr,sizeof(servaddr));//소켓 주소와 네트워크 인터페이스 결합

    if(re == -1){    return -1;    }

 

    re = listen(sock,blog);//백 로그 큐 설정

    if(re == -1){    return -1;    }

    return sock;

}

 

SOCKET  sock_base[FD_SETSIZE];

HANDLE hev_base[FD_SETSIZE];

int cnt;

 

HANDLE AddNetworkEvent(SOCKET sock, long net_event)

{

    HANDLE hev = WSACreateEvent();

 

    sock_base[cnt] = sock;

    hev_base[cnt] = hev;

    cnt++;

 

    WSAEventSelect(sock,hev,net_event);

    return hev;

}

void AcceptProc(int index);

void ReadProc(int index);

void CloseProc(int index);

void EventLoop(SOCKET sock)

{

    AddNetworkEvent(sock,FD_ACCEPT);

 

    while(true)

    {

        int index =WSAWaitForMultipleEvents(cnt,hev_base,false,INFINITE,false);

        WSANETWORKEVENTS net_events;

        WSAEnumNetworkEvents(sock_base[index],hev_base[index],&net_events);

        switch(net_events.lNetworkEvents)

        {

        case FD_ACCEPT: AcceptProc(index); break;

        case FD_READ: ReadProc(index); break;

        case FD_CLOSE: CloseProc(index); break;

        }       

    }

    closesocket(sock);//소켓 닫기

}

 

void AcceptProc(int index)

{   

    SOCKADDR_IN cliaddr={0};

    int len = sizeof(cliaddr);

    SOCKET dosock = accept(sock_base[0],(SOCKADDR *)&cliaddr,&len);

 

    if(cnt ==FD_SETSIZE)

    {

        printf("채팅 방에 꽉 차서 %s:%d 입장하지 못하였음!\n",

                    inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));

        closesocket(dosock);

        return;

    }

 

    AddNetworkEvent(dosock,FD_READ|FD_CLOSE);

    printf("%s:%d 입장\n",inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));

}

 

void ReadProc(int index)

{

    char msg[MAX_MSG_LEN];

    recv(sock_base[index],msg,MAX_MSG_LEN,0);

 

    SOCKADDR_IN cliaddr={0};

    int len = sizeof(cliaddr);

    getpeername(sock_base[index],(SOCKADDR *)&cliaddr,&len);

 

    char smsg[MAX_MSG_LEN];

    sprintf(smsg,"[%s:%d]:%s",inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port),msg);

 

    for(int i = 1; i<cnt; i++)

    {

        send(sock_base[i],smsg,MAX_MSG_LEN,0);

    }

}

 

 

void CloseProc(int index)

{

    SOCKADDR_IN cliaddr={0};

    int len = sizeof(cliaddr);

    getpeername(sock_base[index],(SOCKADDR *)&cliaddr,&len);

    printf("[%s:%d]  님 나감~\n",inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));

 

    closesocket(sock_base[index]);

    WSACloseEvent(hev_base[index]);

 

    cnt--;

    sock_base[index] = sock_base[cnt];

    hev_base[index] = hev_base[cnt];

 

    char msg[MAX_MSG_LEN];

    sprintf(msg,"[%s:%d]님 나감~\n",inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));

    for(int i = 1; i<cnt; i++)

    {

        send(sock_base[i],msg,MAX_MSG_LEN,0);

    }

}

[소스 5.1]  채팅 서버 - WSAEventSelect를 이용한 멀티플렉싱

  

5.2 채팅 클라이언트 구현

 이번에는 채팅 클라이언트를 구현합시다. 먼저 서버에 약속한 포트 번호와 메시지 크기를 매크로 상수로 정의하고 서버의 IP 주소를 정의합시다.

#define PORT_NUM      10200

#define MAX_MSG_LEN 256

#define SERVER_IP        "192.168.34.50"

 

int main()

{

 먼저 윈속을 초기화합니다.

    WSADATA wsadata;

    WSAStartup(MAKEWORD(2,2),&wsadata);//윈속 초기화      

 그리고 소켓을 생성합니다.

    SOCKET sock;

    sock = socket(AF_INET, SOCK_STREAM,IPPROTO_TCP);//소켓 생성

    if(sock == -1){    return -1;    }

 서버에 연결 요청합니다.

    SOCKADDR_IN servaddr={0};//소켓 주소

    servaddr.sin_family = AF_INET;

    servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);

    servaddr.sin_port = htons(PORT_NUM);

    int re = 0;

    re = connect(sock,(struct sockaddr *)&servaddr,sizeof(servaddr));//연결 요청

    if(re == -1){    return -1;    }

 서버와 연결한 후에는 사용자가 입력한 문자열을 서버에 전송하는 작업과 서버로부터 수신한 정보를 콘솔 화면에 출력하는 작업이 있습니다. 이 두 개의 작업은 서로 독립적으로 수행해야 합니다. 여기에서는 서버로부터 수신한 정보를 콘솔 화면에 출력하는 작업을 별도의 스레드 진입점을 작성하여 처리하기로 합시다.

    _beginthread(RecvThreadPoint,0,(void *)sock);

 사용자로부터 문자열을 입력받아 전송합니다. 만약 입력한 문자열이 "exit"이면 종료하기로 합시다.

    char msg[MAX_MSG_LEN]="";

    while(true)

    {

        gets_s(msg,MAX_MSG_LEN);       

        send(sock,msg,sizeof(msg),0);//송신

        if(strcmp(msg,"exit")==0){    break;    }

    }

 소켓을 닫고 윈속을 해제화합니다.

    closesocket(sock);//소켓 닫기   

    WSACleanup();//윈속 해제화

    return 0;

}

 

 서버로부터 메시지를 수신하여 콘솔 화면에 출력하는 스레드 진입점 함수를 작성합시다.

void RecvThreadPoint(void *param)

{

 스레드 진입점 인자로 전달받은 소켓을 참조합니다.

    SOCKET sock = (SOCKET)param;

    char msg[MAX_MSG_LEN];

 

 수신한 메시지 길이가 0보다 크면 수신한 정보를 출력하는 것을 반복합니다.

    while(recv(sock,msg,MAX_MSG_LEN,0)>0)

    {

        printf("%s\n",msg);

    }

 수신한 메시지 길이가 0보다 작거나 같으면 연결이 끊겼거나 오류가 발생한 것이므로 소켓을 닫습니다.

    closesocket(sock);

}

 

#include "common.h"

#define PORT_NUM      10200

#define MAX_MSG_LEN 256

#define SERVER_IP        "192.168.34.50"

 

void RecvThreadPoint(void *param);

int main()

{

    WSADATA wsadata;

    WSAStartup(MAKEWORD(2,2),&wsadata);//윈속 초기화           

   

    SOCKET sock;

    sock = socket(AF_INET, SOCK_STREAM,IPPROTO_TCP);//소켓 생성

    if(sock == -1){    return -1;    }

 

    SOCKADDR_IN servaddr={0};//소켓 주소

    servaddr.sin_family = AF_INET;

    servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);

    servaddr.sin_port = htons(PORT_NUM);

 

    int re = 0;

    re = connect(sock,(struct sockaddr *)&servaddr,sizeof(servaddr));//연결 요청

    if(re == -1){    return -1;    }

    _beginthread(RecvThreadPoint,0,(void *)sock);

    char msg[MAX_MSG_LEN]="";

    while(true)

    {

        gets_s(msg,MAX_MSG_LEN);       

        send(sock,msg,sizeof(msg),0);//송신

        if(strcmp(msg,"exit")==0)

        {

            break;

        }

    }   

    closesocket(sock);//소켓 닫기   

 

    WSACleanup();//윈속 해제화

    return 0;

}

void RecvThreadPoint(void *param)

{

    SOCKET sock = (SOCKET)param;

    char msg[MAX_MSG_LEN];

   

    SOCKADDR_IN cliaddr={0};

    int len = sizeof(cliaddr);

 

    while(recv(sock,msg,MAX_MSG_LEN,0)>0)

    {

        printf("%s\n",msg);

    }

    closesocket(sock);

}

 

[소스 5.2]  채팅 클라이언트

반응형