45. WebSocket: Real-Time Communication in JavaScript
Introduction
WebSocket is a communication protocol that enables real-time data exchange between client and server through a persistent connection. Unlike HTTP, which is request-response based, WebSocket keeps the connection open so the server can send data to the client at any time without being asked.
For IT students, WebSocket is highly relevant to building applications such as online class chat rooms, live notifications when the course schedule changes, and collaborative editing of group assignment documents.
Why Is WebSocket Important?
In a digital campus environment, many scenarios require real-time communication:
- Chat rooms for online class discussions
- Live notifications when a lecturer posts grades
- Live updates of course registration status
- Collaborative whiteboards for presentations
- Live quizzes synchronized for all participants
Without WebSocket, we would have to poll the server repeatedly, which wastes bandwidth and resources.
How WebSocket Works
WebSocket works by upgrading an HTTP connection to a WebSocket connection through a handshake mechanism. Once the connection is open, both the client and the server can send data at any time in full-duplex mode.
Creating a WebSocket Connection
// Create a WebSocket connection to the server
const socket = new WebSocket('ws://localhost:3000/chat');
// The connection can also use a wss:// URL for encryption (HTTPS)
const secureSocket = new WebSocket('wss://chat.university.edu/room');
WebSocket Events
WebSocket has four main events that must be handled:
const socket = new WebSocket('ws://localhost:3000/chat');
// Event: connection opened successfully
socket.addEventListener('open', (event) => {
console.log('WebSocket connection successfully opened');
console.log('Protocol in use:', socket.protocol);
// Send a welcome message to the server
socket.send(JSON.stringify({
type: 'join',
username: 'Student',
room: 'TIF-401'
}));
});
// Event: receiving a message from the server
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'chat':
displayMessage(data.username, data.message);
break;
case 'notification':
showNotification(data.title, data.body);
break;
case 'user_list':
updateOnlineUsers(data.users);
break;
}
});
// Event: connection closed
socket.addEventListener('close', (event) => {
if (event.wasClean) {
console.log(`Connection closed cleanly. Code: ${event.code}`);
} else {
console.log('Connection lost unexpectedly');
}
});
// Event: an error occurred
socket.addEventListener('error', (error) => {
console.error('WebSocket error:', error.message);
});
Sending Data
// Send a text message
socket.send('Hello, this is a message from the client');
// Send JSON data
socket.send(JSON.stringify({
type: 'message',
content: 'Question about midterm material',
sender: 'Mike',
timestamp: Date.now()
}));
// Send binary data (file, image)
const buffer = new ArrayBuffer(1024);
const view = new Uint8Array(buffer);
// ... fill in the data ...
socket.send(buffer);
// Check the status before sending
function safeSend(data) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(data));
} else if (socket.readyState === WebSocket.CONNECTING) {
// Wait for the connection to open
socket.addEventListener('open', () => {
socket.send(JSON.stringify(data));
}, { once: true });
} else {
console.error('Connection unavailable');
reconnect();
}
}
Managing Connection State
class ChatSocket {
constructor(url) {
this.url = url;
this.socket = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
}
connect() {
this.socket = new WebSocket(this.url);
this.socket.addEventListener('open', () => {
console.log('Connected to chat server');
this.reconnectAttempts = 0;
});
this.socket.addEventListener('close', (event) => {
console.log(`Connection closed: ${event.code}`);
this.attemptReconnect();
});
this.socket.addEventListener('error', (error) => {
console.error('Error:', error.message);
});
}
attemptReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
setTimeout(() => {
this.connect();
}, delay);
} else {
console.error('Failed to reconnect after several attempts');
}
}
send(data) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
}
}
disconnect() {
if (this.socket) {
this.socket.close(1000, 'User disconnect');
}
}
}
// Usage
const chat = new ChatSocket('ws://localhost:3000/chat');
chat.connect();
Real-World Example: Real-Time Chat for an Online Class
class ClassChat {
constructor(roomId, username) {
this.roomId = roomId;
this.username = username;
this.socket = null;
this.messageHandlers = [];
}
connect() {
this.socket = new WebSocket(`wss://chat.university.edu/room/${this.roomId}`);
this.socket.addEventListener('open', () => {
console.log(`Joined chat room: ${this.roomId}`);
this.sendSystemMessage('Joined class room');
this.send({
type: 'join',
username: this.username,
roomId: this.roomId
});
});
this.socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'message':
this.displayMessage(data);
break;
case 'typing':
this.showTypingIndicator(data.username);
break;
case 'user_joined':
this.showSystemMessage(`${data.username} bergabung`);
break;
case 'user_left':
this.showSystemMessage(`${data.username} left`);
break;
}
});
this.socket.addEventListener('close', () => {
this.showSystemMessage('Connection lost');
setTimeout(() => this.connect(), 3000);
});
}
sendMessage(message) {
this.send({
type: 'message',
username: this.username,
content: message,
roomId: this.roomId,
timestamp: new Date().toISOString()
});
}
sendTyping() {
this.send({
type: 'typing',
username: this.username,
roomId: this.roomId
});
}
send(data) {
if (this.socket?.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
}
}
displayMessage(data) {
const chatArea = document.getElementById('chat-messages');
const msgEl = document.createElement('div');
msgEl.className = 'chat-message';
msgEl.innerHTML = `
<span class="username">${data.username}</span>
<span class="time">${new Date(data.timestamp).toLocaleTimeString('id-ID')}</span>
<p>${this.escapeHtml(data.content)}</p>
`;
chatArea.appendChild(msgEl);
chatArea.scrollTop = chatArea.scrollHeight;
}
showSystemMessage(text) {
const chatArea = document.getElementById('chat-messages');
const msgEl = document.createElement('div');
msgEl.className = 'system-message';
msgEl.textContent = text;
chatArea.appendChild(msgEl);
}
showTypingIndicator(username) {
const indicator = document.getElementById('typing-indicator');
indicator.textContent = `${username} sedang mengetik...`;
setTimeout(() => {
indicator.textContent = '';
}, 3000);
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Usage
const classChat = new ClassChat('TIF-401', 'Mike Student');
classChat.connect();
Example: Live Notifications for Schedule Changes
class ScheduleNotifier {
constructor() {
this.socket = new WebSocket('wss://notifikasi.university.edu');
this.notifications = [];
}
init() {
this.socket.addEventListener('open', () => {
this.authenticate();
this.subscribe('schedule_changes');
this.subscribe('exam_notifications');
this.subscribe('grade_updates');
});
this.socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
this.handleNotification(data);
});
}
authenticate() {
const token = localStorage.getItem('auth_token');
this.socket.send(JSON.stringify({
type: 'auth',
token: token
}));
}
subscribe(channel) {
this.socket.send(JSON.stringify({
type: 'subscribe',
channel: channel
}));
}
handleNotification(data) {
const notification = {
id: Date.now(),
type: data.type,
title: data.title,
body: data.body,
timestamp: new Date(data.timestamp),
read: false
};
this.notifications.unshift(notification);
this.showToast(notification);
this.updateBadge();
}
showToast(notification) {
const toast = document.createElement('div');
toast.className = `toast toast-${notification.type}`;
toast.innerHTML = `
<strong>${notification.title}</strong>
<p>${notification.body}</p>
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('show');
}, 100);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 5000);
}
updateBadge() {
const unread = this.notifications.filter(n => !n.read).length;
const badge = document.getElementById('notification-badge');
badge.textContent = unread;
badge.style.display = unread > 0 ? 'block' : 'none';
}
}
Example: Live Quiz for a Lecture
class LiveQuiz {
constructor(quizId, studentId) {
this.quizId = quizId;
this.studentId = studentId;
this.socket = new WebSocket(`wss://quiz.university.edu/${quizId}`);
this.currentQuestion = null;
this.answers = {};
}
connect() {
this.socket.addEventListener('open', () => {
this.socket.send(JSON.stringify({
type: 'student_join',
quizId: this.quizId,
studentId: this.studentId
}));
});
this.socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'question':
this.displayQuestion(data.question);
break;
case 'timer':
this.updateTimer(data.remaining);
break;
case 'results':
this.showResults(data);
break;
case 'leaderboard':
this.updateLeaderboard(data.rankings);
break;
}
});
}
displayQuestion(question) {
this.currentQuestion = question;
const container = document.getElementById('quiz-container');
container.innerHTML = `
<div class="question-card">
<h3>Question ${question.number} of ${question.total}</h3>
<p class="question-text">${question.text}</p>
<div class="options">
${question.options.map((opt, i) => `
<button class="option-btn" data-index="${i}">
${opt}
</button>
`).join('')}
</div>
</div>
`;
container.querySelectorAll('.option-btn').forEach(btn => {
btn.addEventListener('click', () => {
this.submitAnswer(parseInt(btn.dataset.index));
});
});
}
submitAnswer(answerIndex) {
const timeTaken = Date.now() - this.currentQuestion.startTime;
this.socket.send(JSON.stringify({
type: 'answer',
quizId: this.quizId,
studentId: this.studentId,
questionId: this.currentQuestion.id,
answer: answerIndex,
timeTaken: timeTaken
}));
// Disable all buttons after answering
document.querySelectorAll('.option-btn').forEach(btn => {
btn.disabled = true;
btn.classList.remove('selected');
});
document.querySelectorAll('.option-btn')[answerIndex].classList.add('selected');
}
updateTimer(seconds) {
const timer = document.getElementById('quiz-timer');
timer.textContent = `${seconds}s`;
if (seconds <= 10) {
timer.classList.add('warning');
}
}
}
Example: Collaborative Document Editing
class CollaborativeDoc {
constructor(docId, userId) {
this.docId = docId;
this.userId = userId;
this.socket = new WebSocket(`wss://docs.university.edu/collab/${docId}`);
this.operations = [];
this.document = '';
}
connect() {
this.socket.addEventListener('open', () => {
this.socket.send(JSON.stringify({
type: 'join_doc',
docId: this.docId,
userId: this.userId
}));
});
this.socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'doc_content':
this.document = data.content;
this.syncEditor();
break;
case 'remote_operation':
this.applyRemoteOperation(data.operation);
break;
case 'cursor_move':
this.showRemoteCursor(data.userId, data.position, data.color);
break;
case 'user_joined':
this.addCollaborator(data.user);
break;
case 'user_left':
this.removeCollaborator(data.userId);
break;
}
});
}
sendOperation(operation) {
this.operations.push(operation);
this.socket.send(JSON.stringify({
type: 'operation',
docId: this.docId,
userId: this.userId,
operation: operation,
version: this.operations.length
}));
}
applyRemoteOperation(operation) {
switch (operation.type) {
case 'insert':
this.document =
this.document.slice(0, operation.position) +
operation.text +
this.document.slice(operation.position);
break;
case 'delete':
this.document =
this.document.slice(0, operation.position) +
this.document.slice(operation.position + operation.count);
break;
}
this.syncEditor();
}
syncEditor() {
const editor = document.getElementById('editor');
const cursorPos = editor.selectionStart;
editor.value = this.document;
editor.setSelectionRange(cursorPos, cursorPos);
}
sendCursorPosition(position) {
this.socket.send(JSON.stringify({
type: 'cursor_position',
docId: this.docId,
userId: this.userId,
position: position
}));
}
}
Real-World Example: Online Queue System for a Campus Canteen
class CanteenQueue {
constructor() {
this.socket = new WebSocket('wss://canteen.university.edu/queue');
this.queuePosition = -1;
this.estimatedWait = 0;
}
connect() {
this.socket.addEventListener('open', () => {
this.requestQueueStatus();
});
this.socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'queue_update':
this.updateQueueUI(data);
break;
case 'order_ready':
this.notifyOrderReady(data.orderId);
break;
case 'queue_position':
this.queuePosition = data.position;
this.estimatedWait = data.estimatedMinutes;
this.displayQueueInfo();
break;
}
});
}
joinQueue(studentId, orderItems) {
this.socket.send(JSON.stringify({
type: 'join_queue',
studentId: studentId,
items: orderItems,
timestamp: new Date().toISOString()
}));
}
updateQueueUI(data) {
document.getElementById('queue-count').textContent = data.totalInQueue;
document.getElementById('your-position').textContent = this.queuePosition;
document.getElementById('estimated-wait').textContent = `${this.estimatedWait} minutes`;
}
notifyOrderReady(orderId) {
if ('Notification' in window && Notification.permission === 'granted') {
new Notification('Order Ready!', {
body: `Order ${orderId} is ready for pickup`,
icon: '/assets/canteen-icon.png'
});
}
const audio = new Audio('/assets/notification.mp3');
audio.play();
}
}
WebSocket Protocol
// WebSocket uses the URI scheme:
// ws:// - for unencrypted connections (default port 80)
// wss:// - for encrypted/TLS connections (default port 443)
// Commonly used status codes
const CLOSE_CODES = {
NORMAL_CLOSURE: 1000, // Normal closure
GOING_AWAY: 1001, // Client navigates away
PROTOCOL_ERROR: 1002, // Protocol error
UNSUPPORTED_DATA: 1003, // Data type not supported
NO_STATUS: 1005, // No status code
ABNORMAL_CLOSURE: 1006, // Abnormal connection drop
INVALID_FRAME: 1007, // Invalid frame
POLICY_VIOLATION: 1008, // Policy violation
MESSAGE_TOO_BIG: 1009, // Message too large
MANDATORY_EXTENSION: 1010, // Required extension missing
INTERNAL_ERROR: 1011 // Internal server error
};
// Handle connection closure properly
socket.addEventListener('close', (event) => {
console.log(`Code: ${event.code}, Reason: ${event.reason}`);
switch (event.code) {
case CLOSE_CODES.NORMAL_CLOSURE:
console.log('Penutupan normal');
break;
case CLOSE_CODES.ABNORMAL_CLOSURE:
console.log('Connection lost, attempting to reconnect...');
break;
case CLOSE_CODES.MESSAGE_TOO_BIG:
console.log('Message too large, reduce its size');
break;
}
});
WebSocket Security
// 1. Always use wss:// (WebSocket Secure)
const socket = new WebSocket('wss://chat.university.edu');
// 2. Validate the origin
// The server must check the Origin header
// 3. Authenticate with a token
socket.addEventListener('open', () => {
socket.send(JSON.stringify({
type: 'auth',
token: getAuthToken()
}));
});
// 4. Rate limiting on the client
class RateLimitedSocket {
constructor(url) {
this.socket = new WebSocket(url);
this.messageQueue = [];
this.messagesPerSecond = 10;
this.sentCount = 0;
this.resetInterval = setInterval(() => {
this.sentCount = 0;
this.processQueue();
}, 1000);
}
send(data) {
if (this.sentCount < this.messagesPerSecond) {
this.socket.send(JSON.stringify(data));
this.sentCount++;
} else {
this.messageQueue.push(data);
}
}
processQueue() {
while (this.messageQueue.length > 0 && this.sentCount < this.messagesPerSecond) {
const data = this.messageQueue.shift();
this.socket.send(JSON.stringify(data));
this.sentCount++;
}
}
destroy() {
clearInterval(this.resetInterval);
this.socket.close();
}
}
Code Explanation
Main WebSocket Functions
new WebSocket(url)- Creates a new connection to a WebSocket serversocket.send(data)- Sends data to the serversocket.close(code, reason)- Closes the connection with a code and reasonsocket.readyState- Current connection status (CONNECTING, OPEN, CLOSING, CLOSED)
Important Events
open- Connection opened successfullymessage- Receiving data from the serverclose- Connection closederror- An error occurred on the connection
Important Properties
socket.readyState- Connection status (0: CONNECTING, 1: OPEN, 2: CLOSING, 3: CLOSED)socket.bufferedAmount- Amount of data not yet sentsocket.protocol- Subprotocol selected by the serverevent.code- Connection close status code
Conclusion
WebSocket is a powerful real-time communication solution for modern campus applications. With a persistent, full-duplex connection, WebSocket enables features such as online class chat, live notifications, live quizzes, and collaborative editing. It is important to always handle reconnection, validate data, and use encryption (wss://) for security. With a solid understanding of event-driven programming and error handling, WebSocket can be relied on for real-time communication in a digital campus environment.