目录
[HTTPS(HTTP Secure)](#HTTPS(HTTP Secure))
[深入剖析 HTTP 的演进](#深入剖析 HTTP 的演进)
[1. HTTP/1.0 (1996) - "一次性邮差"](#1. HTTP/1.0 (1996) - “一次性邮差”)
[2. HTTP/1.1 (1997) - "可复用的邮差" - 目前仍最主流](#2. HTTP/1.1 (1997) - “可复用的邮差” - 目前仍最主流)
[3.HTTP/2 (2015) - "超级高速公路"](#3.HTTP/2 (2015) - “超级高速公路”)
[HTTP/1.1 vs HTTP/2 性能对比](#HTTP/1.1 vs HTTP/2 性能对比)
[WebSocket 再深入:它是如何"升级"的?](#WebSocket 再深入:它是如何“升级”的?)
[WebSocket 和Http/2](#WebSocket 和Http/2)
[1. 通信模型的根本差异(这是最重要的区别)](#1. 通信模型的根本差异(这是最重要的区别))
[2. 对"服务器推送"的误解澄清](#2. 对“服务器推送”的误解澄清)
[3. 如何选择?](#3. 如何选择?)
[开发中(SpringBoot Vue)](#开发中(SpringBoot Vue))
[2.WebClient 详细讲解](#2.WebClient 详细讲解)
[发送 + 接收HTTP请求](#发送 + 接收HTTP请求)
[传统HTTP vs WebSocket](#传统HTTP vs WebSocket)
[WebSocket 实际开发详解(用WebSocketHandler)](#WebSocket 实际开发详解(用WebSocketHandler))
[第一步:Spring Boot后端设置](#第一步:Spring Boot后端设置)
[1. 添加依赖](#1. 添加依赖)
[2. 创建WebSocket配置(就像安装电话线路)](#2. 创建WebSocket配置(就像安装电话线路))
[3. 创建WebSocket处理器(就像接线员)](#3. 创建WebSocket处理器(就像接线员))
[1. 创建WebSocket聊天组件](#1. 创建WebSocket聊天组件)
[WebSocket 实际开发详解(用@ServerEndpoint)](#WebSocket 实际开发详解(用@ServerEndpoint))
[第一步:Spring Boot后端配置](#第一步:Spring Boot后端配置)
[1. 添加依赖(与之前相同)](#1. 添加依赖(与之前相同))
[2. 关键配置:启用ServerEndpoint](#2. 关键配置:启用ServerEndpoint)
[3. 使用@ServerEndpoint创建WebSocket端点](#3. 使用@ServerEndpoint创建WebSocket端点)
[4. 业务服务示例(展示依赖注入)](#4. 业务服务示例(展示依赖注入))
[@ServerEndpoint 核心注解说明](#@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.1
Host: example.com
Upgrade: websocket # "我想升级成WebSocket"
Connection: Upgrade
Sec-WebSocket-Key: dGhl... # 暗号的一部分
Sec-WebSocket-Version: 13
服务器 (核对暗号) <--- HTTP Response ---
HTTP/1.1 101 Switching Protocols # "好的,升级成功!"
Upgrade: websocket
Connection: Upgrade
Sec-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. 创建HttpClientRequest
HttpClientRequest 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 {
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
// 可选的定制配置
restTemplate.setErrorHandler(new MyErrorHandler());
// 设置超时时间等...
return restTemplate;
}
}
然后,在需要的地方注入并使用。
@Service
public class UserService {
@Autowired
private 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:postForObject
User 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 {
@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl("https://jsonplaceholder.typicode.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}
3.然后,在需要的地方注入并使用。
@Service
public class ReactiveUserService {
@Autowired
private 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注解。
方式一:
-
创建一个配置类启用WebSocket,并注册WebSocket端点。
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {@Override public 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<>();
// 接收连接建立
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.add(session);
System.out.println("客户端连接: " + session.getId());
// 主动发送欢迎消息
session.sendMessage(new TextMessage("欢迎连接WebSocket!"));
}
// 接收客户端消息
@Override
protected 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);
}
// 接收连接关闭
@Override
public 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 {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
2.然后,使用@ServerEndpoint注解定义一个端点。
@Component
@ServerEndpoint("/my-websocket")
public class MyWebSocketEndpoint {
@OnOpen
public void onOpen(Session session) {
System.out.println("Connected: " + session.getId());
}
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("Received: " + message);
try {
// 发送回复
session.getBasicRemote().sendText("Echo: " + message);
} catch (IOException e) {
e.printStackTrace();
}
}
@OnClose
public void onClose(Session session) {
System.out.println("Closed: " + session.getId());
}
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
}
前端
在Vue组件中,我们使用JavaScript原生的WebSocket API。
基本步骤:
-
在组件创建时(mounted)建立WebSocket连接。
-
监听WebSocket的open、message、close、error事件。
-
在方法中发送消息。
-
在组件销毁前(beforeUnmount)关闭WebSocket连接。
<template>WebSocket聊天室
</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><!-- 显示接收到的消息 --> <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>重新讲解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 { @Override public 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<>(); // 📞 当有客户端连接时(有人打来电话) @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { sessions.add(session); System.out.println("新客户端连接: " + session.getId()); // 🎤 主动向这个客户端发送欢迎消息 session.sendMessage(new TextMessage("欢迎连接!你是第" + sessions.size() +个用户")); // 📢 向所有客户端广播有新用户加入 broadcast("系统消息: 有新用户加入聊天室"); } // 💬 当收到客户端发来的消息时(对方说话了) @Override protected 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); } // 📞 当客户端断开连接时(对方挂电话了) @Override public 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端点 */ @Bean public 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; @Autowired public void setSomeService(SomeService someService) { ChatWebSocketEndpoint.someService = someService; } // 📞 连接建立时调用 @OnOpen public 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); } // 💬 收到客户端消息时调用 @OnMessage public 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); } } // 📞 连接关闭时调用 @OnClose public void onClose(Session session) { String sessionId = session.getId(); sessions.remove(sessionId); System.out.println("❌ 连接关闭: " + sessionId + ", 剩余在线: " + sessions.size()); // 📢 广播用户离开通知 broadcast("系统: 用户 " + sessionId.substring(0, 6) + " 离开聊天室", sessionId); } // ⚠️ 发生错误时调用 @OnError public 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(); } // 👥 获取所有在线会话ID public static Set<String> getOnlineUsers() { return sessions.keySet(); } }4. 业务服务示例(展示依赖注入)
@Service public class SomeService { public String processMessage(String message) { // 这里可以处理业务逻辑,比如保存到数据库、调用其他服务等 return "处理后的消息: " + message.toUpperCase(); } // 主动推送消息到WebSocket public 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: { // 连接WebSocket connect() { 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) { // 错误处理 }重要特性:
- Session对象
- 每个连接都有一个唯一的Session
- 可以存储用户属性:session.getUserProperties().put("key", value)
- 可以发送消息:session.getBasicRemote().sendText(message)
- 依赖注入问题解决
由于@ServerEndpoint实例由WebSocket容器创建,不是Spring管理的,所以需要特殊处理:
@Component @ServerEndpoint("/ws/chat") public class MyEndpoint { private static SomeService someService; @Autowired public void setSomeService(SomeService someService) { MyEndpoint.someService = someService; // 静态变量保存 } }-
路径参数支持
@ServerEndpoint("/ws/chat/{roomId}/{userId}")
public class ChatEndpoint {@OnOpen public 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
-