5.1 채팅 서버 구현 [TCP/IP 소켓 프로그래밍 with 윈도우즈]

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

*common.h와 common.c 소스 코드 확인 *

#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(pnum);
    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);
    }
}