源码可在CSDN 直接下载
1、引入jar包
maven项目
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
如果是Java项目,引入以下jar包
红框为必须:
javax.websocket-api-1.0.jar
spring-context-4.2.5.RELEASE.jar
spring-websocket-4.2.5.RELEASE.jar
注意引入jar包和项目所用的spring版本保持一致,项目中用4.2.5我这demo也用这个
2、编写配置文件
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter,
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
2、编写主要的实现类
import com.alibaba.fastjson.JSON;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.entity.SocketMessage;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@ServerEndpoint("/webSocket/{userId}")
@Component
public class WebSocketServer {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static AtomicInteger onlineNum = new AtomicInteger();
//concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
private static ConcurrentHashMap<String, Session> sessionPools = new ConcurrentHashMap<>();
//发送消息
public void sendMessage(Session session, String message) throws IOException {
if(session != null){
synchronized (session) {
session.getBasicRemote().sendText(message);
}
}
}
//给指定用户发送信息
public Integer sendInfo(String userId, String message){
Session session = sessionPools.get(userId);
try {
sendMessage(session, message);
return 1;
}catch (Exception e){
e.printStackTrace();
return 0;
}
}
// 群发消息
public void broadcast(String message){
for (Session session: sessionPools.values()) {
try {
sendMessage(session, message);
} catch(Exception e){
e.printStackTrace();
continue;
}
}
}
//建立连接成功调用
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") String userId){
sessionPools.put(userId, session);
addOnlineCount();
// 广播上线消息
SocketMessage msg = new SocketMessage();
msg.setDate(new Date());
msg.setTo("0");
msg.setText(userId);
broadcast(JSON.toJSONString(msg,true));
}
//关闭连接时调用
@OnClose
public void onClose(@PathParam(value = "userId") String userId){
sessionPools.remove(userId);
subOnlineCount();
// 广播下线消息
SocketMessage msg = new SocketMessage();
msg.setDate(new Date());
msg.setTo("-2");
msg.setText(userId);
broadcast(JSON.toJSONString(msg,true));
}
//收到客户端信息后,根据接收人的userId把消息推下去或者群发
// to=-1群发消息
@OnMessage
public void onMessage(String message) throws IOException{
SocketMessage msg= JSON.parseObject(message, SocketMessage.class);
msg.setDate(new Date());
if (msg.getTo().equals("-1")) {
broadcast(JSON.toJSONString(msg,true));
} else {
sendInfo(msg.getTo(), JSON.toJSONString(msg,true));
}
}
//错误时调用
@OnError
public void onError(Session session, Throwable throwable){
throwable.printStackTrace();
}
public static void addOnlineCount(){
onlineNum.incrementAndGet();
}
public static void subOnlineCount() {
onlineNum.decrementAndGet();
}
public static AtomicInteger getOnlineNumber() {
return onlineNum;
}
public static ConcurrentHashMap<String, Session> getSessionPools() {
return sessionPools;
}
}
引入了额外的jar包也是为了这里使用的,其中
这个实体类是为了方便发送消息添加的,也可删掉这块相关的代码,不影响,我这里也贴出,方便大家使用
import com.alibaba.fastjson.annotation.JSONField;
import java.util.Date;
public class SocketMessage {
//发送者name
public String from;
//接收者name
public String to;
//发送的文本
public String text;
//发送时间
@JSONField(format="yyyy-MM-dd HH:mm:ss")
public Date date;
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
@Override
public String toString() {
return "SocketMessage{" +
"from='" + from + '\'' +
", to='" + to + '\'' +
", text='" + text + '\'' +
", date=" + date +
'}';
}
}
3、编写测试接口
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.service.WebSocketServer;
@Controller
@RequestMapping("/web")
public class WebSocketController {
@Autowired
WebSocketServer webSocketServer;
/**
* 发送单人消息
*
* @param userId 接收人用户ID
* @param mes 消息
* @return 1 发送成功 0 发送失败
*/
@RequestMapping("/sendInfo")
@ResponseBody
public Integer sendInfo(@RequestParam("userId") String userId, @RequestParam("mes") String mes) {
Integer num = webSocketServer.sendInfo(userId, mes);
return num;
}
/**
* 群发消息
*
* @param mes 消息
* @return 1 发送成功 0 发送失败
*/
@RequestMapping("/broadcast")
@ResponseBody
public void broadcast(@RequestParam String mes) {
webSocketServer.broadcast(mes);
}
/**
* 测试发送多条
*/
@RequestMapping("/sendInfoFor")
@ResponseBody
public Integer sendInfo() {
for (int i = 0; i < 100; i++) {
webSocketServer.sendInfo(1 + "", i + "");
}
return 1;
}
}
4、简单的html页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<title>websocket测试页面</title>
</head>
<body>
<div class="panel panel-default">
<div class="panel-body">
<div class="row">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-addon">ws地址</span>
<input type="text" id="address" class="form-control" placeholder="ws地址"
aria-describedby="basic-addon1" value="ws://localhost:8010/webSocket/admin">
<div class="input-group-btn">
<button class="btn btn-default" type="submit" id="connect">连接</button>
</div>
</div>
</div>
</div>
<div class="row" style="margin-top: 10px;display: none;" id="msg-panel">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-addon">消息</span>
<input type="text" id="msg" class="form-control" placeholder="消息内容" aria-describedby="basic-addon1">
<div class="input-group-btn">
<button class="btn btn-default" type="submit" id="send">发送</button>
</div>
</div>
</div>
</div>
<div class="row" style="margin-top: 10px; padding: 10px;">
<div class="panel panel-default">
<div class="panel-body" id="log" style="height: 450px;overflow-y: auto;">
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
crossorigin="anonymous"></script>
<script type="text/javascript">
$(function () {
var _socket;
$("#connect").click(function () {
_socket = new _websocket($("#address").val());
_socket.init();
});
$("#send").click(function () {
var _msg = $("#msg").val();
output("发送消息:" + _msg);
_socket.client.send(JSON.stringify(_msg));
});
});
function output(e) {
var _text = $("#log").html();
$("#log").html(_text + "<br>" + e);
}
function _websocket(address) {
this.address = address;
this.client;
this.init = function () {
if (!window.WebSocket) {
this.websocket = null;
return;
}
var _this = this;
var _client = new window.WebSocket(_this.address);
_client.onopen = function () {
output("websocket打开");
$("#msg-panel").show();
};
_client.onclose = function () {
_this.client = null;
output("websocket关闭");
$("#msg-panel").hide();
};
_client.onmessage = function (evt) {
output(evt.data);
};
_this.client = _client;
};
return this;
}
</script>
</body>
</html>
可以把这里修改成自己项目的访问地址,方便每次访问
5、进行测试
启动项目,启动html页面,点击连接
连接成功会如图所示
打开postman发送请求
这里的1是发送成功之后的返回值,也可根据实际情况来修改
html页面已接受到消息
最后附上项目的结构图,demo项目有点粗糙
源码地址: https://download.csdn.net/download/weixin_45352783/12881996
2025-07-05更新,使用AI美化调试页面
效果图
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>WebSocket测试工具</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome 图标 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
/* 原有样式保持不变 */
:root {
--primary-color: #4e73df;
--success-color: #1cc88a;
--danger-color: #e74a3b;
--dark-color: #2e3a59;
--light-color: #f8f9fc;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
min-height: 100vh;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.card {
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: none;
overflow: hidden;
}
.card-header {
background: var(--primary-color);
color: white;
font-weight: 600;
padding: 15px 20px;
border-bottom: none;
}
.card-body {
padding: 25px;
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
transition: all 0.3s;
}
.btn-primary:hover {
background-color: #3a56c4;
border-color: #3a56c4;
transform: translateY(-2px);
}
.btn-success {
background-color: var(--success-color);
border-color: var(--success-color);
transition: all 0.3s;
}
.btn-success:hover {
background-color: #17a673;
border-color: #17a673;
transform: translateY(-2px);
}
.btn-danger {
background-color: var(--danger-color);
border-color: var(--danger-color);
transition: all 0.3s;
}
.btn-danger:hover {
background-color: #d62c1a;
border-color: #d62c1a;
transform: translateY(-2px);
}
.input-group-text {
background-color: var(--light-color);
border: 1px solid #e3e6f0;
}
.form-control:focus, .form-select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(78, 115, 223, 0.25);
}
.log-container {
background-color: var(--dark-color);
color: #e9ecef;
border-radius: 8px;
padding: 15px;
height: 400px;
overflow-y: auto;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
line-height: 1.5;
}
.log-container::-webkit-scrollbar {
width: 8px;
}
.log-container::-webkit-scrollbar-track {
background: #2a3650;
border-radius: 4px;
}
.log-container::-webkit-scrollbar-thumb {
background: var(--primary-color);
border-radius: 4px;
}
.log-entry {
margin-bottom: 8px;
padding: 5px 10px;
border-radius: 4px;
word-break: break-word;
}
.log-entry.info {
background-color: rgba(78, 115, 223, 0.15);
border-left: 3px solid var(--primary-color);
}
.log-entry.success {
background-color: rgba(28, 200, 138, 0.15);
border-left: 3px solid var(--success-color);
}
.log-entry.error {
background-color: rgba(231, 74, 59, 0.15);
border-left: 3px solid var(--danger-color);
}
.log-entry.received {
background-color: rgba(46, 58, 89, 0.15);
border-left: 3px solid #6c757d;
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.status-connected {
background-color: var(--success-color);
box-shadow: 0 0 8px var(--success-color);
}
.status-disconnected {
background-color: var(--danger-color);
}
.status-connecting {
background-color: #f6c23e;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.4; }
100% { opacity: 1; }
}
.connection-status {
display: flex;
align-items: center;
padding: 8px 15px;
background-color: rgba(46, 58, 89, 0.05);
border-radius: 6px;
margin-top: 15px;
font-size: 14px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: var(--dark-color);
margin-bottom: 15px;
display: flex;
align-items: center;
}
.section-title i {
margin-right: 10px;
color: var(--primary-color);
}
.action-buttons {
display: flex;
gap: 10px;
margin-top: 15px;
}
.footer {
text-align: center;
margin-top: 20px;
color: #6e707e;
font-size: 13px;
}
@media (max-width: 768px) {
.card-body {
padding: 15px;
}
.action-buttons {
flex-direction: column;
}
.log-container {
height: 300px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<i class="fas fa-plug me-2"></i>WebSocket测试工具
</div>
<div class="connection-status">
<span class="status-indicator status-disconnected"></span>
<span id="status-text">未连接</span>
</div>
</div>
<div class="card-body">
<div class="section-title">
<i class="fas fa-link"></i>连接设置
</div>
<div class="mb-4">
<label class="form-label">WebSocket地址</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-globe"></i></span>
<input type="text" id="address" class="form-control" placeholder="ws://地址" value="ws://127.0.0.1:8100/webSocket/123456">
<button class="btn btn-primary" type="button" id="connect">
<i class="fas fa-plug me-1"></i>连接
</button>
</div>
</div>
<div id="msg-panel" style="display: none;">
<div class="section-title">
<i class="fas fa-paper-plane"></i>发送消息
</div>
<div class="mb-3">
<label class="form-label">来源(From)</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user"></i></span>
<input type="text" id="from" class="form-control" placeholder="来源" readonly>
</div>
</div>
<div class="mb-3">
<label class="form-label">接收方(To)</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user-friends"></i></span>
<input type="text" id="to" class="form-control" placeholder="输入接收方ID">
</div>
</div>
<div class="mb-3">
<label class="form-label">消息内容(Text)</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-comment"></i></span>
<input type="text" id="msg" class="form-control" placeholder="输入要发送的消息">
</div>
</div>
<div class="d-grid">
<button class="btn btn-success" type="button" id="send">
<i class="fas fa-paper-plane me-1"></i>发送消息
</button>
</div>
</div>
<div class="section-title">
<i class="fas fa-clipboard-list"></i>通信日志
</div>
<div class="log-container" id="log"></div>
<div class="action-buttons">
<button class="btn btn-danger" id="disconnect" disabled>
<i class="fas fa-power-off me-1"></i>断开连接
</button>
<button class="btn btn-secondary" id="clear-log">
<i class="fas fa-trash-alt me-1"></i>清除日志
</button>
</div>
</div>
</div>
<div class="footer">
<p>WebSocket测试工具 © 2025 | 实时通信调试助手</p>
</div>
</div>
</div>
</div>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Bootstrap 5 JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
$(function () {
var _socket;
var isConnected = false;
var fromId = ''; // 存储从地址中提取的from ID
// 更新连接状态显示
function updateStatus(status, text) {
var indicator = $('.status-indicator');
var statusText = $('#status-text');
indicator.removeClass('status-connected status-disconnected status-connecting');
switch(status) {
case 'connected':
indicator.addClass('status-connected');
statusText.text('已连接');
break;
case 'disconnected':
indicator.addClass('status-disconnected');
statusText.text('未连接');
break;
case 'connecting':
indicator.addClass('status-connecting');
statusText.text('连接中...');
break;
case 'error':
indicator.addClass('status-disconnected');
statusText.text(text || '连接错误');
break;
}
// 更新按钮状态
$('#connect').prop('disabled', status === 'connecting' || status === 'connected');
$('#disconnect').prop('disabled', status !== 'connected');
$('#send').prop('disabled', status !== 'connected');
}
// 输出日志
function output(message, type = 'info') {
var timestamp = new Date().toLocaleTimeString();
var logEntry = $('<div>').addClass('log-entry ' + type)
.html(`<span class="text-muted">[${timestamp}]</span> ${message}`);
$('#log').append(logEntry);
// 自动滚动到底部
$('#log').scrollTop($('#log')[0].scrollHeight);
}
// 连接按钮点击事件
$("#connect").click(function () {
var address = $("#address").val().trim();
if (!address) {
output('请输入WebSocket地址', 'error');
return;
}
// 从地址中提取from ID
var parts = address.split('/');
fromId = parts[parts.length - 1]; // 获取最后一部分作为from ID
if (!fromId) {
output('无法从地址中提取来源ID,请检查地址格式', 'error');
return;
}
// 设置来源输入框的值
$("#from").val(fromId);
updateStatus('connecting');
output(`正在连接: ${address}`, 'info');
output(`来源ID: ${fromId}`, 'info');
try {
_socket = new _websocket(address);
_socket.init();
} catch (e) {
updateStatus('error', '连接失败');
output('连接失败: ' + e.message, 'error');
}
});
// 断开连接按钮
$("#disconnect").click(function () {
if (_socket && _socket.client) {
_socket.client.close();
output('已主动断开连接', 'info');
}
});
// 清除日志按钮
$("#clear-log").click(function () {
$("#log").empty();
output('日志已清除', 'info');
});
// 发送按钮点击事件
$("#send").click(function () {
if (!_socket || !_socket.client || _socket.client.readyState !== WebSocket.OPEN) {
output('未连接到WebSocket服务器', 'error');
return;
}
var to = $("#to").val().trim();
var text = $("#msg").val().trim();
if (!to) {
output('请输入接收方ID', 'error');
return;
}
if (!text) {
output('请输入要发送的消息', 'error');
return;
}
// 构建新的JSON消息
var message = {
from: fromId,
to: to,
text: text
};
try {
var jsonMessage = JSON.stringify(message);
output("发送消息: " + jsonMessage, 'success');
_socket.client.send(jsonMessage);
$("#msg").val(''); // 清空消息内容输入框
} catch (e) {
output('发送失败: ' + e.message, 'error');
}
});
// 按Enter键发送消息(在消息内容输入框)
$("#msg").keypress(function (e) {
if (e.which == 13) { // Enter键
$("#send").click();
}
});
function _websocket(address) {
this.address = address;
this.client;
this.init = function () {
if (!window.WebSocket) {
output('您的浏览器不支持WebSocket', 'error');
updateStatus('error', '浏览器不支持');
return;
}
var _this = this;
var _client = new window.WebSocket(_this.address);
_client.onopen = function () {
output("WebSocket连接已建立", 'success');
updateStatus('connected');
$("#msg-panel").show();
};
_client.onclose = function (event) {
_this.client = null;
isConnected = false;
if (event.wasClean) {
output("WebSocket连接已关闭", 'info');
} else {
output("WebSocket连接异常断开", 'error');
}
updateStatus('disconnected');
$("#msg-panel").hide();
};
_client.onerror = function (error) {
output("WebSocket错误: " + error, 'error');
updateStatus('error');
};
_client.onmessage = function (evt) {
try {
// 尝试解析JSON消息
var data = JSON.parse(evt.data);
output("收到消息: " + JSON.stringify(data), 'received');
} catch (e) {
// 如果不是JSON,直接显示原始数据
output("收到消息: " + evt.data, 'received');
}
};
_this.client = _client;
};
return this;
}
// 初始日志
output('WebSocket测试工具已就绪', 'info');
output('请输入WebSocket地址并点击连接按钮', 'info');
});
</script>
</body>
</html>