Skip to content

p5.js, node.js, socket.io

Lauren Lee McCarthy edited this page Jan 19, 2021 · 27 revisions

When you write p5, you are doing what is called "client-side" programming. The JavaScript code we write is executed by the browser itself and runs on the user's local computer. For controlling the local behavior of a web page this is enough. However, there are lots of tasks that web applications perform that can only be accomplished with server-side programming. Here are some examples of functionality that would require server-side programming.

  • Storing data over time (how many times have users visited this page? What are the high scores of this game? etc.)
  • User login / authentication.
  • Accessing APIs.
  • Connecting to hardware (such as arduino or kinect) -- this is a unique sort of scenario where we may run a server-side program locally on a client machine to transfer data from a hardware device that the browser does not have access to.
  • Before we start writing our own servers, we should note that we have been running server-side programs all along. Every time we type python -m SimpleHTTPServer (or python -m http.server if using python3) we are running a simple server program written in python. What does this server program do? It takes requests ("Could I please have that index.html file?") and responds to those requests ("Here, enjoy this index.html file!"). This is all this simple HTTP server can do, handle HTTP Requests and serve up files.

We don't need python for this. We can write server-side programs in all sorts of languages (php, ruby, java, the list goes on) including JavaScript. For example, here's a simple HTTP Server written in JS.

let handleRequest = function (request, response) {
  response.writeHead(200, {'Content-Type': 'text/plain'});
  response.end('Hello World\n');
};

const http = require('http');
let server = http.createServer(handleRequest);
server.listen(8080);

This server is even more simple than the python one. It doesn't even serve any files. No matter what the request might be, the only response ever is "Hello World". This server is written in JavaScript using node.js, a platform for building JavaScript network applications. We'll be getting to the specifics of how this example works later.

Hello World Node

The first thing we need to do to get up and running with server-side programming in JavaScript is to install node. Once you've installed node, to make sure it's working just open up terminal and type node. If it's installed you'll see a prompt. You can type any JavaScript here just like in the browser console to test (hit ctrl-c twice to exit.)

Now make any old file with JavaScript code in it. For example, make a file called "hello.js" with:

console.log('Hello!');

Now back in terminal type node hello.js. You should see Hello! back in the console.

Congratulations, you are now executing server-side JS code via the command line!

Hello World HTTP Server

The next step we need to do is to write a server program that handles HTTP requests. An HTTP request is that thing that happens when you type something into the address bar of your web browser. http://www.google.com makes a request to google's server which then sends you a response (filled with the HTML found in google's homepage.)

To write a Node server that handles requests, we write a function that receives a request argument and acts on a response argument.

let handleRequest = function (request, response) {
  // Handle the request with a response
};

Now we pass that function as a callback to a createServer() method and listen on a port number.

let server = http.createServer(handleRequest);
server.listen(8080);

We are missing one line of code, however. Where does http come from? We need to import the Node "http" module. const http = require('http'); Node comes with a few built-in modules, and there are 70,000 and counting more you can install via npm.

Let's put all the code from above together and add one more line. We'll send back the text "Hello World!" for every HTTP request made to the server.

// This function handles an incoming "request"
// And sends back out a "response";
let handleRequest = function (request, response) {
  response.writeHead(200, {'Content-Type': 'text/plain'});
  response.end('Hello World\n');
};

// HTTP module
let http = require('http');

// Create a server with the handleRequest callback
let server = http.createServer(handleRequest);
// Listen on port 8080
let io = require('socket.io')(server);

console.log('Server started on port 8080');

If we save this as server.js we can then execute the server via node server.js. Navigate to your browser and type localhost:8080 into the address bar and you should see "Hello world".

Serving a p5 sketch

We certainly aren't the first to build a server application with Node. And because just about all servers need a lot of the same code, many developers will use an abstraction on top of Node such as express, a web application framework for node. The getting started with Express guide will give you a good basic sense of how such a framework works.

For us, we're going to bypass express and do a couple more basic things with Node as it relates to p5. For example, how would we serve a p5 sketch? The first thing we need to learn is how to use the file system module to read in a file (say index.html) and write the contents out as a response to a request.

// The file system module
const fs = require('fs');

function handleRequest(request, response) {
  // User file system module to read index.html file
  fs.readFile(__dirname + '/index.html',
    // Callback function for reading
    function (err, data) {
      // if there is an error
      if (err) {
        response.writeHead(500);
        return response.end('Error loading index.html');
      }
      // Otherwise, send the data, the contents of the file
      response.writeHead(200);
      response.end(data);
    }
  );
}

We're really living in JavaScript land now. The readFile() function takes two arguments. The first argument is the path of the file we want to read and the second is a function that will be triggered once the file has been read. Note how we are passing in an anonymous function as an argument to the readFile() function!

While this server works, it will still only send back index.html no matter what the user actually requests. What if you are serving multiple pages including html, JS, and CSS files? In this case, we'll want to read in the actual request from the user via request.url. We can then serve the file back up with the appropriate type (looking at its file extension).

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

function handleRequest(req, res) {
  // What did we request?
  let pathname = req.url;
  
  // If blank let's ask for index.html
  if (pathname == '/') {
    pathname = '/index.html';
  }
  
  // Ok what's our file extension
  let ext = path.extname(pathname);

  // Map extension to file type
  const typeExt = {
    '.html': 'text/html',
    '.js':   'text/javascript',
    '.css':  'text/css'
  };

  // What is it?  Default to plain text
  let contentType = typeExt[ext] || 'text/plain';

  // Now read and write back the file with the appropriate content type
  fs.readFile(__dirname + pathname,
    function (err, data) {
      if (err) {
        res.writeHead(500);
        return res.end('Error loading ' + pathname);
      }
      // Dynamically setting content type
      res.writeHead(200,{ 'Content-Type': contentType });
      res.end(data);
    }
  );
}

This handleRequest() method will serve a p5 sketch including an index.html, sketch.js, and style.css. You can see the full example here.

Sockets

If we want a client to have a synchronous connection with other clients (for example, in a chat application) we can use web sockets. With p5, for example, we could send the mouse location from one sketch to another to create a multi-user whiteboard application. To accomplish this, we'll need to install the socket.io module.

$ npm install socket.io

Now in our server program, we can listen for socket connections.

let server = http.createServer(handleRequest);
server.listen(8080);

let io = require('socket.io').listen(server);

The io object can be passed callback functions to handle events. For example, when a new connection is made, we could pass in a function that prints out a notice to the console.

// Register a callback function to run when we have an individual connection
// This is run for each individual user that connects
io.on('connection', function (socket) {
    console.log("We have a new client: " + socket.id);
  }
);

The anonymous callback function receives a socket argument. This socket argument is a reference for the current client connected. That client can also be passed callback functions. For example, here is a callback for when a client disconnects.

  // We are given a websocket object in our function
  function (socket) {
    console.log("We have a new client: " + socket.id);
    socket.on('disconnect', function() {
      console.log("Client has disconnected");
    });
  }

We can also create custom callbacks for certain kinds of events. Let's make up an event called "mouse." Our p5 sketch will send mouseX and mouseY for each "mouse" event. The server can handle that event by sending back that data to any other connected clients.

    socket.on('mouse',
      function(data) {
        // Data comes in as whatever was sent, including objects
        console.log("Received: 'mouse' " + data.x + " " + data.y);      
        // Send it to all other clients
        socket.broadcast.emit('mouse', data);
      }
    );

Now we have a socket server that receives mouse coordinates and passes them back to all other connected clients. (Note that the server does not broadcast the data back to the sender itself; that can be accomplished with io.sockets.emit('mouse', data);.)

All we have left to do is broadcast and receive the mouse values from our p5 sketch itself. Remember this is the part that is executed by the client browser itself and is not part of the server program.

First, we have to include socket.io in index.html. Insert this line before the line that includes the sketch file:

<script language="javascript" type="text/javascript" src="/socket.io/socket.io.js"></script>

Then, we can use socket.io in setup() like this:

let socket = io();

function setup() {
  
}

Once we have our socket, we can broadcast mouseX and mouseY. Let's do it whenever the user drags the mouse.

function mouseDragged() {
  // Make a little object with mouseX and mouseY
  let data = {
    x: mouseX,
    y: mouseY
  };
  // Send that object to the socket
  socket.emit('mouse',data);
}

Oh, and we forgot one more thing. We need to do something with the data when we receive it! Here we'll create an anonymous function that will handle our "mouse" event.

function setup() {
  // We make a named event called 'mouse' and write an
  // anonymous callback function
  socket.on('mouse',
    function(data) {
      // Draw a blue circle
      fill(0,0,255);
      noStroke();
      ellipse(data.x,data.y,80,80);
    }
  );
}

See the full example for all the pieces working together.

Tom Igoe also has a great tutorial for using a web socket to send data from an arduino.

Deploying your app

This wiki only covers how to set up and run simple node servers locally. When you want to release your app into the wild, you'll need an actual server to run your app on.

Adapted from tutorial written by Dan Shiffman for ITP Creative JS.