Die ganze Klasse malt gleichzeitig an einem Bild: Jeder Pixel, den jemand setzt, erscheint sofort bei allen anderen. Dafür brauchst du WebSockets, Verbindungen, die offen bleiben und in beide Richtungen funktionieren. Die Bibliothek faye-websocket kümmert sich um die Technik dahinter; du kümmerst dich um das, worauf es ankommt: wer verbunden ist, wer welche Nachricht bekommt, und dass der Server keiner Nachricht blind glaubt.
Bei HTTP fragt immer der Browser, und der Server antwortet. Ein Server kann nicht von sich aus etwas schicken. Für eine Pixelwand ist das ein Problem: Wenn jemand anderes malt, soll dein Browser das sofort erfahren. Er könnte jede halbe Sekunde fragen, ob es etwas Neues gibt. Bei 30 Leuten wären das 60 Anfragen pro Sekunde, und fast alle bekämen die Antwort »nichts Neues«.
Eine WebSocket-Verbindung beginnt wie eine HTTP-Anfrage, bleibt dann aber offen. Danach können beide Seiten jederzeit Nachrichten schicken, in beide Richtungen. So funktionieren Chats, Online-Spiele und gemeinsame Dokumente.
In Ein eigener Webserver hast du HTTP von Hand gesprochen. WebSockets von Hand wären noch einmal so viel Arbeit: ein Handshake mit einem Hashwert und danach ein eigenes Format aus Bytes für jede Nachricht. Das übernimmt hier die Bibliothek faye-websocket, so wie in echten Projekten.
Lege einen neuen Ordner an und öffne ihn mit StrgK StrgO:
mkdir /workspace/pixelwand
Installiere dann die drei Gems, die der Server braucht:
gem install faye-websocket puma rackup
Lege server.rb an:
require 'faye/websocket' require 'rackup' # Das Programm hinter dem Server: Für jede Anfrage bekommt es env (Methode, # Pfad, Header …) und gibt Status, Header und Inhalt zurück. App = lambda do |env| if Faye::WebSocket.websocket?(env) # Der Browser möchte eine WebSocket-Verbindung. Faye erledigt den # Handshake und liest und schreibt die Nachrichten. ws = Faye::WebSocket.new(env) # Jede Nachricht geht mit »Echo:« davor zurück ws.on(:message) do |event| puts "Empfangen: #{event.data}" ws.send("Echo: #{event.data}") end ws.rack_response else # Jede andere Anfrage bekommt die Webseite [200, { 'content-type' => 'text/html; charset=utf-8' }, [File.read(File.join(__dir__, 'index.html'))]] end end puts "Der Server wartet auf Port 8000 ..." Rackup::Server.start(app: App, Host: '0.0.0.0', Port: 8000, server: 'puma', Silent: true)
App ist ein Rack-Programm: Für jede Anfrage bekommt es env, einen Hash mit Methode, Pfad und Headern, und gibt drei Dinge zurück, Status, Header und Inhalt, wie die Antwort deines eigenen Webservers.Faye::WebSocket.websocket?(env) erkennt eine Anfrage, die eine WebSocket-Verbindung öffnen will. Dann übernimmt Faye::WebSocket.new(env) den Handshake; ws steht für diese eine Verbindung.ws.on(:message) do |event| … end legt fest, was bei jeder neuen Nachricht passiert. event.data ist der Text, ws.send schickt einen zurück.index.html.Rackup::Server.start startet Puma auf Port 8000 mit App als Programm.Die Webseite index.html hat ein Eingabefeld und zeigt alles, was vom Server kommt:
<!DOCTYPE html> <html lang="de"> <head> <meta charset="utf-8"> <title>Nachrichten</title> <style> body { font-family: system-ui, sans-serif; max-width: 32em; margin: 2em auto; padding: 0 1em; } #log { border: 1px solid #ccc; border-radius: 6px; padding: 0.5em 1em; min-height: 10em; } #log p { margin: 0.3em 0; } form { display: flex; gap: 0.5em; margin-top: 0.8em; } input { flex: 1; font: inherit; padding: 0.3em; } </style> </head> <body> <h1>Nachrichten</h1> <div id="log"></div> <form id="form"> <input id="message" placeholder="Eine Nachricht …" autocomplete="off"> <button>Senden</button> </form> <script> // Dieselbe Adresse wie die Seite, nur mit ws:// (oder wss:// bei https) const url = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws'; const socket = new WebSocket(url); const log = document.getElementById('log'); function show(text) { const paragraph = document.createElement('p'); paragraph.textContent = text; log.append(paragraph); } socket.addEventListener('open', () => show('Verbunden.')); socket.addEventListener('close', () => show('Die Verbindung ist zu.')); // Jede Nachricht vom Server landet hier, ohne dass die Seite fragt socket.addEventListener('message', (event) => show(event.data)); document.getElementById('form').addEventListener('submit', (event) => { event.preventDefault(); // die Seite nicht neu laden const input = document.getElementById('message'); if (input.value !== '') socket.send(input.value); input.value = ''; }); </script> </body> </html>
new WebSocket(url) öffnet die Verbindung, mit ws:// statt http://. addEventListener('message', …) ruft eine Funktion auf, sobald eine Nachricht kommt, ohne dass die Seite fragt. socket.send(…) schickt eine Nachricht.
Starte den Server:
ruby server.rb

Eine WebSocket-Verbindung beginnt mit einer ganz normalen HTTP-Anfrage mit zwei besonderen Headern: Upgrade: websocket sagt, dass der Browser die Verbindung umwandeln möchte, und Sec-WebSocket-Key bringt einen zufälligen String mit. Der Server antwortet mit dem Statuscode 101 Switching Protocols und beweist mit Sec-WebSocket-Accept, dass er WebSockets versteht. Das erledigt Faye; ansehen kannst du es trotzdem. Öffne mit StrgShift5 ein zweites Terminal. curl schickt dort die Header, die sonst der Browser schickt, mit dem Beispiel-Key aus dem Standard:
curl -i --http1.1 -m 2 -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" localhost:8000/ws

Der Server antwortet mit 101 Switching Protocols und Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=, genau dem Wert, den der Standard für diesen Key als Beispiel nennt. Danach bleibt die Verbindung offen, und beide Seiten schicken sich Nachrichten in einem eigenen Format aus Bytes, sogenannten Frames. curl versteht keine Frames und gibt nach zwei Sekunden auf (-m 2).
Öffne jetzt die Adresse von Port 8000 aus dem Reiter »Ports« (»Forwarded Address«) im Browser und schreib eine Nachricht:

Mit welchem Statuscode antwortet der Server auf die Anfrage, mit der der Browser eine WebSocket-Verbindung öffnet?
Ein Echo ist noch langweilig. Spannend wird es, wenn eine Nachricht bei allen ankommt, die gerade verbunden sind. Dafür merkt sich der Server alle offenen Verbindungen in einer Liste $clients. Ersetze server.rb durch:
require 'faye/websocket' require 'rackup' # Alle offenen Verbindungen. Faye ruft die on-Blöcke aller Verbindungen # nacheinander auf, nie zwei gleichzeitig: Die Liste braucht keinen Lock. $clients = [] def broadcast(text) $clients.each { |client| client.send(text) } end App = lambda do |env| if Faye::WebSocket.websocket?(env) ws = Faye::WebSocket.new(env) ws.on(:open) do $clients << ws broadcast("Jemand ist dazugekommen. Jetzt sind wir #{$clients.size}.") end # Was kommt, geht an alle ws.on(:message) do |event| puts "Empfangen: #{event.data}" broadcast(event.data) end ws.on(:close) do $clients.delete(ws) broadcast("Jemand ist gegangen. Jetzt sind wir #{$clients.size}.") end ws.rack_response else [200, { 'content-type' => 'text/html; charset=utf-8' }, [File.read(File.join(__dir__, 'index.html'))]] end end puts "Der Server wartet auf Port 8000 ..." Rackup::Server.start(app: App, Host: '0.0.0.0', Port: 8000, server: 'puma', Silent: true)
ws.on(:open) läuft, sobald die Verbindung steht: Sie kommt in die Liste. ws.on(:close) läuft, wenn sie zu ist, weil jemand den Tab geschlossen hat: Sie fliegt wieder raus.broadcast schickt eine Nachricht an jede Verbindung in der Liste, auch an die, von der sie kam.on-Blöcke aller Verbindungen nacheinander aufruft, nie zwei gleichzeitig. Was passiert, wenn das nicht so ist, siehst du am Ende in Viele Threads.Starte den Server neu und öffne die Seite in zwei Tabs nebeneinander. Was du in einem Tab schreibst, erscheint in beiden.
Ada und Bo haben die Seite offen, der Server ist auf dem Stand »An alle«. Ada schickt »Hallo«. Wer sieht die Nachricht? Überleg erst, dann probier es mit zwei Tabs aus.
Jetzt wird aus dem Chat ein Bild. Der Server bewahrt das Bild auf: 64 × 40 Pixel, jedes eine Farbnummer von 0 bis 15. Wer neu dazukommt, bekommt das ganze Bild, danach nur noch die Pixel, die sich ändern. Die Nachrichten sind jetzt keine einfachen Texte mehr, sondern JSON, eine Schreibweise für Daten, die Ruby und JavaScript beide verstehen: {"x":10,"y":5,"color":4}.
Ersetze server.rb durch:
require 'faye/websocket' require 'rackup' require 'json' # Das Bild: 64 × 40 Pixel, jedes eine Farbnummer von 0 bis 15 WIDTH = 64 HEIGHT = 40 $pixels = Array.new(WIDTH * HEIGHT, 0) # Alle offenen Verbindungen $clients = [] def broadcast(message) text = JSON.generate(message) $clients.each { |client| client.send(text) } end App = lambda do |env| if Faye::WebSocket.websocket?(env) ws = Faye::WebSocket.new(env) # Wer neu ist, bekommt das ganze Bild, danach nur noch einzelne Pixel ws.on(:open) do $clients << ws ws.send(JSON.generate({ 'type' => 'picture', 'width' => WIDTH, 'height' => HEIGHT, 'pixels' => $pixels })) broadcast({ 'type' => 'people', 'count' => $clients.size }) end ws.on(:message) do |event| # Jeder kann alles schicken: nur glauben, was passt message = JSON.parse(event.data) rescue next next unless message.is_a?(Hash) x, y, color = message.values_at('x', 'y', 'color') next unless [x, y, color].all?(Integer) next unless x.between?(0, WIDTH - 1) && y.between?(0, HEIGHT - 1) && color.between?(0, 15) $pixels[y * WIDTH + x] = color broadcast({ 'type' => 'pixel', 'x' => x, 'y' => y, 'color' => color }) end ws.on(:close) do $clients.delete(ws) broadcast({ 'type' => 'people', 'count' => $clients.size }) end ws.rack_response else [200, { 'content-type' => 'text/html; charset=utf-8' }, [File.read(File.join(__dir__, 'index.html'))]] end end puts "Der Server wartet auf Port 8000 ..." Rackup::Server.start(app: App, Host: '0.0.0.0', Port: 8000, server: 'puma', Silent: true)
JSON.generate macht aus einem Hash Text, JSON.parse aus Text wieder einen Hash.type: picture für das ganze Bild, pixel für einen einzelnen Pixel, people für die Zahl der Verbundenen.next überspringt die Nachricht. Ein Server, der fremden Eingaben traut, ist eine Einladung, ihn kaputt zu machen.Die neue index.html zeichnet das Bild in ein <canvas>, ein Element, in das JavaScript zeichnen kann. Es ist nur 64 × 40 Pixel groß. CSS zieht es so groß, wie das Fenster erlaubt, und legt ein feines Gitter darüber, damit man jeden Pixel der Wand sieht:
<!DOCTYPE html> <html lang="de"> <head> <meta charset="utf-8"> <title>Pixelwand</title> <style> body { font-family: system-ui, sans-serif; margin: 0; padding: 1.5em; display: flex; flex-direction: column; align-items: center; background: #14151f; color: #f4f4f4; } h1 { margin: 0.2em 0 0.3em; } #people { margin: 0 0 1em; color: #94b0c2; } /* So breit, wie das Fenster erlaubt, höchstens 960 Pixel, und so schmal, dass die Wand samt Überschrift und Farben in die Höhe passt (das Bild ist 1,6-mal so breit wie hoch). */ #grid { position: relative; width: min(100%, 960px, (100vh - 12em) * 1.6); border-radius: 8px; overflow: hidden; box-shadow: 0 0 0 2px #333c57; } #wall { display: block; width: 100%; height: auto; image-rendering: pixelated; cursor: crosshair; touch-action: none; } /* Feine Linien über dem Bild, 64 Spalten und 40 Zeilen: so sieht man jeden Pixel der Wand. pointer-events: none lässt die Maus zum canvas durch. */ #grid::after { content: ''; position: absolute; inset: 0; pointer-events: none; background-image: linear-gradient(to right, rgba(255, 255, 255, 0.18) 1px, transparent 1px), linear-gradient(to bottom, rgba(255, 255, 255, 0.18) 1px, transparent 1px); background-size: calc(100% / 64) calc(100% / 40); } #colors { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin-top: 1em; } #colors button { width: 28px; height: 28px; border: 0; border-radius: 50%; cursor: pointer; } #colors button.active { box-shadow: 0 0 0 2px #14151f, 0 0 0 4px #f4f4f4; } </style> </head> <body> <h1>Pixelwand</h1> <p id="people">Verbinde …</p> <div id="grid"><canvas id="wall" width="64" height="40"></canvas></div> <div id="colors"></div> <script> // Die Palette »Sweetie 16«, wie bei PixelRAM const PALETTE = ['#1a1c2c', '#5d275d', '#b13e53', '#ef7d57', '#ffcd75', '#a7f070', '#38b764', '#257179', '#29366f', '#3b5dc9', '#41a6f6', '#73eff7', '#f4f4f4', '#94b0c2', '#566c86', '#333c57']; const wall = document.getElementById('wall'); const pen = wall.getContext('2d'); let color = 4; function setPixel(x, y, c) { pen.fillStyle = PALETTE[c]; pen.fillRect(x, y, 1, 1); } // Die Knöpfe für die Farben PALETTE.forEach((value, c) => { const button = document.createElement('button'); button.style.background = value; button.title = 'Farbe ' + c; if (c === color) button.classList.add('active'); button.addEventListener('click', () => { document.querySelector('#colors .active').classList.remove('active'); button.classList.add('active'); color = c; }); document.getElementById('colors').append(button); }); const url = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws'; const socket = new WebSocket(url); socket.addEventListener('message', (event) => { const message = JSON.parse(event.data); if (message.type === 'picture') { // Das ganze Bild, gleich nach dem Verbinden message.pixels.forEach((c, i) => setPixel(i % message.width, Math.floor(i / message.width), c)); } else if (message.type === 'pixel') { setPixel(message.x, message.y, message.color); } else if (message.type === 'people') { document.getElementById('people').textContent = message.count === 1 ? 'Du malst allein.' : message.count + ' Leute malen gerade mit.'; } }); socket.addEventListener('close', () => { document.getElementById('people').textContent = 'Die Verbindung ist zu. Lade die Seite neu.'; }); // Malen: Mausposition in Pixel umrechnen und dem Server schicken. // Gezeichnet wird erst, wenn der Server den Pixel an alle schickt. function sendPixel(x, y) { if (x >= 0 && x < wall.width && y >= 0 && y < wall.height) { socket.send(JSON.stringify({ x: x, y: y, color: color })); } } function pixelAt(event) { const rect = wall.getBoundingClientRect(); return [Math.floor((event.clientX - rect.left) / rect.width * wall.width), Math.floor((event.clientY - rect.top) / rect.height * wall.height)]; } // Die Maus meldet sich nicht bei jedem Pixel: Von der letzten Stelle bis // zur neuen jeden Pixel dazwischen schicken, sonst hat die Linie Lücken let last = null; function paint(event) { const [x, y] = pixelAt(event); const [x0, y0] = last || [x, y]; const steps = Math.max(Math.abs(x - x0), Math.abs(y - y0), 1); for (let i = 1; i <= steps; i++) { sendPixel(Math.round(x0 + (x - x0) * i / steps), Math.round(y0 + (y - y0) * i / steps)); } last = [x, y]; } wall.addEventListener('pointerdown', (e) => { last = pixelAt(e); sendPixel(...last); }); wall.addEventListener('pointermove', (e) => { if (last) paint(e); }); window.addEventListener('pointerup', () => { last = null; }); </script> </body> </html>
setPixel(x, y, c) malt ein Pixel in der Farbe Nummer c. Die 16 Farben sind die Palette »Sweetie 16«, wie bei PixelRAM.paint füllt die Pixel zwischen der letzten und der neuen Stelle auf, sonst hätten Linien Lücken.Starte den Server neu und lade die Seite. Das Bild ist noch leer; male etwas, und lade die Seite dann neu: Dein Bild ist noch da, denn der Server hat es.
Prüfe im zweiten Terminal, ob der Server die neue Seite schickt:
curl -s localhost:8000/ | head -n 5

Lade die Seite im Browser neu und male. Hier malt gerade jemand anderes:

Wie viele Farbnummern stehen in der Liste pixels der Nachricht, die ein
neuer Besuch zuerst bekommt?
Deine Webseite schickt nur gültige Pixel. Warum prüft der Server trotzdem jede Nachricht?
Bau die Pixelwand: Der Server bewahrt das Bild auf, prüft jede Nachricht und schickt jeden Pixel an alle. Speichere.
Jetzt kommt der beste Teil. Gib in deinem Profil unter »Shared Live Apps« Port 8000 frei und sag deiner Klasse die Adresse. Alle, die sie öffnen, malen an deinem Bild, und jeder sieht sofort, was die anderen malen. Oben steht, wie viele gerade mitmalen.
Vielleicht merkt ihr schnell, dass einige wenige das ganze Bild übermalen. Ein paar Ideen, die Pixelwand weiterzubauen, von leicht bis schwer:
WIDTH und HEIGHT im Server und die Größe des <canvas> in der Seite.ws, wann die Verbindung zuletzt gemalt hat (Time.now), und überspringe in ws.on(:message) Pixel, die zu früh kommen.picture.json. Lies die Datei beim Start wieder ein (File.exist?, JSON.parse), dann übersteht das Bild einen Neustart.type für Textnachrichten und ein Eingabefeld unter der Wand.Ein Neustart des Servers löscht das Bild. Darum soll der Server es nach jedem Pixel in eine Datei schreiben, picture.json. Das dauert einen Moment, und solange Faye mit einer Nachricht beschäftigt ist, warten alle anderen. Die übliche Lösung: Mehrere Threads arbeiten die Nachrichten ab, während Faye schon neue annimmt. Dazu zählt der Server mit, wie viele Pixel schon gemalt wurden, und schreibt die Zahl ins Terminal.
Ersetze server.rb durch:
require 'faye/websocket' require 'rackup' require 'json' # Das Bild: 64 × 40 Pixel, jedes eine Farbnummer von 0 bis 15 WIDTH = 64 HEIGHT = 40 $pixels = Array.new(WIDTH * HEIGHT, 0) # Alle offenen Verbindungen $clients = [] # Wie viele Pixel schon gemalt wurden $painted = 0 # Das Bild in eine Datei schreiben, damit es einen Neustart übersteht def save_picture File.write('picture.json', JSON.generate($pixels)) end def broadcast(message) text = JSON.generate(message) $clients.each { |client| client.send(text) } end # Vier Threads arbeiten die Nachrichten ab, während Faye neue annimmt. Mehr # als ein paar sollten es nicht sein: Jeder Thread kostet Speicher, und ein # Workspace darf nicht beliebig viele starten. $queue = Queue.new 4.times do Thread.new do loop do text = $queue.pop # wartet, bis eine Nachricht da ist # Jeder kann alles schicken: nur glauben, was passt message = JSON.parse(text) rescue next next unless message.is_a?(Hash) x, y, color = message.values_at('x', 'y', 'color') next unless [x, y, color].all?(Integer) next unless x.between?(0, WIDTH - 1) && y.between?(0, HEIGHT - 1) && color.between?(0, 15) $pixels[y * WIDTH + x] = color count = $painted save_picture # dauert einen Moment $painted = count + 1 puts "Pixel Nr. #{$painted}" broadcast({ 'type' => 'pixel', 'x' => x, 'y' => y, 'color' => color }) end end end App = lambda do |env| if Faye::WebSocket.websocket?(env) ws = Faye::WebSocket.new(env) # Wer neu ist, bekommt das ganze Bild, danach nur noch einzelne Pixel ws.on(:open) do $clients << ws ws.send(JSON.generate({ 'type' => 'picture', 'width' => WIDTH, 'height' => HEIGHT, 'pixels' => $pixels })) broadcast({ 'type' => 'people', 'count' => $clients.size }) end # Jede Nachricht kommt in die Warteschlange; Faye ist sofort frei ws.on(:message) { |event| $queue << event.data } ws.on(:close) do $clients.delete(ws) broadcast({ 'type' => 'people', 'count' => $clients.size }) end ws.rack_response else [200, { 'content-type' => 'text/html; charset=utf-8' }, [File.read(File.join(__dir__, 'index.html'))]] end end puts "Der Server wartet auf Port 8000 ..." Rackup::Server.start(app: App, Host: '0.0.0.0', Port: 8000, server: 'puma', Silent: true)
ws.on(:message) legt jede Nachricht nur in eine Warteschlange, $queue, und ist sofort frei für die nächste.$queue.pop die nächste Nachricht und verarbeiten sie, jeder in einer Schleife ohne Ende. pop wartet, solange die Schlange leer ist. Ein Thread pro Nachricht wäre einfacher, aber jeder Thread kostet Speicher, und dein Workspace darf nicht beliebig viele starten. Echte Server arbeiten genauso mit einer festen Zahl von Threads.save_picture schreibt das ganze Bild als JSON in die Datei.$painted zählt die Pixel: lesen, speichern, um eins erhöhen.Damit es spannend wird, malen jetzt sehr viele Leute gleichzeitig. Lege flood.js an; es schickt 200 Pixel auf einmal an deinen eigenen Server auf localhost und wartet, bis der Server alle an alle verteilt hat:
// Schickt 200 Pixel auf einmal, als würden 200 Leute gleichzeitig malen. // Nur für den eigenen Server auf localhost: Fremde Server so zu überschütten, // ist ein Angriff. const socket = new WebSocket('ws://localhost:8000/ws'); let back = 0; socket.addEventListener('open', () => { for (let i = 0; i < 200; i++) { socket.send(JSON.stringify({ x: i % 64, y: Math.floor(i / 64) % 40, color: i % 16 })); } console.log('200 Pixel geschickt.'); }); // Fertig, sobald der Server alle 200 Pixel an alle verteilt hat socket.addEventListener('message', (event) => { if (JSON.parse(event.data).type === 'pixel' && ++back === 200) { console.log('200 Pixel zurückbekommen.'); socket.close(); } });
Starte den Server neu und führe das Skript im zweiten Terminal aus:
node flood.js
flood.js ist für deinen eigenen Server da, und nur für den. Einen fremden Server mit Anfragen zu überschütten, bis er für alle anderen nicht mehr erreichbar ist, ist ein Angriff (Denial of Service) und kann als Computersabotage strafbar sein (§ 303b Strafgesetzbuch). Die geteilten Pixelwände deiner Klasse sind für sie genauso ein fremder Server.
flood.js schickt 200 Pixel auf einmal, vier Threads verarbeiten sie.
Welche Zahl steht danach im Terminal des Servers bei »Pixel Nr. …«?
Überleg erst, dann probier es aus.

200 Pixel, und der Zähler kommt nicht annähernd bis 200. Jeder der vier Threads liest $painted, speichert das Bild und schreibt dann seinen Wert plus eins zurück. Während einer speichert, lesen die anderen denselben alten Wert; vier Threads schreiben dieselbe Zahl, und drei Pixel sind beim Zählen verloren. Dasselbe Problem hatte der Besuchszähler deines Webservers; dort war die Lücke zwischen Lesen und Schreiben nur so kurz, dass man den Fehler selten sieht. Hier dauert das Speichern, und der Fehler passiert jedes Mal.
Die Lösung ist ein Lock, in Ruby ein Mutex: Nur ein Thread auf einmal darf durch, die anderen warten davor. Ergänze unter $painted = 0:
$lock = Mutex.new
und schließe das Ändern von Bild und Zähler in $lock.synchronize ein:
$lock.synchronize do $pixels[y * WIDTH + x] = color count = $painted save_picture # dauert einen Moment $painted = count + 1 puts "Pixel Nr. #{$painted}" end
Starte den Server neu und schick noch einmal 200 Pixel:

Jetzt kommt der Zähler bei 200 an. Die Threads stehen nur an der einen Stelle hintereinander, an der sie das Bild und den Zähler ändern; alles andere, das Prüfen der Nachricht und das Verteilen an alle, läuft weiter gleichzeitig. Ein Lock gehört genau um die Teile, die gemeinsame Daten lesen und ändern, und um nicht mehr.
Gegen jemanden, der eine geteilte Pixelwand mit Pixeln überschüttet, hilft der Lock nicht: Er sorgt nur dafür, dass richtig gezählt wird. Dagegen hilft die Wartezeit aus den Ideen oben. Erlaubt der Server jeder Verbindung nur einen Pixel pro Sekunde, kommen von flood.js nur noch die ersten an.
Schütze Bild und Zähler mit einem Lock, sodass der Server nach
node flood.js bei 200 ankommt, und speichere.
Gib deine Pixelwand ab: den Ordner pixelwand mit server.rb,
websocket.rb und index.html.
Eure Pixelwand funktioniert genauso wie große Anwendungen, nur kleiner: Ein Server hält den gemeinsamen Zustand, jeder Browser hat eine offene Verbindung, und jede Änderung geht an alle. So arbeiten Online-Spiele, Chats und Programme, in denen mehrere Leute gleichzeitig an einem Dokument schreiben.
Im Ernstfall läuft eine WebSocket-Verbindung verschlüsselt über wss://, und der Server muss mit Tausenden Verbindungen und mit Leuten umgehen können, die ihm absichtlich falsche Daten schicken. Die Prüfung in ws.on(:message) war ein erster Schritt dahin.
