2011년7월7일_메세지를 보내고 TCP echo서버로 부터 메세지를 수신한 뒤에 종료하는 TCP/IP클라이언트(TCPEchoClient4.c)

 

● Interactive 서버 흐름

             Server                     Client
       ┌--> accept( )  <--------------------------->  connect( )
        |         ↓                                             ↓
        |   read( ) / write( )  <-------------------> read( ) / write( )  
        |         ↓                                             ↓
       └-----close( )                                      close( )

       ※ accept( ) ~ close( )를 반복.

 

● TCP/IP 소켓 프로그래밍C page.24 2.1 IPv4 TCP 클라이언트

소켓 인터페이스를 사용할 때 통신의 단계에 따라 서버와 클라이언트의 사용 방법이 각각 다르므로 클라이언트 및 서버의 구분은 매우 중요하다. 일단 클라이언트부터 알아보자. 클라이언트의 임무는 수동적으로 접속을 기다리고 있는 서버에게 통신을 개시하는 것이다.
전형적인 TCP클라이언트의 통신은 다음 4가지 단계를 가진다.

1. socket( )를 이용하여 TCP 소켓을 생성
2. connect( )를 이용하여 서버와의 연결을 설정
3. send( ), recv( )를 이용하여 통신을 수행
4. close( )를 이용하여 연결을 종료

다음의 TCPEchoClient4.c는 IPv4기반의 TCP 에코 클라이언트를 구현한 코드이다.
※ 상기의 글은 TCP/IP 소켓 프로그래밍C page.24에서 가져왔습니다. –_o


● TCPEchoClient.c

   1:  // TCP/IP 소켓프로그래밍C p.24  TCPEchoClient4.c
   2:  #include <stdio.h>
   3:  #include <stdlib.h>
   4:  #include <string.h>
   5:  #include <unistd.h>            //size_t(?)
   6:  #include <sys/types.h>
   7:  #include <sys/socket.h>
   8:  #include <netinet/in.h>
   9:  #include <arpa/inet.h>
  10:  #include "Practical.h"        //User함수들
  11:   
  12:  #define BUFSIZE 100
  13:   
  14:  int main(int argc, char *argv[])
  15:  {
  16:      char *servIP;                    //첫 번째 인자: 서버IP주소(dotted형식)
  17:      char *echoString;                //두 번째 인자: 보내려는 에코 문자열
  18:      in_port_t servPort;                //포트번호
  19:      int sock;                        //소켓 디스크럽터
  20:      struct sockaddr_in servAddr;    //서버 주소
  21:      int rtnVal;                        //주소값을 구조체에 대입시 에러처리값 저장용
  22:      size_t echoStringLen;            //입력받은 문자열의 길이를 확인
  23:      ssize_t numBytes;                //송수신시 문자의 수
  24:      unsigned int totalBytesRcvd;    //수신한 문자 개수
  25:      char buffer[BUFSIZE];            //입출력 버퍼
  26:      
  27:      if(argc < 3 || argc > 4)        //명령어 인자의 정확한 개수 확인
  28:      {
  29:          DieWithUserMessage("Parameter(s)",
  30:              "<Server Address> <Echo Word> [<Server Port>]");
  31:      }
  32:   
  33:      servIP = argv[1];        //첫 번째 인자: 서버IP주소(dotted형식)
  34:      echoString = argv[2];    //두 번째 인자: 보내려는 에코 문자열
  35:   
  36:      //세 번째 인자(선택): 서버 포트(숫자형식), 7이 잘 알려진 포트로 생략 시 7을 사용.
  37:      servPort = (4 == argc) ? atoi(argv[3]) : 7;
  38:   
  39:      //TCP를 사용하여 안정된 스트림 소켓 생성
  40:      sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  41:      
  42:      if(sock < 0)
  43:      {
  44:          DieWithSystemMessage("socket() failed");
  45:      }
  46:   
  47:      //서버 주소 구조체에 주소값 대입
  48:      memset(&servAddr, 0, sizeof(servAddr));    //0으로 구조체 초기화
  49:      servAddr.sin_family = AF_INET;                //IPv4 주소 패밀리
  50:   
  51:      //주소 변환
  52:      rtnVal = inet_pton(AF_INET, servIP, &servAddr.sin_addr.s_addr);
  53:      
  54:      if(0 == rtnVal)
  55:      {
  56:          DieWithSystemMessage("inet_pton() failed");
  57:      }
  58:      
  59:      servAddr.sin_port = htons(servPort);    //서버포트
  60:   
  61:      //에코서버에 연결 설정
  62:      if(connect(sock, (struct sockaddr *)&servAddr, sizeof(servAddr)) < 0)
  63:      {
  64:          DieWithSystemMessage("connect() failed");
  65:      }
  66:      
  67:      echoStringLen = strlen(echoString);        //입력받은 문자열의 길이를 확인
  68:   
  69:      //서버에 에코 문자열 전송
  70:      numBytes = send(sock, echoString, echoStringLen, 0);
  71:      
  72:      if(numBytes < 0)        //송신 실패시 에러처리
  73:      {
  74:          DieWithSystemMessage("send() failed");
  75:      }
  76:      else if(numBytes != echoStringLen)        //보낸 문자의 수와 문자열의 길이가 같지 않으면
  77:      {
  78:          DieWithUserMessage("send()", "sent unexpected number of bytes");
  79:      }
  80:   
  81:      //서버로부터 동일한 문자열 수신
  82:      totalBytesRcvd = 0;        //수신한 문자 개수 초기화
  83:      fputs("Received: ", stdout);            //돌려받은 에코 문자열 출력을 위한 설정
  84:   
  85:      while(totalBytesRcvd < echoStringLen)
  86:      {
  87:          buffer[BUFSIZE];        //입출력 버퍼
  88:          /* 버퍼크기(byte)만큼 서버로 부터 수신
  89:           * (NULL 문자를 위해 1Byte 남겨놓음.) */
  90:          numBytes = recv(sock, buffer, BUFSIZE - 1, 0);
  91:   
  92:          if(numBytes < 0)
  93:          {
  94:              DieWithSystemMessage("recv() failed");
  95:          }
  96:          else if(0 == numBytes)
  97:          {
  98:              DieWithUserMessage("recv()", "connection closed prematurely");
  99:          }
 100:   
 101:          totalBytesRcvd = totalBytesRcvd + numBytes;            //총 받은 크기를 기록
 102:          buffer[numBytes] = '\0';                            //NULL 문자를 추가하여 문자열 완성!
 103:          fputs(buffer, stdout);                                //에코 버퍼를 출력
 104:      }
 105:   
 106:      fputc('\n', stdout);            //마지막으로 개행문자 출력
 107:   
 108:      close(sock);
 109:      exit(0);
 110:  }
 111:      



● Practical.h

   1:  #ifndef _PRACTICAL_H_        //다음 _PRACTICAL_H_이 define되어 있지 않으면,
   2:  #define _PRACTICAL_H_        //함수들의 원형이 선언되어 있지 않으면,
   3:  //함수들의 원형 선언
   4:  void DieWithUserMessage(const char *, const char *);
   5:  void DieWithSystemMessage(const char *);         
   6:  #endif // _PRACTICAL_H_  #ifndef의 끝을 알려줌.
   7:   



● DieWithMessage.c

   1:  #include <stdio.h>
   2:  #include <stdlib.h>
   3:   
   4:  void DieWithUserMessage(const char *msg, const char *detail)
   5:  {
   6:      fputs(msg, stderr);
   7:      fputs(": ", stderr);
   8:      fputs(detail, stderr);
   9:      fputc('\n', stderr);
  10:      exit(1);
  11:  }
  12:   
  13:  void DieWithSystemMessage(const char *msg)
  14:  {
  15:      perror(msg);
  16:      exit(1);
  17:  }



● 분할컴파일

1. 소스파일 하나씩 따로 컴파일하는 법
  # gcc –c DieWithMessage.c
  # gcc –o TCPEchoClient TCPEchoClient.c DieWithMessage.o

2. Makefile을 이용하여 쉽게 하는 법 (추천)

#주석: TCP echo Client Project
CC = gcc
RUN = TCPEchoClient4

all: Practical.h TCPEchoClient4.o DieWithMessage.o
    $(CC) -o $(RUN) TCPEchoClient4.o DieWithMessage.o

run: all
    ls -al
    ./$(RUN)

clean:
    rm *.o
    rm $(RUN)


  # make

※Macro를 더 선언하면 더 보기 좋을지도..


● 컴파일하여 실행하기 전에 linux TCP echo 서비스켜기

root디렉토리에서,
# setup

image

System services에 들어간다.

image 

서비스목록이 보이고 아래방향 화살표를 계속 누르면,


image image

echo와 echo-udp가 보인다. 스페이스를 눌러 체크하자.
echo는 TCP echo 서버를 만들어 서비스해준다는 것이고,
echo-udp는 UDP echo 서버를 만들어 서비스해준다는 것이다.
모두 loopback주소인 127.0.0.1 Port번호 7로 접속해야 한다.

setup에서 나와,

image

# /etc/init.d/xinetd restart

xinetd는 Extended Internet Services daemon라고 하며 슈퍼데몬으로 상기의 서비스들을 돌리는 프로세스이다.
그래서 xinetd을 재시작해줘야 서비스가 동작할 수 있다. (?)
사실 잘 모르겠습니다. ㅜㅜ 데몬이 뭔지도 까먹었음. ㅠㅠ


● 실행결과


image


//소스분석은 집에 가서 하거나 내일~

● TCP echo client 소스코드의 흐름.

1. 명령어 인자확인 후 적거나 많으면 에러처리.
2. 명령어 인자1의 IP주소 문자열을 *servIP가 가리키게함.
3. 명령어 인자2의 메세지 문자열을 *echoString이 가리키게함.
4. 명령어 인자3이 없으면 디폴트로 7번 포트를 사용하게 하고, 있으면 문자열을 정수로 바꾸어 servPort에 대입.
5. socket( ) – 소켓생성.
6. 서버구조체 생성과 주소변환 후 할당.
7. connect( ) – 연결요청.
8. send( ) – 메세지(인자2) 전송.
9. recv( ) – 메세지 수신.
10. fputs( ) – 메세지를 화면에 출력.
11. close( ) – 소켓소멸.
12. exit( ) – 프로그램 종료.

// 그림을 그려 서버와의 관계도 표현할까?



참조(Reference)





DSCN3525 DSCN3526