1°/ Recalls on TCP socket classes in Java
- In Java, TCP sockets are based on 2 classes : Socket (communication) et ServerSocket (waiting connection).
- class Socket does not allow to directly communicate: there are no send/recv methods in this class.
- communications are available thanks to stream classes.
- class Socket contains 2 methods to retrieve an instance of byte stream, one for input, the other for output : getInputStream() and getOutputStream().
- as for regular files, it is possible to encapsulate a byte stream into a more complex one, that provides additional functionalities like read/writing characters, primary types (int, double, ...) and even objects.
- 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 this course, 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.
2°/ 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 !
3°/ A communication protocol using requests
3.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 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, for a given request, 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.
4.2°/ example : a time server
- The server proposes two service :
- update the current server time,
- giving the current time, taking a time zone into account.
- A bunch of solution exist. Here is a plausible one.
- request identifier : 1 byte
- = 1 for the update time request
- = 2 for the current time request
- request 1 :
- message structure 1 (client->server) : identifier (1 byte), hour (1 byte), minute (1 byte), second (1 byte)
- message 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)
- request 2 :
- message structure 1 (client->server) : identifier (1 byte), time zone (1 int).
- message structure 2 (server->client) : ack (1 bytet). 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),
- message structure 3 (server->client) only if ack=0 : answer size (1 octet), current hour (string, its size is given by the first byte of the answer)
Remarks :
- request 2 illustrates the principle to set conditions for some communications. Indeed, if there is an error, there is no message 3.
- this request also illustrates the principle of messages of variable size. Since the client receives a sequence of characters, it cannot know its size. 2 solutions : 1 - the sequence is terminated by a special character (like 0 or \n), 2 - the server sends the size.
- 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 because a string may be sent as a result of request 2.
- Since an int is sent for request 2, the order of the four bytes must be specified (i.e. endian type : big ou little).
- It is worth noting that this protocol can be adapted to only text.
- In this case, all messages are sequences of characters ending with a \n, so that it is easy to determine the start and the end of a message.
- For example:
- request 1 :
- message structure 1 (client->server) : identifier (string = "SETTIME"), space, hour (string), espace, minute (string), espace, seconde (string), end of line.
- message structure 2 (server->client) : ack (string). ack = "OK" or a message specifying errors, end of line.
4°/ Classical sketches
cf. dedicated article.