Computer Science 기본 지식/소켓 프로그래밍

[TCP/IP 소켓 프로그래밍] (4) UDP 기반 서버 및 클라이언트

로파이 2021. 3. 28. 20:06

출처 : 열혈 TCP/IP 소켓 프로그래밍 윤성우 저

 

UDP 소켓

UDP 소켓은 목적지에 대한 주소 정보를 부여할 필요가 없다. 즉 connect()없이 데이터를 전송할 수 있다.

대신, UDP 패킷에 목적지 주소 정보를 담는 과정이 필요하게 되었다.

 

vs TCP 소켓

TCP 소켓의 경우 일대일 연결을 지향하기 때문에 클라이언트 쪽에서 연결하고자하는 소켓에 상대방의 주소와 포트번호를 부여하는 과정이 필요했다.

이는 흐름 제어와 같은 신뢰성 있는 전송을 위한 연결 확립이 필요했기 때문이다.

연결 확립 과정이 필요없기 때문에 서버 쪽에서 listen()을 통한 연결 대기 상태가 필요가 없다.

 

UDP 특징

TCP와 같이 연결 확립 과정이 필요없고 데이터 전송을 바로 시작할 수 있다. 또한 UDP 패킷에 주소를 부여하기 때문에 다양한 목적지 주소에 따라 다양한 UDP 패킷 한 소켓을 통해 보내기만 하면 된다. 이로써 일대다 통신이 가능하다.

 

TCP에서 신뢰성있는 전송을 위한 흐름제어/ACK를 통한 패킷 손실감지/연결 수립 기능이 없기 때문에 전송 속도면에서도 빠르다. UDP는 빠른 대신에 신뢰성있는 전송이 보장되지 않으므로 패킷 손실 가능성이 있다. 따라서 동영상 실시간 스트리밍과 같이 손실되어도 작은 잡음에도 용인가능한 데이터의 질이 크게 중요하지 않은 응용영역에서 사용가능하다.

 

UDP에는 보내고자하는 데이터가 한꺼번에 담기며 데이터 길이에 대한 정보를 헤더에 포함하고 있다. UDP 프로토콜을 사용하는 패킷을 UDP 데이터그램이라고도 한다.

 

UDP의 데이터 입출력 함수

int sendto(SOCKET sock, const char* buf, int len, int flags, const struct sockaddr* to, int tolen);
  • 성공 시 전송된 바이트 수, 실패시 SOCKET_ERROR
  • sock: UDP 소켓 핸들
  • buf: 전송할 메세지 버퍼
  • len: 전송할 메세지의 데이터 길이
  • flags: 옵션
  • to: 목적지 주소정보를 담고 있는 sockaddr 구조체의 포인터
  • tolen: sockaddr 구조체 길이
int recvfrom(SOCKET s, char* buf, int len, int flags, struct sockaddr* from, int* fromlen);

 

  • 성공 시 수신한 바이트 수, 실패 시 SOCKET_ERROR
  • sock: UDP 소켓 핸들
  • buf: 수신 메세지를 담을 버퍼
  • len: 수신할 데이터의 최대 길이
  • flags: 옵션
  • from: 출발지 주소 정보를 담고자 하는 sockaddr 구조체의 포인터
  • fromlen: sockaddr 구조체 길이로 초기화한 포인터

- sendto 함수 호출과 소켓 주소 할당 (bind/connect)

TCP 에코 서버를 UDP 버전으로 구현한 UDP 클라이어트의 한 부분을 보자.

// 1. 클라이언트 소켓 생성
SOCKET hSocket = socket(PF_INET, SOCK_DGRAM, 0);
if (hSocket == INVALID_SOCKET)
ErrorHandling("socket() error");

// 2. 목적지 IP 주소, Port 번호 할당
SOCKADDR_IN servAddr = {};
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr(argv[1]); // 문자열을 4바이트 주소 정보로 변환
servAddr.sin_port = htons(atoi(argv[2])); // 포트 번호로 변환

//if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
//	ErrorHandling("connect() error");
//else
//	puts("Connected......");

// 메세지 전송 부분
int szfromAddr = 0;
SOCKADDR_IN fromAddr = {};
char message[BUF_SIZE] = {};
while (1)
{
	fputs("Input mesage(Q to quit): ", stdout);
	fgets(message, BUF_SIZE, stdin);

	if (strcmp(message, "q\n") == 0 || strcmp(message, "Q\n") == 0)
		break;

	// UDP 메세지 전송
	sendto(hSocket, message, strlen(message), 0, (SOCKADDR*)&servAddr, sizeof(servAddr));

	// UDP 메세지 수신
	int strLen = recvfrom(hSocket, message, BUF_SIZE, 0, (SOCKADDR*)&fromAddr, &szfromAddr);
	message[strLen] = 0;
	printf("Message from server: %s\n", message);
}
// 소켓 종료
closesocket(hSocket);

 

윈속 라이브러리 초기화와 종료 부분을 제외하고 다음 부분을 볼 수 있는데,

1. 클라이언트 소켓을 생성

2. 목적지 IP 주소와 Port 번호를 할당

3. 메세지 전송

 

- 목적지 주소 부여

connect() 함수를 통해 목적지 IP 주소와 포트 번호를 소켓에 할당하지 않아도 UDP 전송을 할 수 있는 것이 특징이다. 이와 같이 소켓에 주소 정보를 할당하지 않은 UDP 소켓을 unconnected UDP 소켓이라 한다. Unconnected UDP는 sendto 함수 호출시 목적지 IP와 포트번호를 등록한다.

 

- 출발지 주소 부여

또한, 클라이언트의 경우 출발지 IP 주소 및 포트부여를 bind()를 통해 하는 것을 생략할 수 있는데, TCP의 경우 connect를 호출하면서 호스트 주소와 비어있는 포트번호를 자동할당해주는데, UDP는 connect까지 생략가능하기 때문에 실제로 sendto 함수가 호출될 때 자신(소켓)에 대한 IP 주소와 포트번호가 자동 할당된다.

 

- Connected UDP

Unconnected UDP 소켓의 경우 목적지 주소에 대해 sendto를 호출할 때마다 목적지 IP 주소와 포트번호를 할당하고 데이터 전송이 끝나면서 해당 정보를 삭제한다. 이러한 오버헤드를 없애기 위해 TCP의 경우처럼 소켓에 목적지 주소를 connect를 통해 할당할 수 있다. TCP와 같이 연결 수립과정이 있는 것은 아니다. 이러한 Connected UDP 소켓은 목적지 주소 정보가 할당되었기 때문에, sendto와 recvfrom를 대신하여 send와 recv를 사용할 수 있다.

 

- Connected UDP 버전

// 2. 목적지 IP 주소, Port 번호 할당
SOCKADDR_IN servAddr = {};
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr(argv[1]); // 문자열을 4바이트 주소 정보로 변환
servAddr.sin_port = htons(atoi(argv[2])); // 포트 번호로 변환

if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	ErrorHandling("connect() error");
else
	puts("Connected......");

// 메세지 전송 부분
int szfromAddr = 0;
SOCKADDR_IN fromAddr = {};
char message[BUF_SIZE] = {};
while (1)
{
	fputs("Input mesage(Q to quit): ", stdout);
	fgets(message, BUF_SIZE, stdin);

	if (strcmp(message, "q\n") == 0 || strcmp(message, "Q\n") == 0)
		break;

	send(hSocket, message, strlen(message), 0);
	int strLen = recv(hSocket, message, BUF_SIZE-1, 0);
	message[strLen] = 0;

	//// UDP 메세지 전송
	//sendto(hSocket, message, strlen(message), 0, (SOCKADDR*)&servAddr, sizeof(servAddr));

	//// UDP 메세지 수신
	//int strLen = recvfrom(hSocket, message, BUF_SIZE, 0, (SOCKADDR*)&fromAddr, &szfromAddr);
	//message[strLen] = 0;
	printf("Message from server: %s\n", message);
}
// 소켓 종료
closesocket(hSocket);

 

- 데이터 경계가 존재하는 UDP 패킷

보내고자하는 데이터를 잘 분리하여 보내는 TCP와 달리 UDP는 한 번에 전송할 데이터를 담아 그 길이를 헤더에 기록한다. 수신자는 송신자가 보내는 시점에서 항상 연결가능한 상태에 있을 때, 데이터를 완전히 받을 수 있다. 실시간 동영상 스트림에서 연결되는 순간의 시점부터 영상을 볼 수 있는 것과 같은 원리이다.

 

UDP 에코 서버-클라이언트 예제

echo_server_win.cpp

// 1. 서버 소켓 생성
SOCKET hServSock = socket(PF_INET, SOCK_DGRAM, 0);
if (hServSock == INVALID_SOCKET)
	ErrorHandling("socket() error");

// 2. IP 주소, Port 번호 할당
SOCKADDR_IN servAddr = {};
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 단일 IP 주소로 모든 범위의 IP 주소 수신
servAddr.sin_port = htons(atoi(argv[1]));
if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	ErrorHandling("bind() error");

// 3. 클라이언트 정보 수신
char message[BUF_SIZE] = { 0 }; // 메세지 수신 버퍼
SOCKADDR_IN clntAddr = {}; // 클라이언트 주소를 담을 구조체
int szClntAddr = sizeof(clntAddr), strLen = 0;
while(1)
{
        // UDP 메세지 수신
        int strLen = recvfrom(hServSock, message, BUF_SIZE, 0, (SOCKADDR*)&clntAddr, &szClntAddr);
        if (strLen > 0)
        {
        	message[strLen] = 0;
        	printf("Echo : %s\n", message);
        }
	//// UDP 메세지 전송
	sendto(hServSock, message, strLen, 0, (SOCKADDR*)&clntAddr, sizeof(clntAddr));
}

echo_client_win.cpp

위에서 connected UDP의 예시와 같다.

 

참고: 윤성우 저 TCP/IP 소켓 프로그래밍