HTTP、HTTPS 和 WebSocket 协议和开发
目录
HTTP(超文本传输协议)
HTTPS(HTTP Secure)
WebSocket
三者关系
深入剖析 HTTP 的演进
1. HTTP/1.0 (1996) - “一次性邮差”
2. HTTP/1.1 (1997) - “可复用的邮差” - 目前仍最主流
3.HTTP/2 (2015) - “超级高速公路”
HTTP/1.1 vs HTTP/2 性能对比
WebSocket 再深入:它是如何“升级”的?
WebSocket 和Http/2
1. 通信模型的根本差异(这是最重要的区别)
2. 对“服务器推送”的误解澄清
3. 如何选择?
开发中(SpringBoot Vue)
HTTP请求的接收和发送
SpringBoot后端
接收HTTP请求
表面现象:为什么我们不需要手动构建HTTP响应
发送HTTP请求(调用外部API)
1.使用RestTemplate
2.WebClient 详细讲解
Vue前端
发送 + 接收HTTP请求
WebSocket完整通信
后端
前端
重新讲解WebSocket
用一个超形象的比喻理解WebSocket
传统HTTP vs WebSocket
WebSocket 实际开发详解(用WebSocketHandler)
第一步:Spring Boot后端设置
1. 添加依赖
2. 创建WebSocket配置(就像安装电话线路)
3. 创建WebSocket处理器(就像接线员)
第二步:Vue前端使用WebSocket
1. 创建WebSocket聊天组件
四个关键事件:
核心方法:
连接状态:
WebSocket 实际开发详解(用@ServerEndpoint)
第一步:Spring Boot后端配置
1. 添加依赖(与之前相同)
2. 关键配置:启用ServerEndpoint
3. 使用@ServerEndpoint创建WebSocket端点
4. 业务服务示例(展示依赖注入)
第二步:Vue前端使用(与之前类似,但连接地址不同)
@ServerEndpoint 核心注解说明
四个核心注解:
重要特性:
两种方式对比总结
选择建议:
| 协议 | 特点 | 运行过程(简述) | OSI 层 | TCP/IP 层 |
|---|---|---|---|---|
| HTTP | 明文、无状态、请求-响应模型 | 1. 建立 TCP 连接 2. 客户端发送请求 3. 服务器返回响应 4. 关闭连接 | 应用层 | 应用层 |
| HTTPS | 加密、身份验证、完整性校验 | 1. 建立 TCP 连接 2. TLS 握手(交换密钥) 3. 在加密通道内进行 HTTP 通信 | 应用层 | 应用层 |
| WebSocket | 全双工、长连接、低延迟 | 1. HTTP 握手(Upgrade 请求) 2. 连接升级为 WebSocket 3. 双向持久通信,无需重复握手 | 应用层 | 应用层 |
HTTP(超文本传输协议)
所属层级: 应用层协议。它基于传输层的 TCP 协议。
特点:
-
明文传输:请求和响应的内容都是未加密的,容易被窃听和篡改。
-
无状态:服务器不记录每次请求之间的关联信息。每个请求都是独立的(通常使用 Cookie/Session 技术来弥补这一缺陷)。
-
请求-响应模型:通信总是由客户端(如浏览器)发起,服务器被动响应。服务器不会主动向客户端推送消息。
-
简单灵活:传输的内容类型由
Content-Type标头定义,可以传输任意类型的数据。
运行过程:
-
建立 TCP 连接:客户端(浏览器)首先与服务器的 80 端口建立一个可靠的 TCP 连接。
-
发送 HTTP 请求:客户端通过这个连接发送一个请求报文,包含:
-
请求行(方法:GET/POST,URL,协议版本)
-
请求头(Host, User-Agent, Accept 等)
-
请求体(可选,如 POST 方法提交的表单数据)
-
-
服务器处理并返回响应:服务器解析请求,处理业务逻辑,然后返回一个响应报文,包含:
-
状态行(状态码:200 OK,404 Not Found 等)
-
响应头(Content-Type, Content-Length, Set-Cookie 等)
-
响应体(请求的资源,如 HTML、图片、JSON 数据等)
-
-
关闭连接:在 HTTP/1.0 中,每次请求-响应后都会关闭 TCP 连接。HTTP/1.1 引入了持久连接,允许在同一个连接上进行多次请求-响应,减少了建立连接的开销。
HTTPS(HTTP Secure)
所属层级: 应用层协议。它是在 HTTP 和 TCP 之间加入了一个安全层(SSL/TLS)。
特点:
-
加密传输:通过 SSL/TLS 协议对通信内容进行加密,防止数据被窃听。
-
身份验证:通过数字证书验证服务器的身份,防止中间人攻击。
-
完整性保护:通过消息认证码来校验数据在传输过程中是否被篡改。
-
本质还是 HTTP:在建立安全通道后,通信的内容和方式与 HTTP 完全一样。
运行过程:
-
建立 TCP 连接:客户端连接到服务器的 443 端口。
-
TLS 握手:这是 HTTPS 安全的核心。
-
客户端 Hello:客户端向服务器发送支持的加密算法列表和一个随机数。
-
服务器 Hello:服务器选择加密算法,发送自己的数字证书和一个随机数。
-
验证证书:客户端验证证书的合法性(是否由可信机构颁发,域名是否匹配等)。
-
生成会话密钥:客户端用证书中的公钥加密一个预主密钥并发送给服务器。
-
生成会话密钥:服务器用自己的私钥解密得到预主密钥。此时,客户端和服务器都拥有了三个随机数(客户端随机数、服务器随机数、预主密钥),它们使用相同的算法生成唯一的会话密钥。
-
握手结束:双方交换加密完成的“Finished”消息,验证握手是否成功。
-
-
加密的 HTTP 通信:之后的整个 HTTP 请求和响应过程,都使用上一步生成的会话密钥进行加密和解密。
-
关闭连接。
WebSocket
所属层级: 应用层协议。它同样基于 TCP,并借用了 HTTP 的握手过程。
特点:
-
全双工通信:连接建立后,服务器和客户端可以同时向对方发送消息。
-
持久化长连接:只需一次握手,连接就会一直保持,避免了 HTTP 的频繁建立和断开连接的开销。
-
低延迟:由于没有频繁的握手和头部信息,通信效率极高,非常适合实时应用。
-
服务器主动推送:服务器可以随时主动向客户端发送数据,完美解决了 HTTP 轮询带来的性能问题。
运行过程:
-
HTTP 握手请求:客户端首先发送一个特殊的 HTTP 请求,其头部包含:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Upgrade: websocket和Connection: Upgrade表明客户端希望将协议升级为 WebSocket。
2. HTTP 握手响应:服务器如果同意升级,会返回一个状态码为 101 Switching Protocols 的响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
-
Sec-WebSocket-Accept是对客户端发送的 Key 计算后的结果,用于验证握手有效性。
3. WebSocket 数据传输:握手成功后,TCP 连接保持,但通信协议从 HTTP 切换到了 WebSocket。此时,双方使用 WebSocket 定义的数据帧格式进行双向、低开销的数据传输。
4. 关闭连接:任何一方都可以发送一个关闭帧来优雅地终止连接。
三者关系
+-------------------------------------------------------------------+
| 应用层 (Application Layer) |
+-------------------+-------------------+---------------------------+
| HTTP 1.x | HTTP/2 | WebSocket |
| (明文,队头阻塞) | (二进制,多路复用) | (全双工,持久连接) |
+-------------------+-------------------+---------------------------+
| HTTPS (HTTP + TLS/SSL) 安全的传输层 |
+-------------------------------------------------------------------+
| 传输层 (Transport Layer) |
| TCP |
+-------------------------------------------------------------------+
| 网络层 (Network Layer) |
| IP |
+-------------------------------------------------------------------+
核心比喻:
- TCP/IP:相当于高速公路系统。它负责把数据包从A点可靠地送到B点。
- HTTP:相当于在高速公路上跑的邮递车。它负责运送具体的“信件”(网页、图片等),但每次送货都需要重新装货、发货、收货确认,效率较低。
- WebSocket:相当于在高速公路上建立的一条专用管道。一旦建好,双方可以随时、持续地向管道里扔东西,效率极高。
深入剖析 HTTP 的演进
1. HTTP/1.0 (1996) - “一次性邮差”
特点:
-
短连接:每次请求都需要建立一次 TCP 连接,收到响应后立即断开。想象成邮差每送一封信,都要回邮局一趟再送下一封。效率极低。
图示过程:
客户端 (浏览器) 服务器|---- TCP连接 ---->||--- HTTP请求1 --->||<-- HTTP响应1 ----||---- 断开连接 ---->|---- TCP连接 ---->||--- HTTP请求2 --->||<-- HTTP响应2 ----||---- 断开连接 ---->
缺点:建立 TCP 连接是昂贵的(三次握手),频繁连接断开开销巨大。
2. HTTP/1.1 (1997) - “可复用的邮差” - 目前仍最主流
核心改进:
-
持久连接:默认保持 TCP 连接打开,可以在一个连接上发送多个请求和响应。用
Connection: keep-alive头控制。邮差一次出门可以送多封信。 -
管道化:理论上,客户端可以连续发送多个请求,而不用等待上一个响应返回。但在实践中,队头阻塞 问题严重。
队头阻塞比喻:
就像一个单车道收费站(服务器),前面的车交费慢了,后面的车即使准备好了钱也得等着。
图示过程 (无管道化(默认模式)):
客户端 服务器|---- TCP连接 ----------->||--- 请求1 -------------->||<--------- 响应1 --------||--- 请求2 -------------->||<--------- 响应2 --------||--- 请求3 -------------->||<--------- 响应3 --------||---- 断开连接 ----------->|
图示过程 (有管道化(Pipelining)但存在队头阻塞):
客户端 服务器|---- TCP连接 ----------->||--- 请求1 -------------->||--- 请求2 -------------->| # 连续发送请求|--- 请求3 -------------->||<--------- 响应1 --------| # 响应1必须最先返回|<--------- 响应2 --------| # 如果响应1处理慢,2和3就被堵住了|<--------- 响应3 --------|
HTTP/1.1 的其他问题:请求和响应头信息重复且未压缩,浪费带宽。
3.HTTP/2 (2015) - “超级高速公路”
HTTP/2 没有改变 HTTP 的语义(方法、状态码等),而是改变了数据格式和传输方式。
核心改进:
-
二进制分帧:不再是纯文本格式,而是被分解为更小的二进制帧。更容易机器的解析,也更紧凑。
-
多路复用:解决了队头阻塞问题。多个请求和响应可以在同一个连接上混杂传输,互不干扰。
多路复用比喻:
就像一个多车道高速公路。即使一辆车(请求1)在慢速行驶,其他车(请求2、3)也可以从旁边车道超车。
图示过程:
客户端 服务器|--------- TCP连接 --------------->||---- 流1: 头帧 + 数据帧 ---------->||---- 流2: 头帧 + 数据帧 ---------->| # 多个流混杂在一起|---- 流3: 头帧 + 数据帧 ---------->||<---- 流2: 数据帧 ----------------| # 服务器可以优先返回准备好的流2|<---- 流1: 数据帧 ----------------||<---- 流3: 数据帧 ----------------|
-
流:一个虚拟通道,承载一个完整的请求-响应过程。每个流有唯一ID。
-
帧:数据通信的最小单位,每个帧都标明了它属于哪个流。
其他重要特性:
-
头部压缩:使用 HPACK 算法压缩请求头,大大减少了冗余。
-
服务器推送:服务器可以预测客户端需要哪些资源(如CSS、JS),在客户端请求之前就主动推送过去。
HTTP/1.1 vs HTTP/2 性能对比
想象一个网页,需要加载 HTML, CSS, JS, 图片1, 图片2。
HTTP/1.1 (有并发连接数限制,如6个):
时间线 | [HTML] [CSS] [JS] [IMG1] [IMG2] ... | 排队等待,可能有多轮
-
虽然连接复用,但响应必须按顺序返回,容易堵塞。
HTTP/2 (多路复用):
时间线 | [HTML][CSS][JS][IMG1][IMG2]... | 所有资源几乎并行下载
-
所有资源争抢同一个连接的“带宽”,谁先准备好谁就先发送,效率极高。
现在所有的 HTTP 默认用的是哪个版本?
现状总结:
-
HTTP/1.1 是当前绝对的主流和保底选择。
-
HTTP/2 已在绝大多数现代网站和浏览器中普及,是性能优化的首选。
-
HTTP/3 (基于QUIC协议) 正在快速崛起,是未来方向。

| 协议版本 | 使用场景与现状 | 默认程度 |
|---|---|---|
| HTTP/1.0 | 基本已被淘汰。仅在一些非常古老的客户端或嵌入式设备中可能出现。 | 绝非默认 |
| HTTP/1.1 | 目前的绝对基准和保底协议。所有客户端和服务器都100%支持。如果无法协商更新版本,一定会降级到 HTTP/1.1。 | 功能上的“默认” (当其他版本不可用时) |
| HTTP/2 | 现代网站的默认性能选择。超过一半的网站已支持。几乎所有现代浏览器都支持。需要 HTTPS。 | 性能上的“默认” (对于现代网站) |
| HTTP/3 | 前沿和未来。由 Google 推动,基于 QUIC 协议(在 UDP 上而非 TCP)。能进一步解决延迟和队头阻塞问题。支持率正在快速增长(如 Cloudflare, Google 等服务已支持)。 | 正在成为下一代默认 |
WebSocket 再深入:它是如何“升级”的?
阶段一:HTTP 握手 (“敲门,对暗号”)
客户端 (敲门) --- HTTP Request --->GET /chat HTTP/1.1Host: example.comUpgrade: websocket # “我想升级成WebSocket”Connection: UpgradeSec-WebSocket-Key: dGhl... # 暗号的一部分Sec-WebSocket-Version: 13服务器 (核对暗号) <--- HTTP Response ---HTTP/1.1 101 Switching Protocols # “好的,升级成功!”Upgrade: websocketConnection: UpgradeSec-WebSocket-Accept: s3pPL... # 暗号的另一部分
阶段二:WebSocket 全双工通信 (“推开大门,自由交谈”)
客户端 服务器|------- WebSocket 数据帧 ------->| # 客户端可以随时发消息|<------ WebSocket 数据帧 --------| # 服务器也可以随时主动发消息|<------ WebSocket 数据帧 --------||------- WebSocket 数据帧 ------->|... (连接持久,双向通信) ...
WebSocket 和Http/2
-
WebSocket 是为了提供持续的、双向的、对话式的通信通道。
-
HTTP/2 是为了高效地传输大量的 Web 资源(HTML, CSS, JS, 图片等),它优化的是请求-响应模型。
| 特性 | WebSocket | HTTP/2 |
|---|---|---|
| 通信模型 | 真正的、基于消息的全双工通道。连接建立后,双方平等,可随时主动发送消息。 | 增强的请求-响应模型。通信仍由客户端请求发起。服务器推送是对请求的“预测响应”,并非真正的主动通信。 |
| 服务器主动推送 | 原生支持。服务器可以在任何业务逻辑需要时,主动向客户端发送消息。 | 有限的 Server Push。服务器只能将资源推送到客户端缓存,无法触发客户端JavaScript代码执行。 |
| 数据格式 | 轻量级的帧结构。头部开销极小,适合高频、小数据量的消息交换。 | 二进制分帧层。虽然也是二进制,但头部经过HPACK压缩,仍是为传输Web资源优化。 |
| 连接与状态 | 有状态的。连接一旦建立,会一直保持,直到一方关闭。服务器知道哪些客户端在线。 | 无状态的。每个请求-响应在逻辑上仍是独立的(尽管在同一个TCP连接上)。 |
| 与 HTTP 的关系 | 借用 HTTP 进行初始握手,之后完全脱离 HTTP,使用自己的协议。 | 是 HTTP 语义的进化。它改变了数据的传输格式,但没有改变 HTTP 的方法、状态码等核心概念。 |
| 延迟 | 极低。一旦连接建立,消息可以立即往返,没有协议上的开销和延迟。 | 较低。相比 HTTP/1.1 大大降低,但由于仍是请求-响应模式,对于真正的实时应用,延迟高于 WebSocket。 |
解析关键区别
1. 通信模型的根本差异(这是最重要的区别)
-
WebSocket:像一个电话通话
-
你拨号(HTTP 握手),对方接听(101 Switching Protocols)。
-
之后,你们就建立了一条持续的连接,任何一方都可以随时说话,也可以同时听对方说。
-
适用场景: 在线聊天、实时游戏、协同编辑、股票行情推送。任何需要服务器在数据产生时立即通知客户端的场景。
-
-
HTTP/2:像一个高效的邮递员
-
你给他一张购物清单(请求),他可以用一辆大卡车(多路复用)把所有的货品(响应)一起运回来,效率很高。
-
他甚至能预测你可能还需要酱油,于是把酱油也提前塞进你的包裹(Server Push)。
-
但是,他不能主动敲门给你送一封你不知道的信。 所有送来的东西,都必须源于你最初的那张“购物清单”或基于它的预测。
-
适用场景: 加载复杂的网页。它极大地优化了网页的加载性能。
-
2. 对“服务器推送”的误解澄清
这是最关键的混淆点。
-
HTTP/2 Server Push:
-
目的: 是为了减少延迟。当服务器收到对一个HTML页面的请求时,它可以“推送”这个页面所需的CSS和JS文件到客户端缓存,省去了客户端解析HTML后再去请求这些资源的时间。
-
本质: 是对已知资源的缓存填充。推送到浏览器的资源,不能被你的 JavaScript 代码直接接收和处理。它只是安静地待在缓存里,等你需要时直接读取。
-
例子: 浏览器请求
index.html,服务器连同style.css和app.js一起推送过来。
-
-
WebSocket 推送:
-
目的: 是为了传递动态的、未知的业务数据。
-
本质: 是一条消息。当服务器通过 WebSocket 发送数据时,会直接触发客户端的
onmessage事件,你的 JavaScript 代码可以立即处理它。 -
例子: 在聊天应用中,当另一个用户发送了一条消息,服务器通过 WebSocket 立即将它推送到你的浏览器,聊天窗口实时更新。
-
// WebSocket 的推送可以直接被JS代码处理
websocket.onmessage = function(event) {var message = event.data; // 直接拿到消息内容appendMessageToChatWindow(message);
};
3. 如何选择?
-
选择 HTTP/2:
-
你的主要目标是加快网站或Web应用的加载速度。
-
你不需要服务器主动向客户端发送实时业务消息。
-
你的应用架构仍然是传统的“客户端请求,服务器响应”。
-
-
选择 WebSocket:
-
你需要真正的、低延迟的双向通信。
-
你的应用功能依赖于服务器主动发起的事件(如聊天消息、实时通知、游戏状态同步、直播评论)。
-
开发中(SpringBoot Vue)
下面我们分别详细讲解。
- HTTP请求的接收和发送
- WebSocket消息的接收和发送
HTTP请求的接收和发送
SpringBoot后端
接收HTTP请求
1. 依赖配置
<!-- pom.xml -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
2. 接收HTTP请求的Controller
@RestController
@RequestMapping("/api")
public class UserController {// 接收GET请求 - 获取用户列表@GetMapping("/users")public ResponseEntity<List<User>> getUsers() {List<User> users = userService.findAll();return ResponseEntity.ok(users); // 自动构建HTTP响应}// 接收POST请求 - 创建用户@PostMapping("/users")public ResponseEntity<User> createUser(@RequestBody User user) {// @RequestBody 自动将JSON请求体转换为Java对象User savedUser = userService.save(user);return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);}// 接收带路径参数的GET请求@GetMapping("/users/{id}")public ResponseEntity<User> getUserById(@PathVariable Long id) {User user = userService.findById(id);return ResponseEntity.ok(user);}// 接收带查询参数的GET请求@GetMapping("/users/search")public ResponseEntity<List<User>> searchUsers(@RequestParam String keyword,@RequestParam(defaultValue = "0") int page) {List<User> users = userService.search(keyword, page);return ResponseEntity.ok(users);}// 接收PUT请求 - 更新用户@PutMapping("/users/{id}")public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {User updatedUser = userService.update(id, user);return ResponseEntity.ok(updatedUser);}// 接收DELETE请求@DeleteMapping("/users/{id}")public ResponseEntity<Void> deleteUser(@PathVariable Long id) {userService.delete(id);return ResponseEntity.noContent().build(); // 204 No Content}
}
表面现象:为什么我们不需要手动构建HTTP响应
这是Spring MVC框架的功劳
Spring MVC的工作流程:
HTTP请求 -> DispatcherServlet -> @Controller/@RestController -> 返回对象 -> HttpMessageConverter -> JSON/XML -> HTTP响应
具体发生了什么:
写一个简单的Controller方法:
@RestController
public class UserController {@GetMapping("/users/{id}")public User getUser(@PathVariable Long id) {User user = userService.findById(id);return user; // 直接返回Java对象,不是HttpResponse!}
}
-
Spring MVC在背后帮你做了这些:
-
自动设置状态码:默认200 OK
-
自动设置Content-Type头:
application/json -
自动将Java对象序列化为JSON(响应体)
-
自动处理异常,返回合适的错误码
-
这就是框架的价值:把重复的样板代码隐藏起来,让开发者专注于业务逻辑。
发送HTTP请求(调用外部API)
我们使用RestTemplate或WebClient来发送HTTP请求。
| 工具 | 时代 | 编程模型 | 底层实现 | Spring Boot起步依赖 |
|---|---|---|---|---|
| RestTemplate | Spring 3.0+ | 同步阻塞 | 可配置(默认JDK HttpURLConnection) | spring-boot-starter-web |
| WebClient | Spring 5.0+ | 异步非阻塞 (Reactive) | Reactor Netty | spring-boot-starter-webflux |
RestTemplate
-
默认:JDK的
HttpURLConnection -
可替换为:Apache HttpClient、OkHttp等
@Bean
public RestTemplate restTemplate() {return new RestTemplate(new HttpComponentsClientHttpRequestFactory()); // 使用Apache HttpClient
}
WebClient
- 基于Reactor和Netty,使用事件驱动、非阻塞IO模型。
// 简化的执行流程
public Mono<ClientResponse> exchange() {return Mono.defer(() -> {// 1. 创建HttpClientRequestHttpClientRequest request = httpClient.request(httpMethod);// 2. 设置请求头、体等// 3. 发送请求并返回Mono<ClientResponse>return request.response();});
}
1.使用RestTemplate
1.依赖配置
起步依赖:spring-boot-starter-web (这是最常用的Web开发starter)
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
spring-boot-starter-web
├── spring-webmvc # Spring MVC核心
├── spring-web # Spring Web核心
├── jackson-databind # JSON序列化
└── tomcat-embed-core # 内嵌Tomcat
2.RestTemplate在Spring Boot中的使用方式:
方式1:手动配置Bean(推荐,可定制化)
@Configuration
public class RestTemplateConfig {@Beanpublic RestTemplate restTemplate() {RestTemplate restTemplate = new RestTemplate();// 可选的定制配置restTemplate.setErrorHandler(new MyErrorHandler());// 设置超时时间等...return restTemplate;}
}
然后,在需要的地方注入并使用。
@Service
public class UserService {@Autowiredprivate RestTemplate restTemplate;// GET请求 - 获取用户信息public User getUserFromExternalAPI(Long userId) {String url = "https://jsonplaceholder.typicode.com/users/{id}";// 方法1:getForObject - 直接返回对象User user = restTemplate.getForObject(url, User.class, userId);// 方法2:getForEntity - 返回包含响应头的完整响应ResponseEntity<User> response = restTemplate.getForEntity(url, User.class, userId);if (response.getStatusCode() == HttpStatus.OK) {return response.getBody();}return null;}// POST请求 - 创建用户public User createUser(User user) {String url = "https://jsonplaceholder.typicode.com/users";// 方法1:postForObjectUser createdUser = restTemplate.postForObject(url, user, User.class);// 方法2:exchange - 最灵活的方法HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);headers.set("Authorization", "Bearer token123");HttpEntity<User> request = new HttpEntity<>(user, headers);ResponseEntity<User> response = restTemplate.exchange(url, HttpMethod.POST, request, User.class);return response.getBody();}// PUT、DELETE请求public void updateUser(User user) {String url = "https://jsonplaceholder.typicode.com/users/{id}";restTemplate.put(url, user, user.getId());}public void deleteUser(Long userId) {String url = "https://jsonplaceholder.typicode.com/users/{id}";restTemplate.delete(url, userId);}
}
方式2:直接使用(不推荐)
// 可以直接new,但失去了Spring管理的优势
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject("http://example.com", String.class);
2.WebClient 详细讲解
1.配置依赖。起步依赖:spring-boot-starter-webflux
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
spring-boot-starter-webflux
├── spring-webflux # Reactive Web核心
├── reactor-core # Reactor响应式编程
└── netty # 底层网络库
2.配置Bean:
@Configuration
public class WebClientConfig {@Beanpublic WebClient webClient() {return WebClient.builder().baseUrl("https://jsonplaceholder.typicode.com").defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).build();}
}
3.然后,在需要的地方注入并使用。
@Service
public class ReactiveUserService {@Autowiredprivate WebClient webClient;// 异步获取用户public Mono<User> getUserAsync(Long userId) {return webClient.get().uri("/users/{id}", userId).retrieve().bodyToMono(User.class);}// 带错误处理的请求public Mono<User> getUserWithErrorHandling(Long userId) {return webClient.get().uri("/users/{id}", userId).retrieve().onStatus(HttpStatus::is4xxClientError, response -> Mono.error(new RuntimeException("Client error: " + response.statusCode()))).onStatus(HttpStatus::is5xxServerError, response -> Mono.error(new RuntimeException("Server error: " + response.statusCode()))).bodyToMono(User.class);}// POST请求public Mono<User> createUser(User user) {return webClient.post().uri("/users").bodyValue(user).retrieve().bodyToMono(User.class);}// 同步调用(在需要阻塞的地方)public User getUserSync(Long userId) {return webClient.get().uri("/users/{id}", userId).retrieve().bodyToMono(User.class).block(); // 阻塞直到得到结果}
}

RestTemplate/WebClient主要就是为了在分布式系统中调用其他服务!
Vue前端
发送 + 接收HTTP请求
1. 安装依赖
npm install axios
2. HTTP服务封装
// src/services/api.js
import axios from 'axios';// 创建axios实例
const apiClient = axios.create({baseURL: 'http://localhost:8080/api', // Spring Boot后端地址timeout: 10000,headers: {'Content-Type': 'application/json'}
});export default {// 发送GET请求 + 接收响应getUsers() {return apiClient.get('/users').then(response => {// 接收后端响应数据return response.data;}).catch(error => {console.error('获取用户列表失败:', error);throw error;});},// 发送POST请求 + 接收响应createUser(userData) {return apiClient.post('/users', userData).then(response => {// 接收创建成功的用户数据return response.data;});},// 发送PUT请求 + 接收响应updateUser(id, userData) {return apiClient.put(`/users/${id}`, userData).then(response => response.data);},// 发送DELETE请求deleteUser(id) {return apiClient.delete(`/users/${id}`);}
}
3. Vue组件中使用
<template><div><!-- 发送请求的UI --><button @click="loadUsers">加载用户</button><button @click="createUser">创建用户</button><!-- 接收并显示响应数据 --><div v-if="loading">加载中...</div><ul v-else><li v-for="user in users" :key="user.id">{{ user.name }} - {{ user.email }}<button @click="deleteUser(user.id)">删除</button></li></ul></div>
</template><script>
import apiService from '@/services/api.js';export default {data() {return {users: [], // 接收到的数据loading: false // 接收加载状态}},methods: {// 发送GET请求并接收响应async loadUsers() {this.loading = true;try {// 发送请求 + 接收响应this.users = await apiService.getUsers();} catch (error) {console.error('加载失败:', error);} finally {this.loading = false;}},// 发送POST请求并接收响应async createUser() {const newUser = {name: '张三',email: 'zhangsan@example.com'};try {// 发送请求 + 接收响应const createdUser = await apiService.createUser(newUser);console.log('创建成功:', createdUser);this.loadUsers(); // 重新加载列表} catch (error) {console.error('创建失败:', error);}},// 发送DELETE请求async deleteUser(userId) {try {await apiService.deleteUser(userId);this.loadUsers(); // 重新加载} catch (error) {console.error('删除失败:', error);}}},mounted() {this.loadUsers(); // 组件加载时自动获取数据}
}
</script>
WebSocket完整通信
后端
基本步骤:
- 添加WebSocket依赖。
- 配置WebSocket,比如注册WebSocket处理器或使用@ServerEndpoint注解。
- 编写WebSocket处理器,处理连接、消息、关闭和错误。
1.在pom.xml中添加WebSocket依赖。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
两种方式配置WebSocket:一种是实现WebSocketConfigurer,另一种是使用@ServerEndpoint注解。
方式一:
1. 创建一个配置类启用WebSocket,并注册WebSocket端点。
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(new MyWebSocketHandler(), "/ws").setAllowedOrigins("*"); // 允许跨域}
}
2.实现WebSocket处理器,实现WebSocketHandler接口来处理WebSocket连接和消息。
@Component
public class MyWebSocketHandler extends TextWebSocketHandler {private final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();// 接收连接建立@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {sessions.add(session);System.out.println("客户端连接: " + session.getId());// 主动发送欢迎消息session.sendMessage(new TextMessage("欢迎连接WebSocket!"));}// 接收客户端消息@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {String clientMessage = message.getPayload();System.out.println("收到消息: " + clientMessage + " from " + session.getId());// 发送响应消息给这个客户端String response = "服务器回应: " + clientMessage;session.sendMessage(new TextMessage(response));// 广播消息给所有客户端broadcast("用户 " + session.getId() + " 说: " + clientMessage);}// 接收连接关闭@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {sessions.remove(session);System.out.println("客户端断开: " + session.getId());}// 主动发送消息给所有客户端(服务器推送)public void broadcast(String message) {for (WebSocketSession session : sessions) {try {if (session.isOpen()) {session.sendMessage(new TextMessage(message));}} catch (IOException e) {e.printStackTrace();}}}// 主动发送消息给特定客户端public void sendToUser(String sessionId, String message) {for (WebSocketSession session : sessions) {if (session.getId().equals(sessionId) && session.isOpen()) {try {session.sendMessage(new TextMessage(message));} catch (IOException e) {e.printStackTrace();}}}}
}
方式二:
1.配置ServerEndpointExporter。
@Configuration
public class WebSocketConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}
2.然后,使用@ServerEndpoint注解定义一个端点。
@Component
@ServerEndpoint("/my-websocket")
public class MyWebSocketEndpoint {@OnOpenpublic void onOpen(Session session) {System.out.println("Connected: " + session.getId());}@OnMessagepublic void onMessage(String message, Session session) {System.out.println("Received: " + message);try {// 发送回复session.getBasicRemote().sendText("Echo: " + message);} catch (IOException e) {e.printStackTrace();}}@OnClosepublic void onClose(Session session) {System.out.println("Closed: " + session.getId());}@OnErrorpublic void onError(Session session, Throwable error) {error.printStackTrace();}
}
前端
在Vue组件中,我们使用JavaScript原生的WebSocket API。
基本步骤:
- 在组件创建时(mounted)建立WebSocket连接。
- 监听WebSocket的open、message、close、error事件。
- 在方法中发送消息。
- 在组件销毁前(beforeUnmount)关闭WebSocket连接。
<template><div><h3>WebSocket聊天室</h3><!-- 显示接收到的消息 --><div class="message-container"><div v-for="(msg, index) in messages" :key="index" class="message">{{ msg }}</div></div><!-- 发送消息的输入框 --><div class="input-area"><input v-model="inputMessage" @keyup.enter="sendMessage" placeholder="输入消息..."><button @click="sendMessage">发送</button><button @click="connectWebSocket">连接</button><button @click="disconnectWebSocket">断开</button></div><div>连接状态: {{ connectionStatus }}</div></div>
</template><script>
export default {data() {return {websocket: null,messages: [], // 接收到的消息列表inputMessage: '', // 要发送的消息connectionStatus: '未连接'}},methods: {// 建立WebSocket连接connectWebSocket() {// 创建WebSocket连接this.websocket = new WebSocket('ws://localhost:8080/ws');// 接收连接建立事件this.websocket.onopen = (event) => {this.connectionStatus = '已连接';this.messages.push('系统: 连接服务器成功');console.log('WebSocket连接已建立');};// 接收服务器消息this.websocket.onmessage = (event) => {// 接收到服务器发送的消息this.messages.push('服务器: ' + event.data);console.log('收到消息:', event.data);};// 接收连接关闭事件this.websocket.onclose = (event) => {this.connectionStatus = '已断开';this.messages.push('系统: 连接已断开');console.log('WebSocket连接已关闭');};// 接收错误事件this.websocket.onerror = (error) => {this.connectionStatus = '连接错误';this.messages.push('系统: 连接发生错误');console.error('WebSocket错误:', error);};},// 发送消息到服务器sendMessage() {if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {this.websocket.send(this.inputMessage);this.messages.push('我: ' + this.inputMessage);this.inputMessage = ''; // 清空输入框} else {alert('WebSocket未连接');}},// 断开连接disconnectWebSocket() {if (this.websocket) {this.websocket.close();}}},mounted() {// 组件加载时自动连接this.connectWebSocket();},beforeUnmount() {// 组件销毁前断开连接this.disconnectWebSocket();}
}
</script><style scoped>
.message-container {height: 300px;border: 1px solid #ccc;overflow-y: auto;margin-bottom: 10px;padding: 10px;
}
.message {margin: 5px 0;padding: 5px;background: #f5f5f5;border-radius: 4px;
}
.input-area {display: flex;gap: 10px;
}
</style>
重新讲解WebSocket
用一个超形象的比喻理解WebSocket
传统HTTP vs WebSocket
HTTP = 打电话
-
你拨号 → 对方接听 → 你说一句 → 对方回一句 → 挂断
-
每次要说新的话,都要重新拨号、接听、挂断
WebSocket = 微信视频通话
-
拨通一次 → 保持连接 → 双方随时都能说话 → 还能同时说话 → 直到有人挂断
WebSocket 实际开发详解(用WebSocketHandler)
第一步:Spring Boot后端设置
1. 添加依赖
<!-- pom.xml -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2. 创建WebSocket配置(就像安装电话线路)
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {// 注册WebSocket处理器,"/ws"就是客户端的连接地址registry.addHandler(new MyWebSocketHandler(), "/ws").setAllowedOrigins("*"); // 允许所有来源访问}
}
3. 创建WebSocket处理器(就像接线员)
@Component
public class MyWebSocketHandler extends TextWebSocketHandler {// 保存所有在线的客户端连接private static final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();// 📞 当有客户端连接时(有人打来电话)@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {sessions.add(session);System.out.println("新客户端连接: " + session.getId());// 🎤 主动向这个客户端发送欢迎消息session.sendMessage(new TextMessage("欢迎连接!你是第" + sessions.size() +个用户"));// 📢 向所有客户端广播有新用户加入broadcast("系统消息: 有新用户加入聊天室");}// 💬 当收到客户端发来的消息时(对方说话了)@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {String clientMessage = message.getPayload(); // 获取客户端发来的内容String clientId = session.getId();System.out.println("收到来自 " + clientId + " 的消息: " + clientMessage);// 🎤 给这个客户端单独回复String personalReply = "你说: " + clientMessage;session.sendMessage(new TextMessage(personalReply));// 📢 向所有其他客户端广播这条消息(群聊效果)String broadcastMsg = "用户" + clientId.substring(0, 6) + "说: " + clientMessage;broadcastToOthers(session, broadcastMsg);}// 📞 当客户端断开连接时(对方挂电话了)@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {sessions.remove(session);System.out.println("客户端断开: " + session.getId());// 📢 通知其他用户有人离开broadcast("系统消息: 有用户离开了聊天室");}// 📢 广播消息给所有客户端private void broadcast(String message) {for (WebSocketSession session : sessions) {try {if (session.isOpen()) {session.sendMessage(new TextMessage(message));}} catch (IOException e) {e.printStackTrace();}}}// 📢 广播给除指定session外的所有客户端private void broadcastToOthers(WebSocketSession excludeSession, String message) {for (WebSocketSession session : sessions) {if (!session.getId().equals(excludeSession.getId()) && session.isOpen()) {try {session.sendMessage(new TextMessage(message));} catch (IOException e) {e.printStackTrace();}}}}// 🎯 主动给特定用户发送消息(私聊)public void sendToUser(String targetSessionId, String message) {for (WebSocketSession session : sessions) {if (session.getId().equals(targetSessionId) && session.isOpen()) {try {session.sendMessage(new TextMessage("私信: " + message));} catch (IOException e) {e.printStackTrace();}break;}}}
}
第二步:Vue前端使用WebSocket
1. 创建WebSocket聊天组件
<template><div class="websocket-demo"><h2>💬 实时聊天室 (WebSocket)</h2><!-- 连接状态显示 --><div class="status" :class="connectionStatus">状态: {{ statusText }}</div><!-- 连接控制按钮 --><div class="controls"><button @click="connect" :disabled="isConnected">📞 连接</button><button @click="disconnect" :disabled="!isConnected">❌ 断开</button></div><!-- 消息显示区域 --><div class="messages"><div v-for="(msg, index) in messages" :key="index" class="message":class="{ 'my-message': msg.isMyMessage }">{{ msg.content }}</div></div><!-- 消息发送区域 --><div class="send-area"><input v-model="inputMessage" @keyup.enter="sendMessage"placeholder="输入消息...":disabled="!isConnected"><button @click="sendMessage" :disabled="!isConnected">📤 发送</button></div><!-- 在线用户列表 --><div class="online-users" v-if="onlineUsers.length > 0"><h4>👥 在线用户</h4><div v-for="user in onlineUsers" :key="user" class="user">{{ user }}</div></div></div>
</template><script>
export default {name: 'WebSocketChat',data() {return {websocket: null, // WebSocket连接实例messages: [], // 存储所有消息inputMessage: '', // 输入框的消息connectionStatus: 'disconnected', // 连接状态onlineUsers: [], // 在线用户列表myUserId: '' // 我的用户ID}},computed: {// 计算属性:是否已连接isConnected() {return this.websocket && this.websocket.readyState === WebSocket.OPEN;},// 计算属性:状态显示文本statusText() {switch (this.connectionStatus) {case 'connected': return '🟢 已连接';case 'connecting': return '🟡 连接中...';case 'disconnected': return '🔴 未连接';case 'error': return '🔴 连接错误';default: return '未知状态';}}},methods: {// 📞 建立WebSocket连接connect() {this.connectionStatus = 'connecting';// 创建WebSocket连接,连接到后端的 /ws 端点this.websocket = new WebSocket('ws://localhost:8080/ws');// 🎧 监听连接成功事件this.websocket.onopen = (event) => {console.log('✅ WebSocket连接成功');this.connectionStatus = 'connected';this.addSystemMessage('连接服务器成功!');};// 🎧 监听收到消息事件(最重要的部分!)this.websocket.onmessage = (event) => {console.log('📨 收到服务器消息:', event.data);// 处理接收到的消息this.handleReceivedMessage(event.data);};// 🎧 监听连接关闭事件this.websocket.onclose = (event) => {console.log('❌ WebSocket连接关闭');this.connectionStatus = 'disconnected';this.addSystemMessage('连接已断开');};// 🎧 监听连接错误事件this.websocket.onerror = (error) => {console.error('💥 WebSocket错误:', error);this.connectionStatus = 'error';this.addSystemMessage('连接发生错误');};},// ❌ 断开WebSocket连接disconnect() {if (this.websocket) {this.websocket.close();}},// 📤 发送消息到服务器sendMessage() {if (this.inputMessage.trim() && this.isConnected) {// 发送消息this.websocket.send(this.inputMessage);// 在消息列表中显示自己发送的消息this.messages.push({content: `我: ${this.inputMessage}`,isMyMessage: true});// 清空输入框this.inputMessage = '';// 滚动到底部this.$nextTick(() => {this.scrollToBottom();});}},// 🎯 处理接收到的消息handleReceivedMessage(message) {// 根据消息内容进行不同的处理if (message.includes('系统消息')) {this.addSystemMessage(message);} else if (message.includes('欢迎连接')) {this.addSystemMessage(message);} else if (message.includes('你说:')) {// 服务器对个人消息的确认this.messages.push({content: `系统: ${message}`,isMyMessage: false});} else {// 其他用户的消息this.messages.push({content: message,isMyMessage: false});}// 滚动到底部this.$nextTick(() => {this.scrollToBottom();});},// 📝 添加系统消息addSystemMessage(message) {this.messages.push({content: `💡 ${message}`,isMyMessage: false});},// 📜 滚动到底部scrollToBottom() {const messagesContainer = this.$el.querySelector('.messages');if (messagesContainer) {messagesContainer.scrollTop = messagesContainer.scrollHeight;}}},// 组件挂载时自动连接mounted() {this.connect();},// 组件销毁前断开连接beforeUnmount() {this.disconnect();}
}
</script><style scoped>
.websocket-demo {max-width: 600px;margin: 0 auto;padding: 20px;font-family: Arial, sans-serif;
}.status {padding: 10px;border-radius: 5px;margin-bottom: 10px;text-align: center;font-weight: bold;
}.status.connected {background-color: #d4edda;color: #155724;
}.status.connecting {background-color: #fff3cd;color: #856404;
}.status.disconnected, .status.error {background-color: #f8d7da;color: #721c24;
}.controls, .send-area {display: flex;gap: 10px;margin-bottom: 15px;
}.controls button, .send-area button {padding: 8px 16px;border: none;border-radius: 4px;cursor: pointer;
}.controls button:disabled, .send-area button:disabled {opacity: 0.5;cursor: not-allowed;
}.messages {height: 300px;border: 1px solid #ddd;border-radius: 5px;padding: 10px;overflow-y: auto;margin-bottom: 15px;background-color: #f9f9f9;
}.message {margin: 8px 0;padding: 8px 12px;border-radius: 15px;max-width: 80%;word-wrap: break-word;
}.message:not(.my-message) {background-color: #e3f2fd;align-self: flex-start;
}.message.my-message {background-color: #c8e6c9;margin-left: auto;text-align: right;
}.send-area input {flex: 1;padding: 8px 12px;border: 1px solid #ddd;border-radius: 4px;
}.online-users {margin-top: 20px;padding: 10px;border: 1px solid #ddd;border-radius: 5px;background-color: #f0f8ff;
}.user {padding: 5px;border-bottom: 1px solid #e0e0e0;
}
</style>
四个关键事件:
-
onopen - 连接建立时(电话接通)
-
onmessage - 收到消息时(听到对方说话)
-
onclose - 连接关闭时(对方挂断)
-
onerror - 连接错误时(通话故障)
核心方法:
-
WebSocket.send() - 发送消息(说话)
-
WebSocket.close() - 关闭连接(挂断)
连接状态:
-
WebSocket.CONNECTING (0) - 连接中
-
WebSocket.OPEN (1) - 已连接
-
WebSocket.CLOSING (2) - 关闭中
-
WebSocket.CLOSED (3) - 已关闭
WebSocket 实际开发详解(用@ServerEndpoint)
| 方式 | 优点 | 缺点 |
|---|---|---|
| WebSocketHandler | 更Spring风格,依赖注入方便 | 代码相对繁琐 |
| @ServerEndpoint | 代码简洁,标准JSR-356 | 依赖注入需要特殊处理 |
第一步:Spring Boot后端配置
1. 添加依赖(与之前相同)
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2. 关键配置:启用ServerEndpoint
@Configuration
public class WebSocketConfig {/*** 这个Bean是必须的!* 它会自动扫描带有@ServerEndpoint注解的类并注册为WebSocket端点*/@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}
3. 使用@ServerEndpoint创建WebSocket端点
@Component
@ServerEndpoint("/ws/chat") // 🔑 定义WebSocket连接路径
public class ChatWebSocketEndpoint {// 保存所有连接的会话private static final Map<String, Session> sessions = new ConcurrentHashMap<>();// 🔧 解决@ServerEndpoint中@Autowired为null的问题private static SomeService someService;@Autowiredpublic void setSomeService(SomeService someService) {ChatWebSocketEndpoint.someService = someService;}// 📞 连接建立时调用@OnOpenpublic void onOpen(Session session) {String sessionId = session.getId();sessions.put(sessionId, session);System.out.println("🔗 新连接: " + sessionId + ", 当前在线: " + sessions.size());// 🎤 发送欢迎消息sendMessage(session, "欢迎加入聊天室!你的ID: " + sessionId);// 📢 广播用户上线通知broadcast("系统: 用户 " + sessionId.substring(0, 6) + " 加入聊天室", sessionId);}// 💬 收到客户端消息时调用@OnMessagepublic void onMessage(String message, Session session) {String sessionId = session.getId();System.out.println("📨 收到消息: " + message + " from " + sessionId);// 🎯 处理不同类型的消息if (message.startsWith("/name ")) {// 设置昵称handleSetName(message, session);} else {// 普通聊天消息String userNickname = getUserNickname(session);String broadcastMsg = userNickname + ": " + message;// 📢 广播消息给所有人(除了发送者)broadcast(broadcastMsg, sessionId);// 🎤 给发送者回执sendMessage(session, "我: " + message);}}// 📞 连接关闭时调用@OnClosepublic void onClose(Session session) {String sessionId = session.getId();sessions.remove(sessionId);System.out.println("❌ 连接关闭: " + sessionId + ", 剩余在线: " + sessions.size());// 📢 广播用户离开通知broadcast("系统: 用户 " + sessionId.substring(0, 6) + " 离开聊天室", sessionId);}// ⚠️ 发生错误时调用@OnErrorpublic void onError(Session session, Throwable error) {String sessionId = session.getId();System.err.println("💥 WebSocket错误 [" + sessionId + "]: " + error.getMessage());error.printStackTrace();}// 🎯 处理设置昵称private void handleSetName(String message, Session session) {try {String nickname = message.substring(6).trim(); // 提取昵称session.getUserProperties().put("nickname", nickname);sendMessage(session, "系统: 昵称已设置为: " + nickname);} catch (Exception e) {sendMessage(session, "系统: 设置昵称失败");}}// 🎯 获取用户昵称private String getUserNickname(Session session) {String nickname = (String) session.getUserProperties().get("nickname");return nickname != null ? nickname : session.getId().substring(0, 6);}// 📤 发送消息给单个客户端private void sendMessage(Session session, String message) {try {if (session.isOpen()) {session.getBasicRemote().sendText(message);}} catch (IOException e) {System.err.println("发送消息失败: " + e.getMessage());}}// 📢 广播消息给所有客户端(可排除指定会话)private void broadcast(String message, String excludeSessionId) {sessions.forEach((sessionId, session) -> {if (!sessionId.equals(excludeSessionId) && session.isOpen()) {try {session.getBasicRemote().sendText(message);} catch (IOException e) {System.err.println("广播消息失败: " + e.getMessage());}}});}// 🎯 主动发送消息给特定用户(可从其他业务类调用)public static void sendToUser(String targetSessionId, String message) {Session session = sessions.get(targetSessionId);if (session != null && session.isOpen()) {try {session.getBasicRemote().sendText("私信: " + message);} catch (IOException e) {System.err.println("发送私信失败: " + e.getMessage());}}}// 📊 获取在线用户数public static int getOnlineCount() {return sessions.size();}// 👥 获取所有在线会话IDpublic static Set<String> getOnlineUsers() {return sessions.keySet();}
}
4. 业务服务示例(展示依赖注入)
@Service
public class SomeService {public String processMessage(String message) {// 这里可以处理业务逻辑,比如保存到数据库、调用其他服务等return "处理后的消息: " + message.toUpperCase();}// 主动推送消息到WebSocketpublic void pushNotification(String message) {// 可以在这里调用WebSocket的静态方法进行推送ChatWebSocketEndpoint.broadcastToAll("通知: " + message);}
}
第二步:Vue前端使用(与之前类似,但连接地址不同)
<template><div class="chat-room"><h2>💬 聊天室 (@ServerEndpoint方式)</h2><div class="connection-info"><span>状态: {{ connectionStatus }}</span><span>在线用户: {{ onlineCount }}</span></div><!-- 消息显示区域 --><div class="messages-container"><div v-for="(msg, index) in messages" :key="index" class="message":class="{'system-message': msg.type === 'system','my-message': msg.type === 'my','other-message': msg.type === 'other'}">{{ msg.content }}</div></div><!-- 消息发送区域 --><div class="input-area"><input v-model="inputMessage" @keyup.enter="sendMessage"placeholder="输入消息... 输入 /name 昵称 来设置昵称":disabled="!isConnected"><button @click="sendMessage" :disabled="!isConnected">发送</button><button @click="setNickname">设置昵称</button></div><!-- 在线用户列表 --><div class="online-users"><h4>👥 在线用户 ({{ onlineUsers.length }})</h4><div v-for="user in onlineUsers" :key="user" class="user-item">{{ user }}</div></div></div>
</template><script>
export default {name: 'ChatRoom',data() {return {websocket: null,messages: [],inputMessage: '',connectionStatus: 'disconnected',onlineUsers: [],onlineCount: 0,myNickname: '用户' + Math.random().toString(36).substr(2, 5)}},computed: {isConnected() {return this.websocket && this.websocket.readyState === WebSocket.OPEN;}},methods: {// 连接WebSocketconnect() {this.connectionStatus = 'connecting';// 🔑 注意:连接地址与@ServerEndpoint注解的路径一致this.websocket = new WebSocket('ws://localhost:8080/ws/chat');this.websocket.onopen = () => {this.connectionStatus = 'connected';this.addSystemMessage('连接成功!');// 连接成功后设置昵称this.setNickname();};this.websocket.onmessage = (event) => {this.handleReceivedMessage(event.data);};this.websocket.onclose = () => {this.connectionStatus = 'disconnected';this.addSystemMessage('连接已断开');};this.websocket.onerror = (error) => {this.connectionStatus = 'error';this.addSystemMessage('连接错误: ' + error);};},// 处理接收到的消息handleReceivedMessage(message) {if (message.includes('系统:') || message.includes('欢迎') || message.includes('昵称')) {this.messages.push({content: message,type: 'system'});} else if (message.startsWith('我:')) {this.messages.push({content: message,type: 'my'});} else {this.messages.push({content: message,type: 'other'});// 简单的在线用户数统计(根据消息内容判断)if (message.includes('加入聊天室')) {this.onlineCount++;} else if (message.includes('离开聊天室')) {this.onlineCount = Math.max(0, this.onlineCount - 1);}}this.scrollToBottom();},// 发送消息sendMessage() {if (this.inputMessage.trim() && this.isConnected) {this.websocket.send(this.inputMessage);this.inputMessage = '';}},// 设置昵称setNickname() {if (this.isConnected) {this.websocket.send('/name ' + this.myNickname);}},// 添加系统消息addSystemMessage(message) {this.messages.push({content: message,type: 'system'});},// 滚动到底部scrollToBottom() {this.$nextTick(() => {const container = this.$el.querySelector('.messages-container');if (container) {container.scrollTop = container.scrollHeight;}});},// 断开连接disconnect() {if (this.websocket) {this.websocket.close();}}},mounted() {this.connect();},beforeUnmount() {this.disconnect();}
}
</script><style scoped>
.chat-room {max-width: 800px;margin: 0 auto;padding: 20px;display: grid;grid-template-columns: 1fr 200px;grid-gap: 20px;
}.connection-info {grid-column: 1 / -1;display: flex;justify-content: space-between;padding: 10px;background: #f5f5f5;border-radius: 5px;
}.messages-container {grid-column: 1;height: 400px;border: 1px solid #ddd;border-radius: 8px;padding: 15px;overflow-y: auto;background: #fafafa;
}.message {margin: 10px 0;padding: 8px 12px;border-radius: 8px;word-wrap: break-word;
}.system-message {background: #fff3cd;color: #856404;text-align: center;font-style: italic;
}.my-message {background: #d1ecf1;margin-left: 20%;text-align: right;
}.other-message {background: #e2e3e5;margin-right: 20%;
}.input-area {grid-column: 1;display: flex;gap: 10px;
}.input-area input {flex: 1;padding: 10px;border: 1px solid #ddd;border-radius: 4px;
}.input-area button {padding: 10px 15px;border: none;border-radius: 4px;background: #007bff;color: white;cursor: pointer;
}.input-area button:disabled {background: #6c757d;cursor: not-allowed;
}.online-users {grid-column: 2;border: 1px solid #ddd;border-radius: 8px;padding: 15px;background: white;
}.online-users h4 {margin: 0 0 10px 0;color: #333;
}.user-item {padding: 5px 0;border-bottom: 1px solid #f0f0f0;
}
</style>
@ServerEndpoint 核心注解说明
四个核心注解:
@OnOpen - 连接建立时
@OnOpen
public void onOpen(Session session) {// session: 代表一个WebSocket连接
}
@OnMessage - 收到消息时
@OnMessage
public void onMessage(String message, Session session) {// message: 客户端发送的消息内容
}
@OnClose - 连接关闭时
@OnClose
public void onClose(Session session) {// 连接关闭清理工作
}
@OnError - 发生错误时
@OnError
public void onError(Session session, Throwable error) {// 错误处理
}
重要特性:
1. Session对象
- 每个连接都有一个唯一的Session
- 可以存储用户属性:session.getUserProperties().put("key", value)
- 可以发送消息:session.getBasicRemote().sendText(message)
2. 依赖注入问题解决
由于@ServerEndpoint实例由WebSocket容器创建,不是Spring管理的,所以需要特殊处理:
@Component
@ServerEndpoint("/ws/chat")
public class MyEndpoint {private static SomeService someService;@Autowiredpublic void setSomeService(SomeService someService) {MyEndpoint.someService = someService; // 静态变量保存}
}
3. 路径参数支持
@ServerEndpoint("/ws/chat/{roomId}/{userId}")
public class ChatEndpoint {@OnOpenpublic void onOpen(Session session, @PathParam("roomId") String roomId,@PathParam("userId") String userId) {// 可以获取路径参数System.out.println("房间: " + roomId + ", 用户: " + userId);}
}
两种方式对比总结
| 特性 | WebSocketHandler | @ServerEndpoint |
|---|---|---|
| 代码风格 | Spring风格 | JSR-356标准风格 |
| 依赖注入 | 直接使用@Autowired | 需要静态变量中转 |
| 配置方式 | 实现接口+注册 | 注解+ServerEndpointExporter |
| Session管理 | 手动管理集合 | 手动管理集合 |
| 适用场景 | 复杂业务逻辑 | 简单实时通信 |
选择建议:
-
新手/简单项目:推荐
@ServerEndpoint,代码更简洁 -
复杂业务/需要深度Spring集成:推荐
WebSocketHandler
