WebSocket
和HTTP长连接的对比
协议层面
- HTTP 长连接(Keep-Alive):
- 仍然是 HTTP 协议(通常是 HTTP/1.1)。
- 客户端和服务器之间的 TCP 连接可以被复用发送多个 HTTP 请求/响应,但每次仍然是“请求-响应”模型。
- 每个请求/响应之间是独立的,不能实时推送数据。
- WebSocket:
- 是一种独立的协议,从 HTTP 升级(Upgrade)而来(通过 HTTP 发起握手,但之后切换为 WebSocket 协议)。
- 建立后,连接变成全双工通信,支持服务器主动推送数据给客户端。
- 通信是基于帧的格式,比 HTTP 的开销小。
通信模式
- HTTP 长连接:
- 客户端发送请求,服务器响应,仍是单向请求响应模型。
- 想要获得新数据,客户端需要轮询(定时发送请求)。
- WebSocket:
- 双向通信,一旦连接建立,客户端和服务器可以随时互相发送数据。
- 实现实时性需求(如聊天、游戏、股票推送)更高效。
性能与开销
- HTTP 长连接:
- 每次请求仍需要携带完整的 HTTP 头部信息,有较高的冗余。
- 适合请求频率低、实时性要求不高的场景。
- WebSocket:
- 建立后帧格式非常小,通信效率更高。
- 更适合频繁、实时通信的场景。
为什么需要Upgrade
目前已经存在一整套庞大的 HTTP 和 HTTPS 基础设施(包括代理、防火墙、缓存及其他中间组件)。为了提高被广泛采用的可能性,WebSocket 协议被设计成可以在现有基础上进行调整和扩展,而不必为了支持一种新协议而从头构建一个专用端口的全新体系。
同样重要的是,即使 WebSocket 协议去掉了与 HTTP 兼容的握手,它仍然需要一个几乎同样复杂的握手过程,以满足现代 Web 的安全需求 —— 比如确保浏览器和服务器能够验证彼此的身份,以及安全地支持 CORS(跨域资源共享)。即便是“原始”的 Flash Socket,在建立实际连接之前,也会先通过向服务器发送安全策略请求来完成握手过程。
握手过程
- 客户端发起一个HTTP GET 请求,带上Upgrade 头部请求升级协议。
- 服务端识别请求,返回 101 Switching Protocols 响应,表示接受升级。
- 双方随后使用 WebSocket 协议通信(在同一个 TCP 连接上)。
客户端请求示例
GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13
Origin: http://example.com
Header 名称 | 说明 |
---|---|
Upgrade: websocket | 请求升级到 WebSocket 协议 |
Connection: Upgrade | 告诉服务器连接应该升级 |
Sec-WebSocket-Key | 客户端随机生成的 Base64 编码字符串,用于服务器响应时做签名(防伪) |
Sec-WebSocket-Version | WebSocket 协议版本,当前为 13(标准) |
Origin(可选) | 表示请求来源,用于安全校验,防止跨站攻击(CSRF) |
服务端响应示例
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Header 名称 | 说明 |
---|---|
101 Switching Protocols | 状态码:协议切换成功(来自 RFC 7231) |
Upgrade: websocket | 确认已升级协议 |
Connection: Upgrade | 表示这是一个协议升级响应 |
Sec-WebSocket-Accept | 服务端对客户端 Sec-WebSocket-Key 做哈希 + Base64 后返回,防止伪造连接 |
Sec-WebSocket-Key
服务器必须向客户端证明它收到了客户端的 WebSocket 握手,以防服务器接受那些并非 WebSocket 的连接。这可以防止攻击者使用 XMLHttpRequest 或表单提交等方式伪装请求 WebSocket 服务器,从而欺骗服务器。
服务器会从客户端请求中的 Sec-WebSocket-Key 中获取值(即 base64 编码的字符串,去除前后空白),然后将其与一个固定的 GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 拼接,并进行哈希处理。
这个 GUID 是一个全球唯一标识符,选择它是因为它极不可能被那些不了解 WebSocket 协议的网络端点使用,因此起到了标识“你懂 WebSocket”的作用。
还有一层作用是防止浏览器端通过 XMLHttpRequest.setRequestHeader 伪造 WebSocket 请求,因为 WebSocket 相关头(如 Sec-WebSocket-Key、Upgrade)是被浏览器安全策略禁止手动设置的。
简单测试
使用Go作为后端,需要先安装依赖 go get [github.com/gorilla/websocket](http://github.com/gorilla/websocket)
:
package main
import (
"fmt"
"math/rand"
"net/http"
"time"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func serverPing(conn *websocket.Conn) {
go func() {
for {
err := conn.WriteMessage(websocket.PingMessage, []byte("ping"))
if err != nil {
fmt.Println("Ping failed:", err)
return
}
fmt.Println("Ping sent")
time.Sleep(10 * time.Second)
}
}()
}
func getSessionID(r *http.Request) string {
sessionID := r.URL.Query().Get("session_id")
if sessionID == "" {
rand.Seed(time.Now().UnixNano())
sessionID = fmt.Sprintf("sess-%d", rand.Intn(1000000))
}
return sessionID
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
fmt.Println("Upgrade failed:", err)
return
}
defer conn.Close()
fmt.Println("Connected to client")
sessionID := getSessionID(r)
fmt.Printf("Session ID: %s\n", sessionID)
serverPing(conn)
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
fmt.Println("Read failed:", err)
break
}
fmt.Printf("Received: %s\n", message)
err = conn.WriteMessage(messageType, []byte("Echo: "+string(message)))
if err != nil {
fmt.Println("Send failed:", err)
break
}
}
}
func main() {
http.HandleFunc("/", handleWebSocket)
fmt.Println("WebSocket server started on ws://localhost:3000")
http.ListenAndServe(":3000", nil)
}
然后简单的前端页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket Tester</title>
<style>
body { font-family: sans-serif; margin: 2rem; }
#log { border: 1px solid #ccc; padding: 10px; height: 200px; overflow-y: scroll; background: #f9f9f9; }
input, button { padding: 5px; font-size: 16px; }
</style>
</head>
<body>
<h2>WebSocket Client → ws://localhost:3000</h2>
<input type="text" id="messageInput" placeholder="Enter message" />
<button onclick="sendMessage()">Send</button>
<h3>Log</h3>
<div id="log"></div>
<script>
const log = (msg) => {
const el = document.createElement('div');
el.textContent = msg;
document.getElementById('log').appendChild(el);
};
function getSessionID() {
return localStorage.getItem("session_id");
}
function setSessionID(id) {
localStorage.setItem("session_id", id);
}
let socket;
function connect() {
const sessionID = getSessionID();
const wsURL = sessionID
? `ws://localhost:3000/?session_id=${sessionID}`
: "ws://localhost:3000/";
socket = new WebSocket(wsURL);
socket.onopen = () => log("✅ Connected to WebSocket server");
socket.onmessage = (event) => {
const msg = event.data;
if (msg.startsWith("New Session ID:")) {
const newID = msg.split(":")[1].trim();
setSessionID(newID);
log("💾 Stored Session ID: " + newID);
}
log("📨 Received: " + msg);
};
socket.onerror = (e) => log("❌ Error: " + (e.message || "Unknown error"));
socket.onclose = () => {
log("🔌 Connection closed, retrying in 3s...");
setTimeout(connect, 3000);
};
}
connect();
function sendMessage() {
const input = document.getElementById("messageInput");
const msg = input.value.trim();
if (!msg) {
log("⚠️ Please enter a message before sending.");
return;
}
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(msg);
log("📤 Sent: " + msg);
} else {
log("⚠️ Cannot send, socket not open");
}
input.value = "";
}
</script>
</body>
</html>
这里为了测试nginx,观察proxy超时的影响,需要加上以下参数:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_read_timeout 10s;
注意 proxy_read_timeout 是 Nginx 等待“后端服务器”发送数据给客户端的最大时间。
测试结果表现proxy的timeout设置会影响连接的断开,适当的PING和超时设置都很重要。
商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,您可以自由地在任何媒体以任何形式复制和分发作品,也可以修改和创作,但是分发衍生作品时必须采用相同的许可协议。