之前一直对websocket非常好奇,今天花了差不多半个下午的时间来实践,还是学会了websocket基本的使用,同时也做了一个简单的demo。可供之后复习回顾使用,哈哈哈
websocket介绍
在单个TCP连接上进行全双工通信的协议,通讯双方建立长链接
- http与websocket通讯方式示意图
1. 连接方式
- HTTP: 是一种请求-响应协议。客户端发送请求,服务器处理并返回响应。每个请求都是独立的,连接在完成后会关闭。
- WebSocket: 是一种持久连接协议。建立连接后,客户端和服务器可以随时互相发送数据,直到连接关闭。
2. 连接建立
- HTTP: 连接每次请求都要重新建立,增加了延迟。
- WebSocket: 通过一次 HTTP 请求建立连接后,后续数据传输不再需要重新建立连接。
3. 数据传输
- HTTP: 采用文本格式,通常使用 JSON 或 XML 传输数据。
- WebSocket: 可以传输文本和二进制数据,支持更高效的数据传输。
4. 实时性
- HTTP: 不支持实时通信,无法实现服务器主动推送数据到客户端。
- WebSocket: 支持实时双向通信,适合需要即时交互的应用,如在线聊天、游戏等。
5. 性能
- HTTP: 每次请求和响应都需要开销,影响性能。
- WebSocket: 由于连接持久化,减少了连接开销,提高了性能。
6. 适用场景
- HTTP: 适用于静态网页、API 请求等场景。
- WebSocket: 适用于需要实时更新的应用,如在线聊天、实时数据推送、多人游戏等。
总结
- 如果需要实时、双向的通信,选择 WebSocket。
- 如果是传统的请求-响应模式,使用 HTTP 更为合适。
后端部分
依赖导入
xml
<!--websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
websocketConfig类配置
java
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();
}
}
实体类 (感觉没啥必要,可以选择不写)
java
import lombok.Data;
import javax.websocket.Session;
@Data
public class SocketDomain {
private Session session;
private String uri;
}
WebsockerServer类实现(多阅读,思考)
在Spring中,@ServerEndpoint
注解用于定义WebSocket端点,类似于Servlet的URL映射。"重点:每次客户端建立一个WebSocket链接都会创建一个新的WebSocketServer
对象",
每次新的WebSocket连接会创建一个新的WebSocketServer
对象。
详细解释:
-
@ServerEndpoint
的行为:- 根据JSR-356规范,
@ServerEndpoint
注解的类是按每个连接创建一个实例的。也就是说,每个WebSocket连接都会创建一个新的实例来处理该连接。
- 根据JSR-356规范,
-
Spring中的
@ServerEndpoint
:- 虽然
WebSocketServer
类上使用了@Component
注解,但@ServerEndpoint
的行为不会受其影响。Spring支持JSR-356规范,因此会按照规范为每个WebSocket连接创建一个新的实例。
- 虽然
-
实例变量的影响:
- 在
WebSocketServer
类中,session
和clientName
是实例变量,每个连接会有自己独立的实例变量,不会互相干扰。 - 静态变量如
onlineCount
和websocketMap
是共享的,所有实例可以访问和修改这些变量,适用于统计在线用户数量和管理连接。
- 在
-
代码设计的合理性:
- 当前的设计是合理的,因为每个连接有独立的实例变量,而静态变量用于共享数据。
- 如果
WebSocketServer
是单例,实例变量如session
和clienName
会被多个连接共享,导致数据混乱。
结论:
每次新的WebSocket连接会创建一个新的WebSocketServer
实例,每个实例独立处理各自的连接,静态变量用于共享数据
具体代码实现
java
package com.example.crucialfunctiontest.server;
import com.example.crucialfunctiontest.model.entity.SocketDomain;
import io.netty.util.internal.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 在Spring中,@ServerEndpoint注解用于定义WebSocket端点,
* 类似于Servlet的URL映射。对于问题"每次建立一个WebSocket链接是否会创建一个新的WebSocketServer对象",
* 答案是:是的,每次新的WebSocket连接会创建一个新的WebSocketServer对象。
*
*
*/
@Component
@Slf4j
@ServerEndpoint("/websocket/{sid}")
public class WebSocketServer {//每次建立一个websocket链接 就会创建一个WebSocketServer对象
//在线客户端数量
private static int onlineCount = 0; //类共享变量
//Map用来存储已连接的客户端信息
private static ConcurrentHashMap<String, SocketDomain> websocketMap = new ConcurrentHashMap<>();
//当前连接客户端的Session信息
private Session session; //实例变量 互不干扰
//当前客户端名称
private String clientName="";
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid){
if(!websocketMap.containsKey(sid)){
WebSocketServer.onlineCount++;
}
this.session = session; //实例变量初始化
this.clientName = sid;
SocketDomain socketDomain = new SocketDomain();
socketDomain.setSession(session);
socketDomain.setUri(session.getRequestURI().toString());
websocketMap.put(clientName, socketDomain); //clientName 唯一
log.info("用户连接:"+ clientName + ",人数:"+onlineCount);
try {
sendMessage("服务端发送的消息,恭喜你连接成功");
} catch (Exception e) {
e.printStackTrace();
}
}
@OnClose
public void onClose(){
if(websocketMap.containsKey(clientName)){
websocketMap.remove(clientName);
onlineCount--;
log.info("用户关闭:"+ clientName + ",人数:"+onlineCount);
}
}
@OnMessage
public void onMessage(String message,Session session){
if(!StringUtil.isNullOrEmpty(message)){
log.info("收到用户消息:"+clientName+",报文:"+message);
}
}
//给当前客户端发消息
private void sendMessage(String obj) {
synchronized (session) {
//嵌套代码块 锁住session 锁住 session 对象。
//使用 session.getAsyncRemote().sendText(obj) 方法异步发送消息。
this.session.getAsyncRemote().sendText(obj);
}
}
//给指定客户端发送消息,通过clientName找到Session发送
public void sendMessageTo(String clientName,String obj){
SocketDomain socketDomain = websocketMap.get(clientName);
try {
if(socketDomain !=null){
socketDomain.getSession().getAsyncRemote().sendText(obj);
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
//给除了当前客户端的其他客户端发消息
private void sendMessageToAllExpectSelf(String message, Session currentSession) {
for(Map.Entry<String, SocketDomain> client : websocketMap.entrySet()){
Session tempSeesion = client.getValue().getSession();
if( !tempSeesion.getId().equals(currentSession.getId())&&tempSeesion.isOpen()){
tempSeesion.getAsyncRemote().sendText(message);
log.info("服务端发送消息给"+client.getKey()+":"+message);
}
}
}
//给包括当前客户端的全部客户端发送消息
public void sendMessageToAll(String message){
for(Map.Entry<String, SocketDomain> client : websocketMap.entrySet()){
Session seesion = client.getValue().getSession();
if(seesion.isOpen()){
seesion.getAsyncRemote().sendText(message);
log.info("服务端发送消息给"+client.getKey()+":"+message);
}
}
}
//给外部调用的方法接口
public void sendAll(String Message){
sendMessageToAll(Message);
}
}
定时任务(每隔一段时间向前端推送数据,前端通过图表形式显示) spring corn表达式只能有六个参数
定时任务corn表达式 在线网站在线Cron表达式生成器 - 码工具
java
@Component
@Slf4j
public class MyTask {
@Autowired
private WebSocketServer webSocketServer;
@Scheduled(cron = "0/5 * * * * ?") //每隔五秒执行一次
public void sendData(){
//System.out.println("sendData");
//发送一个数组 转为json字符串
webSocketServer.sendAll("服务端发来数据hello world(所有人可以接收)");
List<Integer> randomIntegers = new ArrayList<>();
Random random = new Random();
// 生成长度为 7 的随机整数列表
for (int i = 0; i < 7; i++) {
randomIntegers.add(random.nextInt(1000)); // 生成 0-99 之间的随机整数
}
String clientName="forerunner";
//转为json字符串
// 将 List 转换为 JSON 字符串
String jsonString = JSON.toJSONString(randomIntegers);
System.out.println(jsonString);
webSocketServer.sendMessageTo(clientName,jsonString);
}
}
前端vue3代码 -- 主要是onMessage(当接收到后端数据触发) onOpen(链接建立触发)函数
js
<template>
<div class="websocketTest">
<div class="buttonArea">
<el-button @click="webclick">给后台发送消息</el-button>
<el-button type="danger" plain @click="disConnected">断开链接</el-button>
</div>
<div id="myChart" >
</div>
</div>
</template>
<script setup>
import {useRoute} from "vue-router";
import {ref, onUnmounted, onMounted} from 'vue';
import api from "../../api/index.js";
import * as echarts from "echarts";
const ws = ref(null);
const route = useRoute();
/**
* 网页路径应该是 http://127.0.0.1:5173/wsChart?name=forerunner
*/
let id
const paramInit=()=>{
id=route.query['name'];
// 连接 WebSocket 服务端
ws.value= new WebSocket(`ws://localhost:8099/websocket/${id}`);
}
// 定义事件处理函数
const onOpen = () => {
ws.value.send(id+":服务已连接");
console.log(id,":连接状态:", ws.value.readyState); // 1 表示连接打开
let data=[1,2,3,4,5,6,7]
initChart(data);
};
const onClose = () => {
console.log("服务器关闭");
console.log(id,":连接状态:", ws.value.readyState); // 3 表示连接关闭
};
const onMessage = (message) => {
console.log(id,"收到服务器消息:", message.data);
console.log(id,"消息类型", typeof message.data);
console.log("连接状态:", ws.value.readyState);
if(message.data.includes('[')){
let data=JSON.parse(message.data);
initChart(data);
}
// let data=JSON.parse(message.data);
// initChart(data);
};
const onError = (error) => {
console.error(id,"发生错误:", error);
console.log("连接状态:", ws.value.readyState);
};
// 初始化 WebSocket 事件监听器
const wsInit = () => {
ws.value.onopen = onOpen;
ws.value.onclose = onClose;
ws.value.onmessage = onMessage;
ws.value.onerror = onError;
};
onMounted(() => {
paramInit();
wsInit();
initChart()
})
const initChart = (data) => {
const chart = echarts.init(document.getElementById('myChart'));
chart.setOption({
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: data,
type: 'line'
}
]
}
);
}
// 关闭 WebSocket 连接
onUnmounted(() => {
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
ws.value.close();
}
});
// 发送消息方法
const webclick = () => {
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
ws.value.send(id+"发送消息");
console.log("消息已发送");
} else {
console.warn("WebSocket 连接未打开,无法发送消息");
}
};
const disConnected = () => {
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
ws.value.close();
}
}
</script>
<style scoped>
.websocketTest{
margin: auto;
width: 60%;
height: 600px;
background-color: #fbfbfb;
display: flex;
flex-direction: column;
align-items: center;
}
.buttonArea{
margin-top: 10px;
height: 10%;
}
#myChart{
width: 80%;
height: 90%;
background-color: #FBFAFAFF;
}
</style>
具体效果
间隔五秒后端推送数据给前端 ,前端展示数据