WE WILL BE USING SOCKET.IO FOR LIVE CHAT. SOCKET.IO OFFICIAL TUTORIAL
WE WILL START WITH 1. PROJECT INITIALIZATION,
const express = require("express");
const { createServer } = require("node:http");
const app = express();
const server = createServer(app);
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`app listening to ${PORT}`);
});
2. SERVING HTML
const express = require("express");
const { createServer } = require("node:http");
const { join } = require("node:path");
const app = express();
const server = createServer(app);
CREATES A NEW HTTP SERVER AND TELLS IT TO USE THE EXPRESS APP TO HANDLE
INCOMING HTTP REQUESTS.
app.get("/", (req, res) => {
res.sendFile(join(__dirname, "index.html"));
});
INDEX.HTML
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Socket.IO chat</title>
<style>
body { margin: 0; padding-bottom: 3rem; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
#form { background: rgba(0, 0, 0, 0.15); padding: 0.25rem; position: fixed; bottom: 0; left: 0; right: 0; display: flex; height: 3rem; box-sizing: border-box; backdrop-filter: blur(10px); }
#input { border: none; padding: 0 1rem; flex-grow: 1; border-radius: 2rem; margin: 0.25rem; }
#input:focus { outline: none; }
#form > button { background: #333; border: none; padding: 0 1rem; margin: 0.25rem; border-radius: 3px; outline: none; color: #fff; }
#messages { list-style-type: none; margin: 0; padding: 0; }
#messages > li { padding: 0.5rem 1rem; }
#messages > li:nth-child(odd) { background: #efefef; }
</style>
</head>
<body>
<ul id="messages"></ul>
<form id="form" action="">
<input id="input" autocomplete="off" /><button>Send</button>
</form>
</body>
</html>
3. INTEGRATING SOCKET.IO
TO INSTALL : npm install socket.io
INDEX.JS
const express = require("express");
const { createServer } = require("node:http");
const { join } = require("node:path");
const { Server } = require("socket.io");
io.on('connection', (socket) => {
console.log('a user connected');
});
INDEX.HTML
A SCRIPT TAG IS ADDED.
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
WE CAN ALSO USE A TAG INSTEAD
(OR)
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
</script>
A DISCONNECT BUTTON IS ADDED INORDER TO CHECK IN FURTHER CASES.
IN INDEX.HTML
<form id="form">
<input id="input" placeholder="type here" />
<button type="submit">Send</button>
<button id="toggle-btn">Disconnect</button>
</form>
INDEX.JS
socket.on("disconnect", () => {
console.log(`user disconnected:${socket.id}: `);
});
4. EMITTING EVENTS
WE CAN SEND AND RECEIVE ANY EVENTS.
INDEX.HTML
<script>
const socket = io();
const form = document.getElementById("form");
const input = document.getElementById("input");
form.addEventListener("submit", (e) => {
e.preventDefault();
if (input.value) {
socket.emit('chat message', input.value);
input.value = '';
}
});
in INDEX.JS
io.on("connection", async (socket) => {
socket.on('chat message', (msg) => {
console.log('message: ' + msg);
});
});
5.BROADCASTING
IN ORDER TO SEND AN EVENT TO ANYONE, WE USE io.emit()
IF YOU WANT TO SEND A MESSAGE TO EVERYONE EXCEPT FOR A CERTAIN EMITTING SOCKET, WE USE BROADCAST FLAG.
io.on('connection', (socket) => {
socket.broadcast.emit('hi');
});
io.on('connection', (socket) => {
socket.on('chat message', (msg) => {
io.emit('chat message', msg);
});
});
IN INDEX.HTML
socket.on("chat message", (msg) => {
const item = document.createElement("li");
item.textContent = msg;
messages.appendChild(item);
window.scrollTo(0, document.body.scrollHeight);
6. HANDLING DISCONNECTIONS
TO HANDLE DISCONNECTIONS, WE USE CONNECTION STATE RECOVERY.
THIS FEATURE WILL TEMPORARILY STORE ALL THE EVENTS THAT ARE SENT BY THE SERVER AND WILL TRY TO RESTORE THE STATE OF A CLIENT WHEN IT RECONNECTS.
IN INDEX.JS
const io = new Server(server, {
connectionStateRecovery: {},
INDEX.HTML
<button id="toggle-btn">Disconnect</button>
const toggleButton = document.getElementById("toggle-btn");
toggleButton.addEventListener("click", (e) => {
e.preventDefault();
console.log(socket);
if (socket.connected) {
toggleButton.innerText = "Connect";
socket.disconnect();
} else {
toggleButton.innerText = "Disconnect";
socket.connect();
}
});
7.SERVER DELIVERY
UPON RECONNECTION, THE MESSAGES SHOULD BE SENT IN THE SAME ORDER.
WE NEED TO STORE THE MESSAGES IN A DATABASE.
HERE WE USE MONGODB.
INDEX.JS
const mongoose = require("mongoose");
mongoose
.connect(
"mongodb+srv://marsakatlaabhishek7168:abhi7168@cluster0.sixqbxk.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0"
)
.then(() => {
console.log("DB connected");
});
const MessageSchema = mongoose.Schema({
client_id: String,
clientOffset: {
type: String,
unique: true,
},
content: String,
});
const Message = new mongoose.model("Message", MessageSchema);
socket.on("chat message", async (msg, clientOffset, callback) => {
let newMessage;
try {
newMessage = await Message.create({
client_id: socket.id,
clientOffset: clientOffset,
content: msg,
});
console.log(newMessage);
}
catch (e) {
return;
}
io.emit("chat message", msg, newMessage._id);
INDEX.HTML
const socket = io({
auth: {
serverOffset: 0,
}
socket.on("chat message", (msg, serverOffset) => {
const item = document.createElement("li");
item.textContent = msg;
messages.appendChild(item);
window.scrollTo(0, document.body.scrollHeight);
socket.auth.serverOffset = serverOffset;
});
ALSO IN INDEX.JS
IF SOCKET IS NOT RECOVERED.
if (!socket.recovered) {
try {
const serverOffset = socket.handshake.auth.serverOffset;
SERVER OFFSET - ID OF THE LAST MESSAGE SEEN BY USER
let query = {};
if (serverOffset && mongoose.Types.ObjectId.isValid(serverOffset)) {
IF A VALID SERVEROFFSET EXISTS , ALL THE MESSAGES AFTER THAT (GREATER THAN THAT ID) ARE
FETCHED
query._id = { $gt: mongoose.Types.ObjectId(serverOffset) };
}
const messages = await Message.find({ query }).sort({ _id: 1 });
SORTING MESSAGES OLDER TO NEWER
messages.forEach((messages) => {
socket.emit("chat message", messages.content, messages._id);
});
} catch (e) {
console.log("something went wrong!");
}
}
8. CLIENT DELIVERY
WHEN A CLIENT GETS DISCONNECTED, ANY CALL TO SOCKET.EMIT() IS BUFFERED UNTIL RECONNECTION.
BUT IN SOME CASES, MESSAGES MIGHT BE LOST.
- SERVER CRASHED
- DB TEMPORARILY NOT AVAILABLE
WE CAN USE RETIRES OPTION.
INDEX.HTML
const socket = io({
auth: {
serverOffset: 0,
},
// enable retries
ackTimeout: 10000,
retries: 3,
})
form.addEventListener("submit", (e) => {
e.preventDefault();
if (input.value) {
const clientOffset = `${socket.id}-${counter++}`;
socket.emit("chat message", input.value, clientOffset);
input.value = "";
}
});
INDEX.JS
socket.on("chat message", async (msg, clientOffset, callback) => {
let newMessage;
try {
newMessage = await Message.create({
client_id: socket.id,
clientOffset: clientOffset,
content: msg,
});
console.log(newMessage);
} catch (e) {
MONGO DB CONSTRAINT
if (e.code === 11000) {
callback();
} else {
// NOTHING HERE, USER WAITS UNTIL RECONNECTION
}
return;
}
io.emit("chat message", msg, newMessage._id);
callback();
});
9. SCALING HORIZONTALLY
WE MIGHT HAVE TEMPORARY NETWORK INTERRUPTIONS . INORDER TO SUPPORT THOUSANDS OF CLIENTS, WE USE HORIZONTAL AND VERTICAL SCALING.
HORIZONTAL SCALING (SCALING OUT) - ADDING NEW SERVERS
VERTICAL SCALING (SCALING IN) - ADDING MORE RESOURCES
BY DEFAULT NODE.JS RUNS JS CODE IN A SINGLE THREAD, WHICH MEANS THAT EVEN WITH A 32 CPU CORE ONLY ONE CORE WILL BE USED.
WE WILL ALSO NEED A WAY TO FORWARD EVENTS BETWEEN THE SOCKET.IO SERVERS.
THIS COMPONENT USED TO FORWARD EVENTS IS CALLED ADAPTER.
INDEX.JS
npm install @socket.io/cluster-adapter
IN INDEX.JS
onst { availableParallelism } = require("node:os");
const cluster = require("node:cluster");
const { createAdapter, setupPrimary } = require("@socket.io/cluster-adapter");
const { on } = require("node:events");
if (cluster.isPrimary) {
const numCPUs = availableParallelism();
for (let i = 0; i < numCPUs; i++) {
cluster.fork({
PORT: 3000 + i,
});
}
return setupPrimary();
}
const io = new Server(server, {
connectionStateRecovery: {},
adapter: createAdapter(),
});
WE HAVE ALSO IMPLEMENTED HOW TO DISPLAY USER NAMES (IN OUR CASE SOCKET ID'S) WHEN A NEW USER JOINS THE CHAT.
INDEX.JS
let userList = [];
io.on("connection", async (socket) => {
console.log(`user connected: ${socket.id}`);
userList.push(socket.id);
console.log(userList);
io.emit("userList", userList);
socket.broadcast.emit("system message", `${socket.id} has joined the chat`);
socket.on("disconnect", () => {
console.log(`user disconnected:${socket.id}: `);
userList.pop(socket.id);
socket.broadcast.emit("system message", `${socket.id} has left the chat`);
io.emit("userList", userList);
});
INDEX.HTML
<body>
<nav class="navbar bg-body-tertiary">
<div class="container-fluid">
<span class="navbar-brand mb-0 h5" id="userList"></span>
</div>
</nav>
<ul id="messages"></ul>
const userListElement = document.getElementById("userList");
socket.on("system message", (msg) => {
const item = document.createElement("li");
item.textContent = msg;
item.style.fontStyle = "italic";
item.style.color = "grey";
item.style.textAlign = "center";
messages.append(item);
});
NOTE : THERE WERE MANY ERRORS AFTER FEW TESTS
THE FINAL CODE:
INDEX.HTML
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>socket</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<style>
body {
margin: 0;
padding-bottom: 3rem;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif;
}
#form {
background: rgba(0, 0, 0, 0.15);
padding: 0.25rem;
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
height: 3rem;
box-sizing: border-box;
backdrop-filter: blur(10px);
}
#input {
border: none;
padding: 0 1rem;
flex-grow: 1;
border-radius: 2rem;
margin: 0.25rem;
}
#input:focus {
outline: none;
}
#form > button {
background: #333;
border: none;
padding: 0 1rem;
margin: 0.25rem;
border-radius: 3px;
outline: none;
color: #fff;
}
#messages {
list-style-type: none;
margin: 0;
padding: 0;
}
#messages > li {
padding: 0.5rem 1rem;
}
#messages > li:nth-child(odd) {
background: #efefef;
}
</style>
</head>
<body>
<nav class="navbar bg-body-tertiary">
<div class="container-fluid">
<span class="navbar-brand mb-0 h5" id="userList"></span>
</div>
</nav>
<ul id="messages"></ul>
<form id="form">
<input id="input" placeholder="type here" />
<button type="submit">Send</button>
<button id="toggle-btn">Disconnect</button>
</form>
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
<script>
let counter = 0;
let clientServerOffset = null;
const socket = io({
ackTimeout: 10000,
retries: 3,
auth: {},
});
const form = document.getElementById("form");
const input = document.getElementById("input");
const messages = document.getElementById("messages");
const toggleButton = document.getElementById("toggle-btn");
const userListElement = document.getElementById("userList");
toggleButton.addEventListener("click", (e) => {
e.preventDefault();
console.log(socket);
if (socket.connected) {
toggleButton.innerText = "Connect";
socket.disconnect();
} else {
toggleButton.innerText = "Disconnect";
socket.auth = { serverOffset: clientServerOffset || null };
socket.connect();
}
});
form.addEventListener("submit", (e) => {
e.preventDefault();
if (input.value) {
const clientOffset = `${socket.id}-${counter++}`;
socket.emit("chat message", input.value, clientOffset, () => {
console.log("Message delivered");
});
input.value = "";
}
});
socket.on("userList", (userList) => {
console.log(userList);
console.log(userList.length);
if (userList.length === 0) {
userListElement.textContent = "No users online";
} else if (userList.length === 1) {
userListElement.textContent = `${userList[0]} is online`;
} else {
userListElement.textContent = `${userList.join(",")} are online`;
}
});
socket.on("chat message", (msg, serverOffset) => {
const item = document.createElement("li");
item.textContent = msg;
messages.appendChild(item);
window.scrollTo(0, document.body.scrollHeight);
clientServerOffset = serverOffset;
console.log(clientServerOffset);
});
socket.on("system message", (msg) => {
const item = document.createElement("li");
item.textContent = msg;
item.style.fontStyle = "italic";
item.style.color = "grey";
item.style.textAlign = "center";
messages.append(item);
});
</script>
</body>
</html>
INDEX.JS
const express = require("express");
const { createServer } = require("node:http");
const { join } = require("node:path");
const { Server } = require("socket.io");
const mongoose = require("mongoose");
const { type } = require("node:os");
const { availableParallelism } = require("node:os");
const cluster = require("node:cluster");
const { createAdapter, setupPrimary } = require("@socket.io/cluster-adapter");
const { on } = require("node:events");
if (cluster.isPrimary) {
const numCPUs = availableParallelism();
for (let i = 0; i < numCPUs; i++) {
cluster.fork({
PORT: 3000 + i,
});
}
return setupPrimary();
}
async function main() {
mongoose
.connect(
"mongodb+srv://marsakatlaabhishek7168:abhi7168@cluster0.sixqbxk.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0"
)
.then(() => {
console.log("DB connected");
});
const MessageSchema = mongoose.Schema({
client_id: String,
clientOffset: {
type: String,
unique: true,
},
content: String,
});
const Message = new mongoose.model("Message", MessageSchema);
const app = express();
const server = createServer(app);
const io = new Server(server, {
connectionStateRecovery: {},
adapter: createAdapter(),
});
app.get("/", (req, res) => {
res.sendFile(join(__dirname, "index.html"));
});
let userList = [];
io.on("connection", async (socket) => {
console.log(socket.handshake.auth.serverOffset);
console.log(`user connected: ${socket.id}`);
userList.push(socket.id);
io.emit("userList", userList);
socket.broadcast.emit("system message", `${socket.id} has joined the chat`);
socket.on("disconnect", () => {
console.log(`user disconnected:${socket.id}`);
userList = userList.filter((id) => id !== socket.id);
socket.broadcast.emit("system message", `${socket.id} has left the chat`);
io.emit("userList", userList);
});
socket.on("chat message", async (msg, clientOffset, callback) => {
let newMessage;
try {
newMessage = await Message.create({
client_id: socket.id,
clientOffset: clientOffset,
content: msg,
});
console.log(newMessage);
} catch (e) {
if (e.code === 11000) {
callback();
}
return;
}
io.emit("chat message", msg, newMessage._id.toString());
callback();
});
if (!socket.recovered) {
try {
let serverOffset = socket.handshake.auth.serverOffset;
console.log(serverOffset);
let query = {};
if(serverOffset && mongoose.isValidObjectId(serverOffset)){
query._id = { $gt:new mongoose.Types.ObjectId(serverOffset) };
}
const messages = await Message.find(query).sort({ clientOffset: 1 });
messages.forEach((message) => {
socket.emit("chat message", message.content, message._id.toString());
});
} catch (e) {
console.log(e);
console.log("something went wrong!");
}
}
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`app listening to ${PORT}`);
});
}
main();







Comments
Post a Comment