본문 바로가기

TCP/IP

TCP 클라이언트

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

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

TCPEchoClient4.c는 IPv4 기반의 TCP에코 클라이언트를 구현한 코드이다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Practical.h"


int main(int argc, char *argv[])
{
  char *servIP = argv[1];    //첫 번째 인자: 서버IP주소(dotted 형식)
  char *echoString = argv[2];  //두 번째 인자: 보내려는 에코 문자열
  int sock;
  struct sockaddr_in servAddr;      //서버주소
  int rtnVal;
  in_port_t servPort;
  size_t echoStringLen;
  ssize_t numBytes;
  unsigned int totalBytesRcvd = 0;
  
  if(argc < 3 || argc > 4)  //명령어 인자의 정확한 개수 확인
  {
    DieWithUserMessage("Parameter(s)","<Server Address><Echo Word>[<Server Port>]");
  }


  //세 번째 인자(선택): 서버포트(숫자형식). 7이 잘 알려진 포트로 생략 시 7을 사용
  servPort = (argc == 4) ? atoi(argv[3]) : 7;

  //TCP를 사용하여 안정된 스트림 소켓 생성
  sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if(sock < 0)
  {
    DieWithSystemMessage("socket() failed");
  }

  //서버 주소 구조체 생성
  memset(&servAddr, 0sizeof(servAddr));  //0으로 구조체 초기화
  servAddr.sin_family = AF_INET;      //IPv4주소 패밀리

  //주소 변환
  rtnVal = inet_pton(AF_INET, servIP, &servAddr.sin_addr.s_addr);
  if(rtnVal == 0)
  {
    DieWithUserMessage("inet_pton() failed""invalid address string");
  }

  else if(rtnVal < 0)
  {
    DieWithSystemMessage("inet_pton() failed");
  }

  servAddr.sin_port = htons(servPort);  //서버포트

  //에코 서버에 연결 설정
  if(connect(sock, (struct sockaddr *) &servAddr, sizeof(servAddr)) < 0)
  {
    DieWithSystemMessage("connect() failed");
  }

  echoStringLen = strlen(echoString);  //입력받은 문자열의 길이를 확인

  //서버에 에코 문자열 전송
  numBytes = send(sock, echoString, echoStringLen, 0);
  if(numBytes < 0)
  {
    DieWithSystemMessage("send() failed");
  }

  else if(numBytes != echoStringLen)
  {
    DieWithUserMessage("send()""sent unexeected number of bytes");
  }

  //서버로부터 동일한 문자열 수신
  totalBytesRcvd = 0;  //수신한 문자 개수
  fputs("Received: ", stdout);    //돌려받은 에코 문자열 출력을 위한 설정
  while(totalBytesRcvd < echoStringLen)
  {
    char buffer[BUFSIZE];  //입출력 버퍼
    
/*버퍼크기(byte)만큼 서버로부터 수신
     * (널 문자를 위해 1바이트 남겨놓음)*/

    numBytes = recv(sock, buffer, BUFSIZE - 10);
    if(numBytes < 0)
    {
      DieWithSystemMessage("recv() failed");
    }

    else if(numBytes == 0)
    {
      DieWithUserMessage("recv()""connection closed prematurely");
    }

    totalBytesRcvd += numBytes;  //총 받은 크기를 기록
    buffer[numBytes] = '\0';  //널 문자를 추가하여 문자열 완성
    fputs(buffer, stdout);    //에코 버퍼를 출력
  }

  fputc('\n', stdout);  //마지막으로 개행문자 출력

  close(sock);
  exit(0);
}


Practical.h

#ifndef
 __Practical_h__
#define __Practical_h__

#define BUFSIZE 100

void DieWithUserMessage(const char *msg, const char *detail);

void DieWithSystemMessage(const char *msg);

#endif


DieWithMessage.c

#include
<stdio.h>
#include<stdlib.h>


void DieWithUserMessage(const char *msg, const char *detail)
{
  fputs(msg, stderr);
  fputs(": ", stderr);                                 
    fputs(detail, stderr);
    fputc('\n', stderr);
    exit(1);
}
void DieWithSystemMessage(const char *msg)
{
  perror(msg);
  exit(1);
}


 

IPv4 TCP 서버
서버의 역할은 통신 종단점의 설정과 클라이언트의 접속을 기다리는 것이다. TCP서버는 다음과 같은 4단계의 통신 과정을 일반적으로 가지게 된다.

1. socket()를 통해 TCP 소켓을 생성
2. bind()를 통해 소켓에 포트 번호를 할당
3. listen()을 통해 해당 포트가 연결을 받아들이도록 시스템에 알림
4. 다음과 같은 일을 계속적으로 반복
 - accept()를 통해 각 클라이언트와 통신에 필요한 새로운 소켓을 획득
 - 새롭게 생성된 클라이언트 소켓에 send(), recv()를 호출하여 통신을 수행
 - close()를 통해 클라이언트와 연결을 종료

소켓의 생성, 데이터 송신, 수신, 소켓 종료 등은 클라이언트와 동일하다. 서버가 소켓을 사용하는데 있어서 클라이언트와 다른 점은 주소를 소켓에 바인드(bind)한다는 것이며, 그 소켓을 클라이언트와 연결이 되는 또 다른 소켓을 생성하는 매개체로 이용한다는 것이다. 서버가 클라이언트와 통신하는 과정은 단순하다.
클라이언트로부터 데이터를 받아서 동일한 데이터를 클라이언트에게 다시 돌려보내게 되며, 이 과정은 클라이 언트가 연결을 종료할 때까지 반복한다.

TCPEchoServer4.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Practical.h"

static const int MAXPENDING = 5;  //연결 요청을 대기할 수 있는 최대수

int main(int argc, char *argv[])
{
  in_port_t servPort;
  int servSock;  //서버 소켓 식별자
  struct sockaddr_in servAddr;          //지역 주소
  char clntName[INET_ADDRSTRLEN];  //클라이언트 주소를 담기 위한 문자열
  
  if(argc != 2)  //명령어 인자의 개수 확인
  {
    DieWithUserMessage("Parameter(s)""<Server Port>");
  }

  servPort = atoi(argv[1]);  //첫 번째 인자 : 지역 포트

  //연결 요청을 처리하는 소켓 생성

  if((servSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
  {
    DieWithSystemMessage("socket() failed");
  }

  //지역 주소 구조체 생성
  memset(&servAddr, 0sizeof(servAddr));      //0으로 구조체 초기화
  servAddr.sin_family = AF_INET;          //IPv4 주소 패밀리
  servAddr.sin_addr.s_addr = htonl(INADDR_ANY);  //호스트의 어떠한 IP로도 연결 요청 수락
  servAddr.sin_port = htons(servPort);      //지역 포트

  //지역 주소에 바인드
  if(bind(servSock, (struct sockaddr *) &servAddr, sizeof(servAddr)) < 0)
  {
    DieWithSystemMessage("bind() failed");
  }

  //소켓이 들어오는 요청을 처리할 수 있도록 설정
  if(listen(servSock, MAXPENDING) < 0)
  {
    DieWithSystemMessage("listen() failed");
  }

  for(;;)
  {
    struct sockaddr_in clntAddr;  //클라이언트 주소
    //클라이언트 주소 구조체의 크기설정
    socklen_t clntAddrLen = sizeof(clntAddr);

    //클라이언트의 연결을 기다림
    int clntSock = accept(servSock, (struct sockaddr *) &clntAddr, &clntAddrLen);
    if(clntSock < 0)
    {
      DieWithSystemMessage("accept() failed");
    }

    //clntSock가 클라이언트와 연결됨!
    if(inet_ntop(AF_INET, &clntAddr.sin_addr.s_addr, clntName, sizeof(clntName)) != NULL)
    {
      printf("Handling client %s/%d\n", clntName, ntohs(clntAddr.sin_port));
    }

    else
    {
      puts("Unable to get client address");
    }

  HandleTCPClient(clntSock);
  }
  //실행되지 않음
  return 0;
}


HandleTCPClient.c

#include <stdio.h>
#include <stdlib.h>

#define BUFSIZE 100

void HandleTCPClient(int clntSocket)
{
  char buffer[BUFSIZE];  //에코 문자열을 위한 버퍼
  ssize_t numBytesSent;
  ssize_t numBytesRcvd;

  //클라이언트로부터 메시지 수신
  numBytesRcvd = recv(clntSocket, buffer, numBytesRcvd, 0);
  if(numBytesRcvd < 0)
  {
    DieWithSystemMessage("recv() failed");
  }

  //수신한 문자열을 전송하고 여분의 데이터를 스트림이 끝날 때까지 수신
  while(numBytesRcvd > 0)
  {
    //클라이언트로 에코 메시지를 돌려보냄
    numBytesSent = send(clntSocket, buffer, numBytesRcvd, 0);
    if(numBytesSent < 0)
    {
      DieWithSystemMessage("sent() failed");
    }

    else if(numBytesSent != numBytesRcvd)
    {
      DieWithSystemMessage("sent()""sent unexpected number of bytes");
    }

    //받을 수 있는 데이터가 더 남아 있는지 확인
    numBytesRcvd = recv(clntSocket, buffer, BUFSIZE, 0);
    if(numBytesRcvd < 0)
    {
      DieWithSystemMessage("recv() failed");
    }
  }
  close(clntSocket);  //클라이언트 소켓 종료
}


실행
먼저 각 함수들 *.o파일을 생성한다.
gcc -c 함수입력한파일.c
그러면 함수입력한파일.o가 생성된다.

그 다음 server와 client실행파일 생성
예) gcc -o TCPEchoServer4 TCPEchoServer4.c HandleTCPClient.o
예) gcc -o TCPEchoClient4 TCPEchoClient4.c DieWithMessage.o
이렇게 실행파일 생성 후

서버 실행
TCPEchoServer4 포트번호

클라이언트 실행
TCPEchoClient4 IP주소 "Hello world" 포트번호

결과



'TCP/IP' 카테고리의 다른 글

프로세스간 통신  (0) 2011.07.20
양방향 통신(1:1 대화)  (0) 2011.07.19
inet_addr()함수, (=inet_aton)  (0) 2011.07.01
호스트pc(리틀엔디안) <-> 네트워크(빅엔디안)  (0) 2011.07.01
gethostbyname() 함수  (0) 2011.07.01