C standard에 정의된 I/O 함수를 사용하면 다음과 같은 장점이 있다. (ex. fprintf)
1. Portability에 좋다.
2. 버퍼링을 통한 성능 향상에 도움이 된다.
소켓을 만들면 기본적으로 kernel에서 버퍼를 만든다. 그게 위 그림의 우측에 보이는 소켓 버퍼이다.
TCP 소켓은 오류가 발생하면 데이터 재전송도 해야 되고 하니 이런 버퍼가 꼭 필요하다. 그렇다고 UDP 소켓엔 버퍼가 없는건 아니다. 데이터를 보낼 때도 잠시 담아둬야 할 수도 있고, 받을 때도 application에서 읽기 전까지 잠시 담아둬야 할 수도 있으니 UDP 소켓에서도 버퍼는 필요하다.
그런데 standard I/O 함수는 내부적으로 응용 계층에서 버퍼를 또 사용한다. 즉, 표준 입출력 함수를 사용하면 위 그림의 좌측에 보이는 것 처럼 추가 버퍼가 생기는 것이다.
소켓을 만들 때 생성되는 버퍼들은 위에서 언급한 것들이 주 목적이지만, 표준 입출력 함수에서 제공하는 버퍼는 성능 향상이 주 목적이다.
일반적인 측면에서 standard I/O의 buffering이 주는 장점은 다음과 같다.
1. system call 횟수를 줄인다. buffer를 이용해 한번에 큼직큼직하게 처리해버리니 각 함수의 내부에서 사용하는 system call 횟수도 줄어든다. 덕분에 system call 관련 오버헤드도 줄어든다.
2. 메모리/디스크 접근 횟수를 줄인다. buffer를 이용해 한번에 큼직큼직하게 데이터를 읽거나 쓰기 때문에 비교적 비싼 작업인 메모리/디스크 접근 횟수를 줄일 수 있다. 일단 최대한 많이 읽어와서 버퍼에 담아두고 거기서 조금씩 처리하는 것이다.
3. 사용자가 버퍼 크기를 지정하여 상황에 맞게 최적화 할 수도 있다. (표준입출력 함수의 버퍼를 조절하는 것도 아마 소켓 버퍼 크기 조절할 때처럼 완벽하게 요구사항을 반영해주진 않을듯)
socket 버퍼에 standard I/O 버퍼까지 추가됐을 때 장점은 아래와 같다.
1. 전송하는 데이터 총량이 작아진다. 총량이 어떻게 작아지나 싶을 수도 있는데, 데이터를 보낼 땐 항상 헤더라는게 붙기 때문에 데이터를 내보내는 횟수가 많아질수록 손해이다. 표준입출력 함수의 버퍼에 데이터를 꽉 꽉 담아뒀다가 socket 버퍼로 한번에 보내므로, 한 패킷에 들어가는 데이터가 많아지므로 보내는 패킷의 수는 줄어들 것이다.
2. 소켓의 출력 버퍼로 데이터를 이동시키는 횟수가 줄어든다. 소켓의 출력 버퍼로 데이터를 이동시키는 작업은 꽤 시간이 소모되는데, 표준 입출력 함수의 버퍼에 데이터를 쌓았다가 보내므로 그 횟수를 줄일 수 있다.
Q. 아래에서 실제로 사용할 때 보면 표준 입출력함수 쓰고나서 무조건 fflush로 버퍼 비우라던데, 그럼 바로 위에서 말한 1번 2번 장점이 의미가 있나? 바로바로 버퍼 비우는데?
A. 그래도 아예 없는 것보단 나을 것 같다. 비록 바로 fflush()를 하더라도, 버퍼 덕분에 아예 버퍼가 없을 때보단 데이터를 더 쌓아서 보낼 것 같다. (버퍼가 없으면 내부적으로 정확하게 어떻게 동작하는진 모르겠다만..)
버퍼링이 무조건 좋은 건 아니지만, 데이터 양이 많은 경우 buffer를 사용한 쪽이 더 우수한 성능을 보인다.
실제로 표준입출력 함수와 시스템 함수를 가지고 파일에 읽기/쓰기를 진행했을 때, 표준입출력 함수를 사용한 쪽이 좋은 성능을 보였다. 300MB가 넘어가면 차이가 극명하게 드러난다.
> 표준 입출력 함수 사용에 있어서 불편한 점들
1. 양방향 통신이 쉽지 않다. 데이터가 버퍼링되므로 입출력이 동시에 진행되게 하기 쉽지 않다.
2. fflush 함수 호출이 빈번히 등장할 수 있다.
3. socket 생성 시 반환되는건 file descriptor이므로, standard I/O functions을 사용하려면 이를 FILE * type으로 변경해야한다.
fdopen
#include <stdio.h>
FILE * fdopen(int fd, const char * mode);
file descriptor를 FILE * type으로 변환해주는 함수이다.
첫번째 인자에는 변환할 file descriptor, 두번째 인자에는 생성할 FILE 구조체 포인터의 mode 정보를 전달한다.
두번째 인자에는 fopen에 쓰듯이 "w"나 "w+"나 뭐 이런 애들이 올 수 있다.
성공 시 변환된 FILE 구조체 포인터, 실패 시 NULL 을 반환한다.
fileno
#include <stdio.h>
int fileno(FILE * stream);
반대로 FILE * type을 file descriptor로 변환해주는 함수이다.
성공 시 변환된 file descriptor, 실패 시 -1 을 반환한다.
> 활용 (입출력 스트림 분리)
FILE * readfp = fdopen(sock, "r");
FILE * writefp = fdopen(sock, "w");
이렇게 입력용, 출력용 FILE 구조체 포인터를 애초에 따로 만들어두는게 구현하기 편하고, 입력버퍼/출력버퍼를 구분해서 버퍼링 기능이 향상된다.
각 file pointer에 대해 표준입출력함수를 사용하고 난 후, fflush 함수로 버퍼를 비워주며 사용해야한다. (fputs, fgets 같은거 쓰고 바로 뒤에 fflush 해주기!!)
fflush를 입력 스트림에 쓰는건 표준이 아니라고 알고 있긴한데.. implementation에서 그냥 두 경우 다 지원해주나보다.
※ 문제점
하지만 위처럼 단순하게 입출력 스트림을 분리해서 사용하면 Half-close를 사용할 수 없다.
(이전에 MultiProcess 글에서 했던 입출력 스트림 분리에선 그냥 shutdown 함수 쓰면 Half-close됨.)
fclose(writefp);
위 코드로 Half-close를 할 수 있을 것 같지만, writefp나 readfp나 같은 file descriptor를 공유하므로 위 코드가 실행되면 둘 다 종료 돼버린다.
위 그림에서 윗쪽 경우처럼 하나의 File Descriptor로 FILE 포인터가 만들어졌기 때문에 Half-close가 안되는 것이다.
따라서 아래 경우처럼 특정 socket에 대한 File Descriptor를 복사한 뒤, 각각에 대해 FILE 포인터를 만들면 FILE 포인터 소멸 시 해당 FILE 포인터에 연결된 File Descriptor만 소멸된다.
하지만 아래처럼 하더라도 아직 완전한 Half-close는 아니다. 왜냐하면 socket의 input 혹은 output stream이 닫혀야 상대방에게 종료 요청이 전달되고, 그래야 Half-close이다. 아래 그림처럼 write 관련 FILE 포인터를 닫더라도, 여전히 우리는 socket을 직접 사용해 write를 진행할 수 있다.(아래에서 해결책 제시)
dup & dup2
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
두 함수 모두 첫번째 인자로 복사할 File descriptor를 전달한다. dup2 함수는 두번째 인자로 명시적으로 복사할 File descriptor 값을 지정한다.
성공 시 복사된 File descriptor, 실패 시 -1 을 반환한다.
> Half-close까지 지원하는 예시 코드 (+ 표준 I/O로 입출력 스트림 분리)
//reading용 FILE 포인터
FILE * readfp = fdopen(clnt_sock, "r");
//writing용 FILE 포인터, dup()으로 fd 복사 후 진행
FILE * writefp = fdopen(dup(clnt_sock), "w");
fputs(". . .", writefp); fputs(". . .", writefp);
fflush(writefp);
//이렇게 직접 File Descriptor로 shutdown 함수까지
//호출해줘야 제대로 Half-close가 된다.
shutdown(fileno(writefp), SHUT_WR);
fclose(writefp); //출력용 FILE 포인터도 없애기
fgets(buf, sizeof(buf), readfp);
fclose(readfp); //완전 종료
'수업 > Network Programming' 카테고리의 다른 글
[Network Programming] MultiTherad 기반 서버 (2) | 2023.05.29 |
---|---|
[Network Programming] Multicast & Broadcast (0) | 2023.05.29 |
[Network Programming] 다양한 IO Functions (0) | 2023.05.29 |
[Network Programming] IO Multiplexing 기반 서버 (0) | 2023.05.29 |
[Network Programming] MultiProcess 기반 서버 (0) | 2023.05.29 |