Un guide sur les sockets Java

Un guide sur les sockets Java

1. Vue d'ensemble

Le termesocketprogramming fait référence à l'écriture de programmes qui s'exécutent sur plusieurs ordinateurs dans lesquels les périphériques sont tous connectés les uns aux autres à l'aide d'un réseau.

Il existe deux protocoles de communication utilisables pour la programmation des sockets:User Datagram Protocol (UDP) and Transfer Control Protocol (TCP).

La principale différence entre les deux réside dans le fait qu'UDP est sans connexion, ce qui signifie qu'il n'y a pas de session entre le client et le serveur, tandis que TCP est orienté connexion, ce qui signifie qu'une connexion exclusive doit d'abord être établie entre le client et le serveur pour que la communication puisse avoir lieu.

Ce didacticiel présente les réseauxan introduction to sockets programming over TCP/IP et montre comment écrire des applications client / serveur en Java. Le protocole UDP n'est pas un protocole courant et, en tant que tel, peut ne pas être rencontré souvent.

2. Configuration du projet

Java fournit un ensemble de classes et d'interfaces prenant en charge les détails de communication de bas niveau entre le client et le serveur.

Ceux-ci sont principalement contenus dans le packagejava.net, nous devons donc effectuer l'importation suivante:

import java.net.*;

Nous avons également besoin du packagejava.io qui nous donne des flux d'entrée et de sortie pour écrire et lire tout en communiquant:

import java.io.*;

Par souci de simplicité, nous exécuterons nos programmes client et serveur sur le même ordinateur. Si nous devions les exécuter sur différents ordinateurs en réseau, la seule chose qui changerait est l'adresse IP, dans ce cas, nous utiliseronslocalhost sur127.0.0.1.

3. Exemple simple

Mettons la main à la pâte avec le plus debasic of examples involving a client and a server. Ce sera une application de communication bidirectionnelle où le client accueille le serveur et le serveur répond.

Créons l’application serveur dans une classe appeléeGreetServer.java avec le code suivant.

Nous incluons la méthodemain et les variables globales pour attirer l'attention sur la façon dont nous exécuterons tous les serveurs dans cet article. Dans le reste des exemples des articles, nous omettons ce type de code plus répétitif:

public class GreetServer {
    private ServerSocket serverSocket;
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void start(int port) {
        serverSocket = new ServerSocket(port);
        clientSocket = serverSocket.accept();
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        String greeting = in.readLine();
            if ("hello server".equals(greeting)) {
                out.println("hello client");
            }
            else {
                out.println("unrecognised greeting");
            }
    }

    public void stop() {
        in.close();
        out.close();
        clientSocket.close();
        serverSocket.close();
    }
    public static void main(String[] args) {
        GreetServer server=new GreetServer();
        server.start(6666);
    }
}

Créons également un client appeléGreetClient.java avec ce code:

public class GreetClient {
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void startConnection(String ip, int port) {
        clientSocket = new Socket(ip, port);
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
    }

    public String sendMessage(String msg) {
        out.println(msg);
        String resp = in.readLine();
        return resp;
    }

    public void stopConnection() {
        in.close();
        out.close();
        clientSocket.close();
    }
}

Let’s start the server; dans votre IDE, vous faites cela en l'exécutant simplement en tant qu'application Java.

Et maintenant, envoyons un message d'accueil au serveur à l'aide d'un test unitaire, qui confirme que le serveur envoie effectivement un message d'accueil en réponse:

@Test
public void givenGreetingClient_whenServerRespondsWhenStarted_thenCorrect() {
    GreetClient client = new GreetClient();
    client.startConnection("127.0.0.1", 6666);
    String response = client.sendMessage("hello server");
    assertEquals("hello client", response);
}

Ne vous inquiétez pas si vous ne comprenez pas entièrement ce qui se passe ici, car cet exemple est destiné à nous donner une idée de ce à quoi vous attendre plus tard dans l'article.

Dans les sections suivantes, nous disséqueronssocket communication à l'aide de cet exemple simple et approfondirons les détails avec plus d'exemples.

4. Comment fonctionnent les sockets

Nous allons utiliser l'exemple ci-dessus pour parcourir différentes parties de cette section.

Par définition, unsocket est une extrémité d'une liaison de communication bidirectionnelle entre deux programmes s'exécutant sur des ordinateurs différents sur un réseau. Un socket est lié à un numéro de port afin que la couche de transport puisse identifier l'application à laquelle les données sont destinées à être envoyées.

4.1. Le serveur

En règle générale, un serveur s'exécute sur un ordinateur spécifique du réseau et dispose d'un socket lié à un numéro de port spécifique. Dans notre cas, nous utilisons le même ordinateur que le client et avons démarré le serveur sur le port6666:

ServerSocket serverSocket = new ServerSocket(6666);

Le serveur attend juste, écoutant le socket pour qu'un client fasse une demande de connexion. Cela se passe à l'étape suivante:

Socket clientSocket = serverSocket.accept();

Lorsque le code serveur rencontre la méthodeaccept, il se bloque jusqu'à ce qu'un client lui fasse une demande de connexion.

Si tout se passe bien, le serveuracceptsfait la connexion. Lors de l'acceptation, le serveur obtient un nouveau socket,clientSocket, lié au même port local,6666, et son point de terminaison distant est également défini sur l'adresse et le port du client.

À ce stade, le nouvel objetSocket met le serveur en connexion directe avec le client, nous pouvons alors accéder aux flux de sortie et d'entrée pour écrire et recevoir des messages vers et depuis le client respectivement:

PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

A partir de là, le serveur est capable d'échanger des messages avec le client jusqu'à la fermeture du socket avec ses flux.

Toutefois, dans notre exemple, le serveur peut uniquement envoyer une réponse avant de fermer la connexion. Cela signifie que si nous relançions notre test, la connexion serait refusée.

Pour permettre la continuité de la communication, nous devrons lire dans le flux d'entrée à l'intérieur d'une bouclewhile et ne quitter que lorsque le client envoie une demande de terminaison, nous verrons cela en action dans la section suivante.

Pour chaque nouveau client, le serveur a besoin d'un nouveau socket renvoyé par l'appelaccept. LeserverSocket est utilisé pour continuer à écouter les demandes de connexion tout en répondant aux besoins des clients connectés. Nous n’avons pas encore permis cela dans notre premier exemple.

4.2. Le client

Le client doit connaître le nom d'hôte ou l'adresse IP de la machine sur laquelle le serveur est exécuté et le numéro de port sur lequel le serveur écoute.

Pour faire une demande de connexion, le client essaie de se rendre avec le serveur sur la machine et le port du serveur:

Socket clientSocket = new Socket("127.0.0.1", 6666);

Le client doit également s'identifier auprès du serveur afin qu'il se connecte à un numéro de port local, attribué par le système, qu'il utilisera lors de cette connexion. Nous ne nous en occupons pas nous-mêmes.

Le constructeur ci-dessus ne crée une nouvelle socket que lorsque le serveur aaccepted la connexion, sinon, nous obtiendrons une exception de connexion refusée. Une fois créé avec succès, nous pouvons ensuite obtenir des flux d’entrée et de sortie pour communiquer avec le serveur:

PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

Le flux d'entrée du client est connecté au flux de sortie du serveur, tout comme le flux d'entrée du serveur est connecté au flux de sortie du client.

5. Communication continue

Notre serveur actuel bloque jusqu'à ce qu'un client s'y connecte, puis se bloque à nouveau pour écouter un message du client. Une fois le message envoyé, il ferme la connexion car nous n'avons pas traité la continuité.

Cela n’est donc utile que dans les requêtes ping, mais imaginons que nous voulions mettre en place un serveur de discussion, une communication continue entre le serveur et le client serait certainement nécessaire.

Nous devrons créer une boucle while pour observer en permanence le flux d'entrée du serveur pour les messages entrants.

Créons un nouveau serveur appeléEchoServer.java dont le seul but est de renvoyer tous les messages qu’il reçoit des clients:

public class EchoServer {
    public void start(int port) {
        serverSocket = new ServerSocket(port);
        clientSocket = serverSocket.accept();
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

        String inputLine;
        while ((inputLine = in.readLine()) != null) {
        if (".".equals(inputLine)) {
            out.println("good bye");
            break;
         }
         out.println(inputLine);
    }
}

Notez que nous avons ajouté une condition de terminaison dans laquelle la boucle while se termine lorsque nous recevons un caractère de période.

Nous allons démarrerEchoServer en utilisant la méthode main comme nous l'avons fait pour lesGreetServer. Cette fois, nous le démarrons sur un autre port tel que4444 pour éviter toute confusion.

LeEchoClient est similaire àGreetClient, nous pouvons donc dupliquer le code. Nous les séparons pour plus de clarté.

Dans une classe de test différente, nous allons créer un test pour montrer que plusieurs requêtes vers lesEchoServer seront servies sans que le serveur ferme le socket. Cela est vrai tant que nous envoyons des demandes du même client.

Traiter avec plusieurs clients est un cas différent, que nous verrons dans une section ultérieure.

Créons une méthodesetup pour établir une connexion avec le serveur:

@Before
public void setup() {
    client = new EchoClient();
    client.startConnection("127.0.0.1", 4444);
}

Nous allons également créer une méthodetearDown pour libérer toutes nos ressources, c'est la meilleure pratique pour chaque cas où nous utilisons des ressources réseau:

@After
public void tearDown() {
    client.stopConnection();
}

Testons ensuite notre serveur d'écho avec quelques requêtes:

@Test
public void givenClient_whenServerEchosMessage_thenCorrect() {
    String resp1 = client.sendMessage("hello");
    String resp2 = client.sendMessage("world");
    String resp3 = client.sendMessage("!");
    String resp4 = client.sendMessage(".");

    assertEquals("hello", resp1);
    assertEquals("world", resp2);
    assertEquals("!", resp3);
    assertEquals("good bye", resp4);
}

Il s'agit d'une amélioration par rapport à l'exemple initial, où nous ne communiquerions qu'une seule fois avant que le serveur ne ferme notre connexion; now we send a termination signal to tell the server when we’re done with the session.

6. Serveur avec plusieurs clients

Bien que l'exemple précédent soit une amélioration par rapport au premier, ce n'est toujours pas une si bonne solution. Un serveur doit avoir la capacité de desservir plusieurs clients et plusieurs demandes simultanément.

La gestion de plusieurs clients est ce que nous allons couvrir dans cette section.

Une autre fonctionnalité que nous verrons ici est que le même client peut se déconnecter puis se reconnecter sans obtenir une exception de connexion refusée ni une réinitialisation de la connexion sur le serveur. Auparavant, nous ne pouvions pas faire cela.

Cela signifie que notre serveur sera plus robuste et résilient face aux multiples demandes de plusieurs clients.

Pour ce faire, nous allons créer un nouveau socket pour chaque nouveau client et service que le client demande sur un thread différent. Le nombre de clients desservis simultanément sera égal au nombre de threads en cours d'exécution.

Le thread principal exécutera une boucle while lorsqu'il écoute de nouvelles connexions.

Assez parlé, créons un autre serveur appeléEchoMultiServer.java. À l'intérieur, nous allons créer une classe de thread de gestionnaire pour gérer les communications de chaque client sur son socket:

public class EchoMultiServer {
    private ServerSocket serverSocket;

    public void start(int port) {
        serverSocket = new ServerSocket(port);
        while (true)
            new EchoClientHandler(serverSocket.accept()).start();
    }

    public void stop() {
        serverSocket.close();
    }

    private static class EchoClientHandler extends Thread {
        private Socket clientSocket;
        private PrintWriter out;
        private BufferedReader in;

        public EchoClientHandler(Socket socket) {
            this.clientSocket = socket;
        }

        public void run() {
            out = new PrintWriter(clientSocket.getOutputStream(), true);
            in = new BufferedReader(
              new InputStreamReader(clientSocket.getInputStream()));

            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                if (".".equals(inputLine)) {
                    out.println("bye");
                    break;
                }
                out.println(inputLine);
            }

            in.close();
            out.close();
            clientSocket.close();
    }
}

Notez que nous appelons maintenantaccept dans une bouclewhile. Chaque fois que la bouclewhile est exécutée, elle se bloque à l'appel deaccept jusqu'à ce qu'un nouveau client se connecte, puis le thread gestionnaire,EchoClientHandler, est créé pour ce client.

Ce qui se passe à l'intérieur du thread est ce que nous faisions précédemment dans lesEchoServer où nous ne traitions qu'un seul client. Ainsi,EchoMultiServer délègue ce travail àEchoClientHandler afin qu'il puisse continuer à écouter plus de clients dans la bouclewhile.

Nous utiliserons toujoursEchoClient pour tester le serveur, cette fois nous créerons plusieurs clients chacun envoyant et recevant plusieurs messages du serveur.

Lançons notre serveur en utilisant sa méthode principale sur le port5555.

Pour plus de clarté, nous allons toujours mettre les tests dans une nouvelle suite:

@Test
public void givenClient1_whenServerResponds_thenCorrect() {
    EchoClient client1 = new EchoClient();
    client1.startConnection("127.0.0.1", 5555);
    String msg1 = client1.sendMessage("hello");
    String msg2 = client1.sendMessage("world");
    String terminate = client1.sendMessage(".");

    assertEquals(msg1, "hello");
    assertEquals(msg2, "world");
    assertEquals(terminate, "bye");
}

@Test
public void givenClient2_whenServerResponds_thenCorrect() {
    EchoClient client2 = new EchoClient();
    client2.startConnection("127.0.0.1", 5555);
    String msg1 = client2.sendMessage("hello");
    String msg2 = client2.sendMessage("world");
    String terminate = client2.sendMessage(".");

    assertEquals(msg1, "hello");
    assertEquals(msg2, "world");
    assertEquals(terminate, "bye");
}

Nous pourrions créer autant de cas de test que nous le voudrions, chaque génération d'un nouveau client et le serveur les serviront tous.

7. Conclusion

Dans ce didacticiel, nous nous sommes concentrés suran introduction to sockets programming over TCP/IP et avons écrit une simple application client / serveur en Java.

Le code source complet de l'article se trouve - comme d'habitude - dans le projetGitHub.