跳至主要內容

WebSocket 协议

wzCoding大约 6 分钟Httpwebsocket

WebSocket 协议

引言 如果你开发过聊天室、实时股票看板、或者多人在线协作文档,你一定会遇到一个极其头疼的问题:如果后端的数据变了,前端怎么才能第一时间知道?

传统的 HTTP 协议像一个高傲的冰山:只有客户端(浏览器)主动发请求,服务端才会回话。服务端永远无法主动联系客户端。

为了解决这个“单向通信”的痛点,前端工程师们曾经绞尽脑汁,发明了各种奇技淫巧。直到 WebSocket 的横空出世,才彻底解放了实时 Web 应用的生产力。

今天,我们就来详细拆解 WebSocket 的前世今生、底层握手原理,以及如何在前端优雅地使用它。


一、 历史的挣扎:在 WebSocket 诞生之前

在没有 WebSocket 的漫长岁月里,为了实现“实时更新”,前端主要依靠两种非常蹩脚的方案:

1. 轮询 (Short Polling) —— “好了没?好了没?”

  • 原理:前端写个 setInterval 定时器,每隔 1 秒向后端发一个 HTTP 请求:“有新消息吗?”后端查一下数据库,回一句:“没有。”
  • 痛点:极其浪费资源!不仅白白消耗了大量的 HTTP 握手开销和网络带宽,而且实时性很差(最多有 1 秒的延迟)。大部分时间,服务器都在徒劳地回答“没有”。

2. 长轮询 (Long Polling) —— “没有我就死等!”

  • 原理:前端发一个 HTTP 请求给后端。如果此时没有新消息,后端不立刻返回,而是把请求挂起(Hold 住)。直到真的有新消息了(或者等了 30 秒超时了),后端才把消息返回给前端。前端收到后,立刻再发起下一个长轮询。
  • 痛点:虽然减少了无效请求的数量,但每次建立连接依然要消耗完整的 HTTP 首部(Header),且服务器需要维持大量挂起的并发连接,极易导致内存爆炸。

时代呼唤英雄: 无论是短轮询还是长轮询,本质上依然是 HTTP 的“一问一答”模式。我们需要一个真正全双工(Full-Duplex)、低延迟、客户端和服务端平等交流的协议。 于是,在 2011 年,WebSocket (ws) 被正式标准化。


二、 WebSocket 的真面目:披着 HTTP 外衣的 TCP

很多人对 WebSocket 有个误区,以为它是 HTTP 的升级版。

错!WebSocket 是一个完全独立的、和 HTTP 同级的应用层协议。 只不过,为了能顺利穿透各种防火墙(因为大家都默认放行 80 和 443 端口),WebSocket 巧妙地借用了 HTTP 的通道来完成初次的“握手”,然后就光速“变身”了。

1. 经典的“协议升级”握手过程

当你在前端 new WebSocket('ws://example.com/chat') 时,底层到底发生了什么?

第一步:浏览器发送一个特殊的 HTTP GET 请求 它在请求头(Headers)里塞入了两把关键的钥匙:

GET /chat HTTP/1.1
Host: example.com
Connection: Upgrade              <-- 告诉服务器:我想改变通信协议!
Upgrade: websocket               <-- 我想升级成 websocket 协议!
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== <-- 浏览器生成的一串随机 Base64 密码
Sec-WebSocket-Version: 13

第二步:服务器同意升级,返回 101 状态码 如果服务器支持 WebSocket,它会用一套固定的加密算法处理那串密码,并返回:

HTTP/1.1 101 Switching Protocols <-- 101 状态码登场!意思是:同意切换协议。
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= <-- 服务器算出来的密码回执

第三步:华丽变身,彻底抛弃 HTTP 当浏览器收到 101 状态码,并验证密码回执无误后,之前的 HTTP 协议就功成身退了。 原本的那条 TCP 连接被保留了下来,变成了 WebSocket 的专属数据通道。从此以后,双方发送的数据不再带有冗长的 HTTP Header,而是变成了极其轻量级的二进制数据帧(Frame)

2. WebSocket 的核心优势

  1. 真正的全双工通信:服务端有数据了,随时可以“推”给前端;前端有操作了,随时可以“发”给服务端。没有谁先谁后的规定。
  2. 极低的开销:建立连接后,数据包头部非常小(只有 2~10 字节),比起 HTTP 动辄几百字节的 Cookie 和 Header,节省了海量带宽。
  3. 保持连接状态(有状态):HTTP 是无状态的,每次都要带 Token;而 WebSocket 连接一旦建立,服务端就一直“认识”你,不需要重复鉴权。

三、 使用场景:什么时候该掏出 WebSocket?

并不是所有项目都要用 WebSocket!如果你只是展示一篇新闻,用普通的 HTTP 请求完全足够了。

只有当你遇到以下“高频+低延迟”的需求时,才是 WebSocket 大显身手的舞台:

  1. 即时通讯(IM):网页版微信、客服聊天系统。
  2. 实时数据大屏:股票/币圈 K 线图实时跳动、双十一实时交易额大屏。
  3. 多人在线协作:类似腾讯文档、Figma,你打字的瞬间,别人立刻能看到。
  4. 实时竞技游戏:网页版的多人对战小游戏。

四、 前端实战:WebSocket 原生 API 指北

在现代浏览器中,WebSocket 的原生 API 非常简单优雅,主要由四个事件两个方法组成。

1. 建立连接

// 注意协议头变成了 ws:// (或者加密的 wss:// 类似于 https)
const ws = new WebSocket('wss://api.example.com/chat');

2. 监听四个核心事件 (onopen, onmessage, onerror, onclose)

// 1. 连接成功建立时触发 (握手完成,101 返回后)
ws.onopen = function(event) {
    console.log('✅ WebSocket 连接成功!');
    // 连接成功后,立刻给服务器发个打招呼消息
    ws.send('Hello Server, I am Client!'); 
};

// 2. 🌟 收到服务器主动推过来的消息时触发 (最核心的业务逻辑)
ws.onmessage = function(event) {
    // event.data 就是服务器发来的内容,通常是 JSON 字符串
    console.log('📩 收到服务器消息:', event.data);
    
    // 解析并渲染到页面上
    const msgObj = JSON.parse(event.data);
    renderChatList(msgObj);
};

// 3. 连接发生错误时触发
ws.onerror = function(event) {
    console.error('❌ WebSocket 发生错误:', event);
};

// 4. 连接被关闭时触发 (无论是前端主动关,还是后端断开)
ws.onclose = function(event) {
    console.log('🔌 WebSocket 连接已关闭。代码:', event.code, '原因:', event.reason);
    
    // 实战技巧:可以在这里写一段逻辑,3秒后自动尝试重连 (断线重连机制)
};

3. 主动发送与关闭连接

// 前端主动给服务器发消息 (必须在 onopen 触发后才能发)
function sendMessage() {
    if (ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({ type: 'chat', content: '这是一条新消息' }));
    } else {
        console.warn('连接未准备好,无法发送');
    }
}

// 离开页面或退出登录时,记得礼貌地断开连接,释放服务器资源
function disconnect() {
    ws.close(1000, '用户主动退出'); // 1000 是正常的关闭状态码
}

结语 WebSocket 的出现,完美填补了 HTTP 协议在实时通信领域的空白。它那套“借用 HTTP 握手,随后暗度陈仓升级协议”的设计理念,堪称网络协议演进史上的经典之作。

在实际的企业级开发中,虽然原生 API 已经够用,但为了解决断线重连、心跳保活、房间广播等复杂场景,我们通常会使用成熟的第三方库,比如大名鼎鼎的 Socket.IO(它不仅封装了 WebSocket,还能在不支持的老旧浏览器上自动降级为长轮询)。