1. 简介
WebSocket是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议。
WebSocket的出现是为了解决Http协议只能在客户端发送请求后服务端响应请求的问题,它允许服务端主动向客户端发送请求。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在大多数情况下,为了实现消息推送,往往采用Ajax轮询方式,它遵循的是Http协议,在特定的时间内向服务端发送请求,Http协议的请求头较长,可能仅仅需要获取较小的数据而需要携带较多的数据,而且对于消息不是特别频繁的时候,大部分的轮询都是无意的,造成了极大的资源浪费。
HTML5定义的WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
2. 本文简要
本文基于SpringBoot框架整合WebSocket,实现三种模式发送消息:
- 自己给自己发送消息
- 自己给其他用户发送消息
- 自己给指定用户发送消息
3. 示例代码
- 创建工程
- 修改pom.xml
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.c3stones</groupId>
<artifactId>spring-boot-websocket-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-websocket-demo</name>
<description>Spring Boot WebSocket Demo</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.8.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 创建配置文件
在resources目录下创建application.yml。
server:
port: 8080
spring:
thymeleaf:
prefix: classpath:/view/
suffix: .html
encoding: UTF-8
servlet:
content-type: text/html
# 生产环境设置true
cache: false
- 添加WebSocket配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket配置类
*
* @author CL
*
*/
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter
* <p>
* 该Bean会自动注册添加@ServerEndpoint注解的WebSocket端点
* </p>
*
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
- 创建启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 启动类
*
* @author CL
*
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3.1 自己给自己发送消息
- 创建服务端点
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
/**
* <b style="color: blue"> 自己给自己发送消息 </b>
*
* @author CL
*
*/
@Slf4j
@Component
@ServerEndpoint(value = "/selfToSelf")
public class SelfToSelfServer {
/**
* 在线数
* <p>
* 多线程环境下,为了保证线程安全
* </p>
*/
private static AtomicInteger online = new AtomicInteger(0);
/**
* 建立连接
*
* @param session 客户端连接对象
*/
@OnOpen
public void onOpen(Session session) {
// 在线数加1
online.incrementAndGet();
log.info("客户端连接建立成功,Session ID:{},当前在线数:{}", session.getId(), online.get());
}
/**
* 接收客户端消息
*
* @param message 客户端发送的消息内容
* @param session 客户端连接对象
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("服务端接收消息成功,Session ID:{},消息内容:{}", session.getId(), message);
// 处理消息,并响应给客户端
this.sendMessage(message, session);
}
/**
* 处理消息,并响应给客户端
*
* @param message 客户端发送的消息内容
* @param session 客户端连接对象
*/
private void sendMessage(String message, Session session) {
try {
String response = "Server Response ==> " + message;
session.getBasicRemote().sendText(response);
log.info("服务端响应消息成功,接收的Session ID:{},响应内容:{}", session.getId(), response);
} catch (IOException e) {
log.error("服务端响应消息异常:{}", e.getMessage());
}
}
/**
* 关闭连接
*
* @param session 客户端连接对象
*/
@OnClose
public void onClose(Session session) {
// 在线数减1
online.decrementAndGet();
log.info("客户端连接关闭成功,Session ID:{},当前在线数:{}", session.getId(), online.get());
}
/**
* 连接异常
*
* @param session 客户端连接对象
* @param error 异常
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("连接异常:{}", error);
}
}
- 创建测试页面
在resource下创建views文件夹,并创建测试页面selfToSelfClient.html:
<!DOCTYPE HTML>
<html>
<head>
<title>WebSocket - Self To Self</title>
</head>
<body>
<input id="message" type="text" />
<button onclick="sendMessage()">发送</button>
<button onclick="closeWebSocket()">关闭</button>
<hr/>
<div id="response"></div>
</body>
<script type="text/javascript">
var websocket = null;
// 判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8080/selfToSelf");
} else {
alert("当前浏览器不支持WebSocket");
}
// 建立连接成功时的回调方法
websocket.onopen = function (event) {
printMessage("green", "建立连接成功!!!");
}
// 连接异常时的回调方法
websocket.onerror = function (event) {
printMessage("red", "连接异常!!!");
};
// 客户端接收消息时的回调方法
websocket.onmessage = function (event) {
printMessage("blue", event.data);
}
// 关闭连接时的回调方法
websocket.onclose = function() {
printMessage("yellow", "关闭连接成功!!!");
}
// 监听窗口关闭事件,当窗口关闭时,主动关闭连接,防止连接未断开时关闭窗口,服务端抛出异常
window.onbeforeunload = function() {
websocket.close();
}
// 发送消息
function sendMessage() {
if (websocket.readyState != 1) {
printMessage("red", "未建立连接或者连接已处于关闭状态!");
} else {
var message = document.getElementById('message').value;
websocket.send(message);
}
}
// 关闭连接
function closeWebSocket() {
websocket.close();
}
// 打印消息
function printMessage(color, text) {
document.getElementById("response").innerHTML += "<font color='" + color +"'>" + text + "</font><br/>";
}
</script>
</html>
- 创建跳转页面Controller
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 页面视图Controller
*
* @author CL
*
*/
@Controller
public class ViewController {
/**
* 跳转到<b> 自己给自己发送消息 </b>页面
*
* @return
*/
@RequestMapping(value = "/selfToSelf")
public String selfToSelf() {
return "selfToSelfClient";
}
}
- 测试
- 启动项目
- 浏览器访问:http://127.0.0.1:8080/selfToSelf
- 控制台打印日志
2020-12-29 19:21:39.916 INFO 1096 --- [nio-8080-exec-2] com.c3stones.server.SelfToSelfServer : 客户端连接建立成功,Session ID:0,当前在线数:1 2020-12-29 19:21:43.564 INFO 1096 --- [nio-8080-exec-3] com.c3stones.server.SelfToSelfServer : 服务端接收消息成功,Session ID:0,消息内容:测试1 2020-12-29 19:21:43.583 INFO 1096 --- [nio-8080-exec-3] com.c3stones.server.SelfToSelfServer : 服务端响应消息成功,接收的Session ID:0,响应内容:Server Response ==> 测试1 2020-12-29 19:21:45.290 INFO 1096 --- [nio-8080-exec-4] com.c3stones.server.SelfToSelfServer : 客户端连接关闭成功,Session ID:0,当前在线数:0
- 浏览器截图
3.2 自己给其他用户发送消息
- 创建服务端点
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
/**
* <b style="color: blue"> 自己给其他用户发送消息 </b>
*
* @author CL
*
*/
@Slf4j
@Component
@ServerEndpoint(value = "/selfToOther")
public class SelfToOtherServer {
/**
* 在线数
* <p>
* 多线程环境下,为了保证线程安全
* </p>
*/
private static AtomicInteger online = new AtomicInteger(0);
/**
* 在线客户端连接集合
* <p>
* 多线程环境下,为了保证线程安全
* </p>
*/
private static Map<String, Session> onlineMap = new ConcurrentHashMap<>();
/**
* 建立连接
*
* @param session 客户端连接对象
*/
@OnOpen
public void onOpen(Session session) {
// 在线数加1
online.incrementAndGet();
// 存放客户端连接
onlineMap.put(session.getId(), session);
log.info("客户端连接建立成功,Session ID:{},当前在线数:{}", session.getId(), online.get());
}
/**
* 接收客户端消息
*
* @param message 客户端发送的消息内容
* @param session 客户端连接对象
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("服务端接收消息成功,Session ID:{},消息内容:{}", session.getId(), message);
// 处理消息,并响应给客户端
this.sendMessage(message, session);
}
/**
* 处理消息,并响应给客户端
*
* @param message 客户端发送的消息内容
* @param session 客户端连接对象
*/
private void sendMessage(String message, Session session) {
String response = "Server Response ==> " + message;
for (Map.Entry<String, Session> sessionEntry : onlineMap.entrySet()) {
Session s = sessionEntry.getValue();
// 过滤自己
if (!(session.getId()).equals(s.getId())) {
log.info("服务端响应消息成功,接收的Session ID:{},响应内容:{}", s.getId(), response);
s.getAsyncRemote().sendText(response);
}
}
}
/**
* 关闭连接
*
* @param session 客户端连接对象
*/
@OnClose
public void onClose(Session session) {
// 在线数减1
online.decrementAndGet();
// 移除关闭的客户端连接
onlineMap.remove(session.getId());
log.info("客户端连接关闭成功,Session ID:{},当前在线数:{}", session.getId(), online.get());
}
/**
* 连接异常
*
* @param session 客户端连接对象
* @param error 异常
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("连接异常:{}", error);
}
}
- 创建测试页面
在resource下的views文件夹创建测试页面selfToOtherClient.html:
<!DOCTYPE HTML>
<html>
<head>
<title>WebSocket - Self To Other</title>
</head>
<body>
<input id="message" type="text" />
<button onclick="sendMessage()">发送</button>
<button onclick="closeWebSocket()">关闭</button>
<hr/>
<div id="response"></div>
</body>
<script type="text/javascript">
var websocket = null;
// 判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8080/selfToOther");
} else {
alert("当前浏览器不支持WebSocket");
}
// 建立连接成功时的回调方法
websocket.onopen = function (event) {
printMessage("green", "建立连接成功!!!");
}
// 连接异常时的回调方法
websocket.onerror = function (event) {
printMessage("red", "连接异常!!!");
};
// 客户端接收消息时的回调方法
websocket.onmessage = function (event) {
printMessage("blue", event.data);
}
// 关闭连接时的回调方法
websocket.onclose = function() {
printMessage("yellow", "关闭连接成功!!!");
}
// 监听窗口关闭事件,当窗口关闭时,主动关闭连接,防止连接未断开时关闭窗口,服务端抛出异常
window.onbeforeunload = function() {
websocket.close();
}
// 发送消息
function sendMessage() {
if (websocket.readyState != 1) {
printMessage("red", "未建立连接或者连接已处于关闭状态!");
} else {
var message = document.getElementById('message').value;
websocket.send(message);
}
}
// 关闭连接
function closeWebSocket() {
websocket.close();
}
// 打印消息
function printMessage(color, text) {
document.getElementById("response").innerHTML += "<font color='" + color +"'>" + text + "</font><br/>";
}
</script>
</html>
- 添加跳转页面方法
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 页面视图Controller
*
* @author CL
*
*/
@Controller
public class ViewController {
/**
* 跳转到<b> 自己给自己发送消息 </b>页面
*
* @return
*/
@RequestMapping(value = "/selfToSelf")
public String selfToSelf() {
return "selfToSelfClient";
}
/**
* 跳转到<b> 自己给其他用户发送消息 </b>页面
*
* @return
*/
@RequestMapping(value = "/selfToOther")
public String selfToOther() {
return "selfToOtherClient";
}
}
- 测试
- 启动项目
- 浏览器访问(建议打开3个页签):http://127.0.0.1:8080/selfToOther
- 控制台打印日志
2020-12-29 19:53:52.654 INFO 6420 --- [nio-8080-exec-2] com.c3stones.server.SelfToOtherServer : 客户端连接建立成功,Session ID:0,当前在线数:1 2020-12-29 19:54:01.044 INFO 6420 --- [nio-8080-exec-5] com.c3stones.server.SelfToOtherServer : 客户端连接建立成功,Session ID:1,当前在线数:2 2020-12-29 19:54:14.494 INFO 6420 --- [nio-8080-exec-8] com.c3stones.server.SelfToOtherServer : 客户端连接建立成功,Session ID:2,当前在线数:3 2020-12-29 19:54:27.705 INFO 6420 --- [nio-8080-exec-9] com.c3stones.server.SelfToOtherServer : 服务端接收消息成功,Session ID:0,消息内容:测试1 2020-12-29 19:54:27.705 INFO 6420 --- [nio-8080-exec-9] com.c3stones.server.SelfToOtherServer : 服务端响应消息成功,接收的Session ID:1,响应内容:Server Response ==> 测试1 2020-12-29 19:54:27.753 INFO 6420 --- [nio-8080-exec-9] com.c3stones.server.SelfToOtherServer : 服务端响应消息成功,接收的Session ID:2,响应内容:Server Response ==> 测试1 2020-12-29 19:54:43.084 INFO 6420 --- [nio-8080-exec-2] com.c3stones.server.SelfToOtherServer : 服务端接收消息成功,Session ID:1,消息内容:测试2 2020-12-29 19:54:43.085 INFO 6420 --- [nio-8080-exec-2] com.c3stones.server.SelfToOtherServer : 服务端响应消息成功,接收的Session ID:0,响应内容:Server Response ==> 测试2 2020-12-29 19:54:43.086 INFO 6420 --- [nio-8080-exec-2] com.c3stones.server.SelfToOtherServer : 服务端响应消息成功,接收的Session ID:2,响应内容:Server Response ==> 测试2 2020-12-29 19:54:48.474 INFO 6420 --- [nio-8080-exec-5] com.c3stones.server.SelfToOtherServer : 服务端接收消息成功,Session ID:2,消息内容:测试3 2020-12-29 19:54:48.474 INFO 6420 --- [nio-8080-exec-5] com.c3stones.server.SelfToOtherServer : 服务端响应消息成功,接收的Session ID:0,响应内容:Server Response ==> 测试3 2020-12-29 19:54:48.475 INFO 6420 --- [nio-8080-exec-5] com.c3stones.server.SelfToOtherServer : 服务端响应消息成功,接收的Session ID:1,响应内容:Server Response ==> 测试3
- 浏览器截图
3.3 自己给指定用户发送消息
- 创建服务端点
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
/**
* <b style="color: blue"> 自己给指定用户发送消息 </b>
*
* @author CL
*
*/
@Slf4j
@Component
@ServerEndpoint(value = "/selfToSpecific")
public class SelfToSpecificServer {
/**
* 在线数
* <p>
* 多线程环境下,为了保证线程安全
* </p>
*/
private static AtomicInteger online = new AtomicInteger(0);
/**
* 在线客户端连接集合
* <p>
* 多线程环境下,为了保证线程安全
* </p>
*/
private static Map<String, Session> onlineMap = new ConcurrentHashMap<>();
/**
* 建立连接
*
* @param session 客户端连接对象
*/
@OnOpen
public void onOpen(Session session) {
// 在线数加1
online.incrementAndGet();
// 存放客户端连接
onlineMap.put(session.getId(), session);
log.info("客户端连接建立成功,Session ID:{},当前在线数:{}", session.getId(), online.get());
}
/**
* 接收客户端消息
*
* @param message 客户端发送的消息内容
* @param session 客户端连接对象
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("服务端接收消息成功,Session ID:{},消息内容:{}", session.getId(), message);
// 解析出指定用户
JSONObject jsonObj = JSONUtil.parseObj(message);
if (jsonObj != null) {
Session s = onlineMap.get(jsonObj.get("sessionId"));
// 处理消息,并响应给客户端
this.sendMessage(jsonObj.get("message").toString(), s);
}
}
/**
* 处理消息,并响应给客户端
*
* @param message 客户端发送的消息内容
* @param session 客户端连接对象
*/
private void sendMessage(String message, Session session) {
try {
String response = "Server Response ==> " + message;
session.getBasicRemote().sendText(response);
log.info("服务端响应消息成功,接收的Session ID:{},响应内容:{}", session.getId(), response);
} catch (IOException e) {
log.error("服务端响应消息异常:{}", e.getMessage());
}
}
/**
* 关闭连接
*
* @param session 客户端连接对象
*/
@OnClose
public void onClose(Session session) {
// 在线数减1
online.decrementAndGet();
// 移除关闭的客户端连接
onlineMap.remove(session.getId());
log.info("客户端连接关闭成功,Session ID:{},当前在线数:{}", session.getId(), online.get());
}
/**
* 连接异常
*
* @param session 客户端连接对象
* @param error 异常
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("连接异常:{}", error);
}
}
- 创建测试页面
在resource下的views文件夹创建测试页面selfToSpecificClient.html:
<!DOCTYPE HTML>
<html>
<head>
<title>WebSocket - Self To Specific</title>
</head>
<body>
消息:<input id="message" type="text" /><br/>
Session Id:<input id="sessionId" type="text" /><br/>
<button onclick="sendMessage()">发送</button>
<button onclick="closeWebSocket()">关闭</button>
<hr/>
<div id="response"></div>
</body>
<script type="text/javascript">
var websocket = null;
// 判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8080/selfToSpecific");
} else {
alert("当前浏览器不支持WebSocket");
}
// 建立连接成功时的回调方法
websocket.onopen = function (event) {
printMessage("green", "建立连接成功!!!");
}
// 连接异常时的回调方法
websocket.onerror = function (event) {
printMessage("red", "连接异常!!!");
};
// 客户端接收消息时的回调方法
websocket.onmessage = function (event) {
printMessage("blue", event.data);
}
// 关闭连接时的回调方法
websocket.onclose = function() {
printMessage("yellow", "关闭连接成功!!!");
}
// 监听窗口关闭事件,当窗口关闭时,主动关闭连接,防止连接未断开时关闭窗口,服务端抛出异常
window.onbeforeunload = function() {
websocket.close();
}
// 发送消息
function sendMessage() {
if (websocket.readyState != 1) {
printMessage("red", "未建立连接或者连接已处于关闭状态!");
} else {
var message = document.getElementById('message').value;
var sessionId = document.getElementById('sessionId').value;
var obj = new Object();
obj.message = message;
obj.sessionId = sessionId;
websocket.send(JSON.stringify(obj));
}
}
// 关闭连接
function closeWebSocket() {
websocket.close();
}
// 打印消息
function printMessage(color, text) {
document.getElementById("response").innerHTML += "<font color='" + color +"'>" + text + "</font><br/>";
}
</script>
</html>
- 添加跳转页面方法
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 页面视图Controller
*
* @author CL
*
*/
@Controller
public class ViewController {
/**
* 跳转到<b> 自己给自己发送消息 </b>页面
*
* @return
*/
@RequestMapping(value = "/selfToSelf")
public String selfToSelf() {
return "selfToSelfClient";
}
/**
* 跳转到<b> 自己给其他用户发送消息 </b>页面
*
* @return
*/
@RequestMapping(value = "/selfToOther")
public String selfToOther() {
return "selfToOtherClient";
}
/**
* 跳转到<b> 自己给指定用户发送消息 </b>页面
*
* @return
*/
@RequestMapping(value = "/selfToSpecific")
public String selfToSpecific() {
return "selfToSpecificClient";
}
}
- 测试
- 启动项目
- 浏览器访问(打开2个页签):http://127.0.0.1:8080/selfToSpecific
- 控制台打印日志
2020-12-29 20:27:47.043 INFO 9004 --- [nio-8080-exec-2] c.c3stones.server.SelfToSpecificServer : 客户端连接建立成功,Session ID:0,当前在线数:1 2020-12-29 20:27:49.558 INFO 9004 --- [nio-8080-exec-4] c.c3stones.server.SelfToSpecificServer : 客户端连接建立成功,Session ID:1,当前在线数:2 2020-12-29 20:27:56.886 INFO 9004 --- [nio-8080-exec-7] c.c3stones.server.SelfToSpecificServer : 服务端接收消息成功,Session ID:0,消息内容:{"message":"测试1","sessionId":"1"} 2020-12-29 20:28:06.785 INFO 9004 --- [nio-8080-exec-7] c.c3stones.server.SelfToSpecificServer : 服务端响应消息成功,接收的Session ID:1,响应内容:Server Response ==> 测试1 2020-12-29 20:28:19.007 INFO 9004 --- [io-8080-exec-10] c.c3stones.server.SelfToSpecificServer : 服务端接收消息成功,Session ID:1,消息内容:{"message":"测试2","sessionId":"0"} 2020-12-29 20:28:19.008 INFO 9004 --- [io-8080-exec-10] c.c3stones.server.SelfToSpecificServer : 服务端响应消息成功,接收的Session ID:0,响应内容:Server Response ==> 测试2
- 浏览器截图