传统 HTTP 通信通常采用:
客户端发送请求
↓
服务器处理请求
↓
服务器返回响应
↓
请求结束
例如:
GET /api/user HTTP/1.1
Host: example.com
服务器:
HTTP/1.1 200 OK
Content-Type: application/json
{
"name": "Tom"
}
这种模式本质上是:
Client Request
↓
Server Response
↓
连接/请求结束
但是很多实时业务需要服务器主动向客户端发送数据,例如:
如果使用普通 HTTP,客户端可能需要不断轮询:
Client → Server:有新消息吗?
Client ← Server:没有
Client → Server:有新消息吗?
Client ← Server:没有
Client → Server:有新消息吗?
Client ← Server:有
Client → Server:有新消息吗?
...
这种方式称为 Polling(轮询)。
轮询会产生大量无意义 HTTP 请求,因此出现了更加适合实时通信的技术:
SSE
WebSocket
SSE 全称:
Server-Sent Events
即:
服务器发送事件
SSE 是一种基于 HTTP 的服务器向客户端持续推送数据的通信机制。
通信方向:
Client ───── HTTP Request ─────> Server
Client <──── SSE Event ───────── Server
Client <──── SSE Event ───────── Server
Client <──── SSE Event ───────── Server
Client <──── SSE Event ───────── Server
建立连接以后,服务器可以不断向客户端写入数据。
但 SSE 本身只负责:
Server → Client
因此它是单向通信。
如果客户端需要向服务器发送数据,仍然可以使用:
fetch
axios
普通 HTTP API
例如:
HTTP POST
Client ─────────────────────────> Server
SSE
Client <───────────────────────── Server
这也是很多 AI 对话系统常见的通信模式。
SSE 的数据流向为:
Server → Client
服务器可以主动向客户端推送消息。
客户端如果需要发送数据:
Client → Server
一般通过普通 HTTP 请求完成。
SSE 并没有设计一个新的传输协议。
它本质上仍然是一个 HTTP 请求,只不过服务器:
不立即结束 Response
而是持续向 Response Body 中写数据。
例如:
HTTP Response
│
├── data: message1
│
├── data: message2
│
├── data: message3
│
└── ...
因此 SSE 通常比较容易兼容现有:
HTTP Server
HTTPS
反向代理
负载均衡
不过代理服务器是否会缓存或缓冲响应仍然需要正确配置。
客户端建立 SSE:
const source = new EventSource("/events");
浏览器发送 HTTP 请求:
GET /events HTTP/1.1
Host: localhost:8080
Accept: text/event-stream
服务器不会立即结束响应。
而是返回:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
随后持续发送:
data: hello
data: world
data: test
连接一直保持:
HTTP Response
│
├── Event
├── Event
├── Event
├── Event
└── ...
SSE 的一个重要特点是:
EventSource 内置自动重连
例如网络突然断开:
Client ←X→ Server
浏览器的 EventSource 通常会自动尝试重新建立连接。
服务器还可以指定建议的重连时间:
retry: 3000
表示:
建议客户端大约 3 秒后重新连接
浏览器:
const eventSource = new EventSource("/events");
监听服务器消息:
eventSource.onmessage = function(event) {
console.log("收到消息:", event.data);
};
监听错误:
eventSource.onerror = function(event) {
console.error("SSE 连接异常:", event);
};
服务器需要返回:
Content-Type: text/event-stream
Cache-Control: no-cache
通常还可能看到:
Connection: keep-alive
但需要注意:
Connection 属于逐跳(hop-by-hop)HTTP 头,而且 HTTP/2 不使用这种方式表达连接持久化,因此它不是 SSE 协议成立的必要条件。
SSE 真正关键的是:
Content-Type: text/event-stream
SSE 使用:
text/event-stream
服务器发送的每个事件通常使用空行分隔。
最简单的数据:
data: hello
实际字节形式相当于:
data: hello\n\n
多个事件:
data: hello
data: world
data: SSE
最常用字段:
data: hello
例如 JSON:
data: {"username":"Tom","message":"hello"}
前端:
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log(data);
};
SSE 可以定义事件名称:
event: user-login
data: {"username":"Tom"}
前端监听:
eventSource.addEventListener("user-login", function(event) {
console.log(event.data);
});
因此服务器可以发送不同类型的事件:
event: login
data: Tom
event: logout
data: Jack
event: message
data: Hello
SSE 可以给事件指定 ID:
id: 1001
data: hello
浏览器重新连接时,可以通过:
Last-Event-ID
告诉服务器:
我最后收到的是哪个事件
服务器就有机会继续发送后续事件。
例如:
id: 1001
data: A
id: 1002
data: B
id: 1003
data: C
客户端收到:
1001
1002
随后断线。
重新连接时可能发送:
Last-Event-ID: 1002
服务器可以根据业务逻辑从:
1003
继续推送。
注意:
EventSource 提供 Last-Event-ID 机制,但服务端是否真正保存历史事件并实现“断点续传”,仍然需要开发者自己设计。
服务器可以发送:
retry: 3000
告诉客户端建议的重连等待时间。
单位:
毫秒
一个完整事件可以是:
id: 1001
event: message
retry: 3000
data: {"username":"Tom","message":"Hello"}
结构:
id
event
retry
data
空行
下面实现一个简单 SSE 服务:
GET /events
服务器每秒发送一次当前时间。
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
type Message struct {
Time string `json:"time"`
Message string `json:"message"`
}
func sseHandler(w http.ResponseWriter, r *http.Request) {
// SSE 必需/常用响应头
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("X-Accel-Buffering", "no")
// 确保当前 ResponseWriter 支持立即刷新数据
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
return
}
log.Println("SSE client connected")
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
messageID := 1
for {
select {
case <-r.Context().Done():
// 浏览器关闭连接、刷新页面或网络断开时,
// Request Context 会被取消。
log.Println("SSE client disconnected")
return
case t := <-ticker.C:
message := Message{
Time: t.Format(time.RFC3339),
Message: "Hello from Go SSE server",
}
data, err := json.Marshal(message)
if err != nil {
log.Println("JSON marshal error:", err)
continue
}
// SSE 数据格式
fmt.Fprintf(w, "id: %d\n", messageID)
fmt.Fprintln(w, "event: message")
fmt.Fprintf(w, "data: %s\n\n", data)
// 非常重要:
// 将缓冲区中的内容立即发送给客户端
flusher.Flush()
messageID++
}
}
}
func main() {
http.HandleFunc("/events", sseHandler)
log.Println("SSE server running:")
log.Println("http://localhost:8080/events")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
}
运行:
go run main.go
服务器:
http://localhost:8080
创建:
index.html
代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>SSE Demo</title>
</head>
<body>
<h1>SSE Demo</h1>
<button onclick="connect()">连接</button>
<button onclick="disconnect()">断开</button>
<pre id="output"></pre>
<script>
let eventSource = null;
function connect() {
if (eventSource) {
return;
}
eventSource = new EventSource("http://localhost:8080/events");
eventSource.onopen = function () {
console.log("SSE 已连接");
};
eventSource.addEventListener("message", function (event) {
console.log("Event ID:", event.lastEventId);
console.log("Data:", event.data);
const data = JSON.parse(event.data);
document.getElementById("output").textContent +=
data.time + " -> " + data.message + "\n";
});
eventSource.onerror = function (error) {
console.error("SSE error:", error);
// 不要在这里直接 close,
// 否则 EventSource 的自动重连能力会被关闭。
};
}
function disconnect() {
if (eventSource) {
eventSource.close();
eventSource = null;
console.log("SSE 已主动关闭");
}
}
</script>
</body>
</html>
Go HTTP Server 会进行缓冲。
假设服务器执行:
fmt.Fprintf(w, "data: hello\n\n")
数据不一定马上到达浏览器。
因此流式响应通常需要:
flusher.Flush()
整体过程:
Go Handler
│
│ Write()
↓
HTTP Buffer
│
│ Flush()
↓
TCP
│
↓
Browser
如果没有及时 Flush,就可能出现:
服务器已经生成很多数据
↓
数据仍然停留在缓冲区
↓
浏览器迟迟看不到
这对于:
AI Token Streaming
日志 Streaming
实时通知
尤其重要。
例如 AI 对话:
Browser
│
│ POST /chat
│ {"message":"解释TCP"}
↓
Go Server
│
│ 调用 AI
↓
LLM
│
│ Token
↓
Go Server
│
│ SSE
↓
Browser
浏览器看到:
TCP
TCP 是
TCP 是一种
TCP 是一种可靠的
TCP 是一种可靠的传输层
...
这就是典型的流式输出。
WebSocket 是一种建立在 TCP 连接之上的全双工通信协议。
通信方向:
Client ←────────────→ Server
双方都可以主动发送消息。
例如:
Client ─── Hello ─────────> Server
Client <── Hi ───────────── Server
Client <── New Message ──── Server
Client ─── Message ────────> Server
因此非常适合:
聊天室
在线游戏
协同编辑
实时控制
实时交易界面
经常有人说:
WebSocket 建立以后就从 HTTP 变成 TCP。
这种说法不够准确。
正确理解应该是:
TCP
│
├── HTTP
│
└── WebSocket
WebSocket 和 HTTP 都可以运行在 TCP 之上。
典型 WebSocket 建连过程是:
TCP Connection
↓
HTTP Upgrade Handshake
↓
101 Switching Protocols
↓
WebSocket Protocol
↓
WebSocket Frames
也就是说:
TCP 连接没有变,只是应用层协议从 HTTP Upgrade 握手切换成了 WebSocket。
加密连接则通常是:
TCP
↓
TLS
↓
HTTP Upgrade
↓
WebSocket
对应:
wss://
完整过程:
Client Server
| |
|------ TCP Connection -------------------->|
| |
|------ HTTP Upgrade Request -------------->|
| |
|<----- 101 Switching Protocols ------------|
| |
|========== WebSocket Connection ===========|
| |
|------ Text/Binary Frame ----------------->|
|<----- Text/Binary Frame ------------------|
|<----- Ping -------------------------------|
|------ Pong ------------------------------>|
| |
|------ Close ------------------------------>|
|<----- Close ------------------------------|
| |
X X
客户端:
const socket = new WebSocket("ws://localhost:8080/ws");
浏览器首先发送类似:
GET /ws HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: xxxxxxxxxxxxxxxxx
Sec-WebSocket-Version: 13
其中最重要的是:
Upgrade: websocket
Connection: Upgrade
表示:
客户端希望把当前连接升级为 WebSocket。
客户端会产生随机值:
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
服务器会把 Key 和 WebSocket 标准规定的 GUID:
258EAFA5-E914-47DA-95CA-C5AB0DC85B11
拼接:
Key + GUID
然后:
SHA-1
↓
Base64
最终生成:
Sec-WebSocket-Accept
例如:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
它主要用于验证服务器确实理解 WebSocket 握手,而不是普通 HTTP 服务误处理了这个请求。
它本身并不是用户身份认证或消息加密机制。
真正需要加密通信时应该使用:
wss://
即:
WebSocket over TLS
握手完成以后:
HTTP Header
不会跟在每一条业务消息前面。
数据通过:
WebSocket Frame
传输。
常见 Frame 类型包括:
Text
Binary
Ping
Pong
Close
Continuation
因此 WebSocket 不仅可以发送文本:
Hello
还可以发送二进制:
[]byte
图片数据
音频数据
自定义二进制协议
WebSocket 协议定义:
Ping
Pong
控制帧。
例如:
Server ---- Ping ----> Client
Server <--- Pong ----- Client
它可以帮助应用检测:
连接是否仍然存活
需要注意:
浏览器 WebSocket API 并没有向 JavaScript 暴露直接发送 Ping/Pong 控制帧的接口。
通常 Ping/Pong 由服务器端 WebSocket 库和浏览器协议栈处理。
有些应用也会额外设计业务层心跳:
{
"type": "ping"
}
服务器:
{
"type": "pong"
}
这种 JSON 心跳属于:
应用层心跳
与 WebSocket 协议自己的 Ping/Pong 控制帧不是同一个东西。
Go 标准库 net/http 本身没有提供完整的高级 WebSocket API,因此实际项目通常使用第三方库。
例如:
github.com/coder/websocket
初始化项目:
mkdir websocket-demo
cd websocket-demo
go mod init websocket-demo
安装:
go get github.com/coder/websocket
创建:
main.go
代码:
package main
import (
"context"
"log"
"net/http"
"time"
"github.com/coder/websocket"
"github.com/coder/websocket/wsjson"
)
type Message struct {
Type string `json:"type"`
Content string `json:"content"`
Time string `json:"time"`
}
func websocketHandler(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
// Demo 使用。
// 生产环境应严格检查 Origin。
InsecureSkipVerify: true,
})
if err != nil {
log.Println("WebSocket accept error:", err)
return
}
defer conn.Close(websocket.StatusNormalClosure, "connection closed")
log.Println("WebSocket client connected")
ctx := r.Context()
for {
var clientMessage Message
err := wsjson.Read(ctx, conn, &clientMessage)
if err != nil {
status := websocket.CloseStatus(err)
if status == websocket.StatusNormalClosure ||
status == websocket.StatusGoingAway {
log.Println("WebSocket client disconnected")
return
}
log.Println("WebSocket read error:", err)
return
}
log.Printf(
"收到客户端消息: type=%s content=%s",
clientMessage.Type,
clientMessage.Content,
)
response := Message{
Type: "response",
Content: "Server received: " + clientMessage.Content,
Time: time.Now().Format(time.RFC3339),
}
writeCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
err = wsjson.Write(writeCtx, conn, response)
cancel()
if err != nil {
log.Println("WebSocket write error:", err)
return
}
}
}
func main() {
http.HandleFunc("/ws", websocketHandler)
log.Println("WebSocket server running:")
log.Println("ws://localhost:8080/ws")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
}
创建:
index.html
代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebSocket Demo</title>
</head>
<body>
<h1>WebSocket Demo</h1>
<button onclick="connect()">连接</button>
<input
id="message"
type="text"
placeholder="请输入消息"
/>
<button onclick="sendMessage()">发送</button>
<button onclick="disconnect()">断开</button>
<pre id="output"></pre>
<script>
let socket = null;
function connect() {
if (socket &&
(socket.readyState === WebSocket.OPEN ||
socket.readyState === WebSocket.CONNECTING)) {
return;
}
socket = new WebSocket("ws://localhost:8080/ws");
socket.onopen = function () {
console.log("WebSocket 已连接");
document.getElementById("output").textContent +=
"WebSocket 已连接\n";
};
socket.onmessage = function (event) {
console.log("收到服务器消息:", event.data);
const data = JSON.parse(event.data);
document.getElementById("output").textContent +=
data.time + " -> " + data.content + "\n";
};
socket.onerror = function (error) {
console.error("WebSocket error:", error);
};
socket.onclose = function (event) {
console.log(
"WebSocket 已关闭",
event.code,
event.reason
);
document.getElementById("output").textContent +=
"WebSocket 已关闭\n";
};
}
function sendMessage() {
if (!socket || socket.readyState !== WebSocket.OPEN) {
console.log("WebSocket 尚未连接");
return;
}
const input =
document.getElementById("message");
const message = {
type: "message",
content: input.value,
time: new Date().toISOString()
};
socket.send(
JSON.stringify(message)
);
input.value = "";
}
function disconnect() {
if (socket) {
socket.close(
1000,
"Client closed connection"
);
}
}
</script>
</body>
</html>
客户端发送:
socket.send(JSON.stringify({
type: "message",
content: "Hello Server"
}));
数据流:
Browser
│
│ WebSocket Frame
│
│ {
│ "type":"message",
│ "content":"Hello Server"
│ }
↓
Go Server
Go:
wsjson.Read(ctx, conn, &clientMessage)
服务器处理以后:
wsjson.Write(ctx, conn, response)
返回:
Go Server
│
│ WebSocket Frame
↓
Browser
前端:
socket.onmessage = function(event) {
console.log(event.data);
};
最重要的区别不是:
谁更先进
而是:
通信模型不同
SSE:
HTTP Request
Client ----------------> Server
Event Stream
Client <================ Server
核心方向:
Server
↓
Client
WebSocket:
Client <===============> Server
核心方向:
Client
↕
Server
| 特性 | SSE | WebSocket |
|---|---|---|
| 全称 | Server-Sent Events | WebSocket |
| 通信方向 | Server → Client | Client ↔ Server |
| 通信模型 | 单向 | 全双工 |
| 建连方式 | HTTP 请求 | HTTP Upgrade 握手 |
| 应用层协议 | HTTP Event Stream | WebSocket |
| 浏览器 API | EventSource | WebSocket |
| 自动重连 | 浏览器原生支持 | 通常需要应用自己实现 |
| 文本 | 支持 | 支持 |
| 二进制 | 不直接支持 | 原生支持 |
| JSON | 支持 | 支持 |
| Ping/Pong | 无 WebSocket 式协议控制帧 | 协议原生支持 |
| 消息 ID | SSE 原生支持 id: |
应用层自己设计 |
| 断点恢复 | 可利用 Last-Event-ID | 应用层自己设计 |
| HTTP/HTTPS 基础设施兼容 | 较好 | 通常需要代理显式支持 Upgrade/WS |
| 实现复杂度 | 较低 | 相对较高 |
| AI 流式输出 | 非常适合 | 可以 |
| 实时通知 | 非常适合 | 可以 |
| 实时日志 | 非常适合 | 可以 |
| 聊天系统 | 可以,但客户端发送仍需 HTTP | 非常适合 |
| 在线游戏 | 不适合主要双向链路 | 非常适合 |
| 协同编辑 | 通常不是首选 | 非常适合 |
如果业务基本是:
服务器不断产生数据
↓
浏览器不断接收
优先考虑 SSE。
例如:
AI 流式输出
实时日志
服务器状态
任务执行进度
消息通知
实时监控
新闻/行情推送
典型架构:
POST /chat
Browser -------------> Go Server
│
↓
AI
│
SSE Stream │
Browser <==================
客户端发送请求:
POST /chat
服务器持续返回:
data: 你
data: 你好
data: 你好,
data: 你好,我
data: 你好,我是
如果双方都需要频繁主动发送数据:
Client → Server
Server → Client
优先考虑 WebSocket。
例如:
聊天室
在线游戏
协同编辑
实时白板
远程控制
多人房间
实时交易终端
例如聊天:
WebSocket Server
┌───────┐
│ Go │
└───┬───┘
│
┌────────────┼────────────┐
│ │ │
↓ ↓ ↓
Client A Client B Client C
A:
大家好
发送给服务器:
A → Server
服务器广播:
Server → A
Server → B
Server → C
这就是典型的 WebSocket 聊天室模型。
可以从协议栈理解。
使用 HTTP 时:
┌─────────────────────┐
│ SSE / Event Stream │
├─────────────────────┤
│ HTTP │
├─────────────────────┤
│ TCP │
├─────────────────────┤
│ IP │
└─────────────────────┘
HTTPS:
SSE
↓
HTTP
↓
TLS
↓
TCP
↓
IP
经典 HTTP/1.1 Upgrade 场景:
WebSocket
↓
TCP
↓
IP
加密:
WebSocket
↓
TLS
↓
TCP
↓
IP
但连接最开始通常经历:
HTTP Upgrade
因此生命周期可以理解为:
TCP
│
├─ HTTP Handshake
│
└─ WebSocket Frames
上面的代码主要用于理解通信机制。
真正生产环境还需要处理:
身份认证
Origin 校验
TLS
连接数量限制
超时
心跳
重连
消息大小限制
慢客户端
背压
限流
日志
监控
异常连接清理
负载均衡
反向代理配置
特别是 WebSocket:
一个客户端
=
一个长期连接
如果:
100000 用户在线
服务器就可能需要维护:
100000 个长连接
因此需要重点考虑:
连接管理
文件描述符限制
内存消耗
goroutine 数量
负载均衡
连接迁移
广播机制
SSE 最常见的问题之一不是 Go 代码,而是:
代理缓冲
例如:
Go
↓
Nginx
↓
Browser
如果 Nginx 缓冲响应:
Go 一直 Write + Flush
↓
Nginx Buffer
↓
↓
↓
积累到一定程度
↓
Browser 一次收到大量数据
这样就失去了实时流式效果。
Go 示例中:
w.Header().Set("X-Accel-Buffering", "no")
可以帮助 Nginx 场景关闭当前响应的代理缓冲。
也可以根据实际部署配置 Nginx。
示例为了方便本地学习使用:
websocket.Accept(w, r, &websocket.AcceptOptions{
InsecureSkipVerify: true,
})
生产环境不建议这样做。
应该验证:
Origin
Host
用户身份
Token / Session
否则可能产生跨站 WebSocket 劫持等安全问题。
可以把三种方式理解为:
Client ---- Request ----> Server
Client <--- Response ---- Server
结束
特点:
一问一答
Client ---- Request ----> Server
Client <--- Event ------- Server
Client <--- Event ------- Server
Client <--- Event ------- Server
Client <--- Event ------- Server
...
特点:
客户端建立 HTTP 请求
服务器持续输出 Response
核心:
Server → Client
Client ---- Handshake ---> Server
Client <--- 101 ---------- Server
Client ===== Message ====> Server
Client <==== Message ===== Server
Client ===== Message ====> Server
Client <==== Message ===== Server
特点:
建立一次连接
双方长期双向通信
核心:
Client ↔ Server
可以直接记住:
HTTP:
客户端问一次,服务器答一次。
SSE:
客户端建立一条 HTTP 流,
服务器可以沿着这条流不断往客户端推数据。
WebSocket:
先通过握手建立 WebSocket 连接,
之后客户端和服务器都可以随时给对方发送消息。
技术选型可以进一步简化成:
只需要服务器持续推送
↓
SSE
需要客户端和服务器高频双向通信
↓
WebSocket
典型场景:
AI 流式输出 → SSE
任务进度 → SSE
实时日志 → SSE
服务器通知 → SSE
聊天室 → WebSocket
在线游戏 → WebSocket
协同编辑 → WebSocket
实时双向控制 → WebSocket
最终理解这两种技术时,最重要的不是死记 API,而是记住通信模型:
SSE
Client
↑
│
Server
WebSocket
Client
↕
Server
一旦理解了这个区别,SSE 和 WebSocket 的绝大多数设计选择都会变得非常清楚。