💻 Announcing WebSockets for Cloudflare Workers
Presented by: Kristian Freeman
Originally aired on June 18, 2023 @ 4:30 PM - 5:00 PM EDT
Join us for a conversation with Kristian Freeman, Cloudflare Developer Advocate, about a new feature announced today!
Read the blog post:
English
Developer Week
JAMstack
Transcript (Beta)
Hey everyone, Kristian here, Developer Advocate for Cloudflare Pages and for Cloudflare Workers, and today in this session I'm going to be talking to you about the new WebSocket support that we have just landed in Cloudflare Workers.
So in this session I'm going to first walk you through our template, and that is a new open source project that you can clone down.
You can start to get working on your own WebSocket projects, and then I'm going to of course talk about the code, how it works, and then give you a couple of resources which I'll also link right here on the screen for you to check out as kind of further research in the WebSocket space.
So I'm really excited about this because I personally didn't actually have any experience with WebSockets before working with this project and kind of wrapping my head around how it works in Workers.
I've been really impressed with how easy to use it is, how responsive, and kind of what the class of applications that this whole thing unlocks.
It's really impressive and it works basically seamlessly.
So let's start by looking at the example of what the template actually deploys, and then we'll use that as a kind of springboard to understand how the template itself works.
So here I have this WebSocket template on screen. You can see it's very, very plain HTML.
It just has a couple things here, a number of clicks and then a couple buttons, as well as this incoming WebSocket data.
So let's start by looking at our network tab inspector here, which gives us a really good sense of what is actually going on.
So I refresh the page, you can see it starts by saying opened WebSocket.
So if I look at my network tab here, you can see that it starts off by making this request here, this WebSocket request to this path.
Let's see signalnerv.workers.dev slash WS or WebSocket.
That returns a status code, a 101 switching protocols, we'll talk about that.
And it also sends this upgrade header, which is set to WebSocket.
Okay, so we are indicating to our worker, right, this is a request to workers.dev, this is a worker service function that I've set up, that this is going to be a WebSocket request.
By coming to the messages tab here, I can see that I actually have this WebSocket view, maybe I can expand this just a little bit doesn't look like it.
But if I click here, I do this, click me, you can see number of clicks goes to one.
And then I have these two messages that appeared in the WebSocket view.
First is a click. So it's literally just a piece of data that says click, which is going up, right with this upper arrow here.
And then what we get back down is this JSON payload count is set to one and TZ or timestamp is set to this timestamp, and click this a bunch of times.
And I can see every single time this click data goes up, this JSON payload comes back down.
And if I scroll down here, I can actually see that I get this list of incoming WebSocket data.
So these are a collection of each of these JSON payloads being rendered out to the screen.
But obviously, you know, in the background, the network here is sending these requests back and forth.
We can also click this simulate unknown message, this sends a message up that is called Huh, right, the message that we would assume the WebSocket doesn't understand.
And what we get back is this error unknown message received TZ with the timestamp.
And we also see that we get this unknown message received, and then error unknown message received.
So we're getting this different type of message back, which is an error.
If we send something back that the WebSocket doesn't expect.
So just to top this off, we have this entire request, you'll notice this is pending, right?
So this is because this WebSocket is still open.
If I come up here, and I click close connection, you can see that this WebSocket was open for 2.2 minutes.
And it has some examples of like content being downloaded and stuff like that.
I can also close this connection. And then if I try and do any sort of clicking here, you can see well, it leaves this error.
And that's because the WebSocket is already in closing or closed state.
So if I refresh here, let's just start from the beginning, I have this idea of opening a WebSocket connection, sending these click messages up and getting this data back, which is obviously used to update the UI here, this number of clicks, I can also send unknown messages.
And then I can send normal messages again. And when I'm done, I can close the connection and be done with the WebSocket.
So this is a very, very simple WebSocket example using workers and literally just like vanilla JavaScript.
This isn't using any sort of external libraries or anything to render this HTML.
Now let's look at the template and understand how this all fits together.
So here I've just cloned down the WebSocket template, which I will link to right here.
It's basically an open source GitHub project that does exactly what I just showed you.
So I kind of considered coding through this and just kind of doing it from scratch.
But I figured there was enough code here that I would probably mess up, honestly, and it'd be better if I just showed you how it worked.
So let's start with kind of the two main things that this project does.
So first, there is this idea of handling the WebSocket.
So my client makes a request to the worker, which says, Hey, I would like to set up a WebSocket, the worker looks for particular details, which I'll show you in a sec of how to know if it's a WebSocket.
If that's the case, it returns that WebSocket back to the client.
That's the first thing that this worker does.
The second thing is it renders a template. So you can see here, we look for this URL that comes in for our handle request.
And that comes from this add event listener here, right?
So if a new request comes in, then respond to that event of that request coming in with this handle request function.
So that will look at the URL of the request that's being made.
And in particular, it'll look at the URL path name.
So where is this request supposed to be routed to, we do a switch statement here and say if it's the route path, then return a template, which we'll look at here in just a sec.
And if it's that slash ws, which is the WebSocket, then return this WebSocket handler.
Otherwise, just return a 404 not found, we can test that real quick, of course, by just doing you know, let's say slash about, we just get this not found here.
So two routes, one is the route here, which returns this template.
And the other is the WebSocket request, which we can see here, that's this slash WebSocket path here, which we were just looking at earlier with our list of WebSockets.
So that's all pretty straightforward, I think.
Now let's understand first how the template works, because I think it's probably the easier of the two.
And then we'll dive in later to more details about it.
But for now, we have this template function, which just gets imported right here from this local template file.
This is just HTML. So we have this template string, which is literally just a huge amount of HTML, it does a bunch of stuff.
And we'll look at this in more detail a little bit later.
But basically, it just returns this function, which returns a new response, passing in the template string as the body, and then setting the header here to a content type of text HTML.
So this function simply just returns an HTML string, and that gets returned if the request is looking for that route path.
Okay, so let's look at our WebSocket handler function, which is really straightforward, it's just a couple lines of code, it takes in a request here, and it does the following.
So first, it looks for the presence of an upgrade header.
Okay, so this is a special header that tells Cloudflare Workers runtime, hey, we want to turn this request into a WebSocket.
So first, it looks for that upgrade header. And it says, if that's not set to WebSocket, just return this response with a 400 status code, hey, we are expecting a WebSocket here, and we didn't get it.
If that all checks out, okay, right, if this upgrade header is present, and it's set to WebSocket, now it's time to set up a WebSocket pair.
So the WebSocket pair is super important, it's kind of the key piece of this entire thing.
It is a collection of two WebSockets, one for the client and one for the server, they correspond to each other and the way that the client and the server interact with each other inside of a WebSocket application.
So we say new WebSocket pair, this returns an object with two WebSockets inside of it.
So I just use object dot values to set the values of those two items in the object into an array.
And then I pull client server out of that array.
Okay, then what I do is I say handle session passing in the server WebSocket into that function.
And then I also return the client WebSocket back to well the client.
And the way that we do that is we return a new response, we don't need to pass in a body here, it's just null.
And then we pass two options things in here.
One is a status which is set to 101. That's that switching protocols, which I can show you back here in the network tab in a sec.
And then I also pass in WebSocket.
So I'm saying this is going to be a response indicating we're switching protocols, and giving you the client WebSocket for, you know, connecting to our WebSocket on the server.
So you can see what this looks like here, if I open up the network tab here and I refresh.
So this is going to this client, I'll show you how it connects to the WebSocket later.
But when it does, so it makes a request to this w to the slash ws path returns a status code of 101.
And then it also has this upgrade WebSocket, which came actually from the request here, saying, Hey, this request, I want to WebSocket back.
And that's what it will return as a response.
So now let's look at our handle session here, this is going to be the function that actually sets up the server side of our WebSocket.
So if I come up here, you can see this is our handle session WebSocket.
So there's going to be two things that this does actually three things that it does that are really important.
The first of which and probably the most important is calling WebSocket dot accept.
So this is what tells the workers runtime that it should handle this WebSocket and allow interaction with that WebSocket from the client, this is really important.
So it basically tells the runtime that we're going to start doing stuff with WebSockets.
So prepare accordingly is kind of the simple way of referring to this.
So we call WebSocket dot accept. Now that we've accepted the server side of this WebSocket, we can start to do things on events when that WebSocket has any updates sent to it or anything like that.
That's done with the add event listener, which is right here, we're going to look for two events here, the first of which is called message, and the second of which is called close.
So message is going to be when new data comes in, right?
When I send a message to the WebSocket, this function is going to fire and I'm going to get access to that message as part of this callback function here, right?
The second of these is going to be the close event.
And that is when the WebSocket closes. If there's any kind of cleanup, or any sort of I don't know anything you need to do around like, hey, our WebSocket is closed, let's prepare accordingly.
This is the event where you can hook into that.
And then you can do some function, do some sort of work when that closes.
Right now I'm just console logging that close event. So here in our event listener for message.
So this is where new data comes in, right? And we have this async function, this is actually going to get what we call a message.
So I could change this here with message, that's going to be the data as well as a number of other things about like the protocol of the message, all this information, which I'll link to the documentation, I'll put it right here.
Again, you can check all of that out.
The main thing that I care about here is data, though, that's the actual data inside of that message that's being sent from the client.
So here, I'm just destructuring that data out of my message. And then I can do things with that.
So here, I'm just checking for what that value of that data is.
You'll remember in this example, if I come here and look at messages, when I send events, make this a little bit bigger.
When I send events, I'm getting this, or I'm sending this data up, right?
So click is the data I'm sending up. Or if I simulate unknown message, I'm sending up this ha, or I could send whatever, right?
This is just a plain piece of data that I'm sending up, I could send JSON, or I could send all kinds of other things too.
In this case, I'm kind of sending a simple string value up.
And what I can do inside of my worker is I can then do conditional logic based on that data.
So here I say, if data is equal to click, then I'm going to increment this count here.
So where does this count come from? Well, it's this let count equals zero up here.
So you can think of this as like anytime that my worker starts up, count is going to be reinitialized to zero.
That's not always ideal.
I'll tell you a little bit about that later when we talk about like improvements we can make to this code or other things in the worker's ecosystem that fit well with WebSockets.
But for now, you can see I just say count incremented by one, right plus equal one.
So assign count to count plus one. And then what I'm going to do is I'm going to send a message back down the WebSocket to the client.
So WebSocket dot send JSON dot stringify. And then I'm passing in account here, as well as a timestamp, which is just the current date, right?
So I'm just sending this JSON payload back to the client by calling WebSocket dot send.
So I can receive messages here, right?
WebSocket dot add event listener message. And I can send messages back down that WebSocket by calling dot send.
So if that data, as I showed you already, isn't equal to click, if it's something we don't know, then we'll send a different WebSocket message here that just says JSON dot stringify error unknown message received, and then I'll pass in the timestamp here.
So that is literally all you need to know about the server side of these WebSockets.
You can accept the WebSocket.
Well, let's actually go one step higher. First, we create a WebSocket pair, right?
That's our client and our server, we send the server, or we accept the server WebSocket up here, add any event listeners like message or close, we want to do things when there are updates to this WebSocket.
And then we also send a client WebSocket back down to the client to say this is your side of kind of the WebSocket pair.
And we're going to interact with each other based on these two WebSockets.
So that is the server side of it, like I said, and now we're going to do is talk about the client side of it, which is how we connect to the WebSocket server, how we receive messages and how we send messages inside of our HTML template.
Like I said earlier, this template file is just basically plain vanilla HTML, CSS and JavaScript.
It's very, very simple. And so I'm going to go through it here.
Let me wrap this text real quick. So you can get a better sense of what it looks like.
Also close the sidebar here, but it's really straightforward.
It's surprisingly almost exactly like what we had on the server side.
So let's just take it line by line. First, we have our style here. This is just some CSS, just to space out some, make some margin, right, for the entire thing, and then add a monospace font family here.
We then have two parts of our UI.
So our number of clicks, which is just plain text, and then the span ID num, that's what we're going to use to actually update the number of clicks.
You'll see that a little bit later.
And then we have this button ID of click, which just says click me, right?
Another paragraph here, this is our unknown message. And then we send a, we have this button ID equals unknown, which just simulates an unknown message.
I also spelled recognize their wrongs. Let me fix that real quick. So that's for unknown or ha message, right?
Like, huh, we send some weird data up that the web socket doesn't know how to respond to.
Then we have our, uh, closing our connection, right?
So button ID, close, close connection. And then we have to sort of debug or like logging things.
So P ID equals error and it has a red color. So that's our error message when we simulate an unknown message.
And then we also have our incoming web socket data, which is just a list, right?
An unordered list of, uh, with an ID of events.
This is where we append that incoming web socket data too. So that's all of the sort of layout stuff.
Let's talk about the code in particular, the web socket stuff is so similar to, um, to what we did on the server side that it shouldn't really be surprising in any way.
Um, first we instantiate this web socket variable.
That's the web socket to here in just a second. Um, and then we have this async function web socket.
So it takes a URL in, and then it says, uh, web socket is equal to a new instance of the web socket class where we pass in that URL.
We'll look at that URL in just a sec.
And then we wait for that web socket to come back.
We say, if, uh, there is no web socket, let's throw an error, right? The server didn't accept the web socket in some way, something about this instantiation didn't work.
If we move past that though, say we're here in this line or below, we now have a fully functional web socket.
And all we need to do to interact with that web socket is, um, add event listeners to it for opening the web socket, right?
On a server, we had our close event here on our, um, on our client.
We'll make use of the open event. So we'll say console log opened web socket, and then we'll say, update our count to zero, right?
We're instantiating, uh, this instance of the web socket, and then also setting up our UI at kind of a blank state.
And then we just want to add an event listener for message. This is just like on the server side, right?
We have this message that comes in. I'm going to destructure data out of it.
In this case, I know that, um, the data that's going to coming back down is a Jason string, right?
So I'm just going to Jason parse it.
I'm going to get these values out. So count TZ for time zone and error. And that is going to be all of the information from this, uh, from this data coming back down from the web socket.
I'm going to say, add new event data. That's going to append it into the UI.
And then I'm going to do two things. So if there's an error, then set the error message to error.
Otherwise let's clear the error message here.
So just passing no argument, we'll clear it out. And then let's update the count based on the value coming back from our web socket data.
So that just means when I have it set to one is the count.
And then I click it, the data I'm going to get back from my web socket will be two.
So let's update the count to, uh, two.
So that should all be pretty straightforward. Now, when it comes to the URL, there's, I would say this is like probably on the verge of being trickery and like something kind of funky, but I'll just make sure that it makes sense to you before we continue.
When we make our web socket URL, what we're actually going to do is we're just going to use the current URL, which in this case, if I come back here is web socket template.signalner .workers.dev.
I can actually show you what this looks like here in the, in the, uh, console here.
So if I say window location, that is a instance of the location class, right?
So if I call it to string here, this is just going to be the URL of my current page, right?
Wherever this JavaScript is executing that's on this page.
So I'm going to make a new URL that is window dot location.
Oh, whoops. Make this, uh, a variable. Did I already define that really?
Okay. Well, it's still worked. It seems like, Oh, well that's because it's running right here.
Okay. So I guess I can't run it in the console with the exact same thing.
So let's say URL example. Okay. So that is our URL here, right?
That's this window location. That's where we are on our, uh, in our JavaScript console.
So what we want to do to make this a web socket URL is two things. So one, we want to assign the protocol.
Actually, it'll be URL example, right? Not URL.
So URL example.protocol, which is currently HTTPS. I'm going to assign that to WSS for secure web socket.
So this is like the HTTPS is to WSS what HTTP is to WS.
So that extra S is for like secure. Okay. So now we have URL example. Let's put that to a string.
And now we have a web socket connection to web socket template.signalnerv.workers.dev.
The only other thing we need to do here is give it a path name of slash WS for our web socket route.
And now if we make that a string, that is our final URL for connecting to the web socket route in our workers serverless function.
And to do that, what we're going to do is we're going to attach a couple events to buttons in our UI.
So for instance, document query selector, uh, hash click, which is our, uh, where is it here?
Our button with an ID of click. That's what increments our counter here, right?
And what happens when we click that button?
It's very, very simple. We just send data up the web socket. So WS dot send, and then give it this click value here that will send data up the web socket.
And then when the data comes back, it'll hook into this, add event listener message.
So we click to send a, or we, I should say we send data up and then we get a message back down, right?
That's the sort of basic flow here. Literally everything else here is just UI stuff.
Update count is just a, uh, basically a string value.
In this case, that's a number, but it's effectively string-like where we say document query selector, num set inner text to count.
Okay. Add new event.
This just finds the events list, creates a list element, and then adds the content of our data into that.
So you can see that is, uh, that's these, right? So these are list elements.
They just take the data, turn it into a string, and then just, uh, kind of prepend it or append it to the top of the list so that we get this, uh, list of data here, right?
So that's how that works. And then finally our close connection here, which just, um, first says web socket dot close.
So let's close our client web socket.
That'll close it on the server by the way, too. Um, and then, um, and then just clear out everything else on the UI.
So find the, um, list of data that we had had make it empty here.
That's this update the count back to zero because we've lost all of our accounts, right?
We've closed this connection and then clear out the error message that just happens when we click this close event here, it just calls this close connection function.
And then we have the set error message here.
I know I just skipped a line, but I'll get to that in just a sec.
Um, our set error message here just finds our hash error and then sets the text to the value of the message.
If it's here, otherwise it just clears out completely.
So if I send an error message, um, you'll get that rendered there. And then the final thing is sending an unknown message.
So if I refresh here and then I click simulate unknown message, you can see it gets this, uh, unknown message received.
Well, how does that work? We just say WS dot send, and then we literally just send some random value here, like, huh, for some random message, right?
So we can call WS dot send and send anything up the web socket. And in the server that will be, um, that'll correspond here to this data where we can do things based on what that data that's coming up to the web socket, um, is let's see, did I miss anything?
I think that is, that is everything in this template. There's a fair amount of things going on, but I think once you start to understand the web socket component of it, like really that aspect of it is very simple because working with web sockets is really simple.
The template has some particular stuff.
I'm sure someone could write this and react and it would be a lot cleaner. Um, but you know, for now, I think this is a pretty straightforward look at how you can use web sockets in workers.
There is one last caveat that I want to talk about, and I think it's pretty important, but also a really interesting thing that workers does that not a lot of other places can deal, which is if I refresh this and let's say I just start clicking away, right?
Five, six, seven, eight, nine, 10.
Now what's going to happen when I refresh this page, if you haven't seen it already, you know, I have this active web socket.
Now, if I refresh, everything is going to be gone, right?
So why is that? Well, the reason for that is that workers are, uh, they're stateless, right?
They're serverless functions without any concept of state.
And so when I refresh this, everything just kind of disappears, um, which is unfortunate.
And because of that web sockets on their own are, um, for the most part, not particularly useful for like real stateful applications, because they also do not have any concept of state attached to them.
So the solution for that in making these actually useful is durable objects.
So durable objects were announced back in September of last year.
And now as of the end of March, they are in open beta and durable objects are a way to attach consistency to your web sockets.
And in fact, when we talked about durable objects for the first time back in September, we also showed the beginnings of this web socket stuff.
They really work hand in hand together.
And what this means is that for this user, right? So for say me or for you or anyone who's using this web socket template, I can pass a durable object, a state object to this worker and say, Hey, let's keep track of all of the clicks for this user.
So if the web socket, you know, gets disconnected, if I refresh it, something like that, I will continue to have the state because it will be persisted inside of a durable object, which I can then retrieve and start working with again.
Um, you know, if I refresh the page. So a great example of this is the edge chat demo, which we actually announced with our, um, post for the announcement of durable objects.
This runs on web sockets. I can just say Christian here, I'll find a, let's say a room like Christian's room.
And this is going to be a web socket based connection.
You can see if I do inspect here, let's open up the network and let's just send a message.
So testing, right? Or maybe I need to, let's refresh.
So you can actually see the web socket. So Christian, right?
So here's our web socket here, right? API slash room slash Christians room slash web socket, right?
So this is our web socket, I can send a message up, I send a message up, I get this value back here.
I can also open this in a new, let's make this Christian to, you can see I got a message here that said, join Christian to, and I can say, Hello, I get that message back here, right name, message timestamp.
But if I refresh here, let's say Christian to again, now you can see I get all of that old stuff back here, right?
I get this list of data. Same thing.
If I refresh on this end, and I say Christian, you can see that I got this big list of messages back when I joined my when I joined my web socket to kind of show you the history of everything in this room.
So durable objects, you know, really is kind of the missing piece for if you're working with with web sockets, and you need state, right?
Everyone needs state basically across the board. And durable objects is the way to do that.
So if you're interested in that, I'll put more sets of links here below.
You can check out and you know, the chat demos open source, for instance, if you want to look at a more complex version of what web sockets look like, in kind of productiony use cases, but either the web socket template on its own, just plain stateless interaction on a web socket, or with durable objects, something like chat, either of these are super compelling use cases for workers for web sockets, durable objects, and we hope that you will check them out.
So let us know in our discord, which is discord.gg slash Cloudflare dev, I'll put that here as well.
Let us know what you think about web sockets and durable objects.
And if you build anything cool with it, be sure to let us know there.
And thanks. Enjoy the rest of your developer week