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

Ch 15. 소켓과 표준 입출력

by minjunkim.dev 2020. 8. 10.

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

 

    이번 파트를 공부하기에 앞서, 제가 초반에 정리해서 올렸던 "표준 파일 입출력 함수(고수준 파일 입출력 함수)" 글을 보고 오시면 도움이 될 것입니다.


# 표준 입출력 함수의 두가지 장점
1) 이식성이 좋다
- 모든 표준 함수들은 이식성이 좋음 : 모든 운영체제(컴파일러)가 지원하도록 ANSI C 표준으로 정의했기 때문
2) 버퍼링을 통한 성능의 향상에 도움이 된다

- 소켓을 생성하면 운영체제는 입출력을 위한 버퍼(이하, 소켓 버퍼)를 마련하고,

: TCP 프로토콜을 진행하는데 매우 중요한 역할
- 표준 입출력 함수 사용시, 이와는 별도로 추가적인 또 하나의 버퍼(이하, 입출력 함수 버퍼)를 제공받는다.
- 따라서 소켓을 통한 데이터 송수신시 표준 입출력 함수를 사용하여 데이터를 전송할 경우,

거쳐야 하는 버퍼는 총 "2개"이다.

 

# 데이터 전송시 과정
1) 데이터가 표준 입출력 함수의 출력버퍼에 전달됨
2) 이어서 소켓의 출력버퍼로 데이터가 이동
3) 이어서 상대 호스트로 데이터가 전송됨

 

# 데이터 수신시 과정

1) 데이터가 소켓의 입력버퍼로 전달됨

2) 이어서 표준 입출력 함수의 입력버퍼로 이동

3) 이어서 호스트가 데이터를 수신함

 

# 버퍼

- 기본적으로 성능의 향상이 목적

- 그러나, 소켓 버퍼는 TCP의 구현을 위한 목적이 더 강함.

e.g. TCP에서는 데이터가 분실되면 재전송을 진행하는데,

이것은 데이터를 소켓의 출력버퍼에 저장해놓고 있었기 때문에 가능한 것.

- 반면 표준 입출력 버퍼는 "오로지 성능 향상만을 목적"으로 제공이 됨

- 전송해야 할 데이터의 양이 많을수록 버퍼링의 유무에 따른 성능의 차이는 커짐

- 버퍼링이 미치는 성능의 우월함

10바이트를 전송하고자 하는 상황에서,

1. 1바이트짜리 데이터를 열 개의 패킷에 보내는 경우

2. 10바이트짜리 데이터를 한 개의 패킷에 보내는 경우

를 고려해보자.

데이터 전송을 위해 구성된 패킷에는 "헤더정보"라는 것이 추가되는데,

이는 전송하는 데이터 크기에 상관없이 일정한 크기구조를 갖는다.

만약 헤더정보의 크기를 40바이트로만 잡아도(실제로는 이보다 크다고 함),

 

1) 전송하는 데이터 양

1. 40 x 10 = 400바이트

2. 40 x 1 = 40바이트

 

2) 출력버퍼로의 데이터 이동 횟수 : 데이터 전송을 위해

입출력 함수 버퍼에서 소켓의 출력버퍼로 이동시키는 데도 제법 많은 시간이 소모된다.

1. 이동 횟수 10번

2. 이동 횟수 1번

=> 1), 2)를 고려해보았을 때, 버퍼링은 데이터 송수신 성능에 큰 영향을 미친다는 것을 알 수 있다.

 

# 단순한 파일 복사조차도 시스템 함수보다 표준 입출력 함수 사용이 훨씬 빠르다.
따라서, 실제 네트워크 상에서 데이터를 송수신하는 것이면 더 말할 필요가 없다.


# 표준 입출력 함수의 단점
- 양방향 통신이 쉽지 않음
- 상황에 따라서 fflush 함수의 호출이 빈번히 등장할 수 있음
: 버퍼링 문제로 인해 읽기 => 쓰기, 쓰기 => 읽기 작업의 전환할 때마다,
fflush 함수를 호출해야 하는데, 이렇게 되면 표준 입출력 함수의 장점인 버퍼링 기반의 성능향상에도 영향을 미침
- 파일 디스크립터를 FILE 구조체의 포인터로 변환해야 함
: 기본적으로 소켓은 파일 디스크립터를 반환하는데,

이를 표준 입출력함수에 사용하려면 FILE 포인터로 변환하는 과정을 거쳐야 함.


# fdopen 함수를 이용한 FILE 구조체 포인터로의 변환

#include <stdio.h>

FILE * fdopen(int fildes, const char * mode);
// -> 성공시 변환된 FILE 구조체 포인터, 실패시 NULL 반환


- fildes : 변환할 파일 디스크립터
- mode : 생성할 FILE 구조체 포인터의 모드(mode)정보

 

 

# fileno 함수를 이용한 파일디스크립터로의 변환 : fdopen 함수의 반대 기능

#include <stdio.h>

int fileno(FILE * stream);
// -> 성공시 변환된 파일 디스크립터, 실패시 -1 반환

# 소켓 기반에서의 표준입출력 사용
- 크게 달라질 것은 없으나, 데이터 송수신시에 파일 디스크립터를 이용한 시스템함수(read, write)가 아니라
FILE 구조체포인터를 이용한 표준 입출력함수(fgets, fputs)를 사용하면 된다.
- 파일 디스크립터를 FILE 구조체 포인터로 변환하고,
이를 통해 파일을 열고 fclose()하면 파일(소켓) 자체가 완전히 종료되기 때문에,
따로 close(파일 디스크립터); 를 또 해줄 필요는 없다.

 

- echo_stdserv.c (Ch 04의 echo_server.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
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, clnt_adr;
    socklen_t clnt_adr_sz;
    FILE *readfp;
    FILE *writefp;

    if(argc!=2) // 실행파일의 경로 / PORT번호 를 입력 받아야 함
    {
        printf("Usage : %s <port>\n",argv[0]);
        exit(EXIT_FAILURE);
    }

    serv_sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성
    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); // INADDR_ANY로 IP정보를 초기화 했음을 유의!
    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) // 5번의 클라이언트의 연결요청을 수락하기 위함임
    {
        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(EXIT_FAILURE);
}

 

- echo_stdclnt.c (Ch 04의 echo_client.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
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) // 실행파일의 경로 / IP / PORT 번호를 입력받아야 함
    {
        printf("Usage : %s <IP> <port>\n",argv[0]);
        exit(EXIT_FAILURE);
    }

    sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성
    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................");
    
    /* 파일 디스크립터를 FILE 구조체 포인터로 변환 */
    readfp=fdopen(sock,"r");
    writefp=fdopen(sock,"w");

    while(true) // q나 Q를 입력할 때까지 문자열 전송
    {
        fputs("Input message(Q to exit): ",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(readfp);
    fclose(writefp);
    // close(sock); 이미 파일 구조체 포인터를 통해 닫아주었으므로 필요 없음

    return 0;
}

void error_handling(char *message)
{
    fputs(message,stderr);
    fputc('\n',stderr);
    exit(EXIT_FAILURE);
}

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