WebSokcet案例-聊天框
依赖包
<parent>
<groupId>org.springframework.boot</groupId>
<version>2.3.7.RELEASE</version>
<artifactId>spring-boot-parent</artifactId>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--devtools热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<scope>true</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
启动类
@SpringBootApplication
public class WebSocketApp {
public static void main(String[] args) {
SpringApplication.run(WebSocketApp.class, args);
}
}
登录模拟
public class User {
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public User() {
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
@RestController
public class UserController {
@RequestMapping("/login")
public Result login(User user, HttpSession session){
Result result = new Result();
if (user!=null&&"123".equals(user.getPassword())){
result.setFlag(true);
session.setAttribute("user",user.getUsername());
}else {
result.setFlag(false);
result.setMessage("登录失败");
}
return result;
}
@RequestMapping("/getUsername")
public String getUsername(HttpSession session){
String username = (String) session.getAttribute("user");
return username;
}
}
消息实体类
响应数据
这里用来响应前端数据,供模拟登录使用
/**
* Result:由于登录响应回给浏览器的数据
*/
public class Result {
private boolean flag ;
private String message;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
浏览器=>服务器
接收浏览器发来的数据格式
/**
* 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;
}
}
服务器=>浏览器
向浏览器响应的数据格式
/**
* ResultMessage:服务端发送给浏览器的websocket数据
*/
public class ResultMessage {
private boolean isSystem;
private String fromName;
private Object message; // 如果是系统消息是数组
public boolean isSystem() {
return isSystem;
}
public void setSystem(boolean system) {
isSystem = system;
}
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;
}
}
消息工具类
用于创建一个ResultMessage消息
public class MessageUtils {
public static String getMessage(boolean isSystemMessage,String fromName,Object message){
try {
ResultMessage result = new ResultMessage();
result.setSystem(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;
}
}
消息核心处理类
- @ServerEndpoint(value = “/chat”, configurator = GetHttpSessionConfigurator.class)
- 标识该类为WebSocket的消息处理类
- value:请求路径(前端格式:
ws://localhost:8080/chat
) - configurator:与httpSession对象存储相关(单体项目,多体可以使用其他方式)
- @Component:供Spring管理
- @Slf4j:日志相关
- onlineUsers:一个Map容器,static=>多个ChatEndpoint对象共享一个容器(每建立一个连接有一个新的ChatEndpoint对象)
- Session:会话对象,用于消息发送
- HttpSession:用于获取当前用户(单体项目)
- @OnOpen:连接建立时调用,用于会话建立准备操作,初始化session与httpSession,并将自身放入容器,将自己推送出去(简单理解:A登录了,A的好友收到我登录的消息,好友列表展示我在线)
- @OnMessage:接收到客户端消息时调用,负责消息的推送(将消息推送给要创达的用户)
- @OnClose:连接断开时调用,用于会话销毁事后工作(简单理解:A下线了,A的好友们在列表上看到的是A已离线)
- @OnError:异常时调用
package com.czk.websocket.ws;
import com.czk.websocket.pojo.Message;
import com.czk.websocket.util.MessageUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
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
@Slf4j
public class ChatEndpoint {
// 用来存储每个客户端对象对应的ChatEndpoint对象
private static Map<String, ChatEndpoint> onlineUsers = new ConcurrentHashMap<>(); // 考虑线程安全,用ConcurrentHashMap
// 声明Session对象,通过该对象可用发送消息给指定的用户
private Session session;
// 声明一个HttpSession对象,我们之前在HttpSession对象中存储了用户名
private HttpSession httpSession;
// 连接建立时被调用
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
// 将局部的session对象复制给成员session
this.session = session;
// 获取httpSession对象 (由GetHttpsSessionConfigurator中存入)
HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
this.httpSession = httpSession;
String username = (String) httpSession.getAttribute("user");
// 将当前对象存储到容器(onlineUsers)中
onlineUsers.put(username, this);
// 将当前在线用户名推送给所有的客户端
// 1.获取消息
String message = MessageUtils.getMessage(true, null, getNames());
// 2.调用方法进行系统消息的推送
broadcastAllUsers(message);
}
// 获取系统中所有的登录用户
private Set<String> getNames() {
return onlineUsers.keySet();
}
// 广播,将消息推送给所有客户端
private void broadcastAllUsers(String message) {
log.info("开始广播:{}", message);
try {
Set<String> names = onlineUsers.keySet();
for (String name : names) {
ChatEndpoint chatEndpoint = onlineUsers.get(name);
chatEndpoint.session.getBasicRemote().sendText(message);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 接收到客户端发送的数据时被调用
@OnMessage
public void onMessage(String message, Session session) {
try {
// 将消息(JSON格式),转为Message
ObjectMapper mapper = new ObjectMapper();
Message msg = mapper.readValue(message, Message.class);
// 获取要将数据发送给的用户
String toName = msg.getToName();
// 获取消息数据
String data = msg.getMessage();
// 获取当前登录用户
String username = (String) httpSession.getAttribute("user");
// 获取推送给指定用户的消息格式的数据
String resultMessage = MessageUtils.getMessage(false, username, data);
// 发送数据
log.info("{}给{}发送信息:{}",username,toName,data);
onlineUsers.get(toName).session.getBasicRemote().sendText(resultMessage);
} catch (Exception e) {
e.printStackTrace();
}
}
// 连接关闭时被调用
@OnClose
public void onClose(Session session) {
// 用户下线,移除用户并广播
String username = (String) httpSession.getAttribute("user");
onlineUsers.remove(username);
// 获取推送的消息
String message = MessageUtils.getMessage(true, null, getNames());
broadcastAllUsers(message);
}
}
ServerEndpointConfig
用于存储HttpSession对象,方便@OnOpen中获取
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession = (HttpSession) request.getHttpSession();
// 将httpSession对象存储到配置对象中
sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
}
}
消息配置类
ServerEndpointExporter:用于扫描@ServerEndpoint注解的bean
@Configuration
public class WebSocketConfig {
// 注入ServerEndpointExporter bean对象,自动注册使用了@ServerEndpoint注解的bean
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
前端相关代码
登录
<script>
$("#btn").click(function () {
$.get("login?",$("#loginForm").serialize(),function(res){
if (res.flag){
console.log(res);
location.href = "main.html";
} else {
console.log(res);
$("#err_msg").html(res.message);
}
},"json");
})
</script>
WebSocket
<script>
var username;
$(function () {
var toName;
$.ajax({
url: "getUsername",
success: function (res) {
username = res;
$("#username").html("用户:" + username + "<span>在线</span>");
// $("#username").html("用户:123<span>在线</span>");
},
async: false //同步请求,只有上面好了才会接着下面
});
// 创建websocket对象,用于客户端与服务器通信
var ws = new WebSocket("ws://localhost:8080/chat");
// 对应服务端@OnOpen
ws.onopen = function (ev) {
$("#username").html("用户:" + username + "<span>在线</span>");
}
//接受消息,对应服务端@OnMessage
ws.onmessage = function (ev) {
var datastr = ev.data;
var res = JSON.parse(datastr);
//判断是否是系统消息,虽然实体类是isSystem,但这里是system
if (res.system) {
//好友列表
//系统广播
var names = res.message;
var friends_ul = "<li style='font-size: 18px;font-weight: 800;'>好友列表:</li>"
var userlistStr = friends_ul;
var broadcastListStr = "";
for (var name of names) {
if (name != username) {
userlistStr += "<li><a οnclick=\"showChat(\'" + name + "\')\">" + name + "</a></li></br>";
broadcastListStr += "<li>您的好友:" + name + " 已上线</li>";
}
}
;
$("#friends_ul").html(userlistStr);
$("#broadcast_ul").html(broadcastListStr);
} else {
//不是系统消息
var chat_left="<p class='chat_left'><span class='p_border'>"+res.fromName+"</span>"+res.message+"</p>"
if (toName == res.fromName) {
$("#chat_content").append(chat_left);
}
var chatdata = sessionStorage.getItem(res.fromName);
if (chatdata != null) {
chat_left = chatdata + chat_left;
}
sessionStorage.setItem(res.fromName, chat_left);
}
;
},
// 对应服务端@OnMessage
ws.onclose = function (ev) {
$("#username").html("用户:" + username + "<span>离线</span>");
}
showChat = function (name) {
// alert("dsaad");
toName = name;
//清空聊天区
$("#chat_content").html("");
$("#chat_top").html("当前正与" + toName + "聊天");
var chatdata = sessionStorage.getItem(toName);
if (chatdata != null) {
$("#chat_content").html(chatdata);
}
};
//发送消息
$("#send").click(function () {
//获取输入的内容
var data = $("#message_text").val();
$("#message_text").val("");
var json = {"toName": toName, "message": data};
//将数据展示在聊天区
var chat_right = "<p class='chat_right'>"+data+"<span class='p_border'>我</span></p>"
$("#chat_content").append(chat_right);
var chatdata = sessionStorage.getItem(toName);
if (chatdata != null) {
chat_right = chatdata + chat_right;
}
sessionStorage.setItem(toName, chat_right);
//发送数据
ws.send(JSON.stringify(json));
})
})
</script>
``
效果演示