WebSocket 快速入门:从"寄信"到"打电话"

先问一个问题:你有没有想过,为什么网页聊天、股票行情、在线协作这些功能,以前做起来那么费劲?

答案藏在 HTTP 的规矩里。

HTTP 是一条单行道

HTTP 的工作方式特别像寄信:你寄一封信(请求),对方回一封信(响应)。一封信对应一封信,有来有往,规矩分明。

这个模型在"你问,我答"的场景里完美——打开网页、提交表单、加载图片,都是你主动开口,服务器被动回答。

但问题来了:服务器想主动找你说话怎么办?

比如聊天软件,别人给你发了条消息,服务器得立刻告诉你。可 HTTP 的规矩是"你不寄信,我就不回信"。服务器不知道你什么时候在线,也没法主动开口。

早期解决方案很笨:轮询。前端每隔一秒发一个"有没有新消息?"的请求,服务器每次都回"没有"或者"有一条"。这就像每隔几秒给对方打个电话问"你说话了吗?"——浪费电话费不说,消息还有延迟。

聪明的你应该已经感觉到了:这里需要的不是"更频繁地问",而是一条真正保持连接的电话线

电话线:WebSocket 的模型

WebSocket 就是那条电话线。

  • HTTP 是寄信:每次都要写地址、贴邮票、等回信。
  • WebSocket 是打电话:拨通一次,之后你说你的,我说我的,随时都能说话,不用重新拨号。

专业说法叫"全双工"——双方可以同时说,也可以同时听。你要做的只是拨通一次,之后所有消息都在同一条线路上传。

这就是 WebSocket 的全部核心。剩下的,都是这条电话线的工程细节。

重建它:这条电话线是怎么搭起来的

现在从基本事实出发,看看"电话线"到底怎么搭。不需要背协议规范,只需要想清楚一个问题:浏览器和服务器之间只有 HTTP 这么一种说话方式,怎么让它变成一条持续的线路?

答案:利用 HTTP 本身,但只利用它一次——握手

浏览器发一个特殊的 HTTP 请求,说:"我想升级成 WebSocket。"服务器回一句:"同意。"就这么两句话,线路就建立了。之后两边都不再走 HTTP 那一问一答的规矩,而是用 WebSocket 的帧(frame)直接传数据。

握手长这样(这些头是浏览器替你写的):

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13

注意中间两行:Upgrade: websocketConnection: Upgrade。这就是在说:"我要换一种通信方式。" 服务器回 101 Switching Protocols,意思就是"换好了"。

之后传数据用的"帧",你可以想成电话里的每一句话。浏览器把你要发的字符串自动打包成帧,收到帧自动解包——这些细节你根本不用碰。

所以,WebSocket 不是凭空出现的新协议,它是在 HTTP 握手的基础上"升级"出来的。知道它怎么来的,比记住一堆状态码有用得多。

浏览器里怎么用:极简代码

原生 API 就四个事件,没有魔法。

// 拨号(建立连接)
const ws = new WebSocket('ws://localhost:3000/chat')

// 电话接通了
ws.onopen = () => {
  ws.send('大家好')   // 说话
}

// 对方说话了
ws.onmessage = (event) => {
  console.log('收到:', event.data)
}

// 线路断了(无论什么原因)
ws.onclose = () => {
  console.log('连接关闭')
}

// 出错
ws.onerror = (err) => {
  console.log('出错了', err)
}

把这四行事件处理函数写对,你就能和服务器聊起来。

有个细节值得注意:send() 是主动发,但 onmessage 是被动的——你不知道对方什么时候说话,所以只能"听着"。这跟 HTTP 里"发请求→等响应"的写法完全不同,很多人第一次就卡在这:想不通消息是从哪冒出来的。

想通它很简单:因为线路一直是通的,对方随时可能说话,浏览器只能把"对方说话了"当成一个事件告诉你。你没法预测它,只能监听它。

连接的一生

一条 WebSocket 连接会经历四个阶段,对应你代码里的四个事件:

阶段 事件 你该做什么
建立 onopen 发送初始消息
通信 onmessage 处理收到的数据
出错 onerror 记录日志,准备重连
关闭 onclose 清理资源,决定是否重连

注意 onerroronclose 常常先后发生——出错之后连接就断了,所以别写重复的重连逻辑。

三个绕不开的工程问题

懂了上面的模型,你已经掌握了 WebSocket 的本质。但现实世界的电话线会出各种问题,初级前端迟早会撞上这三个:

1. 心跳:怎么知道线还通着?

TCP 连接断开时,浏览器不一定能立刻感知——比如断网、服务器重启。你以为线还通着,其实对面已经没人了。

解决方法是"心跳":前端每隔一段时间发一个约定的消息(比如 ping),服务器回 pong。如果一段时间没收到回应,就认为连接死了,主动重连。

这就像打电话时每隔几分钟问一句"喂,还在吗?"——信号不好时,靠这个确认对方还听着。

有个细节要分清:WebSocket 协议底层确实有 ping/pong 控制帧,但浏览器的 JS API 不让你手动发协议级 ping(浏览器收到会自动回 pong)。所以前端代码里的心跳,通常是应用层自己约定的消息格式——你和服务器说好"发 ping 算心跳,回 pong 算活着"就行。

2. 断线重连:掉线了怎么办?

现实中连接总会断(网络波动、服务器重启、手机切网)。所以生产环境里几乎都要写重连逻辑:检测到 onclose,过几秒重新 new WebSocket(...),最好加上退避策略(越重连间隔越长)。

function connect() {
  const ws = new WebSocket('ws://example.com/socket')

  ws.onclose = () => {
    console.log('断了,3 秒后重连')
    setTimeout(connect, 3000)  // 简化版:固定 3 秒
  }
}

connect()

注意:WebSocket 没有内置自动重连。 社区库(如 reconnecting-websocket)本质就是把上面这段逻辑做成了工程化版本。

3. 二进制数据:不只是文本

send() 不仅能发字符串,还能发 ArrayBufferBlob——图片、音频、文件都能走这条线路。接收时记得判断类型:

const ws = new WebSocket('ws://example.com/socket')
ws.binaryType = 'arraybuffer'  // 收到二进制时以 ArrayBuffer 形式给你

ws.onmessage = (event) => {
  if (event.data instanceof ArrayBuffer) {
    // 处理二进制数据
  } else {
    // 处理文本
  }
}

你可能会困惑的地方

作为初学者,有三个问题几乎一定会困惑,提前说破:

困惑一:"为什么不用 HTTP 轮询?" —— 因为轮询是"假装实时":消息最快也有一个轮询周期的延迟,而且服务器压力大。WebSocket 是"真实时":消息一到立刻推给你。

困惑二:"WebSocket 和 HTTP 是什么关系?" —— 不是替代关系,是升级关系。握手走的是 HTTP,握手成功后换协议。所以 WebSocket 必须基于支持升级的 HTTP 服务(或网关)才能建立。

困惑三:"和 SSE 有什么区别?" —— SSE(Server-Sent Events)是服务器单向推送(服务器→浏览器),基于 HTTP,简单但只能单向。WebSocket 是双向的。做"服务器推消息、浏览器被动接收"的场景(如行情、通知),SSE 够用;要做聊天、协作这种双向交互,才需要 WebSocket。

延伸:AI 流式输出,用 SSE 还是 WebSocket?

现在最火的应用——让 AI 的回答像打字机一样一个字一个字蹦出来——很多前端会在选型时纠结:这该用 WebSocket,还是 SSE?

别纠结,用刚才的思路拆一下就清楚了。先问一个问题:这个场景里,数据往哪个方向流?

你发一条消息,然后 AI 开始输出。输出期间你基本不说话,只是看着。数据方向是单向的:服务器 → 浏览器。

单向推送,就是 SSE 的主场。

SSE 从名字就能看出来——Server-Sent Events,服务器主动发事件。它基于 HTTP,浏览器原生支持 EventSource,而且断线自动重连(这点 WebSocket 反而没有,要自己写):

const es = new EventSource('/api/chat')   // 打开水龙头

es.onmessage = (event) => {
  // AI 每吐出一段,这里就触发一次
  console.log('新内容:', event.data)
}

打开水龙头,水自己流出来,你只管接——这个类比就是流式输出的全部。为单向放水专门拨一条电话线(WebSocket),是杀鸡用牛刀:握手、心跳、断线重连,复杂度全落在你这边。

这不是纸上谈兵——现在主流大模型接口的流式输出,几乎清一色走 SSE 格式(text/event-stream)。

那什么时候才轮到 WebSocket? 还是回到那个问题:数据是不是双向的。

场景 数据方向 选型
AI 流式输出(打字机效果) 单向:服务器→浏览器 SSE
行情、进度、通知推送 单向 SSE
AI 边说话你边打断、插话 双向 WebSocket
聊天室、在线协作 双向 WebSocket

两个实战细节值得记住:

  • EventSource 只能发 GET、不能带自定义请求头。如果鉴权 token 必须放请求头里,就改用 fetch 读取响应流——SSE 只是一种数据格式,用 fetch 照样能解析,还能精细控制"中途停止生成"。
  • 既要流式又要双向的场景(AI 边输出你边插话),可以 SSE 打底、另用普通 HTTP 请求补发指令,不一定要升级到 WebSocket。

判断标准就一句话:数据单向,用 SSE;数据双向,才上 WebSocket。

动手:本地搭一个 echo 服务

想练手,最稳的路是在本地起一个自己的 echo 服务,顺便把服务器端也看明白了:

// server.js —— 先 npm install ws
const { WebSocketServer } = require('ws')
const wss = new WebSocketServer({ port: 3000 })

wss.on('connection', (ws) => {
  ws.on('message', (data) => ws.send(data.toString()))  // 收到什么,回什么
})
console.log('echo 服务跑在 ws://localhost:3000')
npm install ws && node server.js

然后打开浏览器控制台,连它:

const ws = new WebSocket('ws://localhost:3000')
ws.onopen = () => { console.log('已连接'); ws.send('你好') }
ws.onmessage = e => console.log('收到:', e.data)

localhost 不经过任何外网,连接必成。你发"你好",它回"你好"。

一句话总结

HTTP 是寄信,WebSocket 是打电话。拨通一次,双向随时说。

把上面的本地 echo 服务跑起来,四行事件处理函数敲一遍,发条消息看它回来。自己动手连一次,比读十篇文章都有用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容