实时聊天室
- 前言
- 效果图
- 涉及技术
- springboot
- layui
- websocket
- 实现思路
- websocket在springboot下的实现
- 前端实现
- 建立websocket连接
- 前端对应的websocket方法
- 代码实现
- 后端代码
- 建立连接时
- 接收到消息时
- 发送消息
- 总代码
- 前端代码
- 总结
前言
复习感觉无聊的时候就想拿以前学习的东西做几个小案例,这段时间在搭一个博客网站,正好做到私信这个模块,突然想试试看看可不可以做成一个实时通信的私信功能,思路一来就一发不可收拾,开整开征。
效果图
如下图所示,可以实现文字的即时通讯,图片发送啥的还不支持,有空再搞。(UI有点丑,但能用就彳亍),但功能总归还是比较齐全,不仅仅只是websocket的双工通信,包括但不限于聊天记录的存储,过往聊天记录查看等功能。
涉及技术
springboot
对于我这种熟悉java的人来说,做后端那肯定离不开springboot了,不得不说,springboot yyds!!!如果不是很清楚怎么去搭建一个springboot项目的话,可以去看看手把手搭建一个springboot项目这篇博客。
layui
最近发现layui的模板属实很大气,因此就采用这个模块作为前端模块了,还有一个原因就是简单(前端小白不敢说话),如何使用可以参考layui的使用手册==>layui使用手册,复制即可用
websocket
能做成这个聊天模块最大的功臣,来看下百度给他的定义
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
可能些许有些难懂,但只要明白这个东西可以全双工通信就可以了,主要的作用就是浏览器可以通过websocket向服务端发送消息,同样服务端也可向浏览器发送消息,这不就是咱们平常的聊天吗,我可以和你说话,同时你也可以和我说话。
实现思路
现在我们聊一下怎么实现这个聊天室,最基础的聊天室一定需要两个角色,分别是发送方和接收方,并且在这种情形下,发送方也是接收方,接收方也是发送方,即二者所拥有的功能应该是相同的。
在我们这个聊天室角度来看就是,两个角色都应该有发送消息和接收消息的功能,但是很明显ajax无法做到这个功能,因此我们采用websocket进行消息的接受与发送的服务。那么如何实现呢
websocket在springboot下的实现
有几个注解先了解一下,
@OnOpen //建立socket连接时调用
@OnError //服务端出现问题时调用
@OnClose //socket连接断开时调用
@OnMessage //服务端接收到信息之后调用
这几个注解直接标注在方法上代表当出现以上情况时,就直接调用对应标注的方法,如下图所示,当接收到服务端的信息时就直接调用onMessage方法
前端实现
在h5之后,html也支持socket编程了,所以咱们一般看到的网页都支持,但是确实有极少数一些老年浏览器不支持socket编程,这就没办法了。
建立websocket连接
html打开之后需要先于服务端建立起websocket连接,这样才能与服务端进行交互。下面代码就是建立连接,注意这里前面是ws不是之前的http了。
webSocket = new WebSocket("ws://192.168.43.220:9010/websocket/" + getParams("id"));
前端对应的websocket方法
前端的方法如下图所示,可以看到和上面的名字都是一一对应的,具体作用和后端的方法是一致的
到此我们就可以进行代码的编写了。
代码实现
后端代码
建立连接时
此时会连接数据库,把之前和你聊过天的用户的信息读取出来,传递给浏览器,这里getinfos方法就是实现数据库操作,大家有兴趣可以直接写一下
注意这里会将我们此次websocket的session存储在内存中,方便其他websocket的session与我们通信,但我觉得存储容器可以换成redis,但还没改,有兴趣的可以改一下
@OnOpen
public void onOpen(@PathParam("username") String username, Session session) {
onlineNumber++;
log.info("现在来连接的客户id:" + session.getId() + "用户名:" + username);
this.username = username;
this.session = session;
log.info("有新连接加入! 当前在线人数" + onlineNumber);
try {
clients.put(username, this);
System.out.println(username);
List infos = getInfos(Integer.parseInt(username));
Map<String, Object> map2 = new HashMap<>();
map2.put("messageType", 3);
map2.put("infos", infos);
sendMessageTo(JSON.toJSONString(map2), username);
} catch (Exception e) {
e.printStackTrace();
log.info(username + "上线的时候通知所有人发生了错误");
}
}
接收到消息时
这里的作用主要就是把接收到的信息进行解析,然后使用sendinfoTo来发送消息。
这里websocket的session信息是以键值对的方式存储在内存中,其中key就是对应的账号,可以通过key来查找对应的session,从而实现对该用户的通信。因此这里还有找出对应的session的功能。
*/
@OnMessage
public void onMessage(String message, Session session) {
try {
JSONObject jsonObject = JSON.parseObject(message);
String textMessage = jsonObject.getString("message");
String from = jsonObject.getString("from");
String to = jsonObject.getString("to");
//如果不是发给所有,那么就发给某一个人
//messageType 1在线人员信息 2普通消息
Map<String, Object> map1 = new HashMap<>();
map1.put("messageType", 2);
map1.put("textMessage", textMessage);
map1.put("fromusername", from);
map1.put("tousername", to);
sendInfoTo(JSON.toJSONString(map1), to, textMessage);
} catch (Exception e) {
log.info("发生了错误了");
}
}
发送消息
没啥好说的,就是发送信息
public void sendMessageTo(String message, String ToUserName) throws IOException {
WebSocket webSocket = clients.get(ToUserName);
if (webSocket != null) {
webSocket.session.getAsyncRemote().sendText(message);
} else {
log.info("未上线");
}
}
总代码
package com.xiaow.community.common.vo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xiaow.community.common.fegin.service.AccounService;
import com.xiaow.community.entity.Message;
import com.xiaow.community.service.MessageService;
import com.xiaow.community.util.SpringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
@ServerEndpoint("/websocket/{username}")
public class WebSocket {
/**
* 在线人数
*/
public static int onlineNumber = 0;
/**
* 以用户的姓名为key,WebSocket为对象保存起来
*/
private static Map<String, WebSocket> clients = new ConcurrentHashMap<String, WebSocket>();
/**
* 会话
*/
private Session session;
/**
* 用户名称
*/
private String username;
/**
* OnOpen 表示有浏览器链接过来的时候被调用
* OnClose 表示浏览器发出关闭请求的时候被调用
* OnMessage 表示浏览器发消息的时候被调用
* OnError 表示有错误发生,比如网络断开了等等
*/
//获取发消息的对象
public List getInfos(Integer tid) {
MessageService messageService = SpringUtil.getBean(MessageService.class);
List<Message> list = messageService.getTo(tid);
List<Message> list1 = messageService.getFrom2(tid);
list.addAll(list1);
System.out.println(list);
Collections.sort(list, Comparator.comparing(Message::getSendt).thenComparing(Message::getSendt).reversed());
//进行去重
Set<Integer> set = new HashSet<>();
for (int i = 0; i < list.size(); i++) {
Integer key;
if (list.get(i).getFrom2() == tid) {
key = list.get(i).getTo2();
} else {
key = list.get(i).getFrom2();
}
if (set.contains(key)) {
list.remove(i);
i--;
continue;
}
set.add(key);
}
List acs = new LinkedList();
set.forEach(s -> {
acs.add(s);
});
AccounService accounService = SpringUtil.getBean(AccounService.class);
List getonesasfegin = accounService.getonesasfegin(acs);
return getonesasfegin;
}
public void addinfo(Message message) {
MessageService messageService = SpringUtil.getBean(MessageService.class);
messageService.save(message);
}
/**
* 建立连接
*
* @param session
*/
@OnOpen
public void onOpen(@PathParam("username") String username, Session session) {
onlineNumber++;
log.info("现在来连接的客户id:" + session.getId() + "用户名:" + username);
this.username = username;
this.session = session;
log.info("有新连接加入! 当前在线人数" + onlineNumber);
try {
clients.put(username, this);
System.out.println(username);
List infos = getInfos(Integer.parseInt(username));
Map<String, Object> map2 = new HashMap<>();
map2.put("messageType", 3);
map2.put("infos", infos);
sendMessageTo(JSON.toJSONString(map2), username);
} catch (Exception e) {
e.printStackTrace();
log.info(username + "上线的时候通知所有人发生了错误");
}
}
@OnError
public void onError(Session session, Throwable error) {
log.info("服务端发生了错误" + error.getMessage());
}
/**
* 连接关闭
*/
@OnClose
public void onClose() {
System.out.println("推出账户:" + username);
onlineNumber--;
clients.remove(username);
log.info("有连接关闭! 当前在线人数" + onlineNumber);
}
/**
* 收到客户端的消息
*
* @param message 消息
* @param session 会话
*/
@OnMessage
public void onMessage(String message, Session session) {
try {
JSONObject jsonObject = JSON.parseObject(message);
String textMessage = jsonObject.getString("message");
String from = jsonObject.getString("from");
String to = jsonObject.getString("to");
//如果不是发给所有,那么就发给某一个人
//messageType 1在线人员信息 2普通消息
Map<String, Object> map1 = new HashMap<>();
map1.put("messageType", 2);
map1.put("textMessage", textMessage);
map1.put("fromusername", from);
map1.put("tousername", to);
sendInfoTo(JSON.toJSONString(map1), to, textMessage);
} catch (Exception e) {
log.info("发生了错误了");
}
}
//进行发消息的功能 即向前端推送信息
public void sendInfoTo(String message, String ToUserName, String info) throws IOException {
System.out.println(message);
WebSocket webSocket = clients.get(ToUserName);
if (webSocket != null) {
webSocket.session.getAsyncRemote().sendText(message);
} else {
log.info("未上线");
}
Message message1 = new Message()
.setFrom2(Integer.parseInt(username))
.setTo2(Integer.parseInt(ToUserName))
.setInfo(info)
.setSendt(LocalDateTime.now())
.setState(0);
addinfo(message1);
}
public void sendMessageTo(String message, String ToUserName) throws IOException {
WebSocket webSocket = clients.get(ToUserName);
if (webSocket != null) {
webSocket.session.getAsyncRemote().sendText(message);
} else {
log.info("未上线");
}
}
public void sendMessageAll(String message, String FromUserName) throws IOException {
for (WebSocket item : clients.values()) {
item.session.getAsyncRemote().sendText(message);
}
}
public static synchronized int getOnlineCount() {
return onlineNumber;
}
}
前端代码
前端代码有点杂,有兴趣的可以看一下,没兴趣的复制就可以用
<!DOCTYPE html>
<head>
<title>websocket</title>
<script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.min.js"></script>
<script src="http://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script src="/assets/blog.js"></script>
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="/assets/jquery.min.js"></script>
<link rel="stylesheet" href="/assets/layui-v2.6.8/layui/css/layui.css" media="all">
<script src="/assets/layui-v2.6.8/layui/layui.js" charset="utf-8"></script>
</head>
<body>
<ul class="layui-nav" lay-bar="disabled">
<div class="layui-row">
<div class="layui-col-md4">
<div class="grid-demo grid-demo-bg1" style="padding: 20px 0;">
<a href="./bloglist.html" style="color: white;">
<h1 style="font-size: 20px;">xiaowblog</h1>
</a>
</div>
</div>
<div class="layui-col-md4">
<div class="grid-demo">
<div class="layui-input-block " style="padding: 10px 0;">
<input type="text " name="title " required lay-verify="required " placeholder="请输入内容 " autocomplete="off " class="layui-input ">
</div>
</div>
</div>
<div class="layui-col-md4 ">
<div class="grid-demo grid-demo-bg1 ">
<div style="text-align: right; ">
<li class="layui-nav-item ">
<a href=" ">带徽章<span class="layui-badge ">9</span></a>
</li>
<li class="layui-nav-item ">
<a href=" ">小圆点<span class="layui-badge-dot "></span></a>
</li>
<li class="layui-nav-item " lay-unselect=" " style="left: 0px; ">
<a href="javascript:; "><img src="//t.cn/RCzsdCq " class="layui-nav-img "></a>
<dl class="layui-nav-child ">
<dd><a href="./test.html ">写文章</a></dd>
<dd><a href="javascript:; ">横线隔断</a></dd>
<hr>
<dd style="text-align: center; "><a href=" ">退出</a></dd>
</dl>
</li>
</div>
</div>
</div>
</div>
</ul>
<div class="layui-row layui-col-space1" style="width: 70%;height: 800px; margin-top: 5%;margin-left: 15%;">
<div class="layui-col-md3 " id="friends" style="background-color: darkcyan; height: 100%;">
</div>
<div class="layui-col-md9 " style="height: 100%;border-top:1px solid #000;">
<div id="top" style="height: 5%;background-color: darkcyan;text-align: center;">
<h1 id="topname" style="color: white;"></h1>
</div>
<div id="info" style="height: 65%;margin: 5px;overflow:auto">
</div>
<div id="toolbar" style="height: 5%;text-align: center;">
</div>
<div id="text" style="height: 22%;">
<textarea id="infotext" style="height: 80%;width: 100%;
resize: none;
cursor: pointer;"></textarea>
<div style="height: 20%;padding: 5px;"><button id="sendinfo" value="1" class="layui-btn">发送</button></div>
</div>
</div>
</div>
</body>
<script>
$('#sendinfo').click(function() {
send()
})
</script>
<script type="text/javascript">
var flist = new Array();
var webSocket;
var commWebSocket;
if ("WebSocket" in window) {
webSocket = new WebSocket("ws://192.168.43.220:9010/websocket/" + getParams("id"));
//连通之后的回调事件
webSocket.onopen = function() {
//webSocket.send( document.getElementById('username').value+"已经上线了");
console.log("已经连通了websocket");
};
//接收后台服务端的消息
webSocket.onmessage = function(evt) {
var received_msg = evt.data;
var obj = JSON.parse(received_msg);
//普通消息
if (obj.messageType == 2) {
if (obj.fromusername == $("#sendinfo").attr('value')) {
html = '<div style="padding: 15px;background-color: darkcyan;border-radius: 10px;width:auto; display:inline-block !important; display:inline;color:white;">' + obj.textMessage + '</div><br><br>'
$("#info").append(html);
$("#info").animate({
scrollTop: $("#info").prop("scrollHeight")
}, 400); //0.4秒内滚到底部
} else {
var state = 0;
$(".f_item").each(function() {
if ($(this).attr('value') == obj.fromusername) {
$(this).find('.layui-badge-dot').css("display", "block")
state = 1;
}
})
if (state = 0) {
//说明之前没有聊过天 ,再将改人的信息加入聊天栏中,需要访问account的接口获取用户数据
}
}
}
//渲染聊天栏朋友
if (obj.messageType == 3) {
users = obj.infos;
for (e in users) {
html = ' <div class="f_item" value=' + users[e].acid + ' style="height: 50px;padding: 10px;">\
<div class="layui-row layui-col-space1">\
<div class="layui-col-md3">\
<div class="grid-demo grid-demo-bg1">\
<img src="' + users[e].avator + '" style="height: 80%;width: 100%; border-radius: 100px;" />\
</div>\
</div>\
<div class="layui-col-md9">\
<div class="grid-demo" style="text-align: center;"><h1 style="color:white;" class="username">' + users[e].username + '</h1> <span class="layui-badge-dot" style="display:none"></span>\
</div>\
</div>\
</div>\
</div><br><br>'
$('#friends').append(html)
flist.push(users[e].acid)
}
var item = $('.f_item').click(function() {
$(this).find('.layui-badge-dot').css("display", "none")
$('#info').html("")
$('#topname').text($(this).find('.username').text())
$("#sendinfo").attr('value', $(this).attr('value'))
//渲染消息
$.ajax({
url: "http://localhost:9010/message/getByToAndFrom?to=" + getParams("id") + "&from=" + $(this).attr('value'), //路径 只需改为你的路径即可
type: "get",
dataType: "json",
success: function(data) {
for (e in data.data) {
if (data.data[e].to2 == getParams("id")) {
html = '<div style=" word-break: break-all;word-wrap: break-word;padding: 15px;background-color: darkcyan;border-radius: 10px;width:auto; display:inline-block !important; display:inline;color:white;">' + data.data[e].info + '</div><br><br>'
$("#info").append(html);
} else {
html = '<div style="word-break: break-all;word-wrap: break-word;float:right;padding: 15px;background-color: darkcyan;border-radius: 10px;width:auto; display:inline-block !important; display:inline;color:white;">' + data.data[e].info + '</div><br><br><br><br>'
$("#info").append(html);
}
}
}
})
$("#info").animate({
scrollTop: $("#info").prop("scrollHeight")
}, 400); //0.4秒内滚到底部
})
}
};
//连接关闭的回调事件
webSocket.onclose = function() {
console.log("连接已关闭...");
};
} else {
// 浏览器不支持 WebSocket
alert("您的浏览器不支持 WebSocket!");
}
//发送消息
function send() {
var selectText = $("#infotext").val();
var message = {
"message": selectText,
"to": $('#sendinfo').attr('value'),
"from": getParams("id")
}
webSocket.send(JSON.stringify(message));
$("#infotext").val("");
html = '<div style=" word-break: break-all;word-wrap: break-word;float:right;padding: 15px;background-color: darkcyan;border-radius: 10px;width:auto; display:inline-block !important; display:inline;color:white;">' + selectText + '</div><br><br><br><br>'
$("#info").append(html);
//滚动条到最低部
$("#info").animate({
scrollTop: $("#info").prop("scrollHeight")
}, 400); //0.4秒内滚到底部
}
</script>
</html>
总结
这一次收获不小,之前接触的都是ajax这种http响应的内容,现在突然接触这种全双工的内容,并且可以作出一个东西,感觉收获还是不小的,全部后端代码已上传码云,利用springcloud-alibaba搭建的一个微服务博客后端,目前还在完善中,这一次聊天的模块是其中的一部分,有兴趣的可以看一下完整代码
微服务博客
如果有任何问题可以随时交流,对了最后说一下不足,如果为了应对高并发,一台这样的服务器是承受不住的,需要在多台服务器搭建这样的websocket服务,但是多台服务之间该怎么交互。
个人认为可以采用redis的订阅和发布功能,每台服务器都订阅这个频道,一旦有消息传入,redis即发布消息,各台服务器根据目标id判断是不是接入自己服务的用户从而选择是否通知,从而实现一个简陋的聊天服务集群。
好了,今天就到这了,有缘再写