본문 바로가기
Programming/열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)

Ch 16. 입출력 스트림의 분리에 대한 나머지 이야기

by minjunkim.dev 2020. 8. 10.

    모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!


# 지금까지 우리가 구현해 보았던 입출력 스트림 분리

1) (Ch 10)TCP의 입출력 루틴 분할

: fork함수 호출을 통해 파일 디스크립터 복사 후, 이를 입력용/출력용으로 분리하여 사용

- 입력루틴(코드)과 출력루틴의 독립을 통한 구현의 편의성 증대

- 입력에 상관없이 출력이 가능하게 함으로 인해서 속도의 향상 기대


2) (Ch 15)FILE 포인터를 통한 입출력 분할

: 입력/출력 용도로 파일 구조체 포인터를 만들어 열고, 사용하고, 닫음

- 읽기모드와 쓰기모드의 구분을 통한 구현의 편의성 증대

- 입력버퍼와 출력버퍼를 구분함으로 인한 버퍼링 기능의 향상

 

# 스트림 분리 이후의 EOF에 대한 문제점
- TCP의 입출력 루틴 분할에서는

Hafl-close을 통한 EOF 전달의 필요성에 대해 언급하면서
e.g. shutdown(sock, SHUT_WR); // 출력스트림 연결을 끊으면서 상대에게 EOF 전달
과 같은 방법을 알아보았다.

- 그렇다면, fdopen함수를 통해 파일 디스크립터를 FILE 구조체 포인터로 변환하고,
이를 이용한 입출력 분할에서는 Half-close를 어떻게 할 수 있을까?
- 이 경우, 읽기모드/쓰기모드 FILE 구조체 포인터 둘중 하나에 대해서만

fclose()를 한다고해서 Half-close가 되지 않으며, 소켓이 완전히 연결 종료됨.

이는 "하나의 파일 디스크립터"를 통해 얻은 FILE 구조체 포인터들이기 때문.

 

# FILE 구조체 포인터 입출력 분할에서의 Half-close
- 파일 디스크립터를 복사하여, 각각의 파일 디스크립터를 각각 읽기/쓰기모드 파일 구조체 포인터로 만들면 된다.

- 여기서 파일 디스크립터 복사는 fork 함수호출에 의한 것이 아님.

한 프로세스에 원본과 복사본 파일 디스크립터가 모두 있어야 하기 때문.
- "동일한 파일 또는 소켓의 접근을 위한 또 다른 파일 디스크립터의 생성"이 필요함.
단순한 정수값 복사를 의미하는 것이 결코 아님
- "모든 파일 디스크립터가 소멸되어야 소켓도 소멸된다."라는 특징을 이용하여

Half-close를 위한 환경 조성이 가능해짐
- 그러나 이것만으로는 Half-close는 불가능. 하나의 파일 디스크립터를 통해 얻은 파일 구조체 포인터를 닫았다고 해도, 사실상 아직 소켓에 대한 파일 디스크립터가 한 개 남아 있는 것이므로 이 파일 디스크립터를 통해 사실상 입출력 둘다 가능한 상태임.

- 복사된 파일 디스크립터의 수에 상관없이, EOF의 전송을 동반하는 Half-close를 위해서는 "shutdown 함수"를 호출해야 함을 반드시 기억.

#include <unistd.h>

int dup(int fildes);
int dup2(int fildes, int fildes2);
// -> 성공시 복사된 파일 디스크립터, 실패시 -1 반환

- fildes : 복사할 파일 디스크립터 전달
- fildes2 : 명시적으로 지정할 파일 디스크립터의 정수값 전달

(0보다 크고, 프로세스당 생성할 수 있는 파일 디스크립터의 수보다 작은 값을 전달)

 

# 파일 디스크립터 0,1,2(표준입력/출력/에러)는 항상 자동으로 열림을 기억하자!

 


# FILE 구조체 포인터를 통한 입출력 분할에서의 Half-close 확인 예제

 

sep_serv.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024

int main(int argc, char* argv[])
{
    int serv_sock, clnt_sock;
    FILE* readfp;
    FILE* writefp;

    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t clnt_adr_sz;
    char buf[BUF_SIZE];
    
    serv_sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성

    /* 서버 주소정보 초기화 */
    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]));

    /* 서버 주소정보를 기반으로 주소할당 */
    bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr));

    /*
    이때 진정한 서버소켓(리스닝소켓)이 됨
    연결요청 대기큐가 생성되고, 클라이언트의 연결요청이 가능해짐
    */
    listen(serv_sock,5);

    clnt_adr_sz=sizeof(clnt_adr);
    /* 클라이언트의 연결요청을 수락
    이때, 클라이언트와의 송수신을 위한 새로운 소켓 생성 */
    clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr,&clnt_adr_sz);

    /* 파일 디스크립터를 통해 FILE 구조체 포인터 얻기 */
    readfp=fdopen(clnt_sock,"r");
    writefp=fdopen(dup(clnt_sock),"w"); // 파일 디스크립터를 복사하면서 진행함을 유의하면서 보자!

    /* 클라이언트에게 데이터 전송 */
    fputs("FROM SERVER: Hi- client? \n",writefp);
    fputs("I love all of the world \n",writefp);
    fputs("You are awesome! \n",writefp);
    fflush(writefp);

    /* Half-close */
    shutdown(fileno(writefp),SHUT_WR); // 출력스트림 종료(클라이언트에 EOF 전달)
    fclose(writefp); // 쓰기모드 파일 구조체 포인터가 소멸되면서 복사된 파일 디스크립터도 소멸!

    fgets(buf,sizeof(buf),readfp); // 아직 입력스트림은 살아있으므로 클라이언트로부터 데이터 수신
    fputs(buf,stdout); // 콘솔로 수신한 데이터를 출력
    fflush(stdout);

    fclose(readfp); // 이 때 비로소 모든 파일 디스크립터가 소멸되면서 소켓도 종료됨.

    return 0;
}

sep_clnt.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024

int main(int argc, char* argv[])
{
    int sock;
    char buf[BUF_SIZE];
    struct sockaddr_in serv_adr;

    FILE* readfp;
    FILE* writefp;
    
    sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성

    /* 서버 주소정보 초기화 */
    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]));

    /*
    서버 주소정보를 기반으로 연결요청
    이때 비로소 진정한 클라이언트 소켓이 됨
    서버소켓의 연결요청 대기큐에 들어가게 됨
    */
    connect(sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr));

    /* 파일 디스크립터를 통해 FILE 구조체 포인터를 얻음 */
    readfp=fdopen(sock,"r");
    writefp=fdopen(sock,"w");

    while(true)
    {
        /*
        서버로부터 데이터를 수신
        서버에서 EOF 전달시 NULL 반환
        함수 특성상 자동으로 끝에 널문자 추가
        */
        if(fgets(buf,sizeof(buf),readfp)==NULL)
            break;

        fputs(buf,stdout); // 콘솔로 수신한 데이터를 출력
        fflush(stdout);
    }

    fputs("FROM CLIENT: Thank you! \n",writefp); // 서버에게 데이터 전송
    fflush(writefp);

    fclose(writefp); fclose(readfp); // 클라이언트 소켓 연결 종료

    return 0;
}

[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어