文章目录

  • ​​1.`WebSocket`简介​​
  • ​​2.`WebSocket`特点​​
  • ​​3.`WebSocket` 属性​​
  • ​​4.`WebSocket` 事件​​
  • ​​5.`WebSocket` 方法​​
  • ​​二、SpringBoot2.x整合WebSoket​​
  • ​​1. 实现目标​​
  • ​​2. 步骤​​
  • ​​2.1 新建SpringBoot工程,添加Maven依赖​​
  • ​​2.2项目结构如下​​
  • ​​2.3服务端配置​​
  • ​​2.4客户端基本使用​​
  • ​​二、测试​​
  • ​​2.1启动项目​​
  • ​​2.2访问测试页​​
  • ​​2.3加入用户ID为100、200两个用户​​
  • ​​2.4 服务端日志​​
  • ​​2.5 100 给 200 发消息​​
  • ​​2.6 200收到消息​​
  • ​​2.7 100 给离线用户 666发消息​​
  • ​​2.8 666离线用户上线​​
  • ​​2.9 admin 用户群发​​


## 一、

​WebSocket​

1.​​WebSocket​​简介

WebSocket是一种通信协议,可在单个TCP连接上进行全双工通信。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。

WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于​​服务器推送技术​​的一种。

2.​​WebSocket​​特点

  • 建立在 TCP 协议之上,服务器端的实现比较容易
  • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器
  • 数据格式比较轻量,性能开销小,通信高效
  • 可以发送文本,也可以发送二进制数据
  • 没有同源限制,客户端可以与任意服务器通信
  • 协议标识符是​​ws​​​(如果加密,则为​​wss​​),服务器网址就是 URL

3.​​WebSocket​​ 属性

以下是 WebSocket 对象的属性。假定我们使用了以上代码创建了 Socket 对象:

属性

描述

Socket.readyState

只读属性 readyState 表示连接状态,可以是以下值:0 - 表示连接尚未建立。1 - 表示连接已建立,可以进行通信。2 - 表示连接正在进行关闭。3 - 表示连接已经关闭或者连接不能打开。

Socket.bufferedAmount

只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。

4.​​WebSocket​​ 事件

以下是 WebSocket 对象的相关事件。假定我们使用了以上代码创建了 Socket 对象:

事件

事件处理程序

描述

open

Socket.onopen

连接建立时触发

message

Socket.onmessage

客户端接收服务端数据时触发

error

Socket.onerror

通信发生错误时触发

close

Socket.onclose

连接关闭时触发

5.​​WebSocket​​ 方法

以下是 WebSocket 对象的相关方法。假定我们使用了以上代码创建了 Socket 对象:

方法

描述

Socket.send()

使用连接发送数据

Socket.close()

关闭连接

二、SpringBoot2.x整合WebSoket

1. 实现目标

  • 私发消息
  • 群发消息
  • 支持接收离线消息(内存存储)
  • 统计未读消息数量

2. 步骤

2.1 新建SpringBoot工程,添加Maven依赖

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.dw</groupId>
<artifactId>springboot-websocket</artifactId>
<version>1.0</version>
<name>springboot-websocket</name>
<description>springboot-websocket</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.46</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

2.2项目结构如下

SpringBoot2.x整合WebSoket_websocket

2.3服务端配置

package com.dw.sprboosoc.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
* WebSocket 配置类
* dingwen
* 2021/2/20 10:37
**/
@Configuration
public class WebSocketConfig {

/*
* ServerEndpointExporter 会自动注册使用@ServerEndpoint注解声明的websocket endpoint
* @param []
* @return org.springframework.web.socket.server.standard.ServerEndpointExporter
*/
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}

WebSocketServer

package com.dw.sprboosoc.service;

import com.alibaba.fastjson.JSON;
import com.dw.sprboosoc.constant.MessageEnum;
import com.dw.sprboosoc.dto.WebSocketMessageDto;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
* WebSocket 核心类
* dingwen
* 2021/2/20 10:41
**/
// 添加Bean
@Component
@Slf4j
//访问路径
@ServerEndpoint(value = "/websocket/{sendUserId}")
public class WebSocketServer {
//当前在线连接数,保证线程安全
private static final AtomicInteger currentOnlineNumber = new AtomicInteger();
//concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象
private static final ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<>();
//设置为静态的 公用一个消息map ConcurrentMap为线程安全的map HashMap不安全
private static final ConcurrentMap<String, Map<String, List<WebSocketMessageDto>>> messageMap = new ConcurrentHashMap<>();

/*
*发送消息
* @param [session, message, userId]
* @return void
*/
public void sendMessage(WebSocketMessageDto webSocketMessageDto) throws IOException {
try {
switch (webSocketMessageDto.getMessageEnum()) {
// 广播消息
case all:
sessionPool.values().forEach(se -> {
try {
se.getBasicRemote()
.sendText(webSocketMessageDto.toString());
} catch (IOException e) {
e.printStackTrace();
}
});
log.info("websocket: 广播消息" + webSocketMessageDto);
// 离线
storeOfflineMessage(webSocketMessageDto);
break;
// 私发
case one:
if (judgeUserOnline(webSocketMessageDto.getRecvUserId())) {
// 在线
sessionPool.get(webSocketMessageDto.getRecvUserId())
.getBasicRemote()
.sendText(webSocketMessageDto.toString());
} else {
// 离线
storeOfflineMessage(webSocketMessageDto);
}

log.info("websocket: 私发消息" + webSocketMessageDto);
break;
}


} catch (Exception exception) {
log.error("websocket: 发送消息发生了错误");
}
}

/*
*客户端收到消息
* @param [message]
* @return void
*/
@OnMessage
public void onMessage(String webSocketMessageDtoStr) throws IOException {
WebSocketMessageDto webSocketMessageDto = JSON.parseObject(webSocketMessageDtoStr, WebSocketMessageDto.class);
log.info("websocket:" + webSocketMessageDto.getRecvUserId() + "收到,来自" + webSocketMessageDto.getSendUserId() + "发送的消息" + webSocketMessageDto.getMessage());
sendMessage(webSocketMessageDto);
}

/*
*判断用户是否在线
* @param [recvUserId]
* @return boolean
*/
public boolean judgeUserOnline(String recvUserId) {
boolean flag = !ObjectUtils.isEmpty(sessionPool.get(recvUserId));
String flagStr = flag ? "在线" : "离线";
log.info("websocket: " + recvUserId + ":" + flagStr);
return flag;
}

/*
*用户离线时把消息存储到内存
* @param [recvUserId]
* @return void
*/
public void storeOfflineMessage(WebSocketMessageDto webSocketMessageDto) {
//用户不在线时 第一次给他发消息
if (ObjectUtils.isEmpty(messageMap.get(webSocketMessageDto.getRecvUserId()))) {
Map<String, List<WebSocketMessageDto>> maps = new HashMap<>();
List<WebSocketMessageDto> list = new ArrayList<>();
list.add(webSocketMessageDto);
maps.put(webSocketMessageDto.getRecvUserId(), list);
messageMap.put(webSocketMessageDto.getRecvUserId(), maps);
} else {
//用户不在线时 再次发送消息
Map<String, List<WebSocketMessageDto>> listObject = messageMap.get(webSocketMessageDto.getRecvUserId());
List<WebSocketMessageDto> objects = new ArrayList<>();
if (!ObjectUtils.isEmpty(listObject.get(webSocketMessageDto.getRecvUserId()))) {//这个用户给收消息的这个用户发过消息
//此用户给该用户发送过离线消息(此用户给该用户发过的所有消息)
objects = listObject.get(webSocketMessageDto.getRecvUserId());
//加上这次发送的消息
objects.add(webSocketMessageDto);
//替换原来的map
listObject.put(webSocketMessageDto.getRecvUserId(), objects);
} else {//这个用户没给该用户发送过离线消息
objects.add(webSocketMessageDto);
listObject.put(webSocketMessageDto.getRecvUserId(), objects);
}
messageMap.put(webSocketMessageDto.getRecvUserId(), listObject);


}
}

/*
*成功建立连接后调用
* @param [session, userId]
* @return void
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "sendUserId") String sendUserId) throws IOException {
//成功建立连接后加入
sessionPool.put(sendUserId, session);
//当前在线数量+1
currentOnlineNumber.incrementAndGet();
log.info("websocket:" + sendUserId + "加入连接,当前在线用户" + currentOnlineNumber + "未读消息数:" + getMessageCount(sendUserId));
// 发送离线消息
sendOffLineMessage(sendUserId);
}

/*
* 用户上线时发送离线消息
* @param []
* @return void
*/
@SneakyThrows
public void sendOffLineMessage(String sendUserId) {

if (ObjectUtils.isEmpty(messageMap.get(sendUserId))) {
// 该用户没有离线消息
return;
}
// 当前登录用户有离线消息
//说明在用户没有登录的时候有人给用户发送消息
//该用户所有未收的消息
Map<String, List<WebSocketMessageDto>> lists = messageMap.get(sendUserId);
//对象用户发送的离线消息
List<WebSocketMessageDto> list = lists.get(sendUserId);
if (list != null) {
for (WebSocketMessageDto webSocketMessageDto : list) {
onMessage(JSON.toJSONString(webSocketMessageDto));
}
}

// 删除已发送的消息
removeHasBeenSentMessage(sendUserId, lists);


}

/*
*删除已发送的消息
* @param [sendUserId, map]
* @return void
*/
public void removeHasBeenSentMessage(String sendUserId, Map<String, List<WebSocketMessageDto>> map) {
// map中key(键)的迭代器对象
//用户接收完消息后删除 避免下次继续发送
Iterator iterator = map.keySet().iterator();
while (iterator.hasNext()) {// 循环取键值进行判断
String keys = (String) iterator.next();//键
if (sendUserId.equals(keys)) {
iterator.remove();
}
}
}

/*
*关闭连接时调用
* @param [userId]
* @return void
*/
@OnClose
public void onClose(@PathParam(value = "sendUserId") String sendUserId) {
sessionPool.remove(sendUserId);
currentOnlineNumber.decrementAndGet();
log.info("websocket:" + sendUserId + "断开连接,当前在线用户" + currentOnlineNumber);
}

/*
*发生错误时调用
* @param [session, throwable]
* @return void
*/
@OnError
public void onError(Throwable throwable) {
log.error("websocket: 发生了错误");
throwable.printStackTrace();
}

/**
* 获取该用户未读的消息数量
*/
public int getMessageCount(String recvUserId) {
//获取该用户所有未收的消息
Map<String, List<WebSocketMessageDto>> listMap = messageMap.get(recvUserId);
if (listMap != null) {
List<WebSocketMessageDto> list = listMap.get(recvUserId);
if (list != null) {
return listMap.get(recvUserId).size();
} else {
return 0;
}

} else {
return 0;
}

}
}

2.4客户端基本使用

打开连接

let socket;
function openSocket() {
if(typeof(WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
}else{
console.log("您的浏览器支持WebSocket");
//实现化WebSocket对象,指定要连接的服务器地址与端口 建立连接
let sendUserId = document.getElementById('sendUserId').value;
let socketUrl="ws://192.168.1.108:8081/websocket/"+sendUserId;
console.log(socketUrl);
if(socket!=null){
socket.close();
socket=null;
}
socket = new WebSocket(socketUrl);
//打开事件
socket.onopen = function() {
console.log("websocket已打开");
//socket.send("这是来自客户端的消息" + location.href + new Date());
};
//获得消息事件
socket.onmessage = function(msg) {
let serverMsg = "收到服务端信息:" + msg.data;
console.log(serverMsg);
//发现消息进入 开始处理前端触发逻辑
};
//关闭事件
socket.onclose = function() {
console.log("websocket已关闭");
};
//发生了错误事件
socket.onerror = function() {
console.log("websocket发生了错误");
}
}
}

发送私信

function sendMessage() {
if(typeof(WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
}else {
let recvUserId = document.getElementById('recvUserId').value;
let sendUserId = document.getElementById('sendUserId').value;
let msg = document.getElementById('msg').value;
let webSocketMessageDto = '{"recvUserId":"'+recvUserId+'","sendUserId":"'+sendUserId+'","message":"'+msg+'","messageEnum":"one"}';
console.log(webSocketMessageDto);
socket.send(webSocketMessageDto);
}
}

群发消息

function sendMessages() {
if(typeof(WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
}else {
let msg = document.getElementById('msg').value;
let webSocketMessageDto = '{"message":"'+msg+'","messageEnum":"all"}'; let sendUserId = document.getElementById('sendUserId').value;
let webSocketMessageDto = '{"sendUserId":"'+sendUserId+'","message":"'+msg+'","messageEnum":"all"}';
console.log(webSocketMessageDto);
socket.send(webSocketMessageDto);
}
}

二、测试

2.1启动项目

SpringBoot2.x整合WebSoket_客户端_02

2.2访问测试页

SpringBoot2.x整合WebSoket_websocket_03

2.3加入用户ID为100、200两个用户

SpringBoot2.x整合WebSoket_websocket_04


SpringBoot2.x整合WebSoket_客户端_05

2.4 服务端日志

SpringBoot2.x整合WebSoket_客户端_06

2.5 100 给 200 发消息

SpringBoot2.x整合WebSoket_客户端_07

2.6 200收到消息

SpringBoot2.x整合WebSoket_websocket_08

2.7 100 给离线用户 666发消息

SpringBoot2.x整合WebSoket_spring_09


SpringBoot2.x整合WebSoket_spring_10

2.8 666离线用户上线

SpringBoot2.x整合WebSoket_java_11


SpringBoot2.x整合WebSoket_客户端_12

2.9 admin 用户群发

可以看到所有连接上来的用户都是收到了群发的消息。

SpringBoot2.x整合WebSoket_websocket_13


SpringBoot2.x整合WebSoket_websocket_14

​完整代码地址:码云:https://gitee.com/dingwen-gitee/springboot-websocket.git​