IN THIS ARTICLE
Subscribe to Our Newsletter
Good News! We’ve launched an all new Chat Resource Center.
We recommend checking out our new Chat Resource Center, which includes overviews, tutorials, and design patterns for building and deploying mobile and web chat.
The dream of cloud computing is wonderful: you never run your own server, pay for electricity, or worry about network failures. Sadly, the reality is something else all together. You still have to administer the software on servers. You still need to design failover systems. You still have to secure the servers, even if they are virtual. Cloud computing requires you to build your own distributed computing infrastructure, you just don’t have to mess with the actual hardware anymore. It’s a nice dream, but cloud computing providers only deliver half of it.
The real dream is that you write some code that lives in the network. A message comes in and your code processes it, sending the messages back out. If you need to store some data you can just store it. If you need to call an external webservice you can call it. You don’t have worry about the file system or database administration. You don’t have to worry about whether to spin up 5 or 500 instances of your service. You don’t have worry about co-locating your services near where the users are. In short your code becomes truly serverless.
Distributed Servers Make Simple Code Hard
Let’s consider a simple example. Suppose you want to build a chat app for junior high students with a profanity filter. This is conceptually very simple. It’s just an if statement. If the message contains any of a list of words, send it back to the student. In practice, however, this code is much harder to build.
Making a simple text filter like this is difficult to scale; it requires sending the message from the end user, to the data stream network, back to your server, performing the computation, then back again to the network before reaching the final destination. This ‘back and forth’ adds latency and load to your server, for what is conceptually a simple if statement. And we haven’t even considered data storage and synchronization yet. Your server doesn’t even really want the messages, but it’s the only way to perform computation on data in motion.
Move the Computation to the Data
What if there was a better way? Instead of moving the data to the computation, what if you could move the computation to the data?
PubNub has always had a Data Stream Network (DSN). It’s like a CDN, but for data in motion. Up until now the DSN could route that data in motion quickly and reliably, but not actually change the data in any way. That means you can’t edit contents or perform any computation. If you want to mutate a message you’d have to take it out of the DSN to your own server, do the modifications, then send back to PubNub before it reaches the end user.
This roundabout method of realtime computing not only adds tremendous latency, but it also introduces a new single point of failure and completely destroys user locality. No matter where the data comes from, it has to go to a single server in a single location. If that server goes down, or is simply far away from the user, then the user experience massively suffers.
Now there is a better way: PubNub Functions. You really can write a simple text filter and send it into the network to be run. The code is put near the data it needs to operate on. When a message comes in the code is executed. No discrete servers. No downtime. Just simple logic running at scale. We like to say: Functions just works.
But enough about the architecture. Let’s write some code!
Building your first PubNub BLOCK
Getting started is easy. Just log into your free PubNub account.
Create a new app with a pre-generated keyset.
Now enable Functions from the ‘application add-ons’ section.
Next go to the Functions screen and create a new block.
Now add an event handler to filter the rad words. The type is set to Before Publish or Fire
so that we can filter messages before they are sent to other users.
The code in your block is simple promise-based JavaScript. Below is a simple chat filter that looks for bad words, except we will look for ‘rad’ words. We don’t want the next generation becoming too radical. ‘Excellent’ and ‘Cool’ are fine, and even an occasional ‘awesome’, but ‘gnarly’, ‘tubluar’, and ‘grody’ are right out.
export default (request) => { var radwords = ['gnarly','tubular','grody']; // fill in the final code for(var i=0; i < radwords.length; i++) { var rad = radwords[i]; if (request.message.text.indexOf(rad) >= 0) { request.message.rejected = true; request.message.reason = 'the word ' + rad + ' is too radical!'; console.log("request rejected", request.message); return request.ok(); } } return request.ok(); // Return a promise when you're done }
In a real application you’d want to delegate this filter to a 3rd party text analysis API, but for detecting radical speech this will work well enough. Now save and start your BLOCK by pressing the Save button and the ‘Start Block’ button.
Test the block out by putting in this test message, then watch the console.
{ "text": "Functions are tubular" }
It’s really that easy. You’ve built a filter that can scale to millions of annoying middle school students.
Of course this won’t be very useful without an actual chat app to go with it. PubNub makes that easy too.
Build an HTML Chat App
PubNub’s JavaScript API is very easy to use and can be embedded in any webpage (or NodeJS app, or ReactNative app, etc). Let’s start with a simple webpage:
<html> <head> <script src="https://cdn.pubnub.com/sdk/javascript/pubnub.4.0.8.js"></script> <style type="text/css"> .hbox { display: -webkit-flex; display: flex; -webkit-flex-direction: row; flex-direction: row; width: 50em; } .grow { flex: 1; } textarea { height: 30em; } #error { color: red; } </style> <body> <div class='hbox'> <textarea id='history' class='grow'></textarea> </div> <div class='hbox'> <input id='message' class='grow'/> <button id='send'>Send</button> </div> <div class='hbox' id='error'></div> <script type="text/javascript"> </script> </body> </html>
The HTML above creates a simple chat view. There is a textarea for the history above an input field and a send button. I’m using Flexbox (now supported everywhere) to create hbox
and grow
classes which let us nicely align the different views. Notice the hbox with an id of error
doesn’t have any content. We’ll dynamically add this in a moment.
It looks like this:
Now we can connect to the network. From your PubNub admin dashboard, get your subscribe and publish keys. It is these keys which let your code access PubNub’s network.
In the script
element, add this code:
// a simple JQuery like function to get elements by ID function $(id) { return document.getElementById(id); } //configure pubnub var pubnub = new PubNub({ subscribeKey:"sub-c-f065409c-76af-11e6-86e5-02ee2ddab7fe", publishKey:"pub-c-2da9d382-65a4-4592-9804-aa2f2b28e12d" });
Calling new PubNub()
sets up a connection to the data stream network with your access keys. The JavaScript API has many parameters you can tweak, but the defaults generally suffice. Next add a listener
//when message comes in pubnub.addListener({ message: function(msg) { //if rejected, show error if(msg.message.rejected) { $('error').innerHTML = msg.message.reason; return; } //append to history $('history').value += msg.message.text + '
'; } });
Every time a message comes in this listener will be called. It will check if the message was rejected. If it was then it sents the error message and returns. If the message was not rejected it appends it to the history.
//listen to the 'radchat' channel pubnub.subscribe({channels:['radchat']});
This code subscribes to the channel called radchat
. This is the same channel our block is using. You can have as many channels as you want, but a block can only operate on one channel at a time.
//which click the send button $('send').addEventListener('click',function() { //send the message pubnub.publish({ channel:'radchat', message:{text: $('message').value} }); $('error').innerHTML = ''; });
This sends a new message to the radchat
channel every time the user presses the send button. Now check that the block is still running and try sending a message. That’s it! You’ve just made a massively scalable chat app with rad word filtering.
How it works
When you write a block it is running in live in the Data Stream Network. You can send messages to the block through the dashboard or from your own client side code. When you press the start button the code for your block is instantly sent to every part of the PubNub network around the globe. Now any messages sent to your channel will be handled by an instance of your block running in the data center nearest to the message.
When you update the code for your block, the update is applied to the entire network at once. There is no down time between the old block and the new one. No messages will ever be dropped. Every message will be handled by your code.
For maximum safety and reliability, PubNub Functions uses multiple levels of security. Each block is run inside of a NodeJS virtual machine with access to a very restricted set of APIs. Block code can never access the filesystem or host operating system. Next the entire Node instance is run using cgroups to isolate each process from the others and the host operating system. The use of cgroups ensures a badly written or malicious block can’t consume all resources on the physical machine or access data from another developer’s block. It also provides extra hooks for monitoring and management.
Functions is built on top of the existing secure Data Stream Network so all communication with the outside world is secure and sensitive payloads can be further encrypted on the end device for extra end-to-end security. The Access Manager APIs provide additional access control at the channel level, to further protect sensitive data streams.
For storage, Functions provides a built in key value store called KV Store. You can store and retrieve chat messages or any other JSON-like object in the datastore using the Promise based API. The datastore provides an eventually consistent data model so computation can continue without waiting for locks or synchronization.
One of Functions overarching goals is to maintain low-latency response to realtime messages. We do this by moving computation to where the data is instead of the other way around. The code for your block is sent to multiple edge nodes around the globe simultaneously then messages are routed to the closest edge node.
Additionally, alll APIs are designed to minimize latency through the use of promises and lack of data locking. If your code needs to invoke an external webservice that might take a while to respond, the event handler can both wait for the webservice callback and also send a message to the end user immediately. The immediate message gives the user the information they need right now. When the callback completes your event handler can send an additional followup message with additional information.
Scaling with Microservices
Imagine you built a realtime voting app. 1000 students on a campus can vote on their favorite topic instantly, with a realtime vote tally. You could build this with a quick and dirty NodeJS server to calculate the totals and handle the messaging with websockets. It’s easy. Now imagine you want to scale it to next season’s audience for Dancing with the Stars. Ten million viewers at once. Oops. Not so easy.
Scaling computation is hard. Really hard. And annoying. The problem you want to solve is making a good looking voting app, not distributed synchronization and computation. PubNub Functions makes the problem simple again. It removes the single point of failure of cloud computing. No patching and rebooting the operating system. No syncing. Get back to writing useful code, not wasting time on server headaches. Every second you have to spend on infrastructure is a second you can’t spend on making your application better.
So go get started now. What will you build?