5.6 WebSocket 实时通信
WebSocket 协议基础
什么是 WebSocket
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,适合实时通信场景。
HTTP vs WebSocket:
HTTP:
客户端 服务器
| --请求--> |
| <--响应-- |
| --请求--> |
| <--响应-- |
(每次请求都需要建立连接)
WebSocket:
客户端 服务器
| --握手 (HTTP)--> |
| <--升级协议-- |
| <====TCP 连接====> |
| (双向通信) |
| <--推送消息-- |
| --发送消息--> |
(连接保持,双向通信)WebSocket 握手过程
1. 客户端发送 HTTP 请求 (Upgrade)
GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
2. 服务器响应 (101 Switching Protocols)
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=gorilla/websocket 使用
安装
bash
go get github.com/gorilla/websocket服务端示例
go
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/websocket"
)
// 配置 Upgrader
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
// 允许所有来源 (生产环境应该限制)
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// WebSocket 连接
type Client struct {
conn *websocket.Conn
send chan []byte
}
func (c *Client) readPump() {
defer c.conn.Close()
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("error: %v", err)
}
break
}
// 处理收到的消息
log.Printf("收到:%s", message)
}
}
func (c *Client) writePump() {
defer c.conn.Close()
for {
select {
case message, ok := <-c.send:
if !ok {
// 关闭连接
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
w.Write(message)
if err := w.Close(); err != nil {
return
}
}
}
}
// WebSocket 处理函数
func wsHandler(w http.ResponseWriter, r *http.Request) {
// 升级为 WebSocket 连接
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
client := &Client{
conn: conn,
send: make(chan []byte, 256),
}
go client.writePump()
go client.readPump()
}
func main() {
http.HandleFunc("/ws", wsHandler)
fmt.Println("WebSocket 服务器启动:ws://localhost:8080/ws")
log.Fatal(http.ListenAndServe(":8080", nil))
}客户端示例
go
package main
import (
"fmt"
"log"
"time"
"github.com/gorilla/websocket"
)
func main() {
// 连接 WebSocket 服务器
conn, _, err := websocket.DefaultDialer.Dial("ws://localhost:8080/ws", nil)
if err != nil {
log.Fatal("连接失败:", err)
}
defer conn.Close()
// 发送消息
go func() {
for {
err := conn.WriteMessage(websocket.TextMessage, []byte("Hello Server!"))
if err != nil {
log.Println("发送失败:", err)
return
}
time.Sleep(time.Second)
}
}()
// 接收消息
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Println("接收失败:", err)
return
}
fmt.Println("收到:", string(message))
}
}聊天室实现
完整的聊天室示例
go
package main
import (
"fmt"
"log"
"net/http"
"sync"
"github.com/gorilla/websocket"
)
// 消息
type Message struct {
Type string `json:"type"` // join, chat, leave
User string `json:"user"`
Data string `json:"data"`
}
// 客户端
type Client struct {
hub *Hub
conn *websocket.Conn
send chan []byte
}
// 中心 (Hub) 管理所有客户端
type Hub struct {
mu sync.RWMutex
clients map[*Client]bool
broadcast chan []byte
register chan *Client
unregister chan *Client
}
func NewHub() *Hub {
return &Hub{
clients: make(map[*Client]bool),
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
}
}
func (h *Hub) run() {
for {
select {
case client := <-h.register:
h.mu.Lock()
h.clients[client] = true
h.mu.Unlock()
log.Printf("客户端加入,总数:%d", len(h.clients))
case client := <-h.unregister:
h.mu.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
h.mu.Unlock()
log.Printf("客户端离开,总数:%d", len(h.clients))
case message := <-h.broadcast:
h.mu.RLock()
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
h.mu.RUnlock()
}
}
}
var hub = NewHub()
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
break
}
// 广播消息给所有客户端
c.hub.broadcast <- message
}
}
func (c *Client) writePump() {
defer func() {
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
c.conn.WriteMessage(websocket.TextMessage, message)
}
}
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
client := &Client{
hub: hub,
conn: conn,
send: make(chan []byte, 256),
}
client.hub.register <- client
go client.writePump()
go client.readPump()
}
func main() {
go hub.run()
http.HandleFunc("/ws", wsHandler)
fmt.Println("聊天室服务器启动:ws://localhost:8080/ws")
log.Fatal(http.ListenAndServe(":8080", nil))
}前端 HTML 示例
html
<!DOCTYPE html>
<html>
<head>
<title>WebSocket 聊天室</title>
</head>
<body>
<div id="messages"></div>
<input id="input" placeholder="输入消息...">
<button onclick="send()">发送</button>
<script>
const ws = new WebSocket('ws://localhost:8080/ws');
const messages = document.getElementById('messages');
const input = document.getElementById('input');
ws.onmessage = function(event) {
const msg = document.createElement('div');
msg.textContent = event.data;
messages.appendChild(msg);
};
function send() {
ws.send(input.value);
input.value = '';
}
input.addEventListener('keypress', function(e) {
if (e.key === 'Enter') send();
});
</script>
</body>
</html>WebSocket 心跳机制
心跳检测实现
go
package main
import (
"log"
"net/http"
"time"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
const (
// 心跳间隔
pingPeriod = 30 * time.Second
// 写超时
writeWait = 10 * time.Second
// 读取超时 (pong 等待时间)
pongWait = 60 * time.Second
)
func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
// 设置读取超时
conn.SetReadLimit(65536)
conn.SetReadDeadline(time.Now().Add(pongWait))
// 设置 Pong 处理器
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
// 发送 Ping
go func() {
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
for range ticker.C {
conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
log.Println("发送 Ping 失败:", err)
return
}
}
}()
// 读取消息
for {
_, _, err := conn.ReadMessage()
if err != nil {
break
}
}
conn.Close()
}
func main() {
http.HandleFunc("/ws", wsHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}连接管理
连接池实现
go
package main
import (
"sync"
"time"
"github.com/gorilla/websocket"
)
type Connection struct {
Conn *websocket.Conn
CreatedAt time.Time
LastActive time.Time
Metadata map[string]interface{}
}
type ConnectionPool struct {
mu sync.RWMutex
connections map[string]*Connection
}
func NewConnectionPool() *ConnectionPool {
return &ConnectionPool{
connections: make(map[string]*Connection),
}
}
func (p *ConnectionPool) Add(id string, conn *websocket.Conn) {
p.mu.Lock()
defer p.mu.Unlock()
p.connections[id] = &Connection{
Conn: conn,
CreatedAt: time.Now(),
LastActive: time.Now(),
Metadata: make(map[string]interface{}),
}
}
func (p *ConnectionPool) Get(id string) (*Connection, bool) {
p.mu.RLock()
defer p.mu.RUnlock()
conn, ok := p.connections[id]
return conn, ok
}
func (p *ConnectionPool) Remove(id string) {
p.mu.Lock()
defer p.mu.Unlock()
if conn, ok := p.connections[id]; ok {
conn.Conn.Close()
delete(p.connections, id)
}
}
func (p *ConnectionPool) Broadcast(message []byte) {
p.mu.RLock()
defer p.mu.RUnlock()
for _, conn := range p.connections {
conn.Conn.WriteMessage(websocket.TextMessage, message)
}
}
func (p *ConnectionPool) Count() int {
p.mu.RLock()
defer p.mu.RUnlock()
return len(p.connections)
}
// 清理不活跃的连接
func (p *ConnectionPool) Cleanup(timeout time.Duration) {
p.mu.Lock()
defer p.mu.Unlock()
now := time.Now()
for id, conn := range p.connections {
if now.Sub(conn.LastActive) > timeout {
conn.Conn.Close()
delete(p.connections, id)
}
}
}Redis Pub/Sub 集成
水平扩展方案
go
package main
import (
"encoding/json"
"log"
"github.com/go-redis/redis/v8"
"github.com/gorilla/websocket"
"golang.org/x/context"
)
type RedisHub struct {
redisClient *redis.Client
ctx context.Context
clients map[*Client]bool
mu sync.RWMutex
}
func NewRedisHub(addr string) *RedisHub {
client := redis.NewClient(&redis.Options{
Addr: addr,
})
return &RedisHub{
redisClient: client,
ctx: context.Background(),
clients: make(map[*Client]bool),
}
}
func (h *RedisHub) Subscribe(channel string) {
pubsub := h.redisClient.Subscribe(h.ctx, channel)
_, err := pubsub.Receive(h.ctx)
if err != nil {
log.Println("订阅失败:", err)
return
}
ch := pubsub.Channel()
for msg := range ch {
h.Broadcast([]byte(msg.Payload))
}
}
func (h *RedisHub) Publish(channel string, message []byte) error {
return h.redisClient.Publish(h.ctx, channel, string(message)).Err()
}
func (h *RedisHub) Broadcast(message []byte) {
h.mu.RLock()
defer h.mu.RUnlock()
for client := range h.clients {
select {
case client.send <- message:
default:
h.RemoveClient(client)
}
}
}
func (h *RedisHub) AddClient(client *Client) {
h.mu.Lock()
defer h.mu.Unlock()
h.clients[client] = true
}
func (h *RedisHub) RemoveClient(client *Client) {
h.mu.Lock()
defer h.mu.Unlock()
delete(h.clients, client)
close(client.send)
}WebSocket 检查清单
[ ] 配置 CheckOrigin 函数限制来源
[ ] 实现心跳机制检测连接状态
[ ] 设置合理的读写超时
[ ] 使用中心 (Hub) 管理所有连接
[ ] 处理连接关闭情况
[ ] 广播时处理慢客户端
[ ] 考虑使用 Redis Pub/Sub 扩展
[ ] 添加连接认证
[ ] 限制消息大小
[ ] 使用 TLS (wss://)
[ ] 监控连接数
[ ] 优雅关闭连接
安全注意事项:
1. 验证 Origin 防止 CSRF 攻击
2. 实现认证机制 (JWT、Session)
3. 限制单个 IP 的连接数
4. 验证消息内容和格式
5. 使用 wss:// 加密通信