본문 바로가기
개발/리눅스

[Linux]TCP의 전송특성과 에코 클라이언트의 문제점 해결

by p_human 2020. 4. 1.

잡담

오늘은 TCP의 전송 특성에 대해서 알아볼 것인데 그전에 소켓, 프로토콜, 데이터의 전송방식에 대해서 자세히 알아보겠습니다.
원래는 처음부터 소켓에 대해서 소개를 했어야 했는데 순서가 뒤죽박죽이 돼버렸네요... 그래도 서버/클라이언트 구현에 대한 전체적인 흐름과 코드를 여러 개 작성하고, 그 뒤에 이론을 설명하면 "아 이 함수가 이런 역할을 하는 함수였구나"라고 좀 더 와 닿지 않을까 해서... 이제 진짜로 들어가 볼게요.

 

소켓의 생성

우선 소켓이란 무엇일까요?

소켓은 네트워크를 통한 두 컴퓨터의 연결을 의미하기도 하고, 네트워크 망에 사용되는 도구라고 생각하면 됩니다.

소켓을 사용하면 네트워크에서 데이터가 송수신되는 원리를 잘 몰라도 데이터 송수신을 할 수 있습니다.

또 운영체제가 소켓을 제공해주기 때문에 프로그램을 작성할 때 가져다 사용하기만 하면 됩니다.


그리고 우리가 이전에 TCP 기반 프로그램이나 에코 서버 / 클라이언트 프로그램을 작성했을 때, 코드를 꼼꼼히 살펴본 분들은 이 함수가 눈에 익으실 것입니다.

// socket 생성
sock=socket(PF_INET, SOCK_STREAM, 0);
// socket 함수의 정의
int socket(int domain, int type, int protocol);

첫 번째 인자에는 소켓이 사용할 프로토콜 체계 정보 전달

두 번째 인자에는 소켓의 타입을 전달

세 번째 인자에는 두 컴퓨터 간 통신에 사용되는 프로토콜 정보 전달


socket 함수는 프로토콜 체계와, 데이터 전송방식이 정해진 소켓의 번호를 반환해주는 함수입니다.

여기서 socket 함수가 반환하는 값은 파일 디스크립터라는 놈입니다. 이놈은 운영체제가 만든 파일 또는 소켓을 편하게 관리하기 위해서 임의로 부여된 번호라서 int형 변수로 값을 반환받거나 인자에 넣어주면 됩니다. (단, 이건 리눅스에서만 해당되는 얘기입니다. 윈도에서는 다른 값을 반환하기 때문에 주의해야 합니다.)

소켓에 대한 소개를 다 했으니 이제 프로토콜로 넘어가 보죠

 

프로토콜이란?

위에서 socket 함수에 대해서 설명할 때도 프로토콜이라는 녀석이 여러 번 등장했었습니다. 이 녀석에 대해서 예를 들 때는 사람대 사람을 예로 듭니다. 이게 무슨 말이냐면 A는 한국어를 사용하고, B는 아랍어를 사용한다고 가정해봅시다. 이렇게 되면 A와 B는 의사소통 자체가 불가능하겠죠?

그래서 등장한 게 세계 공통어 영어라고 합시다. 이후에 A, B는 서로 다른 언어를 사용하면서 겪었던 문제를 세계 공통어인 영어를 사용하게 되면서 의사소통을 원활하게 할 수 있게 되었습니다.

이처럼 컴퓨터도 통신을 서로 다른 방법으로 하면 안 되니, 통신 방법에 대해서 정해놓은 규칙을 프로토콜이라고 합니다.
그럼 프로토콜 체계에 대해서 알아보겠습니다.

프로토콜의 체계는 다음과 같이 나눌 수 있습니다.

 

프로토콜 체계(Protocol Family)

앞으로 모든 서버/클라이언트 예제 프로그램들은 프로토콜 체계 중 PF_INET을 사용할 것입니다.

그리고 PF_INET은 socket 함수의 첫 번째인 자로 들어가게 됩니다.

문득 든 생각인데 네트워크 프로그램을 만들 때 PF_INET6을 당연하게 사용하는 날이 올까요? IPv6가 보편화되려면 어느 정도의 시간이 걸릴까요..? 지금보다 몇십 배나 작고, 몇천만 배나 많은 기기들이 사람들 주변에 있는 게 당연할 때일까요? 이런 상상하면 언제나 즐거워요 ㅎㅎ...

지금까지 소켓을 생성하는 socket 함수와 필요한 매개변수인 프로토콜 체계에 대해서 알아보았습니다.
이제 나머지 소켓의 타입들을 알아본 다음에 어제 해결하지 못한 문제를 풀어보겠습니다.

 

 

소켓의 타입(Type)

소켓의 타입이란 소켓의 데이터 전송방식을 의미하고 socket 함수의 두 번째 인자로 소켓의 타입을 지정해주면, 그 방식으로 데이터를 전송하는 소켓이 생성되는 것입니다.

그리고 세 번째 인자는 필요 없을 것 같지만 필요한 이유가 있습니다. 그 이유는 하나의 프로토콜 체계에 데이터 전송 방식이 동일한 프로토콜이 존재할 수 있기 때문에 지정을 해줘야 합니다. 일반적인 상황에서는(IPv4를 사용) 0을 넣어줘도 무방합니다.

소켓의 타입은 두 가지로 나뉩니다.

연결 지향형(SOCK_STREAM), 비 연결 지향형(SOCK_DGRAM)
비 연결 지향형은 다다음 글에서 소개하겠습니다.

먼저 연결 지향형 소켓을 보면 데이터 송수신 방식의 특징을 가지고 있습니다.

 

  • 데이터의 신뢰성
  • 연결 지향
  • 전송되는 데이터의 경계가 존재하지 않는다.

위의 특징 중에서 전송되는 데이터의 경계가 없다는 말이 무슨 의미일까요?

간단히 예를 들어서 설명해보겠습니다.

먼저 A가 편지를 5통을 작성할 때마다 직접 우체부 아저씨에게 가져가서 "이거 20통이 될 때까지 보관했다가 다 차면 그때 B에게 보내주세요."라고 알려줍니다. (여기서 우체부 아저씨는 A, B에게 직접 전달할 수 있다고 가정합니다.)  중요한 점은 A가 5통을 4번에 걸쳐서 보냈다는 걸 기억해주세요. 그리고 우체부 아저씨는 A에게 받은 편지들을 B에게 한 번에 전달했습니다. 그럼 B는 20통의 편지를 받게 될 것입니다.

위의 예에서 중요한 점은 A가 여러 번 나눠서 보냈는데, B는 한 번에 전달받았다는 것입니다. TCP 소켓도 이러한 방식으로 송수신을 진행하기 때문에 "전송되는 데이터의 경계가 없다"라고 말하는 것입니다. 이러한 TCP의 전송방식의 특징을 이용해서 어제 해결하지 못한 에코 클라이언트의 문제점을 풀어보겠습니다.


아래는 에코 클라이언트의 코드입니다.

#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(const char *message);

int main(int argc, const char *argv[])
{
    int sock;
    char message[BUF_SIZE];
    int str_len, recv_len, recv_cnt;
    struct sockaddr_in serv_adr;

    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_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...........");
	
    // 사용자가 q를 입력하기 전까지 계속 반복
    while(1) 
    {
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);
		
        if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
            break;

        // 서버에게 데이터를 한번 보낸다.
        str_len=write(sock, message, strlen(message));
		
        recv_len=0;
        // 서버로부터 전달되는 데이터를 1바이트씩 message변수에 저장한다.
        while(recv_len<str_len)
        {
            recv_cnt=read(sock, &message[recv_len], BUF_SIZE-1);
            if(recv_cnt==-1)
                error_handling("read() error!");
            recv_len+=recv_cnt;
        }
		
        message[recv_len]=0;
        printf("Message from server: %s", message);
    }
	
    close(sock);
    return 0;
}

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

이전의 에코 클라이언트 코드에서는 read함수를 한 번만 호출하고 말았었는데, 이번에는 문자열의 크기만큼 데이터를 받아오기 위해서 반복 호출을 하고 있습니다. 아까 전에 설명했던 TCP의 전송 특징 중에 "전송되는 데이터의 경계는 존재하지 않는다"라고 했었습니다. 이러한 특징이 그대로 적용된 것을 볼 수 있습니다. 저 추가되는 while문을 설명하기 위해서 많은 내용을 설명했네요.


오늘은 설명이 길어서 읽기가 힘들었을 텐데, 여기까지 읽어주신 분들 감사합니다.

그리고 리눅스의 서버/클라이언트 코드를 윈도로 바꾸려고 하는데, 잘 안되네요... 3일 정도면 교체가 가능할 것 같으니, 그때부터 리눅스랑 윈도 코드를 같이 올리겠습니다.

 

여태까지 설명한 내용들을 잘 이해를 했다면? 내일 나가는 진도도 수월.. 하게 진행하실 수 있을 것입니다. 내일은 계산기 서버와 클라이언트를 만들어볼거에요!

재밌겠져??