SpringBoot+websocket 实现web聊天功能(单聊、保存消息)
目录一、前言二、什么是websocket?三、websocket API介绍四、数据库表设计Ⅰ.需求分析(参考csdn私信功能)Ⅱ.表的设计Ⅲ.Sql语句五、前端页面Ⅰ、简单前端代码Ⅱ、页面展示Ⅲ、页面说明六、后端业务处理Ⅰ.引入依赖Ⅱ.websocket配置类一、前言这篇文章主要是分享一次web聊天功能开发的过程。在文章中首先会简单介绍什么是websocket和websocket的方法;然后再详
目录
一、前言
这篇文章主要是分享一次web聊天功能开发的过程。在文章中首先会简单介绍什么是websocket和websocket的API;然后再详细地介绍功能的实现,其中包括了数据库表的设计、简单前端页面的编写、接口的编写等。从基本的页面前端到后端一体式地开发分享。如果你已经对websocket有所了解,你可以直接跳到对应的编码部分进行阅读。
二、什么是websocket?
问题思考:在你们没有了解到在web端通信时要使用websocket之前,你们是通过什么方式来进行web端之间的通信的?
在我没有了解到websocket的时候,我想到的是站内信,即延时通信。欸,到这里很多小伙伴就会问,什么是站内信呢?其实就是客户端发一个请求然后服务器端响应一个请求(BS模型)。在实际的聊天中,可以理解为客户端A发送一条消息给客户端B,消息通过请求提交给服务器,然后服务器进行一个数据库的存储。这时候,服务器端是不会自动将这条消息转发给客户端B的,必须是要客户端B向服务器发送请求是否存在未读消息,服务器才会响应这条未读消息。客户端B会对服务器轮询,不间断地发送请求,看是否存在着未读消息,消耗服务器的资源和带宽。
websocket是一个管道式的编程,建立一条客户端与服务器之间的通道,持续连接。
websocket protocol 是HTML5的一种协议。它实现了浏览器与服务器全双工通信(full-duplex)。通过第一次HTTP Request成功建立了连接通道(握手连接)之后,后续的数据传送都不用再重新发送HTTP Request。
HTTP 与 WebSocket 的主要区别:
三、websocket API介绍
(1)new webSocket(target);
创建websocket对象,客户端与服务器建立连接通道。
var ws = new WebSocket(‘ws://localhost:8080/websocket’);
(2)webSocket.readyState
readyState属性返回实例对象的当前状态,共有四种。CONNECTING:值为0,表示正在连接。OPEN:值为1,表示连接成功,可以通信了。CLOSING:值为2,表示连接正在关闭。CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
(3)webSocket.onopen
实例对象的onopen属性,用于指定连接成功后的回调函数。
ws.onopen = function(){}
(4)webSocket.onclose
实例对象的onclose属性,用于指定连接关闭后的回调函数。
(5)webSocket.onmessage
实例对象的onmessage属性,用于指定收到服务器数据后的回调函数。
注意,服务器数据可能是文本,也可能是二进制数据(blob对象或Arraybuffer对象)
(6)webSocket.send()
实例对象的send()方法用于向服务器发送数据。
(7)webSocket.bufferedAmount
实例对象的bufferedAmount属性,表示还有多少字节的二进制数据没有发送出去,它可以用来判断发送是否结束。
(8)webSocket.onerror
实例对象的onerror属性,用于指定报错时的回调函数。
四、数据库表设计
在数据库表的设计中,我参考了其他博主的文章,也对其设计进行了一点改动。
博主文章:实现类似微信聊天功能的mysql表设计
(一)需求分析(参考csdn私信功能)
- 在一个web网站中,实际上大多数情况下时通过搜索该用户,查看其个人信息页面,然后对其进行私信或者是通过浏览该用户发布的内容,进行私信联系。点击按钮之后,会跳转到聊天列表页面。
- 在聊天列表页面中,会展示与曾经交流过的用户列表,在每一列中会出现的数据是用户名、用户头像、最后一条消息内容、最后一条消息的发送时间、未读数。当点击了私信按钮之后,跳转到聊天列表页面中,在第一列会是当前私信的对象,若是第一次聊天,没有数据返回;若不是第一次聊天,则会返回近期的聊天记录。
- 点击某个聊天列时,进入到聊天窗口,获取对应的聊天记录,将当前用户的聊天列表的记录的状态改为在线。
(二)表的设计
在简单的需求分析之后,开始对表的字段以及表之间的关系进行设计。(其他博主的设计的基础上)
-
用户聊天关系表(chat_user_link)
link_id(主键id)
from_user(发送方用户名,用户表主键)
to_user(接收方用户名,用户表主键)
create_time(创建时间) -
聊天列表表(chat_list)
list_id(主键id,自增)
link_id(用户聊天关系表主键)
from_user(发送方用户名)
to_user(接收方用户名)
from_window(发送方是否在窗口)
to_window(接收方是否在窗口)
unread(未读数)
status(列表状态,是否删除) -
聊天内容详情表(chat_message)
message_id(主键id,自增)
link_id(用户聊天关系表主键)
from_user(发送方用户名)
to_user(接收方用户名)
content(消息内容)
send_time(发送时间)
type(类型)
is_latest(是否是最后一条消息)
这个表结构的设计是最终版本的设计,所以看到从开头看到这里的小伙伴会有点懵懵的,不要紧,你先试着先去了解一下,后面会对某些字段进行说明。(注:每个系统的表的设计是按照其需求去设计的,不同的需求会有不同的设计,如当前系统的用户表的主键是用户名,所以我用了用户名作为聊天关系表的外键与用户表进行一个关联。)欸,很多小伙伴就会说了,你为什么会在三个表中都出现了link_id,from_user,to_user这三个字段呢?简单说明一下就是为了简化数据表的查询,有意地进行数据的冗余,不必要去过多去多表查询。
(三)聊天逻辑
-
点击私信按钮
判断是不是第一次聊天,如果是会在主表生成一条记录返回用户聊天关系表id,并在聊天列表表分别插入两条记录,如果不是第一次聊天进入下一步 -
进入聊天对话框
获取近期几条聊天记录,将用户在此对话的窗口(from_window)改为1,即表示在线 -
发送聊天信息
3.1、先判断对方是否在线,不在线的话对方未读数+1
3.2、将上一条最后一条消息状态改为否
3.3、往聊天详情表插入聊天信息数据
(四)Sql语句
CREATE TABLE `chat_user_link` (
`link_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '聊天主表id',
`from_user` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '发送者',
`to_user` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '接收者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建关联时间',
PRIMARY KEY (`link_id`) USING BTREE,
INDEX `fk_link_user1`(`from_user`) USING BTREE,
INDEX `fk_link_user2`(`to_user`) USING BTREE,
CONSTRAINT `fk_link_user1` FOREIGN KEY (`from_user`) REFERENCES `user` (`username`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `fk_link_user2` FOREIGN KEY (`to_user`) REFERENCES `user` (`username`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
CREATE TABLE `chat_list` (
`list_id` int(0) NOT NULL AUTO_INCREMENT COMMENT '聊天列表主键',
`link_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '聊天主表id',
`from_user` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '发送者',
`to_user` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '接收者',
`from_window` tinyint(0) NULL DEFAULT NULL COMMENT '发送方是否在窗口',
`to_window` tinyint(0) NULL DEFAULT NULL COMMENT '接收方是否在窗口',
`unread` int(0) NULL DEFAULT NULL COMMENT '未读数',
`status` tinyint(0) NULL DEFAULT NULL COMMENT '是否删除',
PRIMARY KEY (`list_id`) USING BTREE,
INDEX `fk_list_link`(`link_id`) USING BTREE,
INDEX `fk_list_user1`(`from_user`) USING BTREE,
INDEX `fk_list_user2`(`to_user`) USING BTREE,
CONSTRAINT `fk_list_link` FOREIGN KEY (`link_id`) REFERENCES `chat_user_link` (`link_id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `fk_list_user1` FOREIGN KEY (`from_user`) REFERENCES `user` (`username`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `fk_list_user2` FOREIGN KEY (`to_user`) REFERENCES `user` (`username`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
CREATE TABLE `chat_message` (
`message_id` int(0) NOT NULL AUTO_INCREMENT COMMENT '聊天内容id',
`link_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '聊天主表id',
`from_user` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '发送者',
`to_user` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '接收者',
`content` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '聊天内容',
`send_time` datetime(0) NOT NULL COMMENT '发送时间',
`type` int(0) NOT NULL COMMENT '消息类型',
`is_latest` tinyint(0) NULL DEFAULT NULL COMMENT '是否为最后一条信息',
PRIMARY KEY (`message_id`) USING BTREE,
INDEX `fk_message_link`(`link_id`) USING BTREE,
INDEX `fk_message_user1`(`from_user`) USING BTREE,
INDEX `fk_message_user2`(`to_user`) USING BTREE,
CONSTRAINT `fk_message_link` FOREIGN KEY (`link_id`) REFERENCES `chat_user_link` (`link_id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `fk_message_user1` FOREIGN KEY (`from_user`) REFERENCES `user` (`username`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `fk_message_user2` FOREIGN KEY (`to_user`) REFERENCES `user` (`username`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
用户表请结合自己的实际情况进行嵌入,修改以上三张表中的from_user、to_user字段。
五、前端页面
注:前端代码和部分后端代码借鉴了其他博主的博客的代码。
其他博主的文章:springboot+websocket构建在线聊天室(群聊+单聊)
(一)简单前端代码
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>My WebSocket</title>
<style>
#message {
margin-top: 40px;
border: 1px solid gray;
padding: 20px;
}
</style>
</head>
<body>
<button onclick="connectWebSocket()">连接WebSocket</button>
<button onclick="closeWebSocket()">断开连接</button>
<hr/>
<br/>
消息:<input id="text" type="text" />
接收者:<input id="toUser" type="text" />
<button onclick="send()">发送消息</button>
<div id="message"></div>
</body>
<script type="text/javascript">
let websocket = null;
function connectWebSocket() {
//这里需要的路径需要配置相对应的路径
const target = "ws://localhost:8080/share/websocket";
//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket(target);
} else {
alert('Not support websocket')
}
//连接发生错误的回调方法
websocket.onerror = function () {
setMessageInnerHTML("error");
};
//连接成功建立的回调方法
websocket.onopen = function (event) {
setMessageInnerHTML("Loc MSG: 建立连接");
}
//接收到消息的回调方法
websocket.onmessage = function (event) {
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
websocket.onclose = function () {
setMessageInnerHTML("Loc MSG:关闭连接");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
websocket.close();
}
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//关闭连接
function closeWebSocket() {
websocket.close();
}
//发送消息
function send() {
//获取输入的文本信息进行发送
const msg = document.getElementById('text').value;
const toUser = document.getElementById('toUser').value;
const chatMsg = {content: msg, toUser: toUser};
websocket.send(JSON.stringify(chatMsg));
}
</script>
</html>
(二)页面展示
(三)页面说明
- 前置准备:你需要成功登录,并将用户的个人信息(username用户名)存入到httpsession域中。因为需要在session域中获取到发送方的用户名即from_user。
- 进行聊天信息发送时,需要进行点击连接webSocket的按钮,进行握手连接,连接成功之后会打印"Loc MSG: 建立连接"。成功连接后,你就可以输入消息,并输入接收者的用户名,然后点击发送消息,即可发送。
- 当你想关闭浏览器或者点击断开连接,你就会与服务器断开连接。
六、后端业务处理
(一)引入依赖
<!--websocket依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.4.1</version>
</dependency>
(二)websocket配置类
@Configuration
public class WebSocketConfig extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
//获取httpsession
HttpSession session = (HttpSession) request.getHttpSession();
sec.getUserProperties().put(HttpSession.class.getName(), session);
}
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
(三)websocket监听器
/**
* 监听器类:主要任务是用ServletRequest将我们的HttpSession携带过去
*/
@Component
public class RequestListener implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
//将所有request请求都携带上httpSession
((HttpServletRequest) sre.getServletRequest()).getSession();
}
public RequestListener() {}
@Override
public void requestDestroyed(ServletRequestEvent arg0) {}
}
(四)chatMessage实体类
public class ChatMessage {
//文本
public static final int MESSAGE_TYPE_TEXT = 0;
//图片
public static final int MESSAGE_TYPE_IMAGE = 1;
//信息id(自增)
private int messageId;
//关系表id
private String linkId;
//发送者
private String fromUser;
//接收者
private String toUser;
//内容
private String content;
//发送时间
private Date sendTime;
//消息类型 0--普通文本(默认)
private int type = MESSAGE_TYPE_TEXT;
//是否为最后一条
private Boolean isLatest;
public ChatMessage() {
}
public ChatMessage(String linkId, String fromUser, String toUser, String content, Date sendTime, Boolean isLatest) {
this.linkId = linkId;
this.fromUser = fromUser;
this.toUser = toUser;
this.content = content;
this.sendTime = sendTime;
this.isLatest = isLatest;
}
public int getMessageId() {
return messageId;
}
public void setMessageId(int messageId) {
this.messageId = messageId;
}
public String getLinkId() {
return linkId;
}
public void setLinkId(String linkId) {
this.linkId = linkId;
}
public String getFromUser() {
return fromUser;
}
public void setFromUser(String fromUser) {
this.fromUser = fromUser;
}
public String getToUser() {
return toUser;
}
public void setToUser(String toUser) {
this.toUser = toUser;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Date getSendTime() {
return sendTime;
}
public void setSendTime(Date sendTime) {
this.sendTime = sendTime;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public Boolean getLatest() {
return isLatest;
}
public void setLatest(Boolean latest) {
isLatest = latest;
}
}
(五)websocket实现类
@ServerEndpoint(value = "/websocket",configurator= WebSocketConfig.class)
@Component
public class MyWebSocket {
//用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet<MyWebSocket> webSocketSet = new CopyOnWriteArraySet<MyWebSocket>();
//用来记录username和该session进行绑定
private static Map<String, Session> map = new HashMap<String, Session>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
//用户名
private String username;
//获取全局容器
private ApplicationContext applicationContext;
//聊天逻辑层service
private ChatService chatService;
/**
* 连接建立成功调用的方法,初始化昵称、session
*/
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
//获取登录时存放httpSession的用户数据
HttpSession httpSession= (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
WebApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(httpSession.getServletContext());
User user = (User) httpSession.getAttribute("user");
this.applicationContext = applicationContext;
this.session = session;
this.username = user.getUsername();
this.chatService = (ChatService) applicationContext.getBean("chatService");
//绑定username与session
map.put(username, session);
webSocketSet.add(this); //加入set中
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
//断开连接,重置窗口值
chatService.resetWindows(username);
webSocketSet.remove(this); //从set中删除
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message) {
//从客户端传过来的数据是json数据,所以这里使用jackson进行转换为chatMsg对象,
ObjectMapper objectMapper = new ObjectMapper();
ChatMessage chatMsg;
try {
chatMsg = objectMapper.readValue(message, ChatMessage.class);
//对chatMsg进行装箱
chatMsg.setFromUser(username);
chatMsg.setSendTime(new Date());
chatMsg.setLatest(true);
Session fromSession = map.get(chatMsg.getFromUser());
Session toSession = map.get(chatMsg.getToUser());
//发送给接收者.
fromSession.getAsyncRemote().sendText(username + ":" + chatMsg.getContent());
if (toSession != null) {
//发送给发送者.
toSession.getAsyncRemote().sendText(username + ":" + chatMsg.getContent());
}
//判断两者是否第一次聊天,进行关系表、聊天列表、空白信息的初始化
chatService.isFirstChat(chatMsg.getFromUser(), chatMsg.getToUser());
//查询聊天两者的联系id
String linkId = chatService.selectAssociation(username, chatMsg.getToUser());
chatMsg.setLinkId(linkId);
//保存聊天记录信息
chatService.saveMessage(chatMsg);
} catch (JsonParseException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 发生错误时调用
*/
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
/**
* 群发自定义消息
*/
public void broadcast(String message) {
for (MyWebSocket item : webSocketSet) {
//异步发送消息.
item.session.getAsyncRemote().sendText(message);
}
}
}
在websocket实现类中,你需要修改的是实际开发中的聊天服务层的接口。
相对应注解的方法,执行对应的操作业务。
(六)chatService业务层接口
public interface ChatService {
/**
* 查询聊天双方的关联id
* @param fromUser
* @param toUser
* @return
*/
String selectAssociation(String fromUser, String toUser);
/**
* 是否第一次聊天
* @param fromUser
* @param toUser
* @return
*/
void isFirstChat(String fromUser, String toUser);
/**
* 保存聊天记录
* @param chatMessage
* @return
*/
void saveMessage(ChatMessage chatMessage);
/**
* 获取当前用户的聊天列表
* @param fromUser
* @return
*/
ResultInfo getFromUserChatList(String fromUser);
/**
* 获取发送者与接收者的最近的聊天记录
* @param fromUser
* @param toUser
* @param currentIndex
* @return
*/
ResultInfo getRecentChatRecords(String fromUser, String toUser, int currentIndex);
/**
* 更新是否在同一窗口值
* @param fromUser
* @param toUser
*/
void updateWindows(String fromUser, String toUser);
/**
* 获取当前用户的未读数
* @param username
* @return
*/
ResultInfo getUnreadTotalNumber(String username);
/**
*
* @param username
*/
void resetWindows(String username);
}
没有贴出全部的代码,包括chatCtroller、业务层接口的实现以及mapper的sql语句。
七、开发中遇到的bug
-
websocket的实现类中,获取的httpsession的值是null,会报空指针异常。
解决方案:配置类中,获取httpsession;配置监听器,携带httsession。 -
websocket的实现类中,业务层的chatService会注入失败,也是null。解决方案:
通过httpsession获取service 方式完成对数据库的操作。
这次的开发分享就到此结束了,希望对你有所帮助!
更多推荐
所有评论(0)