前言
之前写毕业设计的时候就想加上聊天系统,当时已经用ajax长轮询实现了一个(还不懂什么是轮询机制的,猛戳这里:),但由于种种原因没有加到毕设里面。后来回校答辩后研究了一下websocket,并参照网上资料写了一个简单的聊天,现在又重新整理并记录下来。
以下介绍来自维基百科:
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
这里可以看一下官网介绍:http://www.websocket.org/aboutwebsocket.html 官网里面介绍非常详细,我就不做搬运工了,要是有像我一样英语不好的同学,右键->翻译成简体中文
spring对websocket的支持:https://docs.spring.io/spring/docs/4.3.13.RELEASE/spring-framework-reference/htmlsingle/#websocket 这里有一份spring对websocket的详细介绍:https://docs.spring.io/spring/docs/5.1.8.RELEASE/spring-framework-reference/web.html#websocket
四个大章节,内容很多,就不一一展开介绍了
效果
2019/04/30补充:我们这个登录、登出非常简单,就一个请求地址,连页面都没有,所以刚开始我就没有贴出来,导致博客文章阅读起来比较吃力,现在在这里补充一下(由于项目后面有所改动,请求地址少了 springboot/,不过大家看得懂就行),这里只是一个小demo,所有就怎么简单怎么来
登录 http://localhost:10086/websocket/login/huanzi,
登出 http://localhost:10086/websocket/logout/huanzi
上下线有提示
如果这时候发送消息给离线的人,则会收到系统提示消息
群聊
本例中,点击自己是群聊窗口
huanzi一发送群聊,laowang跟xiaofang都不是在当前群聊窗口,出现小圆点+1
huanzi一发送群聊,xiaofang在当前群聊窗口,直接追加消息,老王不在对应的聊天窗口,出现小圆点+1
xiaofang回复,huanzi直接追加消息,laowang依旧小圆点+1
laowang点击群聊窗口,小圆点消失,追加群聊消息
laowang参与群聊
xiaofang切出群聊窗口,laowang在群聊发送消息,xiaofang出现小圆点+1
切回来,小圆点消失,聊天数据正常接收追加
三方正常参与聊天
私聊
huanzis私聊xiaofang,xiaofang聊天窗口在群聊,小圆点+1,而laowang不受影响
xiaofang切到私聊窗口,小圆点消失,数据正常追加;huanzi刚好处于私聊窗口,数据直接追加
效果演示到此结束,下面贴出代码
代码编写
首先先介绍一下项目结构
maven
<!-- springboot websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- thymeleaf模板 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
配置文件
#修改thymeleaf访问根路径spring.thymeleaf.prefix=classpath:/view/
socketChart.css样式
body{
background-color: #efebdc;
}
#hz-main{
width: 700px;
height: 500px;
background-color: red;
margin: 0 auto;
}
#hz-message{
width: 500px;
height: 500px;
float: left;
background-color: #B5B5B5;
}
#hz-message-body{
width: 460px;
height: 340px;
background-color: #E0C4DA;
padding: 10px 20px;
overflow:auto;
}
#hz-message-input{
width: 500px;
height: 99px;
background-color: white;
overflow:auto;
}
#hz-group{
width: 200px;
height: 500px;
background-color: rosybrown;
float: right;
}
.hz-message-list{
min-height: 30px;
margin: 10px 0;
}
.hz-message-list-text{
padding: 7px 13px;
border-radius: 15px;
width: auto;
max-width: 85%;
display: inline-block;
}
.hz-message-list-username{
margin: 0;
}
.hz-group-body{
overflow:auto;
}
.hz-group-list{
padding: 10px;
}
.left{
float: left;
color: #595a5a;
background-color: #ebebeb;
}
.right{
float: right;
color: #f7f8f8;
background-color: #919292;
}
.hz-badge{
width: 20px;
height: 20px;
background-color: #FF5722;
border-radius: 50%;
float: right;
color: white;
text-align: center;
line-height: 20px;
font-weight: bold;
opacity: 0;
}
socketChart.html页面
<!DOCTYPE>
<!--解决idea thymeleaf 表达式模板报红波浪线-->
<!--suppress ALL -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>聊天页面</title>
<!-- jquery在线版本 -->
<script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
<!--引入样式-->
<link th:href="@{/css/socketChart.css}" rel="stylesheet" type="text/css"/>
</head>
<body>
<div id="hz-main">
<div id="hz-message">
<!-- 头部 -->
正在与<span id="toUserName"></span>聊天
<hr style="margin: 0px;"/>
<!-- 主体 -->
<div id="hz-message-body">
</div>
<!-- 功能条 -->
<div id="">
<button>表情</button>
<button>图片</button>
<button id="videoBut">视频</button>
<button onclick="send()" style="float: right;">发送</button>
</div>
<!-- 输入框 -->
<div contenteditable="true" id="hz-message-input">
</div>
</div>
<div id="hz-group">
登录用户:<span id="talks" th:text="${username}">请登录</span>
<br/>
在线人数:<span id="onlineCount">0</span>
<!-- 主体 -->
<div id="hz-group-body">
</div>
</div>
</div>
</body>
<script type="text/javascript" th:inline="javascript">
//项目根路径
var ctx = [[${#request.getContextPath()}]];//登录名
var username = /*[[${username}]]*/'';
</script>
<script th:src="@{/js/socketChart.js}"></script>
</html>
socketChart.js 逻辑代码
//消息对象数组
var msgObjArr = new Array();
var websocket = null;
//判断当前浏览器是否支持WebSocket, springboot是项目名
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:10086/springboot/websocket/"+username);
} else {
console.error("不支持WebSocket");
}
//连接发生错误的回调方法
websocket.onerror = function (e) {
console.error("WebSocket连接发生错误");
};
//连接成功建立的回调方法
websocket.onopen = function () {
//获取所有在线用户
$.ajax({
type: 'post',
url: ctx + "/websocket/getOnlineList",
contentType: 'application/json;charset=utf-8',
dataType: 'json',
data: {username:username},
success: function (data) {
if (data.length) {
//列表
for (var i = 0; i < data.length; i++) {
var userName = data[i];
$("#hz-group-body").append("<div class=\"hz-group-list\"><span class='hz-group-list-username'>" + userName + "</span><span id=\"" + userName + "-status\">[在线]</span><div id=\"hz-badge-" + userName + "\" class='hz-badge'>0</div></div>");
}
//在线人数
$("#onlineCount").text(data.length);
}
},
error: function (xhr, status, error) {
console.log("ajax错误!");
}
});
}
//接收到消息的回调方法
websocket.onmessage = function (event) {
var messageJson = eval("(" + event.data + ")");
//普通消息(私聊)
if (messageJson.type == "1") {
//来源用户
var srcUser = messageJson.srcUser;
//目标用户
var tarUser = messageJson.tarUser;
//消息
var message = messageJson.message;
//最加聊天数据
setMessageInnerHTML(srcUser.username,srcUser.username, message);
}
//普通消息(群聊)
if (messageJson.type == "2"){
//来源用户
var srcUser = messageJson.srcUser;
//目标用户
var tarUser = messageJson.tarUser;
//消息
var message = messageJson.message;
//最加聊天数据
setMessageInnerHTML(username,tarUser.username, message);
}
//对方不在线
if (messageJson.type == "0"){
//消息
var message = messageJson.message;
$("#hz-message-body").append(
"<div class=\"hz-message-list\" style='text-align: center;'>" +
"<div class=\"hz-message-list-text\">" +
"<span>" + message + "</span>" +
"</div>" +
"</div>");
}
//在线人数
if (messageJson.type == "onlineCount") {
//取出username
var onlineCount = messageJson.onlineCount;
var userName = messageJson.username;
var oldOnlineCount = $("#onlineCount").text();
//新旧在线人数对比
if (oldOnlineCount < onlineCount) {
if($("#" + userName + "-status").length > 0){
$("#" + userName + "-status").text("[在线]");
}else{
$("#hz-group-body").append("<div class=\"hz-group-list\"><span class='hz-group-list-username'>" + userName + "</span><span id=\"" + userName + "-status\">[在线]</span><div id=\"hz-badge-" + userName + "\" class='hz-badge'>0</div></div>");
}
} else {
//有人下线
$("#" + userName + "-status").text("[离线]");
}
$("#onlineCount").text(onlineCount);
}
}
//连接关闭的回调方法
websocket.onclose = function () {
//alert("WebSocket连接关闭");
}
//将消息显示在对应聊天窗口 对于接收消息来说这里的toUserName就是来源用户,对于发送来说则相反
function setMessageInnerHTML(srcUserName,msgUserName, message) {
//判断
var childrens = $("#hz-group-body").children(".hz-group-list");
var isExist = false;
for (var i = 0; i < childrens.length; i++) {
var text = $(childrens[i]).find(".hz-group-list-username").text();
if (text == srcUserName) {
isExist = true;
break;
}
}
if (!isExist) {
//追加聊天对象
msgObjArr.push({
toUserName: srcUserName,
message: [{username: msgUserName, message: message, date: NowTime()}]//封装数据
});
$("#hz-group-body").append("<div class=\"hz-group-list\"><span class='hz-group-list-username'>" + srcUserName + "</span><span id=\"" + srcUserName + "-status\">[在线]</span><div id=\"hz-badge-" + srcUserName + "\" class='hz-badge'>0</div></div>");
} else {
//取出对象
var isExist = false;
for (var i = 0; i < msgObjArr.length; i++) {
var obj = msgObjArr[i];
if (obj.toUserName == srcUserName) {
//保存最新数据
obj.message.push({username: msgUserName, message: message, date: NowTime()});
isExist = true;
break;
}
}
if (!isExist) {
//追加聊天对象
msgObjArr.push({
toUserName: srcUserName,
message: [{username: msgUserName, message: message, date: NowTime()}]//封装数据
});
}
}
// 对于接收消息来说这里的toUserName就是来源用户,对于发送来说则相反
var username = $("#toUserName").text();
//刚好打开的是对应的聊天页面
if (srcUserName == username) {
$("#hz-message-body").append(
"<div class=\"hz-message-list\">" +
"<p class='hz-message-list-username'>"+msgUserName+":</p>" +
"<div class=\"hz-message-list-text left\">" +
"<span>" + message + "</span>" +
"</div>" +
"<div style=\" clear: both; \"></div>" +
"</div>");
} else {
//小圆点++
var conut = $("#hz-badge-" + srcUserName).text();
$("#hz-badge-" + srcUserName).text(parseInt(conut) + 1);
$("#hz-badge-" + srcUserName).css("opacity", "1");
}
}
//发送消息
function send() {
//消息
var message = $("#hz-message-input").html();
//目标用户名
var tarUserName = $("#toUserName").text();
//登录用户名
var srcUserName = $("#talks").text();
websocket.send(JSON.stringify({
"type": "1",
"tarUser": {"username": tarUserName},
"srcUser": {"username": srcUserName},
"message": message
}));
$("#hz-message-body").append(
"<div class=\"hz-message-list\">" +
"<div class=\"hz-message-list-text right\">" +
"<span>" + message + "</span>" +
"</div>" +
"</div>");
$("#hz-message-input").html("");
//取出对象
if (msgObjArr.length > 0) {
var isExist = false;
for (var i = 0; i < msgObjArr.length; i++) {
var obj = msgObjArr[i];
if (obj.toUserName == tarUserName) {
//保存最新数据
obj.message.push({username: srcUserName, message: message, date: NowTime()});
isExist = true;
break;
}
}
if (!isExist) {
//追加聊天对象
msgObjArr.push({
toUserName: tarUserName,
message: [{username: srcUserName, message: message, date: NowTime()}]//封装数据[{username:huanzi,message:"你好,我是欢子!",date:2018-04-29 22:48:00}]
});
}
} else {
//追加聊天对象
msgObjArr.push({
toUserName: tarUserName,
message: [{username: srcUserName, message: message, date: NowTime()}]//封装数据[{username:huanzi,message:"你好,我是欢子!",date:2018-04-29 22:48:00}]
});
}
}
//监听点击用户
$("body").on("click", ".hz-group-list", function () {
$(".hz-group-list").css("background-color", "");
$(this).css("background-color", "whitesmoke");
$("#toUserName").text($(this).find(".hz-group-list-username").text());
//清空旧数据,从对象中取出并追加
$("#hz-message-body").empty();
$("#hz-badge-" + $("#toUserName").text()).text("0");
$("#hz-badge-" + $("#toUserName").text()).css("opacity", "0");
if (msgObjArr.length > 0) {
for (var i = 0; i < msgObjArr.length; i++) {
var obj = msgObjArr[i];
if (obj.toUserName == $("#toUserName").text()) {
//追加数据
var messageArr = obj.message;
if (messageArr.length > 0) {
for (var j = 0; j < messageArr.length; j++) {
var msgObj = messageArr[j];
var leftOrRight = "right";
var message = msgObj.message;
var msgUserName = msgObj.username;
var toUserName = $("#toUserName").text();
//当聊天窗口与msgUserName的人相同,文字在左边(对方/其他人),否则在右边(自己)
if (msgUserName == toUserName) {
leftOrRight = "left";
}
//但是如果点击的是自己,群聊的逻辑就不太一样了
if (username == toUserName && msgUserName != toUserName) {
leftOrRight = "left";
}
if (username == toUserName && msgUserName == toUserName) {
leftOrRight = "right";
}
var magUserName = leftOrRight == "left" ? "<p class='hz-message-list-username'>"+msgUserName+":</p>" : "";
$("#hz-message-body").append(
"<div class=\"hz-message-list\">" +
magUserName+
"<div class=\"hz-message-list-text " + leftOrRight + "\">" +
"<span>" + message + "</span>" +
"</div>" +
"<div style=\" clear: both; \"></div>" +
"</div>");
}
}
break;
}
}
}
});
//获取当前时间
function NowTime() {
var time = new Date();
var year = time.getFullYear();//获取年
var month = time.getMonth() + 1;//或者月
var day = time.getDate();//或者天
var hour = time.getHours();//获取小时
var minu = time.getMinutes();//获取分钟
var second = time.getSeconds();//或者秒
var data = year + "-";
if (month < 10) {
data += "0";
}
data += month + "-";
if (day < 10) {
data += "0"
}
data += day + " ";
if (hour < 10) {
data += "0"
}
data += hour + ":";
if (minu < 10) {
data += "0"
}
data += minu + ":";
if (second < 10) {
data += "0"
}
data += second;
return data;
}
java代码有三个类,MyEndpointConfigure,WebSocketConfig,WebSocketServer;
MyEndpointConfigure
/**
* 解决注入其他类的问题,详情参考这篇帖子:webSocket无法注入其他类:
*/
public class MyEndpointConfigure extends ServerEndpointConfig.Configurator implements ApplicationContextAware {
private static volatile BeanFactory context;
@Override
public <T> T getEndpointInstance(Class<T> clazz){
return context.getBean(clazz);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
MyEndpointConfigure.context = applicationContext;
}
}
WebSocketConfig
/**
* WebSocket配置
*/
@Configuration
public class WebSocketConfig{
/**
* 用途:扫描并注册所有携带@ServerEndpoint注解的实例。 @ServerEndpoint("/websocket")
* PS:如果使用外部容器 则无需提供ServerEndpointExporter。
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
/**
* 支持注入其他类
*/
@Bean
public MyEndpointConfigure newMyEndpointConfigure (){
return new MyEndpointConfigure ();
}
}
WebSocketServer
/**
* WebSocket服务
*/
@RestController
@RequestMapping("/websocket")
@ServerEndpoint(value = "/websocket/{username}", configurator = MyEndpointConfigure.class)
public class WebSocketServer {
/**
* 在线人数
*/
private static int onlineCount = 0;
/**
* 在线用户的Map集合,key:用户名,value:Session对象
*/
private static Map<String, Session> sessionMap = new HashMap<String, Session>();
/**
* 注入其他类(换成自己想注入的对象)
*/
@Autowired
private UserService userService;
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("username") String username) {
//在webSocketMap新增上线用户
sessionMap.put(username, session);
//在线人数加加
WebSocketServer.onlineCount++;
//通知除了自己之外的所有人
sendOnlineCount(session, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + username + "'}");
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
//下线用户名
String logoutUserName = "";
//从webSocketMap删除下线用户
for (Entry<String, Session> entry : sessionMap.entrySet()) {
if (entry.getValue() == session) {
sessionMap.remove(entry.getKey());
logoutUserName = entry.getKey();
break;
}
}
//在线人数减减
WebSocketServer.onlineCount--;
//通知除了自己之外的所有人
sendOnlineCount(session, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + logoutUserName + "'}");
}
/**
* 服务器接收到客户端消息时调用的方法
*/
@OnMessage
public void onMessage(String message, Session session) {
try {
//JSON字符串转 HashMap
HashMap hashMap = new ObjectMapper().readValue(message, HashMap.class);
//消息类型
String type = (String) hashMap.get("type");
//来源用户
Map srcUser = (Map) hashMap.get("srcUser");
//目标用户
Map tarUser = (Map) hashMap.get("tarUser");
//如果点击的是自己,那就是群聊
if (srcUser.get("username").equals(tarUser.get("username"))) {
//群聊
groupChat(session,hashMap);
} else {
//私聊
privateChat(session, tarUser, hashMap);
}
//后期要做消息持久化
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 发生错误时调用
*/
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
/**
* 通知除了自己之外的所有人
*/
private void sendOnlineCount(Session session, String message) {
for (Entry<String, Session> entry : sessionMap.entrySet()) {
try {
if (entry.getValue() != session) {
entry.getValue().getBasicRemote().sendText(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 私聊
*/
private void privateChat(Session session, Map tarUser, HashMap hashMap) throws IOException {
//获取目标用户的session
Session tarUserSession = sessionMap.get(tarUser.get("username"));
//如果不在线则发送“对方不在线”回来源用户
if (tarUserSession == null) {
session.getBasicRemote().sendText("{\"type\":\"0\",\"message\":\"对方不在线\"}");
} else {
hashMap.put("type", "1");
tarUserSession.getBasicRemote().sendText(new ObjectMapper().writeValueAsString(hashMap));
}
}
/**
* 群聊
*/
private void groupChat(Session session,HashMap hashMap) throws IOException {
for (Entry<String, Session> entry : sessionMap.entrySet()) {
//自己就不用再发送消息了
if (entry.getValue() != session) {
hashMap.put("type", "2");
entry.getValue().getBasicRemote().sendText(new ObjectMapper().writeValueAsString(hashMap));
}
}
}
/**
* 登录
*/
@RequestMapping("/login/{username}")
public ModelAndView login(HttpServletRequest request, @PathVariable String username) {
return new ModelAndView("socketChart.html", "username", username);
}
/**
* 登出
*/
@RequestMapping("/logout/{username}")
public String loginOut(HttpServletRequest request, @PathVariable String username) {
return "退出成功!";
}
/**
* 获取在线用户
*/
@RequestMapping("/getOnlineList")
private List<String> getOnlineList(String username) {
List<String> list = new ArrayList<String>();
//遍历webSocketMap
for (Entry<String, Session> entry : WebSocketServer.sessionMap.entrySet()) {
if (!entry.getKey().equals(username)) {
list.add(entry.getKey());
}
}
return list;
}
}
后记
后期把所有功能都补全就完美了,表情、图片都算比较简单,之前用轮询实现的时候写过了,但是没加到这里来;音视频聊天的话可以用WbeRTC来做,之前也研究了一下,不过还没搞完,这里贴一下维基百科对它的介绍,想了解更多的自行Google:
WebRTC,名称源自网页即时通信(英语:Web Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音对话或视频对话的API。它于2011年6月1日开源并在Google、Mozilla、Opera支持下被纳入万维网联盟的W3C推荐标准。
最后在加上持久化存储,注册后才能聊天,离线消息上线后接收,再加上用Redis或者其他的缓存技术支持,完美。不过聊天记录要做存储,表设计不知如何设计才合理,如果哪位大佬愿意分享可以留言给我,大家一起进步!
补充
2019-07-03补充:这里补充贴出pom代码,在子类引入父类,如果我们没有父类,只有一个子类,把两个整合一下就可以了
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.huanzi.qch</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
<relativePath/>
</parent>
<description>SpringBoot系列demo代码</description>
<!-- 在父类引入一下通用的依赖 -->
<dependencies>
<!-- spring-boot-starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- springboot web(MVC)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springboot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--lombok插件 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--热部署工具dev-tools-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<scope>runtime</scope>
</dependency>
</dependencies>
<!--构建工具-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<finalName>${project.artifactId}</finalName>
<outputDirectory>../package</outputDirectory>
</configuration>
</plugin>
</plugins>
</build>
</project>
parent.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>springboot-websocket</artifactId>
<version>0.0.1</version>
<name>springboot-websocket</name>
<description>SpringBoot系列——WebSocket</description>
<!--继承父类-->
<parent>
<groupId>cn.huanzi.qch</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<dependencies>
<!-- springboot websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- thymeleaf模板 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
springboot-websocket.xml
在后记的部分我们就提到要加上持久化存储,事实上我们已经开始慢慢在写一套简单的IM即时通讯,已经实现到第三版了,持续更新中...
代码开源
代码已经开源、托管到我的GitHub、码云:
GitHub:https://github.com/huanzi-qch/springBoot
码云:https://gitee.com/huanzi-qch/springBoot