나의 브을로오그으

#2. 소켓의 타입과 프로토콜의 설정 본문

네트워크/열혈 TCP_IP 소켓프로그래밍

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

__jhp_+ 2022. 7. 13. 22:30

#1. 소켓의 프로토콜과 그에 따른 데이터 전송 특성

 

프로토콜이란?

쉽게 말해 대화에 필요한 통신규약을 의미한다.

컴퓨터의 관점에서 보면 컴퓨터 상호간의 대화에 필요한 통신규약이다. 서로 데이터를 주고 받기 위해 정해놓은 약속을 의미한다.

 

 

소켓의 생성

#include <sys/socket.h>

int socket(int domain, int type, int protocol);
-> 성공 시 파일 디스크립터, 실패 시 -1 반환

domain : 소켓이 사용할 프로토콜 체계(Protocol Family) 정보 전달.
type : 소켓의 데이터 전송방식에 대한 정보 전달.
protocol : 두 컴퓨터간 통신에 사용되는 프로토콜 정보 전달.

소켓의 생성을 위해서는 매개변수에 대한 이해가 필수적이다.

 

프로토콜 체계(Protocol Family)

볶음밥도 여러가지 종류의 볶음밥이 있듯이 소켓의 통신 프로토콜 부류도 여러가지가 있다.

이름 프로토콜 체계(Protocol Family)
PF_INET IPv4 인터넷 프로토콜 체계
PF_INET6 IPv6 인터넷 프로토콜 체계
PF_LOCAL 로컬 통신을 위한 UNIX 프로토콜 체계
PF_PACKET Low Level 소켓을 위한 프로토콜 체계
PF_IPX IPX 노벨 프로토콜 체계

(sys/socket.h에 선언되어 있는 프로토콜 체계)

참고로 3번째 인자인 protocol은 1번째 인자로 정해진 Protocol Family 범위 내에서 결정된다.

socket(프로토콜 체계, 소켓의 데이터 전송 방식, 프로토콜 정보);

 

 

소켓의 타입(Type)

소켓의 전송방식을 의미하며, 프로토콜 체계를 결정하더라도 소켓의 데이터 전송을 따로 결정해야한다.

 

 

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

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

특징!

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

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

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

 

연결지향형(stream) 소켓을 장/단점은?

- 서로 독립된 전송 라인을 통해서 데이터를 송수신하기 때문에 라인상에 문제만 없으면 데이터가 소멸되지 않음을 보장받는다.

- 먼저 보내진 데이터보다 뒤에 보내진 데이터가 먼저 올수가 없다. 데이터가 전송한 순서대로 수신해야 하기 때문이다.

- 데이터의 경계가 존재하지 않는다는 무슨의미일까?

데이터가 송수신하는 소켓은 내부적으로 버퍼(buffer)가 있음. 전송한 데이터는 일단 무조건 데이터가 버퍼에 저장됨.

예를들어 write를 1회 했는데, read를 할 때 꼭 데이터를 한번에 read를 않해도되고, 여러번에 걸쳐서 read를 해도 된다.

즉, read, write자체가 어떤 의미를 갖지는 않는다. 따라서 이러한 특성 때문에 '연결지향형 소켓은 데이터의 경계가 존재하지 않는다.'고 말하는 것이다.

 

 

소켓에 존재하는 버퍼가 꽉 차면 데이터가 소멸될까?

연결지향형 소켓은 데이터 경계가 존재하지 않는다에 대한 설명에서 언급했듯이 수신되는 데이터의 저장을 위한, 바이트 배열로 이뤄진 버퍼가 소켓에 존재한다. 그렇다면 이 버퍼가, 수신되는 데이터로 꽉 채워지면 어떻게 될까? 그 이후의 전송 데이터는 소멸될까?

일단 이 버퍼에 수신된 데이터는, read 함수호출을 통해서 데이터가 읽혀지면 읽혀진 만큼 버퍼에서 비워지게 된다. 따라서 버퍼가 마냥 채워진 상태에 놓이진 않는다. 하지만 read 함수호출로 읽혀지는 데이터의 양보다 많은 양의 데이터가 수신되면 버퍼도 꽉 찰 수 있다. 그리고 이 상태에 놓인 소켓은 더 이상 데이터를 수신할 수 없다. 하지만 이 상황에 놓여도 전송되는 데이터는 소멸되는 일은 발생하지 않는다. 데이터를 전송하는 영역의 소켓이 더 이상 데이터를 전송하지 않기 때문이다.

즉, 지금 설명하는 연결지향형 소켓은 자신과 연결된 상대 소켓의 상태를 파악해가면서 데이터를 전송하다.  혹 데이터가 제대로 전송되지 않으면 데이터를 재전송하기까지 한다. 

따라서!!! 연결지향형 소켓의 데이터손실은 특별한 경우가 아니면 발생하지 않는다.

 

또 하나의 특징

소켓 to 소켓!

연결지향형 소켓은 소켓끼리 반드시 1대1로 연결되어있다.

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

(신뢰성 있는 데이터 전송이 목적)

 

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

socket 함수의 두 번째 인자로 SOCK_DGRAM을 전달하면 '비 연결지향형 소켓'이 생성된다. 그리고 이 방식은 속도가 굉장히 빠르다.

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

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

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

데이터를 나누어서 전송하면 당연히 각가 수신하게 된다. 데이터를 2번에 걸쳐서 전송했는데 수신측에서 5번에 걸쳐셔, 10번에 걸쳐서 수신하고 싶어도 수신 할 수 없다. 반드시 2번에 걸쳐서 수신하게 되는데 이를 두고 '전송되는 데이터의 경계가 존재한다'라고 말한다.

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

 

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

(고속의 데이터 전송이 목적)

 

 

프로토콜의 최종선택!

3번째 인자에 대해서 알아보자. 3번째 인자는 프로토콜 정보이다.

보통 3번째 인자에 0을 넣어도 1번째(프로토콜 패밀리), 2번째(소켓 데이터 전송 방식)만으로도 충분하다.

3번째 인자는 언제 필요할까?

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

즉, 소켓의 전송방식은 같은데 그 안에서 프로토콜이 나뉘는 상황이다.

예를 들어 IPv4 인터넷 프로토콜 체계 안에 연결지향형 전송방식이 동일한 프로토콜이 2개있다고 하면 이걸 구분해야 함.

 

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

socket(PF_INET, SOCKET_STREAM, IPPROTO_TCP);
-> 이 두 조건을 만족하는 프로토콜은 IPPROTO_TCP가 유일하다. 그리고 이것을 통해 생성되는 소켓을
"TCP 소켓"이라고 한다.

 

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

socket(PF_INET, SOCKET_DGRAM, IPPROTO_UDP);
이 두 조건을 만족하는 프로토콜은 IPPROTO_UDP 하나이다. 그리고 이렇게 생성되는 소켓을
"UDP 소켓"이라고 한다.

 

연결지향형 소켓! TCP 소켓의 예

UDP 소켓은 다음 챕터에서 알아보고 TCP 소켓에 대해서 먼저 알아보자.

TCP 소켓의 데이터 전송방식은 연결지향형 소켓 통신 방식이므로 read/write를 호출하는 횟수에 제한이 없다.

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

 

[Linux]

hello_server.c 코드는 동일함

 

[hello_client.c] / Linux

#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, IPPROTO_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]));

    if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error!");
        
    // 1바이트씩 읽는다. 연결지향형 통신 방식의 특징
    while(read_len = read(sock, &message[idx++], 1))
    {
        if (str_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);
    reutrn 0;
}

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

(사실 3번째 인자인 IPPROTO_TCP는 생략이 가능하다. 이유는 IP_INET체계에 SOCKET_STREAM을 만족하는 프로토콜은 유일하게 TCP프로토콜뿐이기 때문이다.)

 

윈도우 운영체제의 socket 함수

#include <WinSock2.h>
SOCKET socket(int af, int type, int protocol);
-> 성공 시 소켓 핸들, 실패 시 INVALID_SOCKET 반환

리눅스의 socket과 동일하지만, 반환타입만 다르다. SOCKET는 소켓의 핸들(양의 정수)을 타입 재정의한 값이다.

사실 오류발생 시 INVALID_SOCKET을 return하는데 이는 -1이다.

SOCKET soc = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (soc == -1)
    error_handling(...);
-> 이렇게 -1로 하면 Windows에서 INVALID_SOCKET을 따로 재정의한 이유가 없어진다.

SOCKET soc = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (soc == INVALID_SOCKET)
    error_handling(...);
-> 이렇게 작성하자.

 

 

Window

(server 코드는 동일함)

 

[tcp_client.c]

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")


void ErrorHandling(const char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	SOCKADDR_IN servAddr;

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

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() error!");
	}

	hSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (hSocket == INVALID_SOCKET)
	{
		ErrorHandling("socket() error!");
	}

	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.S_un.S_addr = inet_addr(argv[1]);
	servAddr.sin_port = htons(atoi(argv[2]));

	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	{
		ErrorHandling("connect() error!");
	}
	while (readLen = recv(hSocket, message, 1, 0))
	{
		if (readLen == -1)
		{
			ErrorHandling("recv() error!");
		}
		strLen += readLen;
	}
	printf("Message from server: %s \n", message);
	printf("Function read call count: %d \n", strLen);

	closesocket(hSocket);
	WSACleanup();
	return 0;
}

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

실행은 비주얼 스튜디오 -> Tools -> Command Line -> Developer Command Prompt를 열어서

CL 명령을 이용해 컴파일 후 실행

서버 실행

컴파일
cl tcp_server_win.c

// 실행
tcp_server_win 9190

 

클라이언트 실행

컴파일
cl tcp_client_win.c

// 실행
tcp_client_win 127.0.0.1 9190

 

클라이언트 실행 결과

 

 

[내용 확인문제]

01. 프로토콜이란 무엇을 의미하는가? 그리고 데이터의 송수신에 있어서 프로토콜을 정의한다는 것은 어떠한 의미가 있는가?

답)

프로토콜은 네트워크 상에 연결된 컴퓨터와 컴퓨터 간의 통신 규약을 의미한다.

프로토콜을 정의한다는 것은 컴퓨터 간에 통신규약을 정함으로써 모든 컴퓨터가 서로 통신을 할 때 지켜야 되는 약속이 되기 때문에 범용성적인 측면에서 통신이 원활해진다.

 

02. 연결지향형 소켓인 TCP 소켓의 전송 특성 세가지를 나열하여라.

답)

1. 전송측에서 데이터를 목적지로 보낼 때 전송 라인이 문제가 있지 않은 이상 데이터가 소멸되지 않는다.

2. 전송 순서대로 데이터를 수신받는다.

3. 전송에서 데이터 경계(Boundary)가 존재하지 않는다.. (전송 횟수와 수신 횟수가 어떠한 의미를 갖지 않는다.)

 

03. 다음 중 비 연결지향형 소켓의 특성에 해당하는 것을 모두 고르면?

a. 전송된 데이터는 손실될 수 있다.

b. 데이터의 경계(Boundary)가 존재하지 않는다.

c. 가장 빠른 전송을 목표로 한다.

d. 한번에 전송할 수 있는 데이터의 크기가 제한되어 있지 않다.

e. 연결지향형 소켓과 달리 연결이라는 개념이 존재하지 않는다.

답)

a, c

 

04. 다음 유형의 데이터 송수신에 적합한 타입의 소켓은 무엇인지 결정하고, 그러한 결정을 하게 된 이유를 설명해보자.

답)

a. 서태지와 아이들의 실시간 라이브 방송 멀티미디어 데이터 ( UDP 소켓 )

b. 철수가 압축한 텍스트 파일의 전송 ( TCP 소켓 )

c. 인터넷 뱅킹을 이용하는 고객과 은행 사이에서의 데이터 송수신 ( TCP 소켓 )

 

05. 데이터의 경계(Boundary)가 존재하지 않는 소켓은 어떠한 타입의 소켓인가? 그리고 이러한 소켓은 데이터를 수신할 때 무엇을 주의해야 하는지 서술해보자.

답)

TCP 소켓(연결지향형 데이터 송수신 방식 소켓) 이 소켓을 수신할 때는 데이터의 경계가 없기 때문에 read/write를 하는 횟수에 제한이 없다. 따라서 송신측의 write횟수와 크기에 따라 수신측에서 수신을 해주어야 한다. 만약 한번에 수신하려고 한다면, Busy Waiting하여(실행 흐름 지연) 전부 송신되었을 때 수신해야 한다.

 

06. tcp_server.c와 tcp_client.c에서는 서버가 한번의 write 함수호출을 통해서 전송한 문자열을 여러 차례의 read 함수호출을 통해서 읽어 들였다. 그럼 이번에는 서버가 여러차례의 write 함수호출을 통해서(횟수는 맘대로! 1바이트씩) 전송한 문자열을 클라이언트에서 한번의 read 함수호출을 통해서 읽어 들이는 형태로 예제를 변경해 보자. 단, 이를 위해서 클라이언트는 read 함수의 호출 시기를 다소 늦출 필요가 있다. 서버가 데이터를 모두 전송할 때까지 기다려야 하기 때문이다. 그럼 이를 위해서 리눅스와 윈도우 양쪽 모두에서 다음 유형의 문장을 이용해서 read 또는 recv 함수의 호출시기를 늦추기로 하자.

for(int i = 0; i < 3000; ++i) 
{
    printf("Wait time %d \n", i);
}

이렇게 CPU에게 불필요한 일을 시켜가면서 실행의 흐름을 지연시키는 것을 가리켜 "Busy Waiting"이라 하는데, 이를 적절히 활용하면 우리에게 필요한 만큼 함수의 호출시기를 늦출 수 있다.

 

[tcp_server_win_2.c]

/*
* 내용확인문제 06번
*/

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")


void ErrorHandling(const char* message);

int main(int argc, char* argv[]) {
	WSADATA wsaData;
	SOCKET hServSock, hClntSock;
	SOCKADDR_IN servAddr, clntAddr;

	int szClntAddr;
	char message[] = "Hello World!";
	int writeLen = 0, idx = 0;
	if (argc != 2)
	{
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() error!");
	}

	hServSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (hServSock == INVALID_SOCKET)
	{
		ErrorHandling("socket() error!");
	}

	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	servAddr.sin_port = htons(atoi(argv[1]));

	if (bind(hServSock, &servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	{
		ErrorHandling("bind() error!");
	}

	if (listen(hServSock, 5) == SOCKET_ERROR)
	{
		ErrorHandling("listen() error!");
	}

	szClntAddr = sizeof(clntAddr);
	hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);
	if (hClntSock == INVALID_SOCKET)
	{
		ErrorHandling("accept() error!");
	}

	while (idx != sizeof(message))
	{
		writeLen = send(hClntSock, &message[idx++], 1, 0);
		if (writeLen == SOCKET_ERROR) 
		{
			printf("idx = %d\n", idx);
			ErrorHandling("write() error!");
		}
	}

	closesocket(hServSock);
	closesocket(hClntSock);
	WSACleanup();
	return 0;
}

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

 

[tcp_client_win_2.c]

/*
* 내용확인문제 06번
*/

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")


void ErrorHandling(const char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	SOCKADDR_IN servAddr;

	char message[30] = { 0 };
	int strLen = 0;

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

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() error!");
	}

	hSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (hSocket == INVALID_SOCKET)
	{
		ErrorHandling("socket() error!");
	}

	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.S_un.S_addr = inet_addr(argv[1]);
	servAddr.sin_port = htons(atoi(argv[2]));

	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	{
		ErrorHandling("connect() error!");
	}

	// busy waiting
	for (int i = 0; i < 3000; ++i)
	{
		printf("Wait time %d \n", i);
	}
	strLen = recv(hSocket, message, sizeof(message), 0);
	if (strLen == SOCKET_ERROR)
	{
		ErrorHandling("recv() error!");
	}
	printf("Message from server: %s \n", message);
	printf("Function read call count: %d \n", strLen);

	closesocket(hSocket);
	WSACleanup();
	return 0;
}

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