WebSocket demo拆解:从握手原理到工程实践全攻略

发布时间:2026/10/15 10:25:22
WebSocket demo拆解:从握手原理到工程实践全攻略 简介WebSocket 是依托 TCP 实现浏览器与服务器全双工通信的协议这份资源即围绕该协议整理的一份可运行 Demo。压缩包共 19 个文件大小 41KB包含 Java 源码、JSP 页面、XML 配置、jar 依赖及 readme 说明等较适合刚接触 WebSocket、希望了解握手与消息帧机制或想快速搭建双向通信示例的初学者对照学习。包内工程结构完整既有 .project/.classpath 等 Eclipse 工程配置也有 pom.xml 与 websocket-api.jar便于导入后直接运行直观观察服务器主动推送消息的效果readme 文件还可用作上手指引。目前已有 391 人学习/下载可作为理解浏览器与服务器实时交互、后续扩展即时通信或消息推送功能的基础参考。1. 从一次实时的数据推送说起WebSocket demo到底是什么做后台管理系统的时候最难受的一件事就是页面上的数据要实时刷新。用轮询去拉接口一秒一次太浪费五秒一次又不够快。后来我翻出一个巴掌大的websocket demo前后端加起来不到一百行代码却把实时推送讲得明明白白。它不是一个产品而是一个最小可运行的示例包——前端页面、服务端、连接、收发消息、断开重连全都有。适合刚接触WebSocket的开发者照着跑一遍也适合老手需要写一个原型时快速改改。这篇文章就把这个demo拆开讲清楚它怎么跑起来、参数怎么调、坑在哪里。2. 原理先行WebSocket为什么快以及它的握手和帧格式2.1 轮询、长轮询与WebSocket三种实时方案的取舍在WebSocket普及之前网页端做“实时”更新基本靠三种方式。第一种是普通轮询定时器每隔几秒发一次请求服务端把最新数据返回。缺点是请求里大部分是空数据HTTP头开销大对服务端压力也大。第二种是长轮询客户端发一个请求服务端挂住连接直到有新数据才返回然后客户端马上再发起一个请求。这种方式比普通轮询好一些但每次还是要经历完整的HTTP请求响应而且服务端需要额外的机制来挂起请求并发高了容易把连接数打满。第三种是今天要说的WebSocket它一次握手后建立长连接服务端可以随时主动往客户端推数据不需要重复建连也不再有HTTP头每帧跟着跑。这个demo里最明显能感受到的区别是普通HTTP请求响应服务端发完响应连接就关了WebSocket建立后连接一直在服务端可以连续发好几条消息客户端也一直在收。如果你要做的场景是聊天、实时看板、协同编辑、在线状态更新这类高频率、服务端主动通知的交互用WebSocket合理得多。如果只是定时拉一次排行榜轮询也不是不能用。另外要提醒一点WebSocket是应用层协议底层基于TCP它依赖HTTP完成初次握手但握手之后双方直接走WebSocket协议帧不再有HTTP头。所以别把它理解成“HTTP上的一个通道”更准确的说法是“通过HTTP Upgrade切换协议的独立长连接”。2.2 从HTTP Upgrade到WebSocket帧一次握手中的协议切换WebSocket启动靠的是一次特殊的HTTP请求。客户端发给服务端的握手请求会有Upgrade: websocket和Connection: Upgrade这两个头还会带上Sec-WebSocket-Key——一个Base64编码的随机值。服务端收到后会把这段字符串拼接一个固定GUID然后做SHA-1哈希再用Base64编码作为Sec-WebSocket-Accept返回。这个验证过程是RFC 6455定的必须这样算不能随便换算法。这个demo里有个专门的模块处理握手如果你用Node.js的ws库这部分被封装好了但如果自己手写协议这里就是第一个容易翻车的地方。我曾经看过一个简化实现只返回了101状态码却没有算Sec-WebSocket-Accept结果浏览器直接报Error during WebSocket handshake。原因就是魔术字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11写错了或者根本没有去算。握手成功后连接状态变成OPEN之后传输的数据就不再是HTTP格式而是WebSocket数据帧。每一帧有固定的结构FIN位表示这是否是最后一帧opcode表示帧类型1是文本2是二进制8是关闭9是ping10是pong然后是MASK位和长度字段。客户端发往服务端的帧必须加掩码服务端发往客户端的帧不需要掩码这是协议规定的。初学者可能不理解掩码的意义简单地认为它是对称加密其实它主要为了防缓存污染。这里不展开但写代码时要知道用现成库时库会帮你处理自己拆帧时容易漏掉掩码这一步。2.3 消息格式文本帧、二进制帧与掩码的必要性这个demo里默认使用文本帧也就是send和onmessage里都是字符串。JavaScript的WebSocket对象对于字符串消息会以文本帧发送对于ArrayBuffer或Blob会以二进制帧发送。在服务端Node.js的ws库通过ws.on(message)收到的事件里数据默认是Buffer对象需要用toString()转成字符串才能拿到原始文本。如果消息是JSON就要先JSON.parse()。这里我给你一个判断经验业务应用里大部分消息用文本帧就够了JSON字符串自带结构调试时也能直接打出来看只有传文件、图片流、音视频帧或者二进制协议数据才需要走二进制帧。混用也不是不行但客户端接收时要注意binaryType的设置——默认情况下收到二进制会返回Blob如果你期望的是ArrayBuffer要显式设置socket.binaryType arraybuffer。掩码这块我建议把RFC 6455的帧格式画成一张图放在手边。我最初写解析器时从Buffer里读字节长度字段用的是7比特后来又遇到长度为126和127的特殊情况没处理直接读错字节解析出来全是乱码。如果你用的是成熟库这些都不用管但如果你要看懂协议再扩展掩码和长度扩展字段是绕不过去的。3. 动手跑通demo基于Node.js的ws库和原生JS客户端3.1 准备环境Node.js版本与ws库安装这个demo的服务端用的是Node.js和ws库。ws是目前Node生态里比较主流的WebSocket实现API简洁错误处理也清晰。先在机器上确认Node版本建议至少14以上我测试时用的是18版本动态特性和Buffer处理都正常。然后进入项目目录初始化并安装依赖mkdir websocket-demo cd websocket-demo npm init -y npm install ws8装完之后在node_modules/ws里能看到完整的实现。如果你网络环境受限装不上最新版换成 ws7 也可以API差异不大。这里npm init -y会生成一个默认的package.json后面可以修改main字段指向服务端入口文件。3.2 服务端代码创建WebSocket服务器并处理连接服务端的核心逻辑很简单启动一个HTTP服务器结合ws库创建WebSocket服务监听连接事件为每个连接绑定消息、关闭、错误监听。下面是一个完整的最小实现保存为server.jsconst WebSocket require(ws); const wss new WebSocket.Server({ port: 8080, path: /chat }); // 维护所有已连接的客户端用于广播 const clients new Set(); wss.on(connection, (ws, req) { console.log(客户端已连接IP:, req.socket.remoteAddress); // 加入连接集合 clients.add(ws); ws.isAlive true; // 收到消息时处理这里直接回显并广播给其他客户端 ws.on(message, (data, isBinary) { const message isBinary ? data : data.toString(); console.log(收到:, message); // 如果内容看起来像JSON尝试解析后按type分发 let parsed null; try { parsed JSON.parse(message); } catch (e) { parsed { type: text, content: message }; } // 广播给所有客户端 const outbound JSON.stringify({ sender: server, type: parsed.type || text, content: parsed.content || parsed, }); for (const client of clients) { if (client.readyState WebSocket.OPEN) { client.send(outbound); } } }); ws.on(pong, () { ws.isAlive true; }); ws.on(close, () { console.log(连接关闭); clients.delete(ws); }); ws.on(error, (err) { console.error(连接错误:, err.message); clients.delete(ws); }); // 发送欢迎消息 ws.send(JSON.stringify({ type: welcome, content: 你已经连上demo服务器 })); }); console.log(WebSocket服务已启动监听端口8080路径/chat);这段代码的逻辑分几块创建服务时传了port和path意味着只有访问ws://localhost:8080/chat才能连上用Set保存所有连接方便后续广播收到消息时先尝试JSON.parse如果失败就当作普通文本然后统一组装成带type字段的JSON消息再广播出去。这里很重要的一点是ws.on(message, (data, isBinary) ...)ws库默认会把收到的消息包装成Buffer文本消息需要data.toString()还原isBinary参数可以判断是否二进制如果为true我就保留原始Buffer直接广播避免二进制数据被字符串转换破坏。心跳机制在demo里是可选的但我在connection里初始化了ws.isAlive true并在pong事件里把它设回true。后面你可以加一个定时器定期检查isAlive是否为false如果是就调用ws.terminate()。现在先了解结构下一章我会专门讲这个坑。3.3 客户端代码HTML页面里如何连、如何发、如何收客户端不用装任何依赖浏览器原生就支持WebSocket。写一个index.html里面放一个消息列表、输入框和发送按钮。核心逻辑是创建一个WebSocket实例然后绑定四个事件onopen、onmessage、onclose、onerror。!DOCTYPE html html langzh-CN head meta charsetUTF-8 / titleWebSocket Demo 客户端/title style #messages { border: 1px solid #ccc; height: 300px; overflow-y: auto; padding: 8px; } .msg { margin: 4px 0; } /style /head body div idmessages/div input idinput typetext placeholder输入消息 / button idsend发送/button script const wsProtocol location.protocol https: ? wss : ws; const wsUrl ${wsProtocol}://${location.host}/chat; const socket new WebSocket(wsUrl); socket.onopen () { appendMessage(已连接到服务器); }; socket.onmessage (event) { // 尝试解析JSON解析失败则按原文显示 let text event.data; try { const obj JSON.parse(text); text obj.content || JSON.stringify(obj); } catch (e) { // 不是JSON保持原文 } appendMessage(收到: ${text}); }; socket.onclose (event) { appendMessage(连接关闭code${event.code}, reason${event.reason}); }; socket.onerror (err) { appendMessage(发生错误请查看控制台); console.error(err); }; document.getElementById(send).addEventListener(click, () { const input document.getElementById(input); const value input.value.trim(); if (!value) return; socket.send(JSON.stringify({ type: chat, content: value })); input.value ; }); function appendMessage(text) { const div document.createElement(div); div.className msg; div.textContent text; document.getElementById(messages).appendChild(div); } /script /body /html这里需要注意我并没有把页面放在和WebSocket服务同一个端口所以在wsUrl里直接写死了location.protocol和location.host这要求页面本身也是从某个HTTP服务器打开的。如果你只是双击打开本地HTMLlocation.protocol会是file:生成的地址会变成file:///chat那当然连不上。正确做法是用一个静态服务器托管这个HTML或者直接把HTML放到Node服务的同个端口下。我一般会这么做在server.js里再加一个HTTP服务器让根路径返回这个HTML这样访问http://localhost:8080就能看到页面页面里的location.host也就自然指向了8080端口。如果你怕麻烦也可以用npx serve .起一个静态服务器然后把wsUrl改成ws://localhost:8080/chat两种方式都行。3.4 关键参数说明端口、路径、心跳与日志这个demo里需要关注的参数就几个端口、路径、心跳间隔、最大负载、连接数上限。端口默认用8080别和已有的HTTP服务冲突。如果端口被占用会报EADDRINUSE错误这时换一个端口或者用process.env.PORT读取环境变量。路径服务端传了path: /chat那么客户端URL必须是ws://host:8080/chat。如果不传path默认连接任何路径都能建立。限制路径的好处是前端可以控制只连到合法端点防止恶意扫端口。心跳ws.isAlive这个标志位配合定时ping来做存活检查。虽然ws库自己也有超时机制但如果你想在客户端异常掉线后尽快回收连接必须主动发ping并等待pong。后面我会专门写一个setInterval的方案。最大负载ws库默认允许接收最大100MB的消息如果你希望限制单条消息大小可以在创建Server时配置maxPayload: 1024 * 1024比如1MB。超过之后ws会直接杀掉连接这可以防止大文本把内存打爆。日志demo里我用console.log输出了连接、消息、关闭、错误事件。正式项目建议引入日志级别别把所有细节都打到本地。4. 避坑指南WebSocket demo里最容易翻车的5个场景4.1 现象握手成功但客户端收不到消息服务端日志也没有任何push记录我一开始做广播时发现浏览器状态是OPEN服务端也打印了“客户端已连接”但客户端收不到任何服务端主动推送的消息。排查后发现问题出在服务端代码逻辑上我没有在connection完成后主动推送只有在收到客户端消息才回复。客户端没有发消息自然什么都收不到。原因就是想当然地认为“连接一旦建立服务端就会一直推数据”实际上推数据也需要代码触发。解决方法是在connection回调里立即调用一次ws.send(欢迎消息)或者写一个定时器定期推送。所以当你发现收不到消息先确认服务端是否执行过send方法客户端的onmessage只是“接收”不会主动“拉取”。4.2 现象连接频繁断开服务端日志出现Error: read ECONNRESET这里要分清是客户端掉线还是服务端主动断开。如果客户端在几分钟内没有任何操作连接被断开很可能是没有处理心跳机制。浏览器或中间代理如Nginx会在空闲一段时间后关闭TCP连接。解决办法是服务端定时发送ping帧客户端自动回pong同时客户端也可以自己发送ping或者应用层心跳消息。我在服务端加了一个10秒的定时器const interval setInterval(() { wss.clients.forEach((ws) { if (!ws.isAlive) { ws.terminate(); return; } ws.isAlive false; ws.ping(); }); }, 10000); wss.on(close, () { clearInterval(interval); });这里先设置isAlive false等pong事件再改回true如果一个周期内没收到pong就认为连接死了直接terminate()。这个方式能有效清理僵尸连接但也别把间隔设得太短尤其是移动网络下稍高的延迟就可能误杀。4.3 现象页面在HTTPS环境下WebSocket连接连不上如果你的页面是https://开头的那么浏览器的安全策略要求WebSocket也必须使用wss://不能再用ws://。这个demo里我写了wsProtocol的判断等于做了自适应。但如果你自己手动拼地址很容易复制错。另外如果你用了Nginx反向代理Nginx需要配置proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection upgrade;这两条缺一不可。否则客户端请求到Nginx后Nginx把Upgrade头剥掉了后端收到的就不是WebSocket握手请求自然连接失败。我遇到过一个场景后端日志里显示的是普通GET请求状态码直接返回404那就是Nginx配置少了这两行。4.4 现象广播时只有部分客户端收到消息其他客户端没反应原因大概率是连接集合维护出了问题。有些人喜欢用数组保存客户端客户端关闭时没有从数组里剔除导致数组里依然保留着已关闭的socket对象对这些对象调用send会抛异常。另外如果你在connection回调里写了clients.add(ws)但忘记在close事件里clients.delete(ws)那么这个集合会越来越大漏发到的连接都是已经死掉的僵尸连接。解决办法是用Set而不是数组delete操作时间复杂度O(1)而且不会留下空槽位。同时发送前检查client.readyState WebSocket.OPEN再去send。4.5 现象服务运行几天后内存持续上涨进程崩溃这个问题通常和消息缓冲有关。ws库默认不会无限制缓冲但如果你发送消息的速度大于TCP接收速度数据会堆积在内部缓冲队列里。更常见的原因是在message回调里处理了耗时很长的同步操作比如解析大JSON、写数据库导致事件循环堵塞新的消息不断堆积。解决方法是尽量把耗时操作丢到异步队列里限制maxPayload定期检查ws.bufferedAmount当它大于某个阈值比如10MB时暂停发送或主动断开客户端。我在demo里就没有处理bufferedAmount但实际生产场景一定要看不然内存说涨就涨。5. 从demo到工程心跳、断线重连与消息序列化的进阶写法demo跑通只是第一步真正放到生产上还需要补三个东西可靠的连接状态检测、客户端的自动重连、以及统一的消息格式。这三个点也是最容易被抽离复用的部分。心跳检测除了服务端主动ping外客户端也可以主动发应用层心跳。比如每隔30秒发一条{type:ping, ts: Date.now()}服务端收到后回{type:pong, ts: ...}。这样做的好处是能直接验证业务链路而不只是TCP链路。如果你用协议层的ping/pong某些代理可能拦截得不明不白应用层心跳则完全透明它也影响业务设计。我在多个项目里两种都做协议层ping/pong用于TCP存活应用层心跳用于业务活性判断。两者间隔不能太近否则会加重服务端无谓负载。断线重连的写法核心是重连时机和退避策略。不是断开后立刻重连而是先判断关闭原因。如果event.code是1000正常关闭或者1001服务端主动关闭通常不需要自动重连如果是1006异常断开就应该走重连逻辑。我用过一个指数退避算法let retryTimes 0; const maxRetry 5; function connect() { const socket new WebSocket(wsUrl); socket.onclose (event) { if (event.code 1006 retryTimes maxRetry) { const delay Math.min(1000 * Math.pow(2, retryTimes), 30000); retryTimes; console.log(重连中第retryTimes次延迟delayms); setTimeout(connect, delay); } }; socket.onopen () { retryTimes 0; }; }重连成功时把retryTimes重置为0避免一直重试。这里还有一个细节重连期间要清理掉旧实例的所有监听器否则会叠加内存泄漏。用WebSocket的原生API时可以在onclose里取消旧的定时器或者在实例上保存一个closed标志防止onmessage回调在旧连接上继续执行。消息序列化我想单独强调一下。demo里我用的是简单的JSON没有分字段。实际工程里应该定义一个信封结构{type, messageId, timestamp, data}。type表示消息类型服务端根据它分发到不同处理函数messageId用于去重和拼请求响应timestamp是排查延迟问题的重要依据。我在某个项目里因为没有messageId前端拿到同一条消息重复渲染了两次加了消息ID后用Set去重问题立刻消失。最后说一个我自己的教训曾经有一次我写完demo就直接上线没用maxPayload限制有人往连接里发了一个200MB的字符串服务端内存瞬间拉满进程直接崩溃。从那以后我每次搭建WebSocket服务都强制走一遍这个检查清单心跳是否开启、超时是否配置、负载是否限制、连接集合是否在close时清理、Nginx是否配置了Upgrade头。这套流程救过我很多次希望也能帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询