Your first full-fledged example is to build a bidirectional chat using WebSocket. The end result will be a server that accepts WebSocket connections and messages for your “chat room” and fans the messages out to connected clients. The WebSocket protocol itself is simple, so to write your chat application, you will manage the collection of message data in an array and hold the socket and unique UUID for the client in locally scoped variables.
Long polling is a process that keeps a connection to the server alive without having data immediately sent back to the client. Long polling (or a long-held HTTP request) sends a server request that is kept open until it has data, and the client will receive it and reopen a connection soon after receiving data from the server. This, in effect, allows for a persistent connection with the server to send data back and forth.
In practice, two common techniques are available for achieving this. In the first technique, XMLHttpRequest
is initiated and then held open, waiting for a response from the server. Once this is received, another request is made to the server and held open, awaiting more data. The other technique involves writing out custom script tags possibly pointing to a different domain (cross-domain requests are not allowed with the first method). Requests are then handled in a similar manner and reopened in the typical long-polling fashion.
Long polling is the most common way of implementing this type of application on the Web today. What you will see in this chapter is a much simpler and more efficient method of implementation. In subsequent chapters you will tackle the compatibility issue of older browsers that may not yet support WebSocket.
Chapter 1 showed a basic server that accepted a WebSocket connection and sent any received message from a connected client to the console. Let’s take another look at that code, and add features required for implementing your bidirectional chat:
var
WebSocketServer
=
require
(
'ws'
).
Server
,
wss
=
new
WebSocketServer
({
port
:
8181
});
wss
.
on
(
'connection'
,
function
(
socket
)
{
console
.
log
(
'client connected'
);
socket
.
on
(
'message'
,
function
(
message
)
{
console
.
log
(
message
);
});
});
The WebSocketServer
provided by the popular ws
Node module gets initialized and starts listening on port 8181. You can follow this by listening for a client connection
event and the subsequent message
events that follow. The connection
event accepts a callback function where you pass a socket
object to be used for listening to messages after a successful connection has occurred. This works well to show off a simple connection for our purposes, and now you’re going to build on top of that by tracking the clients that connect, and sending those messages out to all other connected clients.
The WebSocket protocol does not provide any of this functionality by default; the responsibility for creation and tracking is yours. In later chapters, you will dive into libraries such as Socket.IO that extend the functionality of WebSocket and provide a richer API and backward compatibility with older browsers.
Figure 3-1 shows what the chat application looks like currently.
Building on the code from Chapter 1, import a Node module for generating a UUID. First things first, you’ll use npm to install node-uuid
:
% npm install node-uuid
var
uuid
=
require
(
'node-uuid'
);
A UUID is used to identify each client that has connected to the server and add them to a collection. A UUID allows you to target messages from specific users, operate on those users, and provide data targeted for those users as needed.
Next, you’ll enhance the connection to the server with identification and logging:
var
clients
=
[];
wss
.
on
(
'connection'
,
function
(
ws
)
{
var
client_uuid
=
uuid
.
v4
();
clients
.
push
({
"id"
:
client_uuid
,
"ws"
:
ws
});
console
.
log
(
'client [%s] connected'
,
client_uuid
);
Assigning the result of the uuid.v4
function to the client_uuid
variable allows you to reference it later when identifying message sends and any close
event. A simple metadata object in the form of JSON contains the client UUID along with the WebSocket object.
When the server receives a message from the client, it iterates over all known connected clients using the clients
collection, and send back a JSON object containing the message
and id
of the message sender. You may notice that this also sends back the message to the client that initiated, and this simplicity is by design. On the frontend client you don’t update the list of messages unless it is returned by the server:
ws
.
on
(
'message'
,
function
(
message
)
{
for
(
var
i
=
0
;
i
<
clients
.
length
;
i
++
)
{
var
clientSocket
=
clients
[
i
].
ws
;
console
.
log
(
'client [%s]: %s'
,
clients
[
i
].
id
,
message
);
clientSocket
.
send
(
JSON
.
stringify
({
"id"
:
client_uuid
,
"message"
:
message
}));
}
});
The WebSocket server now receives message
events from any of the connected clients. After receiving the message, it iterates through the connected clients and sends a JSON string that includes the unique identifier for the client who sent the message, and the message itself. Every connected client will receive this JSON string and can show this to the end user.
A server must handle error states gracefully and still continue to work. You haven’t yet defined what to do in the case of a WebSocket close
event, but there is something missing that needs to be addressed in the message
event code. The collection of connected clients needs to account for the possibility that the client has gone away, and ensure that before you send a message
, there is still an open WebSocket connection.
The new code is as follows:
ws
.
on
(
'message'
,
function
(
message
)
{
for
(
var
i
=
0
;
i
<
clients
.
length
;
i
++
)
{
var
clientSocket
=
clients
[
i
].
ws
;
if
(
clientSocket
.
readyState
===
WebSocket
.
OPEN
)
{
console
.
log
(
'client [%s]: %s'
,
clients
[
i
].
id
,
message
);
clientSocket
.
send
(
JSON
.
stringify
({
"id"
:
client_uuid
,
"message"
:
message
}));
}
}
});
You now have a server that will accept connections from WebSocket clients, and will rebroadcast received messages to all connected clients. The final thing to handle is the close
event:
ws
.
on
(
'close'
,
function
()
{
for
(
var
i
=
0
;
i
<
clients
.
length
;
i
++
)
{
if
(
clients
[
i
].
id
==
client_uuid
)
{
console
.
log
(
'client [%s] disconnected'
,
client_uuid
);
clients
.
splice
(
i
,
1
);
}
}
});
The server listens for a close
event, and upon receiving it for this client, iterates through the collection and removes the client. Couple this with the check of the readyState
flag for your WebSocket object and you’ve got a server that will work with your new client.
Later in this chapter you will broadcast the state of disconnected and connected clients along with your chat messages.
The simple echo
client from Chapter 1 can be used as a jumping off point for your chat web app. All the connection handling will work as specified, and you’ll need to listen for the onmessage
event that was being ignored previously:
ws
.
onmessage
=
function
(
e
)
{
var
data
=
JSON
.
parse
(
e
.
data
);
var
messages
=
document
.
getElementById
(
'messages'
);
var
message
=
document
.
createElement
(
"li"
);
message
.
innerHTML
=
data
.
message
;
messages
.
appendChild
(
message
);
}
The client receives a message from the server in the form of a JSON object. Using JavaScript’s built-in parsing function returns an object that can be used to extract the message field. Let’s add a simple unordered list above the form so messages can be appended using the DOM methods shown in the function. Add the following above the form element:
<ul
id=
"messages"
></ul>
Messages will be appended to the list using the DOM method appendChild
, and shown in every connected client. So far you have only scratched the surface of functionality that shows off the seamless messaging provided by the WebSocket protocol. In the next section you will implement a method of identifying clients by a nickname.
The WebSocket specification has been left relatively simplistic in terms of implementation and lacks some of the features seen in alternatives. In your code so far, you have already gone a long way toward identifying each client individually. Now you can add nickname identities to the client and server code:
var
nickname
=
client_uuid
.
substr
(
0
,
8
);
clients
.
push
({
"id"
:
client_uuid
,
"ws"
:
ws
,
"nickname"
:
nickname
});
The server gets modified to add the field nickname
to a locally stored JSON object for this client. To uniquely identify a connected client who hasn’t identified a nickname choice, you can use the first eight characters of the UUID and assign that to the nickname
variable. All of this will be sent back over an open WebSocket connection between the server and all of its connected clients.
You will use a convention used with Internet Relay Chat clients (IRC) and accept /nick
new_nick
as the command for changing the client nickname from the random string:
if
(
message
.
indexOf
(
'/nick'
)
==
0
)
{
var
nickname_array
=
message
.
split
(
' '
)
if
(
nickname_array
.
length
>=
2
)
{
var
old_nickname
=
nickname
;
nickname
=
nickname_array
[
1
];
for
(
var
i
=
0
;
i
<
clients
.
length
;
i
++
)
{
var
clientSocket
=
clients
[
i
].
ws
;
var
nickname_message
=
"Client "
+
old_nickname
+
" changed to "
+
nickname
;
clientSocket
.
send
(
JSON
.
stringify
({
"id"
:
client_uuid
,
"nickname"
:
nickname
,
"message"
:
nickname_message
}));
}
}
}
This code checks for the existence of the /nick
command followed by a string of characters representing a nickname. Update your nickname
variable, and you can build a notification string to send to all connected clients over the existing open connection.
The clients don’t yet know about this new field, because the JSON you originally sent included only id
and message
. Add the field with the following code:
clientSocket
.
send
(
JSON
.
stringify
({
"id"
:
client_uuid
,
"nickname"
:
nickname
,
"message"
:
message
}));
The appendLog
function within the client frontend needs to be modified to support the addition of the nickname
variable:
function
appendLog
(
nickname
,
message
)
{
var
messages
=
document
.
getElementById
(
'messages'
);
var
messageElem
=
document
.
createElement
(
"li"
);
var
message_text
=
"["
+
nickname
+
"] - "
+
message
;
messageElem
.
innerHTML
=
message_text
;
messages
.
appendChild
(
messageElem
);
}
Figure 3-2 shows your chat application with the addition of identity.
Your new function signature includes nickname
along with message
, and you can preface every message now with the client nickname. At the client’s request, you can see a nickname preceding messages rather than a random string of characters before each message.
If you were in the middle of a conversation and another person magically appeared in front of you and started talking, that would be odd. To alleviate this, you can add notification of connection or disconnection and send that back to all connected clients.
Your code has several instances where you’ve gone through the trouble of iterating over all connected clients, checking the readyState
of the socket, and sending a similar JSON-encoded string with varying values. For good measure, you’ll extract this into a generic function, and call it from several places in your code instead:
function
wsSend
(
type
,
client_uuid
,
nickname
,
message
)
{
for
(
var
i
=
0
;
i
<
clients
.
length
;
i
++
)
{
var
clientSocket
=
clients
[
i
].
ws
;
if
(
clientSocket
.
readyState
===
WebSocket
.
OPEN
)
{
clientSocket
.
send
(
JSON
.
stringify
({
"type"
:
type
,
"id"
:
client_uuid
,
"nickname"
:
nickname
,
"message"
:
message
}));
}
}
}
With this generic function, you can send notifications to all connected clients, handle the connection state, and encode the string as the client expects, like so:
wss
.
on
(
'connection'
,
function
(
ws
)
{
...
wsSend
(
"message"
,
client_uuid
,
nickname
,
message
);
...
});
Sending messages to all clients post connection is now simple. Connection messages, disconnection messages, and any notification you need are now handled with your new function.
Here is the complete code for the server:
var
WebSocket
=
require
(
'ws'
);
var
WebSocketServer
=
WebSocket
.
Server
,
wss
=
new
WebSocketServer
({
port
:
8181
});
var
uuid
=
require
(
'node-uuid'
);
var
clients
=
[];
function
wsSend
(
type
,
client_uuid
,
nickname
,
message
)
{
for
(
var
i
=
0
;
i
<
clients
.
length
;
i
++
)
{
var
clientSocket
=
clients
[
i
].
ws
;
if
(
clientSocket
.
readyState
===
WebSocket
.
OPEN
)
{
clientSocket
.
send
(
JSON
.
stringify
({
"type"
:
type
,
"id"
:
client_uuid
,
"nickname"
:
nickname
,
"message"
:
message
}));
}
}
}
var
clientIndex
=
1
;
wss
.
on
(
'connection'
,
function
(
ws
)
{
var
client_uuid
=
uuid
.
v4
();
var
nickname
=
"AnonymousUser"
+
clientIndex
;
clientIndex
+=
1
;
clients
.
push
({
"id"
:
client_uuid
,
"ws"
:
ws
,
"nickname"
:
nickname
});
console
.
log
(
'client [%s] connected'
,
client_uuid
);
var
connect_message
=
nickname
+
" has connected"
;
wsSend
(
"notification"
,
client_uuid
,
nickname
,
connect_message
);
ws
.
on
(
'message'
,
function
(
message
)
{
if
(
message
.
indexOf
(
'/nick'
)
===
0
)
{
var
nickname_array
=
message
.
split
(
' '
);
if
(
nickname_array
.
length
>=
2
)
{
var
old_nickname
=
nickname
;
nickname
=
nickname_array
[
1
];
var
nickname_message
=
"Client "
+
old_nickname
+
" changed to "
+
nickname
;
wsSend
(
"nick_update"
,
client_uuid
,
nickname
,
nickname_message
);
}
}
else
{
wsSend
(
"message"
,
client_uuid
,
nickname
,
message
);
}
});
var
closeSocket
=
function
(
customMessage
)
{
for
(
var
i
=
0
;
i
<
clients
.
length
;
i
++
)
{
if
(
clients
[
i
].
id
==
client_uuid
)
{
var
disconnect_message
;
if
(
customMessage
)
{
disconnect_message
=
customMessage
;
}
else
{
disconnect_message
=
nickname
+
" has disconnected"
;
}
wsSend
(
"notification"
,
client_uuid
,
nickname
,
disconnect_message
);
clients
.
splice
(
i
,
1
);
}
}
}
ws
.
on
(
'close'
,
function
()
{
closeSocket
();
});
process
.
on
(
'SIGINT'
,
function
()
{
console
.
log
(
"Closing things"
);
closeSocket
(
'Server has disconnected'
);
process
.
exit
();
});
});
Here is the complete code for the client:
<!DOCTYPE html>
<html
lang=
"en"
>
<head>
<title>
Bi-directional WebSocket Chat Demo</title>
<meta
charset=
"utf-8"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1"
>
<link
rel=
"stylesheet"
href=
"http://bit.ly/cdn-bootstrap-css"
>
<link
rel=
"stylesheet"
href=
"http://bit.ly/cdn-bootstrap-theme"
>
<script
src=
"http://bit.ly/cdn-bootstrap-jq"
></script>
<script>
var
ws
=
new
WebSocket
(
"ws://localhost:8181"
);
var
nickname
=
""
;
ws
.
onopen
=
function
(
e
)
{
console
.
log
(
'Connection to server opened'
);
}
function
appendLog
(
type
,
nickname
,
message
)
{
var
messages
=
document
.
getElementById
(
'messages'
);
var
messageElem
=
document
.
createElement
(
"li"
);
var
preface_label
;
if
(
type
===
'notification'
)
{
preface_label
=
"<span class=\"label label-info\">*</span>"
;
}
else
if
(
type
==
'nick_update'
)
{
preface_label
=
"<span class=\"label label-warning\">*</span>"
;
}
else
{
preface_label
=
"<span class=\"label label-success\">"
+
nickname
+
"</span>"
;
}
var
message_text
=
"<h2>"
+
preface_label
+
" "
+
message
+
"</h2>"
;
messageElem
.
innerHTML
=
message_text
;
messages
.
appendChild
(
messageElem
);
}
ws
.
onmessage
=
function
(
e
)
{
var
data
=
JSON
.
parse
(
e
.
data
);
nickname
=
data
.
nickname
;
appendLog
(
data
.
type
,
data
.
nickname
,
data
.
message
);
console
.
log
(
"ID: [%s] = %s"
,
data
.
id
,
data
.
message
);
}
ws
.
onclose
=
function
(
e
)
{
appendLog
(
"Connection closed"
);
console
.
log
(
"Connection closed"
);
}
function
sendMessage
()
{
var
messageField
=
document
.
getElementById
(
'message'
);
if
(
ws
.
readyState
===
WebSocket
.
OPEN
)
{
ws
.
send
(
messageField
.
value
);
}
messageField
.
value
=
''
;
messageField
.
focus
();
}
function
disconnect
()
{
ws
.
close
();
}
</script>
</head>
<body
lang=
"en"
>
<div
class=
"vertical-center"
>
<div
class=
"container"
>
<ul
id=
"messages"
class=
"list-unstyled"
>
</ul>
<hr
/>
<form
role=
"form"
id=
"chat_form"
onsubmit=
"sendMessage(); return false;"
>
<div
class=
"form-group"
>
<input
class=
"form-control"
type=
"text"
id=
"message"
name=
"message"
placeholder=
"Type text to echo in here"
value=
""
autofocus
/>
</div>
<button
type=
"button"
id=
"send"
class=
"btn btn-primary"
onclick=
"sendMessage();"
>
Send Message</button>
</form>
</div>
</div>
<script
src=
"http://bit.ly/cdn-bootstrap-minjs"
></script>
</body>
</html>
Figure 3-3 shows the chat application with the addition of notifications.
In this chapter you built out a complete chat client and server using the WebSocket protocol. You steadily built a simplistic chat application into something more robust with only the WebSocket API as your technology of choice. Effective and optimized experiences between internal applications, live chat, and layering other protocols over HTTP are all possibilities that are native to WebSocket.
All of this is possible with other technology, and as you’ve probably learned before, there’s more than one way to solve a problem. Comet and Ajax are both battle tested to deliver similar experiences to the end user as provided by WebSocket. Using them, however, is rife with inefficiency, latency, unnecessary requests, and unneeded connections to the server. Only WebSocket removes that overhead and gives you a socket that is full-duplex, bidirectional, and ready to rock ‘n’ roll.
In the next chapter you’ll take a look at a popular protocol for layering on top of WebSocket, to provide transport without the overhead of HTTP.