Tout ce qui est échangé sur le réseau n'est qu'une suite d'octets qui n'a aucun sens a priori. --- Matrix
Comme les machines qui communiquent peuvent avoir des processeurs différents, des OS différents et utiliser des langages de programmation différents, il n'y a aucune convention a priori.
C'est le protocole qui fixe le sens (sémantique) des octets.
Que représentent les octets ?61 E2 82 AC
61 E2 82 ACTaille :
short (2 octets),
int (4 octets) et
long (8 octets)
Endianness :
61 E2 82 AC → 1642234540
61 E2 82 AC → AC 82 E2 61 → 2894258785
Signé vs Non-Signé :
AC 82 E2 61 (non-signé) → 2894258785
AC 82 E2 61 (signé) → -1400708511
61 E2 82 ACUne chaîne de caractères ?
En ASCII (7 bits), ces octets sont invalides : "a???"
En ISO-8859-1, ces octets représentent "aâ ¬"
En UTF-8, ces octets représentent "a€"
Un caractère est un symbole, qui peut être représenté de différentes manières.
En Java, en interne, le type primitif char
permet de représenter ces symboles sur 2 octets (16 bits) dans le jeu de
caractères Unicode (UTF-16).
Il existe de nombreux jeux de caractères (ASCII, UTF-8, ISO-8859-1...)
61 E2 82 ACPour traiter des octets comme des caractères, il faut impérativement connaître le jeu de caractères (charset ) !
61
représente 'a'
E2 n'est associé à aucun caractère
E2 représente 'â'
E2 n'est associé à aucun caractère
E2 82 AC représente '€'
En Java, l'approche historique pour manipuler des octets était d'utiliser des
tableaux de type . byte[]
Problème de performance : la représentation de est figée par la spécification du langage.byte[]
Problème résolu par java.nio en autorisant des implémentations plus performantes.
Pour ce cours, on interdit les
.
byte[]
java.nio : "nouvelles" entrées-sorties (1.4)Gestion mémoire découplée entre système et JVM (performance)
(Input/Output)Stream deviennent des Channel
ByteBuffer au lieu des tableaux byte[] ;
Charset formalisent l'encodage des caractères.
On manipule ces classes sans connaître leur implémentation (plate-forme dépendante).
java.nio.ByteBufferMême concept qu'un tableau d'octets :
zone contiguë de bytes de taille fixe (capacity)
On utilise la notion de zone de travail définie entre :
position = prochain indice à accéder et
limit = premier indice "interdit".
ByteBufferByteBuffer byteBuffer = ByteBuffer.allocate(1024);
La méthode factory ByteBuffer.allocate(int capacity)
crée un ByteBuffer de taille capacity.
La postion est à 0 et la limit est à
capacity.
La mémoire est gérée par le Garbage Collector de Java.
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
Identique à la méthode ci-dessus mais la mémoire n'est pas gérée
par le Garbage Collector (mais par le système).
Les entrées/sorties sont plus performantes mais l'allocation et la libération beaucoup plus lentes.
Dogme : On réserve allocateDirect()
aux ByteBuffer qui "vivent" pendant toute la durée du programme.
ByteBufferL'accès est relatif à la position courante :
put(b) écrit l'octet b à la position couranteget() lit et retourne l'octet à la position couranteLes deux font avancer la position courante (comme dans un flot).
Si la position est en dehors de la zone de travail, une exception est levée :
BufferOverflowException ou BufferUnderflowException
ByteBufferLa zone de travail diminue au fur et à mesure qu'on écrit.
flipPour pouvoir relire ce qui vient d'être écrit : limite := position et position := 0
compactPour pouvoir recaler au début ce qui n'a pas été "consommé"
et se mettre à nouveau en position d'écrire dans le buffer :
remaining() donne la taille de la zone de travail,
c'est à dire le nombre de fois où on peut accéder (par get ou put)hasRemaining() retourne true si on peut accéderposition() donne la position couranteposition(int pos) fixe la position courantelimit() donne la limite actuellelimit(int pos) fixe la limiteclear() remet le buffer "comme neuf"putInt() écrit les 4 octets d'un int au début de la zone de travail et la réduitgetInt() lit les 4 octets d'un int au début de la zone de travail et la réduitputLong()
et getLong() pour 8 octets du long.
putShort()
et getShort() pour 2 octets du short.
Question : que fait buffer.putInt() en mémoire ?
L'ordre de représentation des entiers en mémoire peut être :
java.nio.ByteOrderL'ordre d'un ByteBuffer est par défaut
BigEndian à la création, mais peut être modifié
par order(ByteOrder)
Les autres tampons sont par défaut créés avec l'ordre natif
Un jeu de caractères est l'association d'un code (sur un ou plusieurs octets) pour chacun des caractères de ce jeu.
L'encodage est la traduction d'une suite de caractères (symboles) en une suite d'octets.
Le décodage est l'opération inverse, d'une suite d'octets en une suite de caractères.
Les deux opérations n'ont de sens que relativement à un jeu de caractères.
java.nio.charset.CharsetUn objet Charset représente un jeu de caractères :
Charset charset = Charset.forName("UTF-8"); ou
Charset UTF8 = StandardCharsets.UTF_8;
Des méthodes simples permettent d'encoder ou de décoder dans ce jeu de caractères :
ByteBuffer buffer = charset.encode(String s)CharBuffer cb = charset.decode(ByteBuffer buffer)Dans ce cours, on n'autorisera que ces deux méthodes.
D'autres méthodes plus complètes sont accessibles via les classes
CharsetEncoder et CharsetDecoder.
Selon le jeu de caractères utilisé et le caractère encodé, le nombre d'octets qui le représente est variable.
Attention : encode crée un nouveau buffer possiblement plus grand que la zone de travail !
Il faut être sûr d'avoir l'intégralité des octets pour pouvoir décoder.
FileChannel
Un FileChannel permet de lire et d'écrire des octets à partir d'un fichier.
Nous allons nous en servir pour nous familiariser avec les ByteBuffer en attendant d'avoir vu UDP et TCP.
Path path = Path.of("~/test.txt");
// ouverture en lecture
try (FileChannel fc = FileChannel.open(path, StandardOpenOption.READ)) {
....
}
// ouverture en écriture avec écrasement
try(FileChannel fc = FileChannel.open(path,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)){
....
}
fc.read(ByteBuffer buffer) lit depuis le fichier, via le
canal fc, des octets qu'elle
stocke dans la zone de travail de buffer.
La méthode
read retourne le nombre d'octets lus ou -1 si
le canal est fermé.
Même si le fichier contient plus d'octets que le ByteBuffer,
il n'y a pas de garantie que le buffer soit rempli intégralement.
Par défaut, la lecture est bloquante, i.e., read bloque jusqu'à avoir lu au moins un octet, s'il y a la place dans le buffer buffer.
L'appel à fc.write(ByteBuffer buffer) écrit les
buffer.remaining() octets de buffer
dans le canal fc.
Par défaut, l'écriture est bloquante, i.e., write
retourne quand tous les octets ont été écrits.
On cherche à réaliser un programme qui :
int au clavier et les écrit dans le fichier en BigEndian.Pour simplifier, dans un premier temps, on va écrire les int au fur et à mesure qu'ils sont lus.
NB : ce n'est pas efficace, il vaudrait mieux grouper les écritures.
var path = Path.of(args[1]);
var buffer = ByteBuffer.allocate(Integer.BYTES); // 4 bytes
try(var channel = FileChannel.open(path, StandardOpenOption.CREATE,
StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
var scanner = new Scanner(System.in)) {
while (scanner.hasNextInt()) {
buffer.putInt(scanner.nextInt());
buffer.flip();
channel.write(buffer);
buffer.clear();
}
}
Que faire de l'IOException levée par FileChannel.open et FileChannel.write?
Plus efficace : on prend un gros ByteBuffer que l'on remplit avec les
int. Quand il n'y a plus assez de place dans le ByteBuffer,
on écrit son contenu dans le fichier avant de continuer.
var path = Path.of(args[1]);
var buffer = ByteBuffer.allocate(BUFFER_SIZE);
try(var channel = FileChannel.open(path, StandardOpenOption.CREATE,
StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
var scanner = new Scanner(System.in)) {
while (scanner.hasNextInt()) {
if (buffer.remaining() < Integer.BYTES){
buffer.flip();
channel.write(buffer);
buffer.clear();
}
buffer.putInt(scanner.nextInt());
}
buffer.flip();
channel.write(buffer);
}