哪些网站可以做海报热点的做追星网站效果图
哪些网站可以做海报热点的,做追星网站效果图,jsp网站建立,wordpress4.9.4漏洞1. 为什么需要实时推送#xff1f;从轮询到SSE的进化之路
大家好#xff0c;我是老张#xff0c;在后台开发这块摸爬滚打了十几年。今天想和大家聊聊一个老生常谈但又常聊常新的话题#xff1a;服务端如何主动把数据“推”给浏览器。相信很多朋友都遇到过这样的需求#x…1. 为什么需要实时推送从轮询到SSE的进化之路大家好我是老张在后台开发这块摸爬滚打了十几年。今天想和大家聊聊一个老生常谈但又常聊常新的话题服务端如何主动把数据“推”给浏览器。相信很多朋友都遇到过这样的需求做一个后台任务进度展示或者做一个股票价格实时看板又或者是做一个在线客服的聊天界面。核心问题都一样前端怎么知道后端的数据变了最早我们用的办法叫“轮询”。这就像你每隔5分钟就跑去问一下快递员“我的快递到了吗”简单粗暴但问题也很明显大部分时候你问的都是废话快递根本没到。这不仅浪费你的体力前端资源也烦快递员服务器资源。后来有了“长轮询”就是你问一次快递员说“没到但你等着到了我马上告诉你”然后他拿着电话不挂断直到快递到了才回复你。这比短轮询好点但连接一直占着对服务器压力也不小。直到出现了Server-Sent Events也就是我们今天的主角SSE。它相当于快递公司给你开通了一个专属的广播频道。快递一旦出发途经每一个站点都会自动在这个频道里给你发一条语音通知“您的快递已从北京发出”、“您的快递已到达上海分拣中心”……你完全不用再主动去问只需要打开收音机建立一个连接听着就行。这个方案才是真正的“服务端推送”。在Spring Boot生态里实现这个“广播频道”的神器就是SseEmitter。它用起来非常顺手可以说是构建轻量级实时功能的首选。我经历过从自己手写Comet到用WebSocket再到用SSE的整个过程实测下来对于通知、进度更新、实时数据展示这类场景SSE的简单和高效是无可替代的。接下来我就带大家从零开始彻底玩转SseEmitter。2. 五分钟快速上手你的第一个SseEmitter接口光说不练假把式咱们直接上代码。我会假设你有一个正在运行的Spring Boot 2.x 或 3.x 项目已经引入了基础的Web依赖。2.1 核心依赖与最小化实现首先确保你的pom.xml里有Spring Boot的Web起步依赖这个基本上创建项目时都会选上dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency接下来我们创建一个最简单的控制器。这个接口的功能是一旦有浏览器连接上来就每隔1秒向它推送一条消息一共推送5条。import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; RestController RequestMapping(/api/sse-demo) public class BasicSseController { GetMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter streamEvents() { // 创建一个SseEmitter对象参数0代表永不超时 SseEmitter emitter new SseEmitter(0L); // 启动一个线程来模拟数据推送避免阻塞HTTP请求线程 new Thread(() - { try { for (int i 1; i 5; i) { // 构造并发送一条消息 // 这里用 event().data() 包装是标准做法 emitter.send(SseEmitter.event() .id(String.valueOf(i)) // 可选设置事件ID .name(message) // 可选设置事件名 .data(这是第 i 条消息 System.currentTimeMillis())); // 模拟处理耗时 Thread.sleep(1000); } // 所有数据发送完成标记流结束 emitter.complete(); } catch (Exception e) { // 发生异常以错误形式结束流 emitter.completeWithError(e); } }).start(); // 立即返回SseEmitter对象给Spring MVC框架 return emitter; } }我来拆解一下这段代码的关键点produces MediaType.TEXT_EVENT_STREAM_VALUE这是SSE的“身份证”告诉浏览器这个接口返回的是一个事件流而不是普通的JSON或HTML。浏览器会根据这个头信息以特殊方式处理响应。SseEmitter emitter new SseEmitter(0L)创建发射器。参数是超时时间毫秒0表示永不超时。你也可以设置为30 * 100030秒超过这个时间没有新数据连接会自动关闭。emitter.send()这是推送数据的核心方法。我们一般用SseEmitter.event()来构建一个事件可以链式设置id、name和data。data就是我们要推送的实际内容。新线程推送数据推送通常是个耗时或持续的过程我们必须在一个新线程里做否则会阻塞当前处理请求的Tomcat线程导致接口无法及时返回emitter对象。complete()与completeWithError()这是礼貌地告诉前端数据发完了或者中途出错了你可以关闭连接了。2.2 如何测试这个接口你不需要写任何前端代码用最直观的工具来测试。我强烈推荐使用命令行工具curl它能最纯净地展示SSE流是什么样子。打开你的终端运行curl -N http://localhost:8080/api/sse-demo/stream你会看到类似这样的输出数据一条一条地“流”出来id:1 event:message data:这是第 1 条消息 1646389470234 id:2 event:message data:这是第 2 条消息 1646389471235 ...这就是SSE协议的标准格式每一条消息由data:开头的内容以及可选的id:、event:等字段组成以两个换行符\n\n分隔。浏览器端的EventSourceAPI 就是自动解析这种格式的。如果你更喜欢图形化界面可以用Postman。新建一个GET请求输入上面的URL发送后你会看到响应体那里在不停地加载并一段段地显示接收到的数据。这就证明你的推送服务跑起来了3. 前后端联调让网页“活”起来后端接口跑通了我们得给它配个“显示器”。前端用JavaScript的EventSource对象来接收SSE流简单到不可思议。3.1 前端代码一个简单的HTML页面创建一个index.html文件放在Spring Boot项目的src/main/resources/static/目录下这样Spring Boot就能直接把它当静态资源提供服务。!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleSSE实时数据看板/title style body { font-family: sans-serif; margin: 40px; } #messageContainer { border: 1px solid #ccc; padding: 20px; height: 400px; overflow-y: auto; margin-top: 20px; background-color: #f9f9f9; } .message { padding: 8px; margin-bottom: 5px; border-left: 4px solid #4CAF50; background-color: white; } /style /head body h1 实时消息推送演示/h1 button onclickconnectSSE()开始连接/button button onclickcloseSSE()断开连接/button div idmessageContainer/div script let eventSource null; function connectSSE() { // 如果已存在连接先关闭 if (eventSource) { eventSource.close(); } // 创建 EventSource 对象连接到我们的SSE端点 eventSource new EventSource(/api/sse-demo/stream); // 监听未命名事件默认事件对应后端 send(event().data(...)) eventSource.onmessage function(event) { console.log(收到数据:, event.data); addMessageToPage(event.data); }; // 监听特定命名事件对应后端 send(event().name(myEvent).data(...)) eventSource.addEventListener(myEvent, function(event) { console.log(收到myEvent事件:, event.data); addMessageToPage(自定义事件: event.data); }); // 监听连接打开事件 eventSource.onopen function() { console.log(SSE连接已建立); addMessageToPage(系统连接服务器成功, system); }; // 监听错误事件 eventSource.onerror function(error) { console.error(SSE连接错误:, error); addMessageToPage(系统连接出错或已断开。, error); // 发生错误时EventSource会自动尝试重连。如果想关闭可以在这里调用 eventSource.close() }; } function closeSSE() { if (eventSource) { eventSource.close(); eventSource null; addMessageToPage(系统已主动断开连接。, system); console.log(SSE连接已关闭); } } function addMessageToPage(msg, type info) { const container document.getElementById(messageContainer); const msgElement document.createElement(div); msgElement.className message; msgElement.textContent [${new Date().toLocaleTimeString()}] ${msg}; container.appendChild(msgElement); // 自动滚动到底部 container.scrollTop container.scrollHeight; } // 页面加载时自动连接可选 window.onload connectSSE; /script /body /html3.2 关键点解析与调试技巧这段前端代码有几个值得注意的地方new EventSource(‘/api/sse-demo/stream’)这里用的是相对路径因为HTML页面和API在同一个域名/端口下。如果前后端分离部署你需要填写完整的后端API地址。自动重连这是EventSource的一大优点。如果网络波动导致连接断开它会自动尝试重新连接。你可以通过eventSource.close()手动关闭关闭后就不会自动重连了。事件监听onmessage是监听默认事件。如果你在后端发送时指定了event().name(“update”)那么前端就需要用addEventListener(‘update’, …)来监听。查看连接状态打开浏览器的开发者工具F12进入“网络”Network选项卡找到类型为“事件流”EventStream的请求点进去可以看到实时流动的事件和数据这是调试SSE的利器。现在启动你的Spring Boot应用在浏览器访问http://localhost:8080/index.html点击“开始连接”你就能看到消息一条条地从服务器“流”到网页上了页面无需任何刷新。这种体验比之前用setInterval轮询不知道高到哪里去了。4. 进阶实战处理复杂业务与性能优化简单的定时推送大家都会了但真实项目往往更复杂。比如推送的数据来自一个耗时的外部API调用比如调用大模型生成一段长文本或者需要管理成千上万个连接。下面我就分享几个实战中的进阶玩法。4.1 场景流式推送大模型生成结果这是当前非常普遍的需求用户提问后端调用大模型API模型是一个字一个字地返回结果流式输出我们需要把这个“字流”实时推送给前端营造出打字机效果。这里的关键是后端不能等大模型全部生成完了再一次性推送而是要来一点数据就推一点。我们模拟这个场景。首先假设我们有一个慢速的“数据生产者”比如一个每100毫秒生成一个字符的模拟服务Service public class SlowDataProducerService { /** * 模拟一个慢速的流式数据源比如大模型API * param prompt 输入提示 * return 一个缓慢生成的字符串流 */ public String generateStreamData(String prompt) throws InterruptedException { StringBuilder result new StringBuilder(); String simulatedResponse 这是一个模拟大模型流式返回的文本。它需要一点时间才能生成完毕。; for (char c : simulatedResponse.toCharArray()) { result.append(c); Thread.sleep(100); // 模拟每个字符生成的延迟 } return result.toString(); } }然后我们改造控制器让它能够处理这种“流式源”RestController RequestMapping(/api/sse-advanced) public class AdvancedSseController { Autowired private SlowDataProducerService dataProducer; GetMapping(value /stream-ai, produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter streamAiResponse(RequestParam String question) { SseEmitter emitter new SseEmitter(60_000L); // 设置60秒超时 // 使用线程池提交任务而不是每次都new Thread CompletableFuture.runAsync(() - { try { String streamData dataProducer.generateStreamData(question); // 关键将生成的结果按字符拆分并流式推送 for (int i 0; i streamData.length(); i) { char character streamData.charAt(i); // 每次推送一个字符 emitter.send(SseEmitter.event() .data(String.valueOf(character)) // 可以添加注释帮助前端区分数据块 .comment(字符流推送)); // 稍微延迟控制推送速度避免前端渲染不过来 Thread.sleep(50); } // 推送完成标识 emitter.send(SseEmitter.event().name(complete).data(生成结束)); emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); } }); // 设置连接超时和错误回调 emitter.onTimeout(() - { System.out.println(SSE连接超时客户端可能已断开。); emitter.complete(); }); emitter.onError((ex) - { System.out.println(SSE连接发生错误: ex.getMessage()); emitter.complete(); }); return emitter; } }前端只需要稍作修改将接收到的字符拼接起来即可实现“打字机”效果。这个模式完美契合了调用OpenAI GPT、文心一言等大模型流式接口的场景。4.2 性能与资源管理连接池与广播当用户量上来一个服务实例可能同时维持着几百上千个SSE长连接。管理不好服务器内存和线程资源很快就会耗尽。第一使用线程池。绝对不要为每个推送任务都new Thread()。上面例子中我用了CompletableFuture.runAsync()它使用的是ForkJoinPool公共池。对于更严肃的场景你应该配置一个专用的线程池import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; Service public class SseExecutorService { // 创建一个固定大小的线程池避免无限制创建线程 private final ExecutorService sseExecutor Executors.newFixedThreadPool(50); public ExecutorService getExecutor() { return sseExecutor; } }然后在控制器中注入并使用这个sseExecutor来提交任务。第二实现连接管理与广播。很多时候我们需要向所有在线用户或者某个特定分组的所有用户广播消息比如系统公告、聊天室消息。这就需要我们维护一个SseEmitter的集合。Component public class SseConnectionManager { // 使用线程安全的Map来存储用户ID和其对应的SseEmitter private final ConcurrentHashMapString, SseEmitter emitters new ConcurrentHashMap(); /** * 添加新的连接 */ public SseEmitter add(String userId, SseEmitter emitter) { // 设置超时和错误处理移除无效连接 emitter.onTimeout(() - { System.out.println(用户 userId 的连接超时); emitters.remove(userId); }); emitter.onError((e) - { System.out.println(用户 userId 的连接错误: e.getMessage()); emitters.remove(userId); }); emitter.onCompletion(() - { System.out.println(用户 userId 的连接完成); emitters.remove(userId); }); emitters.put(userId, emitter); return emitter; } /** * 根据用户ID获取连接 */ public SseEmitter get(String userId) { return emitters.get(userId); } /** * 移除连接 */ public void remove(String userId) { emitters.remove(userId); } /** * 向所有在线用户广播消息 */ public void broadcast(String eventName, Object data) { ListString deadUserIds new ArrayList(); emitters.forEach((userId, emitter) - { try { emitter.send(SseEmitter.event().name(eventName).data(data)); } catch (Exception e) { // 发送失败说明连接已失效 deadUserIds.add(userId); } }); // 清理失效连接 deadUserIds.forEach(emitters::remove); } /** * 获取当前在线用户数 */ public int getOnlineCount() { return emitters.size(); } }在控制器里用户登录或访问特定页面时创建一个SseEmitter并注册到SseConnectionManager中。当有需要广播的消息时比如来自管理后台的触发调用broadcast方法即可。这样一个轻量级的实时广播系统就搭建起来了。5. 避坑指南我踩过的那些“坑”用了这么多年SseEmitter有些坑是只有踩过才知道疼。这里我总结几个最常见的希望大家能绕过去。第一个坑连接超时与心跳机制。虽然我们创建SseEmitter时可以设置超时时间但网络环境复杂中间可能有代理、负载均衡器它们对长连接的保持时间有更严格的限制比如Nginx默认的proxy_read_timeout是60秒。解决方案是实现心跳。定期比如每30秒向前端发送一条注释消息comment或空数据事件来保持连接活跃。// 在推送线程中 ScheduledExecutorService scheduler Executors.newScheduledThreadPool(1); scheduler.scheduleAtFixedRate(() - { try { emitter.send(SseEmitter.event().comment(heartbeat)); } catch (Exception e) { scheduler.shutdown(); // 发送失败停止心跳连接可能已断 } }, 0, 30, TimeUnit.SECONDS); // 立即开始每30秒一次第二个坑数据格式与编码。emitter.send()方法发送的data最终会被转换成字符串。如果你发送的是一个复杂的Java对象Spring默认会使用Jackson序列化成JSON字符串。这很好但你要确保前端能正确解析。更稳妥的做法是直接发送JSON字符串。MapString, Object dataMap new HashMap(); dataMap.put(progress, 75); dataMap.put(message, 处理中...); dataMap.put(timestamp, System.currentTimeMillis()); ObjectMapper mapper new ObjectMapper(); String jsonData mapper.writeValueAsString(dataMap); emitter.send(SseEmitter.event().data(jsonData));同时要特别注意中文乱码问题。确保你的Spring Boot应用配置了正确的字符编码UTF-8通常全局配置在application.properties中spring.http.encoding.charsetUTF-8。第三个坑前端连接断开与重连。前端的EventSource在连接错误时会自动重连但重连时可能会遇到问题。比如你的后端接口需要认证而重连时可能不会自动带上认证信息因为EventSource不支持自定义Header这是它的一个局限。对于需要认证的场景更常见的做法是使用WebSocket。或者在建立SSE连接时使用一个一次性的Token作为查询参数如/stream?tokenxxx后端通过Token验证用户身份。前端在重连时需要重新获取Token如果Token过期。第四个坑并发修改与线程安全。当你像我们上面那样维护一个全局的ConcurrentHashMap来管理连接时在广播消息的forEach循环中发送数据如果某个send方法内部触发了onCompletion或onError回调回调里又会去Map中移除这个连接就可能引发ConcurrentModificationException。我的经验是在广播时先复制一份Emitter的集合副本或者像上面broadcast方法那样先记录下发送失败的连接ID最后统一清理。把这些坑都填平你的SseEmitter服务基本就非常稳健了。从简单的进度条到复杂的实时数据大屏、通知中心、简易聊天室SSE都能很好地胜任。它没有WebSocket那么“重”不需要额外的协议升级就是简单的HTTP利用起来非常顺手。下次当你再有“服务器主动告诉浏览器点事儿”这种需求时别再犹豫了试试SseEmitter吧。