Learn WebSockets by creating a chat app
Hey guys, I have been learning about websockets and I built a simple chat app to demonstrate how they work. Reference the github repository github.com/amschel99/Websockets.git
A simple web app that allows users to chat online.
It is built using the ws
API and the WebSocket Protocol on the frontend.
How it's built.
Create 2 folders
server
andclient
In the root run
npm i ws express unique-names-generator uuid --save
This will install all dependencies our server needs. We installed them in the root to avoid issues when deploying to heroku.
Navigate to the server folder and create an index.js
file.
This is where all the logic for the server will be written.
const ws= require("ws")
const express= require("express")
const { v4: uuidv4 } = require('uuid');
const { uniqueNamesGenerator, adjectives, colors, animals } = require('unique-names-generator');
We import the needed packages as shown above.
ws
is the package that allows our server to communicate with a client in a bidirectional way.
express
will be used to serve static files
uuid
will generate a unique id for each client that joins the chat.
unique-names-generator
will generate a unique name for any client that joins the chat.
const app= express()
app is an instance of a http server
const path= require("path")
importing the built in path module from node js
const PORT= process.env.PORT || 8080
locally, the app will run on port 8080 but after deployment, it will run on the enviroment's port number
The Client
The client is built with Html/css and Javascript
index.html
You can find the html code at github.com/amschel99/Websockets/blob/main/c..
index.css
Just some basic styles for the html at github.com/amschel99/Websockets/blob/main/c..
index.js
const url= location.origin.replace(/^http/, 'ws')
const ws= new WebSocket(url)
Most browsers support the WebSocket Protocol.
WebSocket is a class and hence we call its constructor and pass the url.
For example in development, url will be ws://localhost:8080
Back to the Server
app.use('/', express.static(path.resolve(__dirname,'../', "../client")))
const server=app.listen(PORT,()=>{
console.log("running the server")
})
const wss=new ws.Server({noServer:true},()=>{
console.log(`server is running `)
})
The code above creates an instance of a websocket server which will run on port 8080 locally. It will serve the static client folder and also run the websocket server on the same port as we will see below.
server.on('upgrade',async function upgrade(request,socket,head){
//you can handle authentication here
//return socket.end('HTTP/1.1 401 Unauthorized\r\n','ascii')
wss.handleUpgrade(request,socket,head,function done(ws){
wss.emit("connection",ws,request)
})
})
Place this code at the bottom of your file.
Basically The HTTP/1.1 protocol provides a special mechanism that can be used to upgrade an already established connection to a different protocol like WebSocket protocol, using the Upgrade header field. So the http server listens for the upgrade
event and runs the call back function. You can use an if statement to authenticate users e.g
if(not_authenticated){
return socket.end('HTTP/1.1 401 Unauthorized\r\n','ascii')
}
If a user is not authenticated, the code below never runs and hence the WebSocket connection never happens.
wss.handleUpgrade(request,socket,head,function done(ws){
wss.emit("connection",ws,request)
All the server logic
const clients=[]
wss.on("connection", (client)=>{
const shortName = uniqueNamesGenerator({
dictionaries: [adjectives, animals, colors], // colors can be omitted here as not used
length: 2
});
const client_id= uuidv4()
client['id']=client_id
client['name']=shortName
clients.push(client)
const clientNames= clients.map((client)=>{
return (({ name }) => ({ name }))(client);
})
// client.emit("users","admin",clientNames)
client.on("message", function(message){
const{event,data}=JSON.parse(message)
switch(event){
case "join":{
//here is where we should handle the logic of online users
console.log(clients.length)
client.send(JSON.stringify({event:"message",sender:"ADMIN", content:`welcome, from now on you'll be called ${shortName} `}))
client.send(JSON.stringify({event:"users",sender:"ADMIN", content:clientNames}))
const users=clients.filter((client)=>client.id!==client_id)
users.forEach((user)=>{user.send(JSON.stringify({event:"message",sender:"ADMIN",content:`${shortName} joined the chat `}))
user.send(JSON.stringify({event:"users",sender:"ADMIN", content:clientNames}))
user.send(JSON.stringify({event:"users",sender:"ADMIN", content:clientNames}))
}
)
break;
}
case "send_message":{
client.send(JSON.stringify({event:"message",sender:"you", content:data}))
const users=clients.filter((client)=>client.id!==client_id)
users.forEach((user)=>user.send(JSON.stringify({event:"message",sender:shortName,content:data})))
break;
}
case "typing":{
const users=clients.filter((client)=>client.id!==client_id)
users.forEach((user)=>user.send(JSON.stringify({event:"typing",sender:"Admin",content:`${shortName} is typing`})))
break;
}
case "stoptyping":{
const users=clients.filter((client)=>client.id!==client_id)
users.forEach((user)=>user.send(JSON.stringify({event:"stoptyping",sender:"Admin",content:`${data} is typing`})))
break;
}
default:{
client.send("something's wrong")
}
}
})
client.on("close", ()=>{
//here is where we should handle the logic of online users
clients.map((client,i)=>{
if(client.id===client_id){
clients.splice(i,1)
console.log(clientNames)
}
})
const users=clients.filter((client)=>client.id!==client_id)
users.forEach((user)=>{
user.send(JSON.stringify({event:"users",sender:"ADMIN", content:clientNames}))
user.send(JSON.stringify({event:"users",sender:"ADMIN", content:clientNames}))
user.send(JSON.stringify({event:"message",sender:"ADMIN",content:`${shortName} left the chat `}))
})
})
})
Run node index.js
to start the server
The wss
(websocketserver) listens for an event called connection
.
wss.on("connection", (client)=>{})
The callback function contains information about the client who connected. When a client is connected, there are given a unique id and unique name and there are added to the array of connected clients.
client.on("message", function(message){
const{event,data}=JSON.parse(message)
switch(event){
case "join":{
//here is where we should handle the logic of online users
console.log(clients.length)
client.send(JSON.stringify({event:"message",sender:"ADMIN", content:`welcome, from now on you'll be called ${shortName} `}))
client.send(JSON.stringify({event:"users",sender:"ADMIN", content:clientNames}))
const users=clients.filter((client)=>client.id!==client_id)
users.forEach((user)=>{user.send(JSON.stringify({event:"message",sender:"ADMIN",content:`${shortName} joined the chat `}))
user.send(JSON.stringify({event:"users",sender:"ADMIN", content:clientNames}))
user.send(JSON.stringify({event:"users",sender:"ADMIN", content:clientNames}))
}
)
break; }
`
when the server receives a message from a particular connected client, it parses the message received and extracts event and data
It checks what event it receives and for each event, it either sends a message back to the client or broadcasts the message to all the other clients through simple array methods like ``forEach```
client.on("close", ()=>{
//here is where we should handle the logic of online users
clients.map((client,i)=>{
if(client.id===client_id){
clients.splice(i,1)
console.log(clientNames)
}
})
const users=clients.filter((client)=>client.id!==client_id)
users.forEach((user)=>{
user.send(JSON.stringify({event:"users",sender:"ADMIN", content:clientNames}))
user.send(JSON.stringify({event:"users",sender:"ADMIN", content:clientNames}))
user.send(JSON.stringify({event:"message",sender:"ADMIN",content:`${shortName} left the chat `}))
})
})
})
When a clients disconnects, there are removed from the array of clients and the server broadcasts the message that the the client has disconnected to all the other connected clients.
THE CLIENT
WebSocket.prototype.emit=function (event,data){
this.send(JSON.stringify({event,data}))
}
We add an emit prototype to the class that will be used to send messages.
ws.onopen= (e)=>{
button.disabled=false
console.log(`connection open ${JSON.stringify(e)}`)
ws.emit("join")
}
When the connection is open, the clients sends a message to the server. The message contains the event which is join. The server will listen for that event and notify the client that they have joined and also broadcast to all other clients that a new client has joined. (we wrote the logic for that)
ws.onmessage= (message)=>{
const{data}=message
const{sender,content,event}=JSON.parse(data)
switch(event){
case "message":{
generateMessageEntry(content,sender)
break;
}
case "users":{
usersOnline=content
list.innerHTML=""
usersOnline.forEach((user)=>{
//list.innerHTML=""
const userParagraph=document.createElement("p")
const present=`<p>${user.name}</p>`
userTyping=user.name
userParagraph.textContent=user.name
list.appendChild(userParagraph)
})
break;
}
case "typing":{
generateTypingEntry(content,sender)
break;
}
case "stoptyping":{
typingContainer.innerHTML=""
break;
}
default:{
return null;
}
}
}
When the client receives a message from the server, it checks the event and does renders the message depending on the event received from the server.
message.addEventListener("keypress", ()=>{
ws.emit("typing",userTyping)
})
message.addEventListener("keyup", ()=>{
ws.emit("stoptyping",userTyping)
})
const sendMessage=()=>{
const messageValue= message.value
ws.emit("send_message",messageValue )
}
const generateMessageEntry=(message,type)=>{
const div=document.createElement("div")
const pmessage=document.createElement("p")
const author=document.createElement("h6")
pmessage.textContent=`${message} `
author.textContent=` ${type}`
div.appendChild(pmessage)
div.appendChild(author)
messagesContainer.appendChild(div)
}
const generateTypingEntry=(message,type)=>{
const div=document.createElement("div")
const pmessage=document.createElement("p")
const author=document.createElement("h6")
pmessage.textContent=`${message} `
author.textContent=` ${type}`
div.appendChild(pmessage)
div.appendChild(author)
typingContainer.appendChild(div)
}
button.addEventListener("click",sendMessage,false)
message.addEventListener("keypress",(e)=>{
if(e.key==="Enter"){
sendMessage()
return
}
return
})
Above is the rest of the code which is pretty intuitive. The client emits different messages based on events like keypress e.tc
The full source code for the client side login is at github.com/amschel99/Websockets/blob/main/c..
I have also added a scroll.js file for scroll-to-bottom behaviour github.com/amschel99/Websockets/blob/main/c..
DEPLOYING TO HEROKU
Make sure the package.json is in the root directory.
Add the script below.
"start": "node ./server/index.js"
git init.
git remote add origin 'github-url'.
git add .
git commit -m "deploying".
heroku create -a app_name.
git push heroku main.