먼저 포트 번호와 백로그 큐 크기 및 메시지 크기를 매크로 상수로 정의합시다.
*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); } }