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

Ch 05. 내용 확인문제

by minjunkim.dev 2020. 8. 6.

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


01. TCP 소켓의 연결설정 과정인 Three-way handshaking에 대해서 설명해 보자. 특히 총 3회의 데이터 송수신이 이뤄지는데, 각각의 데이터 송수신 과정에서 주고 받는 데이터에 포함된 내용이 무엇인지 설명해 보자.

 

- SYN(동기화 메시지), ACK(응답 메시지) 를 의미

 

1) SYN => SEQ : 1000(A소켓이 패킷에 1000번이라는 번호를 부여하여 B소켓에 전달), ACK : -
2) SYN + ACK => SEQ : 2000(B소켓이 패킷에 2000번이라는 번호를 부여하여 A소켓에 전달),

ACK : 1001(전에 전달한 SEQ가 1000인 패킷을 잘 받았으니 SEQ가 1001인 패킷을 보내라는 의미)
3) ACK -> SEQ : 1001(B소켓에게 ACK로 1001을 받았으므로 SEQ로 1001을 전달),

ACK : 2001(전에 전달한 SEQ가 2000인 패킷을 잘 받았으니 SEQ가 2001인 패킷을 보내라는 의미)
=> 이렇게 "패킷에 번호를 부여해서 확인하는 절차"를 거치기 때문에 손실된 데이터의 확인 및 재전송이 가능
=> 때문에 TCP는 손실 없는 데이터의 전송을 보장


02. TCP는 데이터의 전송을 보장하는 프로토콜이다. 그러나 인터넷을 통해서 전송되는 데이터는 소멸될 수 있다. 그렇다면 TCP는 어떠한 원리로 중간에 소멸되는 데이터의 전송까지 보장을 하는 것인지 ACK와 SEQ를 대상으로 설명해보자.

 

- ACK 번호 = SEQ 번호 + 전송된 바이트 크기 + 1(다음번에 전달될 SEQ의 번호를 알리기 위해 1을 더함)

- TCP 소켓은 ACK 응답을 요구하는 패킷 전송 시에 타이머를 동작시킴
- SEQ를 보내고 일정시간(타이머) 안에 상대 소켓으로부터 ACK를 받지 못한다면 재전송을 진행

- 데이터 전송 호스트는 메시지와 함께 SEQ를 같이 보내고, 데이터 수신 호스트는 메시지를 정상적으로 받았다면 받은 SEQ를 기반으로 ACK 메시지를 보냄

- 이러한 형태로 데이터 송수신을 확인하고, 전송 실패시에는 데이터 재전송을 진행함

 


03. TCP 소켓을 기반으로 write 함수와 read 함수가 호출되었을 때의 데이터 이동을 입력버퍼와 출력버퍼의 상태와 더불어서 설명해보자.

 

# TCP 소켓에 존재하는 입출력버퍼

- write 함수가 호출되는 순간, 데이터는 바로 전송되는 것이 아님 => 데이터가 출력버퍼로 이동함

- 송신 소켓의 출력버퍼의 데이터가 수신 소켓의 입력버퍼로 이동

- read 함수가 호출되는 순간, 데이터는 바로 수신되는 것이 아님 => 입력버퍼에 저장된 데이터를 읽음


04. 데이터를 수신할 상대 호스트의 입력버퍼에 남아있는 여유공간이 50byte인 상황에서 write 함수호출을 통해서 70byte의 데이터 전송을 요청했을 때, TCP는 어떻게 이를 처리하는지 설명해보자.

 

TCP 프로토콜의 일부인 "슬라이딩 윈도우" 프로토콜로 인해, 입력버퍼의 크기를 초과하는 분량의 데이터 전송은 발생하지 않는다. 따라서 70byte 중, 50byte 만 입력버퍼로 데이터가 전송될 것이다.


05. Ch 02에서 보인 예제 tcp_server.c와 tcp_client.c에서는 서버가 전송하는 문자열을 클라이언트가 수신하고 끝낸다. 그런데 이번에는 서버와 클라이언트가 한번씩 문자열을 주고받는 형태로 예제를 변경해보자.

 

단, 데이터의 송수신이 TCP 기반으로 진행된다는 사실을 고려하여 문자열 전송에 앞서 문자열의 길이 정보를 4바이트 정수의 형태로 먼저 전송하기로 하자. 즉, 연결이 된 상태에서 서버와 클라이언트는 다음의 유형으로 데이터를 송수신해야 한다.

 

# 윈도우 기반 프로그램의 구현은 제외하였습니다.

 

# 리눅스 기반 서버

tcp_server3.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char* message);

int main(int argc, char *argv[])
{
    int serv_sock;
    int clnt_sock;
    int i;
    int size;

    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size;

    char send_message[]="Hello World!";
    char recv_message[30];

    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_addr,0,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi(argv[1]));

    // 설정한 주소정보를 토대로 서버소켓의 주소를 할당
    if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
        error_handling("bind() error");

    // 연결대기큐를 생성함(이제서야, 진정한 서버소켓의 역할을 하게됨)
    // 이 함수 호출 이후부터, 클라이언트의 연결요청(connect)가 허용됨
    if(listen(serv_sock,5)==-1)
        error_handling("listen() error");

    clnt_addr_size=sizeof(clnt_addr);
    size=sizeof(send_message); // 각 클라이언트로 전달할 문자열의 바이트 크기(널문자 포함)

    for(i=0;i<3;++i) // 3개의 클라이언트에 대해 연결요청을 수락하기 위함임
    {
        // 연결요청한 클라이언트에 대해 그 요청을 수락
        // 연결대기큐에 있는 순서대로 요청을 수락
        // 아직 연결요청한 클라이언트가 없다면 블로킹

        // 이때, 연결요청한 클라이언트소켓과 1:1로 연결되는 소켓이 생성됨(서버 소켓과는 다른 소켓임)
        clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr, &clnt_addr_size);
        if(clnt_sock==-1)
            error_handling("accept() error");
        
        write(clnt_sock,(char*)&size,4); // 문자열 길이를 4바이트 형태로 클라이언트로 전달
        write(clnt_sock,send_message,size); // 문자열을 널문자 포함하여 클라이언트로 전달
        
        read(clnt_sock,(char*)&size,4); // 문자열 길이를 4바이트 형태로 클라이언트로부터 수신
        read(clnt_sock,recv_message,size); // 문자열을 널문자 포함하여 클라이언트로부터 수신

        printf("Message from client: %s",recv_message);
        
        close(clnt_sock); // 클라이언트 소켓과 연결될때 생성된 소켓의 연결 종료
    }

    close(serv_sock); // 서버소켓의 연결 종료
    return 0;
}

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

 

# 리눅스 기반 클라이언트

tcp_client3.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char* message);

int main(int argc, char *argv[])
{
    int sock;
    struct sockaddr_in serv_addr;
    char message[30];
    int idx=0, size;
    int i;

    if(argc!=3) // 인자로 실행파일명/(서버의)IP/(서버의)Port번호 를 전달받아야 함
    {
        printf("Usage : %s <IP> <port>\n",argv[0]);
        exit(1);
    }

    sock=socket(PF_INET,SOCK_STREAM,0); // TCP 클라이언트소켓 생성
                                        // 아직 명확하게 "클라이언트 소켓"은 아님, 그냥 소켓임
    if(sock==-1)
        error_handling("socket() error");

    // 서버소켓의 주소정보를 설정(자세한 내용은 뒤의 챕터에서 설명)
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_addr.sin_port=htons(atoi(argv[2]));

    // 클라이언트소켓이 주소정보를 토대로 서버소켓에게 연결요청(이때, 이 소켓이 클라이언트소켓이 됨)
    // 이때, 클라이언트소켓의 주소정보가 할당됨
    // IP는 호스트IP로, Port번호는 임의로.
    if(connect(sock,(struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
        error_handling("connect() error!");

    read(sock,(char*)&size,4); // 서버에서 받을 문자열 길이를 4바이트 형태로 수신
    read(sock, message,size); // 서버에서 널문자를 포함한 문자열을 수신
    printf("Message from server: %s \n",message);

    fputs("Input message : ",stdout);
    fgets(message,sizeof(message),stdin); // 서버로 보낼 문자열을 입력받음

    size=sizeof(message);
    write(sock,(char*)&size,4); // 서버로 보낼 문자열 길이를 4바이트 형태로 전달
    write(sock,message,size); // 널문자를 포함한 문자열을 서버로 전달

    close(sock);
    
    return 0;
}

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

# 실행결과


6. 파일을 송수신하기 위한 클라이언트와 서버를 구현하되, 다음 순서의 시나리오를 기준으로 구현해보자

 

- 클라이언트는 프로그램 사용자로부터 전송 받을 파일의 이름을 입력 받는다.

- 클라이언트는 해당 이름의 파일전송을 서버에게 요청한다.

- 파일이 존재할 경우 서버는 파일을 전송하고, 파일이 존재하지 않을 경우 그냥 연결을 종료한다.

 

# 리눅스 기반으로 구현하였고, 저는 연습을 위해 TCP 소켓 기반에서 표준 입출력 함수 사용하여 파일입출력을 구현하였다. 다소 어렵다면, 소켓 기반으로만 파일입출력을 하여도 무방하다.(저도 조금 어려웠네요..) 자세한 내용은 뒤 챕터에서 나올 것이다.

 

file_server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char* message);

int main(int argc, char *argv[])
{
    int serv_sock;
    int clnt_sock;
    int i;
    int size;

    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size;
    FILE *read_fp,*write_fp;

    char file_name[30];
    char line[1024];

    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_addr,0,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi(argv[1]));

    // 설정한 주소정보를 토대로 서버소켓의 주소를 할당
    if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
        error_handling("bind() error");

    // 연결대기큐를 생성함(이제서야, 진정한 서버소켓의 역할을 하게됨)
    // 이 함수 호출 이후부터, 클라이언트의 연결요청(connect)가 허용됨
    if(listen(serv_sock,5)==-1)
        error_handling("listen() error");

    clnt_addr_size=sizeof(clnt_addr);
    
    // 연결요청한 클라이언트에 대해 그 요청을 수락
    // 연결대기큐에 있는 순서대로 요청을 수락
    // 아직 연결요청한 클라이언트가 없다면 블로킹
    // 이때, 연결요청한 클라이언트소켓과 1:1로 연결되는 소켓이 생성됨(서버 소켓과는 다른 소켓임)
    clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr, &clnt_addr_size);
    if(clnt_sock==-1)
        error_handling("accept() error");
    
    read(clnt_sock,file_name,sizeof(file_name)); // 널문자를 포함하여 파일 이름을 클라이언트로부터 수신

    read_fp=fopen(file_name,"r");
    if(read_fp==NULL) // 파일 이름이 존재하지 않으면 모두 종료
    {
        printf("%s not exists.",file_name);
        fclose(read_fp);
        close(serv_sock);
        exit(EXIT_FAILURE);
    }    


    write_fp=fdopen(clnt_sock,"w"); // 클라이언트와 연결된 소켓의 파일 디스크립터를 파일 구조체 포인터로 변환
    while(fgets(line,sizeof(line),read_fp)!=NULL) // 파일로부터 다 읽어서
    {
        fputs(line,write_fp); // 표준 출력 버퍼로 데이터 이동후
        fflush(write_fp); // 표준 출력 버퍼를 비워 소켓 출력버퍼로 바로 이동시키기
    }
    
    fclose(read_fp); // 파일에 대해 닫기
    fclose(write_fp); // 클라이언트와 연결된 소켓의 연결 종료

    close(serv_sock); // 서버소켓 연결종료
    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 <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char* message);

int main(int argc, char *argv[])
{
    int sock;
    struct sockaddr_in serv_addr;
    char file_name[30];
    char line[1024];
    int idx=0, size;
    int i;
    FILE *write_fp,*read_fp;

    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_addr,0,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_addr.sin_port=htons(atoi(argv[2]));

    // 클라이언트소켓이 주소정보를 토대로 서버소켓에게 연결요청(이때, 이 소켓이 클라이언트소켓이 됨)
    // 이때, 클라이언트소켓의 주소정보가 할당됨
    // IP는 호스트IP로, Port번호는 임의로.
    if(connect(sock,(struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
        error_handling("connect() error!");

    fputs("Input file name : ",stdout); 
    scanf("%s",file_name); // 사용자로부터 파일 이름 입력받기

    write(sock,file_name,sizeof(file_name)); // 파일이름을 널문자 포함하여 서버에 전달

    read_fp=fdopen(sock,"r"); // 클라이언트 소켓의 파일 디스크립터를 파일 구조체 포인터로 변환
 
    write_fp=fopen(strcat(file_name,"_cpy"),"w"); // "파일이름_cpy" 파일을 새로 생성하여 여기다 복사할 것임

    while(fgets(line,sizeof(line),read_fp)!=NULL) // 서버로부터 파일을 모두 읽어
    {
        fputs(line,write_fp); // 표준 출력 버퍼에 데이터를 이동시키고
        fflush(write_fp); // 곧바로 출력 버퍼를 비워 데이터 출력
    }

    fclose(read_fp); // 클라이언트 소켓 연결 종료
    fclose(write_fp); // 새로운 파일을 종료
    
    return 0;
}

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

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