在现代 Web 应用开发中,实时数据交互已成为核心需求。WebSocket 和 Server-Sent Events(SSE)作为两种主流的实时通信技术方案,分别适用于双向交互与单向推送场景。本文将基于 Go 语言的 Gin 框架,通过前后端代码示例,演示这两种技术的基本用法。

一、WebSocket 通信实现解析

1. 核心概念与优势

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,允许服务器主动向客户端推送数据,解决了 HTTP 协议的单向通信限制,适用于实时聊天、在线协作等场景。

2. Go 语言 Gin 框架实现代码

package controllers

import (
	"go_cli/utils"
	"log"
	"net/http"
	"strconv"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
)

// Connection 表示一个客户端连接
type Connection struct {
	ID        string
	WebSocket *websocket.Conn
	LastSeen  time.Time
	IP        string
}

// 全局连接管理器,直接使用sync.Map
var connectionManager = sync.Map{}

func HandleWebSocket(ctx *gin.Context) {
	// 配置WebSocket upgrader
	var upgrader = websocket.Upgrader{
		ReadBufferSize:  1024,
		WriteBufferSize: 1024,
		// 允许所有CORS请求
		CheckOrigin: func(r *http.Request) bool {
			return true
		},
	}
	// 将HTTP连接升级为WebSocket连接
	conn, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
	if err != nil {
		// utils.Fail(ctx, 500, "升级连接失败", err)
		return
	}
	defer conn.Close()
	log.Printf("客户端已连接: %s", ctx.Request.RemoteAddr)

	// 生成唯一客户端ID
	clientID := "websocket-" + strconv.FormatInt(time.Now().UnixNano(), 10)

	// 创建新连接对象
	connection := &Connection{
		ID:        clientID,
		WebSocket: conn,
		LastSeen:  time.Now(),
		IP:        ctx.ClientIP(),
	}

	// 添加到连接管理器
	connectionManager.Store(clientID, connection)

	log.Printf("新客户端已连接 (ID: %s, IP: %s)", clientID, connection.IP)

	// 向客户端发送欢迎消息,包含其ID
	if err := conn.WriteMessage(websocket.TextMessage, []byte("欢迎连接到WebSocket服务器! 您的ID: "+clientID)); err != nil {
		log.Println("写入欢迎消息失败:", err)
		return
	}

	// 持续读取客户端消息
	for {
		// 更新最后活跃时间
		connection.LastSeen = time.Now()

		// 读取消息
		messageType, message, err := conn.ReadMessage()
		if err != nil {
			log.Printf("客户端 (ID: %s) 读取消息错误: %v", clientID, err)
			break
		}

		// 记录收到的消息
		log.Printf("从客户端 (ID: %s) 收到消息: %s", clientID, message)

		// 将消息发送回客户端(回显),添加ID前缀
		response := []byte("[" + clientID + "] " + string(message))
		if err := conn.WriteMessage(messageType, response); err != nil {
			log.Printf("向客户端 (ID: %s) 写入消息失败: %v", clientID, err)
			break
		}
	}

	// 从连接管理器中移除
	connectionManager.Delete(clientID)
	log.Printf("客户端断开连接 (ID: %s, IP: %s)", clientID, connection.IP)
}

// ListConnections 获取所有连接列表
func ListConnections() []*Connection {
	var connections []*Connection
	connectionManager.Range(func(key, value any) bool {
		if conn, ok := value.(*Connection); ok {
			connections = append(connections, conn)
		}
		return true
	})
	return connections
}

// GetConnection 获取指定ID的连接
func GetConnection(clientID string) (*Connection, bool) {
	value, exists := connectionManager.Load(clientID)
	if !exists {
		return nil, false
	}
	conn, ok := value.(*Connection)
	return conn, ok
}

3. 前端Vue实现WebSocket客户端

onMounted(() => {
    startWebSocket()
})

onUnmounted(() => {
    stopWebSocket()
})

let ws:WebSocket
const startWebSocket = () => {
    ws = new WebSocket('ws://127.0.0.1:9000/ws/connect')
    ws.onopen = () => {
        console.log('WebSocket连接成功')
        // 在连接建立后,可以发送消息到服务器
        sendWebSocketMessage('Hello, WebSocket!')
    }
    ws.onmessage = (event) => {
        console.log('接收到消息:', event.data)
    }
    ws.onclose = () => {
        console.log('WebSocket连接关闭')
        // 连接关闭后,重新连接
        setTimeout(startWebSocket, 1000)
    }
    ws.onerror = (error) => {
        console.error('WebSocket错误:', error)
    }
}

const stopWebSocket = () => {
    // 主动关闭WebSocket连接
    ws && ws.close()
}

const sendWebSocketMessage = (message:string | ArrayBufferLike | Blob | ArrayBufferView) => {
    // 向服务端发送消息
    ws && ws.send(message)
}

4. 关键逻辑说明

服务端
连接升级:通过gorilla/websocket库的Upgrader将 HTTP 请求升级为 WebSocket 协议,CheckOrigin允许跨域访问。
连接管理:使用sync.Map存储客户端连接,保证并发安全,包含 ID、连接对象、最后活跃时间和 IP 信息。
消息处理:循环读取客户端消息并回显,同时更新连接的活跃时间,断开时自动从连接池中移除。
客户端
创建WebSocket连接:使用new WebSocket(url)构造函数创建一个新的WebSocket对象,其中url是WebSocket服务器的地址。
设置事件处理程序:为WebSocket对象设置各种事件处理程序,如onopen、onmessage、onerror和onclose。
发送消息:当WebSocket连接成功建立后(即onopen事件触发时),客户端可以通过调用send方法发送消息。
接收消息:当服务器发送消息时(即onmessage事件触发时),客户端可以接收消息。
关闭连接:当不再需要WebSocket连接时,可以调用close方法关闭连接。

二、SSE(Server-Sent Events)通信实现解析

1. 核心概念与优势

SSE 是基于 HTTP 的单向通信协议,由服务器主动向客户端推送数据,适合新闻推送、实时通知等场景。其优势在于简单易用,无需复杂的连接管理,且支持自动重连。

2. Go 语言 Gin 框架实现代码

package controllers

import (
	"log"
	"net/http"
	"strconv"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
)

// SSEClient 表示一个SSE客户端连接
type SSEClient struct {
	ID        string
	Writer    http.ResponseWriter
	IP        string
	Connected time.Time
}

// 全局客户端连接管理器
var sseClients = sync.Map{}

func HandleSSE(ctx *gin.Context) {
	// 设置响应头
	ctx.Writer.Header().Set("Content-Type", "text/event-stream")
	ctx.Writer.Header().Set("Cache-Control", "no-cache")
	ctx.Writer.Header().Set("Connection", "keep-alive")

	// 生成唯一客户端ID
	clientID := "sse-" + strconv.FormatInt(time.Now().UnixNano(), 10)

	// 创建新客户端连接
	client := &SSEClient{
		ID:        clientID,
		Writer:    ctx.Writer,
		IP:        ctx.ClientIP(),
		Connected: time.Now(),
	}

	// 添加到连接管理器
	sseClients.Store(clientID, client)

	log.Printf("新SSE客户端已连接 (ID: %s, IP: %s)", clientID, client.IP)

	// 发送欢迎消息
	ctx.SSEvent("welcome", gin.H{"clientID": clientID})
	// ctx.Writer.Write([]byte("data: 欢迎使用SSE服务!\n\n"))
	ctx.Writer.Flush()

	// 监听客户端断开事件
	notify := ctx.Writer.CloseNotify()

	// 定时发送当前时间
	ticker := time.NewTicker(2 * time.Second)
	defer ticker.Stop()

	for {
		select {
		case <-notify:
			// 客户端断开连接,从管理器中移除
			sseClients.Delete(clientID)
			log.Printf("SSE客户端断开连接 (ID: %s, IP: %s)", clientID, client.IP)
		case <-ticker.C:
			// 发送当前时间
			ctx.SSEvent("message", gin.H{"time": time.Now().Format("2006-01-02 15:04:05")})
			// ctx.Writer.Write([]byte("data: " + time.Now().Format("2006-01-02 15:04:05") + "\n\n"))
			ctx.Writer.Flush()
		}
	}
}

// 获取当前所有连接的客户端
func GetAllClients() []*SSEClient {
	var clients []*SSEClient
	sseClients.Range(func(key, value interface{}) bool {
		if client, ok := value.(*SSEClient); ok {
			clients = append(clients, client)
		}
		return true
	})
	return clients
}

// 获取指定ID的连接
func GetSSEClient(clientID string) (*SSEClient, bool) {
	value, exists := sseClients.Load(clientID)
	if !exists {
		return nil, false
	}
	conn, ok := value.(*SSEClient)
	return conn, ok
}

3. 前端Vue实现SSE客户端

onMounted(() => {
    startSSE()
})

onUnmounted(() => {
    stopSSE()
})

let sse:EventSource
const startSSE = () => {
    sse = new EventSource('http://127.0.0.1:9000/sse/connect')
    sse.onopen = () => {
        console.log('SSE连接成功')
    }
    sse.onmessage = (event) => {
        console.log('接收到消息:', event.data)
    }
    sse.onerror = (error) => {
        console.error('SSE错误:', error)
    }
}

const stopSSE = () => {
    // 主动关闭SSE连接
    sse && sse.close()
}

4. 关键逻辑说明

响应头设置:必须设置Content-Type: text/event-stream,禁用缓存并保持长连接。
连接管理:使用sync.Map存储客户端的ResponseWriter,定时任务每秒推送当前时间。
断开处理:通过CloseNotify监听客户端断开事件,自动移除连接。

三、技术对比与适用场景

特性 WebSocket SSE
通信方向 全双工 (双向) 单工 (服务器→客户端)
协议基础 基于 TCP,独立协议(ws/wss) 基于 HTTP
典型场景 实时聊天、在线协作、游戏 新闻推送、系统通知、日志监控
连接管理 需要手动处理心跳、重连 自动重连,服务端单向维护
数据格式 支持文本 / 二进制,灵活性高 仅限文本格式(JSON / 纯文本)
跨域处理 需要服务端显式配置CheckOrigin 依赖 HTTP 响应头Access-Control