ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WebSocket与HTTP的区别:从协议原理到Spring Boot整合实战

WebSocket与HTTP的区别:从协议原理到Spring Boot整合实战 做后端这些年每隔一段时间就会有人来问我同一个问题WebSocket 和 HTTP 到底有什么区别问的人里有刚转行做前端的也有写了好几年 CRUD 的后端甚至还有做硬件的朋友。他们找的资料五花八门但需求都指向同一个方向搞清楚这两个协议的本质差异然后知道自己手头的项目到底该用哪个。这篇文章我会从协议原理讲到实际选型再给一个可以直接跑的 Spring Boot 整合示例最后把我在生产环境里踩过的坑一并列出来。适合后端开发、前端工程师以及对实时通信方案感兴趣的同学。读完之后你至少能回答三件事两者的核心区别是什么、什么场景该用哪个、整合时最容易在哪里翻车。1. 先看清楚 HTTP 的真实运作逻辑1.1 请求-响应模型一次严格的一问一答HTTP 的本质是一个客户端发起请求、服务端返回响应的应用层协议。客户端永远是主动方服务端永远是被动方。浏览器输入网址后发一个 GET 请求服务器返回 HTML 页面这个交互就结束了。如果页面里还有图片、CSS、JS 文件浏览器会再发多个请求每个请求都有自己独立的响应服务器绝不会在返回某个请求的响应之后主动去顺带推送别的东西。这种模型的优点非常明显简单、确定性强、方便缓存和代理。但它有一个天生缺陷——服务端没办法主动开口说话。服务器想知道客户端当前状态、有没有新数据要通知客户端唯一的办法是等客户端来问。于是就有了最原始的实时方案轮询。前端每隔几秒发一次请求把最新数据取走。这个方案在数据量小、实时性要求不高的场景完全够用但一旦频率拉高浪费的请求量会非常夸张。我接过一个物联网大屏项目前期用 5 秒一次的轮询看起来也没什么问题。后来设备点位从几百涨到几千所有点位都靠前端轮询拉取网关和数据库直接被拖垮。那次之后我就明白了一个道理轮询本质上是用流量换实时性在规模上来之前问题只是还没爆发而已。这也是我后来认真研究 WebSocket 的直接原因。1.2 无状态特性以及状态是怎么补回来的HTTP 是典型的无状态协议。同一个客户端连续发送两个请求服务器默认不记得你是谁。默认这两个字很关键因为后来业界通过 Cookie、Session、Token 给 HTTP 补上了记忆能力业务上可以做到看起来有状态但协议本身对状态依然没有任何记忆。这也是面试里高频考的一个点——HTTP 无状态怎么办。从工程角度看无状态其实是个优点服务器可以随便横向扩展任何一台机器都能处理任意请求不需要同步会话数据。真正麻烦的是当我们需要实时交互时——比如用户在聊天框里待了十分钟服务器得记住他连在哪——无状态就变成了障碍。这就是为什么实时通信方案经常不直接用 HTTP而是另起炉灶。1.3 连接复用与长连接HTTP 也在进化很多人误以为 HTTP 每次请求都要新建一条 TCP 连接这个说法在 HTTP/1.1 之后就过时了。HTTP/1.1 引入了 Keep-Alive允许在一条 TCP 连接上串行传多个请求HTTP/2 更进一步多个请求可以在同一条连接上并行复用很大程度上缓解了队头阻塞。但即便做到这一步HTTP 依然是请求-响应模式服务端照样不能主动给客户端推消息。顺便回应一下搜索热度很高的http和tcp的区别一句话就能说清——TCP 是传输层协议负责把字节流可靠地从 A 端传到 B 端HTTP 是应用层协议定义字节流怎么组织、怎么解析。WebSocket 与 HTTP 一样跑在 TCP 之上所以WebSocket 与 HTTP 的对比是应用层协议之间的对比不要拉到 TCP 那一层去混着比。2. WebSocket 为什么会出现以及它是怎么工作的2.1 它要解决的是服务端主动说话的问题WebSocket 的设计目标非常明确让客户端和服务端在同一条 TCP 连接上自由地双向收发数据谁都可以随时开口。它既不是 HTTP 的替代品也不是 HTTP 的竞争对手而是一个互补方案。想要实时推送用 WebSocket想要标准的请求响应接口继续用 HTTP。两者在一个系统里共存是非常正常的。为了达成双向自由通信这个目标WebSocket 有两个核心设计升级握手和帧协议。理解了这两个设计基本上就理解了 WebSocket 的大半。很多人一上来就去看教程里怎么发消息、怎么收消息却忽略了握手机制遇到问题的时候往往一头雾水。2.2 一次握手完成协议升级WebSocket 的建立不是凭空来的而是站在 HTTP 肩膀上。客户端先发一个普通的 HTTP 请求请求头里带上Upgrade: websocket和Connection: Upgrade服务端如果同意就返回状态码 101随后这条 TCP 连接不再按 HTTP 规则解析而是切换成 WebSocket 协议。整个握手过程长这样GET /ws/time HTTP/1.1 Host: localhost:8080 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw Sec-WebSocket-Version: 13服务端返回HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk注意Sec-WebSocket-Key和Sec-WebSocket-Accept这对字段它们的作用是校验握手请求的合法性防止普通 HTTP 请求被错误升级成 WebSocket 连接。整个过程对浏览器来说完全透明这也是为什么 WebSocket 在浏览器里兼容性这么好——底层 TCP 连接还是那条只是应用层协议换了。握手阶段的鉴权是个很容易翻车的点。很多人想当然地在 URL 后面拼 Token或是在请求头里放自定义字段结果发现浏览器 WebSocket API 根本带不了自定义 Header。我自己的推荐做法是把认证信息放在 URL 查询参数里或者等连接建立之后客户端先发一条认证消息服务端校验通过后才正式进入业务逻辑。这个坑后面详细说。2.3 帧格式与消息重组连接升级之后双方不再使用 HTTP 的报文格式而是按 WebSocket 定义的帧格式收发数据。每一帧里都有 opcode标识是文本、二进制、关闭还是 ping/pong 控制帧、mask 位客户端发往服务端的帧必须掩码、payload length 等字段。这些字段细节你可以不背但至少要知道两件事。第一一个业务消息可以拆成多个帧传输到达对端后协议自动重组接收方无感知。第二帧头部开销很小最小的帧头只有两个字节。跟 HTTP 动不动几百字节的报文头相比WebSocket 对高频、小消息的实时推送场景友好得多。实测下来同一台机器、同样的吞吐量WebSocket 的带宽占用可能只有 HTTP 轮询的几十分之一。3. 核心区别逐项拆解一次性说透3.1 连接模型短对话 vs 专线HTTP 的连接是用完即散。发起一次请求建立连接拿到响应连接要么关闭要么进入空闲池等待复用服务器和客户端之间没有一条持续存在的会话通道。WebSocket 则是在一次握手之后建立一条长连接从建立到关闭期间双方随时可以在这条连接上发数据。类比一下HTTP 是打电话拨号、通话、挂断每次都要重新来WebSocket 是建了一条专线接通之后两边随讲随有。这个差别决定了它们在很多场景下的命运。你不可能靠几百次电话实现客服和用户的实时对话但一条专线却可以。3.2 通信方向单向应答 vs 全双工这是两者最本质的区别。HTTP 的通信永远是请求-响应成对出现服务端绝不会主动给客户端发消息WebSocket 是全双工通信客户端可以随时发服务端可以随时推哪怕连着发十条消息、不等待响应也完全没问题。实时弹幕、行情推送、协同编辑这类应用如果没有 WebSocket靠 HTTP 轮询会非常痛苦这一点在前面已经分析过。3.3 数据格式与开销报文头 vs 帧HTTP 报文头是纯文本包含大量首部字段一个 GET 请求的报文头通常就有几百字节高频调用时这些开销累积起来相当可观。WebSocket 的帧头最小只有两个字节控制帧和数据帧都极其轻量。当然WebSocket 的消息内容本身用什么格式——JSON、二进制、Protocol Buffers——完全由你自己决定协议不关心。所以实际项目中WebSocket 的消息体积反而更好控制。3.4 协议依赖与端口两者都基于 TCPWebSocket 的握手阶段依赖 HTTP默认端口同样是 80/443。加密传输时HTTPS 对应 wss://两者共用 443 端口证书体系和信任机制也完全一致。所以如果你已经有一个 HTTPS 站点完全不需要为 WebSocket 单独申请证书直接配置 wss 即可。顺便把搜索量也很高的http和https的区别一并说掉HTTPS 就是 HTTP 加了一层 TLS 加密传输端口由 80 变成 443主要解决防窃听、防篡改和身份认证的问题。对应到 WebSocket 这边就是 ws 和 wss 的关系本质完全一致。底层加密机制是同一套换的只是上面跑的应用层协议。3.5 关键对比表对比维度HTTPWebSocket连接模型每次请求单独建立/复用连接一次握手建立长连接通信方向单向请求-响应全双工双向随时通信数据格式文本报文头部开销大二进制帧头部极小服务端主动推送不支持原生支持状态保存无状态靠 Session/Token 补偿连接本身即状态建立方式直接请求先 HTTP 握手101 升级典型场景页面访问、API 接口聊天、推送、实时同步端口80 / 44380 / 443握手后仍是 TCP3.6 顺带回应HTTP/2 能不能替代 WebSocketHTTP/2 引入了服务端推送Server Push有人觉得这就能替代 WebSocket 了。实际用下来会发现完全是两码事。HTTP/2 的推送只能发生在客户端发起某个请求之后、响应返回之前这个窗口内而且推送内容是被动的、一次性的服务端没办法在任意时刻主动往客户端推一条全新消息。所以生产环境里实时双向通信选 WebSocket 依然是兼容性最好、生态最成熟的主流方案。4. 什么时候用 WebSocket什么时候坚持 HTTP4.1 适合 WebSocket 的场景我判断一个需求是否用 WebSocket核心标准就一条是否存在高频率、低延迟、服务端主动触发的实时数据交换。具体来说下面这些场景基本都是 WebSocket 的天下聊天室、弹幕、协同编辑这类多人实时互动股票行情、数字货币价格、体育比分等高频行情推送物联网设备的状态上报、指令下发设备数量大了之后轮询根本扛不住游戏对战房间内的房间状态和操作同步后台长耗时任务的进度通知比如批量导入、视频转码、模型训练这些场景的共同特征是如果靠 HTTP 轮询延迟和流量成本会成倍上升如果用 WebSocket一条连接就同时搞定了双向通道。我做过一个运营后台的导入进度推送原来前端每 3 秒查一次任务状态导十万行数据时数据库要被查几百次切到 WebSocket 后服务端只在状态变化时推一条消息数据库压力瞬间降没了。4.2 坚持用 HTTP 的场景反过来说如果你只是普通的增删改查、数据报表、静态资源加载老老实实用 HTTP 就好。实时性要求不高、客户端与服务端交互并不频繁、服务端没有任何主动推送需求用 WebSocket 反而是一种浪费还会引入连接管理、心跳保活、断线重连等一堆额外复杂度。另外如果你是做硬件对接的比如 STM32、ESP32 这类设备大多数场景用 HTTP 就足够了。设备端每次采集完数据上报一次服务端在下一次上报时附带回传指令完全能满足需求。WebSocket 在资源受限的嵌入式平台上支持并不普遍除非设备端已经有成熟的客户端库否则不要为了实时性强行上 WebSocket。技术选型不是越先进越好而是越匹配越好。4.3 一张选型判断清单信号建议实时性要求小于 1 秒考虑 WebSocket服务端需要主动推送必须 WebSocket主要是查询和提交表单用 HTTP客户端是浏览器且量级大两者都可以取决于实时性团队没有 WebSocket 经验先小范围试点别全量切换连接数规模大、跨集群部署提前设计会话保持和广播方案5. 实操Spring Boot 整合 WebSocket 完整示例原理讲再多不跑一遍等于零。下面我用 Spring Boot 3 原生 WebSocket 写一个最小可用的实时推送服务前端页面连上来之后服务端每秒推送一次时间客户端也可以随时给服务端发消息。5.1 依赖引入与项目结构Spring Boot 的 WebSocket 支持主要分两块底层是jakarta.websocket的注解模式Spring 又封了一层WebSocketHandler的编程模式。两者都能用我个人更推荐WebSocketHandler模式因为它在 Spring Security、拦截器、参数绑定方面更顺手。先引入依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependencystarter 会把 Tomcat 的 WebSocket 实现以及 Spring 的封装一并带进来不需要额外配置容器。接下来写一个配置类注册 WebSocket 处理器并指定访问路径Configuration public class WebSocketConfig implements WebSocketConfigurer { private final TimePushHandler timePushHandler; public WebSocketConfig(TimePushHandler timePushHandler) { this.timePushHandler timePushHandler; } Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(timePushHandler, /ws/time) .setAllowedOrigins(*); } }这里有个细节setAllowedOrigins(*)在生产环境要慎用它意味着任意页面都能连到你的 WebSocket 服务盗连和消息泛滥的风险会显著上升。如果服务部署在内网或者有明确的前端域名应该把域名白名单写死不要图省事全放通。5.2 服务端处理器与在线用户管理核心处理器继承TextWebSocketHandler。afterConnectionEstablished在连接建立后触发handleTextMessage处理客户端发来的文本消息afterConnectionClosed做资源清理。在线用户我习惯用一个ConcurrentHashMap管理因为多个 WebSocket 连接可能分布在不同的 IO 线程上线程安全问题必须考虑。Component public class TimePushHandler extends TextWebSocketHandler { private final MapString, WebSocketSession sessions new ConcurrentHashMap(); Override public void afterConnectionEstablished(WebSocketSession session) { sessions.put(session.getId(), session); System.out.println(连接建立 session.getId() 当前在线 sessions.size()); } Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { System.out.println(收到消息 message.getPayload()); session.sendMessage(new TextMessage(服务端已收到 message.getPayload())); } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { sessions.remove(session.getId()); System.out.println(连接关闭 session.getId()); } public void pushToAll(String content) throws IOException { TextMessage message new TextMessage(content); for (WebSocketSession session : sessions.values()) { if (session.isOpen()) { session.sendMessage(message); } } } }ConcurrentHashMap是这里的关键选择。如果换成普通的HashMap在高并发连接建立、断开的瞬间内部结构可能被破坏轻则丢连接重则死循环。这是我在生产环境实实在在踩过的坑别问我是怎么知道的。再写一个定时任务每秒向所有在线客户端推送一次服务器时间模拟服务端主动推送Component public class TimePushTask { private final TimePushHandler pushHandler; public TimePushTask(TimePushHandler pushHandler) { this.pushHandler pushHandler; } Scheduled(fixedRate 1000) public void pushTime() { try { pushHandler.pushToAll(当前时间 LocalTime.now()); } catch (IOException e) { e.printStackTrace(); } } }记得在主类上加EnableScheduling否则定时任务不会执行。推送单条连接的时候更要注意客户端突然断开、网络抖动都会让sendMessage抛IOException不做捕获的话定时任务会被一个异常打断后续所有推送全部停摆。5.3 前端连接与消息收发前端用浏览器原生 WebSocket API 就够了不需要任何第三方库const ws new WebSocket(ws://localhost:8080/ws/time); ws.onopen function () { console.log(连接已建立); ws.send(hello from browser); }; ws.onmessage function (event) { const timeDiv document.getElementById(time); timeDiv.innerText event.data; }; ws.onclose function () { console.log(连接已关闭); }; ws.onerror function (err) { console.error(发生错误, err); };这里有个非常常见的坑如果服务端是 HTTPS浏览器里 WebSocket 的地址必须写成wss://写成ws://会被浏览器直接拦截控制台报错信息往往是连接被拒绝或者Mixed Content。同样localhost 环境下用 http 访问页面WebSocket 就写 ws一旦上了线上 https 域名千万记得同步改成 wss。5.4 心跳保活与断线重连长连接最怕的就是半开连接——客户端已经死了服务端还傻乎乎地认为连接在。TCP 层有超时机制但默认超时时间非常长短则几分钟长则几十分钟。为了及时发现死连接业界通用的做法是心跳。心跳有两种常见姿势。第一种是应用层心跳客户端定时发一条自定义的 ping 消息比如{type:ping}服务端收到后回一条 pong如果连续 N 次没收到 pong 就判定连接已死。第二种是协议层心跳WebSocket 原生支持 ping/pong 控制帧Spring 的WebSocketSession底层也支持但很多实现默认没启用。我自己在项目里更倾向于应用层心跳理由很简单排查方便、日志直观、业务上还能顺带做在线状态上报。断线重连则是前端的责任标准写法是收到onclose事件后尝试重新连接但注意要做指数退避别让服务器在断网瞬间收到几千个重连请求。let retryCount 0; const maxRetry 10; function connect() { const ws new WebSocket(ws://localhost:8080/ws/time); ws.onopen function () { retryCount 0; console.log(连接已建立); }; ws.onclose function () { if (retryCount maxRetry) { retryCount; const delay Math.min(30000, Math.pow(2, retryCount) * 1000); console.log(延迟重连次数 retryCount 延迟 delay); setTimeout(connect, delay); } }; ws.onmessage function (event) { document.getElementById(time).innerText event.data; }; } connect();Math.pow(2, retryCount) * 1000就是指数退避的核心逻辑——第一次重连延迟约 2 秒第二次 4 秒第三次 8 秒逐步放缓同时用Math.min(30000, ...)封顶 30 秒避免延迟被推到不可接受的程度。Python 那边我用 Django Channels 实现过类似功能思路完全一致消费者里websocket_connect做连接管理websocket_receive接收消息后台有数据时通过channel_layer.group_send向组内所有客户端推送。如果你们的后端是 Python 强项用 Channels 会比裸 Django 加轮询舒服得多。6. 常见问题与排查技巧实录6.1 握手上不去状态码说明了问题客户端连 WebSocket 时本质上先发了一个带Upgrade头的 HTTP 请求。如果服务器没按预期处理常见的返回是 200、404、403、502。遇到unexpected status这类报错第一件事就是去看响应状态码返回 200服务器把升级请求当普通 HTTP 请求处理了说明路径不对或者有一个过滤器/拦截器提前拦截了请求根本没让握手走到 WebSocket 处理器。返回 404路径没映射上检查注册的路径与前端连接地址是否完全一致。返回 403多半是跨域限制或者鉴权拦截器拒绝了握手。返回 502最常见的原因有两个——负载均衡或反向代理服务器没有配置 WebSocket 支持把 101 升级响应当错误处理了或者代理层的超时时间太短连接还没来得及升级就被切断。Nginx 代理 WebSocket 时至少要加这一段配置location /ws/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; }proxy_read_timeout是关键。Nginx 默认读取超时是 60 秒WebSocket 连接如果 60 秒内没有任何数据流动Nginx 就会主动断开表现就是客户端隔一段时间就掉线。生产环境我一般配到 3600 秒以上由应用层心跳来维持活跃同时心跳也能把没有任何数据这个状态打破。6.2 跨域、鉴权与自定义 Header 的坑浏览器 WebSocket 的new WebSocket(url)不支持自定义请求头。很多从 HTTP 接口转过来的同学习惯在 Header 里塞 Token到了 WebSocket 这里直接傻眼。解决方案就两条路。一条是把 Token 放 URL 参数里比如ws://localhost:8080/ws/time?tokenxxx服务端在握手拦截器里从 URI 取参数校验。另一条是先连上再发一条认证消息服务端校验不通过就强制关闭连接。前者的优点是可以配合拦截器在握手阶段就拒绝连接缺点是 URL 一旦被日志系统记录下来Token 就有泄露风险后者安全性稍好但要处理好握手成功后、认证成功前这个窗口期的消息别让业务逻辑抢在认证前执行。跨域问题则是setAllowedOrigins没配好前端页面在 A 域名WebSocket 服务在 B 端口浏览器默认禁止跨域连接服务端必须明确允许相应来源。内网环境可以直接配*公网服务务必收敛成白名单。6.3 负载均衡与多实例部署的会话保持WebSocket 连接是有状态的同一个客户端必须一直连在同一个后端实例上。如果负载均衡把客户端后续的请求转发到另一台机器因为服务端根本没有这台机器的连接记录消息就会丢失。解决办法有三个按成本从低到高排。第一负载均衡层开启粘性会话sticky session让同一个客户端 IP 或 Cookie 始终命中同一个后端节点配置简单但节点故障时连接会断开。第二在所有实例之间做会话同步比如用 Redis 记录每台实例上的连接服务端需要广播时先把消息发到 Redis再由各实例推给各自的客户端这其实是集群推送的标准做法。第三如果消息量不大可以采用 Redis Pub/Sub 或消息队列做事件广播实现全集群统一推送。很多团队做聊天功能到了这一步才开始认识到WebSocket 的单机实现很容易集群化才是真正的门槛。我的经验是在系统设计阶段就要想清楚消息是单机内转发还是全集群广播别等上线后连接数一涨再重构。6.4 连接数上限、线程与内存调优单机的 WebSocket 连接数是有上限的。首先是操作系统的文件描述符限制Linux 默认 ulimit 通常是 1024改到 65535 或更高是基本功ulimit -n 65535其次是 Tomcat 的线程池配置。WebSocket 连接虽然是长连接不需要每连接一个线程但 Tomcat 处理握手和 IO 事件仍然依赖线程池连接数上来后要把server.tomcat.threads.max适当调大。最后是内存每个连接都有对应的会话对象和缓冲区连接数破万之后内存占用会在几百 MB 甚至上 GB 的量级需要提前规划堆内存和对象复用策略。我做过一个行情推送项目上线前专门做了连接数压测从 1000 到 5000 到 10000 逐步加观察 GC 和 CPU最后把缓冲区大小、线程池参数、心跳频率都做了针对性调整。这类参数在不同项目里差异很大不要照抄网上的配置必须用真实压测数据说话。6.5 关于通过 WebSocket 发送 POST 请求搜索词里有通过websocket发送post请求这里多说一句WebSocket 协议本身并没有 GET、POST 这种方法的区分消息就是一段文本或者二进制语义完全由双方约定。如果你需要在 WebSocket 连接里模拟类似 HTTP 请求的语义自己定义消息格式就行比如{action:POST,path:/order,data:{...}}。但如果你的本意是在 WebSocket 服务端去调用另一个 HTTP 接口那是另一回事。服务端收到消息后再发 HTTP 请求调用第三方 APIWebSocket 只做消息通道实际调用还是用 HTTP 客户端。这两种需求经常被搞混——前者是协议语义设计问题后者是把 WebSocket 当作触发条件的问题实现思路完全不同。6.6 一台机器的实际经验结论最后分享一点个人体会。从协议角度看WebSocket 和 HTTP 根本不是谁替代谁的关系而是互补关系。WebSocket 用于实时双向通道HTTP 用于标准请求响应一个系统里两者共存、各司其职是非常常见的架构形态。我在实际项目里的习惯是所有业务接口、数据查询保持 HTTP只有实时推送、在线通知这些真正需要低延迟双向通信的部分才上 WebSocket。这样既保证了实时体验又没把系统的复杂度推高到不可控的程度。如果刚接触 WebSocket我建议你先别急于上框架的高级封装把原生 API 跑通、把握手流程用抓包工具完整看一遍再逐步加心跳、重连、集群广播。理解原理之后再碰框架遇到问题你才知道该查哪里、该怎么定位。这个顺序能帮你少走至少一个月的弯路。
返回列表