Echo WebSocket Groups
Introduction
WebSocket groups are a powerful feature of the Echo framework that allow you to organize connected clients into logical collections. This grouping mechanism enables efficient broadcasting of messages to specific subsets of clients rather than sending individual messages to each client or broadcasting to all connected clients.
Think of WebSocket groups like chat rooms or channels - they let you segment your users based on interests, permissions, or any other criteria that makes sense for your application. This creates more organized, efficient, and scalable real-time applications.
In this tutorial, we'll explore how to work with Echo WebSocket groups, including:
- Creating and managing groups
- Adding and removing clients from groups
- Broadcasting messages to specific groups
- Implementing practical use cases
Understanding WebSocket Groups
Before diving into code, let's understand the core concept of WebSocket groups:
- Group: A named collection of WebSocket connections
- Client: An individual WebSocket connection (typically a user)
- Broadcasting: Sending a message to all clients in a specific group
Groups are particularly useful when different users need different information. For example, in a chat application, users in the "sports" channel shouldn't receive messages meant for the "technology" channel.
Setting Up WebSocket Groups in Echo
Let's start by setting up a basic Echo server with WebSocket support and group functionality:
package main
import (
"log"
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"golang.org/x/net/websocket"
)
type WebSocketManager struct {
// Maps group names to a set of client connections
groups map[string]map[*websocket.Conn]bool
}
func NewWebSocketManager() *WebSocketManager {
return &WebSocketManager{
groups: make(map[string]map[*websocket.Conn]bool),
}
}
// AddToGroup adds a client to a specific group
func (m *WebSocketManager) AddToGroup(groupName string, client *websocket.Conn) {
if _, exists := m.groups[groupName]; !exists {
m.groups[groupName] = make(map[*websocket.Conn]bool)
}
m.groups[groupName][client] = true
log.Printf("Client added to group %s", groupName)
}
// RemoveFromGroup removes a client from a specific group
func (m *WebSocketManager) RemoveFromGroup(groupName string, client *websocket.Conn) {
if clients, exists := m.groups[groupName]; exists {
delete(clients, client)
log.Printf("Client removed from group %s", groupName)
}
}
// BroadcastToGroup sends a message to all clients in a specific group
func (m *WebSocketManager) BroadcastToGroup(groupName string, message string) {
if clients, exists := m.groups[groupName]; exists {
for client := range clients {
websocket.Message.Send(client, message)
}
log.Printf("Message broadcasted to group %s: %s", groupName, message)
}
}
func main() {
e := echo.New()
e.Use(middleware.Logger())
// Create our WebSocket manager
wsManager := NewWebSocketManager()
// WebSocket handler
e.GET("/ws", func(c echo.Context) error {
websocket.Handler(func(ws *websocket.Conn) {
defer ws.Close()
// Handle the WebSocket connection
for {
// Implement connection handling here
}
}).ServeHTTP(c.Response(), c.Request())
return nil
})
e.Logger.Fatal(e.Start(":8080"))
}
Managing Client Group Membership
Now let's implement handlers for clients to join and leave groups based on messages they send:
// Inside the WebSocket handler:
e.GET("/ws", func(c echo.Context) error {
websocket.Handler(func(ws *websocket.Conn) {
defer ws.Close()
// Process incoming messages
for {
var msg string
err := websocket.Message.Receive(ws, &msg)
if err != nil {
log.Println("Error receiving message:", err)
// Remove client from all groups on disconnect
for groupName, clients := range wsManager.groups {
if _, exists := clients[ws]; exists {
wsManager.RemoveFromGroup(groupName, ws)
}
}
break
}
// Parse the message for commands
if len(msg) > 7 && msg[:5] == "JOIN:" {
groupName := msg[5:]
wsManager.AddToGroup(groupName, ws)
websocket.Message.Send(ws, "Joined group: "+groupName)
} else if len(msg) > 7 && msg[:6] == "LEAVE:" {
groupName := msg[6:]
wsManager.RemoveFromGroup(groupName, ws)
websocket.Message.Send(ws, "Left group: "+groupName)
} else if len(msg) > 7 && msg[:5] == "SEND:" {
// Format: SEND:groupName:message
parts := strings.SplitN(msg[5:], ":", 2)
if len(parts) == 2 {
groupName := parts[0]
message := parts[1]
wsManager.BroadcastToGroup(groupName, message)
}
}
}
}).ServeHTTP(c.Response(), c.Request())
return nil
})
Client-Side Implementation
To complete our understanding, here's a simple JavaScript client implementation that can interact with our group-enabled WebSocket server:
// Establish WebSocket connection
const socket = new WebSocket('ws://localhost:8080/ws');
// Connection opened
socket.addEventListener('open', (event) => {
console.log('Connected to WebSocket server');
// Join a group when connected
socket.send('JOIN:sports');
});
// Listen for messages from the server
socket.addEventListener('message', (event) => {
console.log('Message from server:', event.data);
// Display message in UI
const messagesDiv = document.getElementById('messages');
const messageElement = document.createElement('div');
messageElement.textContent = event.data;
messagesDiv.appendChild(messageElement);
});
// Function to send message to a group
function sendToGroup() {
const group = document.getElementById('groupInput').value;
const message = document.getElementById('messageInput').value;
if (group && message) {
socket.send(`SEND:${group}:${message}`);
document.getElementById('messageInput').value = '';
}
}
// Join a new group
function joinGroup() {
const group = document.getElementById('joinGroupInput').value;
if (group) {
socket.send(`JOIN:${group}`);
document.getElementById('joinGroupInput').value = '';
}
}
// Leave a group
function leaveGroup() {
const group = document.getElementById('leaveGroupInput').value;
if (group) {
socket.send(`LEAVE:${group}`);
document.getElementById('leaveGroupInput').value = '';
}
}
Real-World Application: Multi-Room Chat
Let's implement a practical example - a multi-room chat application where users can join different topic rooms and only receive messages from rooms they've joined.
First, let's enhance our server code:
package main
import (
"encoding/json"
"log"
"net/http"
"strings"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"golang.org/x/net/websocket"
)
type WebSocketManager struct {
groups map[string]map[*websocket.Conn]bool
}
type Message struct {
Type string `json:"type"`
Room string `json:"room"`
Content string `json:"content"`
Sender string `json:"sender"`
}
func NewWebSocketManager() *WebSocketManager {
return &WebSocketManager{
groups: make(map[string]map[*websocket.Conn]bool),
}
}
// Group management methods as before...
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Static("/", "public")
wsManager := NewWebSocketManager()
e.GET("/ws", func(c echo.Context) error {
websocket.Handler(func(ws *websocket.Conn) {
defer ws.Close()
// Store username for this connection
var username string
for {
var msg string
err := websocket.Message.Receive(ws, &msg)
if err != nil {
log.Println("Client disconnected:", err)
// Remove from all rooms
for room := range wsManager.groups {
wsManager.RemoveFromGroup(room, ws)
}
// Notify others about departure
if username != "" {
for room := range wsManager.groups {
systemMsg := Message{
Type: "system",
Room: room,
Content: username + " has left the chat",
}
msgJson, _ := json.Marshal(systemMsg)
wsManager.BroadcastToGroup(room, string(msgJson))
}
}
break
}
// Parse the JSON message
var message Message
if err := json.Unmarshal([]byte(msg), &message); err != nil {
log.Println("Error parsing message:", err)
continue
}
switch message.Type {
case "join":
username = message.Sender
wsManager.AddToGroup(message.Room, ws)
// Notify room about new user
systemMsg := Message{
Type: "system",
Room: message.Room,
Content: username + " has joined the room",
}
msgJson, _ := json.Marshal(systemMsg)
wsManager.BroadcastToGroup(message.Room, string(msgJson))
case "leave":
wsManager.RemoveFromGroup(message.Room, ws)
// Notify room about user leaving
systemMsg := Message{
Type: "system",
Room: message.Room,
Content: username + " has left the room",
}
msgJson, _ := json.Marshal(systemMsg)
wsManager.BroadcastToGroup(message.Room, string(msgJson))
case "message":
// Broadcast the message to the room
msgJson, _ := json.Marshal(message)
wsManager.BroadcastToGroup(message.Room, string(msgJson))
}
}
}).ServeHTTP(c.Response(), c.Request())
return nil
})
e.Logger.Fatal(e.Start(":8080"))
}
Then for the client-side HTML/JS:
<!DOCTYPE html>
<html>
<head>
<title>Multi-Room Chat</title>
<style>
.chat-container {
display: flex;
height: 80vh;
}
.rooms {
width: 200px;
border-right: 1px solid #ccc;
padding: 10px;
}
.chat {
flex-grow: 1;
padding: 10px;
display: flex;
flex-direction: column;
}
.messages {
flex-grow: 1;
overflow-y: auto;
border: 1px solid #eee;
margin-bottom: 10px;
padding: 10px;
}
.system-message {
color: #888;
font-style: italic;
}
.user-message {
margin-bottom: 5px;
}
.sender {
font-weight: bold;
}
</style>
</head>
<body>
<h1>Multi-Room Chat</h1>
<div class="login" id="loginPanel">
<input type="text" id="username" placeholder="Your name">
<button onclick="login()">Enter Chat</button>
</div>
<div class="chat-container" id="chatPanel" style="display:none;">
<div class="rooms">
<h3>Chat Rooms</h3>
<ul id="roomList">
<li><a href="#" onclick="joinRoom('general')">General</a></li>
<li><a href="#" onclick="joinRoom('tech')">Technology</a></li>
<li><a href="#" onclick="joinRoom('sports')">Sports</a></li>
<li><a href="#" onclick="joinRoom('music')">Music</a></li>
</ul>
<div>
<h3>Your Rooms</h3>
<ul id="yourRooms"></ul>
</div>
</div>
<div class="chat">
<h3 id="currentRoom">Select a room to join</h3>
<div class="messages" id="messages"></div>
<div class="input-area">
<input type="text" id="messageInput" placeholder="Type your message...">
<button onclick="sendMessage()">Send</button>
</div>
</div>
</div>
<script>
let socket;
let username = '';
let currentRoom = '';
let joinedRooms = new Set();
function login() {
username = document.getElementById('username').value.trim();
if (!username) return;
// Connect to WebSocket server
socket = new WebSocket('ws://localhost:8080/ws');
socket.onopen = () => {
console.log('Connected to WebSocket server');
document.getElementById('loginPanel').style.display = 'none';
document.getElementById('chatPanel').style.display = 'flex';
};
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
displayMessage(message);
};
socket.onclose = () => {
alert('Disconnected from server');
};
}
function joinRoom(room) {
if (currentRoom === room) return;
currentRoom = room;
document.getElementById('currentRoom').textContent = 'Room: ' + room;
document.getElementById('messages').innerHTML = '';
// If we haven't joined this room before, join it now
if (!joinedRooms.has(room)) {
const message = {
type: 'join',
room: room,
sender: username
};
socket.send(JSON.stringify(message));
joinedRooms.add(room);
updateRoomList();
}
}
function leaveRoom(room) {
if (joinedRooms.has(room)) {
const message = {
type: 'leave',
room: room,
sender: username
};
socket.send(JSON.stringify(message));
joinedRooms.delete(room);
if (currentRoom === room) {
currentRoom = '';
document.getElementById('currentRoom').textContent = 'Select a room to join';
document.getElementById('messages').innerHTML = '';
}
updateRoomList();
}
}
function sendMessage() {
const content = document.getElementById('messageInput').value.trim();
if (!content || !currentRoom) return;
const message = {
type: 'message',
room: currentRoom,
content: content,
sender: username
};
socket.send(JSON.stringify(message));
document.getElementById('messageInput').value = '';
}
function displayMessage(message) {
// Only display messages for the current room
if (message.room !== currentRoom) return;
const messagesDiv = document.getElementById('messages');
const messageEl = document.createElement('div');
if (message.type === 'system') {
messageEl.className = 'system-message';
messageEl.textContent = message.content;
} else {
messageEl.className = 'user-message';
const senderSpan = document.createElement('span');
senderSpan.className = 'sender';
senderSpan.textContent = message.sender + ': ';
messageEl.appendChild(senderSpan);
messageEl.appendChild(document.createTextNode(message.content));
}
messagesDiv.appendChild(messageEl);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function updateRoomList() {
const list = document.getElementById('yourRooms');
list.innerHTML = '';
joinedRooms.forEach(room => {
const li = document.createElement('li');
const roomLink = document.createElement('a');
roomLink.href = '#';
roomLink.textContent = room;
roomLink.onclick = () => joinRoom(room);
const leaveBtn = document.createElement('button');
leaveBtn.textContent = 'Leave';
leaveBtn.style.marginLeft = '5px';
leaveBtn.onclick = (e) => {
e.preventDefault();
leaveRoom(room);
};
li.appendChild(roomLink);
li.appendChild(leaveBtn);
list.appendChild(li);
});
}
</script>
</body>
</html>
Benefits of WebSocket Groups
Using WebSocket groups provides several advantages:
- Efficiency: Messages are only sent to clients who need them, reducing bandwidth usage
- Organization: Clients can be organized by interest, role, or any other logical grouping
- Scalability: As your application grows, groups help manage communication patterns
- Security: Information is only shared with authorized clients in appropriate groups
- User Experience: Users only receive relevant information for their context
Best Practices for WebSocket Groups
When implementing WebSocket groups, keep these best practices in mind:
- Group Naming: Use a consistent naming convention for groups
- Group Cleanup: Remove empty groups when they're no longer needed
- Connection Management: Handle disconnects properly to clean up group memberships
- Error Handling: Implement robust error handling for message broadcasting
- Authentication: Verify that clients have permission to join specific groups
- Scaling: Consider how your group system will scale across multiple server instances
Summary
In this tutorial, we've explored the concept of WebSocket groups in Echo, a powerful feature that enables efficient message broadcasting to specific client subsets. We've learned how to:
- Create and manage WebSocket groups
- Add and remove clients from these groups
- Broadcast messages to specific groups
- Implement a multi-room chat application as a practical example
WebSocket groups are an essential tool for building scalable real-time applications, allowing for organized communication channels that deliver only relevant information to each client.
Further Exercises
To deepen your understanding, try implementing these features:
- Private Messaging: Allow users to send direct messages to specific users
- Persistent Room History: Store recent messages and show them to users when they join a room
- User Presence: Show which users are currently in each room
- Room Permissions: Implement different permission levels (admin, moderator, user)
- Custom Rooms: Allow users to create their own custom rooms
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)