网页聊天程序
文章目录
首先了解Mapper
在MyBatis中,Mapper用于定义与数据库交互的方法。这些方法通常用于执行CRUD(创建、读取、更新、删除)操作和其他与数据库相关的任务。
在MyBatis中,Mapper接口使用Java定义,其中包含与您要对数据库执行的SQL语句或查询相对应的方法签名。这些方法可以使用MyBatis的基于XML或注解的配置来将SQL语句映射到方法调用。
Mapper的主要目的是以一种清晰和有组织的方式定义应用程序中的数据库操作。它有助于将数据库访问逻辑与应用程序的其他业务逻辑分离,从而更容易管理和维护代码库。通过使用Mapper,您可以以声明性和类型安全的方式编写与数据库相关的代码,这是使用MyBatis作为对象-关系映射(ORM)框架的主要优势之一。
websocket的步骤
建立连接请求、服务器响应握手请求、WebSocket 连接建立、数据交换、保持连接以及连接关闭
websocket的特点
用户管理模块
实现会话列表和好友列表的切换
function initSwitchTab() {
// 1. 先获取到相关的元素(标签页的按钮, 会话列表, 好友列表)
let tabSession = document.querySelector('.tab .tab-session');
let tabFriend = document.querySelector('.tab .tab-friend');
// querySelectorAll 可以同时选中多个元素. 得到的结果是个数组
// [0] 就是会话列表
// [1] 就是好友列表
let lists = document.querySelectorAll('.list');
// 2. 针对标签页按钮, 注册点击事件.
// 如果是点击 会话标签按钮, 就把会话标签按钮的背景图片进行设置.
// 同时把会话列表显示出来, 把好友列表隐藏
// 如果是点击 好友标签按钮, 就把好友标签按钮的背景图片进行设置.
// 同时把好友列表显示出来, 把会话列表进行隐藏
tabSession.onclick = function() {
// a) 设置图标
tabSession.style.backgroundImage = 'url(img/对话.png)';
tabFriend.style.backgroundImage = 'url(img/用户2.png)';
// b) 让会话列表显示出来, 让好友列表进行隐藏
lists[0].classList = 'list';
lists[1].classList = 'list hide';
}
tabFriend.onclick = function() {
// a) 设置图标
tabSession.style.backgroundImage = 'url(img/对话2.png)';
tabFriend.style.backgroundImage = 'url(img/用户.png)'
// b) 让好友列表显示, 让会话列表隐藏
lists[0].classList = 'list hide';
lists[1].classList = 'list';
}
}
initSwitchTab();
实现注册登录
// 注册
int insertUser(User user);
// 根据用户名查询登录信息
User selectUserByUserName(String username);
user表中的字段有username userId password
在UserMapper.xml里实现对于数据库中的查找操作
<insert id="insertUser" useGeneratedKeys="true" keyProperty="userId">
insert into user values(null,#{username},#{password})
</insert>
1、注意的是主键设置
2、这是一个注册操作,在插入数据库时,要保证传入的#{username} #{password}
是对应到数据库中的字段,当然也可以取别名,像是下面的从数据库中根据用户名查询登陆信息来看
<select id="selectUserByUserName" resultType="com.example.demo.entity.User">
select* from user where username = #{username}
</select>
在通过UserMapper和数据库进行连接查询操作时,是可以对数据库中要查询的#{username} 和#{password} 进行一个取别名的操作的,然后方便的是,在进行UserController里面,可以使用取的别名来作为参数,也可以使用User类中的属性名作为参数(如果参数是myUsername也可以使用)
// 登录
@RequestMapping("/login")
public Object login(String username, String password, HttpServletRequest request){
// 先判断这个用户是否存在
User user = userMapper.selectUserByUserName(username);
if(user==null || !user.getPassword().equals(password)){
System.out.println("用户名或密码 错误");
return new User();
}
// 用户存在
HttpSession session = request.getSession(true);
if(session==null){
System.out.println("session 创建失败");
return new User();
}
session.setAttribute("user",user);
user.setPassword("");
return user;
}
@RequestMapping("/register")
public Object register(String username,String password){
User user =null;
try {
user = new User();
user.setUsername(username);
user.setPassword(password);
userMapper.insertUser(user);
user.setPassword("");
return user;
} catch (DuplicateKeyException e) {
e.printStackTrace();
}
return new User();
}
利用的是DuplicateKeyException 这个异常,来进行防止用户名重复的注册
实现获取用户登录信息
先设定一下请求和响应
请求:请求类型是get
路径是 userinfo
响应:body里面应该有
当前登录用户的userId
username(用户名)
密码可能也会返回,但是此时的密码要设置为空,否则在客户端抓包的时候就能抓到了
UserController里面实现的
@RequestMapping("/userinfo")
public Object getuserinfo(HttpServletRequest request){
// 先获取session
HttpSession session = request.getSession(false);
if(session==null){
System.out.println("getuserinfo 失败 session==null");
return new User();
}
User user =(User) session.getAttribute("user");
if(user==null){
System.out.println("getuserinfo 失败 user==null");
return new User();
}
// 返回user
user.setPassword("");
return user;
}
客户端代码
1、先判断一下,返回响应中的body里的 userId是不是一个空值
2、定位到需要设置的username的地方,代码里面叫做userDiv
3、把userDiv的值给设置为body里面的username
4、把当前登录用户的userId 给设置到userDiv 的属性中
5、如果用户没有登录的话,就跳转到登录页面
// 获取到当前登录用户
function getuserinfo(){
$.ajax({
type:'get',
url:'userinfo',
success:function(body){
if(body&&body.userId>0){
let userDiv = document.querySelector('.main .left .user');
userDiv.innerHTML = body.username;
userDiv.setAttribute("user-id",body.userId);
}else{
alert("当前用户未登录");
location.assign('/login.html');
}
}
});
}
getuserinfo();
好友管理模块
实现获取好友列表
先进行friend表的设计
好友是多对多的,所以一个用户可以对应很多个好友
所以friend表设计如上
进行前后端交互的设计
定义Friend类
public class Friend {
private int friendId;
private String friendName;
}
Mapper中定义和数据库交互的方法
@Mapper
public interface FriendMapper {
// 根据当前登录的信息,找到好友列表
List<Friend> getFriendListByUserId(int userId);
}
数据库操作
● 这里因为在friend表中,只有两个字段,friendId和userId 所以在 查询的时候,要从friend表中查询到和当前登录信息相同userId的 的friend,体现在friendId上面
● 就是要查询到与userId对应的friendId,此处的friendId就相当于user表中的userId 根据在user表中查到的userId可以在 user表中获取到friend的friendName,后面可以使用
<select id="getFriendListByUserId" resultType="com.example.demo.entity.Friend">
select userId as friendId,username as friendName from user
where userId in
(select friendId from friend where userId = #{userId})
</select>
获取到好友列表
● 因为好友有很多,是一个列表,返回时候采用list进行返回
● 先从请求中的session中获取到当前登录的用户,如果获取不到session或者获取不到用户就返回空的列表
● 通过从user中获取到的userId来进行查找好友
@RestController
@ResponseBody
public class FriendController {
@Autowired
private FriendMapper friendMapper;
// 根据userId来获取到好友列表
@RequestMapping("/friendlist")
public Object getFriendList(HttpServletRequest request){
HttpSession session = request.getSession(false);
if(session==null){
System.out.println("getFriendList failed");
return new ArrayList<Friend>();
}
User user =(User) session.getAttribute("user");
if(user==null){
System.out.println("getFriendList fail");
return new ArrayList<Friend>();
}
List<Friend> friendList = friendMapper.getFriendListByUserId(user.getUserId());
return friendList;
}
}
客户端实现
1、先选中好友列表,把好友列表刷新清空
2、从返回的响应中读取到每个对象中的内容
3、 把响应中读取到的friendId设置到li的属性中,以备后用
4、把当前的标签添加到好友列表中
// 获取到当前登录用户的好友列表
function getFriendList(){
$.ajax({
// 根据约定的格式来进行请求和响应
type:'get',
url:'friendlist',
success:function(body){
// 1、先选中好友列表,把好友列表刷新清空
let friendlistUL= document.querySelector('#friend-list');
friendlistUL.innerHTML="";
// 每一个friend都是一个对象,因为返回的是一个Friend列表,列表中是一个一个的对象
for( let friend of body){
// 2、从返回的响应中读取到每个对象中的内容
// 把每个对象设置为一个li标签
// 把li标签中的内容设置到界面上
let li = document.createElement('li');
li.innerHTML = '<h4>'+friend.friendName+'</h4>';
// 把响应中读取到的friendId设置到li的属性中,以备后用
li.setAttribute("friend-id",friend.friendId);
// 4、把当前的标签添加到好友列表中
friendlistUL.appendChild(li);
li.onclick =function(){
clickFriend(friend);
}
}
}
});
}
getFriendList();
会话管理模块
★★★要实现的功能
1、点击会话,变为高亮状态
2、点击好友列表中的好友,如果会话存在,置顶会话,并设置为高亮状态
会话不存在,就创建会话,置顶会话,并设置为高亮状态
实现获取会话列表
前后端交互的设计:
实现数据库设计:
在message_session 数据库中,有sessionId和lastTime
在message_session_user 数据库中,有sessionId和userId
创建MessageSession
考虑到在客户端界面上要显示
好友名称 最近一条消息 如图显示
public class MessageSession {
private int sessionId;
private List<Friend> friends; //表示一个好友
private String lastMessage; //表示最近一条消息
}
MessageSessionItem类来表示数据库中的一条记录
@Data
public class MessageSessionItem {
private int sessionId;
private int userId;
}
在Mapper中定义与数据库交互的方法
● 首先要通过当前登录的用户id 来找到userId所在的所有sessionId
● 再根据这些sessionId来找到所有会话列表
// 根据userId来查询到所有的sessionId
List<Integer> getSessionIdByUserId(int userId);
// 根据sessionId查询到所有的会话列表
List<Friend> getFriendsBySessionId(int sessionId,int selfUserId);
1、getSessionIdByUserId中虽然在message_session_user中可以找到对应的sessionId,但是我们想让会话列表按照时间的顺序进行排序,只有message_session表中才有时间,所以根据在message_session_user表中查找到的sessionId 又在message_session表中查找结果
2、getFriendsBySessionId 中 message_session_user 中有sessionId 和 userId两个字段,但是没有对应的名称和friendId,所以在message_session_user中查找到对应的userId 又在user表中查找对应的userId,得到的是username和userId,重命名之后进行返回
<select id="getSessionIdByUserId" resultType="java.lang.Integer">
select sessionId from message_session
where sessionId in
(select sessionId from message_session_user where userId = #{userId})
order by lastTime desc
</select>
<select id="getFriendsBySessionId" resultType="com.example.demo.entity.Friend">
select userId as friendId ,username as friendName from user
where userId in
(select userId from message_session_user where sessionId =#{sessionId} and userId !=#{selfUserId})
</select>
1、先从请求的session中获取到session 根据session中获取到user
2、messageSessionMapper来调用查找方法,找到登录的userId中所有的sessionId
3、遍历这个列表,列表中的每个元素都是一个sessionId
4、根据每个sessionId可以查出对应的抛除登录用户的好友名称
5、这是消息传输模块需要完成的,获取历史消息
@RequestMapping("/sessionlist")
public Object getSessionList(HttpServletRequest request){
List<MessageSession> messageSessionList = new ArrayList<MessageSession>();
// 先获取到userId 从userId 中获取到 所有的sessionId
HttpSession session = request.getSession(false);
if(session ==null){
System.out.println("getsessionlist failed session ==null");
return messageSessionList;
}
User user = (User) session.getAttribute("user");
if(user==null){
System.out.println("getsessionlist failed user==null");
return messageSessionList;
}
// 获取到所有的sessionId
List<Integer> sessionlist = messageSessionMapper.getSessionIdByUserId(user.getUserId());
for (int sessionId: sessionlist) {
MessageSession messageSession = new MessageSession();
messageSession.setSessionId(sessionId);
List<Friend> friends = messageSessionMapper.
getFriendsBySessionId(sessionId, user.getUserId());
messageSession.setFriends(friends);
// 这后面对内容是在消息传输模块实现的
String lastMessage = messageMapper.getMessageLastMessage(sessionId);
if(lastMessage==null){
messageSession.setLastMessage("");
}else {
messageSession.setLastMessage(lastMessage);
}
messageSessionList.add(messageSession);
}
return messageSessionList;
}
客户端
1、选中会话列表,进行清空操作
2、创建li标签,里面对应的是会话中好友的名称和消息
3、把响应中的sessionId 添加到当前的li标签的属性中
4、把会话添加到会话列表中
// 获取到会话列表
// 并设置点击高亮
function getSessionList(){
$.ajax({
type:'get',
url:'sessionlist',
success:function(body){
// 1、选中会话列表,进行清空操作
let sessionListUL = document.querySelector("#session-list");
sessionListUL.innerHTML = "";
for(let session of body){
// 针对 lastMessage 的长度进行截断处理
if (session.lastMessage.length > 10) {
session.lastMessage = session.lastMessage.substring(0, 10) + '...';
}
// 2、创建li标签,里面对应的是会话中好友的名称和消息
let li = document.createElement('li');
// 3、把响应中的sessionId 添加到当前的li标签的属性中
li.setAttribute("message-session-id",session.sessionId);
li.innerHTML ='<h3>'+session.friends[0].friendName+'<h3>'+
'<p>'+session.lastMessage+'</p>';
// 4、把会话添加到会话列表中
sessionListUL.appendChild(li);
// 注册一个点击事件,点击以后会高亮
li.onclick = function(){
clickSession(li);
}
}
}
});
}
getSessionList();
添加点击事件
// 设置高亮
function clickSession(currentLi){
// 设置高亮
let allLis = document.querySelectorAll('#session-list>li');
activeSession(allLis, currentLi);
// 获取历史消息
let sessionId = currentLi.getAttribute("message-session-id");
getHistoryMessage(sessionId);
}
function activeSession(allLis, currentLi) {
// 这里的循环遍历, 更主要的目的是取消未被选中的 li 标签的 className
for (let li of allLis) {
if (li == currentLi) {
li.className = 'selected';
} else {
li.className = '';
}
}
}
2、点击好友列表中的好友,如果会话存在,置顶会话,并设置为高亮状态
会话不存在,就创建会话,置顶会话,并设置为高亮状态
客户端
1、点击某个好友,先在findSessionByName 中查询friend对应的会话是否存在
这是因为在前面的获取friendlist里面注册了一个friend的点击事件
2、在findSessionByName内,选择session-list中的所有的li标签,如果session中的username和点击的好友的名字相同,就返回这个会话,表示这个会话存在,否则就返回空
3、接下来,根据sessionLi是否为空来进入条件语句,不为空直接把这个sessionLi添加到好友列表的第一位,并且置为选中状态
4、如果不存在,就要创建新的会话,再把friend的名称给设置到会话列表的会话中,再把这条会话给设置到会话列表的第一个位置,然后状态是被选中状态,因为新添加的会话没有注册点击事件,所以要先添加点击事件
5、之后就是进行往数据库中添加这条会话,是creatSession
6、创建会话需要进行前后端交互的设计
因为在前面的friendList列表中,定义了一个店家clickFriend的操作
function clickFriend(friend){
console.log('点击好友:', friend);
// 先要在所有的列表中查询friend对应的会话是否存在
let sessionLi = findSessionByName(friend.friendName);
let sessionListUL = document.querySelector('#session-list');
if(sessionLi){
// 存在就给这个好友模拟一个点击事件
sessionListUL.insertBefore(sessionLi,sessionListUL.children[0]);
sessionLi.click();
}else{
// 不存在就创建新的会话 并添加模拟点击事件,再模拟
sessionLi =document.createElement('li');
sessionLi.innerHTML = '<h3>'+friend.friendName+'</h3>'+'<p></p>';
sessionListUL.insertBefore(sessionLi,sessionListUL.children[0]);
sessionLi.onclick= function(){
clickSession(sessionLi);
}
sessionLi.click();
// 告知服务器创建新的会话
createSession(friend.friendId,sessionLi);
}
// 建立一个标签页的跳转
let tabSession = document.querySelector('.tab .tab-session');
tabSession.click();
}
function findSessionByName(username){
let sessionListUL = document.querySelectorAll('#session-list>li');
for(let sessionLi of sessionListUL){
let h3 = sessionLi.querySelector('h3');
if(h3.innerHTML==username){
return sessionLi;
}
}
return null;
}
实现创建会话
前后端交互设计
function createSession(friendId,sessionLi){
$.ajax({
type:'post',
url:'session?toUserId='+friendId,
success:function(body){
console.log("创建会话成功! sessionId = "+body.sessionId);
sessionLi.setAttribute("message-session-id",body.sessionId);
},error:function(body){
console.log('会话创建失败!');
}
});
}
服务器
@RequestMapping("/session")
public Object addMessageSession(int toUserId,HttpServletRequest request){
HashMap<String,Integer> resp = new HashMap<>();
HttpSession session = request.getSession(false);
if(session==null){
System.out.println("session session==null");
return new MessageSession();
}
//往message_session 中插入一条会话 获取该会话的sessionId
MessageSession messageSession = new MessageSession();
messageSessionMapper.addMessageSession(messageSession);
User user = (User) session.getAttribute("user");
if(user==null){
return new MessageSession();
}
MessageSessionItem messageSessionItem = new MessageSessionItem();
messageSessionItem.setUserId(user.getUserId());
messageSessionItem.setSessionId(messageSession.getSessionId());
messageSessionMapper.addMessageSessionUser(messageSessionItem);
MessageSessionItem messageSessionItem1 = new MessageSessionItem();
messageSessionItem1.setUserId(toUserId);
messageSessionItem1.setSessionId(messageSession.getSessionId());
messageSessionMapper.addMessageSessionUser(messageSessionItem1);
System.out.println("[addMessageSession] 新增会话成功! sessionId=" +
messageSession.getSessionId()
+ " userId1=" + user.getUserId() + " userId2=" + toUserId);
resp.put("sessionId", messageSession.getSessionId());
// 返回的对象是一个普通对象也可以, 或者是一个 Map 也可以, jackson 都能进行处理.
return resp;
}
MessageSessionMapper:
// 插入 message_session表中
int addMessageSession(MessageSession messageSession);
// 往message_session_user 表中插入
void addMessageSessionUser(MessageSessionItem messageSessionItem);
MessageSessionMapper.xml
<insert id="addMessageSession" useGeneratedKeys="true" keyProperty="sessionId">
insert into message_session values(null,now())
</insert>
<insert id="addMessageSessionUser">
insert into message_session_user values(#{sessionId},#{userId})
</insert>
消息传输模块
实现的功能:
1、获取到历史消息,历史消息的内容不能超过100条,并且按照会话创建的时间进行排序
会话过多的时候,要自动滚动到最下方
先设计message表
现在就是构造消息,往message表中去添加一些消息
我们想要获取到历史最近的一条消息,放到会话列表中进行显示
实现获取到指定会话最近一条消息
MessageMapper中:
//获取 指定会话的最后一条消息
String getMessageLastMessage(int sessionId);
MessageMapper.xml中
直接从message表中根据sessionId来获取消息 但是要求是按照时间顺序排列,并且只能查找到一条记录
<select id="getMessageLastMessage" resultType="java.lang.String">
<!-- 获取到message 的最后一条消息-->
select content from message where sessionId = #{sessionId}
order by postTime desc limit 1
</select>
获取sessionList时,我们没有获取到message中的最后一条消息
所以,找到sessionlist这一方法,还有注意事项,就是会话刚刚创建的时候,可能会没有消息,所以在会话列表中显示的是空白的,于是就有了 lastMessage是否为空的判断
String lastMessage = messageMapper.getMessageLastMessage(sessionId);
if(lastMessage==null){
messageSession.setLastMessage("");
}else {
messageSession.setLastMessage(lastMessage);
}
messageSessionList.add(messageSession);
实现获取到会话历史消息
在之前的clickSession中,我们想要做到点击会话,设置为高亮选中状态,并且在右侧的消息主界面上面显示到所有的历史消息
设计前后端交互:
首先根据message的设计,当前发送者的fromName不知道是谁,名字在user表中有,
所以要把user表和message表进行一个联合查询,使用fromId和userId作为连接条件
MessageMapper.xml
<select id="getMessagesBySessionId" resultType="com.example.demo.entity.Message">
select messageId,fromId,username as fromName ,sessionId ,content
from user,message
where fromId = userId and message.sessionId =#{sessionId}
order by postTime desc limit 100
</select>
MessageMapper
// 获取指定会话的历史消息列表
// 只取默认的100条消息
List<Message> getMessagesBySessionId(int sessionId);
MessageController
因为查询出来的messages是按照时间顺序倒序排列的,所以将它逆置一下
@RequestMapping("/message")
public Object getMessage(int sessionId){
List<Message> messages = messageMapper.getMessagesBySessionId(sessionId);
Collections.reverse(messages);
return messages;
}
客户端
function getHistoryMessage(sessionId){
console.log("获取历史消息 sessionId = "+sessionId);
let titleDiv = document.querySelector('.right .title');
titleDiv.innerHTML="";
let messageShowDiv = document.querySelector('.right .message-show');
messageShowDiv.innerHTML="";
// 2、重新设置会话标题
// 先明确当前选中的是谁
let selectedH3 = document.querySelector('#session-list .selected>h3');
if(selectedH3){
titleDiv.innerHTML = selectedH3.innerHTML;
}
$.ajax({
type:'get',
url:'message?sessionId='+sessionId,
success:function(body){
// 先进行清空操作
for(let message of body){
addMessage(messageShowDiv,message);
}
scrollBottom(messageShowDiv);
}
});
}
// 把 messageShowDiv 里的内容滚动到底部.
function scrollBottom(elem) {
// 1. 获取到可视区域的高度
let clientHeight = elem.offsetHeight;
// 2. 获取到内容的总高度
let scrollHeight = elem.scrollHeight;
// 3. 进行滚动操作, 第一个参数是水平方向滚动的尺寸. 第二个参数是垂直方向滚动的尺寸
elem.scrollTo(0, scrollHeight - clientHeight);
}
function addMessage(messageShowDiv,message){
let messageDiv = document.createElement('div');
// 根据信息是不是自己发的来决定靠左还是靠右
let selfUsername = document.querySelector('.left .user').innerHTML;
if(selfUsername == message.fromName){
// 消息是自己发的 靠右
messageDiv.className = 'message message-right';
}else{
// 消息不是自己发的 靠左
messageDiv.className = 'message message-left';
}
messageDiv.innerHTML ='<div class="box">'
+'<h4>'+message.fromName+'</h4>'
+'<p>'+message.content+'</p>'
+'</div>';
messageShowDiv.appendChild(messageDiv);
}
websocket实现消息转发
WebSocket是一种在单个TCP连接上进行全双工通信的协议,它允许浏览器与服务器之间进行实时的双向数据传输。与传统的HTTP请求-响应模式不同,WebSocket允许服务器主动向客户端推送数据,而不需要客户端发起请求。
特别的一点是,websocket不同于HTTP协议
在进行消息传输时,a发给b的消息,步骤如下
- 用户a通过自己的WebSocket连接发送消息给服务器。
- 服务器收到a发送的消息,并识别出目标用户是b。
- 服务器找到与用户b关联的WebSocket连接。
- 服务器将a的消息直接发送给用户b的WebSocket连接。
- 用户b的WebSocket连接接收到服务器发送的消息。
在进行客户端和服务器通信时,如下图
请求和响应也略有不同
1、先实现客户端发送消息
a) ● 先获取到发送按钮和输入框
● 给发送按钮注册一个点击事件,当输入框中的值不存在时
● 获取到当前选中标签的sessionId(在之前创建会话的时候已经保存了),
也有可能当前标签没有被选中(刚登录的时候)
● 构造req的请求,当前的req是一个js对象,但是websocket中发送的应该是一个JSON格式的字符串,所以 用JSON.stringify来把js对象转换成json格式的字符串
● 通过websocket来发送这个json字符串
●把输入框中的内容置为空
// ///
//实现消息发送/接收逻辑
// ///
function initSendButton(){
// 1.获取到发送按钮 和 消息输入框
let senButton = document.querySelector('.right .ctrl button');
let messageInput = document.querySelector('.right .message-input');
// 2.给发送按钮注册一个点击事件
senButton.onclick = function(){
if(!messageInput.value){
return;
}
// 获取到当前选中的li标签的sessionId
let selectedLi = document.querySelector('#session-list .selected');
if(selectedLi==null){
// 当前没有标签被选中
return;
}
let sessionId = selectedLi.getAttribute("message-session-id");
// 构造一个json数据
let req = {
type:'message',
sessionId:sessionId,
content:messageInput.value
};
req = JSON.stringify(req);
websocket.send(req);
messageInput.value='';
}
}
initSendButton();
2、实现服务器接收/转发消息
- 首先,该类使用了 @Configuration 注解,表示它是一个 Spring 的配置类,会被 Spring 容器扫描并加载。
- @EnableWebSocket 注解用于启用 WebSocket 功能。
- WebSocketConfigurer 接口是 Spring 提供的用于配置 WebSocket 的接口,通过实现该接口,可以注册 WebSocket 处理器和配置 WebSocket 相关信息。
- 在这个配置类中,注入了两个 WebSocket 处理器:TestWebSocketController 和 WebSocketController。
- 在 registerWebSocketHandlers 方法中,通过 WebSocketHandlerRegistry 对象注册了 WebSocketController 作为处理器,同时指定了 WebSocket 的请求路径为 “/WebSocketMessage”。
- 使用 addInterceptors 方法注册了 HttpSessionHandshakeInterceptor 拦截器,这个拦截器的作用是将用户的 HttpSession 中添加的 Attribute 键值对也添加到 WebSocketSession 中。这样在 WebSocketController 中的 afterConnectionEstablished 方法中可以通过 session.getAttributes().get(“user”) 来获取到用户的信息,包括 HttpSession 中的信息
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private TestWebSocketController testWebSocketController;
@Autowired
private WebSocketController webSocketController;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 通过这个方法, 把刚才创建好的 Handler 类给注册到具体的 路径上.
// 此时当浏览器, websocket 的请求路径是 "/test" 的时候, 就会调用到 TestWebSocketAPI 这个类里的方法.
//registry.addHandler(testWebSocketController, "/test");
registry.addHandler(webSocketController, "/WebSocketMessage")
// // 通过注册这个特定的 HttpSession 拦截器, 就可以把用户给
//HttpSession 中添加的 Attribute 键值对
// // 往我们的 WebSocketSession 里也添加一份.
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
}
通过这个类来更好得对websocketsession和userId的映射关系进行维护
创建类的方式能够提高代码的复用性
package com.example.demo.component;
import com.example.demo.config.WebSocketConfig;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import java.beans.IntrospectionException;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
/**
* Created with IntelliJ IDEA.
* Description:通过这个类来更好得对websocketsession和userId的映射关系进行维护
* 创建类的方式能够提高代码的复用性,其实也是可以不创建这个类也能完成上下线的操作的
* User: Wangduan
* Date: 2023-07-21
* Time: 22:58
*/
@Component
public class OnlineUserManager {
// 使用hash表进行保存
private ConcurrentHashMap<Integer, WebSocketSession> sessions = new ConcurrentHashMap<>();
// 1)用户上线 给这个哈希表中插入键值对
public void online(int userId, WebSocketSession session) {
if (sessions.get(userId) != null) {
// 此时说明用户已经在线了, 就登录失败, 不会记录这个映射关系.
// 不记录这个映射关系, 后续就收不到任何消息 (毕竟, 咱们此处是通过映射关系来实现消息转发的)
System.out.println("[" + userId + "] 已经被登录了, 登录失败!");
return;
}
sessions.put(userId, session);
System.out.println("[" + userId + "] 上线!");
}
// 2)用户下线,针对这个哈希表进行删除元素
public void offline(int userId, WebSocketSession session) {
WebSocketSession existSession = sessions.get(userId);
if (existSession == session) {
// 如果这俩 session 是同一个, 才真正进行下线操作. 否则就啥都不干
sessions.remove((userId));
System.out.println("[" + userId + "] 下线!");
}
}
// 3)根据userId 获取到 WebSocketSession
public WebSocketSession getSession(int userId) {
return sessions.get(userId);
}
}
● 当客户端连接到服务器的时候,因为之前在http的session.setAttribute(“user”)中存储了,所以可以拿到userId 就会从httpsession中获取到 userId
然后把这个userId用onlineUserManager.online(user.getUserId(),session)添加到websocketsession中去
●当一个客户端请求消息发过来后,因为是一个json格式的字符串,所以通过jackson工具把这个字符串转换为java对象
如果载荷中的类型是message 的话就要进行下面的转发
●然后把这个请求给重新构造,准备发送给别的客户端,因为构造出来的是一个响应的对象,所以通过jackson库工具进行了转换,转成了json字符串
●然后要根据这个会话的会话id来获取到这个会话中的所有的用户,因为在req中已经知道了这个会话的sessionId,但是此时前面的实现中获取到好友列表是不能获取到自己的,所以就要把自己添加到好友列表中,
●循环获取到的好友列表,进行消息的发送,通过webSocketSession.sendMessage 中把respJson构造成一个文本对象,发送给其他客户端
package com.example.demo.component;
import com.example.demo.entity.*;
import com.example.demo.mapper.MessageMapper;
import com.example.demo.mapper.MessageSessionMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.List;
@Component
public class WebSocketController extends TextWebSocketHandler {
@Autowired
private OnlineUserManager onlineUserManager;
@Autowired
private MessageMapper messageMapper;
@Autowired
private MessageSessionMapper messageSessionMapper;
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("[websocket] 连接成功");
User user = (User) session.getAttributes().get("user");
if(user==null){
return;
}
onlineUserManager.online(user.getUserId(),session);
System.out.println("获取到的userId "+ user.getUserId());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
System.out.println("[websocket] 收到消息" +message.toString());
// 1。先获取到当前用户的信息
User user =(User) session.getAttributes().get("user");
if(user==null){
System.out.println("[websocket] user ==null 未登录用户,无法进行转发" );
return;
}
// 2.针对请求进行解析 把json格式字符串转换成java中的对象
MessageRequest req = objectMapper.readValue(message.getPayload(),MessageRequest.class);
if(req.getType().equals("message")){
// 就进行消息转发
transferMessage(user,req);
}else{
System.out.println("websocket req.type 有误"+message.getPayload());
}
}
// 通过这个方法来完成消息的转发操作
private void transferMessage(User fromUser, MessageRequest req) throws IOException {
// 1。先构造一个待转发的响应对象
MessageResponse resp = new MessageResponse();
resp.setType("message");
resp.setFromId(fromUser.getUserId());
resp.setFromName(fromUser.getUsername());
resp.setSessionId(req.getSessionId());
resp.setContent(req.getContent());
// 把这个java对象转换为json格式字符串
String respJson = objectMapper.writeValueAsString(resp);
System.out.println("transferMessage "+ respJson);
// 2。根据这个sessionId 来获取到这个MessageSession 里都有哪些用户,通过查询数据库就知道了
List<Friend> friends = messageSessionMapper.
getFriendsBySessionId(req.getSessionId(), fromUser.getUserId());
// 这里有一个群聊的方式 也要把自己添加进去 下面是一个对自己添加的操作
Friend myself = new Friend();
myself.setFriendId(fromUser.getUserId());
myself.setFriendName(fromUser.getUsername());
friends.add(myself);
// 3. 循环上面的列表,给列表中的每个用户都发送一份响应消息
for (Friend friend:friends) {
// 知道了每个用户的userId 进一步的查到刚才准备好的OnlineUserManager
WebSocketSession webSocketSession = onlineUserManager.getSession(friend.getFriendId());
if(webSocketSession ==null){
// 如果当前用户为不在线,则不发送
continue;
}
webSocketSession.sendMessage(new TextMessage(respJson));
}
// 4. 把发送的消息构造到数据库中
Message message = new Message();
message.setFromId(fromUser.getUserId());
message.setContent(req.getContent());
message.setSessionId(req.getSessionId());
messageMapper.add(message);
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
System.out.println("[websocket] 连接异常"+exception.toString());
User user = (User)session.getAttributes().get("user");
if(user==null){
return;
}
onlineUserManager.offline(user.getUserId(),session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println("[websocket] 连接关闭"+status.toString());
User user = (User)session.getAttributes().get("user");
if(user==null){
return;
}
onlineUserManager.offline(user.getUserId(),session);
}
}
3、实现客户端接收消息
一、在右侧的消息列表中显示(必须保证当前是被选中状态才可以)
二、在左侧的会话预览区域要显示
1.如果会话存在,要把会话设置为置顶状态
2.如果会话不存在,要创建会话,把会话加入到会话列表中,设置为置顶状态
3.把新的消息设置到会话的预览区域,如果消息太长要进行截断
4.如果当前收到消息的会话处于被选中状态,则把当前的消息给放到右侧消息列表中
新增消息的同时,注意调整滚动条的位置,保证新消息虽然在底部,但是能被直接看到
// ///
// 操作websocket
// ///
let websocket = new WebSocket("ws://127.0.0.1:8080/WebSocketMessage");
websocket.onopen = function(){
console.log("websocket 连接成功!");
}
websocket.onclose = function(){
console.log("websocket 连接断开!");
}
websocket.onerror = function(){
console.log("websocket 连接异常!");
}
websocket.onmessage = function(e){
console.log("websocket 收到消息!"+e.data);
// 这里收到的消息是一个字符串,要转换为js对象
let resp = JSON.parse(e.data);
if(resp.type == 'message'){
handleMessage(resp);
}else{
console.log("resp.type 不符合要求");
}
}
function handleMessage(resp) {
// 把客户端收到的消息, 给展示出来.
// 展示到对应的会话预览区域, 以及右侧消息列表中.
// 1. 根据响应中的 sessionId 获取到当前会话对应的 li 标签.
// 如果 li 标签不存在, 则创建一个新的
let curSessionLi = findSessionLi(resp.sessionId);
if (curSessionLi == null) {
// 就需要创建出一个新的 li 标签, 表示新会话.
curSessionLi = document.createElement('li');
curSessionLi.setAttribute('message-session-id', resp.sessionId);
// 此处 p 标签内部应该放消息的预览内容. 一会后面统一完成, 这里先置空
curSessionLi.innerHTML = '<h3>' + resp.fromName + '</h3>'
+ '<p></p>';
// 给这个 li 标签也加上点击事件的处理
curSessionLi.onclick = function() {
clickSession(curSessionLi);
}
}
// 2. 把新的消息, 显示到会话的预览区域 (li 标签里的 p 标签中)
// 如果消息太长, 就需要进行截断.
let p = curSessionLi.querySelector('p');
p.innerHTML = resp.content;
if (p.innerHTML.length > 10) {
p.innerHTML = p.innerHTML.substring(0, 10) + '...';
}
// 3. 把收到消息的会话, 给放到会话列表最上面.
let sessionListUL = document.querySelector('#session-list');
sessionListUL.insertBefore(curSessionLi, sessionListUL.children[0]);
// 4. 如果当前收到消息的会话处于被选中状态, 则把当前的消息给放到右侧消息列表中.
// 新增消息的同时, 注意调整滚动条的位置, 保证新消息虽然在底部, 但是能够被用户直接看到.
if (curSessionLi.className == 'selected') {
// 把消息列表添加一个新消息.
let messageShowDiv = document.querySelector('.right .message-show');
addMessage(messageShowDiv, resp);
scrollBottom(messageShowDiv);
}
// 其他操作, 还可以在会话窗口上给个提示 (红色的数字, 有几条消息未读), 还可以播放个提示音.
// 这些操作都是纯前端的. 实现也不难, 不是咱们的重点工作. 暂时不做了.
}
function findSessionLi(targetSessionId) {
// 获取到所有的会话列表中的 li 标签
let sessionLis = document.querySelectorAll('#session-list li');
for (let li of sessionLis) {
let sessionId = li.getAttribute('message-session-id');
if (sessionId == targetSessionId) {
return li;
}
}
// 啥时候会触发这个操作, 就比如如果当前新的用户直接给当前用户发送消息, 此时没存在现成的 li 标签
return null;
}