Tuesday, April 7, 2015

Migrating from TCP to UDP to reduce latency



The TCP protocol guarantees a reliable connection between two ports of different machines. It is reliable because it guarantees that packets will arrive in order to the other side. The implementation of the protocol is based on the confirmation packet to get from one end to another. If a packet is not confirmed, the sender  machine will resend the packet. In scenarios  where latency is not an issue is a safe option using the TCP protocol, but in applications that need continuous and flexible interaction between remote clients and the server must bet on the use of UDP.
For our part, we implement what TCP provides us with additional functionality in our application.
A good practice is to design the application assuming that we will have a secure connection. Moreover, it is even recommended to make a first implementation using TCP.  we will keep logic inside communication classes, modules, libraries etc ... (depending on the language). The idea is to have an abstraction of communication between clients and the server that guarantees that  there is really a flow control packet. We gain with this approach?

- We can test the functionality of the application under ideal conditions. We will not have the desired final velocity but it will  we serve us to detect functional errors.
- Some functionalities of the application can function correctly under TCP. For example: Our multiplayer game establishes a dialogue between client and the server before the start of the gamer. In this scenario, having a high latency is not a handicap.
- Similar to the previous point. Once we are using UDP, and if we find a functional error, we can always reuse TCP to rule out the origin of the error relates to the communications layer.
Therefore, our goal is to have a first version of our application using a communication library based on TCP. Once we see that the functional part of the application behaves as expected, we  will replace the communications library that we have developed over UDP.


 
Let's see an approximation of how to implement this library UDP communications.
Before going into detail we will explain the context of our application. Our server, a multiplayer game (snake the net) creates a thread for each incoming connection. For each item, there is a single thread that manages the game while there are other  n threads that manage the relationship with each of the remote clients. It is an architecture that can be used in countless applications. It is important to detail the scenario because this architecture helps us to make a modular and scalable deployment althought it has implications for communications.
- One of the advantages we enjoy with TCP is that for each incoming connection, we will have a dedicated socket so we have a dedicated connection between each client and the server.
Socket client = socket.accept();
In our case, each server  thread, that  serves a remote client, will have this dedicated socket. It will read and write information when needed.
ois = new ObjectInputStream(client.getInputStream());
c=(Command) ois.readObject();
              oos = new ObjectOutputStream(client.getOutputStream());
              oos.writeObject(c);
In the case of UPD, we will not have this dedicated channel and we have to simulate it. After creating  UDP socket, we will shortly read it.
DatagramSocket  socket = new DatagramSocket(port);
      
This socket server will be shared by all threads that serve the socket so we add a layer to solves this deficiency. The solution we have adopted is to create a thread server that is responsible for reading the socket, store and deliver it to each customer when required. The skeleton is as follows:
       private static ArrayList<Client> clients;
       private static ArrayList<Command> commands;
       while(keepReading){
              Command command=UDPCom.getCommand();
             synchronized(commands){
                    commands.add(command);
                    ClientN cli=getClientWaittingCommand(command);
      
                    if (cli!=null)
                           synchronized(cli){
                                  cli.notify();
                           }
       }

1.       After opening the socket, we will create a thread that will read from the socket continuously. Onces a packer arrives, the server registers it in a packets container.

             Command command=UDPCom.getCommand();
             synchronized(commands){
                    commands.add(command);

2.      Then we check whether there has been a client expecting this packet. If you have registered you will be notified to pick up your package. Here we can already deduce that the packet must have a client identifier. This allocation of Ids must be managed between the server and the remote client. In our case, we perform this dialog via TCP (no need to use UDP).

       ClientN cli=getClientWaittingCommand(command);
      
       if (cli!=null)
             synchronized(cli){
                    cli.notify();
             }

From the perspective of the thread that serves the customer, as mentioned, we operate analogously regardless of protocol. Reading will encapsulate a method analogous to that TCP offers.

public static Command readUDPCommand(ClientNibbles client,int timeOut) throws InterruptedException
{
       Command command=getReadedCommandByClient(client);
       if (command==null)
       {
             synchronized(clients)
             {
                    clients.add(client);
             }
             synchronized(client)
             {
                    client.wait(timeOut);
             }
command=getReadedCommandByClient(client);
                   
             synchronized(clients)
             {
                    clients.remove(clients);
             }
                   
       }
       else
             if (command!=null){
                    synchronized(commands){
                           commands.remove(command);
                    }
             }
return command;
       }     

1.       Each thread that serves the customer look first if the UDP server already has a pending packet for him.
2. If we already have it, we get it  and delete it from the list of packet container.
3. If not, we record the client in the waiting client list and wait until the UDP  server notify us that a new packet has arrived. Upon notification, we will read it  from  the unread  packet container and delete it from the container.
By this approach we simply ensure that each thread, serving a remote client, receives the packets we need. We Also get closer the way of working of the two protocols so that we can exchange the TCP / UDP libraries at our convenience. However, it seems that we have not  implemented two of the advantages that  TCP delivers: Ensure the order of arrival of the packets and obtain confirmation of their arrival.
Let's see ... there is no magic solution. The key is to detect which type of traffic can be implemented using UDP. Specifically, our application will only use UDP when the game is running for collecting customer movements  by the server. Consider each of the two points:
- Ensure that the packets arrive.
The server, which is an imperative server has an internal clock that each x time warn us that he has completed a turn and must send the summary of movements back to the players. If you end a turn with no news  from a remote client, we will assume that, for latency issues, their movement is late and it will be discarded. In fact, by this logic we are solving the problem of lost packets. If the package never arrives it will be equivalent to a movement that is late. The only difference is that the packets really never come.
In this case, the result of using UDP is that lost packets will be interpreted in the eyes of the customer as corrections to their movements (only if change of direction). Certainly, but the alternative (using TCP) involves raising the latency and have a percentage of tardy movements much higher.

- Ensure the order of arrival of packets
Let's review what we have done in the UDP server. Put simply, there a thread that reads packets arriving from the socket and leave them in a container for each client to pick their own. Well, we include an identifier sequence in each packet so that the server knows which one you have to collect. It will be enough then to look for packets searching by  this sequence number. If a packet with sequence id greater than expected arrives earlier, It will simply be remain stored until the server needs it
In summary, it appears that we have achieved our goal. We have created a UDP-based library that has the same functionality as TCP. In addition, performing a modular design, we can exchange libraries when needed.

No comments:

Post a Comment