표준 입출력 함수의 두 가지 장점
표준 입출력 함수는 이식성(Portability)이 좋음
표준 입출력 함수는 버퍼링을 통한 성능 향상에 도움이 됨
표준 입출력 함수는 이식성(Portability)이 좋음
ANSI C 기반의 표준 입출력 함수는 모든 컴파일러에서 지원을 하기 때문에 이식성이 좋다.
이는 사실 모든 표준 함수가 동일하다.
표준 입출력 함수는 버퍼링을 통한 성능 향상에 도움이 됨
표준 입출력 함수를 사용할 경우 추가적으로 입출력 버퍼를 제공받는다.
이 얘기를 하면 앞에서 했던 내용과 혼동될 수 있다.
왜냐하면 소켓을 생성하면 기본적으로 OS에서 입출력 버퍼가 생성되기 때문이다.
맞는 말이다. 이는 TCP 프로토콜을 진행하는데 매우 중요한 역할을 한다.
이와는 별도로 표준 입출력 함수를 사용하게 되면, 하나의 버퍼를 더 제공받는다.
예를 들어 fputs 함수를 통해서 "Hello" 문자열을 전송할 경우, 입출력 함수 버퍼에 데이터가 전달된다.
그 후, 소켓의 출력버퍼로 이동하고 상대방에게 문자열이 전송된다.
버퍼는 기본적으로 성능의 향상을 목적으로 한다.
하지만 소켓과 관련된 버퍼는 TCP 의 구현을 위한 목적이 강하다.
예를 들어 TCP의 경우 데이터가 분실되면 재전송을 한다. 그러기 위해서 사용되는 목적이 강하다.
반면에 표준 입출력 함수의 버퍼는 성능 향상만을 목적으로 제공된다.
버퍼링의 성능은 다음과 같은 관점에서 좋다.
1. 전송하는 데이터의 양
2. 출력버퍼로의 데이터 이동 횟수
1byte짜리 데이터를 총 열 번에 걸쳐(열 개의 패킷)에 보내는 경우와
이를 버퍼링해서 10byte짜리 패킷 하나로 묶어서 보내는 것은 다르다.
데이터의 전송을 위한 패킷에는 헤더정보라는 것이 추가된다. 이는 데이터의 크기에 상관없이 일정한 크기 구조를
갖는데 이를 패킷당 40바이트만 잡아도(실제로는 이보다 크다) 다음과 같이 전송해야 할 데이터의 양에는 큰 차이가 난다.
● 1바이트 10회 40 × 10 = 400바이트
● 10바이트 4회 40 × 1 = 40바이트
그리고 데이터의 전송을 위해, 소켓 출력 버퍼로 데이터를 이동시키는 데도 시간이 제법 많이 소모가 된다.
그런데 이욕시 이동 횟수와 관련이 있다. 1바이트를 10회 이동하는데 걸리는 시간이, 이를 묶어서 10바이트를 한 번에
이동하는 것보다 열 배 가까운 시간이 더 소모된다.
표준 입출력 함수와 시스템 함수의 성능비교
버퍼링이 성능에 도움이 되는 이유에 대해서 정리해 보았는데, 실제 눈으로 확인하지 않으면 막연하게만 느껴진다.
그래서 표준 입출력 함수를 이용한 파일복사 프로그램과 시스템 함수를 이용한 파일복사프로그램을 이용해서 실제 버퍼링이 갖는 위력이 어느정도인지 확인해 보겠다. 다음 예제는 시스템 함수를 이용한 파일 복사 프로그램이다.
#include <stdio.h>
#include <fcntl.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;
}
위 예제는 우리들이 쉽게 분석 가능한 read & write 함수 기반의 파일 복사 프로그램이다. 복사 대상은 텍스트 파일로 제한하고, 복사 대상은 크기가 300Mbyte 이상인 파일로 해보자. 그래야 성능의 차이를 확실히 느낄 수 있다.
파일의 이름은 필자는 news.txt로 되어있으나 이는 여러분이 적절히 변경해서 테스트를 해보길 바란다.
위 예제를 기반으로 파일 복사를 진행하고 있는가? 필자의 요구대로 300Mbyte 이상의 파일을 대상으로 복사를 진행하고 있다면
잠시 화장실을 다녀오거나 커피를 마셔도 된다.
버퍼를 제공하지 않는 read & write 함수를 이용해서 데이터를 전송하면, 목적지로 모든 데이터를 전송하는데 오랜 시간이 걸린다.
이어서 다음 예제를 살펴보자 이 예제에서는 표준 입풀력 함수를 이용해서 파일을 복사한다.
#include <stdio.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;
}
위 예제를 이용하면 전보다 훨씬 빠르다는걸 알 수 있다.
위 예제에서는 fputs & fgets 함수를 이용해서 파일을 복사한다. 때문에 기본적으로 버퍼링 기반의 복사가 이뤄진다.
그러므로 전보다 훨씬 빠른 속도로 데이터가 전송이 되는걸 알 수 있다.
표준 입출력 함수 사용에 있어서 몇 가지 불편한 사항
여기서 이야기를 끝내면 표준 입출력 함수가 마냥 좋게만 느껴진다. 하지만 이를 기반으로 하는 입출력에도
나름 단점이 있다.
1. 양방향 통신이 쉽지 않다.
2. 상황에 따라서 fflush 함수의 호출이 빈번히 등장할 수 있다.
3. 파일 디스크립터를 FILE 구조체의 포인터로 반환해야한다.
fflush(함수)란?
fflush 함수: 완벽 가이드핵심 기능:버퍼 비우기: fflush 함수는 출력 스트림(파일, 콘솔 등)에 연결된 버퍼의 내용을 강제로 비웁니다. 버퍼는 데이터를 임시 저장하는 메모리 공간으로, 성능 향상
ddd-a.tistory.com
※ 위에서 fflush를 배웠지만 햇갈리면 같이 복습해보는것도 좋은 방법이다.
fopen 함수 호출 시 반환되는 File 구조체의 포인터를 대상으로 입출력을 진행할 경우
읽고 쓰기가 동시에 가능하려면 r+, w+, a+ 모드로 파일을 열어야 한다.
이 때 버퍼링 문제로 인해서 읽기에서 쓰기로, 쓰기에서 읽기로 작업의 형태를 바꿀 때마다 fflush 함수를 호출해야 한다.
이런 경우에는 표준 입출력 함수의 장점인 버퍼링 기반의 성능향상에 영향을 미친다.
표준 입출력함수의 사용을 위해서는 FILE 포인터가 필요하다. 표준 C 함수에서 요구하는 것은 FILE 구조체의 포인터이다.
하지만 기본적으로 소켓은 생성시에 파일 디스크립터를 반환한다.
즉, 파일 디스크립터를 FILE 구조체의 포인터로 변환해야 한다.
표준 입출력 함수 사용하기
위에서 파일 디스크립터를 FILE 포인터로 변환해야 함을 알았다.이번에는 그 방법을 설명하겠다.
fdopen 함수를 이용한 FILE 구조체 포인터로의 변환
소켓의 생성과정에서 반환된 파일 디스크립터를 표준 입출력 함수의 인자로 전달 가능한 FILE 포인터로 변환하는 일은
fdopen 함수를 통해서 간단히 해결할 수 있다.
#include <stdio.h>
FILE* fdopen(int fildes, const char * mode);
// 성공 시 변환된 FILE 구조체 포인터, 실패시 NULL 반환
/*
fildes : 변환할 파일 디스크립터를 인자로 전달
mode : 생성할 FILE 구조체 포인터의 모드(mode)정보 전달
*/
위 함수의 두 번째 전달인자는, fopen 함수호출 시 전달하는 파일 개발모드와 동일하다. 대표적인 예로 읽기모드인 "r" 과
쓰기모드인 "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;
}
int fd=open("data.dat", O_WRONLY|O_CREAT|O_TRUNC);
open 함수를 사용해서 파일을 생성했으므로 파일 디스크립터가 반환된다.
fp=fdopen(fd, "w");
fdopen 함수호출을 통해서 파일 디스크립터를 FILE 포인터로 변환하고 있다. 이 때 두 번째 인자로 "w"가 전달되었으니,
출력모드의 FILE 포인터가 반환된다.
fputs("Network C programming \n", fp);
위를 통해서 얻은 포인터를 기반으로 표준출력 함수인 fputs 함수를 호출하고 있다.
fclose(fp);
FILE 포인터를 이용해서 파일을 딛고 있다. 이 경우 파일자체가 완전히 종료되기 때문에 파일 디스크립터를 이용해서
또 다시 종료할 필요는 없다. 뿐만 아니라, fclose 함수호출 이후부터는 파일 디스크립터도 의미 없는 정수에 지나지 않는다.
파일 디스크립터란?
파일 디스크립터 완벽 분석: 하나부터 열까지1. 핵심 개념:파일 접근을 위한 핸들: 파일 디스크립터(File Descriptor, 줄여서 fd)는 운영체제에서 프로세스가 파일에 접근하기 위해 사용하는 정수 값
ddd-a.tistory.com
fileno 함수를 이용한 파일 디스크립터로의 변환
이번에는 fdopen 함수의 반대 기능을 제공하는 함수이다.
#include <stdio.h>
int fileno(FILE * stream);
// 성공 시 변환된 파일 디스크립터, 실패시 -1 반환
이 함수 역시 사용방법은 간단하다. 인자로 FILE 포인터를 전달하면, 해당 파일의 파일 디스크립터가 반환된다.
그럼 이어서 fileno 함수의 사용 예를 보이겠다.
#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;
}
open 함수로 얻은 파일 디스크립터를 fdopen을 통해 FILE 포인터로 변환하였다
그 후 fileno 함수를 통해 다시 파일 디스크립터로 변환한 후 정수 값을 출력하였다.
소켓 기반에서의 표준 입출력 함수 사용
위에서 공부한 내용으로 소켓에 적용해보겠다.
이전에 구현했던 에코 서버와 에코 클라이언트를 표준 입출력 함수를 이용해서 데이터가 송수신하도록 변경하였다.
Server
#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);
}
- 입력용, 출력용 FILE 구조체 포인터를 각각 생성해야 한다.
- 표준 C 입출력 함수를 사용할 경우 소켓의 버퍼 이외에 버퍼링이 되기 때문에, 필요하다면 fflush 함수를 직접 호출해야 한다.
반복문에서 fgets, fputs, fflush를 주목해보자
- 표준 입출력 함수는 성능향상을 목적으로 버퍼링을 하기 때문에 fflush 함수를 호출하지 않으면 당장에 클라이언트로 데이터가 전 송된다고 보장할 수 없다.
일반적인 순서
1. 파일 디스크립터를 FILE 구조체 포인터로 변환
2. 표준 입출력 함수 호출
3. 함수 호출 시 fflush 함수 호출을 통해 버퍼를 비움
Client
#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);
}
기존에 시스템 함수를 사용하였을 때는 데이터의 마지막에 0을 삽입(message[strlen] = 0;)하여 수신된 데이터를 문자열로 구성하는 과정이 필요하였지만 표준 입출력 함수의 사용으로 문자열 단위로 데이터를 송수신하기 때문에 생략되었음을 알 수 있다.
'스터디 > TCP 와 IP' 카테고리의 다른 글
12. 멀티쓰레드 기반의 서버구현 (0) | 2024.09.05 |
---|---|
11. 입출력 스트림의 분리에 대한 나머지 이야기 (0) | 2024.09.04 |
9. 다양한 입출력 함수들 (0) | 2024.09.03 |
8. 멀티프로세스 기반의 서버구현 (0) | 2024.09.03 |
7. 도메인 이름과 인터넷 주소 (0) | 2024.09.03 |