이전 시간에 POLL을 이용하여 클라이언트 접속 요청에 대해 연결을 허락하고 클라이어언트와 데이터를 주고 받는 방법에 대해 말씀을 드렸습니다. 이번 시간에는 클라이언트에서 요구하는 이미지 파일을 전송하는 부분에 대해 말씀을 드리겠습니다.

역시, 프로그램부터 보시겠습니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/poll.h>
#include <sys/types.h>
#include <sys/stat.h>

#define  BUFF_SIZE         1024

#define  POLL_MAX_COUNT    2
#define  POLL_SERVER       0
#define  POLL_CLIENT       1

int      server_socket;
int      client_socket;

char     buff[BUFF_SIZE+5];
char     file_name[BUFF_SIZE+5];

struct   pollfd  poll_array[POLL_MAX_COUNT];                // poll 배열

void  connect_client( void)
{
   struct   sockaddr_in   client_addr;
   int      client_addr_size;

   client_addr_size  = sizeof( client_addr);
   client_socket     = accept( server_socket, (struct sockaddr*)&client_addr, &client_addr_size);

   if ( -1 == client_socket)
   {
      printf( "클라이언트 연결 수락 실패\n");
      return;
   }
   printf( "클라이언트 연결 성공\n");

   poll_array[POLL_CLIENT].fd       = client_socket;        // 클라이언트 소켓은 아직 없음
   poll_array[POLL_CLIENT].events   = POLLIN ¦ POLLERR ¦ POLLHUP;
   poll_array[POLL_CLIENT].revents  = 0;
}

void  disconnect_client( void)
{
   if ( 0 < client_socket)
   {
      printf( "클라이언트 연결 종료\n");

      close( client_socket);
   }
   poll_array[POLL_CLIENT].fd    = -1;
}

void  read_client( void)
{
   struct   stat  file_info;
   int      fd;
   int      sz_file;
   int      sz_read;

   sz_read  = read ( client_socket, file_name, BUFF_SIZE);
   if ( 0 < sz_read)
   {
      file_name[sz_read+1]  = '\0';                                  // 파일 이름 끝에 NULL이 없을 경우를 위해
      if ( 0 > stat( file_name, &file_info))
         printf( "파일이 없습니다.\n");
      else
      {
         sz_file  = file_info.st_size;

         write( client_socket, (char *)&sz_file, sizeof( sz_file));  // 파일의 크기 전송

         fd = open( file_name, O_RDONLY);
         while( 0 < ( sz_read = read( fd, buff, BUFF_SIZE)))
            write( client_socket, (char *)buff     , sz_read);       // 파일 전송

         close( fd);
      }
   }
   else                                                     // POLLIN 임에도 읽을 자료가 없다면
   {                                                        // 연결이 끊긴 것임
      disconnect_client();
   }
}

int   main( void)
{
   struct   sockaddr_in   server_addr;
   int      option;
   int      poll_state;

   server_socket  = socket( PF_INET, SOCK_STREAM, 0);
   option         = 1;                                      // SO_REUSEADDR 의 옵션 값을 TRUE 로
   setsockopt( server_socket, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option));

   if( -1 == server_socket)
   {
      printf( "server socket 생성 실패\n");
      exit( 1);
   }

   memset( &server_addr, 0, sizeof( server_addr));
   server_addr.sin_family     = AF_INET;
   server_addr.sin_port       = htons( 4000);
   server_addr.sin_addr.s_addr= htonl( INADDR_ANY);

   if( -1 == bind( server_socket, (struct sockaddr*)&server_addr, sizeof( server_addr) ) )
   {
      printf( "bind() 실행 에러\n");
      exit( 1);
   }

   if( -1 == listen(server_socket, 5))
   {
      printf( "대기상태 모드 설정 실패\n");
      exit( 1);
   }

   poll_array[POLL_SERVER].fd       = server_socket;        // poll 에 디스크립터를 등록한다.
   poll_array[POLL_SERVER].events   = POLLIN;

   poll_array[POLL_CLIENT].fd       = -1;                   // 클라이언트 소켓은 아직 없음

   while( 1)
   {
      poll_state = poll( (struct pollfd *)&poll_array, POLL_MAX_COUNT, 1000);
      if ( 0 > poll_state)
      {
         printf( "치명적인 에러 발생\n");
         return -1;
      }
      if ( 0 < poll_state)
      {
         if( poll_array[POLL_SERVER].revents & POLLIN)      // 연결 요청을 받음
         {
            connect_client();
         }
         if( poll_array[POLL_CLIENT].revents & POLLIN )     // 클라이언트로부터 자료 수신
         {
            read_client();
         }
         if( poll_array[POLL_CLIENT].revents & ( POLLHUP ¦ POLLERR )) // 라인이 끊어 졌거나 에러 발생
         {
            disconnect_client();
         }
      }
   }
   return 0;
}

클라이언트로부터 요구하는 파일 이름을 받으면 그 파일을 전송하는 루틴에 대해서는 색상을 달리해서 출력했습니다. 그 부분의 루틴을 보겠습니다.

void  read_client( void)
{
                  :
   sz_read  = read ( client_socket, file_name, BUFF_SIZE);
   if ( 0 < sz_read)
   {
      file_name[sz_read+1]  = '\0';              // 파일 이름 끝에 NULL이 없을 경우를 위해
      if ( 0 > stat( file_name, &file_info))    // stat()함수로 파일이 있는지 확인합니다.
         printf( "파일이 없습니다.\n");
      else
      {
         sz_file  = file_info.st_size;

         write( client_socket, (char *)&sz_file, sizeof( sz_file));  // 파일의 크기 전송

         fd = open( file_name, O_RDONLY);
         while( 0 < ( sz_read = read( fd, buff, BUFF_SIZE)))
            write( client_socket, (char *)buff     , sz_read);       // 파일 전송

         close( fd);
      }
   }
   else                                        // POLLIN 임에도 읽을 자료가 없다면
   {                                            // 연결이 끊긴 것임
      disconnect_client();
   }
}

파일 전송을 아래와 같이 파일의 크기와 파일 내용을 차례로 전송합니다.

파일 크기 파일 내용
integer( 4 bytes) binary(파일 크기 bytes)

예제에서는 단순하게 구성되었지만 필요한 내용을 추가하면 되겠습니다만, 당연히 상대 방과 전송 순서 및 각 요소의 크기를 미리 정해져야 겠습니다. 가령 아래처럼 말이죠.

파일 종류 파일 크기 파일이름 파일 내용
char( 1 bytes) integer( 4 bytes) string(20 bytes) binary(파일 크기 bytes)

파일을 요청하는 클라이언트를 MS Windows용으로 만들어 첨부합니다. 클라이언트에서는 파일 크기와 파일 내용 순서로 읽어 들이면 되겠습니다.

클라이언트 프로그램은 아래와 같이 수신된 데이터에서 파일의 크기를 구하고 파일을 읽어 들입니다.

char *rxbuff;

rxbuff = read();                  // 소켓으로부터 데이터를 수신합니다.
image_size = *( integer *)rxbuff; // 수신된 첫 부분의 integer로 파일 크기를 구합니다.

............. image_size 만큼 계속 서버로부터 데이터를 수신 ..............

간단하게 적었습니다만 대부분 event dreven 방식으로 처리하기 때문에 위와 같이 한번에 자료를 수신하기는 어렵습니다. 제가 애용하는 방법은 switch()문을 이용하는 것입니다.

int RcvData( SOCKET socket, LPSTR pBuf, int szReceive)
{
   for ( ndx = 0; ndx < nBufSize)   // 수신된 바이트별로
   {
      rcvBuff[ndxBuff++] = lpszbuf[ndx];  // 한개의 바이트 씩 버퍼에 저장

      switch( branch)               // branch
      {
      case BR_GET_INFO  :  if ( HEADER_SIZE == ndxBuff)
                           {
                              szFile   = *(integer *)rcvBuff;  // 파일 크기를 구하고
                              branch   = BR_RCV_DATA;          // 다음 바이트부터는 파일로 처리
                              ndx_buff = 0;
                           }
                           break;
      case BR_RCV_DATA  :  if ( ndxBuff == szFile)
                           {
                              OnReceiveFile( ndxBuff);
                              branch   = BR_GET_INFO;
                              ndxBuff = 0;
                           }
                           break;
      }
}

MS Windows에서 제가 애용하는 개발툴이 델파이라서 파스칼 코드를 C로 바꾸 올렸습니다. 그러므로 컴파일해서 실행해본 코드는 아니지만 이런 식으로 작성을 합니다.

RcvData()는 소켓으로부터 자료가 수신되고 소케 버퍼에 데이터가 쌓이면 발생되는 이벤트 핸들러로 하나의 파일을 전송하더라도 여러번 발생할 수 있습니다. 즉, 하나의 파일을 전송했지만 상대편에서 여러 번으로 나누어서 전송할 수 있으며, 특히 TCP/IP는 UDP/IP와 달리 한개의 전송 데이터가 여러 개로 나뉘어 수신될 수 있기 때문입니다.

참고 내용
    TCP/IP에 대해 더 자세한 내용을 알고 싶으시면 본 포럼의 강좌 게시판에 올려진 아래의 글을 참고하여 주십시오.
  • TCP/IP 1부
  • TCP/IP 2부

프로그램을 처음 시작할 때에는 branch는 BR_GET_INFO 값을 가지며, ndxBuff 도 0(영) 의 값으로 시작합니다.