2011년7월27일_선생님의 채팅프로그램 소스코드(chat_server.c, chat_client.c)의 전체적인 흐름과 분석, 네트워크프로젝트 계획




솔로들을 위한 조금 슬픈 채팅프로그램... 구글번역을 써서 이상한 문장이 되어 버렸다. (ㅠㅠ)
A부터 Z까지 처음부터 코딩하려면 막막하니 일단 선생님께서 하사하신 소스코드를 실행, 분석한 뒤에 아이디어가 나오면..
그 부분에 대해 기능을 추가하고 문제를 수정하는 방식으로 프로젝트를 진행할 예정임.




char_server.c와 char_client.c 소스코드 분석


● chat_server.c

   1: // chat_server.c
   2:  
   3: #include <stdio.h>
   4: #include <fcntl.h>
   5: #include <stdlib.h>
   6: #include <signal.h>
   7: #include <sys/socket.h>
   8: #include <sys/file.h>
   9: #include <netinet/in.h>
  10: #include <string.h>
  11:  
  12: #define    MAXLINE        512
  13: #define    MAX_SOCK    64
  14:  
  15: int getmax(int);
  16: void removeClient(int);            //채팅 탈퇴 처리함수
  17:  
  18: char *escapechar = "exit";
  19: int max_fd1;                    //최대 소켓번호 +1
  20: int num_chat = 0;                //채팅 참가자 수
  21: int client_s[MAX_SOCK];            //채팅 참가자 소켓번호 목록
  22:  
  23:  
  24: int main(int argc, char *argv[])
  25: {
  26:     char rline[MAXLINE], my_msg[MAXLINE];
  27:     char *start = "Connected to chat_server \n";
  28:     int i, j, n;
  29:     int s;
  30:     int client_fd, client_len;
  31:  
  32:     fd_set read_fds;
  33:     struct sockaddr_in client_addr, server_addr;
  34:  
  35:     if(argc != 2)
  36:     {
  37:         fprintf(stderr, "사용법: %s Port \n", argv[0]);
  38:         exit(0);
  39:     }
  40:  
  41:     if((s = socket(PF_INET, SOCK_STREAM, 0)) < 0)
  42:     {
  43:         fprintf(stderr, "Server: Can't open stream socket.\n");
  44:         exit(0);
  45:     }
  46:  
  47:     bzero((char *)&server_addr, sizeof(server_addr));
  48:     server_addr.sin_family = AF_INET;
  49:     server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  50:     server_addr.sin_port = htons(atoi(argv[1]));
  51:  
  52:     if(bind(s, (struct sockaddr *)&server_addr,sizeof(server_addr)) < 0)
  53:     {
  54:         fprintf(stderr, "Server: Can't bind local address.\n");
  55:         exit(0);
  56:     }
  57:  
  58:     listen(s, 5);
  59:  
  60:     max_fd1 = s + 1;            //최대 소켓번호 + 1
  61:  
  62:     while(1)
  63:     {
  64:         FD_ZERO(&read_fds);
  65:         FD_SET(s, &read_fds);
  66:  
  67:         for(i = 0 ; i < num_chat ; i++)
  68:             FD_SET(client_s[i], &read_fds);
  69:  
  70:         max_fd1 = getmax(s) + 1;        //max_fd1 재계산
  71:         if(select(max_fd1, &read_fds, (fd_set *)0, (fd_set *)0, (struct timeval *)0) < 0)
  72:         {
  73:             fprintf(stderr, "select error <= 0\n");
  74:             exit(0);
  75:         }
  76:  
  77:         if(FD_ISSET(s, &read_fds))
  78:         {
  79:             client_len = sizeof(client_addr);
  80:             client_fd = accept(s, (struct sockaddr *)&client_addr, &client_len);
  81:             if(-1 == client_fd)
  82:             {
  83:                 fprintf(stderr, "accept error\n");
  84:                 exit(0);
  85:             }
  86:  
  87:             // 채팅 클라이언트 목록에 추가
  88:             client_s[num_chat] = client_fd;
  89:             num_chat++;
  90:             send(client_fd, start, strlen(start), 0);
  91:             printf("%d번째 사용자 추가. \n", num_chat);
  92:         }
  93:  
  94:         //클라이언트가 보낸 메시지를 모든 클라이언트에게 방송
  95:         for(i = 0 ; i < num_chat ; i++)
  96:         {
  97:             if(FD_ISSET(client_s[i], &read_fds))
  98:             {
  99:                 if((n = recv(client_s[i], rline, MAXLINE, 0)) <= 0)
 100:                 {
 101:                     removeClient(i);
 102:                     continue;
 103:                 }
 104:  
 105:                 if(strstr(rline, escapechar) != NULL)    //종료문자 처리
 106:                 {
 107:                     removeClient(i);
 108:                     continue;
 109:                 }
 110:  
 111:                 //모든 채팅 참가자에게 메시지 방송
 112:                 rline[n] = '\0';
 113:                 for(j = 0 ; j < num_chat ; j++)
 114:                     send(client_s[j], rline, n, 0);
 115:  
 116:                 printf("%s", rline);
 117:             }
 118:         }
 119:     }
 120:         
 121:     return 0;
 122: }
 123:  
 124: //채팅 탈퇴 처리
 125: void removeClient(int i)
 126: {
 127:     close(client_s[i]);
 128:  
 129:     if(i != num_chat - 1)
 130:         client_s[i] = client_s[num_chat - 1];
 131:  
 132:     num_chat--;
 133:     printf("채팅 참가자 1명 탈퇴. 현 참가자 수 = %d \n", num_chat);
 134: }
 135:  
 136: //client_s[]내의 최대 소켓번호 얻기
 137: int getmax(int k)
 138: {
 139:     int max = k;
 140:     int r;
 141:  
 142:     for(r = 0 ; r < num_chat ; r++)
 143:     {
 144:         if(client_s[r] > max)
 145:             max = client_s[r];
 146:     }
 147:  
 148:     return max;
 149: }


● chat_client.c

   1: // chat_client.c
   2:  
   3: #include <stdio.h>
   4: #include <fcntl.h>
   5: #include <stdlib.h>
   6: #include <signal.h>
   7: #include <sys/socket.h>
   8: #include <sys/file.h>
   9: #include <netinet/in.h>
  10: #include <string.h>
  11:  
  12: #define    MAXLINE        512
  13: #define    MAX_SOCK    128
  14:  
  15: char *escapechar = "exit";
  16: char name[10];                //채팅에서 사용할 이름
  17:  
  18: int main(int argc, char *argv[])
  19: {
  20:     char line[MAXLINE], msg[MAXLINE + 1];
  21:     int n, pid;
  22:     int maxfd1;
  23:     int s;
  24:     fd_set read_fds;
  25:     struct sockaddr_in server_addr;
  26:  
  27:     if(argc != 4)
  28:     {
  29:         fprintf(stderr, "사용법: %s serverIP serverPort UserName \n", argv[0]);
  30:         exit(0);
  31:     }
  32:  
  33:     //채팅 참가자 이름 저장
  34:     sprintf(name, "[%s]", argv[3]);
  35:  
  36:     //소켓생성
  37:     if((s = socket(PF_INET, SOCK_STREAM, 0)) < 0)
  38:     {
  39:         fprintf(stderr, "Client: Can't open stream socket.\n");
  40:         exit(0);
  41:     }
  42:  
  43:     //채팅 서버의 소켓주소 저장
  44:     bzero((char *)&server_addr, sizeof(server_addr));
  45:     server_addr.sin_family = AF_INET;
  46:     server_addr.sin_addr.s_addr = inet_addr(argv[1]);
  47:     server_addr.sin_port = htons(atoi(argv[2]));
  48:  
  49:     //연결 요청
  50:     if(connect(s, (struct sockaddr *)&server_addr,sizeof(server_addr)) < 0)
  51:     {
  52:         fprintf(stderr, "Server: Can't bind local address.\n");
  53:         exit(0);
  54:     }
  55:     else
  56:     {
  57:         printf("서버에 접속되었습니다. \n");
  58:     }
  59:     
  60:  
  61:     maxfd1 = s + 1;            //최대 소켓번호 + 1
  62:     FD_ZERO(&read_fds);
  63:     
  64:     while(1)
  65:     {
  66:         FD_SET(0, &read_fds);
  67:         FD_SET(s, &read_fds);
  68:  
  69:         if(select(maxfd1, &read_fds, (fd_set *)0, (fd_set *)0, (struct timeval *)0) < 0)
  70:         {
  71:             fprintf(stderr, "select error <= 0\n");
  72:             exit(0);
  73:         }
  74:  
  75:         if(FD_ISSET(s, &read_fds))
  76:         {
  77:             int size;
  78:             if((size = recv(s, msg, MAXLINE, 0)) > 0)
  79:             {
  80:                 msg[size] = '\0';
  81:                 printf("%s \n", msg);
  82:             }
  83:         }
  84:  
  85:         if(FD_ISSET(0, &read_fds))
  86:         {
  87:             if(fgets(msg, MAXLINE, stdin))
  88:             {
  89:                 sprintf(line, "%s %s", name, msg);
  90:                 if(send(s, line, strlen(line), 0) < 0)
  91:                     printf("send() error \n");
  92:                 
  93:                 if(strstr(msg, escapechar) != NULL)    //종료문자 처리
  94:                 {
  95:                     printf("Good bye. \n");
  96:                     close(s);
  97:                     exit(0);
  98:                 }
  99:             }
 100:         }
 101:     }
 102:         
 103:     return 0;
 104: }


● 실행결과


 
상기의 스크린샷과 같이 이 프로그램들은 문제가 있다.
메세지을 입력하는 동안 다른 사용자(클라이언트)가 메세지를 보내면 커서가 위치한 곳 부터 수신받은 메세지가 덧붙여져..
보기가 좋지 않고 혼란스럽다.
그리고 새로운 사용자가 서버에 접속을 하여도 기존 접속자들에게 알려주지 않는다.
누가 접속해 있는지 모르는데 정상적인 대화가 가능할까?



● 프로그램의 전체적인 흐름

 
TCP/IP통신의 초기단계인 socket( )과 bind( ), listen( )에 대한 설명은 생략합니다.
파란색점선은 제어신호의 이동을 나타내고 select( )의 블로킹이 풀리는 신호의 흐름을 표현한 것이다.
초록색실선은 Client에서 connect( )로 Server에 연결을 요청할 때 accept( )의 블로킹이 풀리는 것을 표현한 것이고,
                ※실제는 아래의 3way-handshaking 도식을 참조할 것.
빨간색실선은 사용자가 다른 사용자에게 메세지를 전송했을 때의 흐름을 표현한 것이다.
검은색실선은 프로그램의 흐름이고, 옅은 분홍색과 옅은 황색의 사각형박스는 그 부분이 반복된다는 것이다. (분홍색은 무한반복임)



● 소스코드분석

<서버>

  71:         if(select(max_fd1, &read_fds, (fd_set *)0, (fd_set *)0, (struct timeval *)0) < 0)

71행: 서버측에서 select( )로 사용중인 모든 장치의 입력의 변화를 체크하여 수신이 되면 즉시 리턴하여 다음 단계로 갑니다.
       여기선 리턴값이 음수일 때 에러처리만 하고 타임아웃이 NULL이므로 타임아웃처리를 따로 할 필요가 없으니 else문을 쓰지 않음.
       그럼 어떤 때에 어떻게 서버는 수신을 받아 블로킹되어 있는 select( )를 푸는 것일까?

        

       TCP/IP통신에서 서버와 클라이언트간의 handshaking도를 보면 Client가 연결을 요청할 때 제어문자를 보내게 되어 있다.
       그러니 select( )에서 서버소켓을 감시하면 Client가 제어문자를 보냈을 때 블로킹이 풀리는 것이다.

<클라이언트>

  50:     if(connect(s, (struct sockaddr *)&server_addr,sizeof(server_addr)) < 0)

50행: 클라이언트측에서 connect( )로 서버에 연결을 요청함.



<서버>

  77:         if(FD_ISSET(s, &read_fds))

77행: select( )에서 사용 중인 모든 장치의 수신을 검사하였다고 하여도 어떤 장치 – 어떤 파일디스크립터에,
       변화가 발생하였는지는 알 수 없다. 그러니 서버소켓을 검사하여 Client의 연결 요청이었는지를 확인한다. (제어문자)

  80:             client_fd = accept(s, (struct sockaddr *)&client_addr, &client_len);

80행: 만약 77행에서 검사하여 소켓에 수신된 값이 있었다면 accept( )로 Client의 연결요청을 수락한다.
       select( )로 변화를 감지했으므로 accept( )에 프로그램의 흐름이 도달할 쯤에는 accept( )가 바로 일을 할 수 있는 상황이 된다.

  87:             // 채팅 클라이언트 목록에 추가
  88:             client_s[num_chat] = client_fd;
  89:             num_chat++;
  90:             send(client_fd, start, strlen(start), 0);
  91:             printf("%d번째 사용자 추가. \n", num_chat);

87 ~ 91행: 이후 서버는 접속한 Client의 소켓디스크립터(파일디스크립터)를 리스트에 추가하고 접속인원수를 카운트한다.
             서버에 접속했다는 확인메세지로  "Connected to chat_server \n"문자열을 전송하고 서버의 터미널창에 접속수를 표시한다.


<클라이언트>

  69:         if(select(maxfd1, &read_fds, (fd_set *)0, (fd_set *)0, (struct timeval *)0) < 0)

69행: 사용 중인 모든 장치를 검사하여 변화가 일어나면 블로킹을 풀고 if문 다음 명령문들을 실행한다.
       if문 내의 명령문들은 예외처리이니 설명을 생략한다.

  75:         if(FD_ISSET(s, &read_fds))
  78:             if((size = recv(s, msg, MAXLINE, 0)) > 0)

75행: 소켓디스크립터에 변화가 일어 났다면? 수신버퍼에 데이터가 들어왔다면 다음 명령문들을 실행한다.
78행: 서버로부터 데이터를 수신받아 msg라는 배열에 저장한다.  

  80:                 msg[size] = '\0';
  81:                 printf("%s \n", msg);

80행: 저수준입출력함수들은 문자열의 끝 표시를 해주지 않는다. 문자열의 끝을 알리기 위해 NULL문자를 끝에 삽입한다.
81행: 클라이언트의 모니터에 수신 받은 메시지를 출력한다.    

  85:         if(FD_ISSET(0, &read_fds))
  87:             if(fgets(msg, MAXLINE, stdin))
  89:                 sprintf(line, "%s %s", name, msg);
  90:                 if(send(s, line, strlen(line), 0) < 0)

85행: 0번 파일디스크립터인 stdin(표준입력장치 = 키보드)의 변화가 발생(엔터키입력)하였다면,
87행: 키보드로 부터 문자열을 입력받아 msg에 저장한다.
89행: name배열과 msg배열의 문자열을 합쳐서 line이라는 배열에 “%s %s”형식에 맞춰 저장한다.
90행: send( )로 소켓디스크립터s에 연결된 네트워크를 통해 line의 길이만큼 line에 저장된 문자열을 전송한다.


주의해서 볼 점은 75행과 85행이고 서로 I/O멀티플렉싱을 시분할로 처리하고 있다.
이렇게 서로 다른 시간대에 처리를 할 수 있는 이유는 모든 장치에는 다른 장치와 연결이 용이하도록 버퍼가 있기 때문이다.
하드웨어적인 버퍼가 있을 수 있고 소프트웨어적인 버퍼가 있을 수 있다.
(키보드의 경우는 소프트웨어적인 버퍼만 있다. 동기신호와 데이터만 날리는 단순한 구조로 되어 있다.)
버퍼에 데이터가 들어가는 것과 응용프로그램의 프로세스가 처리하는 것은 다르다.
프로세스가 처리하지 않아도 커널은 열심히 모든 장치의 데이터들을 특정영역에 보관해 준다. 그리고 제어에 필요한 신호도 발생시킨다.
꼭 신호를 발생시키지 않아도 특정영역에 있는 특정메모리의 값을 참조하면 그 변화를 알 수 있다.
(깊게 들어가면 어려우니 여기서 그만하겠다.)

이렇게 클라이언트는 단순히 채텅서버로 메시지를 보내고 받아 표시하는 행위만 한다.

<서버>

  94:         //클라이언트가 보낸 메시지를 모든 클라이언트에게 방송
  95:         for(i = 0 ; i < num_chat ; i++)
  97:             if(FD_ISSET(client_s[i], &read_fds))
  99:                 if((n = recv(client_s[i], rline, MAXLINE, 0)) <= 0)
 112:                 rline[n] = '\0';
 113:                 for(j = 0 ; j < num_chat ; j++)
 114:                     send(client_s[j], rline, n, 0);
 116:                 printf("%s", rline);

95행: 모든 장치의 변화를 체크하는 반복문.
97행: Server로 부터 수신한 데이터가 있는지 검사하여, 있다면,
99행: recv( )로 수신하여 rline배열에 저장.
112행: 문자열의 끝표시
113행: Server에 접속한 Client수 만큼 반복하여 리스트에 있는 소켓디스크립터를 통해 네트워크로 rline배열에 저장된 문자열을 전송.
116행: 전송한 문자열을 화면에 표시.

서버도 클라이언트처럼 단순한 일을 하는데..채텅서버 사용자가 보내온 메세지를 받아,
모든 사용자에게 보내는 에코 - 유니캐스트를 수행한다.

더 자세한 설명이 필요하면 댓글을 달아주세요.



 

네트워크 프로젝트 계획





 

7월26일...프로젝트 시작하며 선생님의 소스코드를 아무런 생각없이 타이핑.
           선생님: “수업을 여기까지입니다. 더 이상 가르쳐줄께 없삼. 알아서 하삼.”
           학생: “…”
7월27일...chat_server.c & char_client.c 소스코드 분석.
           프로젝트 스케쥴표 작성.
7월28일~29일...터미널창에 윈도우를 추가해보자.

8월1일~5일...방학 싱난다.

8월8일~9일...접속자관리기능추가와 베타테스트를 본격적으로 시작.
8월10일~12일...보고서작성과 마무리 테스트.

세부계획을 세우지 않은 이유는 별 생각이 없기 때문입니다!!