1. Stream을 사용하는 이유

더보기

(배경) 프로그램은 정보를 전달 하고, 정보를 전달받는 작업의 연속이다.

컴퓨터는 수많은 입출력(I/O) 장치(Keyboard, Mouse, Monitor, Printer) 정보를 주고받기 위해 연결된다.

 

 

(문제점) 다양한 입출력(I/O) 장치가 서로 다른 정보 전달 기술을 사용한다면, 이를 호환시키는 것은 너무 어려운 일이다.

애플이 범용 C타입을 규격을 사용하지 않고 8핀, 12핀 독자 규격을 사용할때 겪었던 불편함과 같다.

여러분은 미터, 리터 가 아닌 feet, ml, lb 단위가 익숙한가?

스마트폰 터치패드 키보드로, PC 윈도우를 다룰 수 있는가?

 

 

(해결책) 입출력 표준 기술, Stream

 

현대 체계는 이러한 문제를 '표준화' 로 해결해오고 있다.

 

C언어와 Unix/ Linux는 현대 컴퓨터의 표준화된 개념에 가장 많은 영향을 끼쳤다.

8bit == 1Byte 체계처럼 입출력 (I/O) 장치들 간의 호환성도 C언어와 Unix/ Linux 방식이 절대적인 영향을 주었다.

 

 

1970 "모든 것을 파일로 취급한다" (Everything is a file) - Linux/Unix 핵심 철학


  • 터미널, 디스크 파일, 프린터, 마우스, 네트워크, 소켓, 파이프, 시그널 등도 "파일"로 간주
  • open(), read(), write(), close() 와 같은 동일한 시스템 콜(System Call) 함수를 사용
  • ★개발자는 데이터가 키보드에서 오는지, 파일에서 오는지 신경 쓸 필요 없이 동일한 방식으로 프로그래밍

 

FILE 구조체 내부

typedef struct {
    unsigned char *_base;    /* 버퍼 시작 주소              */
    size_t         _size;    /* 버퍼 크기                   */
    size_t         _cnt;     /* 남은 바이트 수 (읽기/쓰기)  */
    int            _fd;      /* 커널 파일 디스크립터 번호    */
    unsigned int   _flags;   /* EOF, ERR, 읽기/쓰기 모드 등 */
    // ... 플랫폼별 잠금·함수 포인터
} FILE;

 

 

1989  표준 I/O 라이브러리(stdio.h)

 

  • C 언어가 (Unix뿐 아니라) 다른 OS로도 포팅되면서 휴대성 우선 설계.
  • 1989 ANSI C & 1990 ISO C가 스트림/버퍼 규칙을 명문화 → 언어, OS 독립 인터페이스가 정립.

 

1988 POSIX 표준

 

 

 

(장점)

  1. 그렇기에 C언어 프로그래머는
    개별 입출력 장치의 기능과 내부 동작 모르더라도, 개발 업무에 stream 이라는 한가지 능력만 기억하면 된다.

  2. 운영체제는 입출력 장치의 종류에 상관없이, 표준 입출력 스트림 으로 항상 똑같은 절차를 통해 데이터를 읽고 쓴다.

  3. 그래서 네트워크 데이터 송/수신 함수가 파일 입출력 함수와 동일한 read, write 이다.
    (윈도우의 경우, 방식이 미세하게 상이하여 소켓(WinSock)은 send, recv(receive) 함수를 사용한다.)

 

2. Stream 이해

더보기

Stream이 뭔가요?

  • 데이터가 순차적으로 송수신되는 흐름을 체계화시킨 기술입니다.
  • 영어로 '흐르는 시냇물' 이며, 가상 연결 통로에서 바이너리 데이터가 시냇물처럼 흐르는 모습을 의미한다.
  • 끊기지 않고 연속적으로 "데이터가 흐르는 과정"을 추상화(구현) 한 용어
    • Data 가, Source(원본)에서 Destination(목적지)로 Block 단위로 전달되는 과정을 구현(추상화)한 기술을 의미한다.
  • printf, scanf 함수의 내부 구현 구조를 모르더라도 사용할 수 있다. stream도 마찬가지다
    • stream 내부 구현 구조를 모르더라도, stream 이라는 것을 사용하는 것만으로, 키보드에서 입력받은 Data 를 파일이나, 모니터로 내보낼 수 있다.
    • 하지만, prinf 매개변수, 정렬, 동작 방식을 이해해야 하는 것처럼
      stream 도 내부 구현 구조를 모르더라도, 동작 방식에 대한 약간의 이해가 있어야 좀 더 효율적인 사용이 가능하다.



학습관점

 

출력을 위해, 출력 개념과 printf( ) 함수 사용법을 배웠다

입력을 위해, 입력 개념과 scanf( ) 함수 사용법을 배웠다.

 

Stream을 사용하기 위해서는, 1)Stream의 개념과, 2)버퍼(buffer)라는 도구 동작 방법을 약간 깊게 학습해야 한다. 

 

Stream 은 자동차 운전 방법, 한식 요리법, 정장, 우산 과 같은개념적 모델, 방법론이고,
Buffer 는 그 흐름을 실제로 성능 좋게 구현하기 위한 기술적 장치, 실제 소스코드로 다루어지는 도구입니다.

 

구분 스트림 (Stream) - 개념/목표
버퍼 (Buffer) - 기술/도구
비유 된장찌개 요리법 전체
요리에 사용하는 '도마' 또는 '믹싱 볼'
역할 무엇(What)을 할 것인가?
(데이터를 일관된 방식으로 입출력한다)
어떻게(How) 효율적으로 할 것인가?
(입출력 횟수를 줄여 성능을 높인다)
관점 사용자/프로그래머의 관점
(추상화된 편리한 인터페이스)
시스템/엔지니어의 관점
(성능 최적화를 위한 내부 구현)
핵심 가치 추상화, 일관성, 이식성
성능, 효율성, 최적화

 

3. Stream 동작

더보기

3.1 Stream 입출력 예시 1 

 

 

 

 

계층 핵심 역할 주요 기술
1. 애플리케이션
(사용자 공간)
고수준의 쉬운 인터페이스 제공
C 표준 라이브러리 (stdio), FILE 구조체,
사용자 버퍼링
2. 운영체제 커널
(커널 공간)
장치 추상화 및 데이터 중개
시스템 콜 (System Call), 파일 서술자 (File Descriptor),
커널 버퍼 캐시
3. 디바이스 드라이버
(커널 공간)
하드웨어 제어 신호로 변환
특정 하드웨어(그래픽카드, 디스크 등)를 위한 소프트웨어 인터페이스
4. 하드웨어 물리적 데이터 입출력 수행
CPU, 메모리, 그래픽 카드, 모니터 등 물리적 장치

 

 

3.2 Stream 입출력 예시 2 (표준 입출력)

 

네트워크는 키보드, 모니터 대신 양쪽 노드에 Socket이 있다고 생각하면 된다.

 

 

 

4. stream 사용

더보기

4.1 C언어, 표준 입출력 stream 사용 방법

  • C언어, <stdio.h> 라이브러리를 사용해 표준 입출력을 사용한다.
    printf( ) stdout stream 
    scanf( ) stdin stream 

 

4.2 C++, 표준 입출력 stream 사용 방법

  • C++, <iostream> 라이브러리를 사용해표준 입출력을 사용한다.
    cin 객체
    cout 객체

 

4.3 C언어, FILE stream 사용 방법

  • 파일 입출력은, 표준 스트림 처럼 자동으로 Stream이 생성되지 않는다.
    File* 라는 Stream을 의도적으로 생성시켜 사용한다.
    그래서 의도적으로 생성한 stream 은 필수로 닫아줘야 한다.
  • File* fp = fopen("파일.txt", "w");
    fp 라는 file stream 을 생성한 뒤 사용한다.
    fprintf( fp , "Hello world\n");
    fscanf( fp , " ");

 

4.4 소켓 stream 사용 방법

  • Network 통신에서 Socket 으로 통신하면 socket stream 이 자동 생성된다.
  • Socket 꼭! 닫아야 하는 이유는 stream을 닫아야 하기 때문이다.
  • Database 도 마찬가지의 경우다. 

 

4.5 Qt Stream 사용 방법

  • Qt i/o 공식문서 링크
  • Qt 네트워크에서 파일 송수신 방법
    • QDataStream 을 socket에 연결한다. (로컬 파일 연결 작업과 동일) 
    • QByteArray 타입의 데이터를 stream으 주고 받으면 된다.

 

5. Linux 표준 입출력 Stream의 물리적 위치

더보기

 

  • 리눅스의 경우, 최상위 경로에서 dev 디렉토리에서 확인 가능하다.
    운영체제가 만들어 놓은 stdin, stdout, stderr 스트림을 확인 할 수 있다.
  • 위 이미지에서 오른쪽 0, 1, 2 번호는 파일 디스크립터(File Descriptor)이다.
  • 프로세스(프로그램 실행)가 시작되면, stdin, stdout, stderr 정보와 함께 실행된다.
  • File* 를 사용하면 File Stream이 생성되고 File Descriptor를 부여받는다.
    • 아래 stream 모두  File Descriptor 라는 고유 정수 식별값을 부여받는다.
      1. 입력
      2. 출력
      3. 파일
      4. 프린터
      5. 모니터
      6. 데이터베이스
      7. 소켓
      8. ...
  • Socket 이 연결되면 Socket Stream이 생성되고 File Descriptor 를 부여받는다.
    즉, 서버에서 연결된 Socket 들을 fd(File Descripor) 정수값으로 로 식별하여 Client를 구분 할 수 있다.
  • Qt 에서 Socket의 File Descriptor 확인 방법
    • socket->socketDescriptor()
    • QString::number(socket->socketDescriptor())
    • File Descriptor 값은 stream에 부여되는 중복되지 않는 고유한 정수값으로 Socket 의 식별이 가능하다.

 

6. 파일 디스크립터(File Descriptor)

더보기

리눅스에서 파일 디스크립터(File Descriptor)란, C 언어와 같은 프로그래밍 언어가 아닌 시스템(OS)에 의해 관리되는 일련의 정수값으로 파일 또는 소켓의 식별에 사용된다.

(*윈도우는 파일과 소켓을 구분한다.)

 

 

 

▶ UNIX에서 파일을 새로 열면(open) int 타입의 정수를 리턴하는데, 이는 파일 기술자 테이블(file descriptor table) 의 index 번호다.
▶ 예를들어, 2개의 파일을 open하면 파일 기술자는 3과 4가 배정되며, 소켓을 생성하면 파일 기술자와 똑같은 기능과 역할을 하는 소켓 기술자(socket descriptor)가 리턴된다. 즉, 파일과 소켓이 기술자 테이블을 공유한다.

▶ 단순하게 설명하자면, 단체 생활에서 이름보다 번호를 사용하는 것이 편한것과 같다.

 

 

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>

int main(void)
{	
	int fd1, fd2, fd3;
	fd1=socket(PF_INET, SOCK_STREAM, 0);
	fd2=open("test.dat", O_CREAT|O_WRONLY|O_TRUNC);
	fd3=socket(PF_INET, SOCK_DGRAM, 0);
	
	printf("file descriptor 1: %d\n", fd1); // file descriptor 1: 3
	printf("file descriptor 2: %d\n", fd2); // file descriptor 1: 4
	printf("file descriptor 3: %d\n", fd3); // file descriptor 1: 5
    
	// 3 부터 시작하는 이유는 0,1,2 는 표준 입출력에 할당되었기 때문이다.
    
    // 아래 getchar(); 는 이 프로그램이 종료되지 않을 때 사용.
    //getchar();
	
	close(fd1);
	close(fd2);
	close(fd3);
	return 0;
}

 

 

7. 실행중인 프로그램(Process)의 Stream과 File Descriptor

더보기

7.1 리눅스 프로세스 확인 방법

 

- 리눅스에서 실행중인 프로세스를 확인하는 방법은 아래와 같다.

 

ps au

 

 

 

7.2 실행 파일명으로 프로세스 아이디(PID) 찾기

 

- 실행중인 프로세스의 스트림을 확인하기 위해, 프로세스 아이디를 찾는다.

 

ps au | grep 실행파일명

 

 

이전 예제에서, getchar 부분의 주석을 제거하고 실행하여, 입력 전까지 프로세스가 종료되지 않도록 한다.

실행중인 프로세스의 아이디(PID)를 찾는다.

 

 

7.3 PID 경로에서 fd(File Descriptor) 확인

 

- 실행중인 프로세스 아이디 디렉토리로 이동하여, fd 경로에서 리스트를 확인하면, 실행중인 프로그램 내에서 생성한 file descriptor 리스트를 확인 할 수 있다.

 

cd /proc/프로세스아이디/fd
ls -al

 

 

표준 입출력의 파일 디스크립터는 프로세스가 실행되면 공유된다.

프로그램이 실행되어 생성한 파일 디스크립터(소켓2개, 파일1개)는 3,4,5 번의 스트림을 생성하고 프로세스 고유 fd를 할당받았다

 

 

8. 저수준 vs 고수준 vs 프레임워크(응용)

더보기

저수준(FD)

/* 1) open() 예시: 로그 파일에 한 줄 쓰기 */
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int fd = open("log.txt",
              O_WRONLY | O_CREAT | O_APPEND,
              0644);                    // rw-r--r--
const char *msg = "hello\n";
write(fd, msg, strlen(msg));
close(fd);

 

 

 

 

고수준(FILE*)

/* 2) fopen() 예시: 텍스트 파일 읽고 화면에 출력 */
#include <stdio.h>

FILE *fp = fopen("data.txt", "r");
char buf[256];
while (fgets(buf, sizeof buf, fp))
    fputs(buf, stdout);
fclose(fp);

 

  • fopen()은 결국 내부에서 open()을 호출하고 반환된 식별자 int fd를 포함한 로직
    • fopen() >> 내부에서 open() 시스템 호출 실행 >> fd 를 얻어 FILE 구조체에 저장
    • fopen()은 fd를 FILE 구조체에 넣어 버퍼와 상태(flag)를 함께 관리
  • 반대로 이미 열린 fd를 FILE*로 감싸고 싶다면 fdopen(fd, "r")를 사용
    • fopen() → open() : 항상 발생 (자동)
    • open() → fopen() : 자동으로 일어나지 않으며, 필요하면 fdopen()을 명시적으로 호출해야 한다.
int fd = open("data.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd == -1) perror("open");

FILE *fp = fdopen(fd, "a");   // 여기서부터는 fprintf 등 사용 가능
fprintf(fp, "로그 한 줄\n");
fclose(fp);                   // fclose가 close(fd)까지 처리

 

 

 

 

정리

계층  핵심 특징  파일 I/O 예시  TCP/소켓 I/O 예시  대표 라이브러리·함수
저수준
(커널 시스템콜 래퍼)
FD(정수) 를 직접 다룸버퍼 없음·원자적 I/O·플래그 제어 open / read / write / lseek / close socket / connect / send / recv / closebind / listen / accept POSIX libc(syscall 래퍼), Winsock, fcntl, ioctl
고수준
(버퍼, 포맷 스트림)
사용자 공간 버퍼링·문자열 포맷 기능FD ↔ FILE* 변환 가능 fopen / fread / fwrite / fclosefprintf / fscanf fdopen(sock_fd,"r+") → FILE*fprintf, fgets, getline 등 C stdio 계열, C++ <fstream>·<iostream>
최상위
프레임워크 or 응용(Application-level)

(프로토콜
이벤트 추상화)
프로토콜 처리·이벤트 루프·스레드풀·TLS 등까지 캡슐화비동기·재시도·타임아웃 내장 (대개 파일은 표준 I/O로 충분) HTTP/2, WebSocket, gRPC, TLS, 비동기 이벤트 등 고차 기능 네트워크: libcurl, Boost.Asio, libevent, libuv, gRPC, cpp-httplib, OpenSSL BIO, Python socketserver

멀티 I/O: Tokio(Rust), .NET Sockets, Node.js net

 

 

 

 

간단 예시

/* 1) 저수준: 소켓 생성 & 연결 */
int sock = socket(AF_INET, SOCK_STREAM, 0);
connect(sock, (struct sockaddr *)&srv, sizeof srv);

/* 2) 고수준: fdopen 으로 FILE* 래핑 */
FILE *net = fdopen(sock, "r+");      // now buffered & printf-friendly

/* 3) 포맷 I/O 사용 - 전송*/
fprintf(net,
        "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n");
fflush(net);  // 버퍼(비우기) == 커널로 전송

/* 4) 포맷 I/O 사용 - 수신*/
char line[256];
while (fgets(line, sizeof line, net))
    fputs(line, stdout);

/* 5) 종료*/
fclose(net);     /* fclose 가 close(sock) 까지 처리 */

 

  • 1) 저수준 단계
    socket()이 정수 FD를 만들고, connect()로 TCP 세션을 맺습니다.
  • 2) 고수준 승격
    fdopen()이 FD를 FILE*로 감싸 버퍼·포맷 기능을 제공합니다.
  • 3) 포맷 I/O
    fprintf()는 stdio 버퍼에 쌓이고, fflush() 때 write()로 커널 송신 큐에 복사됩니다.
  • 4) 수신
    fgets()는 필요할 때마다 read()로 커널 수신 큐에서 데이터를 가져옵니다.
  • 5) 종료
    fclose()가 남은 버퍼를 flush한 뒤 close()를 호출해 소켓을 닫고, TCP FIN을 전송합니다.

고수준 코드는 fprintf·fgets처럼 간결한 포맷 I/O를 쓰고,

필요하다면 언제든 저수준 FD API( send/recv , setsockopt 등)를 병용