Traitement de fichiers en temps réel avec Deno et WebSockets
Cliquez sur un bouton dans un navigateur, laissez Deno hacher un fichier sur votre machine, puis
recevez sa taille en octets et sa somme de contrôle SHA-256 via WebSocket. Cet exemple lit un seul
fichier fixe, sample.txt, et accepte jusqu’à 1 MiB. Le navigateur envoie une commande et reçoit le
résultat ; il ne téléverse pas le fichier.
Ce tutoriel utilise Deno 2.9.6 et Chromium sous Linux, avec Bash pour les commandes du terminal.
Consultez le guide d’installation de Deno si vous avez besoin de
l’environnement d’exécution, puis vérifiez votre version avec deno --version. Aucun paquet n’est à installer.
Pourquoi Deno ?
Deno fournit le serveur HTTP, la mise à niveau WebSocket, les API de fichiers et Web Crypto utilisés ici. Ses options de permission nous permettent d’accorder à l’application la lecture de deux fichiers et l’accès réseau à une seule adresse de bouclage et un seul port. L’exemple ne nécessite aucun accès en écriture, aux sous-processus ni à l’environnement.
Mise en place d’un serveur WebSocket
Depuis le répertoire de votre choix, collez ceci dans Bash. Cela crée un nouveau projet et un fichier
de trois octets contenant abc, sans saut de ligne final. Si deno-checksum existe déjà, la création
échoue sans l’écraser. Les parenthèses maintiennent votre terminal dans le répertoire parent.
(
mkdir deno-checksum &&
cd deno-checksum &&
printf 'abc' > sample.txt
)
Enregistrez le code suivant sous deno-checksum/server.ts. Il sert la page du navigateur à / et met à
niveau les requêtes reçues sur /ws à l’aide de l’API WebSocket de Deno.
Seule la commande texte exacte hash lance le traitement. Aucun message ne peut choisir un chemin
de fichier.
const hostname = '127.0.0.1'
const port = 8000
const origin = `http://${hostname}:${port}`
const maxFileBytes = 1024 * 1024
const html = await Deno.readTextFile('./index.html')
let busy = false
class FileProblem extends Error {}
async function hashSample(): Promise<{ bytes: number; sha256: string }> {
using file = await Deno.open('./sample.txt', { read: true })
if (!(await file.stat()).isFile) {
throw new FileProblem('Use a regular file for sample.txt.')
}
// One extra byte distinguishes an exact-limit file from an oversized file.
const buffer = new Uint8Array(maxFileBytes + 1)
let bytes = 0
while (bytes < buffer.length) {
const count = await file.read(buffer.subarray(bytes))
if (count === null) break
bytes += count
}
if (bytes > maxFileBytes) {
throw new FileProblem('sample.txt exceeds 1 MiB.')
}
const digest = await crypto.subtle.digest('SHA-256', buffer.subarray(0, bytes))
const sha256 = Array.from(new Uint8Array(digest), (byte) =>
byte.toString(16).padStart(2, '0'),
).join('')
return { bytes, sha256 }
}
Deno.serve({ hostname, port, onListen: () => console.log(`Open ${origin}/`) }, (req) => {
const url = new URL(req.url)
if (req.method !== 'GET' || url.origin !== origin) {
return new Response('Not found', { status: 404 })
}
if (url.pathname === '/') {
return new Response(html, {
headers: { 'content-type': 'text/html; charset=utf-8' },
})
}
if (url.pathname !== '/ws') {
return new Response('Not found', { status: 404 })
}
if (req.headers.get('origin') !== origin) {
return new Response('Forbidden', { status: 403 })
}
if (req.headers.get('upgrade')?.toLowerCase() !== 'websocket') {
return new Response('WebSocket required', { status: 426 })
}
const { socket, response } = Deno.upgradeWebSocket(req)
function send(message: object): void {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(message))
}
}
socket.addEventListener('message', async (event) => {
if (event.data !== 'hash') {
send({ type: 'error', message: 'Send the text command hash.' })
return
}
if (busy) {
send({ type: 'error', message: 'Server is busy. Try again.' })
return
}
busy = true
try {
send({ type: 'started' })
const result = await hashSample()
send({ type: 'result', ...result })
} catch (error) {
const message = error instanceof FileProblem
? error.message
: error instanceof Deno.errors.NotFound
? 'sample.txt was not found.'
: error instanceof Deno.errors.NotCapable || error instanceof Deno.errors.PermissionDenied
? 'Read permission for sample.txt was denied.'
: 'Could not hash sample.txt.'
send({ type: 'error', message })
} finally {
busy = false
}
})
return response
})
file.read() peut renvoyer moins d’octets
que demandé ; la boucle continue donc jusqu’à EOF ou jusqu’à ce que le tampon soit plein. using
ferme le fichier à la sortie de la fonction, y compris en cas d’échec. Conservez sample.txt comme un
fichier local ordinaire et ne le modifiez pas pendant une requête : il ne s’agit pas d’un instantané
du système de fichiers.
Le tampon de lecture contient au maximum 1 MiB plus un octet, même si le fichier grossit. Cette
limite est importante, car crypto.subtle.digest() reçoit ses données d’entrée en mémoire,
sans les traiter en flux. Elle ne signifie pas que l’ensemble du serveur n’utilise que 1 MiB de
mémoire.
Implémentation du client
Enregistrez ceci sous deno-checksum/index.html. Le bouton reste désactivé jusqu’à l’ouverture du socket et tant
qu’une requête est en attente. Les résultats remplacent l’affichage précédent ; rien n’est enregistré
sur le disque.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="data:," />
<title>Deno file checksum</title>
<style>
pre { white-space: pre-wrap; overflow-wrap: anywhere; }
</style>
</head>
<body>
<h1>Hash sample.txt</h1>
<button type="button" disabled>Hash file</button>
<p role="status">Connecting…</p>
<pre aria-label="Checksum result"></pre>
<script type="module">
const button = document.querySelector('button')
const status = document.querySelector('[role="status"]')
const result = document.querySelector('pre')
if (!(button instanceof HTMLButtonElement) || !status || !result) {
throw new Error('Missing page controls')
}
const socket = new WebSocket(`ws://${location.host}/ws`)
let pending = false
socket.addEventListener('open', () => {
status.textContent = 'Ready.'
button.disabled = false
})
button.addEventListener('click', () => {
if (pending || socket.readyState !== WebSocket.OPEN) return
pending = true
button.disabled = true
result.textContent = ''
status.textContent = 'Waiting for the server…'
socket.send('hash')
})
socket.addEventListener('message', (event) => {
const message = JSON.parse(event.data)
if (message.type === 'started') {
status.textContent = 'Hashing sample.txt…'
return
}
if (message.type === 'result') {
result.textContent = JSON.stringify(message, null, 2)
status.textContent = 'Done.'
} else if (message.type === 'error') {
status.textContent = message.message
}
pending = false
button.disabled = false
})
socket.addEventListener('error', () => {
status.textContent = 'Connection error. Check the server.'
button.disabled = true
})
socket.addEventListener('close', () => {
status.textContent = 'Disconnected. Reload to reconnect.'
button.disabled = true
pending = false
})
</script>
</body>
</html>
Exécution du serveur
Depuis le même répertoire parent, exécutez :
(
cd deno-checksum &&
deno run --no-config --no-prompt \
--allow-net=127.0.0.1:8000 \
--allow-read=./index.html,./sample.txt server.ts
)
--no-config évite d’hériter de la configuration Deno d’un projet englobant. --no-prompt fait échouer
les permissions manquantes au lieu de demander un accès plus large. L’autorisation de lecture couvre
la page chargée au démarrage et le fichier d’exemple ouvert à chaque requête. L’accès réseau est
limité à 127.0.0.1:8000 ; le serveur se lie aussi explicitement à cette adresse.
Ouvrez http://127.0.0.1:8000/ et sélectionnez Hash file. Utilisez
exactement cette adresse plutôt que d’ouvrir directement le fichier HTML ou d’utiliser localhost :
la négociation WebSocket vérifie l’origine de la page. Lorsque le statut devient
Done., le résultat devrait être :
{
"type": "result",
"bytes": 3,
"sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
}
Dans un autre terminal, depuis le répertoire parent, vérifiez indépendamment les mêmes octets avec la
commande Linux sha256sum :
sha256sum deno-checksum/sample.txt
Son premier champ devrait correspondre à sha256. Une valeur différente peut signifier que votre
éditeur a ajouté un saut de ligne. Modifiez le fichier une fois la requête terminée, puis
sélectionnez de nouveau Hash file pour hacher son nouveau contenu.
Les fichiers vides et les données binaires fonctionnent également ; le nom .txt n’entraîne
aucun décodage de texte. Arrêtez le serveur avec Ctrl+C une fois que vous avez terminé.
Si le démarrage signale une adresse occupée, choisissez un port libre et modifiez à la fois
const port et le port --allow-net avant de réessayer. L’absence de index.html empêche le
démarrage. L’absence de sample.txt produit en revanche
sample.txt was not found. dans le navigateur ; restaurez le fichier et cliquez de
nouveau. Un fichier dépassant la limite produit
sample.txt exceeds 1 MiB. sans renvoyer de somme de contrôle.
Comprendre le cycle de vie WebSocket
Le client n’envoie de messages que lorsque readyState vaut WebSocket.OPEN. Le serveur envoie d’abord
started, puis result ou error. Une opération démarrée ne prouve pas que le fichier a
été lu avec succès, et les messages de statut ne sont pas des mises à jour de progression en
pourcentage.
Si le navigateur se déconnecte pendant le hachage, l’opération bornée se termine et le serveur
abandonne sa réponse dès que le socket n’est plus ouvert. Le bloc finally libère l’indicateur
d’occupation pour les requêtes suivantes. Rechargez la page pour vous reconnecter et demander une
nouvelle somme de contrôle. Il n’existe ni nouvelle tentative automatique, ni historique des tâches,
ni récupération d’un résultat perdu.
Bonnes pratiques de sécurité
Gardez cette démo en local. La liaison à l’adresse de bouclage et la vérification de l’hôte HTTP et de l’origine WebSocket restreignent l’accès depuis les pages du navigateur. La vérification de l’origine n’est pas une authentification : un client local autre qu’un navigateur peut envoyer lui-même cet en-tête.
1. Validation des entrées
Le protocole accepte exactement hash. Le JSON, les chemins, les messages binaires et tout autre
texte reçoivent une erreur sans déclencher de lecture de fichier. Le nom de fichier provient
uniquement du code du serveur. Les erreurs envoyées à la page utilisent des messages fixes plutôt que
des chemins du système de fichiers ou des traces de pile.
Pour observer la restriction de lecture de Deno, arrêtez le serveur et retirez ,./sample.txt de
l’autorisation de lecture. Redémarrez et cliquez sur le bouton : la page se charge, mais la requête
signale Read permission for sample.txt was denied.. Rétablissez l’autorisation avant de continuer.
Retirer l’autorisation réseau empêche totalement le serveur d’écouter.
2. Limitation du débit
Cet exemple autorise un seul hachage actif pour l’ensemble des connexions. Une deuxième requête envoyée pendant cette opération reçoit Server is busy. Try again.. Le navigateur désactive aussi son bouton pendant l’attente, si bien que des clics répétés ne peuvent pas mettre de travail en file d’attente.
Une limite de concurrence n’est pas une limite de débit. Un client peut envoyer une autre requête dès qu’un hachage se termine, et le serveur ne plafonne pas les connexions. Un service public nécessiterait une authentification, une autorisation ainsi que des limites de requêtes et de connexions, en plus de ces vérifications.
3. Limites de taille des messages
La commande acceptée ne fait que quatre octets ASCII, et le contenu du fichier ne transite jamais par le socket. Cependant, le rejet des autres commandes a lieu après l’arrivée d’un message. Il ne limite pas la mise en mémoire tampon WebSocket entrante. L’API WebSocket ne gère pas la contre-pression, donc la limite de lecture du fichier et l’indicateur d’occupation ne font pas de ce serveur un serveur adapté au trafic non fiable.
Déterminer si vous avez besoin des WebSockets
Pour une seule somme de contrôle, une requête HTTP peut renvoyer le même résultat avec moins de gestion de connexion. Les WebSockets deviennent utiles lorsqu’une page déjà connectée a besoin de messages de statut ou de résultats successifs. Ce petit exemple illustre cet échange sans ajouter de téléversements, de sélection arbitraire de fichiers ni de service de tâches en arrière-plan.
