본문으로 바로가기
반응형

소켓의 타입과 프로토콜 설정

소켓의 프토토콜와 그에 따른 데이터 전송 특성

프로토콜?

컴퓨터간의 대화가 필요한 통신 규약이다.

 

소켓의 생성

소켓의 생성에 사용되는 socket 함수를 저번 포스팅에서 대충 알아봤는데, 이번에는 제대로 알아보자.

int socket(int domain, int type, int protocol);     // 성공시 파일 디스크럽터, 실패시 -1 반환
// domain : 소켓이 사용할 프로토콜 체계(Protocol Family) 정보 전달
// type : 소켓의 데이터 전송방식에 대한 정보 전달
// protocol : 두 컴퓨터간 통신에 사용되는 프로토콜 정보 전달

 

프로토콜 체계(Protocol Family)

스파게티에는 크림, 로제, 토마토 소스 스파게티가 있다. 이 셋은 스파게티 부류에 속한다.

이렇듯 소켓이 통신에 사용하는 프로토콜도 부류가 나뉜다.

그리고 socket 함수의 첫번째 인자로 생성되는 소켓이 사용할 프로토콜의 부류정보를 전달해야 한다.

이러한 부류정보를 가리켜 '프로토콜 체계'라 하며, 프로토콜 체계의 종류는 다음과 같다.

프로토콜 체계
PF_INET : IPv4 인터넷 프로토콜 체계
PF_INET6 : IPv6 인터넷 프로토콜 체계
PF_LOCAL : 로컬 통신을 위한 UNIX 프로토콜 체계
PF_PACKET : LOW Level 소켓을 위한 프로토콜 체계
PF_IPX : IPX 노벨 프로토콜 체계

표에서 보이는 PF_INET에 해당하는 IPv4 인터넷 프로토콜 체계가 이 책에서 주로 설명하는 프로토콜 체계이다.

이외에도 몇몇 프로토콜 체계가 존재하지만 중요도가 떨어지거나 아직 보편화되지 않았으므로, 앞으로의 예제는 PF_INET에 해당하는 프로토콜 체계에 초점을 맞춰서 진행하도록 하겠다.

 

또, 실제 소켓이 사용할 최종 프로토콜 정보는 socket 함수의 세번째 인자를 통해 전달하게 되어있다.

단, 첫번째 인자를 통해 지정한 프로토콜 체계의 범위내에서 세번째 인자가 결정되어야한다.

 

 

소켓의 타입(Type)

소켓의 타입이란 소켓의 데이터 전송방식을 의미하는데, 바로 이 정보를 socket 함수의 두번째 인자로 전달해야한다.

그래야 생성되는 소켓의 데이터 전송방식을 결정할 수 있다. 근데 필자의 이러한 설명이 애매하게 느껴질수도 있는데,

socket 함수의 첫번째 인자를 통해 프로토콜 체계정보를 전달하기 때문이다.

하지만 프로토콜 체계정보가 결정되었다고해서 데이터의 전송방식까지 완전히 결정되는 것은 아니다.

즉, socket 함수의 첫번째 인자로 전달되는 PF_INET에 해당하는 프로토콜 체계에도 둘 이상의 데이터 전송방식이 존재한다.

 

소켓의 타입 1 : 연결지향형 소켓(SOCK_STREAM)

socket 함수의 두번째 인자로 SOCK_STREAM을 전달하면 '연결지향형 소켓'이 생성된다.  

연결지향형 소켓의 특성은 두 사람이 하나의 라인을 통해 물건을 주고받는 상황에 비유할 수 있다.

위 그림이 보이는 데이터(사탕) 송수신 방식의 특징을 정리하면 다음과 같다.

- 중간에 데이터가 소멸되지 않고 목적지로 전송된다.

- 전송 순서대로 데이터가 수신된다.

- 전송되는 데이터의 경계(Boundary)가 존재하지 않는다.

 

우선 위 그림에서는 독립된 별도의 전송라인을 통해 데이터(사탕)을 전달하기 때문에 라인상의 문제만 없다면 데이터가 소멸하지 않음을 보장받을 수 있다. 뿐만아니라, 먼저 보내진 데이터보다 뒤에 보내진 데이터가 일찍 도착할 수 없다.

전송라인에 올려진 순서대로 데이터가 전달되기 때문이다. 마지막으로 데이터의 경계가 존재하지 않음은 다음과 같은 상황일 것이다.

"사탕 100개가 여러번에 걸쳐서 보내졌다. 그러나 구매자는 사탕 100개가 쌓인 다음에 이를 한번에 봉지에 담아갔다."

 

이 상황은 앞서 설명한 write와 read 함수에 적용하면 다음과 같은 상황으로 이어질 수 있음을 의미한다.

 

"데이터를 전송하는 컴퓨터가 세 번의 write 함수 호출을 통해 총 100바이트를 전송하였다.

그런데 데이터를 수신하는 컴퓨터는 한 번의 read 함수 호출을 통해 100바이트 전부를 수신하였다."

 

데이터를 송수신하는 소켓은 버퍼(buffer), 바이트 배열을 지니고있다. 그리고 소켓을 통해 전송되는 데이터는 이 배열에 저장된다. 때문에 데이터가 수신되었다고해서 바로 read 함수를 호출해야 하는 것은 아니다.

이 배열의 용량을 초과하지 않는 한 데이터가 채워진 후에 한번의 read 함수 호출을 통해 데이터를 전부 읽을 수도 있고,반대로 한번의 write 함수호출로 전송된 데이터 전부를 여러번의 read 함수 호출을 통해 읽어들일 수도 있다.

 

즉, read 함수의 호출회수와 write 함수의 호출 횟수는 연결지향형 소켓의 경우 큰 의미를 갖지 않는다고 볼 수 있다.

때문에 연결지향형 소켓은 데이터의 경계가 존재하지 않는다고 말하는 것이다.

 

마지막으로 위의 그림을 다시보면 일하는 사람중에 보내는 쪽에 한명, 받는 쪽에 한명이 있음을 알 수 있다.

이는 연결지향형 소켓의 다음 특성을 반영한 그림이다.

 

"소켓 대 소켓의 연결은 반드시 1:1 이어야 한다."

 

즉, 연결지향형 소켓 하나는 다른 연결지향형 소켓 하나와만 연결이 가능하다. 

그럼 연결지향형 소켓의 특성을 하나의 문장으로 정리하면

 

"신뢰성 있는 순차적인 바이트 기반의 연결지향 데이터 전송 방식의 소켓"

 

책의 필자는 쉽게 압축하려고 노력했다고 썼지만 드럽게 설명을 못하는 것 같다. 

일단 이 문장의 표현을 이해하려고하기보다는 의미하는 바를 느끼는 쪽으로 가야할 것 같다.

 

 

소켓의 타입 2 : 비연결지향형 소켓(SOCK_DGRAM)

socket 함수의 두 번째 인자로 SOCK_DGRAM을 전달하면 '비연결지향형 소켓'이 생성된다.

그리고 비 연결지향형 소켓은 엄청난 속도로 이동하는 오토바이 택배 서비스에 비유할 수 있다.

그림에서 보이는 택배의 물건(데이터) 송수신 방식의 특징을 정리하면 다음과 같다.

- 전송된 순서에 상관없이 가장 빠른 전송을 지향한다.

- 전송된 데이터는 손실의 우려가 있고, 파손의 우려가 있다.

- 전송되는 데이터의 경계(Boundary)가 존재한다.

- 한번에 전송할 수 있는 데이터의 크기가 제한된다.

 

택배는 속도가 생명이다.

따라서 서로 다른 오토바이에 실려서 동일한 목적지를 향하는 두 개의 물건은 출발순서와 상관없이 최대한 빨리 목적지를 향하게 될 것이다. 뿐만아니라 이 과정에서 택배물의 손식 및 파손의 우려도 존재한다.

그리고 오토바이에 실을 수 있는 물건의 크기도 제한된다.

 

따라서 많은 양의 물건을 목적지로 보내려면 두 번 이상에 걸쳐서 물건을 나눠보내야 할 것이다.

그리고 택배 물건 두개가 각각 별도로 배달이 완료되려면 물건을 받는 사람도 두 번에 걸쳐서 물건을 수령해야한다.

택배를 통해 수령할 물건이 2개인데, 이를 3번에 나눠서 수령할 수는 없을 것이다.

이러한 전송 특성을 두고 "전송되는 데이터의 경계가 존재한다." 고 말한다.

 

즉, 비연결지향형 소켓은 연결지향형 소켓에 비해 데이터 전송속도는 빠르나, 데이터의 손실 및 훼손이 발생하지 않음을 보장하지 않는다. 그리고 한번에 전송할 수 있는 데이터의 크기가 제한되며 데이터의 경계가 존재한다.

 

데이터의 경계가 존재한다는 것은 데이터를 전송할때 두 번의 함수 호출이 수반되었다면, 데이터를 수신할때도 두 번의 함수 호출이 수반되어야함을 의미한다. 지금까지 설명한 비연결지향형 소켓의 특성을 정리하자면 다음과 같다.

 

"신뢰성과 순차적 데이터 전송을 보장하지 않는, 고속의 데이터 전송을 목적으로 하는 소켓"

 

참고로 비연결지향형 소켓은 연결이라는 개념이 존재하지 않는데 이는 다음에 알아보자.

 

 

프로토콜의 최종선택

프로토콜의 체계와 소켓의 타입은 각각 socket 함수의 첫번째, 두번째 인자이다.

이번엔 세번째 인자에 대해 알아볼예정인데, 이는 최종적으로 소켓이 사용하게 될 프로토콜 정보를 전달하는 목적으로 존재한다. 하지만 이미 첫번째, 두번째인자로도 프로토콜 체계와 소켓의 데이터 전송방식에 대한 정보까지 전달하기때문에 프로토콜 결정에 충분한 정도가 될 수 있다고 생각할 수도 있다.

 

이 생각대로 socket 함수의 첫번째, 두번째 전달인자를 통해서도 충분히 원하는 유형의 소켓을 생성할 수 있다.

따라서 대부분의 경우, 세번째 인자로 0을 넘겨줘도 우리가 원하는 소켓을 생성할 수 있다. 하지만 다음과 같은 상황때문에 세번째 인자가 필요하다.

 

"하나의 프로토콜 체계안에 데이터 전송방식이 동일한 프로토콜이 둘 이상 존재하는 경우"

 

즉, 소켓의 데이터 전송방식은 같지만, 그 안에서도 프로토콜이 다시 나뉘는 상황이 존재할 수 있다.

그리고 이러한 경우 세번째 인자를통해 원하는 프로토콜 정보를 조금 더 구체화해야 한다.

 

일단 지금까지 설명한 내용을 토대로 소켓의 생성과정에서 socket 함수에 전달할 수 있는 인자 정보를 구성해보자. 다음 요구사항을 만족하는 소켓의 생성문을 구성해보자.

 

"IPv4 인터넷 프로토콜 체계에서 동작하는 연결지향형 데이터 전송 소켓"

 

위 문장에서 'IPv4' 라는 것은 인터넷 주소체계와 관련있는데, 이에 대해서는 이후에 알아보도록하고 지금은 IPv4 기반으로 내용이 전개된다는 사실만 알면된다. PF_INET이 IPv4 인터넷 프로토콜 체계를 의미하고, SOCK_STREAM이 연결지향형 데이터 전송을 의미한다. 그런데 이 두 가지 조건을 만족시키는 프로토콜은 IPPROTO_TCP 하나이기 때문에 다음과 같이 socket 함수 호출문을 구성한다. 그리고 이때 생성되는 소켓을 가리켜 'TCP 소켓'이라 한다.

int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

 

그럼 이번에는 다음 요구사항을 만족하는 소켓의 생성문을 구성해보자.

 

"IPv4 인터넷 프로토콜 체계에서 동작하는 비연결지향형 데이터 전송 소켓"

 

SOCK_DGRAM이 비연결지향형 데이터 전송을 의미하고, 위의 조건을 만족하는 프로토콜은 IPPROTO_UDP 하나이기 때문에 다음과 같이 socket 함수 호출문을 구성하면 된다. 그리고 이때 생성되는 소켓을 가리켜 'UDP 소켓'이라 한다.

int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);

 

 

연결지향형 소켓 : TCP 소켓의 예시

UDP 소켓에 대해서는 별도의 챕터에서 전개되니, 지금은 연결지향형 소켓인 TCP 소켓의 특성을 파악하기위해 예제를 작성해보자. 저번에 했던 Hello World의 소스파일을 변경해서 작성하면된다.

 

- hello_server.c  -> tcp_server.c     :  소스의 변경사항은 없다.

- hello_client.c   -> tcp_client.c     : read 함수의 호출방식을 변경해준다.

 

앞서 작성한 예제 hello_server.c 와 hello_client.c 가 TCP 소켓 기반의 예제이다. 이를 조금 변경해서 TCP 소켓의 다음 특성을 확인해보고자 한다.

"전송되는 데이터의 경계(Boundary)가 존재하지 않는다."

 

이의 확인을위해 write 함수의 호출 횟수와 read 함수의 호출횟수를 불일치 시켜봐야한다. 때문에 read 함수를 호출하는 클라이언트 프로그램에서는 여러 번의 read 함수를 호출해서 서버 프로그램이 전송한 데이터 전부를 수신하는 형태로 변경하였다.

 

 tcp_server.c 코드는 hello_server.c 코드와 같으니 실행 파일도 그냥 hello_server.c로 만들고 tcp_client.c 코드만 새로 작성하고 실행해보자.

// tcp_client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.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 str_len = 0;
   int idx = 0, read_len = 0;

   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(read_len = read(sock, &message[idx++], 1))
   {
      if(read_len == -1)
         error_handling("read() error!");

      str_len += read_len;
   }

   printf("Message from server : %s \n", message);
   printf("Function read call count : %d \n", str_len);
   close(sock);
   return 0;
}

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

 

tcp_client.c 코드에서 대략 중요한 코드는 다음과 같다.

sock=socket(PF_INET, SOCK_STREAM, 0);

TCP 소켓을 생성하고 있다. 첫번째 인자와 두번째 인자로 각각  PF_INET, SOCK_STREAM가 전달되면 세번째 인자는 IPPROTO_TCP는 생략가능하다.

 

 

while(read_len = read(sock, &message[idx++], 1))
{
   if(read_len == -1)
      error_handling("read() error!");

   str_len += read_len;
}

while문에서 read 함수를 반복 호출하고 있다. 중요한 것은 이 함수가 호출될때마다 1바이트씩 데이터를 읽는다는 점이다. 그리고 read 함수가 0을 반환하면 이는 거짓을 의미하기 때문에 while문을 빠져나간다.

 

str_len += read_len;

이 문장이 실행횔때 변수 read_len에 저장되어 있는 값은 항상 1이다. 위의 while문에서 1바이트씩 데이터를 읽고 있기 때문이다. 결국 while문을 빠져나간 이후에 str_len에는 읽어들인 바이트 수가 저장된다.

 

위의 실행 결과를 통해 서버가 전송한 13바이트 짜리 데이터를 총 13회의 read 함수호출로 읽어 들였음을 알 수 있다.

이 코드에서 TCP 소켓의 데이터 전송특성을 기억하도록 하자.

반응형