Chapter 04 - TCP 기반 서버 / 클라이언트 1

04-1 : TCP와 UDP에 대한 이해

TCP(Transmission Control Protocol) : 연결을 지향하기 때문에 스트림 기반 소켓이라고도 부름
TCP는 ‘TCP/IP 프로토콜 스택’에 속해있음

  • APPLICATION 계층 - TCP / UDP 계층 - IP 계층 - LINK 계층으로 구분
  • 각각의 계층은 운영체제와 같은 소프트웨어 또는 NIC와 같은 물리적인 장치가 담당
  • 프로토콜 스택은 OSI 7계층으로 세분화되나, 프로그래머 관점에서는 4계층으로만 이해해도 충분함


LINK 계층

  • 물리적인 영역의 표준화에 대한 결과
  • LAN, WAN, MAN과 같은 네트워크 표준과 관련된 프로토콜을 정의

IP 계층

  • 데이터를 목적지로 전송하기 위한 경로 선택을 담당하는 계층
  • IP(Internet Protocol) 자체는 비 연결지향적이며 신뢰할 수 없는 프로토콜임
  • 즉, 데이터 전송시의 경로가 일정하지 않으며 오류로 인해 경로상 문제가 발생시 다른 경로를 선택하는 과정에서 데이터가 손실되는 문제가 발생할 수 있음

TCP / UDP 계층

  • IP계층에서 탐색한 경로를 바탕으로 실제 데이터를 송수신 하는 역할
  • 따라서 전송(Transport) 계층이라고 부름
  • TCP의 경우
    • IP는 하나의 데이터 패킷이 전송되는 과정에만 중심을 두고 설계되었으므로 여러 데이터 패킷 전송시의 순서 및 전송 그 자체를 신뢰할 수 없음
    • 따라서 TCP는 데이터를 주고받는 과정에서 데이터 확인 및 분실된 데이터 재전송을 통해 신뢰성을 보장함
  • UDP의 경우 IP의 상위계층에서 호스트 대 호스트의 데이터 송수신 방식을 약속한 것은 TCP와 같으나, 확인절차가 없어 신뢰성이 떨어짐
  • 전송 계층까지는 데이터 송수신 과정에서 사용되는 소켓을 통해 적용되므로 프로그래머가 직접 관여할 부분이 적음

APPLICATION 계층

  • 프로그램의 성격에 따라 정해진 클라이언트와 서버간의 데이터 송수신에 대한 규악
  • 대부분의 네트워크 프로그래밍은 APPLICATION 프로토콜의 설계 및 구현이 상당부분을 차지함

04-2 : TCP기반 서버, 클라이언트 구현

대부분의 TCP 서버 프로그램은 아래와 같은 순서로 구현됨
socket()[소켓 생성] -> bind()[소켓 주소할당] -> listen()[연결요청 대기] -> accept()[연결 허용] -> read()/write()[데이터 송수신] -> close()[연결 종료]

  • listen() 함수가 호출된 후에야 클라이언트에서 연결요청을 위한 connect() 함수를 호출할 수 있음
    • listen() 함수의 두번째 인자인 backlog가 연결요청 대기 큐의 크기를 뜻하며, 해당 크기에 따라 대기시킬 수 있는 클라이언트의 연결요청 수가 결정됨
  • accept() 함수로 연결요청 대기 큐에서 대기중이던 클라이언트의 연결요청을 수락
    • 호출 성공시 내부적으로 데이터 입출력에 사용할 소켓을 생성하고 해당 소켓의 파일 디스크립터를 반환
    • 생성된 소켓은 자동으로 연결요청을 한 클라이언트 소켓에 연결되어있음
    • 대기 큐가 비어있는 경우 클라이언트의 연결요청이 들어올 때까지 반환하지 않음


대부분의 TCP 클라이언트 프로그램은 아래와 같은 순서로 구현됨
socket()[소켓 생성] -> connect()[연결 요청] -> read()/write()[데이터 송수신] -> close()[연결 종료]

  • connect() 함수 호출으로 서버에 연결요청
    • 서버에 의해 연결요청이 접수되거나, 오류상황 발생으로 연결요청이 중단될 경우에만 함수 반환이 이루어짐
    • 이 때 연결요청의 접수는 accept() 함수 호출 여부에 관계없이 클라이언트의 연결 요청이 서버의 listen() 함수 호출 이후에 생긴 연결요청 대기 큐에 등록되었음을 의미
    • connect() 함수 호출시 자동으로 소켓에 IP와 포트번호가 할당되기 때문에 클라이언트 프로그램 구현시에는 bind() 함수의 명시적인 호출이 필요없음

04-3 : Iterative 기반의 서버, 클라이언트 구현

Iterative 서버 : accept() ~ close()를 반복하는 서버
Iterative 형태로 동작하는 에코 서버의 동작 방식

  • 서버는 한 순간에 하나의 클라이언트와 연결되어 에코 서비스를 제공
  • 클라이언트는 프로그램 사용자로부터 문자열 데이터를 입력받아 서버에 전송
  • 서버는 전송받은 문자열 데이터를 클라이언트에게 재전송(에코)

Iterative 에코 서버 예제

// echo_server.c

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

using namespace std;

#define BUF_SIZE 1024
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;
	char		message[BUF_SIZE];
	int			str_len, i;

	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);

	for (i = 0; i < 5; i++)
	{
		clnt_sock = accept(serv_sock, (sockaddr*)&clnt_addr, &clnt_addr_size);
		if (clnt_sock == -1)
			error_handling("accept() error");
		else
			cout << "Connected client " << i + 1 << endl;

		while ((str_len = read(clnt_sock, message, BUF_SIZE)) != 0)
			write(clnt_sock, message, str_len);

		close(clnt_sock);
	}

	close(serv_sock);
	return 0;
}

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

Iterative 에코 클라이언트 예제

// echo_client.cpp

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

using namespace std;

#define BUF_SIZE 1024
void	error_handling(char *message);

int		main(int argc, char *argv[])
{
	int	sock;

	struct sockaddr_in	serv_addr;

	char	message[BUF_SIZE];
	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");
	else
		cout << "Connected.........\n";

	while (1)
	{
		cout << "Input message(Q to quit) : ";
		cin.getline(message, BUF_SIZE);

		if (!strcmp(message, "q") || !strcmp(message, "Q"))
			break;

		write(sock, message, strlen(message));
		str_len = read(sock, message, BUF_SIZE - 1);
		message[str_len] = 0;
		cout << "Message from server : " << message << endl;
	}

	close(sock);
	return 0;
}

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

위 예제에서는 TCP 소켓임에도 불구하고 입출력 함수가 호출될 때마다 문자열 단위로 실제 입출력이 이루어진다고 가정했다는 오류가 존재함
이 경우 둘 이상의 write() 함수가 호출되어 문자열 데이터가 전달될경우 원하는 결과를 얻지 못할 수 있음


04-4 : 윈도우 기반으로 구현하기

윈도우 기반 Iterative 에코 서버 예제

// echo_server_win.cpp

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

using namespace std;

#define BUF_SIZE 1024
void	ErrorHandling(char* message);

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

	int		clntAddrSize;
	char	message[BUF_SIZE];
	int		strLen, i;

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

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error");
	
	hServSock = socket(PF_INET, SOCK_STREAM, 0);
	if (hServSock == INVALID_SOCKET)
		ErrorHandling("socket() error");

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

	if (::bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		ErrorHandling("bind() error");
	
	if (::listen(hServSock, 5) == SOCKET_ERROR)
		ErrorHandling("listen() error");
	
	clntAddrSize = sizeof(clntAddr);

	for (i = 0; i < 5; i++)
	{
		hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &clntAddrSize);
		if (hClntSock == INVALID_SOCKET)
			ErrorHandling("accept() error");
		else
			cout << "Connected client " << i + 1 << endl;

		while ((strLen = recv(hClntSock, message, BUF_SIZE, 0)) != 0)
			send(hClntSock, message, strLen, 0);

		closesocket(hClntSock);
	}

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

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

윈도우 기반 Iterative 에코 클라이언트 예제

// echo_client_win.cpp

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

using namespace std;

#define BUF_SIZE 1024
void	ErrorHandling(char *message);

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

	char	message[BUF_SIZE];
	int		strLen;

	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");
	else
		cout << "Connected..........\n";

	while (1)
	{
		cout << "Input message(Q to quit) : ";
		cin.getline(message, BUF_SIZE);

		if (!strcmp(message, "q") || !strcmp(message, "Q"))
			break;

		send(hSocket, message, strlen(message), 0);
		strLen = recv(hSocket, message, BUF_SIZE - 1, 0);
		message[strLen] = 0;
		cout << "Message from server : " << message << endl;
	}

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

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


내용 확인문제

  1. TCP/IP 프로토콜 스택을 4개의 계층으로 구분해보자. 그리고 TCP 소켓이 거치는 계층구조와 UDP 소켓이 거치는 계층구조의 차이점을 설명해보자.

    APPLICATION 계층 - TCP / UDP 계층 - IP 계층 - LINK 계층

  2. TCP/IP 프로토콜 스택 중에서 LINK 계층과 IP 계층이 담당하는 역할이 무엇인지 설명해보자. 그리고 이 둘의 관계도 함께 설명해보자.

    LINK 계층은 LAN, WAN, MAN과 같은 네트워크 표준과 관련된 프로토콜을 정의
    IP 계층은 데이터를 목적지로 전송하기 위한 경로를 선택
    IP 계층은 LINK 계층을 기반으로 구성되는 물리적인 네트워크를 기반으로 하는 데이터 전송의 표준을 담당

  3. TCP/IP 프로토콜 스택을 4개의 계층(또는 7개의 계층)으로 나누는 이유는 무엇인가? 이를 개방형 시스템에 대한 설명과 함께 답해보자.

    인터넷을 통한 효율적인 데이터의 송수신에 대한 문제를 영역별로 나누어 해결하기 위해 프로토콜이 세분화되었으며, 이들이 계층구조를 통해 상호간의 관계를 맺음
    계층화된 영역들은 개방형 시스템으로 표준화 작업이 이루어졌으며, 이로인해 표준에 따라 서로 다른 시스템들의 상호작용이 가능

  4. 클라이언트는 connect 함수호출을 통해서 서버로의 연결을 요청한다. 그렇다면 클라이언트는 서버가 어떠한 함수를 호출한 이후부터 connect 함수를 호출할 수 있는가?

    listen() 함수

  5. 연결요청 대기 큐라는 것이 생성되는 순간은 언제이며, 이것이 어떠한 역할을 하는지 설명해보자. 그리고 accept 함수와의 관계도 함께 설명해보자.

    서버에서 listen() 함수가 호출되면 연결요청 대기 큐가 생성되며, 클라이언트에서 connect() 함수를 호출하여 대기 큐에 연결요청을 쌓음
    서버에서는 accept() 함수를 호출하여 대기 큐에 쌓인 연결요청을 수락함

  6. 클라이언트 프로그램에서 소켓에 주소정보를 할당하는 bind 함수호출이 불필요한 이유는 무엇인가? 그리고 bind 함수를 호출하지 않았을 경우, 언제 어떠한 방식으로 IP주소와 포트번호가 할당되는가?

    클라이언트에서는 connect() 함수 호출시 자동으로 소켓에 IP와 포트번호가 할당되기 때문에 bind() 함수의 호출은 필요하지 않음

  7. Chapter 01에서 구현한 예제 hello_server_win.c와 hello_server_win.c를 Iterative 모델로 변경하고, 제대로 변경이 되었는지 클라이언트와 함께 테스트해보자.

     // hello_server_iterative_win.cpp
     // client는 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		hServSock, hClntSock;
         SOCKADDR_IN	servAddr, clntAddr;
    
         int		szClntAddr;
         char	message = "Hello World!";
    
         if (argc != 2)
         {
             cout << "Usage : " << argv[0] << " <port>\n";
             exit(1);
         }
    
         if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
             ErrorHandling("WSAStartup() error");
    		
         hServSock = socket(PF_INET, SOCK_STREAM, 0);
         if (hServSock == INVALID_SOCKET)
             ErrorHandling("socket() error");
    
         memset(&servAddr, 0, sizeof(servAddr));
         servAddr.sin_family = AF_INET;
         servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
         servAddr.sin_port = htons(atoi(argv[1]));
    
         if (::bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
             ErrorHandling("bind() error");
    		
         if (::listen(hServSock, 5) == SOCKET_ERROR)
             ErrorHandling("listen() error");
    		
         szClntAddr = sizeof(clntAddr);
    
         while (1)
         {
             hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);
             if (hClntSock == INVALID_SOCKET)
                 ErrorHandling("accept() error");
             else
                 cout << "Connected clinet " << i + 1 << endl;
    
             send(hClntSock, message, sizeof(message), 0);
    
             closesocket(hClntSock);
         }
    
         closesocket(hServSock);
         WSACleanup();
         return 0;
     }
    
     void	ErrorHandling(char *message)
     {
         fputs(message, stderr);
         fputc('\n', stderr);
         exit(1);
     }