서버/네트워크

3. UDP 서버

광란의슈가슈가룬 2024. 8. 30. 12:12

TCP 서버를 알아보았으니 이제 코드를 수정해보면서 UDP 서버에 대해 알아보자. 일반적으로 TCP는 안정성이 높고 패킷의 도착순서를 보장하는 반면 UDP는 그렇지 못하지만 속도가 빠르고 바운더리(패킷과 패킷 사이의 경계)가 존재한다는 특징이 있다. 두 가지 전부 장단점이 있으므로 필요에 따라 적절한 프로토콜을 사용하면 된다. 채팅이나 아이템 거래 등과 같이 데이터가 유실되면 문제가 생기거나 패킷 수신의 순서가 중요한 경우 TCP를 사용하면 될 것이고, FPS와 같은 실시간 통신 게임이나 스트리밍 서비스와 같이 빠른 반응이 필요할 경우 UDP를 사용하면 될 것이다.

 

DummyClient.cpp

#include "pch.h"
#include <iostream>

#include <WinSock2.h>
#include <MSWSock.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

void HandleError(const char* cause)
{
	int32 errCode = ::WSAGetLastError();
	cout << cause << " ErrorCode : " << errCode << endl;
}

int main()
{
	// 윈속 라이브러리 초기화 (ws2_32 라이브러리 초기화)
	// 관련 정보가 wsaData에 채워짐
	WSAData wsaData;
	if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		return 0;

	// af : Address Family (AF_INET = IPv4, AF_INET6 = IPv6)
	// type : TCP(SOCK_STREAM) vs UDP(SOCK_DGRAM)
	// protocol : 0 (알아서 골라줌)
	// return : descriptor
	SOCKET clientSocket = ::socket(AF_INET, SOCK_DGRAM, 0);
	if (clientSocket == INVALID_SOCKET) // 실패
	{
		HandleError("Socket");
		return 0;
	}

	// 연결할 목적지 (IP주소 + Port)
	SOCKADDR_IN serverAddr; // IPv4
	::memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	//serverAddr.sin_addr.s_addr = ::inet_addr("127.0.0.1");
	::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
	serverAddr.sin_port = ::htons(7777); // 엔디안 이슈, 네트워크는 주로 빅 엔디안

	//----------------------------------

	while (true)
	{
		// TODO
		char sendBuffer[100] = "Hello World!";

		int32 resultCode = ::sendto(clientSocket, sendBuffer, sizeof(sendBuffer), 0, 
			(SOCKADDR*)&serverAddr, sizeof(serverAddr));

		if (resultCode == SOCKET_ERROR)
		{
			HandleError("SendTo");
			return 0;
		}

		cout << "Send Data! Len : " << sizeof(sendBuffer) << endl;

		SOCKADDR_IN recvAddr;
		::memset(&recvAddr, 0, sizeof(recvAddr));
		int32 addrLen = sizeof(recvAddr);

		char recvBuffer[1000];

		int32 recvLen = ::recvfrom(clientSocket, recvBuffer, sizeof(recvBuffer), 0, 
			(SOCKADDR*)&recvAddr, &addrLen);
		if (recvLen <= 0)
		{
			HandleError("RecvFrom");
			return 0;
		}
		cout << "Recv Data! Data : " << recvBuffer << endl;
		cout << "Recv Data! Len : " << recvLen << endl;

		this_thread::sleep_for(1s);
	}


	//----------------------------------

	// 소켓 리소스 반환
	::closesocket(clientSocket);

	// 윈속 종료, WSAStartup이 호출된 수만큼 실행해줘야 함
	::WSACleanup();
}

Server.cpp

#include "pch.h"
#include "CorePch.h"
#include <Windows.h>
#include "ThreadManager.h"

#include <WinSock2.h>
#include <MSWSock.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

void HandleError(const char* cause)
{
	int32 errCode = ::WSAGetLastError();
	cout << cause << " ErrorCode : " << errCode << endl;
}

int main()
{
	// 윈속 라이브러리 초기화 (ws2_32 라이브러리 초기화)
	// 관련 정보가 wsaData에 채워짐
	WSAData wsaData;
	if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		return 0;

	// UDP
	SOCKET serverSocket = ::socket(AF_INET, SOCK_DGRAM, 0);
	if (serverSocket == INVALID_SOCKET)
	{
		HandleError("Socket");
		return 0;
	}

	// 연결할 목적지 (IP주소 + Port)
	SOCKADDR_IN serverAddr; // IPv4
	::memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);
	serverAddr.sin_port = ::htons(7777); // 엔디안 이슈, 네트워크는 주로 빅 엔디안

	if (::bind(serverSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
	{
		HandleError("Bind");
		return 0;
	}

	while (true)
	{
		SOCKADDR_IN clientAddr;
		::memset(&clientAddr, 0, sizeof(clientAddr));
		int32 addrLen = sizeof(clientAddr);

		char recvBuffer[1000];

		int32 recvLen = ::recvfrom(serverSocket, recvBuffer, sizeof(recvBuffer), 0,
			(SOCKADDR*)&clientAddr, &addrLen);

		if (recvLen <= 0)
		{
			HandleError("RecvFrom");
			return 0;
		}

		cout << "Recv Data! Data : " << recvBuffer << endl;
		cout << "Recv Data! Len : " << recvLen << endl;

		int32 errorCode = ::sendto(serverSocket, recvBuffer, recvLen, 0, 
			(SOCKADDR*)&clientAddr, sizeof(clientAddr));

		if (errorCode == SOCKET_ERROR)
		{
			HandleError("SendTo");
			return 0;
		}

		cout << "Send Data! Len : " << recvLen << endl;
	}

	// 윈속 종료, WSAStartup이 호출된 수만큼 실행해줘야 함
	::WSACleanup();
}

UDP 통신으로 수정한 모습이다. 에러코드를 출력하는 함수를 따로 하나 만들어 관리해주었다. 코드를 잘 보면 TCP와는 다르게 클라이언트 소켓을 만들어 연결해준다는 느낌이 없다. 그냥 목적지에 데이터를 그냥 계속 보내는 느낌이다.

출력 화면

왼쪽이 클라이언트 오른쪽이 서버이다. 결과적으로 보이는 것은 TCP와 같아 보이지만 통신하는 과정이 아예 다르므로 잘 숙지하도록 하자.

 

TCP에서는 한 번에 10개의 데이터를 보내면 받는 쪽에서 데이터와 데이터 사이의 구분을 하지 못해 데이터의 크기가 1000으로 나오는 것을 볼 수 있었다. UDP의 특징 중 하나가 바운더리(경계)라고 했는데 그렇다면 크기가 100으로 나올까?

한 번 직접 확인해보도록 하자.

 

DummuClient.cpp

#include "pch.h"
#include <iostream>

#include <WinSock2.h>
#include <MSWSock.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

void HandleError(const char* cause)
{
	int32 errCode = ::WSAGetLastError();
	cout << cause << " ErrorCode : " << errCode << endl;
}

int main()
{
	// 윈속 라이브러리 초기화 (ws2_32 라이브러리 초기화)
	// 관련 정보가 wsaData에 채워짐
	WSAData wsaData;
	if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		return 0;

	// af : Address Family (AF_INET = IPv4, AF_INET6 = IPv6)
	// type : TCP(SOCK_STREAM) vs UDP(SOCK_DGRAM)
	// protocol : 0 (알아서 골라줌)
	// return : descriptor
	SOCKET clientSocket = ::socket(AF_INET, SOCK_DGRAM, 0);
	if (clientSocket == INVALID_SOCKET) // 실패
	{
		HandleError("Socket");
		return 0;
	}

	// 연결할 목적지 (IP주소 + Port)
	SOCKADDR_IN serverAddr; // IPv4
	::memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	//serverAddr.sin_addr.s_addr = ::inet_addr("127.0.0.1");
	::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
	serverAddr.sin_port = ::htons(7777); // 엔디안 이슈, 네트워크는 주로 빅 엔디안

	//----------------------------------

	while (true)
	{
		// TODO
		for (int32 i = 0; i < 10; i++)
		{
			char sendBuffer[100] = "Hello World!";

			int32 resultCode = ::sendto(clientSocket, sendBuffer, sizeof(sendBuffer), 0,
				(SOCKADDR*)&serverAddr, sizeof(serverAddr));

			if (resultCode == SOCKET_ERROR)
			{
				HandleError("SendTo");
				return 0;
			}

			cout << "Send Data! Len : " << sizeof(sendBuffer) << endl;
		}
		


		SOCKADDR_IN recvAddr;
		::memset(&recvAddr, 0, sizeof(recvAddr));
		int32 addrLen = sizeof(recvAddr);

		char recvBuffer[1000];

		int32 recvLen = ::recvfrom(clientSocket, recvBuffer, sizeof(recvBuffer), 0, 
			(SOCKADDR*)&recvAddr, &addrLen);
		if (recvLen <= 0)
		{
			HandleError("RecvFrom");
			return 0;
		}
		cout << "Recv Data! Data : " << recvBuffer << endl;
		cout << "Recv Data! Len : " << recvLen << endl;

		this_thread::sleep_for(1s);
	}


	//----------------------------------

	// 소켓 리소스 반환
	::closesocket(clientSocket);

	// 윈속 종료, WSAStartup이 호출된 수만큼 실행해줘야 함
	::WSACleanup();
}

Server.cpp

#include "pch.h"
#include "CorePch.h"
#include <Windows.h>
#include "ThreadManager.h"

#include <WinSock2.h>
#include <MSWSock.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

void HandleError(const char* cause)
{
	int32 errCode = ::WSAGetLastError();
	cout << cause << " ErrorCode : " << errCode << endl;
}

int main()
{
	// 윈속 라이브러리 초기화 (ws2_32 라이브러리 초기화)
	// 관련 정보가 wsaData에 채워짐
	WSAData wsaData;
	if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		return 0;

	// UDP
	SOCKET serverSocket = ::socket(AF_INET, SOCK_DGRAM, 0);
	if (serverSocket == INVALID_SOCKET)
	{
		HandleError("Socket");
		return 0;
	}

	// 연결할 목적지 (IP주소 + Port)
	SOCKADDR_IN serverAddr; // IPv4
	::memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);
	serverAddr.sin_port = ::htons(7777); // 엔디안 이슈, 네트워크는 주로 빅 엔디안

	if (::bind(serverSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
	{
		HandleError("Bind");
		return 0;
	}

	while (true)
	{
		SOCKADDR_IN clientAddr;
		::memset(&clientAddr, 0, sizeof(clientAddr));
		int32 addrLen = sizeof(clientAddr);

		this_thread::sleep_for(1s);

		char recvBuffer[1000];

		int32 recvLen = ::recvfrom(serverSocket, recvBuffer, sizeof(recvBuffer), 0,
			(SOCKADDR*)&clientAddr, &addrLen);

		if (recvLen <= 0)
		{
			HandleError("RecvFrom");
			return 0;
		}

		cout << "Recv Data! Data : " << recvBuffer << endl;
		cout << "Recv Data! Len : " << recvLen << endl;

		int32 errorCode = ::sendto(serverSocket, recvBuffer, recvLen, 0, 
			(SOCKADDR*)&clientAddr, sizeof(clientAddr));

		if (errorCode == SOCKET_ERROR)
		{
			HandleError("SendTo");
			return 0;
		}

		cout << "Send Data! Len : " << recvLen << endl;
	}

	// 윈속 종료, WSAStartup이 호출된 수만큼 실행해줘야 함
	::WSACleanup();
}

TCP에서 실험할 때와 마찬가지로 10개의 데이터를 보내고 서버에서는 1초 대기 후 받는 것으로 하였다.

 

출력 결과

결과를 확인해보면 받는 데이터의 사이즈가 100인 것을 알 수 있다. 도착하는 순서는 바뀔지언정 데이터끼리의 구분은 확실하게 된다.

 

UDP에서 sendto를 보면 데이터를 보낼 때마다 매번 도착지의 주소를 입력해주고 있다. 매번 이렇게 정보를 입력하는게 비효율적이라고 생각할 수가 있다. 그래서 즐겨찾기처럼 처음에 도착지에 대한 정보를 등록한 후 알아서 보내도록 하는 버전이 따로 있다. Connected UDP라고 부르는데 Connected 라는 단어를 보고 TCP처럼 직접적인 연결 통로가 생긴다고 오해하지 말자. 내부적으로는 UDP와 똑같지만 소켓에 목적지의 정보가 연결되어 따로 매번 입력해주지 않아도 된다는 것으로 받아들이자.

 

DummyClient.cpp

#include "pch.h"
#include <iostream>

#include <WinSock2.h>
#include <MSWSock.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

void HandleError(const char* cause)
{
	int32 errCode = ::WSAGetLastError();
	cout << cause << " ErrorCode : " << errCode << endl;
}

int main()
{
	// 윈속 라이브러리 초기화 (ws2_32 라이브러리 초기화)
	// 관련 정보가 wsaData에 채워짐
	WSAData wsaData;
	if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		return 0;

	// af : Address Family (AF_INET = IPv4, AF_INET6 = IPv6)
	// type : TCP(SOCK_STREAM) vs UDP(SOCK_DGRAM)
	// protocol : 0 (알아서 골라줌)
	// return : descriptor
	SOCKET clientSocket = ::socket(AF_INET, SOCK_DGRAM, 0);
	if (clientSocket == INVALID_SOCKET) // 실패
	{
		HandleError("Socket");
		return 0;
	}

	// 연결할 목적지 (IP주소 + Port)
	SOCKADDR_IN serverAddr; // IPv4
	::memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	//serverAddr.sin_addr.s_addr = ::inet_addr("127.0.0.1");
	::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
	serverAddr.sin_port = ::htons(7777); // 엔디안 이슈, 네트워크는 주로 빅 엔디안

	// Connected UDP
	::connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));

	//----------------------------------

	while (true)
	{
		// TODO
		char sendBuffer[100] = "Hello World!";

		// Unconnected UDP
		/*int32 resultCode = ::sendto(clientSocket, sendBuffer, sizeof(sendBuffer), 0,
			(SOCKADDR*)&serverAddr, sizeof(serverAddr));*/

		// Connected UDP
		int32 resultCode = ::send(clientSocket, sendBuffer, sizeof(sendBuffer), 0);

		if (resultCode == SOCKET_ERROR)
		{
			HandleError("SendTo");
			return 0;
		}

		cout << "Send Data! Len : " << sizeof(sendBuffer) << endl;
		
		


		SOCKADDR_IN recvAddr;
		::memset(&recvAddr, 0, sizeof(recvAddr));
		int32 addrLen = sizeof(recvAddr);

		char recvBuffer[1000];

		// Unconnected UDP
		/*int32 recvLen = ::recvfrom(clientSocket, recvBuffer, sizeof(recvBuffer), 0, 
			(SOCKADDR*)&recvAddr, &addrLen);*/

		// Connected UDP
		int32 recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);

		if (recvLen <= 0)
		{
			HandleError("RecvFrom");
			return 0;
		}
		cout << "Recv Data! Data : " << recvBuffer << endl;
		cout << "Recv Data! Len : " << recvLen << endl;

		this_thread::sleep_for(1s);
	}


	//----------------------------------

	// 소켓 리소스 반환
	::closesocket(clientSocket);

	// 윈속 종료, WSAStartup이 호출된 수만큼 실행해줘야 함
	::WSACleanup();
}

먼저 connect라는 함수를 이용해 소켓에 도착지의 주소를 연결해준 후 TCP에서 데이터를 보낼 때처럼 send, recv를 사용하면 된다. 서버와 오래 통신을 해야되는 경우에는 Connected UDP 방식을 이용하는 것이 더욱 효과적일 수 있다.

'서버 > 네트워크' 카테고리의 다른 글

6. Select 모델  (0) 2024.09.05
5. 논블로킹(Non-Blocking) 소켓  (2) 2024.09.03
4. 소켓 옵션  (3) 2024.09.01
2. TCP 서버  (0) 2024.08.29
1. 소켓 프로그래밍  (0) 2024.08.29