Les entrées/sorties avec le .NET Framework
Par Sacha Leroux , posté le 14/03/2008
Profil : Développeur | Niveau : Intermédiaire (200)
De tout temps et dans tout programme informatique, dès que les micro-ordinateurs ont été pourvus d'un système de fichiers, les développeurs ont créé des bibliothèques pour y accéder. Je vous propose donc dans cet article un voyage au cour des entrées/sorties avec VB.NET.
Introduction
Dans notre travail de développeur, nous avons régulièrement besoin d'accéder au système de fichiers des postes de travail et autres serveurs sur lesquels nos programmes s'exécutent.
La raison en est multiple ; pour certaines applications nous écrirons des logs, pour d'autres nous voudrons exporter ou importer des données, ou encore compresser des données pour les retourner à un tiers.
Pour nous aider dans cette tâche Microsoft a implémenté, au sein du .NET Framework une API complète qui rassemble toutes les classes nécessaires à la gestion des entrées/sorties. Cet API est regroupée au sein d'un espace de noms nommé System.IO.
Après avoir étudié le contenu de cet espace de noms ainsi que la façon de tirer partie des éléments du système de fichiers de nos PC, je vous propose, dans un second temps, de regarder plus spécifiquement la manière dont nous pouvons manipuler des fichiers. Pour ce faire, nous nous appuieront sur la création d'un outil permettant de compresser, décompresser et diviser des fichiers en plusieurs parties distinctes. Mais commençons d'abord par faire connaissance avec l'élément fondateur des entrées/sorties.
L'espace de noms System.IO
System.IO regroupe plusieurs classes, structures et enumérateurs. Il peut être vu comme regroupant les fonctionnalités principales suivantes dans la gestion des entrées/Sorties :
- Un jeu de classes permettant la navigation et la gestion du système de fichiers,
- Un jeu de classes utilisant les flux pour lire, écrire et compresser des fichiers,
- Des classes transverses comme les exceptions spécifiques.
Si System.IO est l'espace de noms principal, il possède des sous-espaces de noms dont :
- System.IO.Compression qui contient les classes de gestion de la compression,
- System.IO.IsolatedStorage qui contient les classes permettant la gestion d'un espace de stockage isolé,
- System.IO.Ports qui contient les classes nécessaires à la gestion du port série.
Maintenant que nous avons une vision globale, regardons plus en détail la façon dont la navigation dans le système de fichiers est mise à notre disposition.
La Navigation dans le système de fichiers Windows :
Plusieurs classes sont dédiées à la navigation dans le système de fichiers. Le tableau ci-dessous les répertorie et en donne une brève définition.
|
Classes |
Définitions |
|
Gestion des disques |
|
|
DriveInfo |
Gestion des informations d'un disque |
|
Gestion des répertoires |
|
|
Directory |
Gestion des répertoires du système de fichiers |
|
DirectoryInfo |
Informations spécifiques d'un répertoire |
|
Gestion des fichiers |
|
|
File |
Gestion des fichiers |
|
FileInfo |
Informations spécifiques d'un fichier |
|
Classes transverses |
|
|
Path |
Gestion des chemins d'accès |
|
FileSystemWatcher |
C'est un observateur qui permet de notifier des modifications dans le système de fichiers comme l'ajout d'un fichier dans un répertoire. |
C'est un observateur qui permet de notifier des modifications dans le système de fichiers comme l'ajout d'un fichier dans un répertoire.
La différence entre les classes suffixées par info et celles qui ne le sont pas, réside dans le fait que la deuxième classe n'effectue pas de vérifications des droits sur les fichiers à chaque opération ce qui est le cas de la première. Nous pouvons appliquer la règle suivante pour nous aider : pour une ou deux opérations mieux vaut utiliser les classes non suffixés par info pour de nombreuses opérations les autres classes sont plus appropriés.
Désormais passons à la pratique. Avant toute manipulation des entrées/sorties vous devez impérativement dans l'en-tête de vos fichiers sources, importer l'espace de noms System.IO comme suit :
1: Imports System.IO
Gestion des disques par l'exemple:
La récupération des disques disponibles sur le poste de travail est réalisée avec la classe DriveInfo de la façon suivante :
1: Dim internalDrives As DriveInfo() = DriveInfo.GetDrives()
Les informations d'un disque sont accessibles aussi avec la classe DriveInfo. La méthode suivante prend un disque en paramètre et affiche les informations de ce dernier dans la console. Nous pouvons comme cela connaître son nom via la propriété Name ou encore sa taille via la propriété TotalSize :
1: Public Shared Sub DisplayDriveInformation(ByVal aDrive As DriveInfo)2: If aDrive.IsReady Then3: Console.WriteLine("Le nom du disque est : {0}", aDrive.Name)4: Console.WriteLine("La taille Totale du disque est : {0}", aDrive.TotalSize)5: Console.WriteLine("L'espace disponible du disque est : {0}", aDrive.TotalFreeSpace)6: Console.WriteLine("Le format du disque est : {0}", aDrive.DriveFormat)7: End If8: End Sub
Gestion des répertoires par l'exemple:
La création d'un répertoire est certainement l'une des premières actions que l'on souhaite effectuer. Pour ce faire on utilise la méthode CreateDirectory de la classe Directory:
1: Directory.CreateDirectory("c:\Bewise\Articles\IO_VB_NET")
La suppression d'un répertoire, quand à elle, s'effectue via la méthode Delete de la classe Directory:
1: Directory.Delete("c:\Bewise\Articles\Programmez\IO_VB_NET")
Enfin nous pouvons lister les fichiers d'un répertoire via la méthode GetFiles de la classe Directory toujours:
1: Directory.GetFiles("c:\Bewise\Articles\Programmez\IO_VB_NET","*", SearchOption.AllDirectories)
La classe DirectoryInfo nous permet de récupérer diverses informations d'un répertoire. La méthode ci-dessous, qui prend en paramètre un répertoire, affiche son nom et sa date de création par exemple :
1: Public Shared Sub DisplayDirectoryInformation(ByVal oneDirectory As DirectoryInfo)2: Console.WriteLine("Le nom du répertoire est : {0}", oneDirectory.Name)3: Console.WriteLine("Date de création du répertoire : {0}", oneDirectory.CreationTime().ToShortDateString())4: Console.WriteLine("Nombre de fichier contenue dans le répertoire : {0}", oneDirectory.GetFiles("*.*", SearchOption.AllDirectories).Length)5: Console.WriteLine("Le répertoire parent est : {0} ", oneDirectory.Parent.Name)6: End Sub
Gestion des fichiers par l'exemple:
L'une des premières actions que l'on souhaite réaliser dans la manipulation d'un fichier c'est l'ouvrir. Vous trouverez ci-dessous le moyen d'ouvrir un fichier, s'il existe bien sûr:
1: If File.Exists("c:\test.txt") Then2: File.Open("c:\test.txt", FileMode.Open)3: End If
La création d'un fichier est aussi parmi les actions courantes ; elle est réalisée de cette façon :
1: Dim fstream As FileStream = File.Create("c:\test.txt")
Nous avons aussi la possibilité de copier un fichier via la méthode Copy:
1: File.Copy("c:\test.txt", "c:\test2.txt")
Et enfin, une fois toutes ces manipulations, il ne nous reste plus qu'à savoir supprimer un fichier comme présenté ci-dessous:
1: File.Delete("c:\text2.txt")
Nous venons de voir comment manipuler un fichier physique. Le chapitre suivant nous présente, pour sa part, la manière dont nous pouvons les modifier et interagir avec leur contenu.
Analyse des mécanismes de création et de modification des fichiers :
Nous avons vu que la gestion des fichiers démarre à l'aide de la classe File qui met à notre disposition des méthodes statiques pour manipuler des fichiers physiques. Mais qu'en est-il lorsque nous souhaitons utiliser le contenu du fichier ? Et notamment le contenu de différents types comme du texte, du binaire ? Pour répondre à ce besoin, le .NET Framework met à notre disposition les flux de données.
Nous en avons déjà un aperçu, dans l'exemple sur la création d'un fichier, nous utilisons un FileStream en retour de la méthode File.Create. Le FileStream est en réalité un flux de données correspondant à un fichier.
Je vous propose dans ce chapitre de vous présenter ces flux (Stream en anglais) et les moyens fournis pour les manipuler.
Définition, type et organisation des flux
Un flux est une séquence d'octets qui peut alors être représenté sous la forme d'un tableau d'octets comme ci-dessous :
| 01110001 | 01110001 | 01110001 | 01110001 | 01110001 | 01110001 | 01110001 | 01110001 | 01110001 |
Les Flux dans System.IO
Il existe plusieurs types de flux dans l'espace de noms System.IO. Le tableau suivant les décrit :
| Nom de la classe | Description |
| Stream | Classe abstraite dont tous les flux héritent |
| FileStream | Flux de données représentant un fichier physique (image, texte, xml, etc..) |
| MemoryStream | Flux de données en mémoire |
| BufferedStream | Flux de données représentant un Buffer (stockage temporaire d'octet) |
| DeflateStream | Flux compressé de type Deflate |
| GZipStream | Flux compressé de type GZip |
Tous ces flux ont un comportement commun qui vient de la classe abstraite Stream. Le schéma suivant présente la hiérarchie basée sur cette classe :
La classe Stream nous fournit des méthodes permettant l'exploitation de la séquence d'octets qu'elle contient. Ces dernières permettent de naviguer, écrire et lire dans la séquence. Toutes ces méthodes sont implémentées dans les classes concrètes.
ReadByte : Lit un octet et avance la position en conséquence
WriteByte : écrit un octet et avance la position en conséquence
Comme nous pouvons le constater, nous avons à notre disposition le moyen d'écrire ou lire dans le flux mais cela reste complexe car nous manipulons des octets on peut alors se demander comment écrire simplement une information de type texte ?
Fort heureusement pour nous le .NET Framework met à notre disposition de classes dédiées à l'écriture et la lecture dans les flux que nous appelons des Reader et des Writer. Etudions ces classes plus en détail à présent.
Les Reader et Les writer
Le principe des Reader et des Writer sont toujours les mêmes :
· Il nécessite un flux,
· Il s'applique sur ce flux en les prenant en paramètre de leur constructeur.
Nous nous intéresserons ici au reader et writer associés à l'écriture et la lecture de fichier. Nous pouvons en recenser 2 importants :
· Le StreamReader,
· Le StreamWriter.
StreamReader est une classe facilitant la lecture de fichier texte à partir d'un FileStream.
Elle permet donc de lire un fichier Octet par Octet mais surtout de le lire ligne par ligne en considérant chaque ligne comme une chaîne de caractère. L'exemple suivant illustre ce fonctionnement.
Tout d'abord la création d'un flux de données correspondant au fichier « c:\test.txt »
1: Dim fileText As IO.FileStream = File.OpenRead("c:\test.txt")
Ensuite, création d'un StreamReader qui s'appuie sur ce flux de données et nous permettra une lecture simplifiée du fichier :
1: Dim fileText As IO.FileStream = File.OpenRead("c:\test.txt")
Enfin nous pouvons lire les informations contenus dans le fichier en s'appuyant sur le StreamReader. Dans l'exemple ci-dessous, La lecture se fait ligne par ligne via la méthode ReadLine. A chaque lecture de ligne, on enrichit un StringBuilder. La fin du flux est identifiée par la propriété EndOfStream:
1: Dim sb As New StringBuilder2: While Not reader.EndOfStream3: sb.Append(reader.ReadLine())4: End While
Il est nécessaire de fermer le StreamReader, après traitements, pour ne pas bloquer les ressources en mémoire, c'est le rôle de la méthode Close.
1: reader.Close()
A l'image du StreamReader, le StreamWriter a pour rôle de faciliter l'écriture d'un fichier texte à partir d'un FileStream. Il permet d'écrire une chaîne de caractères ou encore une ligne complète avec un retour à la ligne. L'exemple suivant illustre ce fonctionnement.
Comme vous l'avez compris les deux premières étapes sont la création d'un flux de données sur le fichier et la création d'un writer basé sur ce flux :
1: Dim fileText As IO.FileStream=File.Open("c:\Nouveau.txt", FileMode.Create)2: Dim writer As StreamWriter = New StreamWriter(fileText)
Ensuite nous utilisons la méthode WriteLine du writer pour écrire une chaîne de caractères :
1: writer.WriteLine("Hello!")
Le StreamWriter doit être fermé via la méthode Close pour libérer les ressources et le fichier.
1: writer.Close()
Il existe de nombreux autres reader et writer comme par exemple TextReader, TextWriter ou encore le BinaryReader et le BinaryWriter que nous verrons plus loin.
Pour illustrer ce que nous avons vu jusqu'alors, je vous propose de créer un outil de compression.
Création du programme Bewise.compressor :
Les fonctionnalités de notre application sont :
· La compression du flux de données dans un fichier physique avec comme extension « bew » via un flux de données GZipStream.
· La scission du fichier compressé en plusieurs parties (la taille sera fixée à 1Mo ).
· La décompression du flux de données en récupérant les différentes parties s'il y a lieu.
L'IHM de notre outil
Comme le montre l'écran suivant l'interface utilisateur de notre application sera réalisée en windows forms et permettra le paramétrage de l'outil.
La compression
On crée en premier lieu une copie du fichier source identifié par la variable FilePathSource vers le fichier à compresser identifié par la variable FilePathToCompress
1: File.Copy(FilePathSource, FilePathToCompress)
On crée ensuite un flux de données, qui sera compressé, identifié par la variable fileCompressComplete
1: Dim fileCompressComplete As FileStream = File.Open(FilePathToCompress, FileMode.OpenOrCreate, FileAccess.ReadWrite)
Ensuite, afin de réaliser la compression, on utilise un flux de données GZipStream qui s'appuiera sur le FileStream précédent.
1: Dim gzipStream As IO.Compression.GZipStream = New IO.Compression.GZipStream(fileCompressComplete, Compression.CompressionMode.Compress)2: gzipStream.Close()
Une fois ces instructions exécutées, le fichier identifié par la variable FilePathToCompress avec l'extension « Bew » sera compressé. Il nous reste plus qu'à scinder ce fichier en plusieurs parties.
Le découpage du fichier
L'algorithme utilisé est le suivant :
· Création des flux vers le fichier source à scinder
· Récupération de la taille de chaque partie
· Parcourt séquentiel du fichier source, récupération des octets équivalents à la taille désirée et création du nouveau fichier qui contiendra les données lues précédemment.
· On itère l'algorithme précédent pour chacun des morceaux de fichier à créer.
On crée donc un flux vers le fichier compressé identifié par la variable FilePathToCompress
1: Dim fileToSplit As IO.FileStream = File.Open(FilePathToCompress, FileMode.OpenOrCreate, FileAccess.Read)
On crée ensuite un Reader de type binary qui s'appuie sur le FileStream précédent
1: Dim reader As BinaryReader = New BinaryReader(fileToSplit)
On évalue la taille restante du fichier lu pour définir le nombre de fichier à créer
1: Dim sizeRestant As Int32 = reader.BaseStream.Length - reader.BaseStream.Position
On crée une règle de nommage pour les morceaux de fichier, ils correspondront donc à cette règle « NomDuFichier ».Part « count ».bew comme implémenté ci-dessous :
1: Dim filePart As String = ".Part"2: Dim count As Int32 = -1
Tant qu'il y a des données à lire, nous créons un fichier (partie du fichier initiale) et nous écrivons les octets lus à l'aide d'un BinaryWriter. Enfin, nous réaffectons la taille restante en fonction du nombre d'octets lus.
1: While sizeRestant > 02:3: If sizeRestant > size Then4: sizeRestant = size5: End If6:7: Dim value As Byte() = reader.ReadBytes(sizeRestant)8:9: 'création du fichier10: count += 111: Dim FileName As String = filePart & count.ToString() + ".bew"12: Dim fileToCreate As IO.FileStream = File.Open(Path.Combine("c:\", FileName), FileMode.CreateNew, FileAccess.Write)13: Dim writer As New BinaryWriter(fileToCreate)14: writer.Write(value)15: writer.Flush()16:17: writer.Close()18: fileToCreate.Close()19:20: sizeRestant = reader.BaseStream.Length - reader.BaseStream.Position21: End While22:23: reader.Close()24: fileToSplit.Close()
La décompression
Un prérequis au bon fonctionnement de notre application est que tous les morceaux de fichier devront être dans un même répertoire. Ils seront identifiés par leur nom.
Première étape de la décompression : le rassemblement des morceaux de fichier si nécessaire
On crée une variable internalFile correspondant au fichier une fois décompressé
Dim internalFile As String = Path.Combine(FilePath, fileName)
Nous récupérons ensuite la liste des parties de fichiers, si nécessaire, en utilisant un filtre correspondant à la règle de nommage définie plus haut
1: Dim files As String() = Directory.GetFileSystemEntries(FilePath, "*part*.bew")2: Array.Sort(files)
Puis on effectue le rassemblement des morceaux de fichier dans un seul et même fichier
1: For index As Int32 = 0 To files.Length - 12:3: 'Création d'un flux sur un fichier données4: Dim filePart As FileStream = File.Open(files(index), FileMode.Open, FileAccess.Read)5: Dim reader As New BinaryReader(filePart)6: Dim value As Byte() = reader.ReadBytes(filePart.Length)7:8: 'Ouverture du fichier à décompresser9: Dim FileToDecompress As FileStream = File.Open(internalFile, FileMode.Append, FileAccess.Write)10: Dim writer As New BinaryWriter(FileToDecompress)11: writer.Write(value)12:13: 'fermeture des flux14: reader.Close()15: writer.Close()16: filePart.Close()17:18: Next
Seconde étape la décompression : la décompression du fichier réuni
Pour décompresser le fichier, nous utilisons un flux de données GZipStream en mode décompression
1: Dim FileComplete As FileStream = File.Open(Path.Combine(FilePath, fileName), FileMode.OpenOrCreate, FileAccess.ReadWrite)2:3: Dim gzipStream As IO.Compression.GZipStream = New GZipStream(FileComplete, Compression.CompressionMode.Decompress)4: gzipStream.Close()
Nous avons donc bien réalisé notre outil en nous appuyant entièrement sur les classes fournies par le .NET Framework et plus particulièrement ceux de l'espace de noms System.IO
CONCLUSION
L'espace de nom principal pour la gestion des entrées/sorties avec le .NET Framework, se nomme System.IO. On peut diviser ce dernier en deux grands pôles. Tout d'abord, un modèle objet permettant la gestion du système de fichiers : disque, répertoire et fichier. Puis un mécanisme de flux de données permettant de manipuler du contenu.
Les informations des flux sont gérées via des Reader et Writer facilitant la lecture et l'écriture dans ces flux.
Les flux de données sont aussi utilisés dans la gestion des communications TCP, des connexions via des sockets ou encore le pilotage du port série. Cela fera certainement l'objet d'un autre article.



