Dateiverarbeitung in Echtzeit mit Deno und WebSockets
Klicken Sie im Browser auf eine Schaltfläche, lassen Sie Deno eine Datei auf Ihrem Rechner hashen
und empfangen Sie ihre Byteanzahl und SHA-256-Prüfsumme per WebSocket. Dieses Beispiel liest eine
festgelegte Datei, sample.txt, und akzeptiert bis zu
1 MiB. Der Browser sendet einen Befehl und empfängt das Ergebnis; er lädt die Datei nicht hoch.
Die Anleitung verwendet Deno 2.9.6 und Chromium unter Linux sowie Bash für die Terminalbefehle.
Falls Sie die Laufzeitumgebung benötigen, nutzen Sie die
Installationsanleitung für Deno und prüfen Sie Ihre Version mit
deno --version. Es müssen keine Pakete installiert werden.
Warum Deno?
Deno stellt den HTTP-Server, das WebSocket-Upgrade, die Datei-APIs und Web Crypto bereit, die hier zum Einsatz kommen. Mit den Berechtigungsflags gewähren wir der Anwendung Lesezugriff auf zwei Dateien und Netzwerkzugriff auf eine Loopback-Adresse und einen Port. Das Beispiel benötigt keinen Schreibzugriff und keinen Zugriff auf Unterprozesse oder Umgebungsvariablen.
Einen WebSocket-Server einrichten
Fügen Sie Folgendes in Bash ein, ausgehend von einem Verzeichnis Ihrer Wahl. Es erstellt ein neues
Projekt und eine Datei mit drei Bytes, die abc enthält, ohne abschließenden
Zeilenumbruch. Falls deno-checksum bereits existiert, schlägt die Erstellung fehl,
ohne es zu überschreiben. Die Klammern sorgen dafür, dass Ihr Terminal im übergeordneten
Verzeichnis bleibt.
(
mkdir deno-checksum &&
cd deno-checksum &&
printf 'abc' > sample.txt
)
Speichern Sie Folgendes als deno-checksum/server.ts. Es liefert die Browserseite unter
/ aus und führt für Anfragen an /ws
mit der WebSocket-API von Deno ein Upgrade durch.
Nur der exakte Textbefehl hash startet die Verarbeitung. Keine Nachricht
kann einen Dateipfad auswählen.
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() kann weniger Bytes als angefordert
zurückgeben. Daher läuft die Schleife weiter, bis EOF erreicht oder der Puffer voll ist.
using schließt die Datei beim Verlassen der Funktion, auch im Fehlerfall.
Belassen Sie sample.txt als gewöhnliche lokale Datei und ändern Sie sie während
einer Anfrage nicht: Dies ist kein Snapshot des Dateisystems.
Der Lesepuffer fasst höchstens 1 MiB plus ein Byte, selbst wenn die Datei wächst. Diese Grenze ist
wichtig, weil crypto.subtle.digest() seine Eingabe im Arbeitsspeicher entgegennimmt,
ohne sie zu streamen. Das bedeutet nicht, dass der gesamte Server nur 1 MiB Arbeitsspeicher nutzt.
Client implementieren
Speichern Sie dies als deno-checksum/index.html. Die Schaltfläche bleibt deaktiviert, bis der
Socket geöffnet ist, und während eine Anfrage aussteht. Ergebnisse ersetzen die vorherige Anzeige;
es wird nichts auf der Festplatte gespeichert.
<!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>
Den Server starten
Führen Sie im selben übergeordneten Verzeichnis Folgendes aus:
(
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 verhindert, dass die Deno-Konfiguration eines übergeordneten Projekts
übernommen wird. --no-prompt führt bei fehlenden Berechtigungen zu einem Fehler,
statt weitergehenden Zugriff anzufordern. Die Leseberechtigung gilt für die Seite, die beim Start
geladen wird, und die Beispieldatei, die bei jeder Anfrage geöffnet wird. Der Netzwerkzugriff ist
auf 127.0.0.1:8000 beschränkt; der Server bindet sich auch explizit an diese Adresse.
Öffnen Sie http://127.0.0.1:8000/ und wählen Sie Hash file.
Verwenden Sie genau diese Adresse, statt die HTML-Datei direkt zu öffnen oder
localhost zu verwenden: Der WebSocket-Handshake prüft den Ursprung der Seite.
Sobald der Status Done. lautet,
sollte das Ergebnis so aussehen:
{
"type": "result",
"bytes": 3,
"sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
}
Prüfen Sie dieselben Bytes unabhängig davon in einem weiteren Terminal im übergeordneten
Verzeichnis mit dem Linux-Befehl sha256sum:
sha256sum deno-checksum/sample.txt
Das erste Feld der Ausgabe sollte mit sha256 übereinstimmen. Ein anderer
Wert kann bedeuten, dass Ihr Editor einen Zeilenumbruch hinzugefügt hat. Ändern Sie die Datei nach
Abschluss der Anfrage und wählen Sie erneut Hash file,
um den neuen Inhalt zu hashen. Auch leere Dateien und Binärdaten funktionieren; der Name
.txt löst keine Textdecodierung aus. Beenden Sie den Server anschließend
mit Ctrl+C.
Falls beim Start eine belegte Adresse gemeldet wird, wählen Sie einen freien Port und ändern Sie
sowohl const port als auch den Port in --allow-net,
bevor Sie es erneut versuchen. Fehlt index.html, wird der Start gestoppt.
Fehlt dagegen sample.txt, erscheint im Browser
sample.txt was not found.; stellen Sie die Datei wieder her
und klicken Sie erneut. Eine Datei, die größer als der Grenzwert ist, führt zu
sample.txt exceeds 1 MiB., ohne eine Prüfsumme zurückzugeben.
Den WebSocket-Lebenszyklus verstehen
Der Client sendet nur, solange readyState den Wert
WebSocket.OPEN hat. Der Server sendet zuerst started,
dann entweder result oder error.
Ein gestarteter Vorgang belegt nicht, dass die Datei erfolgreich gelesen wurde, und die
Statusmeldungen sind keine prozentualen Fortschrittsanzeigen.
Wenn der Browser während des Hashens die Verbindung trennt, wird der begrenzte Vorgang abgeschlossen.
Der Server verwirft seine Antwort, sobald der Socket nicht mehr geöffnet ist. Der Block
finally gibt das Belegt-Flag für nachfolgende Anfragen frei.
Laden Sie die Seite neu, um die Verbindung wiederherzustellen und eine neue Prüfsumme anzufordern.
Es gibt keinen automatischen Wiederholungsversuch, keinen Job-Verlauf und keine Wiederherstellung
eines verlorenen Ergebnisses.
Bewährte Verfahren für die Sicherheit
Nutzen Sie diese Demo nur lokal. Die Bindung an Loopback sowie die Prüfung des HTTP-Hosts und des WebSocket-Ursprungs schränken den Zugriff von Browserseiten ein. Die Ursprungsprüfung ist keine Authentifizierung: Ein lokaler Client außerhalb des Browsers kann diesen Header selbst senden.
1. Eingabevalidierung
Das Protokoll akzeptiert exakt hash. JSON, Pfade, Binärnachrichten und
anderer Text erhalten eine Fehlermeldung, ohne einen Dateilesevorgang zu starten. Der Dateiname
stammt ausschließlich aus dem Servercode. Fehler, die an die Seite gesendet werden, verwenden
feste Meldungen statt Dateisystempfaden oder Stacktraces.
Um die Lesebeschränkung von Deno zu sehen, stoppen Sie den Server und entfernen Sie
,./sample.txt aus der Leseberechtigung.
Starten Sie ihn erneut und klicken Sie auf die Schaltfläche: Die Seite wird geladen, aber die
Anfrage meldet Read permission for sample.txt was denied..
Stellen Sie die Berechtigung wieder her, bevor Sie fortfahren. Wenn die Netzwerkberechtigung
entfernt wird, kann der Server überhaupt keine Verbindungen annehmen.
2. Ratenbegrenzung
Dieses Beispiel erlaubt über alle Verbindungen hinweg einen aktiven Hash-Vorgang. Eine zweite Anfrage während dieses Vorgangs erhält Server is busy. Try again.. Auch der Browser deaktiviert während der Wartezeit seine Schaltfläche, sodass wiederholte Klicks keine Verarbeitung in die Warteschlange stellen können.
Eine Begrenzung gleichzeitiger Vorgänge ist keine Ratenbegrenzung. Ein Client kann eine weitere Anfrage senden, sobald ein Hash-Vorgang abgeschlossen ist, und der Server begrenzt die Anzahl der Verbindungen nicht. Ein öffentlicher Dienst bräuchte zusätzlich zu diesen Prüfungen Authentifizierung, Autorisierung sowie Begrenzungen für Anfragen und Verbindungen.
3. Begrenzung der Nachrichtengröße
Der akzeptierte Befehl umfasst nur vier ASCII-Bytes, und Dateiinhalte werden nie über den Socket übertragen. Andere Befehle werden jedoch erst abgewiesen, nachdem eine Nachricht eingetroffen ist. Das begrenzt nicht die Pufferung eingehender WebSocket-Daten. Die WebSocket-API bietet keine Backpressure. Das Leselimit und das Belegt-Flag machen diesen Server daher nicht für nicht vertrauenswürdigen Datenverkehr geeignet.
Entscheiden Sie, ob Sie WebSockets benötigen
Für eine einzelne Prüfsumme kann eine HTTP-Anfrage dasselbe Ergebnis mit weniger Aufwand für die Verbindungsverwaltung liefern. WebSockets werden nützlich, wenn eine bereits verbundene Seite aufeinanderfolgende Statusmeldungen oder Ergebnisse benötigt. Dieses kleine Beispiel zeigt diesen Austausch, ohne Uploads, eine beliebige Dateiauswahl oder einen Dienst für Hintergrundjobs hinzuzufügen.
