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

Ch 07. 소켓의 우아한 연결종료

by minjunkim.dev 2020. 8. 8.

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


# TCP에서 "연결과정"에서는 큰 변수가 발생하지 않으나,
"종료과정"에서는 예상치 못한 일이 발생할 수 있기 때문에, 종료과정을 명확히 해야함

# close(리눅스) / closesocket(윈도우) => 완전 종료를 의미(데이터 송수신이 양쪽 다 불가능한 상황을 의미)
- 한 호스트의 일방적 연결종료는 의도치 않은 데이터 소멸을 야기할 수 있음
- 이를 위해 Half-close(스트림의 일부만 종료) 존재

# 소켓을 통해 두 호스트가 연결되면, 데이터 송수신이 가능한 상태가 됨(스트림이 형성된 상태가 됨)
- 스트림은 한쪽 방향의 흐름만 가능하므로, 양방향 통신을 위해서는 (입출력)스트림 두 개가 필요함
- Half-close는 이 두 개의 스트림 중 하나만 끊는 것임

- close/closesocket은 두 개의 스트림 모두를 끊는 것임


# 우아한 종료(Half-close)를 위한 shutdown 함수

#include <sys/socket.h>

int shutdown(int sock, int howto);
// -> 성공시 0, 실패시 -1 반환

- sock : 종료할 소켓의 파일 디스크립터 전달

- howto : 종료방법에 대한 정보 전달

1) SHUT_RD(입력 스트림 종료) : 데이터 수신 및 관련 함수 호출 불가능.

단, 데이터가 입력버퍼에 전달되더라도 삭제됨
2) SHUT_WR(출력 스트림 종료) : 데이터 송신 및 관련 함수 호출 불가능.

단, 출력버퍼에 남아있던 데이터는 목적지로 전송됨
3) SHUT_RDWR(입출력 스트림 종료) : 1, 2를 인자로 하여 shutdown 함수를 한번씩 호출한 것과 같음


# Half-close가 필요한 이유

- 클라이언트가 서버에 접속하면, 서버는 클라이언트에게 약속된 파일을 전송하고,
클라이언트는 파일을 다 수신하면, 잘 받았다는 의미로 서버에 데이터를 전송 하는 경우를 고려해보자.
1) 서버는 단순히 클라이언트에 데이터(파일)를 전송하기만 하면 되나,
2) 클라이언트는 데이터를 언제까지 수신해야 할지 알 도리가 없음
- 클라이언트가 무작정 입력함수 호출하면 => 블로킹에 빠질 수 있고
- 파일의 끝을 의미하는 문자를 약속한다고 해도 => 데이터 안에 그 문자가 있을 수도 있음
따라서,
=> 서버가 파일 전송이 끝났음을 알리는 목적으로 클라이언트에 "EOF"를 마지막에 전송하게 하자
=> 이러면 파일의 데이터와 중복될 일도 없다. 그렇다면 서버는 EOF를 어떻게 전달할 수 있을까
=> 단순하게 close를 호출하여 입출력 스트림 모두를 종료해도 EOF가 전달되지만,

이렇게 되면 클라이언트로부터 서버가 데이터를 수신할 수 없음
=> 따라서, shutdown 함수로 서버의 출력스트림만을 끊으면, 클라이언트로 EOF를 전달하면서도

클라이언트로부터 여전히 데이터 수신이 가능해짐


# Half-close 기반의 파일전송 프로그램

 

file_server.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 30
void error_handling(char *message);

int main(int argc,char *argv[])
{
    int serv_sd, clnt_sd;
    FILE * fp;
    char buf[BUF_SIZE];
    int read_cnt;

    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t clnt_adr_sz;

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

    fp=fopen("file_server.c","rb"); // 지금 이 소스파일 자체를 바이너리 읽기전용 형태로 엶
    serv_sd=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_sd, (struct sockaddr*)&serv_adr, sizeof(serv_adr));

    /* 이 때, 비로소 진정한 서버소켓이 되며
    연결요청 대기큐가 생성됨.
    이 함수 호출 이후부터 클라이언트의 connect 함수 호출이 허용됨.
    */
    listen(serv_sd, 5);

    clnt_adr_sz=sizeof(clnt_adr);
    clnt_sd=accept(serv_sd,(struct sockaddr*)&clnt_adr,&clnt_adr_sz);
    // 연결요청 대기큐에서 대기중인(client 함수를 호출한) 클라이언트와의 연결을 수락

    while(true)
    {
        read_cnt=fread((void*)buf,1,BUF_SIZE,fp);
        /* 파일로부터 한 요소의 크기가 1바이트이고 요소의 개수가 BUF_SIZE 만큼인
        배열만큼 데이터를 파일로부터 읽어들여서 buf에 저장 */

        if(read_cnt<BUF_SIZE) // 방금 읽은 내용이 파일의 마지막 내용이라는 의미이므로
        {
            write(clnt_sd,buf,read_cnt); // 클라이언트로 송신하고 반복문 탈출
            break;
        }
        write(clnt_sd,buf,BUF_SIZE); // 읽은 파일 내용을 클라이언트로 송신
    }

    /* 클라이언트로 모두 파일을 송신후 출력스트림 종료
    이러면, 클라이언트는 서버로부터 EOF를 수신함
    */
    shutdown(clnt_sd,SHUT_WR); 
    
    /* 클라이언트가 파일을 다 수신하고 보낸 메시지를 서버가 수신 */
    read(clnt_sd,buf,BUF_SIZE);
    printf("Message from client: %s\n",buf);


    fclose(fp); // /* 파일 닫기 */
    close(clnt_sd); /* 클라이언트와 연결을 위해 생성된 소켓 연결종료 */
    close(serv_sd); /* 서버소켓 연결종료 */
    return 0;
}

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

file_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 30
void error_handling(char *message);

int main(int argc,char *argv[])
{
    int sd;
    FILE *fp;

    char buf[BUF_SIZE];
    int read_cnt;
    struct sockaddr_in serv_adr;

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

    fp=fopen("receive.dat","wb"); // 바이너리 파일을 쓰기전용으로 엶
    sd=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(sd, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
    
    while((read_cnt=read(sd,buf,BUF_SIZE))!=0) // 서버로부터 EOF를 수신할때까지 데이터를 수신하여
        fwrite((void*)buf,1,read_cnt,fp); // 연 파일에 데이터를 쓰기
    
    puts("Received file data");
    write(sd, "Thank you",10); // 서버로부터 파일을 다 전송받으면, 서버에게 메시지 전달
    fclose(fp); // 파일 닫기
    close(sd); // 클라이언트 소켓 종료
    return 0;
}

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

 

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