☰
Spring Boot整合Jetty内置WebSocket实现前后端实时通信
2026/10/21 2:07:14 网站建设 项目流程

前阵子在做一个模拟项目X,需要把后端产生的实时数据推到网页上。第一反应是轮询,但数据更新的频率不高不低,轮询要么浪费流量要么延迟严重,想来想去还是用WebSocket干净。项目技术栈本身就是Spring Boot,容器换成了Jetty,所以标题里写的“SpringBoot使用Jetty内置的WebSocket实现简单前后端通信”就是我的实际解决方案。这篇算是把完整的做法、代码、踩坑过程都梳理了一遍,适合想用最快速度把WebSocket跑起来,又对Jetty这种嵌入式容器不太熟的人。

我尽量把依赖和配置写得细一点,因为这种项目难点通常不在业务上,而在“为什么我这个Tomcat下能跑的代码换到Jetty就跑不通了”“为什么握手总失败”这类环境问题上。读完之后,你至少能拿到一个能直接改改就用的最小实现,并且知道后续遇到连接断开、跨域、心跳这类问题该往哪个方向查。

1. 项目整体设计与方案选型

1.1 需求核心在哪里

项目需求说白了就一句话:后端某几个指标有波动,前端页面要马上看到,不能靠用户手动刷新。这个场景用HTTP非常别扭,因为HTTP是请求-响应模型,服务端没法主动把数据塞给浏览器。要让前端实时收到数据,常见办法只有几条路:

  • 前端定时轮询:实现最简单,但实时性差,数据变化越频繁轮询间隔越短,服务器压力越大。
  • 长轮询:本质上还是HTTP,只是把响应时间拉长,达到“假装实时”的效果,但会占用一批连接线程。
  • SSE:服务端单向推送,浏览器支持很好,但只能服务器往客户端推,双向交互还是要另开接口。
  • WebSocket:真正意义上的全双工长连接,一端握手成功后,双方可以随时互发消息。

对于我这个场景,前端不仅要收数据,偶尔还要向后端发控制指令,所以WebSocket是唯一能把两条路并成一条路的方案。而且项目规模不大,引入WebSocket不需要复杂的消息队列,直接用容器自带的实现就够了。

1.2 为什么是 Jetty,不是默认的 Tomcat

Spring Boot 默认内嵌容器是Tomcat,但它本身支持灵活切换,只要在依赖里排除掉 spring-boot-starter-tomcat,引入 spring-boot-starter-jetty 就能换成Jetty。做这个选择不是强制要求,主要考虑点有三个:

第一,Jetty定位轻量,启动速度快,内存占用小。对这种并发量不高的中小型实时通信Demo,Jetty跑起来比Tomcat更省资源,尤其在开发机上频繁重启时,体感非常明显。

第二,Jetty自身就是成熟Web服务容器,它的WebSocket实现做得比较早,完全支持JSR-356规范,也支持底层原生API。如果是“想用容器自己那一套WebSocket”,Jetty是个很顺手的选择,不需要额外装独立WebSocket服务器。

第三,同一套代码以后想改成全异步Servlet模型,Jetty也支持得很顺。虽然这个项目用不到,但保留这个扩展空间不亏。

1.3 WebSocket 在 Jetty 里是怎么干活儿的

要理解后面的配置,先得大概知道一次WebSocket连接在嵌入式Jetty里怎么建立。浏览器发起的是一个“带Upgrade请求头的HTTP请求”,Jetty收到之后发现这个请求不是普通静态资源,而是希望升级成WebSocket,于是把连接交给对应的WebSocket处理器,底层Socket就直接接管,双方后续可以互发文本帧或二进制帧。

在这个过程中,Spring Boot起的作用是搭好Servlet容器,Spring WebSocket做的是抽象层封装。我们代码里写的WebSocketHandler,最终会被Spring适配到Jetty的WebSocket实现上。这样即使用了Jetty,业务代码还是面向Spring的接口在写,而不是直接跟Jetty的原生类打交道,可维护性会好很多。

在选型上我最终采用了Spring的WebSocketHandler方案,而不是Jetty原生注解,原因是后面还要结合Spring MVC的拦截器做用户登录态传递,原生API当然也能做,但用Spring的抽象省得自己处理一堆底层状态。

2. 环境准备与基础配置

2.1 依赖到底怎么配

整个工程只需要一个普通Spring Boot Maven项目,不需要额外安装任何WebSocket服务。依赖部分最关键的是三件事:引入Spring WebSocket的starter、去掉默认Tomcat、换上Jetty。

我用的依赖配置大致如下:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> </dependencies>

注意一点,如果项目里还引了spring-boot-starter-webflux,WebSocket依赖的包可能冲突,因为WebFlux和Spring MVC不能同时占用一个Web容器。我调研时踩到过这个坑,后来整个工程删掉WebFlux相关依赖就安静了。正常做REST接口,引入spring-boot-starter-web就够了,别再画蛇添足加别的。

2.2 Jetty 相关配置项

在application.yml里其实不用写太多东西。Jetty的默认端口仍然是8080,如果想改端口就写server.port。不过我对连接配置做了一点调整,避免测试时连接被空闲超时干掉:

server: port: 8080 jetty: threads: acceptors: 2 selectors: 4 max-connections: 200

这个配置只是把Jetty的线程模型稍微调一下。acceptors负责接收TCP连接,selectors负责处理网络事件。对于WebSocket这种长连接服务,selectors数量可以适当比acceptors多一些。如果你不确定要配多少,直接用默认值也没问题,小项目根本碰不到瓶颈。

2.3 确认容器已经切换成功

配置完成之后,启动项目时留意日志。如果启动日志里出现了Jetty相关的标志字样,并且在监听8080端口,那说明切换成功。如果日志还是Tomcat那一套,说明排除Tomcat依赖没生效,去看依赖树,把可能从传递依赖里冒出来的Tomcat也一并排除掉。

有些人会在这里打包出错,原因是当前环境里可能设置了系统的CATALINA_HOME之类,但这跟Spring Boot内嵌容器没关系,别看岔了方向。排查时直接跑mvn dependency:tree,确认spring-boot-starter-tomcat不在列表里,基本就稳了。

3. 服务端核心通信逻辑

3.1 先决定用什么方式写服务端

服务端写WebSocket接收端,最少有两种姿势:

  • 用JSR-356的@ServerEndpoint注解:这是Java标准里的WebSocket端点写法,Jetty天生支持。类上标一个@ServerEndpoint("/ws/demo"),再写@OnOpen、@OnMessage、@OnClose方法,就完事了。这套API很轻,不需要额外注册,Jetty会在启动时自动扫描加载。
  • 用Spring的WebSocketHandler接口:需要继承TextWebSocketHandler,然后通过WebSocketConfigurer手动注册端点。优点是能跟Spring容器无缝整合,Handler里可以直接注入Service,也能走Spring拦截器。

两种方式我都在项目里跑过。如果你只想做最快的Demo,@ServerEndpoint在Jetty下确实最省事,甚至连starter-websocket都不太需要。但是一旦想拿Spring的注入、拦截、消息监听那一套,就会发现在JSR-356的类里注入Spring Bean很绕,需要自己写一个全局的ApplicationContext工具。

我最后选的是Spring方案,原因很实际:这类小项目的下一步十有八九是接数据库、接鉴权、接业务Service,与其后面从标准API切到Spring,不如一开始就用Spring封装。下面所有内容也以这个方案为主。

3.2 WebSocketHandler 具体实现

Spring里最常用的处理器是TextWebSocketHandler,它默认只处理文本消息,跟我们的JSON通信场景刚好吻合。如果以后要传图片、文件分片,可以换BinaryWebSocketHandler或者重写handleMessage。

一个简单的处理器核心代码是这样的:

package com.example.demo.websocket; import java.util.concurrent.CopyOnWriteArraySet; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; import org.springframework.stereotype.Component; @Component public class DeviceDataHandler extends TextWebSocketHandler { // 用一个线程安全的集合保存所有在线会话 private final CopyOnWriteArraySet<WebSocketSession> sessions = new CopyOnWriteArraySet<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { super.afterConnectionEstablished(session); sessions.add(session); System.out.println("连接建立:" + session.getId() + ",当前在线数:" + sessions.size()); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload = message.getPayload(); System.out.println("收到消息:" + payload); // 这里简单回显,实际会解析payload后执行对应业务 if (session.isOpen()) { session.sendMessage(new TextMessage("服务端已收到:" + payload)); } } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { sessions.remove(session); System.out.println("连接关闭:" + session.getId() + ",状态:" + status); } }

这里面有个关键点:WebSocketSession在建立连接后,底层Socket可能随时被关闭,所以发送消息前一定要判断session.isOpen()。如果要给所有客户端广播,要遍历sessions集合逐个发,一个客户端发送异常不能影响其他客户端,最好把每个发送动作都放到try-catch里。

3.3 端点注册与跨域配置

光有Handler还不够,得让Spring把“/ws/device”这个路径映射到Handler上。这一步通过实现WebSocketConfigurer接口完成:

package com.example.demo.websocket; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { private final DeviceDataHandler deviceDataHandler; public WebSocketConfig(DeviceDataHandler deviceDataHandler) { this.deviceDataHandler = deviceDataHandler; } @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(deviceDataHandler, "/ws/device") .setAllowedOrigins("*"); } }

setAllowedOrigins("*") 表示允许任何来源连接。生产环境不建议这么写,要限制成自己前端的域名,否则别人可以随便连你的WebSocket端口。但开发阶段这个配置能省掉一大半跨域问题。如果前端和后端不同端口,不设置AllowedOrigins,浏览器控制台会报跨域错误,而服务端日志还看不出明显异常,非常误导人。

到这里,后端接收端点其实已经能跑了。启动项目后,前端就可以尝试连接ws://localhost:8080/ws/device。

3.4 广播与点对点发送怎么实现

在很多实时场景里,不只是“谁发消息回给谁”,还需要把一条消息推送给所有人。比如设备状态变化要通知所有监控页面,就是典型的广播。我上面的代码里,DevDataHandler里有一个CopyOnWriteArraySet专门保存所有在线session,广播方法可以这样写:

public void broadcast(String jsonMessage) { for (WebSocketSession session : sessions) { if (session.isOpen()) { try { synchronized (session) { session.sendMessage(new TextMessage(jsonMessage)); } } catch (Exception e) { // 记录日志,但不要中断循环 e.printStackTrace(); } } } }

这里我用了synchronized块,原因是同一个session可能被多个线程触发发送,如果不做同步,Jetty内部可能会报“Cannot call sendMessage() while another sendMessage() is in progress”之类的错误。这个错误挺常见,尤其是自己起了一个定时器线程往页面推数据,页面又时不时代发表单消息给服务端,两个线程抢同一个session,不锁就会翻车。

3.5 业务线程怎么主动推送消息

现在后端Handler只是“被动”接收消息,那“主动”推送设备状态变化该怎么做?我的做法是用Spring的@Scheduled定时任务配合广播方法。

比如模拟一个“每隔3秒产生一次设备状态”的服务:

package com.example.demo.service; import java.time.LocalTime; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import com.example.demo.websocket.DeviceDataHandler; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @Service @EnableScheduling public class DeviceStatusService { private final DeviceDataHandler handler; private final ObjectMapper objectMapper = new ObjectMapper(); private final AtomicInteger counter = new AtomicInteger(0); public DeviceStatusService(DeviceDataHandler handler) { this.handler = handler; } @Scheduled(fixedRate = 3000) public void pushStatus() { try { Map<String, Object> data = new HashMap<>(); data.put("type", "STATUS"); data.put("time", LocalTime.now().toString()); data.put("counter", counter.incrementAndGet()); data.put("status", "RUNNING"); handler.broadcast(objectMapper.writeValueAsString(data)); } catch (Exception e) { e.printStackTrace(); } } }

这里用到了Jackson把Map转成JSON字符串,前端收到的就是标准JSON。代码里每次推送都通过同一个Handler实例广播,所有在线的页面都能收到这份数据。

注意一点:@Scheduled默认是单线程调度器,如果只有一个定时任务,直接把Method改成无参public即可。如果项目里有多个定时任务,建议在配置类上指定线程池大小,否则两个任务都到时间时只能排队跑。这个不属于WebSocket本身的问题,但很多人写到这里会困惑为什么定时推送不按时执行。

3.6 如果要用 JSR-356 原生的写法

前面说了,如果只是想验证Jetty对WebSocket的原生支持,还可以用@ServerEndpoint快速写一个。这里给一个极简示例作为备选,它不需要继承Spring的Handler,直接在类上标注即可:

package com.example.demo.websocket; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; @ServerEndpoint("/ws/native") public class NativeEchoEndpoint { @OnOpen public void onOpen(Session session) { System.out.println("原生端点连接:" + session.getId()); } @OnMessage public String onMessage(String message, Session session) { return "echo:" + message; } @OnClose public void onClose(Session session) { System.out.println("原生端点关闭:" + session.getId()); } }

Jetty会自动扫描这种类,前端连接ws://localhost:8080/ws/native就能用。不过这里我要提醒一下,如果你同时在项目里配了Spring的WebSocketConfigurer,又在类上写@ServerEndpoint,注意两者路径别冲突。之前我把端点都设计成了同一个路径,启动时没报错,但前端连接时只会走到其中一个处理器上,排查了半天才找到原因。

所以我的建议是:一个项目里选定一种方案,要么全部走Spring抽象,要么全部走JSR-356原生API,混着用很容易精神分裂。

4. 前端通信与联调测试

4.1 写一个最简单的前端页面

前端我直接用原生JavaScript的WebSocket对象,没有引任何框架。这样能把逻辑说得最清楚。工具页面大致长这样:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>Jetty WebSocket Demo</title> </head> <body> <h2>WebSocket 测试页</h2> <div> <input type="text" id="messageInput" placeholder="输入要发送的内容"> <button onclick="sendMessage()">发送</button> <button onclick="connect()">重新连接</button> </div> <ul id="messageList"></ul> <script> let ws = null; function connect() { if (ws) { ws.close(); } ws = new WebSocket("ws://" + window.location.host + "/ws/device"); ws.onopen = function () { appendMsg("连接已建立"); }; ws.onmessage = function (event) { appendMsg("收到:" + event.data); }; ws.onerror = function (event) { appendMsg("发生错误,详见控制台"); }; ws.onclose = function (event) { appendMsg("连接关闭,code=" + event.code); }; } function sendMessage() { const input = document.getElementById("messageInput"); if (ws && ws.readyState === WebSocket.OPEN) { ws.send(input.value); appendMsg("发送:" + input.value); } else { appendMsg("连接尚未建立"); } } function appendMsg(text) { const li = document.createElement("li"); li.textContent = text; document.getElementById("messageList").appendChild(li); } connect(); </script> </body> </html>

这个页面可以直接放在src/main/resources/static目录下。因为示例工程里还有Spring MVC,直接访问http://localhost:8080/static.html就能打开页面,后端也是同一个端口,所以我在new WebSocket时直接拼接了不太完整的地址——按需修改即可,比如把窗口位置的host换成后端实际的IP和端口。

前端有几个状态值得记住:WebSocket.CONNECTING=0,OPEN=1,CLOSING=2,CLOSED=3。发送数据前必须检查readyState是不是OPEN,否则浏览器会直接抛异常。connect()函数里第一次关掉旧的ws时,不要立刻又开新的,最好在onclose回调里再执行重连逻辑,否则会有竞态问题。

4.2 浏览器控制台怎么验证

写完页面后,最直接的验证方式是打开浏览器开发者工具,切到Network面板,刷新页面看WebSocket连接请求。点击那个连接名称,能看到Status Code是101 Switching Protocols,这表示握手成功。如果看到404或者426,那问题基本都在路径或升级请求处理上。

如果不想写前端,也可以用浏览器控制台直接执行:

let socket = new WebSocket("ws://localhost:8080/ws/device"); socket.onmessage = function (e) { console.log(e.data); }; socket.send('hello');

这个办法适合快速测试,不需要刷新整个页面。我在联调时经常开两个浏览器Tab,一个做发送端,一个做接收端,同时开着开发者工具的Network面板看帧数据,调试效率很高。

4.3 怎么判断数据是JSON还是普通文本

后端推送的数据里我带头了type字段和time字段,这样前端可以根据type走不同分支。比如“STATUS”渲染到状态区,“LOG”追加到日志列表。前端处理时不要直接JSON.parse所有消息,因为有时候服务端会先发一条连接成功提示,内容可能不是合法JSON。稳妥做法是先根据业务前缀或头字段判断。

我在这个Demo里把“发送”和“广播”两种消息混在同一个连接上,不算规范。正式项目应该设计成事件类型字段,例如:

{"type":"STATUS","payload":{...}}

客户端解析时统一取type,再取payload,代码结构会整洁很多。WebSocket本身不关心消息是JSON还是纯字符串,所以这层协议设计要靠自己约束。

5. 联调排错与实战问题

5.1 常见问题速查表

把一个多小时的调试过程压缩一下,这些问题是最高频的:

现象可能原因解决办法
浏览器显示WebSocket握手失败,状态码404端点路径不对,Handler没有注册成功核对注册路径,确认前端连接的路径完全一致
握手失败,状态码426 Upgrade RequiredJetty没有启用WebSocket支持,或者Spring Boot版本匹配问题检查stater依赖,确认没有把spring-boot-starter-websocket漏掉
浏览器报跨域错误没配置setAllowedOrigins开发阶段设为"*",生产环境改为具体域名
连接建立后马上断开服务端有异常未被捕获查看服务端日志,特别关注onOpen和handleTextMessage里的错误
能收到消息但发不出去服务端发送前没有判断session.isOpen,或session被并发占用发送前判断isOpen,必要时用synchronized包裹sendMessage
同一浏览器访问两个页面只有一个页面收得到不同Tab连接被复用,或者页面脚本事件覆盖每个页面单独new WebSocket,检查代码里是否共用变量

这表不能覆盖所有情况,但排查时先对照一遍,能省下很多无用功。

5.2 连接为什么总是被断开

开发时经常遇到前端刚打开几秒钟,连接就自动关闭。常见原因之一就是空闲超时。Jetty对WebSocket默认设置了空闲时间,如果一个连接在一段时间内没有收发任何帧,容器就主动断开。

如果你不想让连接被闲置回收,可以在Handler注册时或者通过容器工厂配置。Spring Boot里可以通过注册ServletServerContainerFactoryBean来调整:

@Bean public ServletServerContainerFactoryBean createWebSocketContainer() { ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); container.setMaxTextMessageBufferSize(8192); container.setMaxBinaryMessageBufferSize(8192); container.setMaxSessionIdleTimeout(60000L); return container; }

这里把空闲超时设置成了60秒。注意如果前端也设置了超时,比如Nginx或网关层的proxy_read_timeout很短,服务端再怎么放长也没用,前端和网关也要同步调大。

5.3 心跳机制怎么设计

对于生产环境,单纯把空闲超时调大并不是最好的方案。更主动的做法是加心跳。服务端定期发一个ping消息,前端收到后回pong,这样连接就一直保持活跃状态。

如果前端用的是浏览器原生WebSocket,它收到ping帧会由底层自动回pong,写JavaScript代码时甚至看不到这个过程。但很多WebSocket库或测试工具不会自动回pong,所以应用层要设计一套自己的心跳更稳。

我在这里的做法是:前端每30秒发送一个{ "type": "HEARTBEAT" }消息,服务端在handleTextMessage里识别到这个type后,直接返回一个{ "type": "HEARTBEAT_ACK" },其余业务消息照常处理。这样即使底层没有自动pong回包,也能保证链路是通的。

心跳间隔要小于Jetty的空闲超时时间,否则心跳还没发出去连接就被断了。一般设置为空闲超时的一半比较合适。

5.4 CopyOnWriteArraySet 和 Session 泄漏问题

在线Session集合我用了CopyOnWriteArraySet,因为并发场景下读多写少,这个集合遍历和发送时很安全。但它也有代价:每次写入都会复制整个底层数组。对于动辄几万人的在线列表来说不划算,但对这个项目几千上限完全没问题。

Session泄漏是很坑的一件事。如果客户端直接拔网线,不经过正常的TCP FIN包,服务端onClose可能不会立刻触发,已经断开的session会一直留在集合里。解决办法有两个:

  • 定期遍历集合,检查session.isOpen(),把关闭的会话移除。
  • 在广播发送时,如果发送异常且类型是Session Closed之类的错误,立即从集合里移除。

我自己的做法是两套都上:定时任务每5分钟扫一次,清理兜底;广播发送遇到异常时顺手移除有问题会话。这样能最大程度避免“僵尸Session”越积越多。

5.5 代理服务器和网关层要注意的点

如果项目不是内网直连,而是经过Nginx一类的反代服务转发,这就特别容易出幺蛾子。WebSocket握手时有个关键请求头:

Upgrade: websocket Connection: Upgrade

如果网关把这两个头吞掉或者改写,后端收到的就不是一个正常的升级请求,握手必然失败。所以在Nginx配置里需要显式设置长短连接协议相关的头转发支持,并且把超时时间适当调大,否则就算握手成功,过一会儿也会被网关正常关闭。

我在本地调试时没遇到这个问题,一到联调环境就频繁断开,最后查出来不是后端代码的问题,而是网关层超时太短。这类问题你从后端日志看,只有一句“connection closed”,非常难定位,所以先确认链路里有没有代理层。

5.6 日志里到底该打哪些点

我建议在Handler里把这几处信息打上日志:连接建立、收到业务消息、连接关闭、发送异常。尤其连接关闭时要打印closeCode和reason,这比一堆堆栈更容易定位问题。

如果用Spring框架,别直接用System.out.println,统一用logger。但在这个Demo里为了减少配置,我就用了系统输出,如果是正式项目建议换成logback或者log4j2。WebSocket连接的日志量会比较大,每次收发都打全量内容可能把磁盘写爆,生产环境只打会话状态和异常,业务消息可以拉到debug级别。

6. 一些扩展建议与个人心得

如果你只是想跑通一个Demo,看到网页上能实时收到后端定时推送的内容,到这里已经完成了。但如果你打算把这段代码继续往正式项目里推,我劝你先想清楚这几件事。

第一,这个方案没有鉴权机制。WebSocket握手本质上还是一个HTTP请求,所以可以复用Spring MVC里的Interceptor去做登录校验。在registerWebSocketHandlers方法里加上.addInterceptors,或者直接配置HandshakeInterceptor,在校验失败时返回false,连接就会被拒掉。我实际做的时候,发现很多人会忽略这一步,导致别人能直接连上ws端口拿数据,这等于开了一个后门。

第二,消息格式要尽早规范。我见过项目上线后把聊天内容和业务指令混在一起发,前端解析逻辑越写越乱。最好一上来就约定一个通用消息结构,比如包含type、requestId、payload、timestamp,后面加功能就只需要新增type,不用改框架。前期花半天定义协议,后期能省几天。

第三,Jetty本身很稳,但你要知道它默认对WebSocket的限制。文本消息缓冲区大小、二进制消息缓冲区大小、最大连接数、空闲时间,这些默认值不一定适合你的业务。当业务方反馈“图片传不上来”或“长文本被截断”时,先去查ServletServerContainerFactoryBean里的缓冲区配置,别一开始就怀疑代码逻辑。

第四,不要指望WebSocket是万能的。它是长连接,这就意味着服务器端要一直维护在线状态,连接数上来之后内存和CPU开销都会增加。小并发下完全没有问题,但如果你预期会有数万连接,就需要考虑分布式方案,比如把session信息放到Redis里,或者借助消息中间件做广播。到那一步,单机Jetty已经不够了,重点是API设计要提前考虑到横向扩展。

最后分享一个我自己写代码时的习惯:无论多简单的WebSocket项目,我都会在Handler里维护一个“最后活跃时间”字段,并定时清理超时连接。这样做的好处不只是防僵尸连接,还能帮你发现前端有没有悄悄断线重连。调试的时候你打印一下“在线数=xxx,当前时间=yyy,最后活跃=zzz”,很多摸不着头脑的缺数据问题都能一眼看到规律。

这次用Jetty跑WebSocket的经历,整体比我预想的顺畅。只要把依赖切换、Handler注册、跨域配置这三板斧做对,后面的业务逻辑都是水到渠成的事。如果你正卡在某个握手或断开问题上,建议先按我第5节给的排查顺序走一遍,多半能解开。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询