이전 시간에 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와 달리 한개의 전송 데이터가 여러 개로 나뉘어 수신될 수 있기 때문입니다.
프로그램을 처음 시작할 때에는 branch는 BR_GET_INFO 값을 가지며, ndxBuff 도 0(영) 의 값으로 시작합니다.