C++ UDP 서버 실습

정은성·2023년 5월 29일
3
post-thumbnail

※ Rookiss님의 [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 강의를 보고 정리한 글입니다.

먼저 UDP란?

비연결형, 신뢰성 없는 프로토콜이다.

바로 에코서버를 구현해보자.

먼저 구현 전 에러 처리가 반복되는 것 같으니 따로 함수로 만들어준다.

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

소켓 생성

TCP서버에선 서버에서 소켓이 2개가 필요했다. 바로 Listening소켓과 Client소켓이다. 하지만 UDP에선 연결을 필요로하지 않기 때문에 소켓 하나로 충분하다.

소켓 생성은 type만 STREAM이 아닌 DGRAM으로 바꿔주면 된다.

SOCKET serverSocket = ::socket(AF_INET, SOCK_DGRAM, 0);

if (serverSocket == INVALID_SOCKET) {
	HandleError("Socket");
	return 0;
}

IP,Port설정 후 bind

클라이언트는 bind가 필요없다. sendto를 하는 시점에서 Ip와 Port가 모두 설정된다. (포트는 자동)

SOCKADDR_IN serverAddr;
::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");
}

Recv

UDP는 연결이 되어있지 않기 때문에 어디서 이것을 보내줬는 지 받아줄 변수를 따로 만들어 저장 한 뒤 나중에 Send에 활용한다.

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

Send

받았던 정보를 다시 보내준다.

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

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

전체 코드

서버

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

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

	// 연결할 목적지 -> IP + Port ex) XX아파트 YY 호
	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");
	}
	
	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;
	}

	// 윈속 종료
	::WSACleanup();

}

클라이언트

int main()
{
	// 원속 초기화 (ws2_32 라이브러리 초기화)
	// 관련 정보가 wsaData에 채워짐
	WSAData wsaData;
	if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		return 0;
	SOCKET clientSocket = ::socket(AF_INET, SOCK_DGRAM, 0);
	if (clientSocket == INVALID_SOCKET) {
		HandleError("Socket");
		return 0;
	}
	
		SOCKADDR_IN serverAddr;
		::memset(&serverAddr, 0, sizeof(serverAddr));
		serverAddr.sin_family = AF_INET;
		::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) {
			char sendBuffer[100] = "Hello World";

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

			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;

	}

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

	// 윈속 종료 -> Start 해준 만큼
	::WSACleanup();
}

데이터 바운더리

UDP에서도 TCP와 마찬가지로 10번 보내고 한번에 받아보자.

보내기

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

어떤 일이 일어날까?

TCP는 1000바이트가 한 번에 받아졌었다. 하지만 UDP의 경우 100바이트로 10번 받아진다.

TCP는 데이터 바운더리가 확실하지않아 겹쳐서 보내지기도하고 덜 보내지기도하지만, UDP는 한 번에 보내진다.

이것이 UDP의 특징, 데이터 바운더리가 확실하다는 점이다.

Connected UDP

현재 받고 보내는 과정에서 계속 데이터가 어디서왔는지, 어디로 보내야하는지 계속 지정해줘야한다.

하지만 지속적으로 같은 곳에 보내고싶은 상황이라면 굉장히 번거로운 작업이다. 이것을 위해 Connected UDP라는 것이 있다. Connected라고 연결되는 것은아니고, 내부 작업은 동일하다.

사용방법은 간단하다. TCP처럼 connect함수를 사용해주면된다.

SOCKADDR_IN serverAddr;
::memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
serverAddr.sin_port = ::htons(7777); 

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

Send와 Recv

TCP와 동일한 함수를 사용한다. 하지만 내부적으론 UDP와 동일하게 진행되는 걸 기억하자.

::send(clientSocket, sendBuffer, sizeof(sendBuffer), 0);

::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);

0개의 댓글