Advanced Web Technologies and Protocols

This document presents some advanced technics and protocols related to Web technologies. Some are new standards that are not fully supported by all browsers.

TypedArray & ArrayBuffer

Les TypedArray sont des représentation typées de tableaux de valeurs. La structure de donnée sous-jacente est le ArrayBuffer (un buffer binaire). Leur accès est est beaucoup plus rapide que les tableaux classiques. Plusieurs types sont disponibles :

Int8Array();
Uint8Array();
Uint8ClampedArray();
Int16Array();
Uint16Array();
Int32Array();
Uint32Array();
Float32Array();
Float64Array();

WebWorker

A simple mechanism that permits the creation of threads that run scripts. Also provides communication facilities :

WebWorkers have a different context than the main app so no shared memory. However, Transferrable Objects can be transferred from one thread to the other.

Demonstration of a WebWorker-Enabled App

https://www-apps.univ-lehavre.fr/forge/pigne/WEB-webworker-demo

The WebWorker demo Web App is a simple demonstration of the usage of WebWorkers. The app shows that heavy computation work can be done on WebWorkers without affecting the main process’ performances.

The main App

The first part of the app shows a rotating rainbow-colored disc rendered at optimal frame rate (60 fps) using the requestAnimationFrame() function. The code for this animation is located in the public/anim.js file.

The lower part is composed of a rectangular area (1000x300) filled with randomly colored pixels. This area is composed of 10 arrays of random data coding for the color of each pixel. Each array of data is given to a WebWorker that does some computation on it and then return it back to main process.

Creating workers

The app creates 10 WebWorkers and assign them a script name (ww.js) that they will load :

// Creation of 10 WebWorkers
var workers = [];
for (var i = 0; i < 10; i++) {
  workers.push(new Worker("ww.js"));
}

sending data to workers

An array of data is created foreach worker. We use TypedArray and binary buffers (ArrayBuffer). These are fast typed arrays.

var SIZE = 100 * 300 * 4;
arrayBuffer = new ArrayBuffer(SIZE);
view = new Uint8Array(arrayBuffer);
// Initialize with random values within [0,255]
// ...

Sending the data with an unique id to the worker so it can start working. The last parameter ([view.buffer]) is the reference of the array included in the data attribute that will be transferred to the worker.

var data = {
  'id': workers_id++,
  'data': view.buffer
};
worker.postMessage(data, [view.buffer]);

Receiving Data

Data can be received from the worker thanks to a message passing mechanism with event listeners.

When message is received from a WebWorker, the main app immediately renders it on the canvas. We do not wait for the animation frame event or we could loose the access to the data.

worker.addEventListener("message", handler, false);
function handler (event){
  // get index and data from this worker
  var id = event.data.id;
  var data = event.data.data;
  // create a image from the binary data
  var arr = new Uint8ClampedArray(data);
  // draw the image on the dedicated canvas (shift by the `id` value)
  ctx.putImageData(new ImageData(arr, 100, 300), id * 100, 0);
}

The WebWorker

The aim of the WebWorker, although not really efficient or realistic, is to somehow search for the average color from the data set that is given.

The worker mimics the behavior of a genetic algorithm trying to optimize the colors in the dataset. This data is represented as a 2D array. One line is seen as a solution vector (a chromosome). The set off all lines are the population of chromosomes. Given a fitness function, classical genetic algorithms (GAs) iteratively try to modify the dataset in order to improve the fitness. The GA selects 2 random chromosomes (2lines of the dataset), then create a third one that is a combination of both (the offspring chromosome). If this new chromosome has a better fitness than one of the parents it takes that parent’s place in the dataset.

The fitness here is the pairwise difference between any pair of adjacent colors of the vector. The lower the difference, the better the fitness.

When started the worker waits for messages containing the data to be used for the algorithm.

After the computation, it sends a message back to the main app transferring the data again.

postMessage({
  'id': id,
  'data': arr.buffer,
  'fitness': maxFitness,
  'round':round++,
  'index':maxIndex
}, [arr.buffer]);

Analysis

Although the algorithm needs a lot of CPU cycles the most extensive task is the exchange of information. Although this example uses transferable objects, data transfer is always a bottleneck.

WebSocket

WebSocket is an evolved communication protocol that allow full duplex communication between the server and the client. It uses an event based model so that server and client can be notified went messages arrive. The client does not have to ask the server for available data, the server can send it directly.

It relies on the underlying TCP connection opened by HTTP but does not use HTTP. It can support text of binary data.

A WebSocket Demo

https://www-apps.univ-lehavre.fr/forge/pigne/WEB-websocket-demo

This project is a simple WebSocket demo. It focusses on showing the basic mechanisms used to create a bidirectional (full duplex) communication WebSocket.

The application is a simple group chat, where any connected client receives messages sent by everyone. There is no Same-origin policy control and security involved.

The application code is two-folded: the server code and the client code.

The server

In development mode two server are used. One for the static contents of the Web app (html, js, css) and one for the WeBSocket management. The Websocket server uses the basic http project plus the ws WebSocket implementation.

The WebSocket server is created atop an HTTP server on a different port than the static assets (html, css, js files) server from Webpack. The Webpack dev server is configured to proxy the WebSocket requests to the WS server.

What the WS server does is :

What the static assets server does is :

The client

The client uses the default implementation of the WebSocket standard that is implemented on the browser (good overall support).

What the client does:

Analysis

This application is a very basic group chat with very little control and no security. The aim here is to show that a very few number of lines of code can already provide great communication facilities.

A more robust application would use a third party library that would take care of security, protocol mismatches and browser support. Socket.io is one of the most achieved projects for WebSockets.

Promise

Les Promise (promesse) sont des objets permettant d’effectuer des traitements asynchrones. Une promesse représente le résultat renvoyé par l’exécution d’une fonction asynchrone. Ce résultat peut être disponible immédiatement après l’exécution, plus tard, voir jamais.

new Promise( /* exécuteur */ function(resolve, reject) { ... } );

La fonction exécuteur prends 2 paramètres : resolve et reject. Ce sont des fonctions. l’exécuteur commence le travail asynchrone. Si le travail s’exécute sans problème la fonction resolve est exécutée, s’il y a une erreur, reject est exécuté.

function trucQuiprendDuTemps() {
  return new Promise((resolve, reject) =>
  {
    setTimeout(
      function() {
      var r = Math.random();
        if(r > 0.1) {
          resolve(r);
        } else {
          reject(r);
        }
      }, 3000
    );
  })
}

const p = trucQuiprendDuTemps();

p.then(
  (r) => {console.log("Yes!", r)},
  (r) => {console.log("Erreur ! Pas de chance", r)}
)

Fetch

Fetch est une novelle fonctionnalité (Living Standard) qui unifie et simplifie la manière de transférer de l’information entre un navigateur et un serveur. Son but est de remplacer le standard XMLHttpRequest. Elle propose une définition générique des objets Request et Response et repose sur l’utilisation des Promise.

fetch(url, options).then((response) => {
  // handle HTTP response
}, (error) => {
  // handle network error
})

Différences avec jQuerry.ajax()

examples from (https://github.com/github/fetch) :

Status différent de 2xx

fetch(...).then(function(response) {
  if (response.ok) {
    return response
  } else {
    var error = new Error(response.statusText)
    error.response = response
    throw error
  }
})

HTML

fetch('/users.html')
  .then(function(response) {
    return response.text()
  })
  .then(function(body) {
    document.body.innerHTML = body
  })

JSON

fetch('/users.json')
  .then(function(response) {
    return response.json()
  }).then(function(json) {
    console.log('parsed json', json)
  }).catch(function(ex) {
    console.log('parsing failed', ex)
  })

Response metadata

fetch('/users.json').then(function(response) {
  console.log(response.headers.get('Content-Type'))
  console.log(response.headers.get('Date'))
  console.log(response.status)
  console.log(response.statusText)
})

Post form

var form = document.querySelector('form')

fetch('/users', {
  method: 'POST',
  body: new FormData(form)
})

Post JSON

fetch('/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Hubot',
    login: 'hubot',
  })
})

File upload

var input = document.querySelector('input[type="file"]')

var data = new FormData()
data.append('file', input.files[0])
data.append('user', 'hubot')

fetch('/avatars', {
  method: 'POST',
  body: data
})

Async Functions (ES7)

the async function declaration defines an asynchronous function that returns an AsyncFunction object.

The function runs asynchronously (via the event loop) and uses a Promise to return values.

The syntax and structure of the function looks synchronous and is easier to read and write than async code.

async function name([param[, param[, ... param]]]) {
   statements
}

the await reserved keyword pauses the execution of the sync function and waits for the passed promise to resolve before resuming the execution of the async function.

Example from MDN:

function resolveAfter2Seconds() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('resolved!');
    }, 2000);
  });
}

async function asyncCall() {
  console.log('calling');
  const result  = await resolveAfter2Seconds();
  // result should be the string : "resolved!"
  console.log("result: ", result);
}

asyncCall();