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

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

프로토콜 = 컴퓨터 상호간의 대화에 필요한 통신규약

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// 성공시 fd, 실패시 -1 반환

domain : 생성되는 소켓이 사용할 프로토콜 체계(Protocol Family) 정보

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

type : 소켓의 데이터 전송방식

  • SOCK_STREAM : 연결지향형 소켓(TCP)
    • 데이터가 전송 도중에 소멸되지 않음
    • 전송 순서대로 데이터가 수신됨
    • 전송되는 데이터의 경계(Boundary)가 존재하지 않음 : 송수신 버퍼에 저장 - 버퍼의 상태를 확인해가며 송수신함
    • 반드시 1대 1로 연결됨
  • SOCK_DGRAM : 비 연결지향형 소켓(UDP)
    • 전송 순서에 관계없이 가장 빠른 전송을 지향
    • 데이터의 손실 및 손상 우려가 있음
    • 전송되는 데이터의 경계가 존재
    • 한번에 전송할 수 있는 데이터의 크기가 제한
    • 소켓 연결 개념이 존재하지 않음

protocol : 최종적으로 소켓이 사용하게될 프로토콜 정보

  • 인자로 0을 줄 경우 domaintype만으로도 원하는 소켓을 생성할 수 있음
  • 단, 하나의 프로토콜 체계 안에 데이터의 전송방식이 동일한 프로토콜이 둘 이상 존재하는 경우가 있기 때문에 protocol 인자를 사용


IPv4 프로토콜 체계의 연결지향형 데이터 전송 소켓 생성
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)

IPv4 프로토콜 체계의 비 연결지향형 데이터 전송 소켓 생성
int tcp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)

리눅스 기반 TCP 소켓 예시

// tcp_server.cpp는 hello_server.cpp와 동일
// tcp_client.cpp

#include <iostream>
#include <fstream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

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)
	{
		cout << "Usage : " << argv[0] << " <IP> <port>\n";
		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, (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;
	}

	cout << "Message from server : " << message << endl;
	cout << "Function read call count : " << str_len << endl;
	close(sock);
	return 0;
}

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

02-2 : 윈도우 기반에서 이해 및 확인하기

#include <winsock2.h>
SOCKET socket(int af, int type, int protocol);
// 성공시 SOCKET, 실패시 INVALID_SOCKET 반환

반환형인 SOCKET은 소켓의 핸들 값을 저장하기 위해 정의된 자료형으로 사실상은 정수임
오류발생시 반환하는 INVALID_SOCKET 역시 실제 값은 -1

윈도우 기반 TCP 소켓 예시

// tcp_server_win.cpp는 hello_server_win.cpp와 동일
// hello_client_win.cpp

#include <iostream>
#include <winsock2.h>

using namespace std;

void	ErrorHandling(char *message);

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

	char	message[30];
	int		strLen = 0;
	int		idx = 0, readLen = 0;

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

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

	hSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET)
		ErrorHandling("socket() error");
	
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = inet_addr(argv[1]);
	servAddr.sin_port = htons(atoi(argv[2]));

	if (::connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		error_handling("connect() error");

	while (readLen = recv(hSocket, &message[idx++], 1, 0))
	{
		if (readLen == -1)
			ErrorHandling("read() error!");
		
		strLen += readLen;
	}

	cout << "Message from server : " << message << endl;
	cout << "Function read call count : " << strLen << endl;

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

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


내용 확인문제

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

    프로토콜이란 컴퓨터 상호간의 대화에 필요한 통신규약이며, 데이터 송수신에서 프로토콜을 정의한다는 것은 데이터를 주고받는 방식을 규정한다는 의미임


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

    데이터가 전송 도중에 소멸되지 않음
    전송 순서대로 데이터가 수신됨
    전송되는 데이터의 경계가 존재하지 않음

  2. 다음 중 비 연결형 소켓의 특성에 해당하는 것을 모두 고르면?
    • 전송된 데이터는 손실될 수 있다. (O)
    • 데이터의 경계(Boundary)가 존재하지 않는다. (X)
    • 가장 빠른 전송을 목표로 한다. (O)
    • 한번에 전송할 수 있는 데이터의 크기가 제한되어있지 않다. (X)
    • 연결지향형 소켓과 달리 연결이라는 개념이 존재하지 않는다. (O)
  3. 다음 유형의 데이터 송수신에 적합한 타입의 소켓은 무엇인지 결정하고, 그러한 결정을 하게 된 이유를 설명해보자.
    • 서태지와 아이들의 실시간 라이브 방송 멀티미디어 데이터
      UDP, 실시간으로 데이터가 갱신되므로 데이터의 일부 손실에 민감하지 않으며 전송속도가 중요시됨
    • 철수가 압축한 텍스트 파일의 전송
      TCP, 압축파일은 데이터가 손상될경우 압축해제가 불가능함
    • 인터넷 뱅킹을 이용하는 고객과 은행 사이에서의 데이터 송수신
      TCP, 데이터의 손실에 민감하며 데이터의 전송 순서와 수신 순서가 달라져서는 안됨
  4. 데이터의 경계(Boundary)가 존재하지 않는 소켓은 어떠한 타입의 소켓인가? 그리고 이러한 소켓은 데이터를 수신할 때 무엇을 주의해야하는지 서술해보자.

    연결지향형 소켓(TCP)
    입출력 함수의 호출 횟수는 데이터의 송수신에 유의미한 영향을 주지 않고, 데이터의 손실이 없어 송신된 데이터와 수신된 데이터가 정확히 같아야 하기 때문에 송수신 데이터를 비교하여 정확한 전송이 이루어졌는지를 확인해야함

  5. tcp_server.ctcp_client.c에서는 서버가 한번의 write 함수호출을 통해서 전송한 문자열을 여러 차례의 read 함수호출을 통해서 읽어들였다. 그럼 이번에는 서버가 여러차례의 write 함수호출을 통해서 전송한 문자열을 클라이언트에서 한번의 read 함수호출을 통해서 읽어들이는 형태로 예제를 변경해보자. 단, 이를 위해서 클라이언트는 read 함수의 호출 시기를 다소 늦출 필요가 있다. 서버가 데이터를 모두 전송할 때까지 기다려야하기 때문이다. 그럼 이를 위해서 리눅스와 윈도우 양쪽 모두에서 다음 유형의 문장을 이용해서 read 또는 recv 함수의 호출시기를 늦추기로 하자.
     for (i = 0; i < 3000; i++)
         printf("Wait time %d \n", i);
    

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

     // q6_server.cpp
    
     #include <iostream>
     #include <fstream>
     #include <unistd.h>
     #include <arpa/inet.h>
     #include <sys/socket.h>
    
     using namespace std;
    
     void	error_handling(char *message);
    
     int		main(int argc, char *argv[])
     {
         int	serv_sock;
         int	clnt_sock;
    
         struct sockaddr_in	serv_addr;
         struct sockaddr_in	clnt_addr;
    
         socklen_t	clnt_addr_size;
         string		message = "Hello World!";
    
         if(argc != 2)
         {
             cout << "Usage : " << argv[0] << " <port>\n";
             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, (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, (sockaddr*)&clnt_addr, &clnt_addr_size);
         if (clnt_sock == -1)
             error_handling("accept() error");
    
         int	idx = 0, str_len = 0, write_len = 0;
         while ((write_len = write(clnt_sock, &((message.c_str())[idx++]), 1)))
         {
             if (write_len == -1)
                 error_handling("write() error!");
    
             str_len += write_len;
             if (str_len == message.length())
                 break;
         }
    
         close(clnt_sock);
         close(serv_sock);
         return 0;
     }
    
     void	error_handling(char *message)
     {
         fputs(message, stderr);
         fputc('\n', stderr);
         exit(1);
     }
    
     // q6_client.cpp
    
     #include <iostream>
     #include <fstream>
     #include <unistd.h>
     #include <arpa/inet.h>
     #include <sys/socket.h>
    
     using namespace std;
    
     void	error_handling(char *message);
    
     int		main(int argc, char *argv[])
     {
         int	sock;
    
         struct sockaddr_in	serv_addr;
    
         char	message[30];
         int		str_len;
    
         if(argc != 3)
         {
             cout << "Usage : " << argv[0] << " <IP> <port>\n";
             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, (sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
             error_handling("connect() error");
    
         for (int i = 0; i < 3000; i++)
             cout << "Wait time " << i << endl;
    
         str_len = read(sock, message, sizeof(message) - 1);
         if (str_len == -1)
             error_handling("read() error");
    
         cout << "Message from server : " << message << endl;
         close(sock);
         return 0;
     }
    
     void	error_handling(char *message)
     {
         fputs(message, stderr);
         fputc('\n', stderr);
         exit(1);
     }