Part1_TCP 기반 서버 클라이언트 2

·2023년 11월 6일
0

[에코 를라이언트! TCP 기반에서의 완벽 구현]

TCP는 연결 지향 프로토콜로서 전송되는 데이터의 경계가 없음
그러다 보니 한 번의 write 함수 호출을 통해서 “ABCD”라는 문자열을 전송할지라도 그 데이터들이 반드시 하나의 패킷으로 구성되어서 전송된다고 보장할 수 없음
상황에 따라서 “AB” 문자열이 먼저 하나의 패킷으로 전송되고, 그 다음에 “C”가 전송되고, 마지막으로 “D”가 전송될 수도 있음
→ client가 read()를 호출하는 시점이 “AB” 패킷만 클라이언트에 도달 한 상태일 수도 있음

그렇다면 TCP 기반으로 데이터를 주고 받을 때 어떤 방법을 사용해야 할까?

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

void error_handling(char* message);

int main(int argc, char** argv)
{
    int sock;
    char message[30];
    int str_len, recv_len, recv_num;
    struct sockaddr_in serv_addr;

    if(argc!=3)
    {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    sock=socket(PF_INET, SOCK_STREAM, 0);
    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]));

    if(connect(sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error!");

    while(1)
    {
        fputs("전송할 메시지를 입력하세요 (q to quit) : ", stdout);
        fgets(message, sizeof(message), stdin);

        if(!strcmp(message, "q\n")) break;
        write(sock, message, strlen(message));

        for(recv_len = 0; recv_len < str_len;)
        {
            recv_num=read(sock, &message[recv_len], str_len - recv_len);
            if(recv_num == -1)
                error_handling("read() error!");
            recv_len += recv_num;
        }
        
        message[str_len] = 0;
        printf("서버로부터 전송된 메시지 : %s \n", message);
    }

    close(sock);
    return 0;
}

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

이전의 에코 클라이언트와 달리 전송된 데이터가 에코되어 완전히 돌아올 때까지 계속해서 read 함수를 호출해서 수신한 데이터를 배열에 저장함

→ 정확히 전송한 바이트 크기만큼의 데이터를 수신할 수 있음

[경계가 없는 TCP 기반의 데이터 전송]

TCP 기반에서 데이터 전송 시 경계가 없다고 했는데 어떻게 확인할까?

→ 여러 번의 write 함수 호출로 전송된 데이터를 한 번의 read 함수를 호출해서 수신하고, 한 번의 write 함수 호출로 전송된 데이터를 여러 번의 read 함수 호출로 나누어서 수신해보자

// bnd_server.c

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

#define BUFSIZE 100

void error_handling(char* message);

int main(int argc, char** argv)
{
    int serv_sock;
    int clnt_sock;
    char message[BUFSIZE];
    int str_len;

    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    int clnt_addr_size;

    if(argc!=2)
    {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    serv_sock=socket(PF_INET, SOCK_STREAM, 0);
    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");
    
    if(listen(serv_sock, 5) == -1)
        error_handling("listen() error");
    
    clnt_addr_size=sizeof(clnt_addr);
    clnt_sock=accept(serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size);

    if(clnt_sock == -1)
         error_handling("accept() error");

    // 5초간 대기 상태로 들어감
    // 메시지가 하나만 도착했을 때 서버가 read 함수를 호출하지 않도록
    sleep(5);
    str_len=read(clnt_sock, message, BUFSIZE);
    write(clnt_sock, message, str_len);

    close(clnt_sock);
    return 0;
}

void error_handling(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
// bnd_client.c

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

void error_handling(char* message);

int main(int argc, char** argv)
{
    int sock;
    int str_len, i;
    struct sockaddr_in serv_addr;

    char msg1[] = "Hello Everybody";
    char msg2[] = "I am so happy!!!";
    char message[10];

    if(argc!=3)
    {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    sock=socket(PF_INET, SOCK_STREAM, 0);
    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]));

    if(connect(sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error!");

    // 메시지 1차, 2차 전송
    write(sock, msg1, strlen(msg1));
    write(sock, msg2, strlen(msg2));

    for(i = 0; i < 4; i++)
    {
        str_len=read(sock, message, sizeof(message)-1);
        message[str_len] = 0;
        printf("서버로부터 전송된 메시지 : %s \n", message);
    }

    close(sock);
    return 0;
}

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

[실행 결과]

클라이언트가 4번의 read 함수 호출을 통해서 모든 메시지를 수신했다는 것을 알 수 있음

[버퍼가 존재한다?]

서버가 데이터를 한 번에 대략 40바이트 정도 전송하고, 클라이언트는 대략 10바이트씩 4번에 걸쳐서 수신을 했음
→ 클라이언트 영역에는 데이터를 수신하기 위한 버퍼가 존재함
⇒ 클라이언트에서 read()를 호출할 때마다 버퍼에서 데이터를 조금씩 읽어들이고 있음

서버와 클라이언트는 흐름을 제어해 가면서 데이터를 주고 받음
→ 대표적으로 슬라이딩 윈도우 프로토콜이 있음

서버 영역에서 write 함수 호출을 통해서 100바이트를 클라이언트 영역으로 전송했다고 치면 write 함수의 호출과 동시에 클라이언트 영역으로 모든 데이터가 바로 전송되는 것은 아님
write 함수 호출과 동시에 서버의 출력 버퍼로 100바이트가 전달됨
그 후 TCP에 의해서 수신측 호스트가 수용할 수 있는 만큼의 데이터만 패킷화해서 전송하기 시작함

예를 들어, 데이터를 수신하는 호스트가 현재 30바이트까지 수용할 수 있다는 메시지를 서버로 전달하면, 100바이트 중에서 30바이트만 패킷화해서 전송하고, 70바이트는 버퍼에 남겨둠
다시 클라이언트가 20바이트까지 수용할 수 있다고 메시지를 전달 해 오면 20바이트를 패킷화해서 전송함
이런식으로 총 100바이트를 전송하게 되므로 몇 개의 패킷이 생성되어 데이터가 전달될지는 아무도 알 수 없음

⇒ 클라이언트와 서버 모두 소켓 생성 시 입력과 출력을 위한 버퍼가 커널에 의해 생성되며, 데이터 송/수신이 흐름 제어 프로토콜을 기반으로 해서 진행됨

[TCP의 내부 구조]

  • 연결 설정 (3 way handshake) TCP 클라이언트의 connect() 호출과 동시에 시작됨
    (1) SYN
    (B야 전송할 데이터가 있으니 연결하자, SEQ : 1000)
    (2) SYN + ACK
    (A야 연결 요청 패킷 잘 받았고 나도 준비됐어, SEQ : 2000, ACK : 1001)
    (3) ACK
    (B야 패킷 잘 받았고 데이터 이제 주고 받자, SEQ : 1001, ACK : 2001)
  • 연결 종료 (4 way handshake) (1) FIN
    B야 이제 종료할게 (SEQ : 5000)
    (2) ACK
    A야 알겠어 잠시만 기다려줘 (SEQ : 7500, ACK : 5001)
    (3) FIN
    A야 나도 종료 준비 끝났어 종료하자 (SEQ : 7501, ACK : 5001)
    (5) ACK
    B야 알겠어 안녕(SEQ : 5001, ACK : 7502)

0개의 댓글