Categories
Development

WebSockets with Node.js

WebSockets are a protocol that provides full-duplex communication channels over a single, long-lived connection. They are designed for real-time, event-driven web applications and allow for low-latency communication between a client (typically a web browser) and a server. Here are some key points about WebSockets:

  1. Full-Duplex Communication: Unlike HTTP, which is request-response based, WebSockets allow for two-way communication where both client and server can send and receive messages independently of each other.
  2. Persistent Connection: WebSocket connections are persistent, meaning they remain open as long as both the client and server agree to keep the connection alive. This reduces the overhead associated with establishing new connections.
  3. Low Latency: WebSockets are ideal for scenarios requiring real-time updates because they reduce the latency associated with polling or long-polling techniques.
  4. Protocol: WebSockets are established by upgrading an HTTP/HTTPS connection using a WebSocket handshake, switching the protocol from HTTP to WebSocket.
  5. Use Cases: Common use cases include live chat applications, real-time notifications, collaborative editing, online gaming, and any application requiring real-time data updates.

Node.js in Codespace

I want to test WebSockets with Node.js in GitHub Codespace.

Node Version Manager (nvm)

# test installation
command -v nvm
nvm


# check version
nvm ls
       v18.20.1
       v20.12.1
->       system
default -> 20 (-> v20.12.1)
iojs -> N/A (default)
unstable -> N/A (default)
node -> stable (-> v20.12.1) (default)
stable -> 20.12 (-> v20.12.1) (default)
lts/* -> lts/iron (-> v20.12.1)
lts/argon -> v4.9.1 (-> N/A)
lts/boron -> v6.17.1 (-> N/A)
lts/carbon -> v8.17.0 (-> N/A)
lts/dubnium -> v10.24.1 (-> N/A)
lts/erbium -> v12.22.12 (-> N/A)
lts/fermium -> v14.21.3 (-> N/A)
lts/gallium -> v16.20.2 (-> N/A)
lts/hydrogen -> v18.20.1
lts/iron -> v20.12.1


# check node version
node --version
v20.12.1

Node.js

# install LTS
nvm install --lts
Installing latest LTS version.
Now using node v20.14.0 (npm v10.7.0)


# check version
nvm ls


# check node version
node --version
v20.14.0

Some npm commands

# Show installed Nodes
nvm ls
# Show available versions
nvm ls-remote
# Install latest version
nvm install node
# Install LTS version
nvm install --lts
# Install a specific version (list available -> example 16.20.2)
nvm install 16.20.2
# Use a specific version
nvm use 16.20.2
# Show npm version
npm --version

Simple Website with Node.js

To serve an HTML page using Node.js, we can use the built-in http module.

Create an HTML file

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Node.js HTML Server</title>
</head>
<body>
    <h1>Hello, World!</h1>
    <p>This is a simple HTML page served by Node.js.</p>
</body>
</html>

Create a Node.js script to serve the HTML file

const http = require('http');
const fs = require('fs');
const path = require('path');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
    if (req.method === 'GET' && req.url === '/') {
        const filePath = path.join(__dirname, 'index.html');
        fs.readFile(filePath, (err, data) => {
            if (err) {
                res.statusCode = 500;
                res.setHeader('Content-Type', 'text/plain');
                res.end('Internal Server Error');
            } else {
                res.statusCode = 200;
                res.setHeader('Content-Type', 'text/html');
                res.end(data);
            }
        });
    } else {
        res.statusCode = 404;
        res.setHeader('Content-Type', 'text/plain');
        res.end('Not Found');
    }
});

server.listen(port, hostname, () => {
    console.log(`Server running at http://${hostname}:${port}/`);
});

Run the Node.js server

node server.js

Test Node.js server in terminal

curl localhost:3000
http localhost:3000

Test Node.js server in browser

WebSocket in Codespace

To create a Node.js server that provides both an HTTP server for serving an HTML page and a WebSocket server for real-time communication, we can use the ws library for WebSockets.

Install the ws library

npm install ws

Create the HTML file

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket Example</title>
</head>
<body>
    <h1>WebSocket Example</h1>
    <p id="message">Connecting...</p>
    <script>
        const socket = new WebSocket('ws://localhost:3000');

        socket.onopen = function(event) {
            document.getElementById('message').innerText = 'Connected';
            socket.send('Hello Server');
        };

        socket.onmessage = function(event) {
            document.getElementById('message').innerText = 'Received: ' + event.data;
        };

        socket.onclose = function(event) {
            document.getElementById('message').innerText = 'Disconnected';
        };

        socket.onerror = function(error) {
            document.getElementById('message').innerText = 'Error: ' + error.message;
        };
    </script>
</body>
</html>

Create the Node.js server

const http = require('http');
const fs = require('fs');
const path = require('path');
const WebSocket = require('ws');

const hostname = '127.0.0.1';
const port = 3000;

// Create HTTP server
const server = http.createServer((req, res) => {
    if (req.method === 'GET' && req.url === '/') {
        const filePath = path.join(__dirname, 'index.html');
        fs.readFile(filePath, (err, data) => {
            if (err) {
                res.statusCode = 500;
                res.setHeader('Content-Type', 'text/plain');
                res.end('Internal Server Error');
            } else {
                res.statusCode = 200;
                res.setHeader('Content-Type', 'text/html');
                res.end(data);
            }
        });
    } else {
        res.statusCode = 404;
        res.setHeader('Content-Type', 'text/plain');
        res.end('Not Found');
    }
});

// Create WebSocket server
const wss = new WebSocket.Server({ server });

wss.on('connection', ws => {
    console.log('Client connected');

    ws.on('message', message => {
        console.log(`Received: ${message}`);
        ws.send('Hello Client');
    });

    ws.on('close', () => {
        console.log('Client disconnected');
    });
});

server.listen(port, hostname, () => {
    console.log(`Server running at http://${hostname}:${port}/`);
});

Run the Node.js server

node server.js

Test Node.js server in terminal

curl localhost:3000
http localhost:3000

Test Node.js server in browser

Brower Fix

We need to adjust the address of the WebSocket.

When setting up a WebSocket connection from the client-side script within the HTML file, the WebSocket URL must match the address and port where the WebSocket server is running. This URL should include the WebSocket protocol (ws:// or wss:// for secure connections)

Adjustment:

const socket = new WebSocket('wss://symmetrical-disco-g454xqrq9pqvfw6pr-3000.app.github.dev/:3000');

OK, this works, so make it a little more dynamic:

const socket = new WebSocket('wss://' + location.host + '/:3000');

Test WebSocket from terminal

Unfortunately, curl and httpie do not natively support WebSocket protocols. To test WebSocket connections using command-line we can use wscat, which is specifically designed for WebSocket communication.

npm install -g wscat

Using wscat to Test WebSocket Connections:

wscat -c ws://localhost:3000

Connected (press CTRL+C to quit)
> hello
< Hello Client

Build a Chat App

Let's build a "Chat App"

Server

We enhance the server to dynamically answer to a message:

[...]
     ws.on('message', message => {
        console.log(`Received: ${message}`);
        ws.send('Hello ' + message);
    });
[...]

Amazing!

Client

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket Chat Example</title>
    <style>
        #chat {
            resize: none;
            overflow: hidden;
            min-width: 70%;
            min-height: 300px;
            max-height: 300px;
            overflow-y: scroll;
        }
        #msg {
            min-width: 40%;
        }
    </style>
</head>
<body>
    <h1>WebSocket Chat Example</h1>
    <p id="message">Connecting...</p>
    <br/>
    <div class="container">
        <br/>
        <div class="row">
            <textarea id="chat"></textarea>
        </div>
        <div class="row">
            <input id="msg" type="text" placeholder="enter your message">
            <button id="send" type="button" disabled>send</button>
        </div>
    
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <script>
        var connected = false;
        var socket;

        $( document ).ready(function() {
            connect();
            $("#send").click(sendMessage);

            $("#name").keypress(function(event){
                if(event.keyCode == 13 || event.which == 13) {
                    connect();
                }
            });

            $("#msg").keypress(function(event) {
                if(event.keyCode == 13 || event.which == 13) {
                    sendMessage();
                }
            });

            $("#chat").change(function() {
                scrollToBottom();
            });

            $("#name").focus();
        });

        var connect = function() {
            if (! connected) {
                socket = new WebSocket('wss://' + location.host + '/:3000');
                socket.onopen = function(m) {
                    connected = true;
                    console.log("Connected to the web socket");
                    $("#send").attr("disabled", false);
                    $("#connect").attr("disabled", true);
                    $("#name").attr("disabled", true);
                    $("#chat").append("[Chatbot] Howdy, how may I help you? \n");
                    $("#msg").focus();
                    $("#message").text('Connected');
                };
                socket.onmessage = function(m) {
                    console.log("Got message: " + m.data);
                    $("#message").text('Received: ' + m.data);
                    $("#chat").append("[Chatbot] " + m.data + "\n");
                    scrollToBottom();
                };
                socket.onclose = function(event) {
                    console.log("Disconnected");
                    $("#message").text('Disconnected');
                    $("#chat").append("[Chatbot] Disconnected" + "\n");
                    scrollToBottom();
                };
                socket.onerror = function(error) {
                    console.log("Error: " + error.message);
                    $("#message").text('Error: ' + error.message);
                    $("#chat").append("[Chatbot] Error: " + error.message + "\n");
                    scrollToBottom();
                };
            }
        };

        var sendMessage = function() {
            if (connected) {
                var value = $("#msg").val();
                console.log("Sending " + value);
                $("#chat").append("[You] " + value + "\n")
                socket.send(value);
                $("#msg").val("");
            }
        };

        var scrollToBottom = function () {
            $('#chat').scrollTop($('#chat')[0].scrollHeight);
        };

    </script>
</body>
</html>

Awesome!

Also 'Chatbot' is working from terminal:

Categories
Development

Günstige Website

Domain

Für ein R&D-Projekt benötige ich eine eigene Domain. Diese soll möglichst günstig zu bekommen sein und das bei einem Anbieter, der eine Bezahlung über Bitcoin oä ermöglicht. Bei der Recherche bin ich dabei auf diesen Anbieter gestoßen: Porkbun

Das Projekt läuft unter dem Arbeitstitel: Dagobert Doge. Also suche ich nach einer entsprechenden Domain:

Es werden einige verfügbare Domains angezeigt:

Ich entscheide mich für DagobertDoge.space. Klingt cool und kostet nur 1,16 $ im ersten Jahr:

Registrieren, bezahlen und schon erscheint die Domain im Domain Management:

EMail

Die Einrichtung eines gehosteten Email Accounts wird in der Knowledge Base von Porkbun beschrieben. Der erste Monat ist kostenfrei.

Domain Management -> Email -> Option 1: Email Hosting -> Configure

Nach dem einmonatigem Testzeitraum wird die Gebühr für ein ganzes Jahr eingezogen, also noch flugs einen Reminder zum rechtzeitigen Löschen angelegt.

Anschließend werden die Email Configuration Settings angezeigt, sehr hilfeich. Außerdem eine DMARC Notice. Wenn man auf den Configure Button klickt, werden die Einstellungen automatisch vorgenommen. Was es mit DMARC genau auf sich hat muss ich bei Gelegenheit evaluieren.

Der Webmail Client präsentiert sich sehr aufgeräumt, nice:

Alternativ wäre auch ein dauerhaft kostenfreies Email Forwarding möglich:

Website auf Github Pages

Die Website soll auf Github Pages gehostet werden. Das ist kostenfrei und über die üblichen Git Tools editierbar. Zumindest stelle ich mir das so vor, der Test kommt jetzt:

In den Details des Domain Managements -> Quick Connect -> Manage:

Github auswählen und einen neuen Account anlegen:

Auf I Need One klicken, schon öffnet sich GitHub in einem neuen Tab:

Und siehe da, wenn wir jetzt DagobertDoge öffnen, sehen wir:

Die Website - Ein Template

Die HelloWorld-Seite sieht maximal spartanisch aus, daher habe ich ein frei verfüg- und nutzbares Template für eine fancy Website gesucht und auf html DESIGN gefunden:

Die Website - Das Projekt

Das Git Projekt auf meinen Arbeitsrechner clonen:

cd [...]/workspace
git clone https://github.com/DagobertDoge/dagobertdoge.github.io.git
cd dagobertdoge.github.io

Die Dateien aus dem Bitcypo Template werden in das dagobertdoge.github.io Verzeichnis kopiert.

Git einrichten und Änderungen in das Repository übertragen:

git config user.email "admin@dagobertdoge.space"
git config user.name "DagobertDoge"
git add *
git commit
git push
Username for 'https://github.com': DagobertDoge
Password for 'https://DagobertDoge@github.com': xxx

Dann noch ein paar kleine Anpassungen im HTML und in einem Image & dann das ganze ins GitHub Repository pushen.

Die Website - Das erste Resultat

DagobertDoge.space

Whois

Eine Whois-Abfrage ergab, dass keinerlei persönlichen Informationen von mir im Whois Record eingetragen wurden. Ich brauche also keine Angst vor zB Spam haben.

Abschluss

Die Etappenziele sind erreicht: Eine eigene Domain für schmales Geld, Webmail für zumindest einen Monat, danach wenigstens noch die Mailweiterleitung. Bei Gelegenheit sollte ich mal schauen, ob es nicht einen Dienst gibt, der einem gratis das Mailhosting übernimmt. Das müsste ja technisch möglich sein über einen entsprechenden Eintrag im MX Record.

Das Repository für den Code der Website und sogar das Hosting der (statischen) Seite gibt es for free.

Ein Template für die erste Version der Website gibt es auch for free.

Nachtrag: Basic authentication deprecation

Nach dem Checkin in GitHub erreichte mich diese EMail:

Hi @DagobertDoge,

You recently used a password to access the repository at DagobertDoge/dagobertdoge.github.io with git using git/2.20.1.

Basic authentication using a password to Git is deprecated and will soon no longer work. Visit https://github.blog/2020-12-15-token-authentication-requirements-for-git-operations/ for more information around suggested workarounds and removal dates.

Thanks,
The GitHub Team

Für die Verwendung über die Konsole benötige ich also einen personal access token, sonst ist bald Schluß mit Lustig.

Der Anleitung folgend auf das Profil Photo klicken -> Settings -> Developer Settings -> Personal access tokens -> Generate a personal access token:

Generate new token with descriptive name and permissions. To use your token to access repositories from the command line, select repo.

Es wird ein Personal access token generiert und angezeigt. Dieser ist unbedingt zu notieren, denn er kann nicht noch einmal angezeigt werden.

"Once you have a token, you can enter it instead of your password when performing Git operations over HTTPS."

Damit ich nicht bei jedem Commit etc. den Username & Token eingeben muss, aktiviere ich das Caching der Credentials:

# Set git to use the credential memory cache
$ git config --global credential.helper cache

# Set the cache to timeout after 1 hour (setting is in seconds)
$ git config --global credential.helper 'cache --timeout=3600'

Git kann jetzt wie zuvor verwendet werden. Beim ersten Befehl muss einmalig Username & Token eingegeben werden, diese werden für die darauffolgende Stunde gecached.