211 lines
8.0 KiB
Markdown
211 lines
8.0 KiB
Markdown
# Technical Design: WebSocket Chat
|
|
|
|
## Architecture Overview
|
|
|
|
This feature integrates the existing `pkg/realtime` package into the `chat-api` service to provide WebSocket chat functionality with Redis pub/sub for horizontal scaling.
|
|
|
|
```
|
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
│ Client 1 │ │ Client 2 │ │ Client N │
|
|
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
|
│ WebSocket │ WebSocket │ WebSocket
|
|
▼ ▼ ▼
|
|
┌──────────────────────────────────────────────────────┐
|
|
│ chat-api Pod 1 │
|
|
│ ┌─────────────────────────────────────────────────┐ │
|
|
│ │ realtime.Handler │ │
|
|
│ │ ┌─────────────────────────────────────────┐ │ │
|
|
│ │ │ LocalHub │ │ │
|
|
│ │ │ - connections map[id]Connection │ │ │
|
|
│ │ │ - rooms map[room]map[id]struct{} │ │ │
|
|
│ │ └─────────────────────────────────────────┘ │ │
|
|
│ └─────────────────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ┌─────────────────────────────────────────────────┐ │
|
|
│ │ RedisBroadcaster │ │
|
|
│ │ - Publish() → Redis channel │ │
|
|
│ │ - Run() ← Redis PSubscribe │ │
|
|
│ └─────────────────────────────────────────────────┘ │
|
|
└──────────────────────────┬───────────────────────────┘
|
|
│
|
|
┌────────────┴────────────┐
|
|
│ Redis Pub/Sub │
|
|
│ realtime:global │
|
|
│ realtime:room:{name} │
|
|
└────────────┬────────────┘
|
|
│
|
|
┌──────────────────────────┴───────────────────────────┐
|
|
│ chat-api Pod 2 │
|
|
│ (same structure) │
|
|
└──────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## Component Design
|
|
|
|
### 1. Configuration Extension
|
|
|
|
Extend `internal/config/config.go` to include Redis configuration:
|
|
|
|
```go
|
|
type Config struct {
|
|
// ... existing fields ...
|
|
|
|
// Redis
|
|
RedisURL string
|
|
}
|
|
|
|
func Load() *Config {
|
|
return &Config{
|
|
// ... existing ...
|
|
RedisURL: getEnv("REDIS_URL", "redis://localhost:6379"),
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Service Bootstrap (main.go)
|
|
|
|
Wire up the realtime components in the service entry point:
|
|
|
|
```go
|
|
func main() {
|
|
cfg := config.Load()
|
|
ctx := context.Background()
|
|
|
|
// Create hub (local connection registry)
|
|
hub := realtime.NewHub(logger)
|
|
go hub.Run(ctx)
|
|
|
|
// Create Redis broadcaster (for multi-pod)
|
|
var broadcaster realtime.Broadcaster
|
|
if cfg.RedisURL != "" {
|
|
redisClient := redis.NewClient(parseRedisURL(cfg.RedisURL))
|
|
broadcaster = realtime.NewRedisBroadcaster(redisClient, hub, logger)
|
|
go broadcaster.Run(ctx)
|
|
}
|
|
|
|
// Register routes with realtime handler
|
|
api.RegisterRoutes(application, exampleService, hub, broadcaster)
|
|
}
|
|
```
|
|
|
|
### 3. Route Registration
|
|
|
|
Add WebSocket routes in `internal/api/routes.go`:
|
|
|
|
```go
|
|
func RegisterRoutes(
|
|
application *app.App,
|
|
exampleService *service.ExampleService,
|
|
hub realtime.Hub,
|
|
broadcaster realtime.Broadcaster,
|
|
) {
|
|
// ... existing routes ...
|
|
|
|
// WebSocket handler
|
|
wsHandler := realtime.NewHandler(hub, logger, realtime.HandlerConfig{
|
|
Broadcaster: broadcaster,
|
|
OnConnect: func(conn realtime.Connection) {
|
|
logger.Info("client connected", "id", conn.ID())
|
|
},
|
|
OnDisconnect: func(conn realtime.Connection) {
|
|
logger.Info("client disconnected", "id", conn.ID())
|
|
},
|
|
OnMessage: func(conn realtime.Connection, msg *realtime.Message) *realtime.Message {
|
|
// Set message type if not specified
|
|
if msg.Type == "" {
|
|
msg.Type = realtime.MessageTypeChat
|
|
}
|
|
return msg
|
|
},
|
|
AuthRequired: cfg.AuthEnabled,
|
|
})
|
|
|
|
// Mount WebSocket routes
|
|
application.Mount("/api/chat-api/ws", wsHandler.Routes())
|
|
}
|
|
```
|
|
|
|
### 4. Message Flow
|
|
|
|
1. **Client → Server (Incoming)**
|
|
- Client sends JSON message via WebSocket
|
|
- `WSClient.readPump()` decodes message
|
|
- `OnMessage` callback transforms/filters
|
|
- `RedisBroadcaster.Publish()` sends to Redis channel
|
|
|
|
2. **Redis → All Pods (Distribution)**
|
|
- Redis delivers to all subscribed pods
|
|
- `RedisBroadcaster.Run()` receives via PSubscribe
|
|
- Filters out messages from same pod (echo prevention)
|
|
- Forwards to `LocalHub.Broadcast()`
|
|
|
|
3. **Server → Clients (Outgoing)**
|
|
- `LocalHub.doBroadcast()` iterates room connections
|
|
- `WSClient.Send()` queues message
|
|
- `WSClient.writePump()` writes to WebSocket
|
|
|
|
### 5. Redis Channel Naming
|
|
|
|
| Pattern | Usage |
|
|
|---------|-------|
|
|
| `realtime:global` | Messages without room (broadcast to all) |
|
|
| `realtime:room:{name}` | Room-specific messages |
|
|
|
|
### 6. Graceful Shutdown
|
|
|
|
The existing `pkg/realtime` package handles:
|
|
- Context cancellation triggers hub shutdown
|
|
- Hub closes all connections
|
|
- WSClient sends close frame
|
|
- Redis broadcaster closes subscription
|
|
|
|
## File Changes
|
|
|
|
| File | Change |
|
|
|------|--------|
|
|
| `services/chat-api/internal/config/config.go` | Add `RedisURL` field |
|
|
| `services/chat-api/cmd/server/main.go` | Wire hub, broadcaster, Redis client |
|
|
| `services/chat-api/internal/api/routes.go` | Mount WebSocket handler |
|
|
| `services/chat-api/internal/api/spec.go` | Add WebSocket endpoint documentation |
|
|
|
|
## Testing Strategy
|
|
|
|
### Unit Tests
|
|
- Config loading with Redis URL
|
|
- Message transformation in OnMessage callback
|
|
|
|
### Integration Tests
|
|
- WebSocket connection upgrade
|
|
- Message send/receive flow
|
|
- Room-based isolation
|
|
- Multi-client broadcast
|
|
|
|
### Manual Testing
|
|
```bash
|
|
# Terminal 1: Start server
|
|
./scripts/dev.sh
|
|
|
|
# Terminal 2: Connect with wscat
|
|
wscat -c ws://localhost:8001/api/chat-api/ws
|
|
|
|
# Terminal 3: Another client
|
|
wscat -c ws://localhost:8001/api/chat-api/ws
|
|
|
|
# Send message from Terminal 2, verify received in Terminal 3
|
|
{"type":"chat","data":{"content":"Hello!"}}
|
|
```
|
|
|
|
## Security Considerations
|
|
|
|
1. **Authentication**: Optional auth via `AUTH_ENABLED` config
|
|
2. **Origin Check**: WebSocket upgrader validates Origin header (configurable)
|
|
3. **Message Size**: 64KB limit prevents memory exhaustion
|
|
4. **Buffer Limits**: 256-message buffer drops excess (prevents slow-client backpressure)
|
|
|
|
## Rollout Plan
|
|
|
|
1. Deploy with `REDIS_URL` set to enable cross-pod messaging
|
|
2. Without `REDIS_URL`, operates in single-pod mode (local broadcast only)
|
|
3. No database migrations required
|
|
4. Feature is additive (no breaking changes)
|