Spring Boot + WebSocket + Vue 实现 Ollama 流式会话功能
在当今的 Web 应用开发中,实时通信和流式数据处理变得越来越重要。本文深入探讨了使用 Spring Boot 作为后端,结合 WebSocket 技术和 Vue 前端框架来实现与 Ollama 的流式会话。
技术选型与简介
-
Spring Boot
Spring Boot 是建立在强大的 Spring 框架之上的一种创新型开发框架。它通过预设的默认配置和自动化的配置管理,极大地简化了 Spring 应用的创建和部署流程。开发人员不再需要花费大量时间去处理繁琐的配置文件,而是能够迅速启动项目并专注于核心业务逻辑的实现。其丰富的 starter 依赖库,使得引入各种常用技术组件变得轻松快捷,为构建高效、稳定的后端服务提供了坚实基础。
关键特性包括自动配置、嵌入式服务器支持(如 Tomcat、Jetty 等)、健康检查和监控端点、强大的依赖管理等。这些特性使得开发人员能够更高效地开发、测试和部署应用,同时也降低了出错的概率。
对于企业级应用开发来说,Spring Boot 提供了出色的可扩展性和集成性,可以轻松与各种数据库、消息队列、缓存系统等进行集成,满足复杂业务场景的需求。
-
WebSocket
WebSocket 是一种革命性的通信协议,它打破了传统 HTTP 请求-响应模式的限制。通过建立一个持久的全双工连接,使得服器能够实时主动地向客户端推送数据,而客户端也可以随时向服务器发送数据,实现了真正意义上的实时双向通信。
相较于传统的轮询或长轮询技术,WebSocket 显著降低了网络开销和延迟,提高了数据传输的效率和实时性。它适用于需要实时更新数据的场景,如在线聊天、实时游戏状态同步、金融行情实时推送等。
在安全性方面,WebSocket 通常与 SSL/TLS 结合使用,确保数据在传输过程中的保密性和完整性。
-
Vue
Vue 作为一款轻量级的前端框架,以其简洁易懂的语法和高效的渲染机制受到广大开发者的喜爱。它采用了组件化的开发模式,将用户界面分解为独立、可复用的组件,大大提高了开发效率和代码的可维护性。
Vue 的核心库专注于视图层,通过数据驱动的方式实现视图的自动更新,使开发者能够更直观地管理页面的状态和交互逻辑。同时,它还拥有丰富的生态系统,包括路由管理(Vue Router)、状态管理(Vuex)等,为构建复杂的单页应用提供了强大的支持。
对于渐进式的开发理念,Vue 允许开发者可以根据项目的需求逐步引入和扩展功能,从简单的页面应用到大型的企业级前端应用都能轻松应对。
后端实现(Spring Boot)
pom.xml 依赖:
<dependencies>
<!-- Spring Boot WebSocket 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 其他相关依赖 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.18</version>
</dependency>
</dependencies>
application.yaml 配置:
server: port: 8080 servlet: context-path: /yourContextPath
spring: websocket: stomp: register-simple-broker: true endpoint: /ws allowed-origins: "*" external-host: http://localhost:11434
后端核心代码:
import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import org.springframework.stereotype.Component; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketHttpHeaders; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.client.standard.StandardWebSocketClient; import org.springframework.web.socket.handler.AbstractWebSocketHandler; import org.springframework.web.socket.handler.TextWebSocketHandler;
import javax.annotation.Resource; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLDecoder; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch;
@Component public class ChatWebSocketHandler extends TextWebSocketHandler {
private String chatId; private String baseUrl; @Value("${spring.websocket.external-host}") private String externalHost;
private ConcurrentHashMap<String, ExternalWebSocketHandler> externalHandlerMap = new ConcurrentHashMap<>();
@Override public void afterConnectionEstablished(WebSocketSession clientSession) throws InterruptedException { System.out.println("Connected to client"); parseParameters(Objects.requireNonNull(clientSession.getUri())); connectToExternalWebSocket(clientSession); }
private void parseParameters(URI uri) { // 解析路径变量 chatId String path = uri.getPath(); String[] segments = path.split("/"); if (segments.length > 1) { this.chatId = segments[segments.length - 1]; }
System.out.println("Parsed chatId: " + chatId);
// 解析查询参数 baseUrl String query = uri.getQuery(); Map<String, String> queryParams = new HashMap<>(); if (query!= null) { String[] pairs = query.split("&"); for (String pair : pairs) { int idx = pair.indexOf("="); try { queryParams.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"), URLDecoder.decode(pair.substring(idx + 1), "UTF-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } } this.baseUrl = MapUtil.getStr(queryParams, "baseUrl", ""); }
private void connectToExternalWebSocket(WebSocketSession clientSession) throws InterruptedException { String template = "{host}/{chatId}?base_url={baseUrl}"; Map<String, Object> urlParams = new HashMap<>(); urlParams.put("host", externalHost); urlParams.put("chatId", chatId); urlParams.put("baseUrl", baseUrl); String url = StrUtil.format(template, urlParams);
StandardWebSocketClient client = new StandardWebSocketClient(); WebSocketHttpHeaders headers = new WebSocketHttpHeaders(); CountDownLatch connectionLatch = new CountDownLatch(1); ExternalWebSocketHandler externalHandler = new ExternalWebSocketHandler(connectionLatch, clientSession); client.doHandshake(externalHandler, headers, URI.create(url)) .addCallback( result -> System.out.println("External connection established"), ex -> System.err.println("External connection failed: " + ex.getMessage()) ); externalHandlerMap.put(clientSession.getId(), externalHandler); connectionLatch.await(); }
@Override protected void handleTextMessage(WebSocketSession clientSession, TextMessage message) throws Exception { System.out.println("Received message from client: " + message.getPayload()); ExternalWebSocketHandler externalHandler = externalHandlerMap.get(clientSession.getId()); externalHandler.forwardMessage(message.getPayload()); }
@Override public void handleTransportError(WebSocketSession session, Throwable exception) { System.err.println("Transport error: " + exception.getMessage()); }
@Override public void afterConnectionClosed(WebSocketSession session, org.springframework.web.socket.CloseStatus status) { externalHandlerMap.remove(session.getId()); System.out.println("Connection closed: " + session.getId()); }
public static class ExternalWebSocketHandler extends AbstractWebSocketHandler {
private final CountDownLatch connectionLatch; private WebSocketSession clientSession; private WebSocketSession externalSession;
public ExternalWebSocketHandler(CountDownLatch connectionLatch, WebSocketSession clientSession) { this.connectionLatch = connectionLatch; this.clientSession = clientSession; }
@Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { System.out.println("Connected to external service: " + session.getId()); this.externalSession = session; sendInitialMessage(); connectionLatch.countDown(); }
@Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { clientSession.sendMessage(message); }
public void forwardMessage(String message) throws Exception { if (externalSession!= null && externalSession.isOpen()) { externalSession.sendMessage(new TextMessage(message)); } else { System.err.println("External WebSocket session is not open"); } }
private void sendInitialMessage() throws Exception { } } }
配置 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 {
@Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new ChatWebSocketHandler(), "/chat"); } }
前端实现(Vue)
<template> <div> <div v-if="messages.length > 0" v-for="message in messages" :key="message">{{ message }}</div> </div> </template>
<script> export default { data() { return { messages: [], socket: null, // 新增后端需要的参数 chatId: '', baseUrl: '' }; }, mounted() { this.initWebSocket(); }, methods: { initWebSocket() { // 传递后端需要的参数 this.socket = new WebSocket(
ws://your-backend-url/chat?chatId=``{this.chatId}&baseUrl=``{this.baseUrl}
); this.socket.onmessage = event => { this.messages.push(event.data); }; } } }; </script>
通过以上后端与前端的配合,实现了 Spring Boot + WebSocket + Vue 的 Ollama 流式会话。
总结:
本文详细介绍了从技术选型到具体实现的全过程,涵盖了 Spring Boot 后端的代码编写、配置(包括从 yaml 文件读取外部主机配置),以及 Vue 前端的连接和消息处理(增加了后端需要的参数传递)。需要注意的是,开发者在运行此项目前,应确保启动 Ollama 服务,其 URL 为 http://localhost:11434,以便实现完整的功能。这有助于深入理解和实践相关技术在实时通信和流式会话中的应用。