UDP (User Datagram Protocol) est un protocole de communication utilisé par Internet. Il fait partie de la couche transport, comme TCP. Il est détaillé dans la RFC 768.
Le rôle de ce protocole est de permettre la transmission de données (sous forme de datagrammes) de bout en bout (c’est à dire entre processus, chacun étant défini par une adresse IP et un numéro de port). UDP utilise un mode de transmission sans connexion.
Le protocole étant extrêmement simple (l’en-tête est de 8 octets), il est souvent décrit comme étant non-fiable mais plus rapide que TCP.
En théorie, la taille maximale d’un datagramme est de 65536 octets mais la plupart des systèmes limitent la taille des datagrammes à 8Ko (8192 octets). De nombreux protocoles de la couche Application utilisent même un datagramme de 512 octets.
Remarque : le protocole fonctionnant sans établir de connexion, il autorise une communication broadcast et multicast contrairement à TCP.
L’interface Java des sockets (package java.net) offre un accès simple aux sockets.
On l’utilise en plaçant au début du fichier : import java.net.*
;
Plusieurs classes interviennent lors de la réalisation d’une communication par sockets :
java.net.InetAddress
permet de manipuler des adresses IP.java.net.SocketServer
permet de programmer l’interface côté serveur en mode connecté.java.net.Socket
permet de programmer l’interface côté client et la communication par flot via les sockets.java.net.DatagramSocket
et java.net.DatagramPacket
permettent de programmer la communication en mode datagramme UDP en mode non connecté.Remarque : si le compilateur émet des messages du genre “must catch exception”, il faudra donc utiliser un bloc try/catch
ainsi :
try
{
... // code
}
catch (Exception e)
{
e.printStackTrace();
}
Cette classe représente les adresses IP et un ensemble de méthodes pour les manipuler. Elle encapsule aussi la résolution des noms.
public static InetAddress getLocalHost() throws UnknownHostException
public static InetAddress getByName(String host) throws UnknownHostException
public static InetAddress[] getAllByName(String host) throws UnknownHostException
// obtient le nom complet correspondant à l'adresse IP
public String getHostName()
// obtient l'adresse IP sous forme %d.%d.%d.%d
public String getHostAddress()
// obtient l'adresse IP sous forme d'un tableau d'octets
public byte[] getAddress()
Côté client, on utilise les constructeurs suivants :
public Socket(String host, int port) throws UnknownHostException, IOException
public Socket(InetAddress address, int port) throws IOException
public Socket(String host, int port, InetAddress localAddr, int localPort) throws UnknownHostException, IOException
public Socket(InetAddress addr, int port, InetAddress localAddr, int localPort) throws IOException
Les deux premiers constructeurs construisent une socket connectée à la machine et au port spécifiés. Par défaut, la connexion est de type TCP fiable. Les deux autres interfaces permettent en outre de fixer l’adresse IP et le numéro de port utilisés côté client (plutôt que d’utiliser un port disponible quelconque).
Ces constructeurs correspondent à l’utilisation des primitives socket
, bind
(éventuellement) et connect
.
Remarque : Côté serveur, la méthode accept()
de la classe ServerSocket
renvoie une Socket de service connecté au client.
Certaines méthodes sont bloquantes, mais l’attente peut être limitée dans le temps par l’appel préalable de la méthode setSoTimeout()
. Cette méthode prend en paramètre le délai de garde exprimé en millisecondes. La valeur par défaut 0 équivaut à l’infini. À l’expiration du délai de garde, l’exception java.io.InterruptedIOException
est levée.
La fermeture d’une socket se fait par l’appel de la méthode close()
.
Enfin, les méthodes suivantes retrouvent l’adresse IP ou le port d’une socket :
public InetAddress getInetAddress()
public int getLocalPort()
La classe DatagramSocket
permet d’envoyer et de recevoir des paquets (des datagrammes UDP). Il s’agit donc de messages non fiables (possibilités de pertes et de duplication), non ordonnés (les messages peuvent être reçus dans un ordre différent de celui d’émission) et dont la taille reste assez faible.
public DatagramSocket() throws SocketException
public DatagramSocket(int port) throws SocketException
La communication utilise les méthodes :
public void send(DatagramPacket p) throws IOException
public void receive(DatagramPacket p) throws IOException
Ces opérations permettent d’envoyer et de recevoir un paquet. Un paquet est un objet de la classe DatagramPacket
qui possède une zone de données et (éventuellement) une adresse IP et un numéro de port (destinataire dans le cas send
, émetteur dans le cas receive
).
Les principales méthodes sont :
public DatagramPacket(byte[] buf, int length)
public DatagramPacket(byte[] buf, int length, InetAddress address, int port)
public InetAddress getAddress()
public int getPort()
public byte[] getData()
public int getLength()
public void setAddress(InetAddress iaddr)
public void setPort(int iport)
public void setData(byte[] buf)
public void setLength(int length)
Exemple d’une réception :
public void recevoir()
{
byte[] reception = new byte[1024];
while (socket != null && !socket.isClosed())
{
try
{
final DatagramPacket paquetRecu = new DatagramPacket(reception, reception.length);
socket.receive(paquetRecu);
final String data = new String(paquetRecu.getData(), paquetRecu.getOffset(), paquetRecu.getLength());
Log.d(TAG, "Réception de " + paquetRecu.getAddress().getHostAddress() + ":" + paquetRecu.getPort() + " -> " + data);
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
Exemple d’une émission :
public void envoyer(String requete)
{
byte[] emission = new byte[1024];
try
{
emission = requete.getBytes();
DatagramPacket paquetRetour = new DatagramPacket(emission, emission.length, adresseIP, PORT);
socket.send(paquetRetour);
}
catch (IOException e)
{
e.printStackTrace();
}
}
Remarque : Pour les besoins de tests, on pourra utiliser netcat
comme outil réseau côté PC.
Diverses méthodes renvoient le numéro de port local et l’adresse de la machine locale (getLocalPort()
et getLocalAddress()
), et dans le cas d’une socket connectée, le numéro de port distant et l’adresse distante (getPort()
et getInetAddress()
). Comme précédemment, on peut spécifier un délai de garde pour l’opération receive avec setSoTimeout()
. On peut aussi obtenir ou réduire la taille maximale d’un paquet avec getSendBufferSize()
, getReceiveBufferSize()
, setSendBufferSize() et
setReceiveBufferSize()`.
Enfin, la méthode close()
libère les ressources du système associées à la socket.
Attention, la communication réseau par socket sous Android nécessitera d’utiliser des threads.
Toute application Android est composée d’une multitude de threads.
L’UI thread est le fil d’exécution d’une activité. Les méthodes onCreate()
, onStart()
, OnPause()
, onResume()
, onStop()
, onDestroy()
de l’activité sont toutes exécutées dans ce thread.
L’UI thread est responsable de l’affichage et des interactions avec l’utilisateur et surtout c’est le seul thread qui doit modifier l’affichage.
Remarque : on ne peut pas effectuer des traitements consommateurs en temps dans l’UI thread car celui-ci se “figerait” et ne répondrait plus aux actions de l’utilisateur. D’autre part si une activité réagit en plus de cinq secondes, elle sera tuée par l’ActivityManager
qui la considérera comme morte.
En Java, il y a plusieurs façons de créer et exécuter un thread. Le principe de base revient à dériver (extends
) une classe Thread
ou à implémenter (implements
) l’interface Runnable
et à écrire le code du thread dans la méthode run()
. Ensuite, on appelera la méthode start()
pour démarrer le thread et la méthode stop()
pour l’arrêter.
Exemple (on pourrait ajouter le numéro de port comme paramètre du constructeur de la classe) :
// Une classe "Thread"
public class CommunicationUDP implements Runnable
{
private final String TAG = "CommunicationUDP";
private MainActivity activity;
private DatagramSocket socket;
private InetAddress adresseIP = null;
private final int PORT = 5000;
public static final int TIMEOUT_RECEPTION_REPONSE = 30000;
public CommunicationUDP(String adresseIP, MainActivity activity)
{
this.activity = activity;
try
{
socket = new DatagramSocket(PORT);
socket.setSoTimeout(CommunicationUDP.TIMEOUT_RECEPTION_REPONSE);
}
catch (SocketException se)
{
se.printStackTrace();
}
try
{
this.adresseIP = InetAddress.getByName(adresseIP);
}
catch (UnknownHostException e)
{
e.printStackTrace();
}
}
@Override
public void run()
{
// le code du thread
}
}
// Instanciation
communicationUDP = new CommunicationUDP("239.0.0.42", MainActivity.this); // ici avec une adresse multicast !
Thread tCommunicationUDP = new Thread(communicationUDP, "CommunicationUDP");
// Démarrage
tCommunicationUDP.start(); // -> appel run()
Attention : par contre les threads de l’applications ne pourront pas accéder directement à l’IHM car seul l’UI thread peut modifier l’affichage. Pour cela Android porpose plusieurs solutions et nous utiliserons la classe Handler
.
On peut simplement utiliser runOnUiThread()
:
public void recevoir()
{
byte[] reception = new byte[1024];
while (socket != null && !socket.isClosed())
{
try
{
final DatagramPacket paquetRecu = new DatagramPacket(reception, reception.length);
socket.receive(paquetRecu);
final String data = new String(paquetRecu.getData(), paquetRecu.getOffset(), paquetRecu.getLength());
Log.d(TAG, "Réception de " + paquetRecu.getAddress().getHostAddress() + ":" + paquetRecu.getPort() + " -> " + data);
activity.runOnUiThread(new Runnable()
{
@Override
public void run()
{
activity.setTextView(paquetRecu.getAddress().getHostAddress() + ":" + paquetRecu.getPort() + " -> " + data + "\n");
}
});
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
Ou utiliser Handler
comme moyen de communication entre le thread UI et un autre thread.
L’Handler
est associé à l’activité qui le déclare et travaille au sein du thread d’IHM (l’UI thread). Ce qui signifie que tout traitement effectué par le handler “gèle” l’IHM le temps qu’il soit effectué. Il faut donc considérer le handler comme celui qui met à jour l’IHM, le thread qui appelle le handler a en charge le traitement. Le handler ne doit se charger que de mettre à jour l’IHM, tout autre comportement est une erreur de conception.
public class MyActivity extends AppCompatActivity implements View.OnClickListener
{
// Gère les communications avec le thread réseau
final private Handler handler;
...
}
Un thread communique avec cet handler au moyen de messages (ou des objets Runnable
). Pour cela :
le thread récupère un objet « Message » du pool du handler par la méthode Message.obtain()
. Il peut ensuite ajouter un code (what) et d’autres paramètres (arg1, arg2 ou obj) ;
le thread envoie le message au handler en utilisant la méthode sendMessage()
qui envoie le message et le place à la fin de la queue,
le handler doit surcharger sa méthode handleMessage()
pour répondre aux messages qui lui sont envoyés.
Exemple (on pourrait ajouter le numéro de port comme paramètre du constructeur de la classe) côté thread :
public class CommunicationUDP implements Runnable
{
private Handler handler;
...
public CommunicationUDP(String adresseIP, Handler handlerUI)
{
...
// Récupère l'handler de l'IHM
handler = handlerUI;
...
}
...
// Une méthode pour diffuser un message vers l'IHM
public void diffuser(int code, String message)
{
Message msg = Message.obtain();
msg.what = code;
if(message != null)
msg.obj = message;
handler.sendMessage(msg);
}
}
Exemple d’utilisation :
diffuser(CODE_CONNEXION, null);
// ou :
diffuser(CODE_RECEPTION, "Hello world!");
Côté activité (IHM) :
public class MyActivity extends AppCompatActivity implements View.OnClickListener
{
...
// Gère les communications avec le thread réseau
final private Handler handler = new Handler()
{
public void handleMessage(Message msg)
{
super.handleMessage(msg);
if(msg.what == CODE_CONNEXION)
{
// Ici on peut modifier l'IHM
...
}
...
}
};
...
}
L’application a besoin des droits d’accès (uses-permission
) au réseau pour fonctionner.
Pour cela, il faut modifier le fichier AndroidManifest.xml
de l’application et reconstruire le projet :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.tv.myapplication2">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
...
</manifest>
L’application nécessite la présence d’un réseau pour fonctionner. On va tout d’abord vérifier son fonctionnement en utilisant ConnectivityManager
et WifiManager
avant de continuer.
Pour cela, on ajoute le code suivant dans la méthode onCreate()
de l’activité :
ConnectivityManager cm = (ConnectivityManager)getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
if(cm != null && (cm.getActiveNetworkInfo() == null || !cm.getActiveNetworkInfo().isConnected()))
{
Log.d(TAG, "Réseau indisponible !");
//Toast.makeText(getApplicationContext(), "Aucun réseau disponible !", Toast.LENGTH_SHORT).show();
}
else
{
Log.d(TAG, "Réseau disponible");
}
WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
if (!wm.isWifiEnabled())
{
Log.d(TAG, "WiFi indisponible !");
wm.setWifiEnabled(true);
}
else
{
Log.d(TAG, "WiFi disponible");
}
On peut aussi obtenir des informations sur le service réseau avec WifiInfo
et/ou DhcpInfo
:
WifiInfo wi = wm.getConnectionInfo();
Log.d(TAG, "WiFi : " + wi.toString() + " " + wi.getIpAddress() + " " + wi.getMacAddress());
DhcpInfo di = wm.getDhcpInfo();
Log.d(TAG, "WiFi : " + di.toString() + " " + toInetAddress(di.ipAddress).getHostAddress() + " " + toInetAddress(di.netmask).getHostAddress() + " " + toInetAddress(di.gateway).getHostAddress());
© Thierry Vaira <tvaira@free.fr>