Skip to main content
GameDev.net gamedev.net
🔒 Locked

connecting with udp

Started by staticVoid2 Dec 15, 2009 at 4:22 PM 10 replies 1.6k views
Original Post
staticVoid2
staticVoid2
I'm trying to establish a connection between two computers using udp. The client sends a request packet to the server and the server sends another packet back to the client informing them. The code I am using on the client looks like this:

        char buffer[256];
        // while the client is not registered with the server...
	while(!registered)
	{
		// send the request if still not registered
		sendto(this->sendSocket, (char*)&request, sizeof(request), 0, (SOCKADDR*)&this->sendAddr, sizeof(this->sendAddr));

		// wait to recv a packet from the server
		recvfrom(this->recvSocket, buffer, 256, 0, 0, 0);

                switch(buffer[0])
                {
                   case CLIENT_REPLY:
                   {
                      registered = true;
                   }
                }
        }

sendAddr is the address of the server (sockaddr_in). the request is just an unsigned char. The server seems to always recieve this request even though I have made a mistake... the recvfrom function blocks so the request is only sent once. (there is no point in having the while(!registered)). When the server replies it only sends one packet which sometimes gets lost (this packet is 12 bytes). here is the server code:


while(true)
{
   char buffer[256];
   sockaddr_in sender;
   int length = sizeof(sender);
   // recieve packet from client
   recvfrom(this->recvSocket, buffer, 256, 0, (SOCKADDR*)&sender, &length);

   switch(buffer[0])
   {
      case CLIENT_REQUEST:
      {
         ClientReply reply;
         reply.type = CLIENT_REPLY;
         sendto(this->sendSocket, (char*)&reply, sizeof(reply), 0, (SOCKADDR*)&sender, sizeof(sender));
      }
   }
}

this process runs in a separate thread. My questions are: does the packet size play a factor in the probability that the packet will get lost? the client request (1 byte) always seems to make it to the server wheras the client reply (12 bytes) rarely makes it back to the client. (I tried wrapping the sendto function in a for(int x = 10; x--;) loop and the reply seems to make it back more often but surely this can't be the answer). does the recvfrom function have a timeout? or can you set one? rather than having to create a new thread to listen for incoming messages and have one do re-send the request every so often? and generally whats the best way to have the client send a request and then wait for a reply and then resend the request after a certain time?
Indigo Darkwolf
Indigo Darkwolf
Quote:
Original post by staticVoid2
does the packet size play a factor in the probability that the packet will get lost?

Sort of, yes. It's somewhat complicated, but the answer isn't to send tiny packets. The answer is to detect that you need to retransmit, which usually involves giving the packet a unique ID and having the recipient transmit this ID back to the sender to acknowledge that they received the packet. Of course, the receiver would have to store this unique ID for at least a little while, in case the ack didn't get across the wire and the sender re-transmits the packet, which means the receiver can skip processing the retransmitted packet and just re-send the ack. Better yet, transmit the ack twice as part of two separate transmissions, to improve the odds that it'll get across the wire. In the event that the sender receives both acks, just observe that the packet that was acked no longer exists in a buffer (since you would be able to un-buffer the reliable packet upon receiving the first ack) and ignore the re-ack.

Sound like a lot of work? Well, UDP is not the "easy" way to do reliable networking. If you need a reliable channel for communication, I would seriously suggest TCP. If you're really gung-ho about UDP, you should know that you'll need to reproduce at least part of the TCP implementation. Simply using TCP from the start saves you this trouble.

Quote:
Original post by staticVoid2
does the recvfrom function have a timeout? or can you set one?

Yes and yes, but it's not part of recvfrom, it's part of the socket. Look for something to the effect of "ioctlsocket()" or "setsockopt()". You can usually even set a timeout of "0", or non-blocking.

Quote:
Original post by staticVoid2
rather than having to create a new thread to listen for incoming messages

I would recommend setting the timeout of your socket to be non-blocking, and then call recvfrom every so often to check for incoming traffic. Specifically, I would periodically do a while(recvfrom() > 0) to gather all incoming traffic at fixed intervals. Once per frame may even be appropriate timing.

Quote:
Original post by staticVoid2
and have one do re-send the request every so often?

I would suggest having a transmit() function that's called every so often that sends any packets that need sending, including re-transmits. But again, if you have traffic that absolutely must get across the wire, I would seriously suggest you consider TCP.

Quote:
Original post by staticVoid2
and generally whats the best way to have the client send a request and then wait for a reply and then resend the request after a certain time?

Again, TCP. If it's gotta get there, the answer is TCP, or else implementing something TCP-like where a reliable packet gets buffered, transmitted, and retransmitted after a timeout, repeating until some larger timeout expires when you conclude that the receiver just isn't listening anymore (broken pipe, asteroid landed on it, etc).

Globals are not evil. Singletons are evil.
hplus0603
hplus0603
Note that, if you re-implement TCP, it's important to take care of avoiding catastrophic congestion failure. Imagine if, when packets were dropped, everyone would try *harder* to get the data through. In that case, once a router starts dropping packets because it's congested, everyone will congest even more by trying harder. This is why TCP drops to half the estimated bandwidth on a dropped packet, and only adds to estimated bandwidth linearly on successful acks. When things get congested, all TCP stacks will back off until the congestion clears.
enum Bool { True, False, FileNotFound };
staticVoid2
staticVoid2
so could I use TCP for esablishing a connection and then switch to UDP for sending gamestate packets every frame?
oliii
oliii
noooo.... You can do that with UDP easily. Just keep sending requests, like "can I connect?..." every 1/2 second or so.

If you don't get a reply after a while, abort.

I like to use mini state machines for connections.

Just a crude example.

STATE_CONNECTING :
client : try to connect to a host. Sends ConnRequest(parameters[]) at regular intervals. Parameters are some game-side properties, that the game host can accept or reject. For example, if we have been banned, or if supporting split-screen, how many local players you have.

STATE_PENDING_ACCEPT :
host : the host received a ConnRequest(parameters[]). Connection now waits for the game to accept or reject the request. If the game refused the connection, go into STATE_DESTROYED.

STATE_ACCEPTED :
host : The connection has been accepted by the game. sends ConnAccepted() back to the client at regular intervals. As soon as we receive a KeepAlive() message from the client, we can go into the ESTABLISHED state.

STATE_ESTABLISHED :
client : we've just received a ConnAccepted() from the host. Keep sending KeepAlive() back to the host. Once in that state, you can start sending player inputs (for example).
host : Keep sending KeepAlive() back to the client. Once in that state, you can start sending game states.

STATE_CLOSING :
client : we are trying to leave the game. send a fixed number of ConnClosing() messages back to the host (say, 5 or 10) at regular intervals (say, 1/2 second). If host did not respond with a ConnDestroyed() reply, exit game anyway.

STATE_DESTROYED :
host : the game kicked a client. send a fixed number of ConnDestroyed(reason) messages to the client (say, 5 or 10) at regular intervals (say, 1/2 second). If the client does not receive the message, no matter, he will time out eventually on his own.

misc:
host : as soon as we receive a ConnClosing() from a client, go into STATE_DESTROYED. if you haven't received any messages from a client for a while (say, 10 seconds timeout), kick the connection and go into STATE_DESTROYED.

client : as soon as we receive a ConnDestroyed(reason) from the host, leave the game. if you haven't received any messages from the host for a while (say, 10 seconds timeout), go into STATE_CLOSING.
Everything is better with Metal.
Indigo Darkwolf
Indigo Darkwolf
You could, yes. Can your game can tolerate dropped packets of data? Why are you choosing to pursue UDP instead of using TCP? Does the nature of your game require extremely high performance networking, or are you taking the hard road for academic/educational purposes?

Edit:
Oliii's solution is fine, as long as your game has relatively little state to it. It's reproducing about as little TCP as you can (SYN, SYN/ACK, ACK for establishing the connection map to STATE_CONNECTING, STATE_PENDING_ACCEPT, and STATE_ACCEPTED; FIN, FIN/ACK termination map to STATE_DESTROYED, STATE_CLOSING), and does put you into a state where clients can send data to the server, and the server can send data to the client.

The advantage to this is that if your game's state can fit within an MTU (or within several MTUs if you can break state into multiple packets), then you have a dead-simple communications protocol, which has relatively low latency, especially on a LAN.

The downside is that if you have a lot of static objects then you're wasting bandwidth by needlessly transmitting state updates, how much depends on how many objects and how much state you need to sync their possible changes. You have no way of detecting or dealing with network congestion. You have to be careful that your game can survive and not desynchronize when a state update is dropped. Also, features like in-game chat are either unreliable, or require you to implement a smaller but similar-looking state machine for sending chat messages, or you implement that much more of the TCP protocol so you can detect a "reliable packet" that needs to be re-transmitted. If you think you have more than a few types of messages that need to be transmitted reliably, I would reconsider TCP or implement a more generic reliable packet transmission and handling system.

It really boils down to what your needs are.

[Edited by - Indigo Darkwolf on December 16, 2009 12:40:48 PM]

Globals are not evil. Singletons are evil.
staticVoid2
staticVoid2
I've ran into a potential problem with the state machine approach...

Say the server had the addresses of 3 clients which it was sending server updates to every frame:

struct ServerUpdate{   // <data>};



another client tries to connect by constantly sending a ClientRequest packet to the server, at this point the client is in CONNECTING state.

When a new client connects all other clients need to know about this so the server transitions into a VERIFY state (once it receives the ClientRequest packet)in which it sends ClientJoined packets to each client and waits for them to verify...

void Server::update(){   switch(state)   {      case VERIFY:      {         ClientJoined joined;         joined.name = "<client_name>";         // send to all clients         for(unsigned int i = 0; i < this->clients.size(); ++i)         {            sendto(sendsock, (char*)&joined, sizeof(joined), 0, this->clients.address, sizeof(sockaddr_in));         }         // non-blocking         char buffer[256];         if(recvfrom(recvsock, buffer, 256, 0, 0, 0) > 0)         {            // check for the correct packet            if(buffer[0] == VERIFY)            {               Verify* verify = (Verify*)buffer;               if(!verified[verify->clientId])               {                  if(++verifyCount == clients.size())                  {                     // all clients have verified                     state = NORMAL;                  }                  verified[verify->clientId] = true;               }            }         }      }      break;   }}



when the client recieves a ClientJoined packet it transitions into a VERIFY state in which it constantly sends a Verify packet back to the server until it starts receiving server updates again but what if a previous server update is delayed (as udp packets may arrive late) and it makes its way to the client when in the VERIFY state? the client might not have actually verified that they have acknoledged the new client to the server yet this server update will cause the client to transition out of the VERIFY state meaning the server will be waiting forever for that client to verify?
Indigo Darkwolf
Indigo Darkwolf
In your state machine model, if the server is in the VERIFY state, it should continue to send verify packets to all clients until all clients have responded appropriately. Each verify packet should kick the clients into the VERIFY state, so even if a client receives a VERIFY and an UPDATE immediately after it so it doesn't immediately respond with a VERIFY, it should get kicked into VERIFY by the next verify packet from the server.

Is there some reason that wouldn't work in your game?

Edit: I'm immeasurably curious about why your clients need to establish connections to each other, and you can't just create a new player object and start sending updates about it. You shouldn't have to pause the game for everyone every time a new player connects. You've given no information about your game, however, so I'm just assuming it's a FPS.

Globals are not evil. Singletons are evil.
staticVoid2
staticVoid2
Quote:

Edit: I'm immeasurably curious about why your clients need to establish connections to each other, and you can't just create a new player object and start sending updates about it. You shouldn't have to pause the game for everyone every time a new player connects. You've given no information about your game, however, so I'm just assuming it's a FPS.


It is an fps game.

how then would you send the initial base state to each client or tell the clients to allocate resources for a new player.

or would I be better to just send the player base state each frame, currently this is just the name of the player (20 characters) so it's not much overhead.
Indigo Darkwolf
Indigo Darkwolf
Quote:
Original post by staticVoid2
how then would you send the initial base state to each client or tell the clients to allocate resources for a new player.

or would I be better to just send the player base state each frame, currently this is just the name of the player (20 characters) so it's not much overhead.

While that may not sound like a bad solution at face value, the math of it is a little sketchy. Consider a dedicated server with 16 players, sending network updates at 30hz. That's 20 bytes of player names which can occur 16 times in a packet that's sent to 16 players every 1/30th of a second, or 20*16*16*30 bytes per second, multiplied by 8 for bits per second. That works out to 1,228,800 bits per second, or about 1.2Mbps. My residential cable service gives me about 1Mbps upstream, so if I'm the server then I've just flooded by upstream with string names alone, without even saving room for the IPv4 or UDP headers.

The above example can be optimized by not sending player state data to each local player, which would reduce it to 20*15*16*30*8bps, or about 1.1Mbps before headers. If the server isn't dedicated, we can further reduce that to 20*15*15*30*8, but that still leaves us around 1Mbps. You can really get a win if you reduce the frequency of transmits - reducing network updates to 10hz brings you down to under 360Kbps, but that's still more than a third of my maximum upstream, and it doesn't account for housemates, roommates, siblings, or others making excessive use of the available bandwidth for their own dubious purposes, though I guess that's a problem beyond any game's capability to resolve.

The moral of the story is that bandwidth is a scarce resource.

I guess at that point, since your strategy has been to implement connection-level state machines for every packet, then you have no choice but to pause the game when new players arrive or whenever you have any sort of data that you have to guarantee is transmitted. That goes back to my original point that if you have more than a few types of reliable packets, you're probably best off either using a TCP/UDP hybrid to try and make use of the best elements each protocol has to offer, or implementing a reliable transport mechanism into your UDP networking code that can ensure the delivery of "reliable" packets while still allowing you to "fire-and-forget" the "unreliable" ones.

[Edited by - Indigo Darkwolf on December 22, 2009 6:41:22 PM]

Globals are not evil. Singletons are evil.
staticVoid2
staticVoid2
so I can/should use TCP for sending the base state?

I take it I could just create a new tcp socket and bind it to a different port then?
Indigo Darkwolf
Indigo Darkwolf
That should work. Then you can send updates for each object in the game to all connected players on your UDP socket, and clients can simply ignore the updates to players that they haven't been told about yet, which would be really easy to do if you prefix each player's state update in the packet with an ID number for each player.

You still have to have each client's UDP connection ping the server like you're doing already (for NAT traversal reasons*), but at least that can be handled on a per-connection basis without pausing the whole game for everyone.

The TCP connection should remain open on its own, but to help detect dropped players you may want to periodically (~10 seconds?) send a tiny heartbeat through it.

That seems like the simplest solution, or at least simpler than implementing a more generic reliable transport mechanism into your UDP implementation.

Edit: *About NAT traversal. It's a hairy problem. The executive summary is that your server will need to either be directly connected to the internet or have the appropriate ports forwarded to it by the router it's sitting behind. Clients who may be sitting behind a NAT of their own will also have to be the ones that request a connection to the server, so their routers can dynamically determine what port should be temporarily remapped to forward replies from the server back to the client. For more information (or rather, to get a sense of how complicated it would be to attempt to make robust NAT-traversal), see the Wikipedia article on Network Address Translation.

Globals are not evil. Singletons are evil.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.