[RTMP] 입출력 스트림의 분리

2 minute read

먼저 입출력 스트림의 분리가 무엇을 의미하는지부터 보겠습니다.

입출력 스트림의 이해

소켓 file descriptor의 이해

socket()을 사용하면 리턴되는 값은 소켓에 대한 file descriptor입니다. 리눅스에서는 소켓도 파일과 같이 취급하기 때문에 소켓을 다룰 때도 file descriptor라는 명칭을 사용합니다. 소켓 fd는 운영체제가 내부적으로 만드는 소켓에 대한 포인터입니다. fd에 들어있는 값은 양의 정수입니다. 0,1,2는 각각 stdin, stdout, stderr에 대한 fd입니다. 그래서 개발자가 socket()를 호출할 때 return되는 fd는 3이상의 값을 가집니다. 그리고 fd값은 관리하는 file의 수가 증가할 때마다 1씩 증가합니다.

소켓 버퍼의 이해

특정 소켓이 만들어지고 이 소켓에 대한 fd가 생성되었다는 것은 해당 소켓으로 입력과 출력이 모두 가능함을 의미합니다. 그리고 입력과 출력 각각에 대해서는 소켓의 버퍼가 할당되어 있습니다. read()/write()을 호출하는 것이 당장 데이터가 입력/출력된다는 것이 아니라 소켓의 버퍼로부터 데이터를 읽고, 소켓 버퍼로 데이터가 옮겨지는 것입니다.

버퍼의 역할을 흔히 아는 버퍼링을 수행하는 것입니다. 1바이트씩 읽거나 보내는 것이 아니라 버퍼에 읽을/보낼 데이터를 쌓아두고 한 번에 수행하기 위함입니다. 파일 다운로드의 경우 버퍼가 없으면 속도가 심각하게 느려집니다.

또 다른 버퍼

표준 입출력 버퍼

소켓 버퍼 말고도 표준입출력 함수(fputs, fgets, etc)등을 사용할 때 추가적으로 얻을 수 있는 버퍼가 있습니다.

  • PC <-입출력 함수 입력 버퍼- <-소켓 입력 버퍼- 인터넷 ㄴ입출력 함수 출력 버퍼-> -소켓 출력 버퍼>

입출력 함수 버퍼도 성능을 위해서 존재하며, 표준 입출력을 사용할 때 얻을 수 있는 이점으로 생각하면 됩니다.

아래는 read()/write()를 사용한 파일의 복사입니다.

#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;
}

아래는 표준입출력 fgets/fpus를 사용한 파일의 복사입니다. 굳이 두 파일을 실행해보지는 않아도 표준 입출력이 훨씬 더 빠르다는 것만 알면 됩니다.

#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;
}

데이터 버퍼

데이터 버퍼는 제가 편의를 위해서 붙힌 이름입니다. 바로 위의 예제에서 ‘gets(buf, BUF_SIZE, fp1)’를 할 때 입력받은 데이터를 저장할 공간도 buf라는 이름을 지어주었습니다. 이 또한 BUF_SIZE만큼 데이터를 받기 때문에 버퍼링을 하는 것이고 역시 성능향상에 목적이 있습니다.

입출력 스트림을 분리의 의미

앞서 표준입출력을 사용하는 경우 추가적인 버퍼를 사용할 수 있고, 이에 대해 성능 이점이 추가적으로 제공된다고 했습니다. 그런데 소켓 fd는 표준입출력의 인자로 전달될 수 없습니다. 표준입출력의 인자는 FILE* 타입만을 받기 때문에 소켓 fd를 형변환해주어야 합니다.

#include <stdio.h>

/**
 * fd -> FILE*로 변환.
 * 일반적인 파일 int fd = open("data.dat", O_WRONLY); 뿐만 아니라
 * 소켓 int clnt_sock = accept(serv_sock, ~~~~)도 fildes로 전달할 수 있습니다.
 * 
 * @param fildes 변환할 파일 디스크립터를 인자로 전달
 * @param mode 생성할 FILE 구조체 포인터의 모드 정보 전달. "r", "w" 등 
 * 
*/
FILE* fdopen(int files, const char* mode);

/**
 * FILE* -> fd로 변환
 * @param stream 변환할 파일 포인터
*/
int fileno(FILE* stream);

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");

이렇게 얻어진 readfp와 writefp는 각각 입력 스트림과 출력스트림을 담당하고 아래와 같이 사용합니다.

char message[BUF_SIZE];

...

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);

하나의 소켓 fd는 하나의 소켓을 가리키고 있고, 하나의 소켓에는 소켓 입력 버퍼와 소켓 출력 버퍼가 하나씩 있습니다. 이들 각각의 소켓 입력/출력 버퍼를 사용한 스트림을 담당하는 readfp과 writefp를 구분하는 것을 입출력 스트림의 분리라고 합니다.

입출력 스트림을 분리의 두 가지 장점

  1. 읽기모드와 쓰기모드의 구분을 통한 구현의 편의성 증대
  2. 입력버퍼와 출력버퍼를 구분함으로 인한 버퍼링 기능의 향상

입출력 스트림을 분리하는 것의 의미를 알아보면서 첫 번째 장점에 대한 이해는 되었다고 생각합니다. 제가 이해하기에, 두 번째 장점의 버퍼는 데이터 버퍼를 의미합니다. 데이터를 버퍼에 읽고, 그 버퍼를 사용해서 바로 쓴다면 하나의 버퍼만 사용하는 것이 효율적입니다. 하지만

Tags:

Categories:

Updated: