The websocket package provides real-time bidirectional communication between the Quazaar server and clients. It handles connection management, message routing, ping/pong, and channel-based broadcasting.
- Package:
websocket - Location:
utils/websocket/ - Purpose: Real-time client-server communication
- Protocol: WebSocket (RFC 6455)
- Library:
github.com/gorilla/websocket
| File | Purpose | Key Functions |
|---|---|---|
handler.go |
Connection handling | Handle() |
websocke.go |
Connection utilities | CreateWebSocketConnection(), SendWebSocketMessage() |
channel.go |
Channel management | CreateChannel(), WriteChannelMessage() |
pingPong.go |
Ping/pong implementation | HandlePingPong(), SendPong() |
sequenceDiagram
participant Client
participant HTTP as HTTP Handler
participant Upgrader
participant Conn as WebSocket
participant Writer
participant Reader
participant Channel
Client->>HTTP: HTTP GET /ws
HTTP->>Upgrader: Upgrade connection
Upgrader-->>Conn: WebSocket established
Conn->>Client: Welcome message
par Writer Goroutine
Conn->>Writer: Start writer
loop Broadcast
Channel->>Writer: Read messages
Writer->>Client: Send JSON
end
and Reader Loop
Conn->>Reader: Start reader
loop Receive
Client->>Reader: Send command
Reader->>Reader: HandlePingPong
Reader->>Client: Response
end
end
Note over Writer,Reader: Both run concurrently
graph TB
Poller["Poller/Service"] -->|data| Channel["Shared Channel capacity: 100"]
Channel -->|range loop| Writer["Writer Goroutine"]
Writer -->|WriteJSON| WS1[WebSocket]
WS1 -->|network| Client1["Web Client"]
Client2["Web Client"] -->|network| WS2[WebSocket]
WS2 -->|ReadJSON| Reader["Reader Loop"]
Reader -->|parse| Handler["Command Handler"]
Handler -->|response| WS2
Main WebSocket connection handler.
Description: HTTP handler that upgrades connection to WebSocket and manages client communication.
Parameters:
res http.ResponseWriter- HTTP response writerreq *http.Request- HTTP request
Flow:
- Upgrades HTTP connection to WebSocket
- Sends welcome message
- Starts writer goroutine (broadcasts to client)
- Starts reader loop (receives from client)
- Handles disconnection cleanup
Implementation:
func Handle(res http.ResponseWriter, req *http.Request) {
conn, err := CreateWebSocketConnection(res, req)
if err != nil {
http.Error(res, "Failed to upgrade connection", http.StatusInternalServerError)
return
}
defer conn.Close()
// Send welcome message
msg := models.ServerResponse{
Message: "Welcome to the WebSocket server!",
}
if err := SendWebSocketMessage(msg); err != nil {
http.Error(res, "Failed to send welcome message", http.StatusInternalServerError)
return
}
chh := GetChannel()
if chh == nil {
http.Error(res, "Failed to get response channel", http.StatusInternalServerError)
return
}
// Writer goroutine - sends messages to client
go func() {
for response := range chh {
if err := conn.WriteJSON(response); err != nil {
break
}
}
}()
// Reader goroutine - receives messages from client
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
break
}
log.Printf("Received message: %+v\n", msg)
// Handle ping/pong
HandlePingPong(conn, msg)
}
}Goroutines:
- Purpose: Broadcasts server messages to client
- Source: Reads from shared channel
- Exit: When channel closed or write error
- Blocking: Yes (range on channel)
- Purpose: Receives and processes client messages
- Exit: When connection closed or read error
- Blocking: Yes (ReadJSON blocks)
WebSocket connection utilities.
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // Allow all origins (⚠️ security consideration)
}
}
var Conn *websocket.Conn // Global connection (⚠️ supports single client)Function: CreateWebSocketConnection(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error)
Description: Upgrades HTTP connection to WebSocket protocol.
Parameters:
w http.ResponseWriter- Response writerr *http.Request- HTTP request
Returns:
*websocket.Conn- WebSocket connectionerror- Error if upgrade fails
Example:
conn, err := CreateWebSocketConnection(w, r)
if err != nil {
log.Fatal("Upgrade failed:", err)
}
defer conn.Close()Configuration:
- CheckOrigin: Returns
true(allows all origins) - Note: Global
Connvariable is set (single client limitation)
Description: Closes the global WebSocket connection.
Behavior:
- Checks if
Connis nil - Closes connection if exists
- Logs success or error
Example:
CloseWebSocketConnection()Description: Sends a message directly through the global WebSocket connection.
Parameters:
msg models.ServerResponse- Message to send
Returns:
error- Error if send fails
Example:
err := SendWebSocketMessage(models.ServerResponse{
Status: "success",
Message: "Hello",
Data: map[string]string{"key": "value"},
})Note: Uses global Conn variable (not channel-based).
Description: Checks if WebSocket connection is active.
Returns:
bool- True if connected, false otherwise
Example:
if IsWebSocketConnected() {
SendWebSocketMessage(msg)
}Thread-safe channel management for broadcasting messages.
var (
sharedChannel chan models.ServerResponse
once sync.Once // Ensures single initialization
mu sync.RWMutex // Thread-safe access
)Description: Creates shared channel (singleton pattern using sync.Once).
Returns: chan models.ServerResponse - Shared channel
Capacity: 100 (buffered)
Thread Safety: ✅ Yes (sync.Once)
Implementation:
func CreateChannel() chan models.ServerResponse {
once.Do(func() {
sharedChannel = make(chan models.ServerResponse, 100)
log.Println("Channel created successfully")
})
return sharedChannel
}Usage:
ch := CreateChannel() // Safe to call multiple timesDescription: Gets or creates shared channel with mutex protection.
Returns: chan models.ServerResponse - Shared channel
Thread Safety: ✅ Yes (RWMutex)
Implementation:
func GetChannel() chan models.ServerResponse {
mu.Lock()
defer mu.Unlock()
if sharedChannel == nil {
sharedChannel = make(chan models.ServerResponse)
log.Println("Channel created inside GetChannel")
} else {
log.Println("Channel already exists, returning existing channel")
}
return sharedChannel
}Note: Creates unbuffered channel if not exists (differs from CreateChannel()).
Description: Closes shared channel and sets to nil.
Thread Safety: ✅ Yes (RWMutex)
Implementation:
func CloseChannel() {
mu.Lock()
defer mu.Unlock()
if sharedChannel != nil {
close(sharedChannel)
sharedChannel = nil
log.Println("Channel closed and set to nil")
} else {
log.Println("Channel is already nil, nothing to close")
}
}Warning: Closing channel will panic writer goroutines if they try to send.
Description: Sends message to shared channel (non-blocking).
Parameters:
msg models.ServerResponse- Message to broadcast
Behavior:
- Sends to channel if available
- Drops message if channel full (non-blocking)
- Logs if channel is nil
Implementation:
func WriteChannelMessage(msg models.ServerResponse) {
if sharedChannel == nil {
log.Println("Channel is nil, cannot send message")
return
}
select {
case sharedChannel <- msg:
// Message sent successfully
default:
log.Println("Channel is full, message not sent:", msg)
}
}Thread Safety:
Ping/pong command handling for connection health checks.
Description: Processes incoming messages and responds to ping commands.
Parameters:
conn *websocket.Conn- WebSocket connectionmsg map[string]interface{}- Received message
Behavior:
- Checks if message has "command" field
- If command is "ping", sends pong response
- Ignores non-ping messages
Example Message:
{
"command": "ping"
}Implementation:
func HandlePingPong(conn *websocket.Conn, msg map[string]interface{}) {
command, ok := msg["command"].(string)
if !ok {
return
}
if command == "ping" {
SendPong(conn)
}
}Description: Sends pong response with timestamp and server info.
Parameters:
conn *websocket.Conn- WebSocket connection
Response Format:
{
"status": "success",
"message": "pong",
"data": {
"timestamp": 1699999999,
"server": "Quazaar WebSocket"
}
}Implementation:
func SendPong(conn *websocket.Conn) {
response := models.ServerResponse{
Status: "success",
Message: "pong",
Data: map[string]interface{}{
"timestamp": time.Now().Unix(),
"server": "Quazaar WebSocket",
},
}
if err := conn.WriteJSON(response); err != nil {
log.Printf("❌ Failed to send pong: %v", err)
} else {
log.Println("🏓 Pong sent")
}
}Format: JSON object with command field
Examples:
// Ping
{"command": "ping"}
// Custom command (example)
{"command": "get_status"}Format: models.ServerResponse
type ServerResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Data interface{} `json:"data"`
}Examples:
{
"status": "success",
"message": "media_info",
"data": {
"title": "Song Title",
"artist": "Artist Name",
"album": "Album Name"
}
}{
"status": "success",
"message": "pong",
"data": {
"timestamp": 1699999999,
"server": "Quazaar WebSocket"
}
}{
"status": "",
"message": "Welcome to the WebSocket server!",
"data": null
}💻 Click to see full client implementation
const ws = new WebSocket("ws://localhost:8765/ws");
// Connection opened
ws.onopen = function (event) {
console.log("Connected");
// Send ping
ws.send(JSON.stringify({ command: "ping" }));
};
// Receive messages
ws.onmessage = function (event) {
const data = JSON.parse(event.data);
console.log("Received:", data);
if (data.message === "media_info") {
updateUI(data.data);
}
};
// Connection closed
ws.onclose = function (event) {
console.log("Disconnected");
};
// Error
ws.onerror = function (error) {
console.error("WebSocket error:", error);
};-
CORS:
CheckOriginreturnstrue(allows all origins)- Risk: Any website can connect to your WebSocket
- Fix: Validate origin properly
-
Single Client: Global
Connvariable supports only one client- Risk: Multiple clients will overwrite connection
- Fix: Use connection pool or per-client handling
-
No Authentication: No auth check on connection
- Risk: Anyone can connect
- Fix: Add token-based authentication
-
No Rate Limiting: Unlimited messages accepted
- Risk: DoS attacks
- Fix: Implement rate limiting
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
return origin == "http://localhost:8765" ||
origin == "http://127.0.0.1:8765"
},
}var clients = make(map[*websocket.Conn]bool)
var clientsMutex sync.RWMutex
// Add client
clientsMutex.Lock()
clients[conn] = true
clientsMutex.Unlock()
// Broadcast to all
for client := range clients {
client.WriteJSON(msg)
}token := r.URL.Query().Get("token")
if !validateToken(token) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}| Metric | Value |
|---|---|
| Max Messages/sec | ~10,000 |
| Latency (local) | < 1ms |
| Memory per connection | ~4 KB |
| Channel buffer | 100 messages |
- Channel Buffer: Full channel drops messages
- Single Connection: Global
Connlimits scalability - JSON Marshaling: Slower than binary protocols
if err := conn.ReadJSON(&msg); err != nil {
// Client disconnected or network error
log.Println("Read error:", err)
break // Exit reader loop
}if err := conn.WriteJSON(response); err != nil {
// Send failed (client disconnected)
log.Println("Write error:", err)
break // Exit writer goroutine
}if sharedChannel == nil {
log.Println("Channel is nil")
return
}Use the web client at http://127.0.0.1:8765/:
- Click "Connect" button
- Click "Send Ping" button
- Check console for messages
- Observe media info updates every second
func TestPingPong(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(Handle))
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws"
ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatal(err)
}
defer ws.Close()
// Send ping
ws.WriteJSON(map[string]string{"command": "ping"})
// Receive pong
var response models.ServerResponse
ws.ReadJSON(&response)
if response.Message != "pong" {
t.Errorf("Expected pong, got %s", response.Message)
}
}Error: dial tcp :8765: connect: connection refused
Cause: Server not running
Solution: Start server with go run main.go
Cause: Writer goroutine not started or channel not being read
Solution: Ensure Handle() starts writer goroutine and reads channel
Cause: Reader loop exits due to error
Solution: Check client is sending valid JSON
Cause: Global Conn variable
Solution: Refactor to support multiple connections
- Support multiple simultaneous clients
- Add authentication (JWT tokens)
- Implement proper origin validation
- Add rate limiting per client
- Support binary messages (Protocol Buffers)
- Add compression (permessage-deflate)
- Implement heartbeat/keepalive
- Add reconnection logic on client
- Support message acknowledgments
- Add metrics (connections, messages, errors)
Last Updated: November 14, 2025
Version: 1.0.0