Client TCP

Cours 06

Principe de TCP

En TCP, on établit une connexion entre deux machines. En général, le client fait la demande de connexion que le serveur accepte. Une fois la connexion établie, le client et le serveur peuvent lire et écrire des données.

La connexion a deux canaux :

  • un canal pour écrire vers la machine à laquelle on est connecté,
  • un canal pour lire les données venant de la machine à laquelle on est connecté.

On peut fermer l'un de ces canaux sans fermer la connexion.

Connexion à 2 canaux

Garanties de TCP

  • Fiabilité : Le protocole TCP garantit qu'aucune donnée n'est perdue.

  • Intégrité : Le protocole TCP garantit que les données arrivent dans l'ordre où elles ont été envoyées et qu'elles ne sont pas modifiées.

  • Non préservation des limites : Le protocole TCP ne garantit pas que les données arrivent en une seule fois.

    Si on écrit 100 octets sur une connexion TCP, on pourra recevoir 40 octets puis 60 octets. On pourra aussi recevoir 100 octets. Si on écrit 50 octets puis 50 octets, le serveur pourra recevoir 100 octets en une seule fois.

Client vs Serveur

En général, plusieurs clients se connectent à un unique serveur.

Comme en UDP, le point d'attachement côté client comme côté serveur se fait par des sockets, liées à une adresse IP et un numéro de port.

Pour l'instant, nous ne considérons que le cas des clients.

TCP coté Client

La socket TCP est représentée par un objet SocketChannel.

Une SocketChannel sc est créée grâce à la méthode factory SocketChannel.open(). Elle n'est pas connectée.

La connexion à un serveur se fait par l'appel à la méthode sc.connect(SocketAddress).

// creation of the socket (not connected)
SocketChannel sc = SocketChannel.open();

var serverAddress = new InetSocketAddress("www.google.com", 80);
// connection to server
sc.connect(serverAddress);

Lecture et écriture sur SocketChannel

Une fois la SocketChannel sc connectée, il est possible d'envoyer ou de recevoir des octets vers ou depuis le serveur.

  • La méthode sc.read(ByteBuffer buffer) lit des données du serveur et les stocke dans buffer.
  • La méthode sc.write(ByteBuffer buffer) écrit les octets de buffer vers le serveur.

Les méthodes sont les mêmes que pour la classe FileChannel.

Par défaut, ces méthodes (read et write) sont bloquantes.

La méthode read termine quand au moins 1 octet a été lu ou que la fermeture de la connexion est détectée. La méthode write termine quand toute la zone de travail a été écrite.

Lecture sur SocketChannel (1/2)

La lecture se fait avec la méthode read(ByteBuffer buffer). Les données lues sont écrites dans la zone de travail de buffer (au plus buffer.remaining()).

Attention : la zone de travail de buffer ne vas pas forcément être remplie intégralement !

S'il y a plus de données que la taille de buffer, elles ne sont pas perdues et seront lues au prochain read.

La méthode read renvoie le nombre d'octets lus ou -1 si la connexion a été fermée en écriture par l'interlocuteur.
On peut encore lui écrire mais on ne recevra plus jamais rien.

Lecture sur SocketChannel (2/2)

  var buffer = ByteBuffer.allocate(BUFFER_SIZE);
  var read = sc.read(buffer);

  if (read == -1){
    System.out.println("Connection closed for reading");
  } else {
    System.out.println("Read "+ read +" bytes");
  }

Écriture sur SocketChannel

L'écriture se fait avec la méthode write(ByteBuffer buffer).

Les données de la zone de travail de buffer sont intégralement écrites. Par défaut, la méthode bloque jusqu'à la fin de l'écriture.

  ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);

  buffer.putLong(1l);
  buffer.putLong(2l);
  buffer.flip();
  
  sc.write(buffer);

Fermeture

On peut fermer...

  • ... seulement le canal d'écriture avec shutdownOutput(). L'interlocuteur est notifié (son read renvoie -1).
  • ... seulement le canal de lecture avec shutdownInput(). Opération purement locale : l'interlocuteur n'est pas notifié.
  • ... tous les canaux avec close().
    Indispensable pour libérer les ressources système.

Non préservation des limites

Quand on reçoit des données, a priori, on ne peut pas :

  • savoir quand/si on a bien tout reçu,
  • savoir comment découper les données reçues.

C'est le rôle du protocole ! Il y a trois solutions usuelles :

  • fermer la connexion pour signaler la fin des données,
  • utiliser un marqueur (i.e. une suite d'octets) pour signaler la fin des données,
  • commencer par transmettre la taille des données.

Fermeture de la connexion (1/3)

Si on a un seul bloc de données à envoyer, on peut l'écrire puis fermer la connexion en écriture. L'interlocuteur saura qu'il n'y a plus de données quand read renverra -1 (c'est ce que fait HTTP 1.0).

Problèmes :

  • Plusieurs requêtes = plusieurs connexions.
  • Comment choisir la taille du buffer de réception ?

Marqueur de fin (2/3)

Si on a plusieurs données à envoyer, comme par exemple plusieurs chaînes encodées en ASCII, on peut convenir d'une suite d'octets marquant la fin de chaque chaîne (par exemple \r\n dans les headers HTTP).

Problèmes :

  • Complexe à mettre en œuvre du coté du récepteur.
  • Comment choisir la taille du buffer de réception ?

Préfixer l'envoi par la taille (3/3)

On peut écrire la taille (par exemple avec un long) des données avant de les envoyer.

Problème :

  • Celui qui envoie doit connaître la taille totale des données avant de les envoyer.

(c'est le principe des chunks en HTTP/1.1)