이번 Chapter에서는 포준 입출력 함수를 이용한 데이터 송수신 방법에 대해 소개한다.
- 표준 입출력 함수는 이식성(Portability)이 좋음
- 표준 입출력 함수는 버퍼링을 통한 성능 향상에 도움이 됨
첫 번째 내용에 대해 설명해보겠다.
ANSI C 기반의 표준 입출력 함수는 모든 컴파일러에서 지원을 하기 때문에 이식성이 좋다.
이는 사실 모든 표준 함수가 동일하다.
두 번째 내용에 대해 설명해보겠다.
표준 입출력 함수를 사용할 경우 추가적으로 입출력 버퍼를 제공받는다.
이 얘기를 하면 앞에서 했던 내용과 혼동될 수 있다.
왜냐하면 소켓을 생성하면 기본적으로 OS에서 입출력 버퍼가 생성되기 때문이다.
맞는 말이다. 이는 TCP 프로토콜을 진행하는데 매우 중요한 역할을 한다.
이와는 별도로 표준 입출력 함수를 사용하게 되면, 하나의 버퍼를 더 제공받는다.
- 표준 입출력 함수를 이용해서 데이터를 전송할 경우 그림과 같이 소켓의 입출력 버퍼 이외의 버퍼를 통해서 버퍼링이 된다.
예를 들어 fputs 함수를 통해서 "Hello" 문자열을 전송할 경우, 입출력 함수 버퍼에 데이터가 전달된다.
그 후, 소켓의 출력버퍼로 이동하고 상대방에게 문자열이 전송된다.
버퍼는 기본적으로 성능의 향상을 목적으로 한다.
하지만 소켓과 관련된 버퍼는 TCP 의 구현을 위한 목적이 강하다.
예를 들어 TCP의 경우 데이터가 분실되면 재전송을 한다. 그러기 위해서 사용되는 목적이 강하다.
반면에 표준 입출력 함수의 버퍼는 성능 향상만을 목적으로 제공된다.
버퍼링의 성능은 다음과 같은 관점에서 좋다.
1. 전송하는 데이터의 양
2. 출력버퍼로의 데이터 이동 횟수
1byte짜리 데이터를 총 열 번에 걸쳐(열 개의 패킷)에 보내는 경우와
이를 버퍼링해서 10byte짜리 패킷 하나로 묶어서 보내는 것은 다르다.
데이터의 전송을 위한 패킷에는 헤더정보라는 것이 추가된다.
헤더 정보와 패킷의 수를 곱해서 생각해보자.
추가로 소켓의 출력버퍼로 데이터를 이동시키는 데도 시간이 제법 많이 소모가 된다.
10회 이동시키는 것이 빠르겠는가 1회 이동시키는 것이 빠르겠는가
충분히 설명이 됐다고 생각하고 넘어가겠다.
시스템 함수와 표준 입출력 함수를 이용하여 비교하여 보여주겠다.
시스템 함수
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define BUF_SIZE 3
int main(int argc, char *argv[])
{
int fd1, fd2, len;
char buf[BUF_SIZE];
fd1=open("news.txt", O_RDONLY);
fd2=open("cpy.txt", O_WRONLY|O_CREAT|O_TRUNC);
while((len=read(fd1, buf, sizeof(buf)))>0)
write(fd2, buf, len);
close(fd1);
close(fd2);
return 0;
}
표준 입출력 함수
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#define BUF_SIZE 3
int main(int argc, char *argv[])
{
FILE * fp1;
FILE * fp2;
char buf[BUF_SIZE];
fp1=fopen("news.txt", "r");
fp2=fopen("cpy.txt", "w");
while(fgets(buf, BUF_SIZE, fp1)!=NULL)
fputs(buf, fp2);
fclose(fp1);
fclose(fp2);
return 0;
}
- 시스템 함수를 이용한 파일 복사
버퍼링 없는 파일 복사
open(), read(), write() 함수- 표준 입출력 함수를 이용한 파일 복사
버퍼링 기반의 파일 복사
fopen(), fgets(), fputs() 함수
300Mbytes 이상의 파일을 대상으로 테스트 시 속도차이가 매우 크다는 것을 알 수 있다.
추가적으로 파일 복사를 통해 속도 차이를 알아보겠다
시스템 함수 기반의 파일 복사
#include <stdio.h>
#include <fcntl.h>
#include <time.h>
#include <unistd.h>
#define BUF_SIZE 3
int main(int argc, char *argv[])
{
int fd1, fd2;
int len;
char buf[BUF_SIZE];
unsigned long nano = 1000000000;
unsigned long t1,t2;
struct timespec start, end;
if(argc != 3)
{
printf("Usage: %s <src_file> <dest_file>\n", argv[0]);
return -1;
}
fd1 = open(argv[1], O_RDONLY);
fd2 = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC);
clock_gettime(CLOCK_REALTIME, &start);
t1 = start.tv_nsec + start.tv_sec * nano;
while((len = read(fd1, buf, sizeof(buf))) > 0)
write(fd2, buf, len);
clock_gettime(CLOCK_REALTIME, &end);
t2 = end.tv_nsec + end.tv_sec * nano;
printf("syscpy elapsed time : %ld milliseconds\n", (t2-t1)/1000000);
close(fd1);
close(fd2);
return 0;
}
시스템 함수 기반의 파일 복사
#include <stdio.h>
#include <fcntl.h>
#include <time.h>
#include <unistd.h>
#define BUF_SIZE 3
int main(int argc, char *argv[])
{
FILE *f1;
FILE *f2;
char buf[BUF_SIZE];
unsigned long nano = 1000000000;
unsigned long t1,t2;
struct timespec start, end;
if(argc != 3)
{
printf("Usage: %s <src_file> <dest_file>\n", argv[0]);
return -1;
}
f1 = fopen(argv[1], "r");
f2 = fopen(argv[2], "w");
clock_gettime(CLOCK_REALTIME, &start);
t1 = start.tv_nsec + start.tv_sec * nano;
while(fgets(buf,BUF_SIZE, f1) != NULL)
fputs(buf, f2);
clock_gettime(CLOCK_REALTIME, &end);
t2 = end.tv_nsec + end.tv_sec * nano;
printf("Stdcpy elapsed time : %lld milliseconds\n", (t2-t1)/1000000);
fclose(f1);
fclose(f2);
return 0;
}
- 실행결과
시간을 측정하는 함수를 사용하여 구현하였다.
실행 결과는 30.5MB 파일을 대상으로 복사했을 때 약 26.5배의 차이가 발생하였다.
무조건 좋은 점만 있는 것은 아니다. 다음과 같은 단점이 있다.
1. 양방향 통신이 쉽지 않다.
2. 상황에 따라서 fflush 함수의 호출이 빈번히 등장할 수 있다.
3. 파일 디스크립터를 FILE 구조체의 포인터로 반환해야한다.

위에서 파일 디스크립터를 FILE 포인터로 변환해야 함을 알았다.
이번에는 그 방법을 설명하겠다.
소켓의 생성으로 반환된 파일 디스크립터를 FILE 포인터로 변환하는 일은 fdopen 함수를 사용한다.
#include <stdio.h>
FILE* fdopen(int fildes, const char * mode);
// 성공 시 변환된 FILE 구조체 포인터, 실패시 NULL 반환
/*
fildes : 변환할 파일 디스크립터를 인자로 전달
mode : 생성할 FILE 구조체 포인터의 모드(mode)정보 전달
*/
두 번째 매개 변수는 fopen과 동일하게 읽기 모드인 "w"와 쓰기 모드인 "w" 등이 있다.
#include <stdio.h>
#include <fcntl.h>
int main(void)
{
FILE *fp;
int fd=open("data.dat", O_WRONLY|O_CREAT|O_TRUNC);
if(fd==-1)
{
fputs("file open error", stdout);
return -1;
}
fp=fdopen(fd, "w");
fputs("Network C programming \n", fp);
fclose(fp);
return 0;
}
- 실행 결과
이번에는 fdopen 함수의 반대 기능을 제공하는 함수이다.
#include <stdio.h>
int fileno(FILE * stream);
// 성공 시 변환된 파일 디스크립터, 실패시 -1 반환
인자로 FILE 포인터를 전달하면 해당 파일의 파일 디스크립터가 반환된다.
#include <stdio.h>
#include <fcntl.h>
int main(void)
{
FILE *fp;
int fd=open("data.dat", O_WRONLY|O_CREAT|O_TRUNC);
if(fd==-1)
{
fputs("file open error", stdout);
return -1;
}
printf("First file descriptor: %d \n", fd);
fp=fdopen(fd, "w");
fputs("TCP/IP SOCKET PROGRAMMING \n", fp);
printf("Second file descriptor: %d \n", fileno(fp));
fclose(fp);
return 0;
}
- 실행 결과
같은 값이 출력되는 것으로 적절히 변환되었음을 증명하였다.
위에서 공부한 내용으로 소켓에 적용해보겠다.
이전에 구현했던 에코 서버와 에코 클라이언트를 표준 입출력 함수를 이용해서 데이터가 송수신하도록 변경하였다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char message[BUF_SIZE];
int str_len, i;
struct sockaddr_in serv_adr;
struct sockaddr_in clnt_adr;
socklen_t clnt_adr_sz;
FILE * readfp;
FILE * writefp;
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock==-1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
clnt_adr_sz=sizeof(clnt_adr);
for(i=0; i<5; i++)
{
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
if(clnt_sock==-1)
error_handling("accept() error");
else
printf("Connected client %d \n", i+1);
readfp=fdopen(clnt_sock, "r");
writefp=fdopen(clnt_sock, "w");
while(!feof(readfp))
{
fgets(message, BUF_SIZE, readfp);
fputs(message, writefp);
fflush(writefp);
}
fclose(readfp);
fclose(writefp);
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
일반적인 순서
1. 파일 디스크립터를 FILE 구조체 포인터로 변환
2. 표준 입출력 함수 호출
3. 함수 호출 시 fflush 함수 호출을 통해 버퍼를 비움
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_adr;
FILE * readfp;
FILE * writefp;
if(argc!=3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0);
if(sock==-1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("connect() error!");
else
puts("Connected...........");
readfp=fdopen(sock, "r");
writefp=fdopen(sock, "w");
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
fputs(message, writefp);
fflush(writefp);
fgets(message, BUF_SIZE, readfp);
printf("Message from server: %s", message);
}
fclose(writefp);
fclose(readfp);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
- 실행 결과
참고 : 윤성우의 열혈 TCP/IP 소켓 프로그래밍
Git : https://github.com/im2sh/Socket_Programming/tree/main/lab11