E/S de sockets Java : bloquantes, non bloquantes et asynchrones

Photo d’Evi Radauscher sur Unsplash

Lors de la description des E / S, les termes non bloquant et asynchrone sont souvent utilisés de manière interchangeable, mais il existe une différence significative entre eux. Dans cet article sont décrites les différences théoriques et pratiques entre les opérations d’E / S de sockets non bloquantes et asynchrones en Java.

Les sockets sont des points d’extrémité pour effectuer une communication bidirectionnelle par les protocoles TCP et UDP. Les API Java sockets sont des adaptateurs pour les fonctionnalités correspondantes des systèmes d’exploitation. Communication des sockets dans les systèmes d’exploitation compatibles POSIX (Unix, Linux, Mac OS X, BSD, Solaris, AIX, etc.) est réalisée par des sockets Berkeley. La communication des sockets dans Windows est effectuée par Winsock qui est également basé sur des sockets Berkeley avec des fonctionnalités supplémentaires pour se conformer au modèle de programmation Windows.

Les définitions POSIX

Dans cet article sont utilisées des définitions simplifiées de la spécification POSIX.

Thread bloqué – un thread qui attend une condition avant de pouvoir continuer l’exécution.

Blocking – propriété d’un socket qui fait attendre que l’action demandée soit exécutée avant de revenir.

Non bloquant – propriété d’un socket qui provoque le retour sans délai des appels à celui-ci, lorsqu’il est détecté que l’action demandée ne peut pas être terminée sans un délai inconnu.

Opération d’E /S synchrone – une opération d’E /S qui provoque le blocage du thread demandeur jusqu’à la fin de cette opération d’E /S.

Opération d’E / S asynchrone – une opération d’E / S qui ne provoque pas en soi le blocage du thread demandeur ; cela implique que le thread et l’opération d’E / S peuvent s’exécuter simultanément.

Ainsi, selon la spécification POSIX, la différence entre les termes non bloquant et asynchrone est évidente:

  • non bloquant – propriété d’un socket qui provoque le retour sans délai des appels à celui—ci
  • E/S asynchrones — propriété sur une opération d’E/S (lecture ou écriture) qui s’exécute en même temps que le thread demandeur

Modèles d’E/S

Les modèles d’E/S suivants sont les plus courants pour le fonctionnement compatible POSIX systèmes:

  • modèle d’E/ S bloquant
  • modèle d’E / S non bloquant
  • Modèle de multiplexage d’E / S
  • modèle d’E / S piloté par signal
  • modèle d’E / S asynchrone

Modèle d’E / S bloquant

Dans le modèle d’E/S de blocage, l’application effectue un appel système de blocage jusqu’à ce que les données soient reçues au niveau du noyau et copiées de l’espace du noyau dans l’espace utilisateur.

 blocage du modèle d'E/S

 blocage du modèle d'E / S

Les Plus:

  • Le modèle d’E/S le plus simple à implémenter

Inconvénients:

  • L’application est bloquée

Modèle d’E/S non bloquant

Dans le modèle d’E/S non bloquant, l’application effectue un appel système qui renvoie immédiatement l’une des deux réponses:

  • si l’opération d’E / S peut être terminée immédiatement, les données sont renvoyées
  • si l’opération d’E / S ne peut pas être terminée immédiatement, un code d’erreur est renvoyé indiquant que l’opération d’E / S se bloquerait ou que le périphérique est temporairement indisponible

Pour terminer l’opération d’E / S, l’application doit attendre occupé (effectuer des appels système répétés) jusqu’à la fin.

 modèle d'E / S non bloquant

 modèle d'E / S non bloquant

Les Plus:

  • L’application n’est pas bloquée

Inconvénients:

  • L’application devrait attendre jusqu’à la fin, ce qui entraînerait de nombreux commutateurs de contexte utilisateur-noyau
  • Ce modèle peut introduire une latence d’E / S car il peut y avoir un écart entre la disponibilité des données dans le noyau et les données lues par l’application

Modèle de multiplexage d’E/ S

Dans le modèle de multiplexage d’E / S (également connu sous le nom de non- – blocage du modèle d’E / S avec notifications de blocage), l’application effectue un appel système de sélection de blocage pour commencer à surveiller l’activité sur de nombreux descripteurs. Pour chaque descripteur, il est possible de demander une notification de sa disponibilité pour certaines opérations d’E/S (connexion, lecture ou écriture, occurrence d’erreur, etc.). Lorsque l’appel système select renvoie qu’au moins un descripteur est prêt, l’application effectue un appel non bloquant et copie les données de l’espace noyau dans l’espace utilisateur.

 Modèle de multiplexage d'E/S

 Modèle de multiplexage d'E/S

Les Plus:

  • Il est possible d’effectuer des opérations d’E/S sur plusieurs descripteurs dans un thread

Inconvénients:

  • L’application est toujours bloquée lors de l’appel système sélectionné
  • Tous les systèmes d’exploitation ne prennent pas efficacement en charge ce modèle

Modèle d’E/S pilotée par le signal

Dans le modèle d’E/S pilotée par le signal, l’application effectue un appel non bloquant et enregistre un gestionnaire de signaux. Lorsqu’un descripteur est prêt pour une opération d’E/S, un signal est généré pour l’application. Ensuite, le gestionnaire de signaux copie les données de l’espace noyau dans l’espace utilisateur.

 modèle d'E/S pilotée par le signal

 modèle d'E/S pilotée par le signal

Les Plus:

  • L’application n’est pas bloquée
  • Les signaux peuvent fournir de bonnes performances

Inconvénients:

  • Tous les systèmes d’exploitation ne prennent pas en charge les signaux

Modèle d’E / S asynchrone

Dans le modèle d’E / S asynchrone (également connu sous le nom de modèle d’E / S chevauchantes), l’application effectue l’appel non bloquant et démarre une opération d’arrière-plan dans le noyau. Lorsque l’opération est terminée (les données sont reçues au niveau du noyau et sont copiées de l’espace du noyau dans l’espace utilisateur), un rappel d’achèvement est généré pour terminer l’opération d’E/S.

Une différence entre le modèle d’E/S asynchrone et le modèle d’E/S pilotées par le signal est qu’avec les E/S pilotées par le signal, le noyau indique à l’application quand une opération d’E/S peut être lancée, mais avec le modèle d’E/S asynchrone, le noyau indique à l’application quand une opération d’E/S est terminée.

 modèle d'E/S asynchrone

 modèle d'E/S asynchrone

Les Plus:

  • L’application n’est pas bloquée
  • Ce modèle peut fournir les meilleures performances

Inconvénients:

  • Le modèle d’E/S le plus compliqué à implémenter
  • Tous les systèmes d’exploitation ne prennent pas efficacement en charge ce modèle

Api d’E/S Java

L’API d’E/S Java est basée sur des flux (InputStream, OutputStream) qui représentent un flux de données unidirectionnel bloquant.

API Java NIO

L’API Java NIO est basée sur les classes Channel, Buffer et Selector, qui sont des adaptateurs pour les opérations d’E/ S de bas niveau des systèmes d’exploitation.

La classe Channel représente une connexion à une entité (périphérique matériel, fichier, socket, composant logiciel, etc.) capable d’effectuer des opérations d’E/S (lecture ou écriture).

Par rapport aux flux unidirectionnels, les canaux sont bidirectionnels.

La classe Buffer est un conteneur de données de taille fixe avec des méthodes supplémentaires pour lire et écrire des données. Toutes les données de Canal sont traitées par Buffer mais jamais directement: toutes les données envoyées à un Canal sont écrites dans un Buffer, toutes les données reçues d’un Canal sont lues dans un Buffer.

Par rapport aux flux, qui sont orientés octets, les canaux sont orientés blocs. Les E / S orientées par octets sont plus simples, mais pour certaines entités d’E / S peuvent être plutôt lentes. Les E / S orientées blocs peuvent être beaucoup plus rapides mais sont plus compliquées.

La classe Selector permet de s’abonner aux événements de nombreux objets SelectableChannel enregistrés en un seul appel. Lorsque des événements arrivent, un objet Sélecteur les envoie aux gestionnaires d’événements correspondants.

API Java NIO2

L’API Java NIO2 est basée sur des canaux asynchrones (AsynchronousServerSocketChannel, AsynchronousSocketChannel, etc.) qui prennent en charge les opérations d’E/S asynchrones (connexion, lecture ou écriture, gestion des erreurs).

Les canaux asynchrones fournissent deux mécanismes pour contrôler les opérations d’E/S asynchrones. Le premier mécanisme consiste à renvoyer un java.util.simultané.Objet futur, qui modélise une opération en attente et peut être utilisé pour interroger l’état et obtenir le résultat. Le deuxième mécanisme consiste à passer à l’opération un java.nio.canal.objet CompletionHandler, qui définit les méthodes de gestionnaire qui sont exécutées après la fin ou l’échec de l’opération. L’API fournie pour les deux mécanismes est équivalente.

Les canaux asynchrones fournissent un moyen standard d’effectuer des opérations asynchrones indépendamment de la plate-forme. Cependant, la quantité que l’API Java Sockets peut exploiter les capacités asynchrones natives d’un système d’exploitation dépendra de la prise en charge de cette plate-forme.

Socket echo server

La plupart des modèles d’E / S mentionnés ci-dessus sont implémentés ici dans des serveurs echo et des clients avec des API Java sockets. Les serveurs et les clients echo fonctionnent selon l’algorithme suivant:

  1. un serveur écoute un socket sur un port TCP enregistré 7000
  2. un client se connecte à partir d’un socket sur un port TCP dynamique au socket serveur
  3. le client lit une chaîne d’entrée depuis la console et envoie les octets de son socket au socket serveur
  4. le serveur reçoit les octets de son socket et les renvoie au socket client
  5. le client reçoit les octets de son socket et écrit la chaîne en écho sur la console
  6. lorsque le client reçoit le même nombre d’octets qu’il a envoyés, il se déconnecte du serveur
  7. lorsque le serveur reçoit une chaîne spéciale, il cesse d’écouter

La conversion entre les chaînes et les octets est ici effectuée explicitement dans le codage UTF-8.

En outre, seuls des codes simplifiés pour les serveurs d’écho sont fournis. Le lien vers les codes complets pour les serveurs et les clients Echo est fourni dans la conclusion.

Serveur d’écho d’E/S de blocage

Dans l’exemple suivant, le modèle d’E/S de blocage est implémenté dans un serveur d’écho avec l’API d’E/S Java.

Le ServerSocket.accepter les blocs de méthode jusqu’à ce qu’une connexion soit acceptée. Le flux d’entrée.lisez les blocs de méthode jusqu’à ce que les données d’entrée soient disponibles ou qu’un client soit déconnecté. Le flux de sortie.écrire des blocs de méthode jusqu’à ce que toutes les données de sortie soient écrites.

public class IoEchoServer { public static void main(String args) throws IOException {
ServerSocket serverSocket = new ServerSocket(7000); while (active) {
Socket socket = serverSocket.accept(); // blocking InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream(); int read;
byte bytes = new byte;
while ((read = is.read(bytes)) != -1) { // blocking
os.write(bytes, 0, read); // blocking
} socket.close();
} serverSocket.close();
}
}

Blocage du serveur d’écho NIO

Dans l’exemple suivant, le modèle d’E/S de blocage est implémenté dans un serveur d’écho avec l’API Java NIO.

Les objets ServerSocketChannel et SocketChannel sont configurés par défaut en mode de blocage. Le ServerSocketChannel.la méthode accept bloque et renvoie un objet SocketChannel lorsqu’une connexion est acceptée. Le Serveur.lisez les blocs de méthode jusqu’à ce que les données d’entrée soient disponibles ou qu’un client soit déconnecté. Le Serveur.écrire des blocs de méthode jusqu’à ce que toutes les données de sortie soient écrites.

public class NioBlockingEchoServer { public static void main(String args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("localhost", 7000)); while (active) {
SocketChannel socketChannel = serverSocketChannel.accept(); // blocking ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
int read = socketChannel.read(buffer); // blocking
if (read < 0) {
break;
} buffer.flip();
socketChannel.write(buffer); // blocking
} socketChannel.close();
} serverSocketChannel.close();
}
}

Serveur d’écho NIO non bloquant

Dans l’exemple suivant, le modèle d’E/S non bloquant est implémenté dans un serveur d’écho avec l’API Java NIO.

Les objets ServerSocketChannel et SocketChannel sont explicitement configurés en mode non bloquant. Le ServerSocketChannel.la méthode accept ne bloque pas et renvoie null si aucune connexion n’est encore acceptée ou un objet SocketChannel sinon. Le Serveur.read ne bloque pas et renvoie 0 si aucune donnée n’est disponible ou si un nombre positif d’octets est lu dans le cas contraire. Le Serveur.la méthode d’écriture ne bloque pas s’il y a de l’espace libre dans le tampon de sortie du socket.

public class NioNonBlockingEchoServer { public static void main(String args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(7000)); while (active) {
SocketChannel socketChannel = serverSocketChannel.accept(); // non-blocking
if (socketChannel != null) {
socketChannel.configureBlocking(false); ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
int read = socketChannel.read(buffer); // non-blocking
if (read < 0) {
break;
} buffer.flip();
socketChannel.write(buffer); // can be non-blocking
} socketChannel.close();
}
} serverSocketChannel.close();
}
}

Multiplexage du serveur d’écho NIO

Dans l’exemple suivant, le modèle d’E/S de multiplexage est implémenté dans une API Java NIO de serveur d’écho.

Lors de l’initialisation, plusieurs objets ServerSocketChannel, configurés en mode non bloquant, sont enregistrés sur le même objet Sélecteur avec la clé de sélection.Argument OP_ACCEPT pour spécifier qu’un événement d’acceptation de connexion est intéressant.

Dans la boucle principale, le Sélecteur.sélectionnez des blocs de méthode jusqu’à ce qu’au moins l’un des événements enregistrés se produise. Puis le Sélecteur.La méthode selectedKeys renvoie un ensemble d’objets SelectionKey pour lesquels des événements se sont produits. En parcourant les objets SelectionKey, il est possible de déterminer quel événement d’E/ S (connect, accept, read, write) s’est produit et quels objets sockets (ServerSocketChannel, SocketChannel) ont été associés à cet événement.

L’indication d’une touche de sélection qu’un canal est prêt pour une opération est un indice, pas une garantie.

public class NioMultiplexingEchoServer { public static void main(String args) throws IOException {
final int ports = 8;
ServerSocketChannel serverSocketChannels = new ServerSocketChannel; Selector selector = Selector.open(); for (int p = 0; p < ports; p++) {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannels = serverSocketChannel;
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress("localhost", 7000 + p)); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} while (active) {
selector.select(); // blocking Iterator<SelectionKey> keysIterator = selector.selectedKeys().iterator();
while (keysIterator.hasNext()) {
SelectionKey key = keysIterator.next(); if (key.isAcceptable()) {
accept(selector, key);
}
if (key.isReadable()) {
keysIterator.remove();
read(selector, key);
}
if (key.isWritable()) {
keysIterator.remove();
write(key);
}
}
} for (ServerSocketChannel serverSocketChannel : serverSocketChannels) {
serverSocketChannel.close();
}
}
}

Lorsqu’un objet SelectionKey indique qu’un événement d’acceptation de connexion s’est produit, il crée le ServerSocketChannel.accepter l’appel (qui peut être non bloquant) pour accepter la connexion. Après cela, un nouvel objet SocketChannel est configuré en mode non bloquant et est enregistré sur le même objet Sélecteur avec la clé de sélection.Argument OP_READ pour spécifier que maintenant un événement de lecture est intéressant.

private static void accept(Selector selector, SelectionKey key) throws IOException {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept(); // can be non-blocking
if (socketChannel != null) {
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
}

Lorsqu’un objet SelectionKey indique qu’un événement de lecture s’est produit, il est créé dans le SocketChannel.appel de lecture (qui peut être non bloquant) pour lire les données de l’objet SocketChannel dans un nouvel objet ByteByffer. Après cela, l’objet SocketChannel est enregistré sur le même objet Sélecteur avec la clé de sélection.Argument OP_WRITE pour spécifier que maintenant un événement d’écriture est intéressant. De plus, cet objet ByteBuffer est utilisé lors de l’enregistrement en tant que pièce jointe.

private static void read(Selector selector, SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer); // can be non-blocking buffer.flip();
socketChannel.register(selector, SelectionKey.OP_WRITE, buffer);
}

Lorsqu’un objet SelectionKeys indique qu’un événement d’écriture s’est produit, il crée le SocketChannel.appel d’écriture (qui peut être non bloquant) pour écrire des données dans l’objet SocketChannel à partir de l’objet ByteByffer, extrait de SelectionKey.méthode de fixation. Après cela, le socketchanneau.l’appel cloase ferme la connexion.

private static void write(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer buffer = (ByteBuffer) key.attachment(); socketChannel.write(buffer); // can be non-blocking
socketChannel.close();
}

Après chaque lecture ou écriture, l’objet SelectionKey est supprimé de l’ensemble des objets SelectionKey pour empêcher sa réutilisation. Mais l’objet SelectionKey pour l’acceptation de la connexion n’est pas supprimé pour pouvoir effectuer la prochaine opération similaire.

Serveur d’écho NIO2 asynchrone

Dans l’exemple suivant, le modèle d’E/S asynchrone est implémenté dans un serveur d’écho avec l’API Java NIO2. Les classes AsynchronousServerSocketChannel, AsynchronousSocketChannel sont utilisées ici avec le mécanisme des gestionnaires de complétion.

Le canal AsynchronousServerSocketChannel.la méthode accept initie une opération d’acceptation de connexion asynchrone.

public class Nio2CompletionHandlerEchoServer { public static void main(String args) throws IOException {
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(7000)); AcceptCompletionHandler acceptCompletionHandler = new AcceptCompletionHandler(serverSocketChannel);
serverSocketChannel.accept(null, acceptCompletionHandler); System.in.read();
}
}

Lorsqu’une connexion est acceptée (ou que l’opération échoue), la classe AcceptCompletionHandler est appelée, ce qui par le canal AsynchronousSocketChannel.read(destination ByteBuffer, Une pièce jointe, CompletionHandler < Entier, ? la méthode super A >) initie une opération de lecture asynchrone de l’objet AsynchronousSocketChannel vers un nouvel objet ByteBuffer.

class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, Void> { private final AsynchronousServerSocketChannel serverSocketChannel; AcceptCompletionHandler(AsynchronousServerSocketChannel serverSocketChannel) {
this.serverSocketChannel = serverSocketChannel;
} @Override
public void completed(AsynchronousSocketChannel socketChannel, Void attachment) {
serverSocketChannel.accept(null, this); // non-blocking ByteBuffer buffer = ByteBuffer.allocate(1024);
ReadCompletionHandler readCompletionHandler = new ReadCompletionHandler(socketChannel, buffer);
socketChannel.read(buffer, null, readCompletionHandler); // non-blocking
} @Override
public void failed(Throwable t, Void attachment) {
// exception handling
}
}

Lorsque l’opération de lecture est terminée (ou échoue), la classe ReadCompletionHandler est appelée, qui par le AsynchronousSocketChannel.write(source ByteBuffer, Une pièce jointe, Entier CompletionHandler <, ? la méthode super A >) initie une opération d’écriture asynchrone vers l’objet AsynchronousSocketChannel à partir de l’objet ByteBuffer.

class ReadCompletionHandler implements CompletionHandler<Integer, Void> { private final AsynchronousSocketChannel socketChannel;
private final ByteBuffer buffer; ReadCompletionHandler(AsynchronousSocketChannel socketChannel, ByteBuffer buffer) {
this.socketChannel = socketChannel;
this.buffer = buffer;
} @Override
public void completed(Integer bytesRead, Void attachment) {
WriteCompletionHandler writeCompletionHandler = new WriteCompletionHandler(socketChannel);
buffer.flip();
socketChannel.write(buffer, null, writeCompletionHandler); // non-blocking
} @Override
public void failed(Throwable t, Void attachment) {
// exception handling
}
}

Lorsque l’opération d’écriture est terminée (ou échoue), la classe WriteCompletionHandler est appelée, qui par le AsynchronousSocketChannel.la méthode close ferme la connexion.

class WriteCompletionHandler implements CompletionHandler<Integer, Void> { private final AsynchronousSocketChannel socketChannel; WriteCompletionHandler(AsynchronousSocketChannel socketChannel) {
this.socketChannel = socketChannel;
} @Override
public void completed(Integer bytesWritten, Void attachment) {
try {
socketChannel.close();
} catch (IOException e) {
// exception handling
}
} @Override
public void failed(Throwable t, Void attachment) {
// exception handling
}
}

Dans cet exemple, les opérations d’E / S asynchrones sont effectuées sans pièce jointe, car tous les objets nécessaires (AsynchronousSocketChannel, ByteBuffer) sont passés en tant qu’arguments de constructeur pour les gestionnaires de complétion appropriés.

Conclusion

Le choix du modèle d’E/S pour la communication des sockets dépend des paramètres du trafic. Si les demandes d’E / S sont longues et peu fréquentes, les E / S asynchrones sont généralement un bon choix. Cependant, si les demandes d’E / S sont courtes et rapides, la surcharge de traitement des appels du noyau peut rendre les E / S synchrones bien meilleures.

Bien que Java fournisse un moyen standard d’effectuer des E / S de sockets dans les différents systèmes d’exploitation, les performances réelles peuvent varier considérablement en fonction de leur implémentation. Il est possible de commencer à étudier ces différences avec l’article bien connu de Dan Kegel Le problème C10K.

Des exemples de code complets sont disponibles dans le référentiel GitHub.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.