Sascha Schulz
2024-12-09 b4eb2a57ca9566adaf67de3e099e4529af2bb170
index.html
@@ -4122,6 +4122,368 @@
                     </code>
                  </pre>
               </section>
               <section>
                  <h3>Buffer</h3>
                  <p>Array-artige Struktur für rohe Bytes</p>
                  <pre>
                     <code class="js" data-trim data-line-numbers>
                        const b1 = Buffer.from("abc"); // bevorzugt
                        const b2 = Buffer.alloc(100, 0); // Länge mit initialen Daten
                        const b3 = Buffer.allocUnsafe(100); // Länge ohne initiale Daten
                        const b4 = new Buffer("abc"); //  offiziell nicht empfohlen
                        console.log(b1); // => &lt;Buffer 61 62 63>
                        console.log(b1.toString("hex")); // => "616263"
                     </code>
                  </pre>
               </section>
               <section>
                  <p>Mit Buffern arbeiten:</p>
                  <pre>
                     <code class="js" data-trim data-line-numbers>
                        const a = Buffer.from("a") + Buffer.from("b");
                        const b = Buffer.concat([Buffer.from("a"), Buffer.from([98])]);
                        const c = b.toString()
                        console.log(a); // => ab
                        console.log(b); // => &lt;Buffer 61 62>
                        console.log(c); // => ab
                        for (const byte of a) {
                           console.log(byte); // => 97 98
                        }
                     </code>
                  </pre>
               </section>
               <section>
                  <p>Aufgabe</p>
                  <p>Implementiere eine XOR-Verschlüsselung (<code>^</code>-Operator), sodass jedes Byte einer Original-Nachricht der Reihe nach mit dem entsprechenden Byte eines Schlüsselwortes verschlüsselt wird und gebe die verschlüsselte Nachricht als Text aus.</p>
               </section>
               <section>
                  <p>Aufgabe</p>
                  <p>Implementiere zur vorherigen Verschlüsselung eine Entschlüsselung.</p>
               </section>
               <section>
                  <p>Lösung</p>
                  <pre>
                     <code class="js" data-trim data-line-numbers>
                        const word = Buffer.from("abcdef");
                        const key = Buffer.from("xyz");
                        const encrypted = Buffer.alloc(word.length, 0);
                        // encrypt
                        for (let i = 0; i &lt; word.length; i++) {
                           encrypted[i] = word[i] ^ key[i % key.length];
                        }
                        // decrypt
                        for (let i = 0; i &lt; word.length; i++) {
                           word[i] = encrypted[i] ^ key[i % key.length];
                        }
                        console.log(word.toString()); // => "abcdef"
                     </code>
                  </pre>
               </section>
               <section>
                  <h3>Streams</h3>
                  <p>Konstanter Fluss von (meist) großen Datenmengen in kleinen Paketen</p>
               </section>
               <section>
                  <p>Gängige Typen von Streams</p>
                  <ul>
                     <li>Standard In / Out</li>
                     <li>HTTP-Requests / -Responses</li>
                     <li>TCP-Sockets</li>
                     <li>Lese- und Schreib-Streams auf Dateien</li>
                     <li>Große Datenbank-Ergebnisse mittels Cursor</li>
                  </ul>
               </section>
               <section>
                  <pre>
                     <code class="js" data-trim data-line-numbers>
                        const readableStream = fs.createReadStream("./source.txt");
                        const writableStream = fs.createWriteStream("./target.txt");
                        readableStream.on("data", (data /* Buffer */) =&gt; {
                           // process data here
                           writableStream.write(data)
                        });
                        readableStream.on("end", (data) =&gt; {
                           // stream has finished, e.g. EOF (end of file)
                        });
                        // alternatively pipe content
                        readableStream.pipe(writableStream);
                     </code>
                  </pre>
               </section>
               <section>
                  <p>Aufgabe</p>
                  <p>Lese eine beliebige Datei als Readable Stream, verschlüssele den Inhalt mit der XOR-Methode und schreibe die verschlüsselten Bytes in eine Zieldatei</p>
               </section>
               <section>
                  <p>Aufgabe</p>
                  <p>Ändere das Programm, sodass auf Konsolenebene ein Pipe (|) ermöglicht wird:</p>
                  <pre>
                     <code class="bash" data-trim data-line-numbers>
                        echo "Hello World!" | node index.js
                     </code>
                  </pre>
                  <p>Nutze dafür die implizit vorhandenen Streams von Standard In / Out:</p>
                  <pre>
                     <code class="js" data-trim data-line-numbers>
                        process.stdin; // Readable Stream
                        process.stdout; // Writable Stream
                     </code>
                  </pre>
               </section>
               <section>
                  <h3>Kryptographie</h3>
               </section>
               <section>
                  Mithilfe des nativen <code>crypto</code>-Moduls ist es möglich, moderne sichere kryptografische Verfahren zu verwenden:
                  <pre>
                     <code class="js" data-trim data-line-numbers>
                        const crypto = require("crypto");
                     </code>
                  </pre>
                  Hierzu nutzt NodeJS selber die weit verbreitete OpenSSL-Bibliothek
               </section>
               <section>
                  Einen MD5-Hash erzeugen:
                  <pre>
                     <code class="js" data-trim data-line-numbers>
                        const crypto = require("crypto");
                        const hash = crypto.createHash("md5");
                        const md5 = hash.update("abcdefg")
                           .digest(); // Buffer (default)
                           // .digest("hex") // Hexadezimal
                           // .digest("utf8") // Bytes forciert als Chars
                        console.log(md5);
                     </code>
                  </pre>
               </section>
               <section>
                  <p>Aufgabe</p>
                  <p>Erzeuge jeweils den md5- und den sha256-Hash als Hex-Wert der beiden Texte in der Datei <code>text-examples.txt</code> und vergleiche jeweils ihren md5- und sha256-Hash miteinander</p>
               </section>
               <section>
                  <h3>Zippen</h3>
               </section>
               <section>
                  Mit dem nativen Modul <code>zlib</code> ist es möglich, Kompression in seinen Anwendungen zu nutzen.
                  <pre>
                     <code class="js" data-trim data-line-numbers>
                        const zlib = require("zlib");
                        // Komprimieren
                        zlib.deflate("abc" /* buffer | string */, (err, compressed) => {});
                        // Dekomprimieren
                        zlib.inflate(compressed /* buffer */, (err, decompressed) => {});
                        // Als Stream
                        const zipper = zlib.createDeflate();
                        const unzipper = zlib.createInflate();
                        source.pipe(zipper).pipe(target);
                        source.pipe(unzipper).pipe(target);
                     </code>
                  </pre>
               </section>
               <section>
                  <p>Aufgabe</p>
                  <p>Nutze die einfachen Varianten <code>zlib.deflate</code> und <code>zlib.inflate</code>, um einen beliebigen Text zu komprimieren und anschließend wieder zu dekomprimieren</p>
               </section>
               <section>
                  <p>Aufgabe</p>
                  <p>Nutze die Stream-Varianten <code>zlib.createDeflate</code> und <code>zlib.createInflate</code>, um jeweils ein getrenntes Programm für die Kompression und Dekompression mit <code>process.stdin</code> und <code>process.stdout</code> zu machen, sodass in der Konsole folgende Nutzung möglich ist:</p>
                  <pre>
                     <code class="bash" data-trim data-line-numbers>
                        echo "abc" | node compress.js | node decompress.js
                        cat file.txt | node compress.js > file.zipped
                        cat file.zipped | node decompress.js > file.txt
                     </code>
                  </pre>
               </section>
               <section>
                  <h3>TCP-Verbindungen</h3>
               </section>
               <section>
                  <ul>
                     <li>Bidirektionale Verbindung</li>
                     <li>Duplexfähig</li>
                     <li>Besteht immer aus Server- und Client-Socket</li>
                  </ul>
               </section>
               <section>
                  Mit dem nativen Modul <code>net</code> ist es möglich, eine TCP-Verbindung aufzubauen
                  <pre>
                     <code class="js" data-trim data-line-numbers>
                        /* Server */
                        const net = require("net");
                        const server = net.createServer((socket) => {
                           // new incoming connection
                           socket.on("data", (data) => {
                              // data is a buffer
                           });
                           socket.write("Hello World!");
                        });
                        server.listen(3000);
                     </code>
                  </pre>
               </section>
               <section>
                  <pre>
                     <code class="js" data-trim data-line-numbers>
                        /* Client */
                        const net = require("net");
                        const socket = net.connect(3000, () => {
                           // 'connect' listener
                        });
                        socket.on("data", (data) => {
                           // data is a buffer
                        });
                     </code>
                  </pre>
               </section>
               <section>
                  <p>Aufgabe</p>
                  <p>Implementiere die Clientlogik in der <code>tcp-client.js</code>, um dem Server ein <code>"Hello Server!"</code> zurück zu senden</p>
               </section>
               <section>
                  <p>Aufgabe</p>
                  <p>Sende eine festgelegte Datei vom Server zum Client. Der Server liest die Datei ein und sendet die Inhalte zum Client, welcher diese wiederum in eine Zieldatei schreibt. Eventuell darf auch noch eine Kompression mittels Zip / Deflate implementiert werden.</p>
               </section>
               <section>
                  <p>Kurze Wiederholung des HTTP-Protokolls</p>
                  <p>Request</p>
                  <pre>
                     <code class="http" data-trim data-line-numbers>
                        GET / HTTP/1.1
                        Host: localhost:3456
                     </code>
                  </pre>
                  <p>Response</p>
                  <pre>
                     <code class="http" data-trim data-line-numbers>
                        HTTP/1.1 200
                        Content-Type: text/plain
                        Content-Length: 12
                        Hello World!
                     </code>
                  </pre>
               </section>
               <section>
                  <p>Aufgabe</p>
                  <p>Implementiere mithilfe des TCP-Sockets einen minimalen HTTP-Server, welcher <code>Hello &lt;name></code> als Text zurück gibt, wobei <code>&lt;name></code> aus dem Query-Parameter gelesen werden soll</p>
               </section>
               <section>
                  <h3>HTTP-Server</h3>
               </section>
               <section>
                  <pre>
                     <code class="js" data-trim data-line-numbers>
                        const http = require("http");
                        const server = http.createServer((req, res) => {
                           res.writeHeader(200, {
                              "Content-Type": "text/plain", /* Put Content-Type here */
                           });
                           console.log(req.rawHeaders);
                           console.log(req.method);
                           console.log(req.url);
                           res.write(/* response content */);
                           res.end(); /* important if no Content-Length is specified */
                        });
                        server.listen(3456);
                     </code>
                  </pre>
               </section>
               <section>
                  <p>Aufgabe</p>
                  <p>Experimentiert mit verschiedenen Möglichkeiten von <code>Content-Length</code> und dem Vorhandensein von <code>res.end()</code>, mit längerer oder kürzerer <code>Content-Length</code> als der tatsächliche Body oder teilt den Body in zwei <code>res.write()</code> und verzögert künstlich mit einem setTimeout() den zweiten Teil und schaut euch im Browser bei den Entwickler-Tools die Timings der Response an.</p>
               </section>
               <section>
                  <h3>Worker Threads</h3>
               </section>
               <section>
                  <p>Was sind Worker Threads?</p>
                  <p>Eine bidirektionale nachrichten-basierte Implementierung von Multithreading in NodeJS</p>
               </section>
               <section>
                  <p>Beispiel:</p>
                  <pre>
                     <code class="js" data-trim data-line-numbers>
                        // Main Thread
                        const { Worker } = require('worker_threads');
                        const data = 'some data';
                        const worker = new Worker("path/to/worker.js", { workerData: data });
                        worker.on('message', message => console.log('Reply from Thread:', message));
                        // Worker Thread
                        const { workerData, parentPort } = require('worker_threads');
                        const result = workerData.toUpperCase();
                        parentPort.postMessage(result);
                     </code>
                  </pre>
               </section>
               <section>
                  <p>Weitere nützliche Funktionen:</p>
                  <pre>
                     <code class="js" data-trim data-line-numbers>
                        // Main Thread
                        worker.on('message', message => processMessage(message));
                        worker.on('error', error => console.error(error));
                        worker.postMessage({a: 1}); // send any data to the worker
                        worker.terminate(); // manually terminate worker thread
                        // Worker Thread
                        parentPort.on('message', message => doSomething(message)); // receive messages from the main thread
                        process.exit(0); // terminate self, optionally with exit code
                     </code>
                  </pre>
               </section>
               <section>
                  <p>Aufgabe</p>
                  <p>
                     Nutze die in den <code>examples</code> vorhandene Funktion <code>isPrime</code> (010-nodejs/mt-main.js und 010-nodejs/mt-worker.js) und implementiere mit Hilfe der <code>Worker Threads</code> die Suche nach Primzahlen.
                     Die gewünschte maximale Zahl, bis zu der die Primzahlen gesucht werden sollen als auch die Anzahl der Threads, mit der gleichzeitig gesucht werden soll, sollen möglichst einfach parametrisierbar sein (einfache Variable im Code genügt).
                     Probiert verschiedene Thread-Anzahlen aus und vergleicht einmal die benötigten Zeiten.
                  </p>
               </section>
               <section>
                  <p>Hinweise:</p>
                  <ul>
                     <li>Bei der Suche bis zu 1.000.000 und 8 Threads soll der erste Thread von 0 bis 125.000 suchen, der zweite von 125.000 bis 250.000 usw.</li>
                     <li>Nutzt Promises, um auf alle Ergebnisse warten zu können. (s. <code>Promise.all</code>)</li>
                     <li>per <code>workerData</code> können auch Objekte übergeben werden</li>
                     <li>Die gefundenen Primzahlen können ausgegeben werden, müssen aber nicht. In erster Linie geht es darum, die Arbeit auf mehrere Threads aufzuteilen.</li>
                  </ul>
               </section>
            </section>
         </div>
      </div>