Skip to main content

Echo WebSocket Client

Introduction

WebSockets provide a powerful way to create real-time, bidirectional communication channels between clients and servers. An Echo WebSocket Client is a program that connects to a WebSocket server, sends messages, and receives the same messages back. This echo functionality serves as an excellent starting point for learning about WebSockets.

In this tutorial, we'll explore how to build WebSocket clients that connect to echo servers. You'll learn the fundamentals of establishing connections, sending messages, handling responses, and managing the WebSocket lifecycle.

Understanding WebSocket Clients

A WebSocket client establishes a persistent connection with a server, allowing for continuous data exchange without the overhead of repeatedly opening new connections (unlike traditional HTTP requests).

Key Characteristics of WebSocket Clients

  • Persistent Connection: Maintains a single TCP connection for ongoing communication
  • Full-Duplex Communication: Allows simultaneous sending and receiving of data
  • Low Latency: Reduces overhead compared to HTTP polling techniques
  • Event-Driven: Uses events to handle different aspects of the connection lifecycle

Building a Simple Echo WebSocket Client

Let's create a basic WebSocket client in JavaScript that connects to an echo server. We'll use the browser's native WebSocket API.

Basic Implementation

javascript
// Create a new WebSocket connection to an echo server
const socket = new WebSocket('wss://echo.websocket.org');

// Connection opened
socket.addEventListener('open', function (event) {
console.log('Connection established!');

// Send a message to the server
socket.send('Hello, Echo Server!');
});

// Listen for messages from the server
socket.addEventListener('message', function (event) {
console.log('Message from server:', event.data);
});

// Listen for any errors
socket.addEventListener('error', function (event) {
console.error('WebSocket error:', event);
});

// When connection closes
socket.addEventListener('close', function (event) {
console.log('Connection closed. Code:', event.code, 'Reason:', event.reason);
});

Output

When you run this code in a browser console, you'll see:

Connection established!
Message from server: Hello, Echo Server!

WebSocket Client Event Handlers

WebSocket clients use several event handlers to manage the connection lifecycle:

1. Open Event

The open event fires when the connection is successfully established:

javascript
socket.addEventListener('open', function (event) {
console.log('Connected to the echo server!');
});

2. Message Event

The message event fires when the client receives data from the server:

javascript
socket.addEventListener('message', function (event) {
console.log('Received:', event.data);
});

3. Error Event

The error event fires when something goes wrong with the connection:

javascript
socket.addEventListener('error', function (event) {
console.error('Error occurred:', event);
});

4. Close Event

The close event fires when the connection closes:

javascript
socket.addEventListener('close', function (event) {
// event.code contains the close code
// event.reason contains a description
// event.wasClean indicates if the closure was clean
console.log(
`Connection closed. Code: ${event.code}, Reason: ${event.reason}, Clean: ${event.wasClean}`
);
});

Sending Different Data Types

WebSocket allows sending different types of data. Let's explore how to handle various data formats:

Text Data

Text is the most common format:

javascript
// Sending text
socket.send('Hello, this is a text message');

JSON Data

For structured data, we often send JSON:

javascript
// Sending JSON data
const data = {
user: 'Alice',
message: 'Hello everyone!',
timestamp: new Date().toISOString()
};

socket.send(JSON.stringify(data));

// On receiving, you'll need to parse it
socket.addEventListener('message', function (event) {
try {
const jsonData = JSON.parse(event.data);
console.log('Received:', jsonData.user, jsonData.message);
} catch (e) {
console.log('Received non-JSON message:', event.data);
}
});

Binary Data

WebSockets also support binary data like ArrayBuffer or Blob:

javascript
// Creating a binary message (example: a simple array of bytes)
const binaryData = new Uint8Array([1, 2, 3, 4, 5]);
socket.send(binaryData.buffer);

// To handle binary messages
socket.binaryType = 'arraybuffer'; // or 'blob'

socket.addEventListener('message', function (event) {
if (event.data instanceof ArrayBuffer) {
const view = new Uint8Array(event.data);
console.log('Received binary data:', view);
} else {
console.log('Received text data:', event.data);
}
});

Building an Interactive Echo Client

Let's create a more practical example—an interactive chat-like interface that sends messages to an echo server:

html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Echo WebSocket Client</title>
<style>
#chat-container {
width: 400px;
margin: 0 auto;
}
#messages {
height: 300px;
border: 1px solid #ccc;
margin-bottom: 10px;
padding: 10px;
overflow-y: auto;
}
#message-form {
display: flex;
}
#message-input {
flex-grow: 1;
margin-right: 10px;
}
.sent {
color: blue;
text-align: right;
}
.received {
color: green;
}
.system {
color: gray;
font-style: italic;
}
</style>
</head>
<body>
<div id="chat-container">
<h2>Echo WebSocket Chat</h2>
<div id="connection-status"></div>
<div id="messages"></div>
<form id="message-form">
<input type="text" id="message-input" placeholder="Type a message..." autocomplete="off" disabled>
<button type="submit" id="send-button" disabled>Send</button>
</form>
</div>

<script>
document.addEventListener('DOMContentLoaded', () => {
const messagesDiv = document.getElementById('messages');
const messageForm = document.getElementById('message-form');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
const connectionStatus = document.getElementById('connection-status');
let socket;

// Function to add a message to the chat
function addMessage(text, type) {
const messageElement = document.createElement('div');
messageElement.textContent = text;
messageElement.className = type;
messagesDiv.appendChild(messageElement);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}

function connect() {
// Create a new WebSocket connection to an echo server
socket = new WebSocket('wss://echo.websocket.org');

connectionStatus.textContent = 'Connecting...';
connectionStatus.style.color = 'orange';

// Connection opened
socket.addEventListener('open', (event) => {
connectionStatus.textContent = 'Connected';
connectionStatus.style.color = 'green';

// Enable the form
messageInput.disabled = false;
sendButton.disabled = false;
messageInput.focus();

addMessage('Connected to Echo Server', 'system');
});

// Listen for messages
socket.addEventListener('message', (event) => {
addMessage(`Echo: ${event.data}`, 'received');
});

// Handle errors
socket.addEventListener('error', (event) => {
addMessage('Connection error', 'system');
console.error('WebSocket error:', event);
});

// When connection closes
socket.addEventListener('close', (event) => {
messageInput.disabled = true;
sendButton.disabled = true;

connectionStatus.textContent = 'Disconnected';
connectionStatus.style.color = 'red';

const reason = event.reason ? `: ${event.reason}` : '';
addMessage(`Connection closed (code: ${event.code}${reason})`, 'system');

// Try to reconnect after a short delay
setTimeout(connect, 5000);
});
}

// Handle message submission
messageForm.addEventListener('submit', (event) => {
event.preventDefault();

const message = messageInput.value.trim();
if (message && socket && socket.readyState === WebSocket.OPEN) {
socket.send(message);
addMessage(`You: ${message}`, 'sent');
messageInput.value = '';
}
});

// Start the connection
connect();
});
</script>
</body>
</html>

This example creates an interactive chat interface that:

  • Connects to an echo server
  • Displays connection status
  • Lets users send messages and see the echoed responses
  • Automatically tries to reconnect if the connection is lost

Connection States

Understanding WebSocket connection states is crucial for managing the client lifecycle:

javascript
const socket = new WebSocket('wss://echo.websocket.org');

// Check the current state of the connection
function checkState() {
switch(socket.readyState) {
case WebSocket.CONNECTING:
console.log('Connecting...');
break;
case WebSocket.OPEN:
console.log('Connection established');
break;
case WebSocket.CLOSING:
console.log('Closing connection...');
break;
case WebSocket.CLOSED:
console.log('Connection closed');
break;
default:
console.log('Unknown state');
}
}

// Check initial state
checkState();

// Check state after connection is established
socket.addEventListener('open', checkState);

// Check state when connection is closing
socket.addEventListener('close', checkState);

Handling Connection Timeouts and Reconnection

In real-world applications, connections may fail or drop. Here's how to implement a reconnection strategy:

javascript
class ReconnectingWebSocket {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.socket = null;
this.isConnected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
this.reconnectInterval = options.reconnectInterval || 3000;
this.listeners = {
message: [],
open: [],
close: [],
error: []
};

this.connect();
}

connect() {
this.socket = new WebSocket(this.url);

// Set up timeout for connection
const connectionTimeout = setTimeout(() => {
if (this.socket.readyState !== WebSocket.OPEN) {
console.log('Connection timeout, closing socket...');
this.socket.close();
}
}, this.options.timeout || 10000);

this.socket.addEventListener('open', (event) => {
clearTimeout(connectionTimeout);
this.isConnected = true;
this.reconnectAttempts = 0;
console.log('Connection established');
this.listeners.open.forEach(listener => listener(event));
});

this.socket.addEventListener('message', (event) => {
this.listeners.message.forEach(listener => listener(event));
});

this.socket.addEventListener('error', (event) => {
console.error('WebSocket error:', event);
this.listeners.error.forEach(listener => listener(event));
});

this.socket.addEventListener('close', (event) => {
clearTimeout(connectionTimeout);
this.isConnected = false;
this.listeners.close.forEach(listener => listener(event));

if (this.reconnectAttempts < this.maxReconnectAttempts) {
const delay = this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts);
console.log(`Reconnecting in ${delay}ms... (Attempt ${this.reconnectAttempts + 1})`);

setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
} else {
console.log('Max reconnect attempts reached');
}
});
}

send(data) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(data);
return true;
}
return false;
}

addEventListener(type, callback) {
if (this.listeners[type]) {
this.listeners[type].push(callback);
return true;
}
return false;
}

removeEventListener(type, callback) {
if (this.listeners[type]) {
this.listeners[type] = this.listeners[type].filter(
listener => listener !== callback
);
return true;
}
return false;
}

close(code, reason) {
if (this.socket) {
// Prevent reconnection attempts
this.reconnectAttempts = this.maxReconnectAttempts;
this.socket.close(code, reason);
}
}
}

// Usage:
const socket = new ReconnectingWebSocket('wss://echo.websocket.org', {
maxReconnectAttempts: 5,
reconnectInterval: 2000,
timeout: 5000
});

socket.addEventListener('open', () => {
socket.send('Hello after connection!');
});

socket.addEventListener('message', (event) => {
console.log('Message received:', event.data);
});

This implementation:

  • Handles connection timeouts
  • Implements exponential backoff for reconnection attempts
  • Provides a clean API similar to the native WebSocket

Security Considerations

When implementing WebSocket clients, keep these security considerations in mind:

  1. Use WSS (WebSocket Secure): Always prefer wss:// over ws:// to encrypt data transmission
  2. Validate Server Responses: Never trust data received from the server without validation
  3. Implement Authentication: Use tokens or cookies to authenticate connections
  4. Protect Against Cross-Site WebSocket Hijacking: Implement origin checking on your server
  5. Rate Limit Messages: Prevent flooding the server with too many messages
javascript
// Example of adding authentication token to a WebSocket connection
const token = "your-auth-token";
const socket = new WebSocket(`wss://echo.websocket.org?token=${token}`);

// Alternative: Send authentication message after connection
socket.addEventListener('open', function() {
socket.send(JSON.stringify({
type: "auth",
token: "your-auth-token"
}));
});

Real-World Use Cases

WebSocket echo clients can be used for:

  1. Testing WebSocket Connections: Verify that your server's WebSocket implementation works correctly
  2. Latency Measurement: Measure round-trip time for messages
  3. Heartbeat Systems: Build keepalive mechanisms to ensure connections remain open
  4. Debugging: Troubleshoot connection issues with echo responses
  5. Learning: Understand WebSocket communication principles

Here's a simple latency-measuring example:

javascript
function measureLatency(socket, rounds = 5) {
let currentRound = 0;
const results = [];
let startTime;

function sendPing() {
startTime = performance.now();
socket.send(`ping-${currentRound}`);
}

const originalMessageHandler = socket.onmessage;
socket.onmessage = function(event) {
if (event.data.startsWith('ping-')) {
const latency = performance.now() - startTime;
results.push(latency);
console.log(`Round ${currentRound + 1} latency: ${latency.toFixed(2)}ms`);

currentRound++;
if (currentRound < rounds) {
setTimeout(sendPing, 1000);
} else {
const avgLatency = results.reduce((sum, val) => sum + val, 0) / results.length;
console.log(`Average latency: ${avgLatency.toFixed(2)}ms`);

// Restore original handler
socket.onmessage = originalMessageHandler;
}
} else if (originalMessageHandler) {
originalMessageHandler(event);
}
};

// Start the first ping
sendPing();
}

// Usage
const socket = new WebSocket('wss://echo.websocket.org');
socket.addEventListener('open', () => {
console.log('Starting latency test...');
measureLatency(socket, 10);
});

Summary

In this tutorial, you've learned how to build and use Echo WebSocket clients. We've covered:

  • The basics of creating WebSocket connections
  • Handling WebSocket lifecycle events
  • Sending and receiving different data types
  • Building an interactive WebSocket echo client
  • Managing connection states and implementing reconnection strategies
  • Security considerations for WebSocket clients
  • Real-world use cases and examples

WebSockets enable real-time bidirectional communication, making them ideal for applications like chat systems, live dashboards, multiplayer games, and collaborative tools. Starting with echo clients provides a solid foundation for understanding WebSocket principles before moving on to more complex implementations.

Additional Resources

For further learning about WebSocket clients:

  1. MDN WebSocket API Documentation
  2. WebSocket.org Echo Test
  3. RFC 6455: The WebSocket Protocol

Exercises

  1. Basic Echo Client: Modify the simple echo client to display the round-trip time for each message.
  2. Chat Room: Extend the interactive example to support multiple channels.
  3. Data Visualization: Create a client that sends numeric data to an echo server and visualizes the echoed values on a chart.
  4. Reconnection Logic: Implement your own reconnection logic with exponential backoff.
  5. Binary Data: Build an echo client that sends and receives binary data, such as images or files.

By mastering Echo WebSocket Clients, you've taken the first step toward building powerful real-time applications with WebSocket technology!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)