赞
踩
先展示一下在线聊天室的效果(需要在两个不同的浏览器中打开)
相信大家在很多网站看到过以上效果:当收到一封新的邮件时,未读邮件图标右上角上的总数就会加一
大家有没有想过,服务器是如何主动地将消息实时推送给客户端的呢
客户端以固定的时间间隔(例如每秒或每几分钟)向服务器发送 HTTP 请求,服务器接收到请求后,处理请求并返回数据给客户端
长轮询是一种改进的轮询技术,客户端向服务器发送 HTTP 请求。服务器收到请求后,会阻塞请求,直到有新数据或者达到指定的超时时间才会返回结果
本文的重点,下面会详细介绍
SSE(Server-Send Event):服务器发送事件,主要用于服务器向客户端推送实时更新(不需要客户端主动请求)
text/event-stream
类型的数据流信息SSE 仅支持从服务器到客户端的单向通信,客户端无法通过 SSE 发送数据到服务器
全双工(Full Duplex):允许数据在两个方向上同时传输
半双工(Half Duplex):允许数据在两个方向上传输,但是同一个时间段内只允许一个方向传输
WebSocket 是一种基于 TCP 的网络通信协议,允许在客户端和服务器建立全双工的通信通道
这意味着客户端和服务器可以在任何时候互相发送消息,不需要像传统的 HTTP 请求那样等待响应
WebSocket 非常适合于需要实时更新数据的应用场景,如在线游戏、实时聊天、实时数据推送等
WebSocket 协议会在客户端和服务器之间建立一条持久的连接通道,连接建立后,双方可以在任意时间通过这个通道发送数据,每次请求无需重新建立连接
WebSocket 的数据传输是双向的,这意味着服务器可以主动向客户端推送数据,而不仅仅是响应客户端的请求
WebSocket 连接建立的步骤:
let webSocket = new WebSocket(URL)
URL说明:
ws
事件 | 事件处理函数 | 描述 |
---|---|---|
open | webSocket.onopen | 连接建立时触发 |
message | webSocket.onmessage | 客户端接收到服务器发送的数据时触发 |
close | webSocket.onclose | 连接关闭时触发 |
方法名称 | 描述 |
---|---|
send() | 发生数据给服务端 |
后端环境:
服务端占用的端口为7024
以下只是简略的步骤,详细实现步骤请参考源代码
Tomcat 从 7.0.5 版本开始支持 WebSocket ,并且实现了 Java WebSocket 规范
Java Websocket 应用由一系列的 Endpoint 组成,Endpoint 是一个 java 对象,代表 WebSocket 连接的一端,对于服务器端,我们可以理解为处理具体 WebSocket 消息的接口
我们可以通过两种方式定义 Endpoint :
通常情况下,对于使用了 @ServerEndpoint
注解的类,Spring 会将其作为单例管理,这意味着无论有多少用户连接到这个 WebSocket 端点,Spring 容器中只有一个该类的实例
这种方式有利于资源管理和性能优化,因为不需要为每一个新的连接创建一个新的端点实例
对于大多数情况而言,这种设计是有利的,因为 WebSocket 端点通常需要处理多个用户的连接和消息,并且共享一些状态或数据(例如广播消息给所有连接的客户端)
通过单例模式管理端点类可以确保所有客户端共享同一个实例,这样可以更容易地实现全局广播或其他跨会话的功能
Endpoint 接口中明确定义了与生命周期相关的方法,各个方法如下:
方法 | 描述 | 注解 |
---|---|---|
onOpen() | 开启一个新的 WebSocket 连接时调用 | @OnOpen |
onClose() | WebSocket 连接关闭时调用 | @OnClpse |
onError() | WebSocket 连接出现异常时调用 | @OnError |
WebSocket
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
Web
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
fastjson2
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.50</version>
</dependency>
编程式:
注解式:
@OnMessage
注解指定接收消息的方法本文使用的是注解式
发送消息由 RemoteEndpoint 完成,其实例由 Session(不是 HttpSession ,是 WebSocketSession)维护
有 2 种发送消息的方式:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
import jakarta.servlet.http.HttpSession; import jakarta.websocket.HandshakeResponse; import jakarta.websocket.server.HandshakeRequest; import jakarta.websocket.server.ServerEndpointConfig; /** * 获取HttpSession,这样的话,ChatEndpoint类就能操作HttpSession */ public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator { @Override public void modifyHandshake(ServerEndpointConfig serverEndpointConfig, HandshakeRequest request, HandshakeResponse response) { // 获取 HttpSession 对象 HttpSession httpSession = (HttpSession) request.getHttpSession(); // 将 httpSession 对象保存起来,存到 ServerEndpointConfig 对象中 // 在 ChatEndpoint 类的 onOpen 方法就能通过 EndpointConfig 对象获取在这里存入的数据 serverEndpointConfig.getUserProperties().put(HttpSession.class.getName(), httpSession); } }
import cn.edu.scau.config.GetHttpSessionConfig; import cn.edu.scau.utils.MessageUtils; import cn.edu.scau.websocket.pojo.Message; import com.alibaba.fastjson2.JSON; import jakarta.servlet.http.HttpSession; import jakarta.websocket.*; import jakarta.websocket.server.ServerEndpoint; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @ServerEndpoint(value = "/chat", configurator = GetHttpSessionConfig.class) @Component public class ChatEndpoint { // 保存在线的用户,key为用户名,value为 Session 对象 private static final Map<String, Session> onlineUsers = new ConcurrentHashMap<>(); private HttpSession httpSession; /** * 建立websocket连接后,被调用 * * @param session Session */ @OnOpen public void onOpen(Session session, EndpointConfig config) { this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName()); String user = (String) this.httpSession.getAttribute("currentUser"); if (user != null) { onlineUsers.put(user, session); } // 通知所有用户,当前用户上线了 String message = MessageUtils.getMessage(true, null, getFriends()); broadcastAllUsers(message); } private Set<String> getFriends() { return onlineUsers.keySet(); } private void broadcastAllUsers(String message) { try { Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet(); for (Map.Entry<String, Session> entry : entries) { // 获取到所有用户对应的 session 对象 Session session = entry.getValue(); // 使用 getBasicRemote() 方法发送同步消息 session.getBasicRemote().sendText(message); } } catch (Exception exception) { exception.printStackTrace(); } } /** * 浏览器发送消息到服务端时该方法会被调用,也就是私聊 * 张三 --> 李四 * * @param message String */ @OnMessage public void onMessage(String message) { try { // 将消息推送给指定的用户 Message msg = JSON.parseObject(message, Message.class); // 获取消息接收方的用户名 String toName = msg.getToName(); String tempMessage = msg.getMessage(); // 获取消息接收方用户对象的 session 对象 Session session = onlineUsers.get(toName); String currentUser = (String) this.httpSession.getAttribute("currentUser"); String messageToSend = MessageUtils.getMessage(false, currentUser, tempMessage); session.getBasicRemote().sendText(messageToSend); } catch (Exception exception) { exception.printStackTrace(); } } /** * 断开 websocket 连接时被调用 * * @param session Session */ @OnClose public void onClose(Session session) throws IOException { // 1.从 onlineUsers 中删除当前用户的 session 对象,表示当前用户已下线 String user = (String) this.httpSession.getAttribute("currentUser"); if (user != null) { Session remove = onlineUsers.remove(user); if (remove != null) { remove.close(); } session.close(); } // 2.通知其他用户,当前用户已下线 // 注意:不是发送类似于 xxx 已下线的消息,而是向在线用户重新发送一次当前在线的所有用户 String message = MessageUtils.getMessage(true, null, getFriends()); broadcastAllUsers(message); } }
前端使用的技术栈:Vue3 + Axios + ElementPlus
以下只是简略的步骤,详细实现步骤请参考源代码
因为项目采用的是前后端分离的开发模式,所以跨域问题是不可避免的
如果不知道怎么解决跨域问题,可以参考我的另一篇文章:Vue3项目(由Vite构建)中通过代理解决跨域问题
当然,如果你有 nginx 的基础,也可以使用 nginx 代理解决跨域问题
向后端发送登录请求需要使用这个 axios 实例
import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 60000, headers: { 'Content-Type': 'application/json;charset=UTF-8' } }) request.interceptors.request.use( ) request.interceptors.response.use(response => { if (response.data) { return response.data } return response }, (error) => { return Promise.reject(error) }) export default request
vite.config.js
import {fileURLToPath, URL} from 'node:url' import {defineConfig} from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue() ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, server: { proxy: { '/api': { target: 'http://localhost:7024', changeOrigin: true, rewrite: (path) => { return path.replace('/api', '') } } } } })
webSocket.value = new WebSocket('ws://localhost:7024/chat')
webSocket.value.onopen = onOpen
// 接收到服务端推送的消息后触发
webSocket.value.onmessage = onMessage
webSocket.value.onclose = onClose
{
"toName": "张三",
"message": "你好"
}
系统消息格式:
{
"system": true,
"fromName": null,
"message": ["李四", "王五"]
}
推送给某一个用户的消息格式:
{
"system": false,
"fromName": "张三",
"message": "你好"
}
备注:
WebSocket 服务端:在线聊天室-服务端
WebSocket 客户端:在线聊天室-客户端
视频教程:在线聊天室
项目的实际意义不大,但是可以让小白初步了解 WebSocket
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。