블로그 이미지
▶ 홈페이지 : www.solldesk.com ▶ 문의전화 : 02-6901-7050
유키하라

Recent Comment

Archive

calendar

1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30          
  • 11,303total
  • 0today
  • 0yesterday
2008.12.29 18:15 ┗ CCNA/CCNP

4.2 채팅 서버 프로그램

▶ 채팅 서버(chat_server.c)는 임의의 클라이언트가 채팅에 참가하는 요청을 하면 이를 채팅 참가 리스트에 추가하며, 채팅에 참가하고 있는 클라이언트들이 보내오는 메시지를 모든 채팅 참가자에게 다시 방송하는 기능을 수행한다.

4.2.1 채팅 서버 구조

▶ 채팅 서버는 임의의 클라이언트로부터의 채팅 참가요구를 처리하면서 동시에 어떤 클라이언트가 보내온 채팅 메시지를 모든 클라이언트에게 방송하는 일을 처리하여야 한다.

▶ 이와같이 서버가 두 가지 일을 병행하여 처리하기 위해서 새로운 클라이언트가 접속될 때마다 프로세스를 만들는 concurrent 서버로 구현하는 방법이 가능하다.

▶ 그러나 처리해야 할 작업이 생길 때마다 프로세스를 만들어 나가는 방식은 프로그램 작성은 편리하지만 시스템 자원의 활용면이나 프로그램의 안정적인 동작면에서는 불리한 경우가 많다.

▶ 여기서 소개하는 채팅 서버 프로그램 chat_server.c에서는 apparent concurrent 서버 구조를 사용한다.

▶Apparent concurrent 서버는 concurrent 서버처럼 여러 클라이언트에게 병행하여 서비스를 제공하면서도 프로세스를 클라이언트 수만큼 계속 복제하지 않는 방식이다.

▶ 본 예제에서는 하나의 프로세스가 여러 클라이언트와의 통신을 담당하도록 한다.

▶ 한편 채팅 서버 프로그램은 클라이언트의 접속요청을 처리하는 동시에 다른 클라이언트들이 보내온 메시지를 모든 채팅 참가자 클라이언트에게 전달해야 하므로 프로세스가 어느 한 곳에 멈추어 있을 수 있는 blocking 모드로 동작하면 안 된다.

▶ 따라서 소켓을 통한 I/O를 비동기 모드로 처리하여야 하는데 이를 위하여 select() 시스템 콜을 사용한다.

▶ 그림 4-1에 apparent concurrent 서버 모델을 이용한 채팅 서버의 동작을 나타냈다.

▶ 서버는 먼저 socket()을 호출하여 채팅 참가자를 접수할 소켓을 개설하고(이 소켓을 초기소켓이라고 부르겠다) 이 소켓을 자신의 소켓주소와 bind()해 둔다.

▶ 다음에 이 초기소켓을 대상으로 select()를 호출하여 초기소켓에 어떤 I/O 변화가 생길 때까지 기다린다.

그림 4-1 apparent concurrent 모델의 채팅 서버

▶ 초기소켓에서 처음 발생할 수 있는 I/O 변화는 채팅 참가자가 연결요청을 보내왔을 때인데 이때 서버는 accept()를 호출하여 새로운 참가자 접속을 처리하고 accept()가 리턴하는 소켓번호를 참가자 리스트(client_s[])에 등록시킨다.

▶ 서버는 이 새로 생긴 소켓과 초기소켓을 대상으로 하여 다시 select()를 호출한다.

▶ select() 함수의 기능은 소켓에서 발생하는 I/O 변화를 기다리다가 지정된 I/O 변화가 감시 대상 소켓들 중 하나에서라도 발생하면 select() 문이 리턴된다.

▶ 응용 프로그램에서는 select()가 리턴되었을 때 어떤 소켓에서 어떤 I/O 변화가 발생하였는지를 확인하여 필요한 작업을 처리하면 된다.

▶ 그림 4-1에 서버가 초기소켓과 하나 이상의 채팅 참가자 리스트(client_s[])를 대상으로 다시 select()를 호출하고 select() 문이 리턴되면 이것이 초기소켓에서 발생한 것인지(즉, 새로운 채팅 참가자가 연결요청을 한 것인지) 아니면 기존의 참가자 중 누군가 채팅 메시지를 보낸 것인지를 구분하여 필요한 기능을 수행하는 것을 나타냈다.

▶ 그림 4-2에 채팅 프로그램의 전체적인 구성을 나타냈다.

▶ 서버는 초기소켓 s를 통하여 새로운 채팅 참가자를 접수(accept)하며 새로 참가한 클라이언트들의 소켓번호는 배열 client_s[]에 들어 있게 된다.

▶ 예를들어 그림 4-2에는 현재 4명의 채팅 참가자가 있는데 서버 프로그램은 초기소켓 s를 포함하여 모두 다섯 개의 소켓에서 발생하는 I/O 변화를 감지하도록 select()를 호출하여야 한다.

그림 4-2 채팅 서버와 클라이언트의 연결관계

(s: 초기소켓, ,client_s[]: 채팅 참가자의 소켓 번호 배열)

4.2.2 select() 시스템

▶ select() 시스템 콜의 사용 문법은 다음과 같다.

int select (

int maxfdp1, /* 최대 파일( 소켓)번호 크기 + 1 */

fd_set *readfds, /* 읽기상태 변화를 감지할 소켓 지정 */

fd_set *writefds, /* 쓰기상태 변화를 감지할 소켓 지정 */

fd_set *exceptfds, /* 예외상태 변화를 감지할 소켓 지정 */

struct timeval *tvptr); /* select() 시스템 콜이 기다리는 시간 */

▶ select()의 첫번째 인자 maxfdp1은 'I/O 변화를 감시할 총 소켓의 개수 +1'의 값을 지정하여야 하는데 보통 현재 open된 소켓번호 중 가장 큰 값에 1을 더한 값을 사용한다.

▶ 한편 시스템내에서 개설할 수 있는 파일기술자의 최대값은 <sys/types.h>에 FD_SETSIZE로 정의되어 있다.

▶ fd_set(file descriptor set) 타입의 인자 readfds, writefds, exceptfds는 각각 읽기, 쓰기, 예외상황 발생과 같은 I/O 변화가 발생하였을 때 이를 감지할 대상이 되는 소켓들을 지정하는 배열형 구조체이다.

▶ 즉, 이 세 가지 구조체를 통하여 어떤 소켓에서 어떤 I/O 변화 발생을 감지할지를 선택하여 지정할 수 있다(뒤에서 자세히 설명함).

▶ 마지막 인자 tvptr은 select() 시스템 콜이 기다리는 시간을 지정하는데 tvptr이 NULL인 경우에는 지정한 I/O 변화가 발생할 때까지 계속 기다리며, 0인 경우에는 기다리지 않고 바로 리턴되고 그 외의 값인 경우에는 지정된 시간만큼 또는 도중에 I/O 변화가 발생할 때까지 기다린다.

▶ fd_set 타입의 구조체(위에서 readfds, writefds, exceptfds)와 소켓번호와의 관계는 그림 4-3과 같다.

▶ 예를들어 그림 4-3에서 readfds 구조체의 소켓번호 0번과 3번이 1로 세트되어 있으므로 키보드나(파일번호 0번), 소켓번호 3에서 어떤 데이터가 입력되어 응용 프로그램이 이를 읽을 수 있는 상태가 되면 select()문이 리턴된다.

▶ 한편 writefds에서는 소켓번호 1번과 3번이 1로 세트되어 있으므로 파일기술자 1번(표준 출력)이나 소켓번호 3번이 write를 할 수 있는 상태로 변하면(예를들면 송신 버퍼가 비워짐) select()문이 리턴된다.

▶ 이와같이 각각 fd_set 타입 구조체에 I/O변화를 감지할 소켓(또는 파일)번호를 1로 세트하여 select()를 호출해 두면 해당 조건이 만족되는 순간 select()문이 리턴된다.

▶ 한편 UNIX에서는 fd_set 타입의 구조체를 편리하게 처리할 수 있도록, 예를들면 특정 소켓의 I/O 변화 감지를 쉽게 선택하거나 취소할 수 있도록 매크로를 제공하고 있다(표 4-1 참조).

그림 4-3 id_set 타입의 구조체와 소켓번호와의 관계

FD_ZERO(fd_set *fdset) *fdset의 모든 비트를 지운다.
FD_SET(int fd, fd_set *fdset) *fdset 중 소켓 fd에 해당하는 비트를 1로 한다.
FD_CLR(int fd, fd_set *fdset) *fdset 중 소켓 fd에 해당하는 비트를 0으로 한다.
FD_ISSET(int fd, fd_set *fdset) *fdset 중 소켓 fd에 해당하는 비트가 세트되어 있으면 양수값인 fd를 리턴한다.

표 4-1 fd_set를 사용하기위한 매크로

▶ select()를 호출하려면 먼저 fd_set 구조체를 만들어야 하는데 본 절에서 소개할 chat_server.c의 경우에는 읽기에 대한 I/O 변화만 확인하면 되므로 fd_set 타입의 구조체 read_fds 하나만 선언하고 FD_ZERO(&read_fds)를 호출하여 read_fds의 모든 소켓번호 위치를 disable시킨다.

▶ 다음에는 I/O 변화에 관심을 갖는 소켓번호(또는 파일기술자)들을 선택하여야 하는데, chat_server.c에서는 클라이언트와의 접속 요구를 처리할 초기소켓 s와 각 채팅 클라이언트마다 배정된 소켓번호들의 배열 client_s[] 두 가지의 소켓이 있다.

▶ 현재 채팅에 참가하고 있는 클라이언트 수를 num_chat이라고 하면 아래와 같이 FD_ZERO를 사용하여 read_fds를 초기화하고 FD_SET를 사용하여 I/O 변화를 감지할 소켓들을 선택한다.

fd_set read_fds;

FD_ZERO(&read_fds); /* *read_fds의 모든 소켓을 0으로 초기화 */

FD_SET(s, &read_fds); /* 초기소켓 선택 */

for(i = 0; i < num_chat; i++) /* 모든 클라이언트 접속 소켓 선택 */

FD_SET(client_s[i], &read_fds);

▶위에서 초기화한 read_fds를 select()의 두번째 인자(즉, 읽기 변화 감지용 fd_set)로 지정하고, 쓰기 및 예외 발생에 해당하는 fd_set는 NULL로 지정하여 다음과 같이 select()를 호출한다.

select(maxnfdsp1, &read_fds, (fd_set *)0, (fd_set *)0, (struct timeval *)0);

▶ 한편 select() 문의 첫번째 인자 'I/O 변화를 감시할 총 소켓의 개수+1'의 값으로는 현재 개설된 '최대 소켓번호 +1'을 사용하였다.

▶ 위의 select()는 두 가지 종류의 입력에 대하여 리턴되므로 select()가 리턴되면 어떤 입력이 발생하였는지를 판단하여야 하며 이를 위하여 FD_ISSET 매크로를 사용한다.

▶ FD_ISSET의 사용법을 아래에 보였는데 매크로 FD_ISSET는 read_fds 구조체에서 소켓번호 s에서 I/O 변화가 있었으면 양수값인 소켓번호 s를 리턴한다.

FD_ISSET(s, &read_fds);

 

▶ 아래에서 FD_ISSET 실행 결과가 양수이면 클라이언트로부터 연결요청(즉, 채팅 참가요청)이 온 것이므로 accept()를 불러 채팅 참가요청을 처리한다.

▶ 한편 client_s[] 중 하나의 소켓이 세트되었다면 어떤 채팅 참가자가 채팅 메시지를 전송한 것이므로 이 메시지를 받아 모든 참가자에게 방송하면 된다.

if (FD_ISSET(s, &read_fds)) {

/* 초기소켓 s에서 입력 발생 */

clilen =sizeof(client_addr);

client_fd = accept(s, (struct sockaddr *)&client_addr, &clilen);

}

/* 클라이언트 소켓번호 배열 client[] 차례로 검색 */

for(i = 0; i < num_chat; i++) {

if (FD_ISSET(client_s[i], &read_fds)) {

/* 소켓 client_s[i]에서 채팅 메시지 수신 */

/* 모든 참가자에게 채팅 메시지 방송 */

}

}

한편 클라이언트가 채팅에서 탈퇴하려면 종료문자(: exit) 보내도록 하였고 서버는 종료문자를 수신한 경우 이를 확인하고(exitCheck() 함수 사용) client_s[] 배열에서 해당 클라이언트의 소켓번호를 삭제한 채팅 참가자 num_chat 1 감소시킨다.

▶ 아래는 채팅 서버를 수행한 결과를 보이고 있다.

# chat_server 4001

대화방 서버 실행..

4.2.3 프로그램 주요부분 설명

▶ chat_server.c에서 소켓을 개설할 때 인터넷 프로토콜 체계(PF_INET)를 사용하였으며 스트림 형태의(TCP) 프로토콜을 선택하였다.

▶ 아래는 소켓을 만들고 소켓에 주소를 부여하는(bind) 과정을 보였는데 소켓주소 구조체의 내용을 널(NULL)로 초기화하기 위하여 함수 bzero()를 사용하였다.

▶ IP 주소로는 현재 채팅 서버 프로그램이 실행되는 호스트의 IP 주소를(INADDR_ANY), 그리고 서비스를 제공할 포트번호는 서버 프로그램 실행시 사용자가 입력한 값(예를들면 4001)을 사용하였다.

s = socket(PF_INET, SOCK_STREAM, 0);

bzero((char *)&server_addr, sizeof(server_addr));

server_addr.sin_family = PF_INET;

server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

server_addr.sin_port = htons(atoi(argv[1]));

▶ 아래는 어떤 클라이언트가 보낸 메시지를 다른 모든 클라이언트에게 전송하는 동작을 나타냈다.

▶ 여기서 배열 client_s[]에는 각 채팅 참가자들과 연결되는 소켓번호들이 들어 있다.

▶ 첫번째 for() 루프는 현재 채팅에 참가하는 모든 클라이언트를 대상으로 보내온 채팅 메시지가 있는지 검색하며, 두번째 for() 루프에서는 채팅 메시지를 모든 클라이언트에게 방송한다

/* 채팅 메시지가 도착했는지 검사 */

for(i = 0; i < num_chat; i++) {

if(FD_ISSET(client_s[i], &read_fds)) {

if((n = recv(client_s[i], rline, MAXLINE,0)) > 0) {

rline[n] = '\0';

/* 종료문자 입력시 채팅 탈퇴 처리 */

if(exitCheck(rline, escapechar, 5) == 1) {

shutdown(client_s[i], 2);

/* client_s[] 크기 조절 */

if(i != num_chat - 1)

client_s[i] = client_s[num_chat - 1];

num_chat--;

continue;

}

/* 모든 채팅 참가자에게 메시지 방송 */

for(j = 0; j < num_chat; j++)

send(client_s[j], rline, n, 0);

printf("%s", rline);

}

}

}

 

/*----------------------------------------------------------------------------------------------
 파일명 : chat_server.c   
 기  능 : 채팅 참가자 관리, 채팅 메시지 수신 및 방송
 컴파일 : cc -o chat_server chat_server.c readline.c -lsocket -lnsl
 실행예 : chat_server 4001
-----------------------------------------------------------------------------------------------*/
#include  <stdio.h>
#include  <fcntl.h>
#include  <stdlib.h>
#include  <signal.h>
#include  <sys/socket.h>
#include  <sys/file.h>
#include  <netinet/in.h>

#define MAXLINE  1024
#define MAX_SOCK  512
 
char *escapechar = "exit\n";
int readline(int, char *, int);

int main(int argc, char *argv[]) 
{
   char  rline[MAXLINE], my_msg[MAXLINE];
   char  *start = "대화방에 오신걸 환영합니다...\n";
   int  i, j, n;
   int  s, client_fd, clilen;
   int nfds;   /* 최대 소켓번호 +1 */
   fd_set read_fds; /* 읽기를 감지할 소켓번호 구조체 */
   int num_chat = 0;  /* 채팅 참가자 수 */
   /* 채팅에 참가하는 클라이언트들의 소켓번호 리스트 */
   int  client_s[MAX_SOCK];
   struct sockaddr_in  client_addr, server_addr;
  
   if(argc < 2)  {
      printf("실행방법 :%s 포트번호\n",argv[0]);
      return -1;
   }
  
   printf("대화방 서버 초기화 중....\n");

   /* 초기소켓 생성 */
   if((s = socket(PF_INET, SOCK_STREAM, 0)) < 0)  {
      printf("Server: Can't open stream socket.");  
      return -1;
   }
  
   /* server_addr 구조체의 내용 세팅 */
   bzero((char *)&server_addr, sizeof(server_addr)); 
   server_addr.sin_family = AF_INET;             
   server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
   server_addr.sin_port = htons(atoi(argv[1]));    
  
   if (bind(s,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0) {
      printf("Server: Can't bind local address.\n");
      return -1;
   }
  
   /* 클라이언트로부터 연결요청을 기다림 */
   listen(s, 5);

   nfds = s + 1;  /* 최대 소켓번호 +1 */
   FD_ZERO(&read_fds);
  
   while(1) {
      /* (최대 소켓번호 +1) 값을 갱신 */
      if((num_chat-1) >= 0)  nfds = client_s[num_chat-1] + 1;

      /* 읽기 변화를 감지할 소켓번호를 fd_set 구조체에 지정 */
      FD_SET(s, &read_fds);
      for(i=0; i<num_chat; i++)  FD_SET(client_s[i], &read_fds);
     
   /*--------------------------------------- select() 호출 ----------------------------------------- */
      if (select(nfds, &read_fds, (fd_set *)0, (fd_set *)0,(struct timeval *)0) < 0) {
      printf("select error\n");
      return -1;
      }
     /*------------------------------ 클라이언트 연결요청 처리 ------------------------------- */
      if(FD_ISSET(s, &read_fds)) {
      clilen = sizeof(client_addr);
      client_fd = accept(s, (struct sockaddr *)&client_addr, &clilen);

      if(client_fd != -1)  {
      /* 채팅 클라이언트 목록에 추가 */
      client_s[num_chat] = client_fd;
      num_chat++;
      send(client_fd, start, strlen(start), 0);
      printf("%d번째 사용자 추가.\n",num_chat);
      }
      }
     
      /*------ 임의의 클라이언트가 보낸 메시지를 모든 클라이언트에게 방송 ----- */
      for(i = 0; i < num_chat; i++)  {
     if(FD_ISSET(client_s[i], &read_fds)) {
        if((n = recv(client_s[i], rline, MAXLINE,0))  > 0)  {
          rline[n] = '\0';

          /* 종료문자 입력시 채팅 탈퇴 처리 */
          if (exitCheck(rline, escapechar, 5) == 1) {
       shutdown(client_s[i], 2);
       if(i != num_chat-1)     client_s[i] = client_s[num_chat-1];
       num_chat--;
       continue;
          }

           /* 모든 채팅 참가자에게 메시지 방송 */ 
           for (j = 0; j < num_chat; j++)  send(client_s[j], rline, n, 0);
           printf("%s", rline);
        }
     }
      }
   }
}

/* ------------------------------- 종료문자 확인 함수 ----------------------------
exitCheck()는 다음의 세 개의 인자를 필요로 한다
 rline: 클라이언트가 전송한 문자열 포인터
 escapechar: 종료문자 포인터
 len: 종료문자의 크기
---------------------------------------------------------------------------------------------*/
int exitCheck(rline, escapechar, len)
  char *rline;  /* 클라이언트가 전송한 메시지 */
  char *escapechar; /* 종료문자 */
  int  len;
  {
     int i, max;
     char *tmp;
  
     max = strlen(rline); 
     tmp = rline;
     for(i = 0; i<max; i++) {
        if (*tmp == escapechar[0]) {
     if(strncmp(tmp, escapechar, len) == 0)
        return 1;
        } else
  tmp++;
     }
   return -1;
}

 


posted by 솔데스크IT아카데미 유키하라
TAG ,

티스토리 툴바