WebSocket+springBoot实现简易聊天
一、关于WebSocket
http是前端主动发送请求给后端,后端被动返回响应,而websocket是双向的双工通讯,后端也能主动给前端发送消息
消息推送的常见方法:
- 轮询
每间隔一定时间发送http请求,让后端发送数据 - 长轮询
发送ajax请求,后端数据未更新时就阻塞(若时间长到超时,返回请求),能减少轮询的时间 - Sse(server-sent event)
在服务端和客户端之间开一个通道,有数据变更时以数据流(text/event-stream)的方法传输到客户端
4.websocket
全双工:允许数据在两个方向上同时传输
半双工:允许数据在两个方向上传输,但是一次只能在一个方向上传输
单工:信息只能沿着一个方向传输(http请求 )

前端API:
- 创建对象
let ws=new WebSocket(URL);
URL: ws://localhost/访问路径
注:默认端口为8080 - 相关事件
ws.onopen:连接建立时触发;
ws.onmessage:客户端收到服务器端发送的数据时触发
ws.onclose:连接断开时触发 - 方法
send():发送数据给服务端

后端API:
需要tomcat7.0.5版本以上
1.Java WebSocket由一系列Endpoint对象组成,代表WebSocket链接的一端
2.Endpoint在后端接收到http连接时实例化,并只在连接时有效,断开连接后结束;
两种方法定义EndPoint
1.编程式:类继承javax.websocket.Endpoint并实现方法
2.注解式:在pojo类上加@ServerEndpoint
对应的方法及注解:
onOpen():当开启一个会话时调用(握手成功后调用)
onClose():当会话关闭时调用
onError():当链接过程异常时调用

二、使用WebSocket
(一)引入依赖
需引入WebSocket依赖,有几种方式:
1. 原生 Java WebSocket API(JSR-356)
如果直接使用 Java 标准的 WebSocket API(JSR-356),无需额外依赖,因为 Java EE 环境(如 Tomcat 8+、GlassFish 等)已内置支持。但如果是纯 Java SE 项目或需要显式引入 API 接口
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope> <!-- 服务器已内置,无需打包 -->
</dependency>
2. Spring Boot 项目(推荐)
Spring Boot 对 WebSocket 有封装,使用 spring-boot-starter-websocket 可快速集成,自动适配底层服务器(如 Tomcat):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.7.0</version> <!-- 版本可根据项目实际使用的 Spring Boot 版本调整 -->
</dependency>
3.Tomcat 服务器单独依赖
如果项目直接部署在 Tomcat 上,且需要显式引入 Tomcat 的 WebSocket 实现(通常无需,Tomcat 已内置):
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>9.0.65</version> <!-- 与 Tomcat 版本对应 -->
<scope>provided</scope>
</dependency>
(二)类
项目整体目录:

config包:
GetHttpSessionConfigurator类:
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession = (HttpSession) request.getHttpSession();
config.getUserProperties().put(HttpSession.class.getName(),httpSession);
}
}
WebSocketConfig类:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
controller类:
import com.ljp.websocket.pojo.Result;
import com.ljp.websocket.pojo.User;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
@RestController
public class UserController {
@PostMapping("/login")
public Result login(@RequestBody User user, HttpSession session) {
Result result = new Result();
if (user != null && "123".equals(user.getPassword())) {
result.setFlag(true);
// 将数据存储到 session 对象中
session.setAttribute("user", user.getUsername());
} else {
result.setFlag(false);
result.setMessage("登陆失败");
}
return result;
}
}
pojo包:
Message类:
/**
浏览器发送给服务器的websocket数据
*/
public class Message {
private String toName;
private String message;
public String getToName() {
return toName;
}
public void setToName(String toName) {
this.toName = toName;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
Result类:
import lombok.Data;
@Data
public class Result {
public boolean flag;
public String message;
}
ResultMessage类:
**
* 服务器发送给浏览器的websocket数据
*/
public class ResultMessage {
private boolean isSystem;
private String fromName;
private Object message;//如果是系统消息是数组
public boolean getIsSystem() {
return isSystem;
}
public void setIsSystem(boolean isSystem) {
this.isSystem = isSystem;
}
public String getFromName() {
return fromName;
}
public void setFromName(String fromName) {
this.fromName = fromName;
}
public Object getMessage() {
return message;
}
public void setMessage(Object message) {
this.message = message;
}
}
user类:
import lombok.Data;
@Data
public class User {
private int id;
private String username;
private String password;
}
Utils包: MessageUtils类:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ljp.websocket.pojo.Result;
import com.ljp.websocket.pojo.ResultMessage;
/**
用来封装消息的工具类
*/
public class MessageUtils {
public static String getMessage(boolean isSystemMessage,String fromName, Object message) {
try {
ResultMessage result = new ResultMessage();
result.setIsSystem(isSystemMessage);
result.setMessage(message);
if(fromName != null) {
result.setFromName(fromName);
}
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
}
ws包:
ChatEndpoint类:
import com.alibaba.fastjson.JSON;
import com.ljp.websocket.config.GetHttpSessionConfigurator;
import com.ljp.websocket.pojo.Message;
import com.ljp.websocket.utils.MessageUtils;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfigurator.class)
@Component
public class ChatEndpoint {
private static final Map<String, Session> onlineUsers = new ConcurrentHashMap<>();
public HttpSession httpsession;
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
//1.通过HttpSession获取到用户名
httpsession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
String user =(String) httpsession.getAttribute("user");
onlineUsers.put(user,session);
//2.广播消息,将登录的所以用户推送给所有在线用户
String message = MessageUtils.getMessage(true, null, onlineUsers.keySet());
broadcastAllUsers(message);
}
//广播方法
private void broadcastAllUsers(String message) {
try {
//遍历map集合
Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet();
for (Map.Entry<String, Session> entry : entries) {
//获取到所有用户对应的session对象
Session session = entry.getValue();
//发送消息(这里是同步)
session.getBasicRemote().sendText(message);
}
} catch (Exception e) {
//记录日志
}
}
@OnMessage
public void onMessage(String message) {
try {
//将消息推送给指定用户
Message msg= JSON.parseObject(message,Message.class);
//获取到接收者的名称
String toName = msg.getToName();
String mess=msg.getMessage();
//获取消息接收方的session
Session session = onlineUsers.get(toName);
String msg1 = MessageUtils.getMessage(false, (String) httpsession.getAttribute("user"), mess);
session.getBasicRemote().sendText(msg1);
}catch (Exception e){
e.printStackTrace();
}
}
@OnClose
public void onClose(Session session) {
//1.从onlineUsers剔除下线用户
String user =(String) httpsession.getAttribute("user");
onlineUsers.remove(user);
//2.广播消息,将下线的用户推送给所有在线用户
String message = MessageUtils.getMessage(true, null, onlineUsers.keySet());
broadcastAllUsers(message);
}
}
(三)前端页面
index.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>聊天登录</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.login-container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 300px;
}
.login-container h2 {
text-align: center;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
}
.form-group input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #45a049;
}
.error-message {
color: #f44336;
margin-bottom: 10px;
text-align: center;
}
</style>
</head>
<body>
<div class="login-container">
<h2>聊天登录</h2>
<div id="errorMessage" class="error-message" style="display: none;"></div>
<form id="loginForm">
<div class="form-group">
<label for="username">用户名:</label>
<input type="text" id="username" required>
</div>
<div class="form-group">
<label for="password">密码:</label>
<input type="password" id="password" required>
</div>
<button type="submit">登录</button>
</form>
</div>
<script>
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
fetch('http://localhost:8080/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
})
.then(response => response.json())
.then(data => {
console.log(data);
if (data.flag) {
// 登录成功,跳转到聊天页面
window.location.href = 'chat.html'; // 请确保chat.html存在或修改为实际路径
} else {
// 登录失败,显示错误信息
const errorMessage = document.getElementById('errorMessage');
errorMessage.textContent = data.message || '登录失败,请重试';
errorMessage.style.display = 'block';
}
})
.catch(error => {
console.error('登录请求出错:', error);
const errorMessage = document.getElementById('errorMessage');
errorMessage.textContent = '网络错误,请重试';
errorMessage.style.display = 'block';
});
});
</script>
</body>
</html>
chat.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket 聊天示例</title>
<style>
#chatContainer {
width: 400px;
margin: 0 auto;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
}
#messageList {
height: 300px;
overflow-y: auto;
margin-bottom: 10px;
border: 1px solid #eee;
padding: 5px;
}
input[type="text"] {
width: 70%;
padding: 5px;
}
button {
padding: 5px 10px;
}
#onlineUsers {
margin-bottom: 10px;
font-weight: bold;
}
</style>
</head>
<body>
<div id="chatContainer">
<div id="onlineUsers">在线用户:</div>
<div id="messageList"></div>
<input type="text" id="messageInput" placeholder="输入消息">
<button onclick="sendMessage('lisi')">给lisi发送</button>
<button onclick="sendMessage('zhangsan')">给zhangsan发送</button>
</div>
<script>
let socket;
let username;
connectWebSocket();
function connectWebSocket() {
socket = new WebSocket('ws://' + window.location.host + '/chat');
socket.onopen = function (event) {
console.log('WebSocket 连接已建立');
};
socket.onmessage = function (event) {
try {
const data = JSON.parse(event.data);
console.log(data);
const messageList = document.getElementById('messageList');
const onlineUsersDiv = document.getElementById('onlineUsers');
if (data.isSystem) {
// 系统消息(在线用户更新等)
if (data.message && Array.isArray(data.message)) {
onlineUsersDiv.innerHTML = '在线用户:' + data.message.join(', ');
} else {
console.warn('在线用户列表格式不正确:', data.message);
onlineUsersDiv.innerHTML = '在线用户:无法获取';
}
} else {
// 聊天消息
const messageItem = document.createElement('div');
// 修正:使用 data.fromName 而不是 data.from
messageItem.innerText = (data.fromName || '系统') + ':' + data.message;
messageList.appendChild(messageItem);
}
} catch (error) {
console.error('消息解析失败:', error, '原始数据:', event.data);
}
};
socket.onclose = function (event) {
console.log('WebSocket 连接已关闭');
};
socket.onerror = function (event) {
console.error('WebSocket 出错:', event);
};
}
function sendMessage(name) {
const input = document.getElementById('messageInput');
const message = input.value.trim();
const messageList = document.getElementById('messageList');
if (message!== '') {
const sendData = {
toName: name,
message: message
};
socket.send(JSON.stringify(sendData));
const messageItem = document.createElement('div');
messageItem.innerText = "我" + ':' + input.value;
messageList.appendChild(messageItem);
input.value = '';
}
}
</script>
</body>
</html>
更多推荐




所有评论(0)