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.