🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Some questions about UDP in C++

Started by
5 comments, last by hplus0603 5 years, 4 months ago

I have a few questions regarding using UDP in C++.

 

  • Since UDP is connectionless, is there any reason for a UDP server to have more than one socket created?
  • Let's assume you are running two programs on your pc that both use UDP. Program A and program B. What is stopping program A from reading all of the packets that are intended for program B to receive before program B can read them? Is it just the specific port that is originally used when the socket is bound? If this is the case, how can you run multiple instances of a game that uses UDP if they are all set up to use the same designated port?
  • For TCP, if you want to host a server you have to open ports through your router for the port your server will use for listening. Is this not required for UDP since no actual connection exists? If it is not required, why is there an option to forward ports for UDP in router configurations?
Advertisement

1. In general, you can do fine with a single socket. Perhaps some OS/library combinations make throughput higher if you have multiple sockets open, but I haven't actually run into that in practice. Also, if you want to receive on multiple ports, you'd need multiple sockets. Finally, some red/green "always-up" deployment methods may need multiple sockets to support the roll-forward / roll-back scenarios. That's pretty esoteric, though.

2. Yes, in general, the two programs need to use different ports. Two different programs, using the same port, running on the same host, will not work well. Multiple processes can bind to the same UDP port on the same host (network interface, really) using the appropriate setsockopt() (REUSEPORT or REUSEADDR) and this is sometimes useful (see above.)

2b. If you want to run multiple instances of the server for the same game that binds to the same port on the same host, you will need to expose multiple IP addresses for the same network interface, which you typically do by creating multiple virtual interfaces that alias to the same physical interface. As long as none of the processes bind to the "every interface" address (IPADDR_ANY, or 0.0.0.0) and each process binds to its own virtual interface address, it'll work fine. Obviously, clients need to talk to the correct IP address.

3. For UDP, you still need to open a port, because if a packet shows up at the router, saying "hey, I'm here for port X," the router won't otherwise know where to forward that packet. There are ways around this, called "NAT punch-through," that requires collaboration with some shared host on the outside of the router for both client and server.

Regarding how "real games" do this, you may end up with some kind of proxy or gateway, that receives UDP packets from all possible clients, and then maps the incoming packets to the appropriate address/port/process on the back-end by inspecting which client it came from, for example. That intermediate proxy would have to build its own session state for the game, even though UDP itself doesn't have session state.

enum Bool { True, False, FileNotFound };
16 hours ago, hplus0603 said:

1. In general, you can do fine with a single socket. Perhaps some OS/library combinations make throughput higher if you have multiple sockets open, but I haven't actually run into that in practice. Also, if you want to receive on multiple ports, you'd need multiple sockets. Finally, some red/green "always-up" deployment methods may need multiple sockets to support the roll-forward / roll-back scenarios. That's pretty esoteric, though.

2. Yes, in general, the two programs need to use different ports. Two different programs, using the same port, running on the same host, will not work well. Multiple processes can bind to the same UDP port on the same host (network interface, really) using the appropriate setsockopt() (REUSEPORT or REUSEADDR) and this is sometimes useful (see above.)

2b. If you want to run multiple instances of the server for the same game that binds to the same port on the same host, you will need to expose multiple IP addresses for the same network interface, which you typically do by creating multiple virtual interfaces that alias to the same physical interface. As long as none of the processes bind to the "every interface" address (IPADDR_ANY, or 0.0.0.0) and each process binds to its own virtual interface address, it'll work fine. Obviously, clients need to talk to the correct IP address.

Okay thank you very much. This cleared up a lot of confusion I was having.

 

However, I tried to make a very basic server/client and i'm running into some issues.

I want this to function where the client will just send the server 4 byte packets with an unsigned 32 bit integer that increments from 0. When the server gets the packet, it should echo it back to the client. Currently i'm not concerned with dropped, repeated, out of order packets.

 

This is the code I came up with. Sorry about the length, I tried to make it as short as I could for this example.


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

#define HOSTIP "192.168.0.11"

enum RunType
{
	Server,
	Client
};

RunType runtype = RunType::Server;

void DoServer()
{
	SOCKET serverSocketHandle = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

	if (serverSocketHandle == NULL)
	{
		std::cerr << "Failed to create socket" << std::endl;
		return;
	}

	DWORD nonBlocking = 1;
	if (ioctlsocket(serverSocketHandle,	FIONBIO, &nonBlocking) != 0)
	{
		std::cerr << "Failed to set socket non-blocking." << std::endl;
		return;
	}

	sockaddr_in listenAddress;
	listenAddress.sin_family = AF_INET;
	listenAddress.sin_addr.s_addr = INADDR_ANY;
	listenAddress.sin_port = htons(8000);
	int listenAddressSize = sizeof(listenAddress);

	if (bind(serverSocketHandle,(const sockaddr*)&listenAddress,sizeof(sockaddr_in)) < 0)
	{
		std::cerr << "Failed to bind socket." << std::endl;
		return;
	}

	uint32_t recvPacketCounter = 0;

	while (true)
	{
		if (GetAsyncKeyState(VK_ESCAPE))
			return;
		if (recvfrom(serverSocketHandle, (char*)(&recvPacketCounter), sizeof(uint32_t), NULL, (sockaddr*)&listenAddress, &listenAddressSize) > 0) //Server is receiving messages
		{
			sendto(serverSocketHandle, (char*)&recvPacketCounter, sizeof(uint32_t), NULL, (sockaddr*)&listenAddress, sizeof(sockaddr_in)); //This send is never being picked up by the client?
			recvPacketCounter = ntohl(recvPacketCounter);
			std::cout << "Server - [Sender Port: " << ntohs(listenAddress.sin_port) << "] Echoing packet: " << recvPacketCounter << std::endl;
		}
	}
	
}

void DoClient()
{
	SOCKET clientSocketHandle = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

	if (clientSocketHandle == NULL)
	{
		std::cerr << "Failed to create socket" << std::endl;
		return;
	}

	DWORD nonBlocking = 1;
	if (ioctlsocket(clientSocketHandle, FIONBIO, &nonBlocking) != 0)
	{
		std::cerr << "Failed to set socket non-blocking." << std::endl;
		return;
	}

	uint32_t ip = INADDR_NONE;
	inet_pton(AF_INET, HOSTIP, &ip);
	if (ip == INADDR_NONE)
	{
		std::cerr << "Failed to resolve host." << std::endl;
		return;
	}

	uint32_t hostip = INADDR_NONE;
	inet_pton(AF_INET, HOSTIP, &hostip);
	if (hostip == INADDR_NONE)
	{
		std::cerr << "Failed to resolve host ip." << std::endl;
		return;
	}

	sockaddr_in sendtoAddress;
	sendtoAddress.sin_family = AF_INET;
	sendtoAddress.sin_addr.s_addr = hostip;
	sendtoAddress.sin_port = htons(8000);

	sockaddr_in recvAddress;
	recvAddress.sin_family = AF_INET;
	recvAddress.sin_addr.s_addr = INADDR_ANY;
	recvAddress.sin_port = 0; //Not specifying a port for client so an available port will be selected when binding
	int recvAddressSize = sizeof(recvAddress);

	if (bind(clientSocketHandle, (const sockaddr*)&recvAddress, sizeof(sockaddr_in)) < 0)
	{
		std::cerr << "Failed to bind socket." << std::endl;
		return;
	}

	uint32_t packetCounter = 0;
	uint32_t recvPacketCounter = 0;
	while (true)
	{
		if (GetAsyncKeyState(VK_ESCAPE))
			return;
		uint32_t tempval = packetCounter;
		tempval = htonl(tempval);
		sendto(clientSocketHandle, (char*)(&tempval), sizeof(uint32_t), NULL, (sockaddr*)&sendtoAddress, sizeof(sockaddr_in)); //<-This works
		if (recvfrom(clientSocketHandle, (char*)(&recvPacketCounter), sizeof(uint32_t), NULL, (sockaddr*)&recvAddress, &recvAddressSize) > 0) //<-Never receiving a message
		{
			recvPacketCounter = ntohl(recvPacketCounter);
			std::cout << "Client - [Sender Port: " << ntohs(recvAddress.sin_port) << "] Received packet: " << recvPacketCounter << std::endl;
		}
		Sleep(100);
		packetCounter += 1;
	}
}

int main()
{
	char input = 0;
	while (input != 'c' && input != 's')
	{
		std::cout << "Enter 'c' for client or 's' for server:";
		std::cin >> input;
	}

	if (input == 's')
		runtype = RunType::Server;
	else
		runtype = RunType::Client;

	WSADATA init;
	if (WSAStartup(MAKEWORD(2, 2), &init) != 0)
	{
		std::cerr << "WSA Startup failed with error code: " << WSAGetLastError() << std::endl;
		return -1;
	}

	if (runtype == RunType::Server)
	{
		DoServer();
	}
	if (runtype == RunType::Client)
	{
		DoClient();
	}

	WSACleanup();

	std::cout << "Program end." << std::endl;
	return 0;
}

 

If I run the server/client on the same machine, it appears to work fine. However, I know something must be wrong because when I run the server on one machine and the client on another, I get different behavior.

When I run the server/client on different machines, only the server will be printing out that it is receiving packets. It seems like the client is not set up right to properly receive the packets from the server. Is there anything that sticks out that I did wrong here to cause this issue?

A few things, small and big:

1) You should initialize listenAddress to 0 before filling out fields; either by using memset(), or by declaring it as

sockaddr_in listenAddress = {};

2) Because you don't do anything on a timer, you might do better not turning on non-blocking mode; as it is, your server will be burning a full CPU doing nothing but polling the socket.

3) NULL is not the "error socket value" in either Unix or Windows. Use the constant INVALID_SOCKET which I think aliases to 0xffffffff (which, when read as a signed 32-bit int, equals -1 which is the Unix error value.)

4) You need to assign sizeof(sockaddr_in) to listenAddressSize before the call to recvfrom(), each time. You have no idea what that value will become after the first call to the function.  Similarly, you should use listenAddressSize, not sizeof(sockaddr_in), when sending the response, although this only really matters once you build a dual-stack IP4/IP6 implementation.

5) sendtoAddress also needs to be initialized to 0 before filling out fields.

6) inet_pton() is a simple wrapper on sscanf(), it doesn't "resolve" anything. The error messages are thus misleading.

7) you practically never bind() the client socket. You're probably better off removing the call to bind() in the client code.The socket will be assigned a random-ish, unused port number the first time it sends a datagram, automatically, and that port will be in the address the server sees to return the packet to.

? recvAddressSize should have the value sizeof(sockaddr_in) each time you call recvfrom().

9) You should check errors for sendto() for both clients and servers.

10) What does Ethereal say when you capture UDP packets on port 8000 on the client and server when it doesn't work?

 

enum Bool { True, False, FileNotFound };
16 hours ago, hplus0603 said:

A few things, small and big:

1) You should initialize listenAddress to 0 before filling out fields; either by using memset(), or by declaring it as

 



sockaddr_in listenAddress = {};

 

2) Because you don't do anything on a timer, you might do better not turning on non-blocking mode; as it is, your server will be burning a full CPU doing nothing but polling the socket.

3) NULL is not the "error socket value" in either Unix or Windows. Use the constant INVALID_SOCKET which I think aliases to 0xffffffff (which, when read as a signed 32-bit int, equals -1 which is the Unix error value.)

4) You need to assign sizeof(sockaddr_in) to listenAddressSize before the call to recvfrom(), each time. You have no idea what that value will become after the first call to the function.  Similarly, you should use listenAddressSize, not sizeof(sockaddr_in), when sending the response, although this only really matters once you build a dual-stack IP4/IP6 implementation.

5) sendtoAddress also needs to be initialized to 0 before filling out fields.

6) inet_pton() is a simple wrapper on sscanf(), it doesn't "resolve" anything. The error messages are thus misleading.

7) you practically never bind() the client socket. You're probably better off removing the call to bind() in the client code.The socket will be assigned a random-ish, unused port number the first time it sends a datagram, automatically, and that port will be in the address the server sees to return the packet to.

? recvAddressSize should have the value sizeof(sockaddr_in) each time you call recvfrom().

9) You should check errors for sendto() for both clients and servers.

10) What does Ethereal say when you capture UDP packets on port 8000 on the client and server when it doesn't work?

 

Thanks again for all of the help. You're the best! I'm not sure what exactly the line of code was that was giving me the issue, but I made the changes you mentioned other than changing to blocking sockets, and when I went to test using Wireshark it was working properly!

 

I am thinking maybe the issue was that I was not reinitializing the length value for the last argument into recvfrom. I had no idea that you were supposed to! Also to note, I changed the functionality so that the server will send back the negative value that the client sent just for testing purposes and changed it to send a signed integer instead of an unsigned integer.

 

Edit: I still need to add error checking when calling sendto and recvfrom

 

For anyone interested, here was the final code.


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

#define HOSTIP "192.168.0.11"
const int g_sleepDuration = 1 * 1000; //1 Packet per second

enum RunType
{
	Server,
	Client
};

RunType runtype = RunType::Server;

void DoServer()
{
	SOCKET serverSocketHandle = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

	if (serverSocketHandle == INVALID_SOCKET)
	{
		std::cerr << "Failed to create socket" << std::endl;
		return;
	}

	DWORD nonBlocking = 1;
	if (ioctlsocket(serverSocketHandle,	FIONBIO, &nonBlocking) != 0)
	{
		std::cerr << "Failed to set socket non-blocking." << std::endl;
		return;
	}

	sockaddr_in listenAddress = {};
	listenAddress.sin_family = AF_INET;
	listenAddress.sin_addr.s_addr = INADDR_ANY;
	listenAddress.sin_port = htons(8000);
	int listenAddressSize = sizeof(sockaddr_in);

	if (bind(serverSocketHandle,(const sockaddr*)&listenAddress,sizeof(sockaddr_in)) < 0)
	{
		std::cerr << "Failed to bind socket." << std::endl;
		return;
	}

	int32_t recvPacketCounter = 0;

	while (true)
	{
		if (GetAsyncKeyState(VK_ESCAPE))
			return;
		listenAddressSize = sizeof(sockaddr_in);
		if (recvfrom(serverSocketHandle, (char*)(&recvPacketCounter), sizeof(uint32_t), NULL, (sockaddr*)&listenAddress, &listenAddressSize) > 0) //Server is receiving messages
		{
			recvPacketCounter = ntohl(recvPacketCounter);
			std::cout << "Server - [Sender Port: " << ntohs(listenAddress.sin_port) << "] Echoing packet: " << recvPacketCounter << std::endl;
			recvPacketCounter = -recvPacketCounter;
			recvPacketCounter = htonl(recvPacketCounter);
			sendto(serverSocketHandle, (char*)&recvPacketCounter, sizeof(uint32_t), NULL, (sockaddr*)&listenAddress, sizeof(sockaddr_in)); //This send is never being picked up by the client?
		}
		Sleep(1);
	}
	
}

void DoClient()
{
	SOCKET clientSocketHandle = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

	if (clientSocketHandle == INVALID_SOCKET)
	{
		std::cerr << "Failed to create socket" << std::endl;
		return;
	}

	DWORD nonBlocking = 1;
	if (ioctlsocket(clientSocketHandle, FIONBIO, &nonBlocking) != 0)
	{
		std::cerr << "Failed to set socket non-blocking." << std::endl;
		return;
	}

	uint32_t ip = INADDR_NONE;
	inet_pton(AF_INET, HOSTIP, &ip);
	if (ip == INADDR_NONE)
	{
		std::cerr << "Failed to resolve host." << std::endl;
		return;
	}

	uint32_t hostip = INADDR_NONE;
	inet_pton(AF_INET, HOSTIP, &hostip);
	if (hostip == INADDR_NONE)
	{
		std::cerr << "Failed to convert host ip from text to 32 bit binary." << std::endl;
		return;
	}

	sockaddr_in sendtoAddress = {};
	sendtoAddress.sin_family = AF_INET;
	sendtoAddress.sin_addr.s_addr = hostip;
	sendtoAddress.sin_port = htons(8000);

	sockaddr_in recvAddress = {};
	int recvAddressSize = sizeof(sockaddr_in);

	int32_t packetCounter = 0;
	int32_t recvPacketCounter = 0;
	while (true)
	{
		if (GetAsyncKeyState(VK_ESCAPE))
			return;
		uint32_t tempval = packetCounter;
		tempval = htonl(tempval);
		sendto(clientSocketHandle, (char*)(&tempval), sizeof(uint32_t), NULL, (sockaddr*)&sendtoAddress, sizeof(sockaddr_in)); //<-This works
		recvAddressSize = sizeof(sockaddr_in);
		if (recvfrom(clientSocketHandle, (char*)(&recvPacketCounter), sizeof(uint32_t), NULL, (sockaddr*)&recvAddress, &recvAddressSize) > 0) //<-Never receiving a message
		{
			recvPacketCounter = ntohl(recvPacketCounter);
			std::cout << "Client - [Sender Port: " << ntohs(recvAddress.sin_port) << "] Received packet: " << recvPacketCounter << std::endl;
		}
		Sleep(g_sleepDuration);
		packetCounter += 1;
	}
}

int main()
{
	char input = 0;
	while (input != 'c' && input != 's')
	{
		std::cout << "Enter 'c' for client or 's' for server:";
		std::cin >> input;
	}

	if (input == 's')
		runtype = RunType::Server;
	else
		runtype = RunType::Client;

	WSADATA init;
	if (WSAStartup(MAKEWORD(2, 2), &init) != 0)
	{
		std::cerr << "WSA Startup failed with error code: " << WSAGetLastError() << std::endl;
		return -1;
	}

	if (runtype == RunType::Server)
	{
		DoServer();
	}
	if (runtype == RunType::Client)
	{
		DoClient();
	}

	WSACleanup();

	std::cout << "Program end." << std::endl;
	return 0;
}

 

Quote

I'm not sure what exactly the line of code was that was giving me the issue

It might be educational for you to make one change at a time, and figure out which one it was ? (Of course, it may be more than one.)

My guess is that it's the client-side bind() and the size-value in recvfrom(). But only a controlled experiment will tell for sure. The reason recvfrom() needs the size value, is that otherwise it doesn't know how much it can write into the buffer you pass it (it can't assume it's a sockaddr_in, because sockets work with a bunch of different protocols, including IP6, which may have bigger address structs!) It thus reads the size of the buffer on input, and then updates the value on output to whatever the size of the address received actually was.

enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement