1、WebSocket 前置基础
1.1 WebSocket 与 Http对比
连接方式
- HTTP: HTTP 是基于请求-响应模式的协议。客户端发送请求,服务器响应请求。每次请求都需要建立新的连接,完成数据传输后连接断开。
- WebSocket: WebSocket 是一种全双工协议,建立连接后,客户端和服务器之间可以进行双向通信,无需每次都建立新的连接。
数据传输方式
- HTTP: HTTP 使用明文传输数据,数据以消息的形式进行传输。
- WebSocket: WebSocket 使用二进制帧传输数据,可以有效地减少数据传输量。
效率
- HTTP: HTTP 的效率受到连接建立和关闭的开销影响,尤其是在频繁请求的情况下。
- WebSocket: WebSocket 由于保持连接,可以避免每次请求的连接建立和关闭开销,效率更高。
实时性
- HTTP: HTTP 无法实现实时通信,因为每次请求都需要等待响应。
- WebSocket: WebSocket 可以实现实时双向通信,适合需要实时数据交互的应用场景。
1.2 WebSocket 工作原理
- 握手阶段: 客户端向服务器发起一个 WebSocket 握手请求,请求升级 HTTP 连接为 WebSocket 连接。握手请求包含一些特定的头部信息,例如
Connection: Upgrade
,Upgrade: websocket
,Sec-WebSocket-Key
等。 - 建立连接: 服务器接收到握手请求后,会进行验证,如果验证成功,会发送一个 WebSocket 握手响应,表明连接成功建立。
- 数据传输: 连接建立后,客户端和服务器就可以通过 WebSocket 进行双向的数据传输了。数据传输采用帧的形式,每个帧包含 opcode、掩码、数据长度、数据等信息。
- 关闭连接: 当客户端或服务器一方需要关闭连接时,可以通过发送一个关闭帧来关闭连接。
1.3 WebSocket 应用场景
- 实时聊天: WebSocket 非常适合用来构建实时聊天应用,因为可以实时地将消息传递给所有参与者。
- 游戏开发: 在多人在线游戏中,WebSocket 可以用来实时同步游戏状态、玩家位置、聊天信息等数据。
- 股票行情: WebSocket 可以用来实时推送股票行情信息,让用户能够及时了解最新的市场动态。
- 数据监控: WebSocket 可以用来实时监控数据,例如网站流量、服务器状态、传感器数据等。
2、代码案例
源码地址 ---> websocket: websocket demo (gitee.com)
2.1 前端代码
<!DOCTYPE html>
<html lang="zh">
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<head>
<title>WebSocket 页面</title>
</head>
<body>
WebSocket Simple Demo
<p>登录用户名:</p>
<input type="text" id="usernameInput" />
<button onclick="login()" id = "login">登录</button>
<button onclick="logout()" id = "logout">退出登录</button>
<p>输入要发送的消息(eg. tom:msg 给tom发送msg):</p>
<input type="text" id="messageInput" />
<button onclick="sendMessage()" id="send">发送</button>
<p>接收到的消息:</p>
<button onclick="clearMsg()">清空消息</button>
<div id="messageOutput"></div>
<script>
document.getElementById('login').disabled = false;
document.getElementById('logout').disabled = true;
document.getElementById('send').disabled = true;
let ws; // 全局变量存储 WebSocket 连接
// 连接 WebSocket
function login() {
const username = document.getElementById('usernameInput').value;
if (username === null || username.trim() === '') {
alert('请输入用户名!');
return
} else {
// 用户名有效,进行其他操作
console.log(`用户名是:${username}`);
}
document.getElementById('login').disabled = true;
document.getElementById('logout').disabled = false;
document.getElementById('send').disabled = false;
ws = new WebSocket('ws://localhost:8080/simple/' + username);
// 连接成功
ws.onopen = function(event) {
alert("登录成功")
};
ws.onclose = function(event) {
document.getElementById('usernameInput').value = '';
document.getElementById('login').disabled = false;
document.getElementById('logout').disabled = true;
document.getElementById('send').disabled = true;
}
// 收到消息
ws.onmessage = function(event) {
const message = event.data;
document.getElementById('messageOutput').innerHTML += message + '<br>';
};
// 连接错误
ws.onerror = function(event) {
console.error('WebSocket 连接错误!');
};
}
function clearMsg() {
document.getElementById('messageOutput').innerHTML = '';
}
// 退出登录
function logout() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close() // 连接关闭
document.getElementById('usernameInput').value = '';
document.getElementById('login').disabled = false;
document.getElementById('logout').disabled = true;
document.getElementById('send').disabled = true;
alert("退出成功")
} else {
console.warn('WebSocket 连接未建立,无法发送消息');
}
}
// 发送消息
function sendMessage() {
const message = document.getElementById('messageInput').value;
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(message);
document.getElementById('messageInput').value = '';
} else {
console.warn('WebSocket 连接未建立,无法发送消息');
}
}
</script>
</body>
</html>
2.2 spring后端代码
maven依赖
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncodi
<spring-boot.version>2.6.13</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
config 配置
package com.xy.websocket.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @author 谢阳
* @version 1.0
* @date 2024-07-11 14:35
* @description 自动注册使用@ServerEndpoint注解
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocket类
package com.xy.websocket.ws;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
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.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author 谢阳
* @version 1.0
* @date 2024-07-11 14:41
* @description
*/
@ServerEndpoint("/simple/{name}")
@Slf4j
@Component
@EnableScheduling
public class SimpleEndpoint {
private static final Map<String, Session> sessions = new HashMap<>();
private final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@OnOpen
public void onOpen(Session session,@PathParam("name") String name) {
sessions.put(name, session);
log.info("注册用户 [{}],同时在线数 [{}]",name,sessions.size());
}
@OnMessage
public void onMessage(String message, Session session,@PathParam("name") String name) {
String[] split = message.split(":");
if (split.length == 2) {
Session sendSession = sessions.get(split[0]);
if (sendSession != null) {
LocalDateTime now = LocalDateTime.now();
String format = DTF.format(now);
sendSession.getAsyncRemote().sendText(format + " " + name + ":" + split[1]);
} else {
session.getAsyncRemote().sendText("用户未上线");
}
} else{
session.getAsyncRemote().sendText("服务端收到消息");
}
}
@OnClose
public void onClose(Session session, @PathParam("name") String name) throws IOException {
int oldSize = sessions.size();
Session remove = sessions.remove(name);
log.info("在线数 [{}],退出用户 [{}],剩余在线 [{}] 个",oldSize,name,sessions.size());
remove.close();
}
@Scheduled(cron = "*/20 * * * * ?")
public void task() {
LocalDateTime now = LocalDateTime.now();
String format = DTF.format(now);
sessions.forEach((k, v) -> {
String string = Arrays.stream(sessions.keySet().toArray()).collect(Collectors.toList()).toString();
v.getAsyncRemote().sendText(format + " 同时在线数 [" + sessions.size()+ "] 个" + ", 在线用户 " + string);
});
}
}