프로토콜이란?
멀리 떨어진 두 사람이 대화를 하기 위해서는 나름의 대화방법을 정해야 합니다. 한 사람은 전화로 하고 한 사람은 편지로 대화를 시도한다면 대화가 똑바로 이루어질 리가 없겠죠. 따라서 이 두 사람이 전화로 대화를 하기로 결정하였다면 이 두사람이 대화에 사용한 프로토콜은 전화라고 얘기할 수 있습니다. 이렇듯 프로토콜은 대화에 필요한 통신규약입니다. 컴퓨터의 관점에서 얘기하면 프로토콜은 컴퓨터 상호간의 대화에 필요한 통신규약이라고 할 수 있습니다.
소켓의 생성
앞서 포스팅한 글에서는 소켓과 관련된 함수를 사용하는 코드만 보고 넘어갔었는데, 각 함수에 대해 자세히 설명해보도록 하겠습니다. 아래 함수는 소켓을 생성하는 데 사용하는 함수이고, 전달해야 하는 인자는 다음과 같습니다.
- int domain: 소켓에 사용할 프로토콜 체계(Protocol Family) 전달
- int type: 소켓의 데이터 전송방식에 대한 정보 전달
- int protocol: 두 컴퓨터간 통신에 사용되는 프로토콜 정보 전달
// 성공 시 파일 디스크립터, 실패 시 -1 반환
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
프로토콜 체계(Protocol Family)
소켓이 통신에 사용하는 프로토콜에는 여러 종류들이 있습니다. 그리고 위에서 봤듯이 소켓을 생성할 때 첫 번째 인자로 소켓이 사용할 프로토콜의 부류정보를 전달해야 합니다. 이러한 부류정보를 가리켜서 프로토콜 체계라고 하며, 그 종류는 다음과 같습니다.
이름 | 프로토콜 체계(Protocol Family) |
PF_INET | IPv4 인터넷 프로토콜 체계 |
PF_INET6 | IPv6 인터넷 프로토콜 체계 |
PF_LOCAL | 로컬 통신을 위한 UNIX 프로토콜 체계 |
PF_PACKET | Low Level 소켓을 위한 프로토콜 체계 |
PF_IPX | IPX 노벨 프로토콜 체계 |
위 표에서 보이는 PF_INET에 해당하는 IPv4 인터넷 프로토콜 체계가 앞으로 포스트에서 주로 다루게 될 프로토콜 체계입니다. 이외에도 몇몇 프로토콜 체계들이 존재하지만, 중요도가 떨어지거나 보편화되지 않았으므로 주로 PF_INET 프로토콜 체계에 맞춰서 설명을 하겠습니다.
소켓의 타입
소켓의 타입이란 소켓의 데이터 전송방식을 뜻합니다. 이 정보를 socket 함수의 두번째 인자로 전달해야 합니다.
그런데 위에서 프로토콜 체계를 정해줬는데 데이터 전송방식을 또 정해줘야하나..싶을 수도 있는데, 프로토콜 체계가 정해졌다고 해서 데이터 전송방식까지 정해지는 것은 아닙니다. 주로 사용할 PF_INET 프로토콜 체계에도 둘 이상의 데이터 전송방식이 존재합니다. 대표적인 두 가지 데이터 전송방식을 살펴보겠습니다.
소켓의 타입 1: 연결지향형 소켓(SOCK_STREAM)
socket 함수의 두 번째 인자로 SOCK_STREAM을 전달하면 연결지향형 소켓이 만들어집니다. 연결지향형 소켓은 다음과 같은 특징들을 지닙니다.
- 소켓 대 소켓의 연결은 반드시 1대 1
- 중간에 데이터가 소멸되지 않고 목적지로 전송
- 전송 순서대로 데이터 수신
- 전송되는 데이터의 경계가 존재하지 않음
마지막 특징에 전송되는 데이터의 경계가 존재하지 않는다는 말이 무슨 말인지 이해가 안될 수도 있을 것 같아 부연설명을 하겠습니다. 데이터를 전송하는 컴퓨터가 데이터를 세 묶음을 보냈지만, 데이터를 수신하는 컴퓨터는 이를 한 묶음으로 처리할 수도 있다는 말입니다. 다시말하면, read 함수와 write 함수의 호출 횟수가 연결지향형 소켓에서는 큰 의미를 갖지 못한다고 말할 수 있습니다.
소켓의 타입2: 비 연결지향형 소켓(SOCK_DGRAM)
socket 함수의 두 번째 인자로 SOCK_DGRAM을 전달하면 비 연결지향형 소켓이 생성됩니다. 이 소켓은 다음과 같은 특징들을 지닙니다.
- 전송된 순서에 상관없이 가장 빠른 전송을 지향
- 전송된 데이터는 손실의 우려가 있고, 파손의 우려가 있음
- 전송되는 데이터의 경계가 존재
- 한 번에 전송할 수 있는 데이터의 크기가 제한
이번에는 연결지향형 소켓과는 다르게 전송되는 데이터의 경계가 존재합니다. 비 연결지향형 소켓에서는 데이터를 택배로 이해하면 편합니다. 택배상자가 두 개인데, 이를 세 번에 걸쳐서 나눠받을 수는 없습니다. 다시말하면, 비 연결지향형 소켓에서는 read 함수의 호출 횟수와 write함수의 호출 횟수가 같아야 합니다.
프로토콜의 최종선택
이제 socket 함수의 세 번째 인자에 대해 살펴볼 차례입니다. socket 함수의 첫 번째, 두 번째 인자를 통해서도 충분히 원하는 유형의 소켓을 만들 수 있습니다. 따라서 대부분의 경우 세 번째 인자로 그냥 0을 넘겨줘도 우리가 원하는 소켓을 생성할 수 있습니다. 하지만 하나의 프로토콜 체계 안에 데이터의 전송방식이 동일한 프로토콜이 둘 이상 존재하는 상황 때문에 세 번째 인자가 필요합니다.
예를 두 가지 들어보겠습니다.
IPv4 인터넷 프로토콜 체계(PF_INET)에서 동작하는 연결지향형 데이터 전송 소켓(SOCK_STREAM)을 만든다고 할 때, 이 두 가지 조건을 만족시키는 프로토콜은 IPPROTO_TCP 하나이기 때문에 다음과 같이 소켓 호출문을 구성할 수 있습니다. 그리고 이 때 생성되는 소켓을 가리켜 TCP 소켓이라고 합니다.
socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
그리고 IPv4 인터넷 프로토콜 체계(PF_INET)에서 동작하는 비 연결지향형 데이터 전송 소켓(SOCK_DGRAM)을 만든다고 할 때, 이 두 조건을 만족하는 프로토콜은 IPPROTO_UDP 하나이기 때문에 다음과 같이 소켓 호출문을 구성할 수 있습니다. 그리고 이 때 생성되는 소켓을 가리켜 UDP 소켓이라고 하겠습니다.
socket(PF_INET, SOCK_STREAM, IPPROTO_UDP);
연결지향형 소켓! TCP 소켓의 예
마지막으로 연결지향형 소켓인 TCP 소켓의 예를 살펴보고 이번 포스트를 마무리하겠습니다. 앞서 이 글에서 다뤘던 Hello world! 서버 프로그램을 그대로 사용하고, 클라이언트 소스를 조금 변경해서 TCP 소켓의 전송되는 데이터의 경계가 존재하지 않는 특성을 확인해보겠습니다.
/*
서버 프로그램으로는 hello_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 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]));
// connect 함수호출로 서버 프로그램에 연결 요청
if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error");
// hello_client와 달라진 부분.
// TCP 소켓의 특성을 확인하기 위하여 read를 여러번 호출해서 데이터를 수신
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);
}
결과는 아래와 같습니다.
'네트워크 프로그래밍 > C' 카테고리의 다른 글
[네트워크 프로그래밍/C] 네트워크 바이트 순서와 인터넷 주소 변환 (0) | 2022.02.21 |
---|---|
[네트워크 프로그래밍/C] 주소정보의 표현 (0) | 2022.02.17 |
[네트워크 프로그래밍/C] 소켓에 할당되는 IP주소와 PORT 번호 (0) | 2022.02.17 |
[네트워크 프로그래밍/C] 리눅스 기반 파일 조작하기 (0) | 2022.02.16 |
[네트워크 프로그래밍/C] 네트워크 프로그래밍과 소켓의 이해 (0) | 2022.02.16 |