User:Csmith1991/Vibe.d Documentation/websocket

From D Wiki
Jump to: navigation, search

Vibe.d Websockets Tutorial

This tutorial is extended from here. Instead of a simple counter, this provides a functioning web chat application.

WebSockets overview

WebSocket: First and foremost, a WebSocket starts with a secret WebSocket handshake for you. This secret handshake basically allows us to treat our previous HTTP(S) connection as a stateful connection. Having a stateful connection allows us to do things previously unavailable on our webpage. This connection could handle something from the simple chat client in this example, to an entire realtime video game. Additionally, the entire connection is handled over port 80, which is generally open, so there's little concern there. As of May 16, 2015, all major browsers support the upgrade request required to establish the socket connection.

NOTE The URI Scheme changes from http/https to ws/wss respectively. Without this, you're not using websockets.


Vibe API

Vibe handles everything within vibe.http.websockets. When you implement a Websocket listener you need to specify scope. This is because you're dealing with a single websocket.

The main methods you use on a websocket are:

  • read(): Read incoming data from the connection.
  • send(): Send data to the connection.

In the example we send all our data as text.

Code

As with any code example, you've probably skipped the text above, and just want a quick example of how things work. This is perfectly fine, just read the javascript function connect() and handleWebSocketConnection() from the d code.

Files

   .
   ├── public/
   |   ├── index.html    
   |   └── scripts/
   |       └── websocket.js
   ├── source/
   |   └── app.d

index.html

<!DOCTYPE html>
<html>
    <head>
        <title>WebSockets Client</title>
        <style>
            #chatLog {
                height: 400px;
                padding-bottom: 20px;
                overflow-y: scroll;
            }
        </style>
    </head>
    <body>
        <h1>WebSockets Client</h1>
        <div id="chatLog"></div><!-- this is where we'll insert chat into -->

        <div id="login">
            <!-- {which: 13} is a hack to make the button function the same as onkeypress -->
            <input id="name" type="text" placeholder="Your Name" onkeypress="startConnection(event);" /> <button id="connect" onclick="startConnection({which: 13});">Connect</button>
        </div>
        <div id="chat" style="display:none;">
            <input id="text" type="text" placeholder="press enter to submit" onkeypress="chat(event);" /> <button id="send" onclick="chat({which: 13});">Send</button><br />
            <button id="disconnect" onclick="endConnection();">Disconnect</button>
        </div>
        <script src="/scripts/websocket.js"></script>
    </body>
</html>

websocket.js

// Keep socket variable at global level
var socket = {};

function connect(name) {
    try {

        // Test if websocket doesn't exist, and if not, create a new connection
        if (! ('readyState' in socket)) {
            socket = new WebSocket(getBaseURL() + '/ws');
        }

        socket.onopen = function() {
            try {
                socket.send(name); // Tell the server who you are, handle validation server side!

                // Swap the chat and login divs around
                document.getElementById('name').value = '';
                document.getElementById('login').style.display = 'none';
                document.getElementById('chat').style.display = 'block';
                document.getElementById('text').focus();
            } catch (exception) {
                alert(exception);
            }
        }
        socket.onmessage = function(msg) {
            var msgVal = JSON.parse(msg.data); // We're anticipating messages formatted as "{'name':'Csmith1991', 'text':'example'}"
            var chatLog = document.getElementById('chatLog');
            chatLog.innerHTML += '<p>' + msgVal.name + ': ' + msgVal.text + '</p>'; // Add to the chatLog
            chatLog.scrollTop = chatLog.scrollHeight; // Scroll chatLog to bottom

        }
        socket.onclose = function() {
            socket = {}; // Remove socket connection

            // Swap the chat and login divs around. Note we don't do this until the connection has closed.
            document.getElementById('chat').style.display = 'none';
            document.getElementById('login').style.display = 'block';
            document.getElementById('name').focus();
        }
    } catch (exception) {
        alert(exception);
    }
}

function startConnection(event) {
    // 13 = enter button
    if (event.which === 13 || event.keyCode === 13 ) {
        // Connect to WebSocket server
        connect(document.getElementById('name').value);
    }
}

function chat(event) {
    if (event.which === 13 || event.keyCode === 13 ) {
        var myObj = document.getElementById('text');
        socket.send(myObj.value);
        myObj.value = '';
        myObj.focus();
    }
}

function endConnection() {
    // We want to close the connection on the server, so we create a message that the server listens for to close on
    socket.send('/close');
}

function getBaseURL() {
    // Get the WebSocket server address e.g. ws://127.0.0.1:8080

    var href = window.location.href.substring(7); // strip "http://"
    var idx = href.indexOf('/');
    return 'ws://' + href.substring(0, idx);
}

app.d

module webSocketExample;

import vibe.d;
import vibe.utils.array;

private WebSocket[] sockets;

shared static this()
{
    /// Use /ws to identify websocket requests, serve files out of the public folder otherwise
    auto router = new URLRouter;
    router.get("/", staticRedirect("/index.html"));
    router.get("/ws", handleWebSockets(&handleWebSocketConnection));
    router.get("*", serveStaticFiles("public/"));

    auto settings = new HTTPServerSettings;
    settings.port = 8080;
    settings.bindAddresses = ["::1", "127.0.0.1"];
    listenHTTP(settings, router);
}

void handleWebSocketConnection(scope WebSocket socket)
{
    // Add socket to sockets list
    sockets ~= socket;

    // Get username
    socket.waitForData(1.seconds);
    string name = socket.receiveText;

    // Server-side validation of results
    if (name !is null)
    {
        logInfo("%s connected @ %s.", name, socket.request.peer);
        sendTextToOtherClients(null, "System", name ~ " connected to the chat.");
    }
    else
    {
        // Kick person out
        socket.send("{\"name\":\"System\", \"text\":\"Invalid name.\"}");
        
        socket.close;
        sockets.removeFromArray!WebSocket(socket);
        logInfo("%s disconnected.", name);
        return;
    }

    // message loop
    while (socket.waitForData)
    {
        if (!socket.connected) break;
        // we got something
        auto text = socket.receiveText;
        
        // Close if recieve "/close"
        if (text == "/close") break;

        logInfo("Received: \"%s\" from %s.", text, name);
        // Relay text to everyone else
        sendTextToOtherClients(socket, name, text);
    }

    // Remove socket from sockets list and close socket
    socket.close;
    sockets.removeFromArray!WebSocket(socket);
    logInfo("%s disconnected.", name);
    
    sendTextToOtherClients(null, "System", name ~ " disconnected to the chat.");
}

void sendTextToOtherClients(scope WebSocket src_socket, string name, string text)
{
    foreach (socket; sockets)
    {
        // Don't send it to people who won't get it.
        if (!socket.connected) continue;
        
        logInfo("Sending: \"%s\" to %s.", text, socket.request.peer);
        // JSON encoding for simplicity
        socket.send("{\"name\":\"" ~ name ~ "\", \"text\":\"" ~ text ~ "\"}");
    }
}