WebSocket Proxy
Build real-time applications with WebSocket support.
Simple Echo Server
go
package main
import "C"
import (
"fmt"
sdk "github.com/AssetsArt/nylon/sdk/go/sdk"
)
func main() {}
func init() {
plugin := sdk.NewNylonPlugin()
plugin.AddPhaseHandler("echo", func(phase *sdk.PhaseHandler) {
phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
err := ctx.WebSocketUpgrade(sdk.WebSocketCallbacks{
OnOpen: func(ws *sdk.WebSocketConn) {
fmt.Println("[Echo] Client connected")
ws.SendText("Welcome to Echo Server!")
},
OnMessageText: func(ws *sdk.WebSocketConn, msg string) {
fmt.Printf("[Echo] Received: %s\n", msg)
ws.SendText("Echo: " + msg)
},
OnMessageBinary: func(ws *sdk.WebSocketConn, data []byte) {
fmt.Printf("[Echo] Received %d bytes\n", len(data))
ws.SendBinary(data)
},
OnClose: func(ws *sdk.WebSocketConn) {
fmt.Println("[Echo] Client disconnected")
},
OnError: func(ws *sdk.WebSocketConn, err string) {
fmt.Printf("[Echo] Error: %s\n", err)
},
})
if err != nil {
res := ctx.Response()
res.SetStatus(400)
res.BodyText("WebSocket upgrade failed")
res.RemoveHeader("Content-Length")
res.SetHeader("Transfer-Encoding", "chunked")
ctx.End()
return
}
})
})
}Build:
bash
go build -buildmode=c-shared -o echo.soConfiguration:
yaml
plugins:
- name: ws
type: ffi
file: ./echo.so
services:
- name: websocket
service_type: plugin
plugin:
name: ws
entry: "echo"
routes:
- route:
type: host
value: localhost
name: websocket
paths:
- path: /ws
service:
name: websocketClient:
javascript
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onopen = () => {
console.log('Connected');
ws.send('Hello!');
};
ws.onmessage = (event) => {
console.log('Received:', event.data);
};Chat Room
go
plugin.AddPhaseHandler("chat", func(phase *sdk.PhaseHandler) {
phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
ctx.WebSocketUpgrade(sdk.WebSocketCallbacks{
OnOpen: func(ws *sdk.WebSocketConn) {
// Join lobby
ws.JoinRoom("lobby")
// Notify others
ws.BroadcastText("lobby", "A user joined the chat")
// Welcome message
ws.SendText("Welcome! You are in the lobby.")
},
OnMessageText: func(ws *sdk.WebSocketConn, msg string) {
// Broadcast to everyone in lobby
ws.BroadcastText("lobby", msg)
},
OnClose: func(ws *sdk.WebSocketConn) {
// Leave room
ws.LeaveRoom("lobby")
// Notify others
ws.BroadcastText("lobby", "A user left the chat")
},
})
})
})Multi-Room Chat
go
plugin.AddPhaseHandler("multi-room", func(phase *sdk.PhaseHandler) {
phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
currentRoom := "lobby"
ctx.WebSocketUpgrade(sdk.WebSocketCallbacks{
OnOpen: func(ws *sdk.WebSocketConn) {
ws.JoinRoom(currentRoom)
ws.SendText("Joined " + currentRoom)
ws.SendText("Commands: /join <room>, /leave, /rooms")
},
OnMessageText: func(ws *sdk.WebSocketConn, msg string) {
// Handle commands
if strings.HasPrefix(msg, "/join ") {
newRoom := strings.TrimPrefix(msg, "/join ")
// Leave current room
ws.LeaveRoom(currentRoom)
// Join new room
currentRoom = newRoom
ws.JoinRoom(currentRoom)
ws.SendText("Joined " + currentRoom)
ws.BroadcastText(currentRoom, "A user joined")
return
}
if msg == "/leave" {
ws.LeaveRoom(currentRoom)
currentRoom = ""
ws.SendText("Left room")
return
}
if msg == "/rooms" {
ws.SendText("Current room: " + currentRoom)
return
}
// Broadcast message
if currentRoom != "" {
ws.BroadcastText(currentRoom, msg)
}
},
OnClose: func(ws *sdk.WebSocketConn) {
if currentRoom != "" {
ws.LeaveRoom(currentRoom)
}
},
})
})
})JSON-based Protocol
go
type Message struct {
Type string `json:"type"`
Data map[string]interface{} `json:"data"`
}
plugin.AddPhaseHandler("json-ws", func(phase *sdk.PhaseHandler) {
phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
ctx.WebSocketUpgrade(sdk.WebSocketCallbacks{
OnMessageText: func(ws *sdk.WebSocketConn, msg string) {
var message Message
if err := json.Unmarshal([]byte(msg), &message); err != nil {
ws.SendText(`{"type":"error","data":{"message":"Invalid JSON"}}`)
return
}
switch message.Type {
case "join":
room := message.Data["room"].(string)
ws.JoinRoom(room)
response := Message{
Type: "joined",
Data: map[string]interface{}{
"room": room,
},
}
data, _ := json.Marshal(response)
ws.SendText(string(data))
case "message":
room := message.Data["room"].(string)
text := message.Data["text"].(string)
broadcast := Message{
Type: "message",
Data: map[string]interface{}{
"room": room,
"text": text,
"timestamp": time.Now().Unix(),
},
}
data, _ := json.Marshal(broadcast)
ws.BroadcastText(room, string(data))
default:
ws.SendText(`{"type":"error","data":{"message":"Unknown message type"}}`)
}
},
})
})
})Authentication
go
plugin.AddPhaseHandler("auth-ws", func(phase *sdk.PhaseHandler) {
phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
req := ctx.Request()
// Check token in query
query := req.Query()
params, _ := url.ParseQuery(query)
token := params.Get("token")
if !validateToken(token) {
res := ctx.Response()
res.SetStatus(401)
res.BodyText("Unauthorized")
res.RemoveHeader("Content-Length")
res.SetHeader("Transfer-Encoding", "chunked")
ctx.End()
return
}
// Extract user info
userID := getUserIDFromToken(token)
ctx.WebSocketUpgrade(sdk.WebSocketCallbacks{
OnOpen: func(ws *sdk.WebSocketConn) {
// Join user-specific room
ws.JoinRoom("user:" + userID)
ws.SendText(fmt.Sprintf("Authenticated as %s", userID))
},
OnMessageText: func(ws *sdk.WebSocketConn, msg string) {
// User is authenticated, handle message
ws.BroadcastText("authenticated", fmt.Sprintf("%s: %s", userID, msg))
},
OnClose: func(ws *sdk.WebSocketConn) {
ws.LeaveRoom("user:" + userID)
},
})
})
})Complete Example
plugin.go:
go
package main
import "C"
import (
"encoding/json"
"fmt"
"strings"
"time"
sdk "github.com/AssetsArt/nylon/sdk/go/sdk"
)
type ChatMessage struct {
Type string `json:"type"`
User string `json:"user,omitempty"`
Room string `json:"room,omitempty"`
Message string `json:"message,omitempty"`
Timestamp int64 `json:"timestamp"`
}
func main() {}
func init() {
plugin := sdk.NewNylonPlugin()
plugin.AddPhaseHandler("chat", func(phase *sdk.PhaseHandler) {
phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
req := ctx.Request()
// Get username from query
query := req.Query()
params, _ := url.ParseQuery(query)
username := params.Get("username")
if username == "" {
res := ctx.Response()
res.SetStatus(400)
res.BodyText("Username required")
res.RemoveHeader("Content-Length")
res.SetHeader("Transfer-Encoding", "chunked")
ctx.End()
return
}
currentRoom := "lobby"
ctx.WebSocketUpgrade(sdk.WebSocketCallbacks{
OnOpen: func(ws *sdk.WebSocketConn) {
ws.JoinRoom(currentRoom)
// Send welcome
welcome := ChatMessage{
Type: "system",
Message: fmt.Sprintf("Welcome %s! You are in %s", username, currentRoom),
Timestamp: time.Now().Unix(),
}
data, _ := json.Marshal(welcome)
ws.SendText(string(data))
// Notify others
joined := ChatMessage{
Type: "join",
User: username,
Room: currentRoom,
Timestamp: time.Now().Unix(),
}
data, _ = json.Marshal(joined)
ws.BroadcastText(currentRoom, string(data))
},
OnMessageText: func(ws *sdk.WebSocketConn, msg string) {
// Handle /join command
if strings.HasPrefix(msg, "/join ") {
newRoom := strings.TrimPrefix(msg, "/join ")
// Leave current room
left := ChatMessage{
Type: "leave",
User: username,
Room: currentRoom,
Timestamp: time.Now().Unix(),
}
data, _ := json.Marshal(left)
ws.BroadcastText(currentRoom, string(data))
ws.LeaveRoom(currentRoom)
// Join new room
currentRoom = newRoom
ws.JoinRoom(currentRoom)
joined := ChatMessage{
Type: "join",
User: username,
Room: currentRoom,
Timestamp: time.Now().Unix(),
}
data, _ = json.Marshal(joined)
ws.BroadcastText(currentRoom, string(data))
// Confirm to user
confirm := ChatMessage{
Type: "system",
Message: "Joined " + currentRoom,
Timestamp: time.Now().Unix(),
}
data, _ = json.Marshal(confirm)
ws.SendText(string(data))
return
}
// Regular message
message := ChatMessage{
Type: "message",
User: username,
Room: currentRoom,
Message: msg,
Timestamp: time.Now().Unix(),
}
data, _ := json.Marshal(message)
ws.BroadcastText(currentRoom, string(data))
},
OnClose: func(ws *sdk.WebSocketConn) {
// Notify others
left := ChatMessage{
Type: "leave",
User: username,
Room: currentRoom,
Timestamp: time.Now().Unix(),
}
data, _ := json.Marshal(left)
ws.BroadcastText(currentRoom, string(data))
ws.LeaveRoom(currentRoom)
},
OnError: func(ws *sdk.WebSocketConn, err string) {
fmt.Printf("[Chat] Error for %s: %s\n", username, err)
},
})
})
})
}Client (HTML):
html
<!DOCTYPE html>
<html>
<head>
<title>Chat</title>
<style>
#messages { height: 400px; overflow-y: scroll; border: 1px solid #ccc; padding: 10px; }
.message { margin: 5px 0; }
.system { color: gray; font-style: italic; }
.join { color: green; }
.leave { color: red; }
</style>
</head>
<body>
<div id="messages"></div>
<input id="input" type="text" placeholder="Type message or /join <room>">
<button onclick="send()">Send</button>
<script>
const username = prompt('Enter username:');
const ws = new WebSocket(`ws://localhost:8080/ws?username=${username}`);
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
const div = document.createElement('div');
div.className = 'message ' + msg.type;
if (msg.type === 'message') {
div.textContent = `${msg.user}: ${msg.message}`;
} else if (msg.type === 'system') {
div.textContent = msg.message;
} else if (msg.type === 'join') {
div.textContent = `${msg.user} joined ${msg.room}`;
} else if (msg.type === 'leave') {
div.textContent = `${msg.user} left ${msg.room}`;
}
document.getElementById('messages').appendChild(div);
};
function send() {
const input = document.getElementById('input');
ws.send(input.value);
input.value = '';
}
document.getElementById('input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') send();
});
</script>
</body>
</html>Configuration
yaml
# Runtime config
http:
- 0.0.0.0:8080
config_dir: "./config"
# WebSocket adapter
websocket:
adapter_type: memory # or redis for multi-server
# If using Redis
# websocket:
# adapter_type: redis
# redis:
# host: localhost
# port: 6379
# db: 0
# key_prefix: "nylon:ws"yaml
# Proxy config
plugins:
- name: chat
type: ffi
file: ./chat.so
services:
- name: websocket
service_type: plugin
plugin:
name: chat
entry: "chat"
- name: static
service_type: static
static:
root: ./public
index: index.html
routes:
- route:
type: host
value: localhost
name: chat
paths:
- path: /ws
service:
name: websocket
- path:
- /
- /{*path}
service:
name: staticBest Practices
1. Validate Input
go
OnMessageText: func(ws *sdk.WebSocketConn, msg string) {
if len(msg) > 1000 {
ws.SendText("Message too long")
return
}
// Process message
}2. Handle Errors
go
err := ctx.WebSocketUpgrade(callbacks)
if err != nil {
res := ctx.Response()
res.SetStatus(400)
res.BodyText("Upgrade failed")
res.RemoveHeader("Content-Length")
res.SetHeader("Transfer-Encoding", "chunked")
ctx.End()
return
}3. Clean Up Resources
go
OnClose: func(ws *sdk.WebSocketConn) {
ws.LeaveRoom("lobby")
// Clean up other resources
}4. Use Rooms for Broadcasting
go
// Efficient - only sends to room members
ws.BroadcastText("lobby", msg)
// Inefficient - manual iteration
for _, conn := range allConnections {
conn.SendText(msg)
}5. Authenticate Connections
go
// Check token before upgrade
token := params.Get("token")
if !validateToken(token) {
res := ctx.Response()
res.SetStatus(401)
res.BodyText("Unauthorized")
res.RemoveHeader("Content-Length")
res.SetHeader("Transfer-Encoding", "chunked")
ctx.End()
return
}See Also
- WebSocket Support - WebSocket plugin guide
- Request Handling - Access request information
- Configuration - WebSocket configuration