Websocket 实现前后端双向实时交互
以前很多网站为了实现服务器与浏览器的实时交互,一般采用前端 Ajax 定期轮询或者长连接技术请求后端接口,这种传统的模式带来很明显的缺点,浏览器与服务器之间采用 http 通信效率不高,前后端交互实时性不好,其实本质上还是前端对服务端的单向请求交互,服务端无法主动给前端发送消息。
WebSocket 是 HTML5 提供的一种在单个 TCP 连接上进行全双向通讯的协议,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输,使得客户端和服务器之间的数据交换变得更加简单。
本篇博客通过一个简单的 demo 介绍前后端通过 Websocket 进行双向交互,并在博客下方提供源代码下载。
一、前端 Websocket API
前端创建 Websocket 对象代码如下:
// websocket 的连接协议是 ws,例如 ws://localhost:8086/socket/zhangsan
var socket = new WebSocket(url);
WebSocket事件:
| 事件 | 事件处理程序 | 描述 |
|---|---|---|
| open | socket.onopen | 连接建立时触发 |
| message | socket.onmessage | 客户端接收服务端数据时触发 |
| error | socket.onerror | 通信发生错误时触发 |
| close | socket.onclose | 连接关闭时触发 |
WebSocket方法:
| 方法 | 描述 |
|---|---|
| socket.send() | 使用连接发送数据 |
| socket.close() | 关闭连接 |
本篇博客为了简化前端代码和相关引用,采用传统的 jquery 操作 dom 元素,编写一个 index.html 页面,代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>websocket</title>
<script src="./jquery-3.7.1.min.js"></script>
<script>
//获取浏览器地址栏 get 请求参数
function GetQueryString(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]);
return null;
}
var socket;
if (typeof (WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
} else {
var host = window.location.host;
var username = GetQueryString("username");
//实现化 WebSocket 对象,与服务器建立连接
socket = new WebSocket("ws://" + host + "/socket/" + username);
//打开事件
socket.onopen = function () {
console.log("Socket 已连接");
};
//获得消息事件
socket.onmessage = function (msg) {
console.log(msg.data);
$("#msg").append("接收到消息:" + msg.data + "</br>");
};
//关闭事件
socket.onclose = function () {
console.log("Socket已关闭");
};
//发生了错误事件
socket.onerror = function () {
alert("Socket发生了错误");
}
//关闭连接
function closeWebSocket() {
socket.close();
}
//发送消息
function send() {
var message = $('#text').val();
socket.send(message);
}
}
</script>
</head>
<body>
<div id="msg"></div>
<input id="text" type="text"/>
<button type="button" onclick="send()">发送消息测试</button>
</body>
</html>
二、后端 Websocket 实现
后端 Websocket 跟前端一样,也是实现 4 个事件,只不过可以通过给具体方法加注解进行实现,比较简单。
新建一个名称为 springboot_websocket 的 springboot 工程,其结构如下:

前端的 index.html 页面,放在 resources 下面的 static 目录下,springboot 默认将 static 目录下的文件作为可随意访问的前端文件。
首先看一下 pom 文件,springboot 已经集成了 websocket 依赖包,只需要引入 spring-boot-starter-websocket 即可。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jobs</groupId>
<artifactId>springboot_websocket</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<!--引入 WebSocket 依赖包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.5</version>
</plugin>
</plugins>
</build>
</project>
对于 application.yml 配置文件,只配置了后端服务启动的端口:
server:
port: 8086
我们需要让 spring 容器管理 websocket 对象,暴露 websocket 服务,我这里将代码放在了启动类里面了,代码如下:
package com.jobs;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Slf4j
@SpringBootApplication
public class MainApp {
public static void main(String[] args) {
SpringApplication.run(MainApp.class, args);
log.info("项目已经启动...");
}
//让 spring 容器管理 WebSocket,将服务服务暴露出去
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
自定义编写一个 WebSocketServer 类,里面编写相关方法,通过给方法加注解,实现 websocket 的相关事件:
package com.jobs.websocket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
//配置 WebSocket 服务的连接地址,每次连接都会实例化一个对象
@ServerEndpoint(value = "/socket/{username}")
@Slf4j
@Component
public class WebSocketServer {
//存储 username 与 Session 的对应关系,key 是 username
private static Map<String, Session> sessionMap = new HashMap<String, Session>();
//存储 Session 的 id 和 username 的对应关系
private static Map<String, String> idnameMap = new HashMap<String, String>();
//WebSocket 连接建立后调用该方法
//注意:当前 Socket Session 属于长连接类型(有状态),因此不能持久化对象到数据库中
@OnOpen
public void onOpen(@PathParam("username") String username, Session session) {
//根据 username 获取 Socket Session,如果一个用户重复连接,只保留该用户最后连接的 Socket Session
Session userSession = sessionMap.get(username);
if (userSession != null) {
idnameMap.remove(userSession.getId());
sessionMap.remove(username);
}
//存储 username 和 Socket Session 的对应关系
sessionMap.put(username, session);
//存储 Socket Session 的 ID 和 username 的对应关系
idnameMap.put(session.getId(), username);
}
//关闭链接
@OnClose
public void onClose(Session session) {
//根据 Scocket Session 的 ID 获取 username
String username = idnameMap.get(session.getId());
//移除用户信息
sessionMap.remove(username);
idnameMap.remove(session.getId());
}
//异常处理
@OnError
public void onError(Session session, Throwable throwable) {
String username = idnameMap.get(session.getId());
log.error("用户 " + username + " 的 WebSocket 通信发生了异常:" + throwable.getMessage());
}
//接收客户端发送过来的消息
@OnMessage
public void onMessage(Session session, String message) throws IOException {
String username = idnameMap.get(session.getId());
log.info("用户 " + username + " 接收到客户端发来的消息是:" + message);
//同步给客户端发送消息
session.getBasicRemote().sendText("服务端收到消息:" + message);
//异步给客户端发送消息
//session.getAsyncRemote().sendText("收到的消息:" + message);
}
//封住的消息发送方法,用于其它地方的服务端代码进行调用,给客户端发送消息
public void sendMessage(String username, String message) throws IOException {
//获取用户的 Socket Session 对象
Session session = sessionMap.get(username);
if (session != null) {
//给指定会话发送消息
session.getBasicRemote().sendText(message);
}
}
}
在上面的 WebSocketServer 类中,编写了一个 sendMessage 方法,可以在其他类中注入 WebSocketServer 对象,调用 sendMessage 方法就可以给前端页面发送消息,为了方便测试,这里编写了一个后端接口,可以通过 postman 或 apipost 工具进行调用,测试给前端发送消息:
package com.jobs.controller;
import com.jobs.dto.SendMsgDTO;
import com.jobs.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private WebSocketServer webSocketServer;
//通过后端代码,向指定用户发送信息,测试指定用户的前端页面是否可以收到信息
@PostMapping("/sendmsg")
public String send(@RequestBody SendMsgDTO sendMsgDTO) throws IOException {
webSocketServer.sendMessage(sendMsgDTO.getUsername(), sendMsgDTO.getMsg());
return "send success";
}
}
上面的参数 SendMsgDTO 类只是为了传递参数数据,其内容如下:
package com.jobs.dto;
import lombok.Data;
@Data
public class SendMsgDTO {
//用户名
private String username;
//发送的消息内容
private String msg;
}
三、测试验证效果
启动 springboot 项目,由于我是本地启动,所以访问的域名地址是 localhost,使用 2 个不同的浏览器访问,代表 2 个不同的用户:
使用谷歌浏览器访问 http://localhost:8086?username=lisi 表示用户李四
使用 Edge 浏览器访问 http://localhost:8086?username=zhangsan表示用户张三

浏览器打开后,其实已经跟服务器建立了 websocket 建立了连接,此时通过文本框想服务器发送消息,能够得到服务器的实时响应:

我们使用 apipost 工具,通过 http://localhost:8086/test/sendmsg接口向 lisi 发送消息,lisi 的前端页面能够实时接收到数据,zhangsan 的前端页码则不会收到发给 lisi 的数据,如下图:

然后我们再使用 apipost 工具,通过 http://localhost:8086/test/sendmsg接口向 zhangsan 发送消息,zhangsan 的前端页面能够实时接收到数据,lisi 的前端页码则不会收到发给 zhangsan 的数据,如下图:

本篇博客的源代码下载地址为:https://files.cnblogs.com/files/blogs/699532/springboot_websocket.zip
浙公网安备 33010602011771号