나의 브을로오그으

#6. UDP 기반 서버/클라이언트 본문

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

#6. UDP 기반 서버/클라이언트

__jhp_+ 2022. 8. 2. 18:57

#1. UDP 소켓의 특성

UDP는 신뢰할 수 없는 전송방법을 제공한다. 그러나 SEQ, ACK와 같은 작업을 하는 일이 없기 때문에 좋은 성능을 발휘한다. 신뢰성보다 성능이 중요하다면 UDP가 더 나은 선택이 될 수 있다.

그렇다면 UDP의 역할은 어디까지일까? 앞서 TCP는 신뢰성 없는 IP를 기반으로 신뢰성 있는 데이터의 송수신을 위해서 '흐름제어(Flow Control)'를 한다고 설명했는데, 이 흐름제어가 UDP에는 존재하지 않는다.

TCP와 UDP의 가장 큰 차이점이 바로 이 흐름제어이며, TCP의 흐름제어를 빼면, 별로 남는게 없다.

 

호스트 A -> 호스트 B : 패킷 전달될 때 호스트 B로 잘 도착하도록 하는것이 IP의 역할인데, 이렇게 전달된 UDP 패킷을 호스트 B내에 존재하는 UDP 소켓 중 하나에게 최종적으로 전달하는 것은 IP의 역할이 아닌 UDP의 역할이다.

UDP의 역할 중 가장 중요한 것은 호스트로 수신된 패킷을 PORT 정보를 참조하여 최종 목적지인 UDP 소켓에 전달하는 것이다.

 

압축파일 처럼 10000개의 데이터 중 1개만 잘못되도 문제가 생기는 경우에는, TCP를 기반으로 송수신 해야하지만, 그렇지 않은경우에는 UDP가 더 나은선택이 될 수 있다. 실시간 동영상 스트리밍 서비스 같은? 만약 손실된 데이터가 있더라도, 아주 작은 잡음정도에 불과하다.

TCP가 UDP보다 항상 느린건 아니다.

- 데이터 송수신 이전, 이후에 거치는 연결설정(3-ways handshake) 및 해제과정

- 데이터 송수신 과정에서 거치는 신뢰성보장을 위한 흐름제어

위 2가지 사항이 TCP가 UDP보다 느리다고 말할 수 있는 근거이다.

 

 

#2. UDP 기반 서버/클라이언트의 구현

UDP서버, 클라이언트는 TCP와 같이 연결된 상태로 데이터를 송수신하지 않는다. TCP와 달리 연결 설정의 과정이 필요 없다. 따라서 TCP 서버 구현과정에서 거쳤던 listen 함수와 accept 함수의 호출불필요하다. UDP 소켓의 생성과 데이터의 송수신 과정만 존재할 뿐이다.

 

UDP에서는 서버건 클라이언트건 하나의 소켓만 있으면 된다.

TCP에서는 소켓과 소켓의 관계가 일대일 이었다. 때문에 서버에서 열 개의 클라이언트에게 서비스를 제공하려면 문지기의 역할을 하는 서버 소켓을 제외하고도 열 개의 소켓이 더 필요했다. 그러나 UDP는 서버건 클라이언트건 하나의 소켓만 있으면 된다. 앞서 UDP 소켓에 비유할 수 있다.  UDP소켓이 하나 있다면 어디건 데이터를 전송할 수 있다.

 

UDP 기반의 데이터 입출력 함수

TCP 소켓을 생성하고 나서 데이터를 전송하는 경우에는, 주소 정보를 따로 추가하는 과정이 필요 없다. 왜냐하면 TCP 소켓은 목적지에 해당하는 소켓과 연결된 상태이기 떄문이다. 즉, TCP소켓은 목적지의 주소를 이미 알고있는 상태이다.(클라이언트 소켓), 그러나 UDP 소켓은 연결상태를 유지하지 않으므로, 데이터를 전송할 때마다 반드시 목적지의 주소정보를 별도로 추가해야 한다.

 

[발신]

/* Linux */
#include <sys/socket.h>

ssize_t sendto(int sock, void* buff, size_t nbytes, int flags,
                                     struct sockaddr* to, socklen_t addrlen);
-> 성공 시 전송된 바이트 수, 실패 시 -1 반환

sock : 데이터 전송에 사용될 UDP 소켓의 파일 디스크립터를 인자로 전달.

buff : 전송할 데이터를 저장하고 있는 버퍼의 주소 값 전달.

nbytes : 전송할 데이터 크기를 바이트 단위로 전달.

flags : 옵션 지정에 사용되는 매개변수, 지정할 옵션이 없다면 0 전달.

to : 목적지 주소정보를 담고 있는 sockaddr 구조체 변수의 주소 값 전달.

addrlen : 매개변수 to로 전달된 주소 값의 구조체 변수 크기 전달.8

 

[수신]

#include <sys/socket.h>

ssize_t recvfrom(int sock, void* buff, size_t nbytes, int flags,
                                       struct sockaddr* from, socklen_t* addrlen);
-> 성공 시 수신한 바이트 수, 실패 시 -1 반환.

sock : 데이터 수신에 사용될 UDP 소켓의 파일 디스크립터를 인자로 전달.

buff : 데이터 수신에 사용될 버퍼의 주소 값 전달.

nbytes : 수신할 최대 바이트 수 전달, 때문에 매개변수 buff가 가리키는 버퍼의 크기를 넘을 수 없다.

flags : 옵션 지정에 사용되는 매개변수, 지정할 옵션이 없다면 0 전달.

from : 발신지 정보를 채워 넣을 sockaddr 구조체 변수의 주소 값 전달.

addrlen : 매개변수 from으로 전달된 주소에 해당하는 구조체 변수의 크기정보를 담고 있는 변수의 주소값 전달.

 

 

UDP 기반의 에코 서버와 에코 클라이언트

[uecho_server.c]

/* linux */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

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

int main(int argc, char* argv[])
{
	int serv_sock;
	char message[BUF_SIZE];
	int str_len;
	socklen_t clnt_adr_sz;

	struct sockaddr_in serv_adr, clnt_adr;
	if (argc != 2)
	{
		printf("Usage : %s <port> \n", argv[0]);
		exit(1);
	}

	serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
	if (serv_sock == -1)
	{
		error_handling("socket() error");
	}

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

	if (bind(serv_sock, (sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
	{
		error_handling("bind() error");
	}

	while (1)
	{
		clnt_adr_sz = sizeof(clnt_adr);
		str_len = recvfrom(serv_sock, message, BUF_SIZE, 0,
			(struct sockaddr*)&clnt_adr, &clnt_adr_sz);
		sendto(serv_sock, message, BUF_SIZE, 0,
			(struct sockaddr*)&clnt_adr, clnt_adr_sz);
	}
	close(serv_sock);
	return 0;
}

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

특징

1. UDP 소켓의 생성을 위해서 데이터 송수신 방식으로 SOCK_DGRAM을 인자로 전달한다.

2. recvfrom() 함수를 호출함으로써 서버 ip로 전달되는 모든 데이터를 수신한다.

3. revcfrom() 함수 호출을 통해 송신측 주소정보도 함께 얻는데, 이 주소를 이용해 역으로 송신도 가능하다.

4. 지금 무한루프를 빠져나오는 조건을 안주었기 때문에 사실상 무한히 동작한다.

 

 

[uecho_client.c]

/* linux */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

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

int main(int argc, char* argv[])
{
	int sock;
	char message[BUF_SIZE];
	int str_len;
	socklen_t adr_sz;

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

	sock = socket(PF_INET, SOCK_DGRAM, 0);
	if (sock == -1)
	{
		error_handling("socket() error");
	}

	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_familiy = AF_INET;
	serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
	serv_adr.sin_port = htons(atoi(argv[2]));

	while (1)
	{
		fputs("Insert message(q to Q): ", stdout);
		fgets(message, sizeof(message), stdin);
		if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
		{
			break;
		}

		sendto(sock, message, strlen(message), 0,
			(struct sockaddr*)&serv_adr, sizeof(serv_adr));
		adr_sz = sizeof(from_adr);
		str_len = recvfrom(sock, message, BUF_SIZE, 0,
			(struct sockaddr*)&from_adr, &adr_sz);
		message[str_len] = 0;
		printf("Message from server: %s \n", message);
	}
	close(sock);
	return 0;
}

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

특징

1. UDP 소켓 생성 후 데이터 송수신을 바로 함

2. 데이터를 서버로 송신하고, 서버에서 재전송한 데이터를 수신하고 있다.

 

Q. TCP 소켓 통신에서는 클라이언트가 connect() 함수를 호출하면, 클라이언트의 IP주소와 PORT번호가 자동 할당되어 연결요청을 보냈는데, UDP는 언제 할당할까?

A. 모든 소켓에는 IP 주소와 PORT번호가 할당되어야 한다. 다만, 직접 할당 하느냐, 간접 할당 하느냐의 차이일 뿐이다. 

일반적으로 sendto 함수를 호출 할 때 IP번호와 임의의 PORT번호가 할당된다.

 

잘 생각해보면 sendto함수를 호출해서 목적지로 데이터를 전송할 때 출발지의 IP와 PORT번호는 미리 할당이 되어있어야 한다.

따라서 bind()함수 호출을 통해서 주소정보를 할당 해도 되고(TCP, UDP 가리지 않고 호출 가능), 만약 sendto()함수 호출 전까지 할당되어 있지 않다면 자동 할당된다. 또한 한번 할당되면 계속 유지하기 때문에 다른 UDP 소켓과도 데이터를 주고받을 수 있다. 물론 IP 주소는 호스트 IP주소, PORT 번호는 할당되지 않은 PORT번호를 사용해야한다.

 

 

 

#3. UDP의 데이터 송수신 특성과 UDP에서의 connect 함수호출

데이터의 경계가 존재하는 UDP 소켓

TCP에서 데이터의 경계가 존재하지 않는다고 했는데, 이 말은 데이터 송수신 과정에서 호출하는 입출력함수의 횟수가 큰 의미를 지니지 않는다.는 의미이다. 즉, 100바이트 크기의 데이터를 1번에 보냈지만, read를 할 때 10바이트씩 10번을 읽든, 50바이트씩 2번을 읽든 입출력함수 호출 횟수 자체가 중요하지 않다는 의미이다.

 

반대로 UDP는 데이터의 경계가 존재하는 프로토콜이므로, 데이터 송수신 과정에서 호출하는 입출력함수의 호출횟수가 큰 의미를 지닌다. 그래서 송수신측에서 호출하는 입출력함수의 횟수가 정확히 일치해야 한다.

 

<Linux 버전>

[bound_host1.c]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

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

int main(int argc, char* argv[])
{
	int sock;
	char message[BUF_SIZE];
	struct sockaddr_in my_adr, your_adr;
	socklen_t adr_sz;
	int str_len, i;

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

	sock = socket(PF_INET, SOCK_DGRAM, 0);
	if (sock == -1)
		error_handling("socket() error");

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

	if (bind(sock, (struct sockaddr*)&my_adr, sizeof(my_adr)) == -1)
		error_handling("bind() error");

	for (i = 0; i < 3; ++i)
	{
		sleep(5);			// delay 5 sec
		adr_sz = sizeof(your_adr);
		str_len = recvfrom(sock, message, BUF_SIZE, 0,
			(struct sockaddr*)&your_adr, &adr_sz);
		printf("Message %d: %s \n", i + 1, message);
	}
	close(sock);
	return 0;
}

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

 

[bound_host2.c]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

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

int main(int argc, char* argv[])
{
	int sock;
	char msg1[] = "Hi!";
	char msg2[] = "I`m another UDP host!";
	char msg3[] = "Nice to meet you";

	struct sockaddr_in your_adr;
	socklen_t your_adr_sz;
	if (argv != 3)
	{
		printf("Usage : %s <ip> <port> \n", argv[0]);
		exit(1);
	}

	sock = socket(PF_INET, SOCK_DGRAM, 0);
	if (sock == -1)
		error_handling("socket() error");

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

	sendto(sock, msg1, sizeof(msg1), 0,
		(struct sockaddr*)&your_adr, sizeof(your_adr));
	sendto(sock, msg2, sizeof(msg2), 0,
		(struct sockaddr*)&your_adr, sizeof(your_adr));
	sendto(sock, msg3, sizeof(msg3), 0,
		(struct sockaddr*)&your_adr, sizeof(your_adr));

	close(sock);
	return 0;
}

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

코드를 간단하게 설명하면 3번 보내고 3번 받는 코드이다.

그런데 recvfrom 함수의 주기가 5초마다 recv하기 때문에 전송 host에서 3회 연속으로 보내게 되면 이 데이터는 이미 bound_host1.c에 전송된 상태에 놓이게 된다. 만약 TCP였다면 이 데이터를 단한번의 입력함수로 수신이 가능하겠지만, UDP는 recvfrom의 3번 호출을 요구한다.

 

 

UDP 데이터그램(datagram)

UDP 소켓이 전송하는 패킷을 가리켜 데이터그램이라고 표현하기도 하는데, 사실 데이터그램도 패킷의 일종이다. 다만 TCP 패킷과 달리 데이터의 일부가 아닌, 그 자체가 하나의 데이터로 의미를 가질 때 데이터그램이라 표현할 뿐이다. 이는 UDP의 데이터 전송특성과 관계가 있다. UDP는 데이터의 경계가 존재하기 때문에 하나의 패킷이 하나의 데이터로 간주된다. 따라서 데이터그램이라고 표현하는 것이다.

 

 

conntected UDP 소켓, unconnected UDP 소켓

TCP 소켓에는 데이터를 전송할 목적지의 IP와 PORT번호를 등록하는 반면, UDP 소켓에는 데이터를 전송할 목적지의 IP와 PORT번호를 등록하지 않는다. 때문에 sendto 함수호출을 통한 데이터의 전송과정은 다음과 같이 크게 세 단계로 나눌 수 있다.

1단계. UDP 소켓에 목적지의 IP와 PORT번호를 등록

2단계. 데이터 전송

3단계. UDP 소켓에 등록된 목적지 정보 삭제

sendto 함수가 호출될 때마다 위의 과정을 반복한다.

그래서 하나의 UDP소켓으로 다양한 목적지로 데이터 전송이 가능하다. 그리고 이렇게 목적지 정보가 등록되어 있지 않은 소켓을 가리켜 unconnected UDP 소켓이라 한다. 반면, 목적지 정보가 등록된 소켓은 connected UDP 소켓이라 한다.

물론 UDP 소켓은 기본적으로 unconnected 소켓이다. 매번 데이터를 전송할 때마다 소켓을 등록해야 한다.

만약 하나의 호스트와 오랜 시간 데이터를 송수신해야 한다면, UDP 소켓을 connected 소켓으로 만드는 것이 효율적이다. 그러면 위의 3단계 과정이 1/3으로 줄어듦.

 

 

connected UDP 소켓 생성

sock = socket(PF_INET, SOCK_DGRAM, 0);
memset(&adr, 0, sizeof(adr));
adr.sin_family = AF_INET;
adr.sin_addr.s_addr = htonl(INADDR_ANY);
adr.sin_port = hotns(atoi(argv[1]));
connect(sock, (struct addrsock*)&adr, sizeof(adr));

connect함수를 호출했다고 해서 TCP소켓 연결 과정 처럼 연결설정 과정을 거치지 않는다. 소켓에 단지 IP주소와 PORT번호가 등록될 뿐이다. 이렇게 connect함수를 호출하여 소켓에 IP주소와 PORT번호를 등록했다면 sendto, recvfrom 함수를 호출뿐만아니라 read, write 함수 호출로도 송수신이 가능해진다.

 

[uecho_con_client.c]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

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

int main(int argc, char* argv[])
{
	int sock;
	char message[BUF_SIZE];
	struct sockaddr_in serv_adr;
	int str_len;

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

	sock = socket(PF_INET, SOCK_DGRAM, 0);
	if (sock == -1)
		error_handling("socket() error");

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

	connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr));

	while (1)
	{
		fputs("Insert message(q to Q): ", stdout);
		fgets(message, sizeof(message), stdin);
		if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
			break;
		/*
		sendto(sock, message, strlen(message), 0,
			(struct sockaddr*)&serv_adr, sizeof(serv_adr));
		*/
		write(sock, message, strlen(message));

		/*
		adr_sz=sizeof(from_adr);
		str_len = recvfrom(sock, message, BUF_SIZE, 0,
					(struct sockaddr*)&from_adr, &adr_sz);
		*/
		str_len = read(sock, message, sizeof(message) - 1);

		message[str_len] = 0;
		printf("Message from server: %s \n", message);
	}
	close(sock);
	return 0;
}

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

확실히 단일 소켓과 통신하게 된다면 connect함수를 호출해서 소켓에 ip주소와 port번호를 등록하는 것이 코드도 간결해지고 깔끔해진다.

 

 

#4. 윈도우 기반으로 구현하기

#include <WinSock2.h>
int sendto(SOCKET s, const char* buf, int len, int flags, const struct sockaddr* to, int tolen);
-> 성공 시 전송된 바이트 수, 실패 시 SOCKET_ERROR 반환

int recvfrom(SOCKET s, char* buf, int len, int flags, const struct sockaddr* from, int fromlen);
-> 성공 시 수신된 바이트 수, 실패 시 SOCKET_ERROR 반환

 

[uecho_server_win.c]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winSock2.h>

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

#define BUF_SIZE 30
void ErrorHandling(const char* message);


int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET servSock;
	char message[BUF_SIZE];
	int strLen;
	int clntAdrSz;

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

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

	servSock = socket(PF_INET, SOCK_DGRAM, 0);
	if (servSock == INVALID_SOCKET)
	{
		ErrorHandling("socket() error");
	}

	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family = AF_INET;
	servAdr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	servAdr.sin_port = htons(atoi(argv[1]));
	if (bind(servSock, (PSOCKADDR)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
	{
		ErrorHandling("bind() error");
	}
	
	while (1)
	{
		clntAdrSz = sizeof(clntAdr);
		strLen = recvfrom(servSock, message, BUF_SIZE, 0, (PSOCKADDR)&clntAdr, &clntAdrSz);
		sendto(servSock, message, strLen, 0, (PSOCKADDR)&clntAdr, sizeof(clntAdr));
	}

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

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

 

[uecho_client_win.c]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winSock2.h>

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

#define BUF_SIZE 30
void ErrorHandling(const char* message);


int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET sock;
	char message[BUF_SIZE];
	int strLen;

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

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

	sock = socket(PF_INET, SOCK_DGRAM, 0);
	if (sock == INVALID_SOCKET)
	{
		ErrorHandling("socket() error");
	}

	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family = AF_INET;
	servAdr.sin_addr.S_un.S_addr = inet_addr(argv[1]);
	servAdr.sin_port = htons(atoi(argv[2]));
	connect(sock, (PSOCKADDR)&servAdr, sizeof(servAdr));
	
	while (1)
	{
		fputs("Insert message (q to quit): ", stdout);
		fgets(message, sizeof(message), stdin);
		if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
			break;

		send(sock, message, strlen(message), 0);
		strLen = recv(sock, message, sizeof(message) - 1, 0);
		message[strLen] = 0;
		printf("Message from server: %s\n", message);
	}

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

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

여기서도 connect() 함수를 호출함으로써 소켓에 ip주소와 port번호를 등록하였다.(임시x)

따라서 linux에서 read, write를 썼던것처럼 WIndow에서는 send, recv 사용이 가능하다.

 

 

내용 확인문제

01. TCP보다 UDP가 빠른 이유는 무엇인가? 그리고 TCP는 데이터의 전송을 신뢰할 수 있지만 UDP는 신뢰할 수 없는 이유는 또 무엇인가?

답)

TCP보다 빠른 이유는 UDP에는 TCP처럼 연결을 설정하는 3 ways-handshake, SEQ+ACK와 같은 것을 하지 않아 불필요한 작업 없이 그저 보내기만 하면 되므로 상당히 빠르다. 물론 데이터의 크기가 커질수록 성능 차이는 점점 좁혀진다.

TCP는 연결 후 데이터를 송신하게 되면 수신측에서 ACK에 송신 데이터를 전부 받았는지 받은 크기의 데이터를 수신 메시지에 실어서 보낸다. 만약 송신된 데이터 크기와 수신 데이터 크기가 다르다면 재전송을 하는 등 이러한 작업을 통해 데이터의 신뢰성을 확보 할 수 있다. 그러나 UDP는 송신 측이 데이터를 보내면 수신 측 잘 받았든 못받았든 상관 없이 그저 보내기만 하면 송신 작업이 끝나므로 일반적으로 데이터의 신뢰성을 보장 하지 않는다.

 

 

02. 다음 중 UDP의 특성이 아닌 것을 모두 고르면?

a. UDP는 TCP와 달리 연결의 개념이 존재하지 않는다. 따라서 반드시 TCP에서 보인 것처럼 1대 1의 형태로 데이터를 송수신하지 않을 수 있다.

b. UDP 기반으로 데이터를 전송할 목적지가 두 군데라면, 총 두 개의 소켓을 생성해야 한다.

c. UDP 소켓은 TCP 소켓이 할당한 동일한 번호의 PORT에 재할당이 불가능하다.

d. UDP 소켓과 TCP 소켓은 공존할 수 있다. 따라서 필요하다면 한 호스트상에서 TCP 방식과 UDP 방식의 데이터 송수신을 모두 진행할 수 있다.

e. UDP 소켓을 대상으로도 connect 함수를 호출할 수 있는데, 이러한 경우 UDP 소켓도 TCP 소켓과 마찬가지로 Three-way handshaking 과정을 거치게 된다.

답)

b. UDP기반 소켓은 목적지가 n개여도 목적지 IP 주소와 PORT번호만 있으면 송신이 가능하며, 1개의 소켓만 있어도 된다.

c. 재할당이 가능하다.

e. connect() 함수를 통해 소켓에 IP주소와 PORT 할당 시 TCP소켓처럼 Three-way handshaking과정을 거치지 않는다.

 

 

03. UDP 데이터그램이 최종 목적지인 상대 호스트의 UDP 소켓에 전달되는데 있어서, IP가 담당하는 부분과 UDP가 담당하는 부분을 구분 지어 설명해보자.

답)

IP는 호스트 -> 호스트로의 데이터 전송 까지만 담당한다. 이후 특정 프로세스로 데이터가 전달되기 위해서는 전송계층에 해당하는 UDP의 역할이 필요하면 수신 측 호스트에 도착한 UDP 데이터그래(패킷)을 PORT번호를 확인하여 수신 측 UDP 소켓의 입력 버퍼에 전달하는 역할을 UDP가 담당한다.

 

 

04. UDP는 일반적으로 TCP보다 빠르다. 그러나 송수신하는 데이터의 성격에 따라서 그 차이는 미미할 수 있고, 반대로 매우 클 수 있다. 그렇다면 어떠한 상황에서 UDP는 TCP보다 매우 좋은 성능을 보이는지 설명해보자.

답)

데이터 송수신 속도

- 일반적으로는 UDP가 TCP보다 매우 빠름

- 단, 송수신 데이터의 크기가 클수록 TCP와 UDP의 속도차가 줄어듦

 

UDP 사용

- UDP는 압축 파일 다운과 같은 곳에서 사용하기에는 부적합하지만 스트리밍 서비스와 같은 지속적인 데이터 전송이 필요한 곳에서는 유용하게 쓰일 수 있음.

 

UDP는 일반적으로 특정 상황을 만족할 때를 제외하고는 TCP보다 빠르다.

TCP는 데이터 신뢰성 보장, 잦은 연결, 연결 해제 과정이 존재하므로 UDP보다 태생적으로 빠를 수 없다.

그러므로 송수신하는 데이터의 크기가 작고(데이터가 유실될 확률이 줄어듦), 잦은 연결이 필요한 경우 UDP는 매우 효과적이다

 

 

05. 클라이언트의 TCP 소켓은 connect 함수를 호출할 때 자동으로 IP와 PORT가 할당된다. 그렇다면 bind 함수를 호출하지 않는 UDP 소켓은 언제 IP와 PORT가 할당되는가?

답)

sendto 함수를 호출 할 때 기본적으로 발신지 주소정보가 할당되어 있어야 한다. 만약 소켓에 발신지 IP주소와 PORT번호가 할당되지 않았다면, 자동으로 할당해준다. (이때 PORT번호는 사용하지 않는 PORT번호가 임의 할당 됨, 프로그램 종료 전까지 attach)

 

 

06. connect 함수의 호출문장은 TCP 클라이언트의 경우 반드시 삽입해야 하는 문장이다. 그러나 UDP의 경우는 선택적으로 삽입이 가능하다. 그렇다면 UDP에서 connect 함수를 호출하면 어떠한 이점이 있는가?

답)

매번 sendto, recvfrom 함수 호출시 마다 목적지 주소 정보를 할당, 도착 시 삭제 작업을 매번 해야한다.(unconnected UDP)

그러나 connect 함수를 호출하여 목적지 주소정보를 고정으로 할당하면, 주소정보 할당, 삭제 작업 없이 송수신이 가능하므로 훨씬 간결해지고, send, recv, write, read와 같은 함수 사용으로 송수신이 가능해진다.(connected UDP)

 

 

07. 본문에서 보인 예제 uecho_server.c와 uecho_client.c를 참고해서 서버와 클라이언트 상호간에 한번씩 메시지를 주고받는 형태로 대화를 진행하는 예제를 작성해보자. 단, 주고받는 대화는 콘솔상에 출력한다.

답)

 

[udp_server_win_6.c]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winSock2.h>

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

#define BUF_SIZE 30
void ErrorHandling(const char* message);


int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET servSock;
	char message[BUF_SIZE];
	int strLen;
	int clntAdrSz;

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

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

	servSock = socket(PF_INET, SOCK_DGRAM, 0);
	if (servSock == INVALID_SOCKET)
	{
		ErrorHandling("socket() error");
	}

	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family = AF_INET;
	servAdr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	servAdr.sin_port = htons(atoi(argv[1]));
	if (bind(servSock, (PSOCKADDR)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
	{
		ErrorHandling("bind() error");
	}

	while (1)
	{
		clntAdrSz = sizeof(clntAdr);
		strLen = recvfrom(servSock, message, BUF_SIZE, 0, (PSOCKADDR)&clntAdr, &clntAdrSz);
		message[strLen] = 0;
		printf("Message from client: %s", message);
		fputs("Insert message : ", stdout);
		fgets(message, sizeof(message), stdin);
		sendto(servSock, message, strLen, 0, (PSOCKADDR)&clntAdr, sizeof(clntAdr));
	}

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

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

 

[udp_client_win_6.c]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winSock2.h>

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

#define BUF_SIZE 30
void ErrorHandling(const char* message);


int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET sock;
	char message[BUF_SIZE];
	int strLen;

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

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

	sock = socket(PF_INET, SOCK_DGRAM, 0);
	if (sock == INVALID_SOCKET)
	{
		ErrorHandling("socket() error");
	}

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

	while (1)
	{
		fputs("Insert message (q to quit): ", stdout);
		fgets(message, sizeof(message), stdin);
		if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
			break;

		send(sock, message, strlen(message), 0);
		strLen = recv(sock, message, sizeof(message) - 1, 0);
		message[strLen] = 0;
		printf("Message from server: %s", message);
	}

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

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