5-1. 에코 클라이언트! TCP 기반에서의 완벽 구현

 TCP는 연결 지향 프로토콜로서 전송되는 데이터의 경계(boundary)가 없다. 그래서 한번의 write 함수 호출을 통해서 "ABCD"라는 문자열을 전송할지라도 그 데이터들이 반드시 하나의 패킷으로 구성되어서 전송된다고 보장할 수 없다. 상황에 따라서 "AB" 문자열이 먼저 하나의 패킷으로 전송되고, 그 다음에 "C" 가 전송되고, 마지막으로 "D" 가 전송될 수도 있다.

 그래서 이전에 구현한 에코 클라이언트에서 위와 같은 상황이 발생한다면 문자열의 일부분만을 수신하는 결과가 있을 수 있게 된다.

 그러므로 write 함수 호출을 통해서 데이터를 전송하는 경우, 반드시 하나의 패킷으로 구성되어서 데이터가 전달되어야만 제대로 동작할 것이다. TCP는 절대로 이것을 보장해 주지 않는다.

 다음은 이를 보완한 코드이다.
 
echo_client2.c (Language : c)
  1. /***************************************************************************
  2. *            echo_client2.c
  3. *
  4. *  Fri Jan  4 18:14:10 2008
  5. *  Copyright  2008  pchero21
  6. *  pchero21@gmail.com
  7. ****************************************************************************/
  8.  
  9. /*
  10. *  This program is free software; you can redistribute it and/or modify
  11. *  it under the terms of the GNU General Public License as published by
  12. *  the Free Software Foundation; either version 2 of the License, or
  13. *  (at your option) any later version.
  14. *
  15. *  This program is distributed in the hope that it will be useful,
  16. *  but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  18. *  GNU General Public License for more details.
  19. *
  20. *  You should have received a copy of the GNU General Public License
  21. *  along with this program; if not, write to the Free Software
  22. *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  23. */
  24.  
  25. #include <stdio.h>
  26. #include <stdlib.h>
  27. #include <string.h>
  28. #include <unistd.h>
  29. #include <arpa/inet.h>
  30. #include <sys/types.h>
  31. #include <sys/socket.h>
  32.  
  33. void error_handling(char *message);
  34.  
  35. int main(int argc, char **argv)
  36. {
  37.     int sock;
  38.     char message[30];
  39.     int str_len, recv_len, recv_num;
  40.     struct sockaddr_in serv_addr;
  41.        
  42.     if(argc != 3) {
  43.         printf("Usage : %s <IP> <port>\n", argv[0]);
  44.         exit(1);
  45.     }
  46.    
  47.     sock = socket(PF_INET, SOCK_STREAM, 0);
  48.     if(sock == -1)
  49.         error_handling("socket() error");
  50.    
  51.     memset(&serv_addr, 0, sizeof(serv_addr));
  52.     serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
  53.     serv_addr.sin_port = htons(atoi(argv[2]));
  54.    
  55.     if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
  56.         error_handling("connect() error!");
  57.    
  58.     while(1) {
  59.         fputs("전송할 메시지를 입력하세요. (q to quit) : ", stdout);
  60.         fgets(message, sizeof(message), stdin);
  61.    
  62.         if(!strcmp(message, "q\n"))
  63.             break;
  64.         str_len = write(sock, message, strlen(message));    /* data send */
  65.        
  66.         for(recv_len = 0; recv_len < str_len; ) {   /* data receive */
  67.             recv_num = read(sock, &message[recv_len], str_len - recv_len);
  68.             if(recv_num == -1)
  69.                 error_handling("read() error!");
  70.             recv_len += recv_num;
  71.         }
  72.         message[str_len] = 0;
  73.         printf("서버로부터 전송된 메시지 : %s \n", message);
  74.     }
  75.     close(sock);
  76.     return 0;
  77. }
  78.  
  79. void error_handling(char *message)
  80. {
  81.     fputs(message, stderr);
  82.     fputc('\n', stderr);
  83.     exit(1);
  84. }
  85.  

 이전의 에코 클라이언트와 차이가 나는 부분은 66부터 71까지이다. 코드를 보면, 전송된 데이터가 에코되어 완전히 돌아올 때까지, 계속해서 read 함수를 호출해서 수신한 데이터를 배열에 저장해 나가고 있다. 따라서 정확히 전송한 바이트 크기만큼의 데이터를 수신할 수 있다.

 하지만 이러한 방법은 수신해야 할 데이터의 크기를 미리 알고 있는 경우가 아니라면 적용하기 어렵다. TCP 기반의 에코 클라이언트가 구현하기 쉬운 이유가 바로 이것이다. 수신해야 할 데이터의 크기를 미리 알고 있다는 것이다.

 그러나 다른 소켓 프로그램의 경우에서는 이런 상황이 드물게 발생한다. 즉 데이터를 수신하는 측은 몇 바이트를 수신해야 하는지 알지 못한다. 이러한 경우에는 프로그램을 구현하기가 조금 더 복잡해 질 것이다.


    5-2. 경계(Boundary)가 없는 TCP기반의 데이터 전송

 이번에는 경계(Boundary)가 없다는 사실을 확인해 보자.

 시나리오는 다음과 같다. 클라이언트는 두번의 메시지를 서버로 전송한다. 하지만 서버는 전송되어 오는 메시지를 한번의 read를 통해서 모두 읽어 들인다. 그리고 나서 이 메시지를 한 번의 write 함수를 호출해서 클라이언트로 전송한다. 마지막으로 클라이언트는 네 번의 read 함수를 호출해서 모든 메시지를 수신한다.

 만약 시나리오대로 프로그램이 정상적으로 실행된다면 TCP 에서 경계(Boundary)가 없다는 것이 확인된다.

 서버쪽 코드이다.

bnd_server.c (Language : c)
  1. /***************************************************************************
  2. *            bnd_server.c
  3. *
  4. *  Fri Jan  4 18:28:25 2008
  5. *  Copyright  2008  pchero21
  6. *  pchero21@gmail.com
  7. ****************************************************************************/
  8.  
  9. /*
  10. *  This program is free software; you can redistribute it and/or modify
  11. *  it under the terms of the GNU General Public License as published by
  12. *  the Free Software Foundation; either version 2 of the License, or
  13. *  (at your option) any later version.
  14. *
  15. *  This program is distributed in the hope that it will be useful,
  16. *  but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  18. *  GNU General Public License for more details.
  19. *
  20. *  You should have received a copy of the GNU General Public License
  21. *  along with this program; if not, write to the Free Software
  22. *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  23. */
  24.  
  25. #include <stdio.h>
  26. #include <stdlib.h>
  27. #include <string.h>
  28. #include <unistd.h>
  29. #include <arpa/inet.h>
  30. #include <sys/types.h>
  31. #include <sys/socket.h>
  32.  
  33. #define BUFSIZE 100
  34.  
  35. void error_handling(char *message);
  36.  
  37. int main(int argc, char **argv)
  38. {
  39.     int serv_sock;
  40.     int clnt_sock;
  41.     char message[BUFSIZE];
  42.     int str_len;
  43.  
  44.     struct sockaddr_in serv_addr;
  45.     struct sockaddr_in clnt_addr;
  46.     int clnt_addr_size;
  47.    
  48.     if(argc != 2) {
  49.         printf("Usage : %s <port>\n", argv[0]);
  50.         exit(1);
  51.     }
  52.    
  53.     serv_sock = socket(PF_INET, SOCK_STREAM, 0);
  54.     if(serv_sock == -1)
  55.         error_handling("socket() error!");
  56.    
  57.     memset(&serv_addr, 0, sizeof(serv_addr));
  58.     serv_addr.sin_family = AF_INET;
  59.     serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  60.     serv_addr.sin_port = htons(atoi(argv[1]));
  61.    
  62.     if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
  63.         error_handling("bind() error!");
  64.    
  65.     if(listen(serv_sock, 5) == -1)
  66.         error_handling("listen() error!");
  67.    
  68.     clnt_addr_size = sizeof(clnt_addr);
  69.     clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
  70.        
  71.     if(clnt_sock == -1)
  72.         error_handling("accept() error!");
  73.    
  74.     sleep(5);
  75.     str_len = read(clnt_sock, message, BUFSIZE);        /* message receive */
  76.     write(clnt_sock, message, str_len);       /* message send */
  77.    
  78.     close(clnt_sock);
  79.     return 0;
  80. }
  81.  
  82. void error_handling(char *message)
  83. {
  84.     fputs(message, stderr);
  85.     fputc('\n', stderr);
  86.     exit(1);
  87. }
  88.  


 다음은 클라이언트쪽 코드이다.

bnd_client.c (Language : c)
  1. /***************************************************************************
  2. *            bnd_client.c
  3. *
  4. *  Fri Jan  4 18:37:52 2008
  5. *  Copyright  2008  pchero21
  6. *  pchero21@gmail.com
  7. ****************************************************************************/
  8.  
  9. /*
  10. *  This program is free software; you can redistribute it and/or modify
  11. *  it under the terms of the GNU General Public License as published by
  12. *  the Free Software Foundation; either version 2 of the License, or
  13. *  (at your option) any later version.
  14. *
  15. *  This program is distributed in the hope that it will be useful,
  16. *  but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  18. *  GNU General Public License for more details.
  19. *
  20. *  You should have received a copy of the GNU General Public License
  21. *  along with this program; if not, write to the Free Software
  22. *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  23. */
  24.  
  25. #include <stdio.h>
  26. #include <stdlib.h>
  27. #include <string.h>
  28. #include <unistd.h>
  29. #include <arpa/inet.h>
  30. #include <sys/types.h>
  31. #include <sys/socket.h>
  32.  
  33. void error_handling(char *message);
  34.  
  35. int main(int argc, char **argv)
  36. {
  37.     int sock;
  38.     int str_len, i;
  39.     struct sockaddr_in serv_addr;
  40.    
  41.     char msg1[] = "Hello Everybody!";
  42.     char msg2[] = "I am so happy!!!";
  43.     char message[10];
  44.    
  45.     if(argc != 3) {
  46.         printf("Usage : %s <IP> <port> \n", argv[0]);
  47.         exit(1);
  48.     }
  49.  
  50.     sock = socket(PF_INET, SOCK_STREAM, 0);
  51.     if(sock == -1)
  52.         error_handling("socket() error!");
  53.    
  54.     memset(&serv_addr, 0, sizeof(serv_addr));
  55.     serv_addr.sin_family = AF_INET;
  56.     serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
  57.     serv_addr.sin_port = htons(atoi(argv[2]));
  58.    
  59.     if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
  60.         error_handling("connect() error!");
  61.    
  62.     write(sock, msg1, strlen(msg1));    /* 메시지 1차 전송 */
  63.     write(sock, msg2, strlen(msg2));    /* 메시지 2차 전송 */
  64.     sleep(10);
  65.    
  66.     for(i = 0; i < 4; i++) {        /* 메시지 총 4번 수신 */
  67.         str_len = read(sock, message, sizeof(message) - 1);
  68.         message[str_len] = 0;
  69.         printf("서버로부터 전송된 메시지 : %s \n", message);
  70.     }
  71.     close(sock);
  72.    
  73.     return 0;
  74. }
  75.  
  76. void error_handling(char *message)
  77. {
  78.     fputs(message, stderr);
  79.     fputc('\n', stderr);
  80.     exit(1);
  81. }
  82.  


 실행 화면

사용자 삽입 이미지

사용자 삽입 이미지


    5-3. TCP의 내부 구조

 1) 첫 번째 단계 : 연결 설정
 소켓은 전 이중(full-duplex)모드이므로 데이터를 양방향으로 주고 받을 수 있다. 따라서 데이터 송,수신을 하기 전에 준비 과정이 필요하다.

사용자 삽입 이미지

 송신 호스트가 SYN Flag값을 1로 설정한 TCP Packet과 임의의 Sequence Number를 수신 호스트로 보낸다.
    Ex)  송신  -------------------------->  수신
                     SYN=1, SEQ=J

 수신 호스트가 Session성립을 원하면 SYN Flag를 1로 설정하고 Ack를 송신 호스트가 보낸 SEQ번호의 다음 번호로 정하고 수신 호스트에 따로 설정한 SEQ번호를 보낸다.
    Ex)  송신  <--------------------------  수신
             SYN=1, ACK=J+1, SEQ=K

 송신 호스트는 ACK 값을 수신지 호스트가 보낸 SEQ번호의 다음 번호로 정하여 수신 호스트에 보낸다.
    Ex)  송신  -------------------------->  수신
                        ACK=K+1

 서로간에 데이터를 주고 받을 수 있다는 것을 완벽히 확인하는데 세 번의 패킷 전송이 있었다. 따라서 이것을 가리켜 three-way handshaking이라 한다.

 2) 두 번째 단계 : 데이터 송,수신
 송,수신 단계에서의 흐름 제어(flow control)은 첫 번째 단계보다 훨씬 복잡하다. 여러가지 흐름 제어 기법들이 사용되는데 여기서는 기본적인 개념만 소개한다.

 Sender 와 Receiver 서로 데이터를 주고 받으면서 SEQ를 받은 데이터의 수만큼 증가 시키고 ACK 를 보낸다. 만약 ACK가 시간내에 수신되지 않는다면 패킷을 보낸측은 해당 패킷을 재전송한다.

 3) 세 번째 단계 : 연결 종료
사용자 삽입 이미지
위의 그림에서 먼저 initialtor 가 Responder 에게 종료 요청의 메시지를 담은 패킷을 전송한다.

 Responder 는 종료 메시지를 잘 받았다는 ACK 를 전송한다. 이것은 아직 Responder가 아직 종료할 상황이 아니라는 것을 의미한다. 예를 들어 Responder는 아직도 전송해야 할 데이터가 남아 있는 상황을 생각 해 볼 수 있다.

 다음 Responder 도 Initialtor 에게 종료 요청 메시지를 보낸다.

 마지막으로 initialtor가 최종적인 수신 응답 메시지 ACK를 전송하며 연결은 종료된다.


 아마도 한번에 이해하기가 힘들 것이다. 더 자세히 알고 싶다면 전문적인 네트워킹 자료를 찾아보길 바란다.