Klanten realtime data laten zien in webapplicatie, hoe doe je dat? In veel moderne webapplicaties worden WebSockets gebruikt om realtime data te versturen vanuit de backend naar de front-end van de applicatie. Het WebSocket protocol is full-duplex, dit houdt in dat je niet alleen data van de server naar de client kan sturen, maar ook andersom. Voor veel applicaties betekent dit dat er niet constant ge-polled hoeft te worden voor nieuwe data, maar dat de data direct naar de client wordt gestuurd zodra deze beschikbaar is.

Hoe ging het vroeger?

Het internet is gebouwd op dit idee: client vraagt om data bij de server en de server geeft hier een antwoord op. Dit was een groot aantal jaar de manier om data van een server te krijgen, totdat in 2005 AJAX werd geïntroduceerd. Hiermee kan een client data opvragen bij de server op het moment dat zij dit nodig heeft, of kan een client pollen naar nieuwe data.

Het probleem van dit soort oplossingen is dat er relatief veel data naar de server verstuurd wordt om een beetje data terug te krijgen. Dit zorgt voor veel vertraging en is in veel gevallen ongewenst, bijvoorbeeld wanneer je een livestream op een website wilt zetten. Het grootste probleem is dat er overbodige data, bijvoorbeeld headers en cookies, of onveranderde data naar de client wordt gestuurd. Dit zorgt voor een onnodig hoge load.

Wat we echt nodig hebben is een snelle manier om data van de server naar de client te sturen, zonder dat de client hierom moet vragen. Dat is precies wat WebSockets doen.

Hoe werken WebSockets?

WebSockets bieden een constante connectie tussen de client en de server, zodat beide met elkaar kunnen communiceren wanneer hen dat uitkomt.

De client begint met het sturen van een zogenaamde handshake naar de server. Dit proces start met een normale HTTP request naar de server. Hierin zit een upgrade header, zodat de server weet dat de client een WebSocket connectie wil opbouwen met de server:

GET ws://websocket.example.com/ HTTP/1.1
Origin: http://example.com
Connection: Upgrade
Host: websocket.example.com
Upgrade: websocket

Wanneer de server dit ondersteunt zal de server reageren met de volgende reactie:

HTTP/1.1 101 WebSocket Protocol Handshake
Date: Wed, 16 Oct 2013 10:07:34 GMT
Connection: Upgrade
Upgrade: WebSocket

Nu de handshake voltooid is zal de initiële connectie vervangen worden door een WebSocket connectie die dezelfde onderliggende TCP/IP connectie gebruikt. Op dit moment kan de client of server beginnen met het versturen van data.

Met een WebSocket kan je zoveel data sturen als je maar wilt, zonder de overhead die klassieke HTTP requests met zich meebrengen. De data wordt als een bericht verstuurd. Deze berichten bestaan elk uit één of meerdere “frames”, die de data bevatten die je probeert te verzenden. Om te zorgen dat het bericht aan de andere kant op de juiste manier kan worden gereconstrueerd, wordt voor elk frame 4 tot 12 bytes aan data toegevoegd over het bericht. Dit framesysteem helpt het bericht compact te houden zonder overbodige data, om de vertraging zo kort mogelijk te houden. Daarnaast zorgt het ervoor dat een bericht gereconstrueerd kan worden als er een frame verloren gaat.

Demo time!

Na al deze theorie is het dan eindelijk tijd om wat te laten zien. Als voorbeeld gaan we Socket.io gebruiken. Socket.io is een relatief simpele Node package waarmee een socketconnectie opgezet kan worden in JavaScript. Natuurlijk zijn er nog vele andere packages die de moeite waard zijn om naar te kijken, onder andere:
•  Pusher (PHP, Node.JS, Ruby, Rails, ASP.Net, Python, Go en Java)
•  laravel-echo-server (Laravel)
•  WS (Node.JS)

We gaan in deze blogpost een chat client maken met Socket.io. Als eerste maken we een simpele HTML-pagina met een input voor je gebruikersnaam.

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>DIJ Socket.io demo</title>
</head>
<body>
  <ul class="pages">
    <li class="login page">
      <div class="form">
        <h3 class="title">Wat wordt je gebruikers naam?</h3>
        <input class="usernameInput" type="text" maxlength="14" />
      </div>
    </li>
  </ul>
  <script src=”https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js”></script>
  <script src=”https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js”></script>
</body>
</html>

Wanneer we dit gedaan hebben, maken we er nog een pagina bij met een input waarin je jouw bericht kan typen en waar alle berichten op verschijnen.

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>DIJ Socket.io demo</title>
</head>
<body>
  <ul class="pages">
   <li class="chat page">
     <div class="chatArea">
       <ul class="messages"></ul>
     </div>
     <input class="inputMessage" placeholder="Type hier..."/>
   </li>
    <li class="login page">
      <div class="form">
        <h3 class="title">Wat wordt je gebruikers naam?</h3>
        <input class="usernameInput" type="text" maxlength="14" />
      </div>
    </li>
  </ul>
  <script src=”https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js”></script>
  <script src=”https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js”></script>
</body>
</html>

Nu kun je deze pagina’s nog stylen, zodat het er ook mooi uitziet.

Als het goed is heb je nu twee pagina’s die er mooi uitzien, maar nog niks doen. Dat gaan we nu veranderen. Als eerste initialiseren we een paar elementen en maken we een aantal variabelen aan die we later gaan gebruiken.

var TYPING_TIMER_LENGTH = 400; // ms
 
var $window = $(window);
var $usernameInput = $('.usernameInput'); // Input for username
var $messages = $('.messages'); // Messages area
var $inputMessage = $('.inputMessage'); // Input message input box
 
var $loginPage = $('.login.page'); // The login page
var $chatPage = $('.chat.page'); // The chatroom page
 
var username;
var connected = false;
var typing = false;
var lastTypingTime;
var $currentInput = $usernameInput.focus();

We gaan nu de Socket.io connectie initaliseren. Hiervoor gebruiken we de demo socket van Socket.io.

var socket = io('https://socket-io-chat.now.sh/');

Nu de socketconnectie is gemaakt moeten we aan de server vertellen wat jouw username is. We doen dit wanneer er een keydown event komt. Wanneer dit gebeurt gaan we een “add user” event triggeren.

$window.keydown(event => {
  // Auto-focus the current input when a key is typed
  if (!(event.ctrlKey || event.metaKey || event.altKey)) {
    $currentInput.focus();
  }
  // When the client hits ENTER on their keyboard
  if (event.which === 13) {
    setUsername();
  }
});
 
const cleanInput = (input) => {
  return $('<div/>').text(input).html();
}
 
const setUsername = () => {
  username = 'DIJ_' + cleanInput($usernameInput.val().trim());
 
  // If the username is valid
  if (username) {
    $loginPage.fadeOut();
    $chatPage.show();
    $loginPage.off('click');
    $currentInput = $inputMessage.focus();
 
    // Tell the server your username
    socket.emit('add user', username);
  }
}

Vervolgens komt een “login” event van de server terug. Dit betekent dat we succesvol zijn verbonden met de chat.

socket.on('login', (data) => {
  connected = true;
  // Display the welcome message
  var message = "Welkom in de Socket.IO chat";
  log(message, {
    prepend: true
  });
  addParticipantsMessage(data);
});
 
const log = (message, options) => {
  var $el = $('<li>').addClass('log').text(message);
  addMessageElement($el, options);
}
 
const addMessageElement = (el, options) => {
  var $el = $(el);
 
  // Setup default options
  if (!options) {
    options = {};
  }
  if (typeof options.prepend === 'undefined') {
    options.prepend = false;
  }
 
  if (options.prepend) {
    $messages.prepend($el);
  } else {
    $messages.append($el);
  }
  $messages[0].scrollTop = $messages[0].scrollHeight;
}
 
const addParticipantsMessage = (data) => {
  var message = '';
  if (data.numUsers === 1) {
    message += "Er is 1 persoon online!";
  } else {
    message += "Er zijn " + data.numUsers + " personen online!";
  }
  log(message);
}

Nu we dit hebben, gaan we ervoor zorgen dat we berichten kunnen versturen en ontvangen.

socket.on('new message', (data) => {
  addChatMessage(data);
});
 
// LET OP AAN DEZE BESTAANDE FUNCTIE VOEGEN WE EEN PAAR REGELS TOE!!
$window.keydown(event => {
  // Auto-focus the current input when a key is typed
  if (!(event.ctrlKey || event.metaKey || event.altKey)) {
    $currentInput.focus();
  }
  // When the client hits ENTER on their keyboard
  if (event.which === 13) {
    if (username) {
      sendMessage();
      socket.emit('stop typing');
      typing = false;
    } else {
      setUsername();
    }
  }
});
 
$inputMessage.on('input', () => {
  updateTyping();
});
 
const updateTyping = () => {
  if (connected) {
    if (!typing) {
      typing = true;
      socket.emit('typing');
    }
    lastTypingTime = (new Date()).getTime();
 
    setTimeout(() => {
      var typingTimer = (new Date()).getTime();
      var timeDiff = typingTimer - lastTypingTime;
      if (timeDiff >= TYPING_TIMER_LENGTH && typing) {
        socket.emit('stop typing');
        typing = false;
      }
    }, TYPING_TIMER_LENGTH);
  }
}
 
const sendMessage = () => {
  var message = $inputMessage.val();
  // Prevent markup from being injected into the message
  message = cleanInput(message);
  // if there is a non-empty message and a socket connection
  if (message && connected) {
    $inputMessage.val('');
    addChatMessage({
      username: username,
      message: message
    });
    // tell server to execute 'new message' and send along one parameter
    socket.emit('new message', message);
  }
}
 
const addChatMessage = (data, options) => {
  // Don't fade the message in if there is an 'X was typing'
  var $typingMessages = getTypingMessages(data);
  options = options || {};
    if ($typingMessages.length !== 0) {
    options.fade = false;
    $typingMessages.remove();
  }
 
  var $usernameDiv = $('<span class="username"/>')
    .text(data.username)
    .css('color', getUsernameColor(data.username));
  var $messageBodyDiv = $('<span class="messageBody">')
    .text(data.message);
 
  var $messageDiv = $('<li class="message"/>')
    .data('username', data.username)
    .addClass(typingClass)
    .append($usernameDiv, $messageBodyDiv);
 
  addMessageElement($messageDiv, options);
}

De basis van de chatroom zou nu moeten werken, maar eigenlijk willen we nog weten wanneer er iemand weggaat of erbij komt en wanneer er iemand aan het typen is. Dit mag je nu zelf bedenken hoe dit moet werken. In de netwerktab van je inspector kun je zien welke events er allemaal voorbij komen en met welke data.


Als het goed is begin je nu een beetje te begrijpen hoe het trucje werkt. Als je dit interessant vond en er meer over wilt weten, dan zou je ook eens kunnen kijken naar bijvoorbeeld video streaming met WebSockets.

Wanneer je als bedrijf opzoek bent naar een dergelijke oplossing laat het ons dan weten, de koffie staat altijd klaar!

Geschreven door: Nick Verschoor

Meer kennis bijspijkeren? Kom dan naar onze Meetup: Ode aan de Code!

Bekijk onze Meetups

Wij zijn altijd op zoek naar getalenteerde vakgenoten!

Bekijk onze vacatures