1°/ Recalls on TCP socket & client/server
 
  • TCP is a reliable protocol of communication over the network, i.e. :
    • if a packet of data is lost, it is sent again,
    • if packets are physically received in a wrong order, TCP reorganize them.
  • It can be used by asking the operating system to create TCP sockets on both communicating parts, and to connect them.
  • This connection phase (= a hand-shake) is necessary before being able to send/receive any data.
  • As for phone calls, this phase is initiated by a client application that wants to talk with a server application. Obviously, the server must be ready to receive a connection request.

 

  • Nevertheless, creating a socket is not sufficient: it must be bound to an IP address and a port, both on client & server.
  • Indeed, there are many machines on the network that run a server application but a client wants to talk with a precise one. The IP address is used to choose the "good one".
  • Furthermore, the same machine may execute several server applications. The port is used to choose the desired one.
  • This is the same for the client: when the server wants to send back something to it, it must be to the machine that runs the client, AND the good client.

 

  • The last point is that the server does not use the same socket for receiving connection requests and to communicate with a client.
  • Indeed, if the connection is accepted, a second socket is created by the operating system, dedicated to communications.
  • This scheme allows to let the first socket free to receive other connection requests, while the second one is used to send/receive data.
  • Nevertheless, this is only possible if the server is able to do several things concurrently, i.e. creating child processes or threads.

/ TCP socket classes in Java
 
  • Compared to C, Java sockets are really easy to create and use: few lines of code are sufficient.
  • Java TCP sockets are based on 2 classes : Socket (communication) et ServerSocket (waiting connection).
  • There is a "drawback" of the class Socket: it does not allow to directly communicate: there are no send/recv methods in this class.
  • communications are available thanks to stream classes.

 

  • Socket contains 2 methods to retrieve an instance of byte stream, one for input, the other for output : getInputStream() and getOutputStream().
  • these classes represent the sequences of bytes that can be read/write from/to the socket.
  • It is perfect and simple for sending/receiving files, but not for values.
  • For example, if a client wants to send an integer value to a server, this value must be converted into an array of 4 bytes before being sent. Upon reception, the server must convert these 4 bytes back to an integer value.
  • It is not very complex and time-costly operations but it implies more code to write.
  • It is the same if client/server wants to manipulate lines of text: encoding/decoding a String into a byte array is fastidious to implement, even more if different coding systems are used (iso-8859, utf8, ...)
  • Fortunately, Java API provides other stream classes, that can encapsulate a byte stream into a more complex one. These classes provides additional functionalities like read/writing characters, primary types (int, double, ...) and even objects.

 

3°/ What about IoT ?

  • In the context of an IoT application, with possibly parts implemented using different languages, a universal solution must be favored.
  • For example, object streams are forbidden because the serialization process to "compact" an object as a byte array is propre to Java and does not exists in other languages, or not with the same scheme of serialization.
  • The most universal is to use byte streams but it is as simple, for example if we have to communicate values over 255. In this case, to read an int, we have to take several bytes and reassemble them as an int. Sending an int is generally simpler but we have to take the endian-ness of the processor into account. Indeed, the four bytes of an int value are stored in a different order in a little and big endian architecture.
  • In fact, the best way is to use characters, provided we specify the encoding norm that is used (utf-8, iso-8859, ...). Moreover if we only use characters from the ascii table, we are very close to a simple and universal solution.
  • This is the solution that is used in the following, more especially classes BufferedReader et PrintStream, so that we can reader/write lines of text, instead of arrays of characters.
  • It allows a perfect compatibility between codes written for micro-controllers and applications in C or Java.
 

4°/ Basic example

This example is a simple echo client/server (i.e. server sends back the client message)

MyServer.java :

import java.io.*;
import java.net.*;
 
class MyServer  {
 
  public static void main(String []args) {
 
    ServerSocket sockConn = null; // connection waiting socketde connexion
    Socket sockComm = null; // communication socket
    int port = -1;
    PrintStream ps;
    BufferedReader br;
    try {
        port = Integer.parseInt(args[0]);
        sockConn = new ServerSocket(port);
        sockComm = sockConn.accept();
        ps = new PrintStream(sockComm.getOutputStream());
        br = new BufferedReader(new InputStreamReader(sockComm.getInputStream()));
        String msg = br.readLine(); // reception (blocking)
        ps.println(msg); // renvoi
    }
    catch(IOException e) {
        System.err.println("pb socket : "+e.getMessage());
        System.exit(1);
    } 
  }
}

 

MyClient.java :

import java.io.*;
import java.net.*;
 
class MyClient  {
 
  public static void main(String []args) {
 
    Socket sockComm = null; // communication socket
    String ipServ;
    int port = -1;
    PrintStream ps;
    BufferedReader br;
    try {
        ipServ = args[0];
        port = Integer.parseInt(args[1]);
        sockComm = newSocket(ipServ, port);
        ps = new PrintStream(sockComm.getOutputStream());
        br = new BufferedReader(new InputStreamReader(sockComm.getInputStream()));
        System.out.println("I say: "+args[2]);
        ps.println(args[2]); // send
        String msg = br.readLine(); // reception (blocking)
        System.out.println("Server answers: "+msg);
    }
    catch(IOException e) {
        System.err.println("pb socket : "+e.getMessage());
        System.exit(1);
    } 
  }
}
 

Remarks:

  • the way to establish the connection is always the same but the communication phase will change according to the application needs.
  • the server address can be an IP or a canonical name.
  • In that state, this server is stupid: it ends after its answer ! 

 

5°/ A communication protocol using requests

5.1°/ why a protocol

  • If a server proposes different services, they must be identified so that a client can provide this identifier when it sends a service request to the server.
  • This identifier is used by the server to determine the service and thus how to process the client request.
  • A request may have some parameters to parameterize the service processing.
  • Nevertheless, a program is not as flexible as a human, so a request cannot be structured anyhow.
  • Indeed, in a question, the order of subject, verb, objects can be variable. Moreover, the context influences this order and the vocabulary used. Theses variations are forbidden for a server program.
  • For such a program, the verb is the identifier, ans the objects are the parameters. To achieve its correct implementation, there are the following constraints:
    • the identifier must have the same type for all requests (an int, a string, ...),
    • the identifier must be the first thing sent by the client,
    • for a given request, the type (and even the format), the order and the number of parameters must be fixed (NB: the number constraint can be released in some cases)
    • the structure of the server answer must also be fixed for a given request.

 

  • Generally, to request a given service, the client sends identifier + parameters, then the server processes the request and sends back the result to the client.
  • But sometimes, several back and forth communications are needed, implying several processing phases

 

  • Defining the communication protocol consists in fixing:
    • the type of the identifier and its value for each requests.
    • the structure of messages exchanged while processing the request.

 

  • It is apparently simple but in practice, things are getting harsh as soon as we want a "polite" server. i.e. a server that signals request errors to the client.
  • Indeed, the simplest solution is to create a server that never asks to malformed requests. It is a bad idea, especially to debug the client/server application.
  • So, the protocol must include error messages, while keeping its consistency if there are no errors. This is the difficult part.

 

 5.2°/ example : a time server
 
  • The server proposes two services :
    1. update the current server time, providing a login & password,
    2. giving the current time, taking a time zone into account.
  • A bunch of solutions exist, and they mainly depend on what kind of data are used. Here are two solutions, based on binary and text data.

 

 5.2.1°/ Using binary data (i.e. classical types of C: byte, short, int, double, ...)

  • request identifier : 1 byte
    • = 1 for the update time service
    • = 2 for the current time service

 

  • service 1 :
    • request structure (client->server) : identifier  = 1 byte, hour = 1 byte, minute = 1 byte, second = 1 byte, login = several bytes, followed by one equals to 0, password = several bytes, , followed by one equals to 0
    • answer structure 2 (server->client) : ack =1 byte. ack allows the client to know if its request is valid. If ack = 0, all is valid. If ack = 1, invalid hour (i.e.. <0 or >23), if ack=2, invalid minute, if ack=4 invalid second. Values can be combined if several errors (e.g. ack = 1+2+4 = 7 if all is invalid)
  • service 2 :
    • request structure (client->server) : identifier = 1 byte, time zone = 1 int.
    • answer (part 1) structure (server->client) : ack = 1 byte. ack allows the client to know if its request is valid. If ack = 0, all is valid. If ack = 1, invalid hour (i.e.. <-11 or >12),
    • answer (part 2) structure (server->client) only if ack=0 : answer size = 1 byte, size of the following string = 1 short, current time = several bytes

 

Remarks :

  • these requests illustrate the principle of processing messages of variable size. Since the client receives a sequence of characters, it cannot know its size. 2 solutions : as in request 1 - the sequence is terminated by a special character (like 0 or \n), as in request 2 - the size of the sequence is given before the latter.
  • service 2 illustrates the principle to set conditions for some communications. Indeed, if there is an error, there is no answer part 2.
  • this protocol uses primary types. As said in the first section, this is the most universal way to specify a protocol.
  • Nevertheless, this protocol is not perfectly designed on 2 points :
    • since characters can be encoded over several bytes, depending on the norm (UTF-8, ISO8859, ...), the protocol should specify this norm.
    • Since an int is sent for request 2, the order of the four bytes must be specified (i.e. endian type : big ou little).

 5.2.2°/ Using text data

The simplest way is to use lines of text with a "classic" format: an identifier followed by the parameters of the request, all separated by a unique character, generally a space. This is the solution used for the example.

  • service 1 :
    • request structure (client->server) : "SETTIME hour minute seconde login password"
    • answer structure(server->client) : "OK" if success, otherwise "ERR error_message".
  • service 2 :
    • request structure (client->server) : "GETTIME time_zone"
    • answer (part 1) structure(server->client) : "OK" if success, otherwise "ERR error_message".
    • answer (part 2) structure(server->client) : "hour minute second".

6°/ Classical sketches
cf. dedicated article: client/server templates