一套简单的web即时通讯——第二版
前言
接上一版,这一版的页面与功能都有所优化,具体如下:
1、优化登录拦截
2、登录后获取所有好友并区分显示在线、离线好友,好友上线、下线都有标记
3、将前后端交互的值改成用户id、显示值改成昵称nickName
4、聊天消息存储,点击好友聊天,先追加聊天记录
5、登录后获取所有未读消息并以小圆点的形式展示
6、搜索好友、添加好友
优化细节
1、登录拦截由之前的通过路径中获取账号,判断WebSocketServer.loginList中是否存在key改成登录的时候设置cookie,登录拦截从cookie中取值
登录、登出的时候设置、删除cookie,
/**
* 登录
*/
@PostMapping("login")
public Result<ImsUserVo> login(ImsUserVo userVo, HttpServletResponse response) {
//加密后再去对比密文
userVo.setPassword(MD5Util.getMD5(userVo.getPassword()));
Result<List<ImsUserVo>> result = list(userVo);
if (result.isFlag() && result.getData().size() > 0) {
ImsUserVo imsUserVo = result.getData().get(0);
//置空隐私信息
imsUserVo.setPassword(null); //add WebSocketServer.loginList
WebSocketServer.loginList.put(imsUserVo.getUserName(), imsUserVo); //设置cookie
Cookie cookie = new Cookie("imsLoginToken", imsUserVo.getUserName());
cookie.setMaxAge(60 * 30);
//设置域
// cookie.setDomain("huanzi.cn");
//设置访问路径
cookie.setPath("/");
response.addCookie(cookie); return Result.of(imsUserVo);
} else {
return Result.of(null, false, "账号或密码错误!");
}
} /**
* 登出
*/
@RequestMapping("logout/{username}")
public ModelAndView loginOut(HttpServletResponse response, @PathVariable String username) {
new WebSocketServer().deleteUserByUsername(username,response);
return new ModelAndView("login.html");
}
ImsUserController.java
改成关闭websocket时不做操作,仅减减socket连接数
/**
* 连接关闭调用的方法
*/
@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;
}
}
deleteUserByUsername(logoutUserName,null);
} /**
用户下线
*/
public void deleteUserByUsername(String username, HttpServletResponse response){
//在线人数减减
WebSocketServer.onlineCount--;
if(WebSocketServer.onlineCount <= 0){
WebSocketServer.onlineCount = 0;
} if(StringUtils.isEmpty(response)){
return;
} //用户集合delete
WebSocketServer.loginList.remove(username); //删除cookie 思路就是替换原来的cookie,并设置它的生存时间为0
//设置cookie
Cookie cookie = new Cookie("imsLoginToken", username);
cookie.setMaxAge(0);
//设置域
// cookie.setDomain("huanzi.cn");
//设置访问路径
cookie.setPath("/");
response.addCookie(cookie); //通知除了自己之外的所有人
sendOnlineCount(username, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + username + "'}");
}
WebSocketServer.java
在登录拦截器中从cookie取用户账户
//其实存的是用户账号
String imsLoginToken = "";
Cookie[] cookies = request.getCookies();
if (null != cookies) {
for (Cookie cookie : cookies) {
if ("imsLoginToken".equals(cookie.getName())) {
imsLoginToken = cookie.getValue();
}
}
} if(WebSocketServer.loginList.containsKey(imsLoginToken)){
//正常处理请求
filterChain.doFilter(servletRequest, servletResponse);
}else{
//重定向登录页面
response.sendRedirect("/imsUser/loginPage.html");
}
LoginFilter.java
2、登录之后的用户列表不再是显示websocket连接的用户,而是登录用户的好友,同时要区分显示好友的在线与离线,所以新增一个获取在线好友的接口
/**
* 获取在线好友
*/
@PostMapping("getOnlineList")
private Result<List<ImsUserVo>> getOnlineList(ImsFriendVo imsFriendVo) {
return imsFriendService.getOnlineList(imsFriendVo);
} /**
* 获取在线好友
*/
@Override
public Result<List<ImsUserVo>> getOnlineList(ImsFriendVo imsFriendVo) {
//好友列表
List<ImsFriendVo> friendList = list(imsFriendVo).getData(); //在线好友列表
ArrayList<ImsUserVo> onlineFriendList = new ArrayList<>(); //遍历friendList
for(ImsFriendVo imsFriendVo1 : friendList){
ImsUserVo imsUserVo = imsFriendVo1.getUser();
if (!StringUtils.isEmpty(WebSocketServer.getSessionMap().get(imsUserVo.getId().toString()))) {
onlineFriendList.add(imsUserVo);
}
}
return Result.of(onlineFriendList);
}
ImsFriend
//连接成功建立的回调方法
websocket.onopen = function () {
//获取好友列表
// $.post(ctx + "/imsFriend/list",{userId: username},function (data) {
// console.log(data)
// });
$.ajax({
type: 'post',
url: ctx + "/imsFriend/list",
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
dataType: 'json',
data: {userId: user.id},
success: function (data) {
if (data.flag) {
//列表
let friends = data.data;
for (let i = 0; i < friends.length; i++) {
let friend = friends[i].user;
let $friendGroupList = $("<div class=\"hz-group-list\">" +
"<img class='left' style='width: 23px;' src='https://avatars3.githubusercontent.com/u/31408183?s=40&v=4'/>" +
"<span class='hz-group-list-username'>" + friend.nickName + "</span><span id=\"" + friend.id + "-status\" style='color: #9c0c0c;;'>[离线]</span>" +
"<div id=\"hz-badge-" + friend.id + "\" class='hz-badge'>0</div>" +
"</div>");
$friendGroupList.user = friend;
$("#hz-group-body").append($friendGroupList);
} //好友人数
$("#friendCount").text(friends.length); getOnlineList(user.id);
}
},
error: function (xhr, status, error) {
console.log("ajax错误!");
}
});
}; /**
* 获取在线好友
*/
function getOnlineList(userId){
$.ajax({
type: 'post',
url: ctx + "/imsFriend/getOnlineList",
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
dataType: 'json',
data: {userId: userId},
success: function (data) {
if (data.flag) {
//列表
let onlineFriends = data.data;
for (let i = 0; i < onlineFriends.length; i++) {
let friend = onlineFriends[i];
$("#" + friend.id + "-status").text("[在线]");
$("#" + friend.id + "-status").css("color", "#497b0f");
}
//好友人数
$("#onlineCount").text(onlineFriends.length);
}
},
error: function (xhr, status, error) {
console.log("ajax错误!");
}
});
}
socketChart.js
3、将之前前后端传递用户账户username改成用户id,同时,显示的是nickName昵称,改动的地方比较多,我就不贴代码了
4、消息存储
后端存储关键代码
/**
* 服务器接收到客户端消息时调用的方法
*/
@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("userId").equals(tarUser.get("userId"))) {
//群聊
groupChat(session,hashMap);
} else {
//私聊
privateChat(session, tarUser, hashMap);
} //后期要做消息持久化
ImsFriendMessageVo imsFriendMessageVo = new ImsFriendMessageVo();
imsFriendMessageVo.setToUserId((Integer) tarUser.get("userId"));
imsFriendMessageVo.setFromUserId((Integer) srcUser.get("userId"));
//聊天内容
imsFriendMessageVo.setContent(hashMap.get("message").toString());
try {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
imsFriendMessageVo.setCreatedTime(simpleDateFormat.parse(hashMap.get("date").toString()));
imsFriendMessageVo.setUpdataTime(simpleDateFormat.parse(hashMap.get("date").toString()));
} catch (ParseException e) {
e.printStackTrace();
}
imsFriendMessageService.save(imsFriendMessageVo); } catch (IOException e) {
e.printStackTrace();
}
}
WebSocketServer.java
前端点击好友时,获取聊天记录关键代码
//读取聊天记录
$.post(ctx + "/imsFriendMessage/getChattingRecords", {
fromUserId: userId,
toUserId: toUserId
}, function (data) {
if (data.flag) {
for (let i = 0; i < data.data.length; i++) {
let msgObj = data.data[i]; //当聊天窗口与msgUserName的人相同,文字在左边(对方/其他人),否则在右边(自己)
if (msgObj.fromUserId === userId) {
//追加聊天数据
setMessageInnerHTML({
id: msgObj.id,
isRead: msgObj.isRead,
toUserId: msgObj.toUserId,
fromUserId: msgObj.fromUserId,
message: msgObj.content,
date: msgObj.createdTime
});
} else {
//追加聊天数据
setMessageInnerHTML({
id: msgObj.id,
isRead: msgObj.isRead,
toUserId: msgObj.fromUserId,
message: msgObj.content,
date: msgObj.createdTime
});
}
}
}
});
socketChart.js
/**
* 获取A-B的聊天记录
*/
@RequestMapping("getChattingRecords")
public Result<List<ImsFriendMessageVo>> getChattingRecords(ImsFriendMessageVo imsFriendMessageVo){
return imsFriendMessageService.getChattingRecords(imsFriendMessageVo);
} @Override
public Result<List<ImsFriendMessageVo>> getChattingRecords(ImsFriendMessageVo imsFriendMessageVo) {
//A对B的聊天记录
List<ImsFriendMessageVo> allList = new ArrayList<>(super.list(imsFriendMessageVo).getData());
Integer fromUserId = imsFriendMessageVo.getFromUserId();
imsFriendMessageVo.setFromUserId(imsFriendMessageVo.getToUserId());
imsFriendMessageVo.setToUserId(fromUserId);
//B对A的聊天记录
allList.addAll(super.list(imsFriendMessageVo).getData());
//默认按时间排序
allList.sort(Comparator.comparingLong(vo -> vo.getCreatedTime().getTime()));
return Result.of(allList);
}
ImsFriendMessage
5、登录后获取所有未读消息并以小圆点的形式展示
登录成功后获取与好友的未读消息关键代码,在获取好友列表之后调用
//获取未读消息
$.post(ctx + "/imsFriendMessage/list",{toUserId:userId,isRead:0},function(data){
if(data.flag){
let friends = {}; //将fromUser合并
for (let i = 0; i < data.data.length; i++) {
let fromUser = data.data[i]; if(!friends[fromUser.fromUserId]){
friends[fromUser.fromUserId] = {};
friends[fromUser.fromUserId].count = 1;
}else{
friends[fromUser.fromUserId].count = friends[fromUser.fromUserId].count + 1;
}
} for (let key in friends) {
let fromUser = friends[key];
//小圆点++
$("#hz-badge-" + key).text(fromUser.count);
$("#hz-badge-" + key).css("opacity", "1");
}
}
});
socketChart.js
6、搜索好友、添加好友
可按照账号、昵称进行搜索,其中账号是等值查询,昵称是模糊查询
关键代码
//搜索好友
function findUserByUserNameOrNickName() {
let userNameOrNickName = $("#userNameOrNickName").val();
if (!userNameOrNickName) {
tip.msg("账号/昵称不能为空");
return;
} $.post(ctx + "/imsUser/findUserByUserNameOrNickName", {
userName: userNameOrNickName,
nickName: userNameOrNickName,
}, function (data) {
if (data.flag) {
$("#friendList").empty();
for (let i = 0; i < data.data.length; i++) {
let user = data.data[i];
let $userDiv = $("<div>" +
"<img style='width: 23px;margin: 0 5px 0 0;' src='" + user.avatar + "'/>" +
"<span>" + user.nickName + "(" + user.userName + ")</span>" +
"<button onclick='tipUserInfo($(this).parent()[0].user)'>用户详情</button>" +
"<button onclick=''>加好友</button>" +
"</div>");
$userDiv[0].user = user;
$("#friendList").append($userDiv);
}
}
});
}
socketChart.js
/**
* 根据账号或昵称(模糊查询)查询
*/
@PostMapping("findUserByUserNameOrNickName")
public Result<List<ImsUserVo>> findUserByUserNameOrNickName(ImsUserVo userVo) {
return imsUserService.findUserByUserNameOrNickName(userVo);
} @Override
public Result<List<ImsUserVo>> findUserByUserNameOrNickName(ImsUserVo userVo) {
return Result.of(CopyUtil.copyList(imsUserRepository.findUserByUserNameOrNickName(userVo.getUserName(), userVo.getNickName()), ImsUserVo.class));
} @Query(value = "select * from ims_user where user_name = :userName or nick_name like %:nickName%",nativeQuery = true)
List<ImsUser> findUserByUserNameOrNickName(@Param("userName") String userName,@Param("nickName") String nickName);
ImsUser
添加好友
首先要修改ims_friend结构,SQL如下,添加了一个字段is_agree,是否已经同意好友申请 0已申请但未同意 1同意 -1拒绝,之前查询好友列表的post请求则需要新增参数isAgree=1
/*
Navicat Premium Data Transfer Source Server : localhost
Source Server Type : MySQL
Source Server Version : 50528
Source Host : localhost:3306
Source Schema : test Target Server Type : MySQL
Target Server Version : 50528
File Encoding : 65001 Date: 14/05/2019 17:25:35
*/ SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0; -- ----------------------------
-- Table structure for ims_friend
-- ----------------------------
DROP TABLE IF EXISTS `ims_friend`;
CREATE TABLE `ims_friend` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`user_id` int(11) NULL DEFAULT NULL COMMENT '用户id',
`friend_id` int(11) NULL DEFAULT NULL COMMENT '好友id',
`friend_type` int(11) NULL DEFAULT NULL COMMENT '好友分组id',
`friend_remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '好友备注',
`is_agree` int(1) NULL DEFAULT NULL COMMENT '是否已经同意好友申请 0已申请但未同意 1同意 -1拒绝',
`created_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`updata_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '好友表' ROW_FORMAT = Compact; SET FOREIGN_KEY_CHECKS = 1;
在工具栏新加一个系统消息,贴出对应关键代码
//监听单击系统消息,弹出窗口
$("body").on("click", "#sysNotification", function () {
//此处为单击事件要执行的代码
if ($(".sysNotification").length <= 0) {
tip.dialog({
title: "系统消息",
class: "sysNotification",
content: "<div></div>",
shade: 0
});
} else {
$(".sysNotification").click();
}
$("#sysNotification").find(".hz-badge").css("opacity",0);
$("#sysNotification").find(".hz-badge").text(0); //已拒绝 //申请好友
$.post(ctx + "/imsFriend/list", {
friendId: userId,
isAgree: 0,
}, function (data) {
if (data.flag) {
for (let i = 0; i < data.data.length; i++) {
let user = data.data[i].user;
let $userDiv = $("<div>" +
"<img style='width: 23px;margin: 0 5px 0 0;' src='" + user.avatar + "'/>" +
"<span>" + user.nickName + "(" + user.userName + ")</span> 申请添加好友<br/>" +
"<button onclick='tipUserInfo($(this).parent()[0].user)'>用户详情</button>" +
"<button onclick='agreeAddFriend(" + data.data[i].id + ")'>同意</button>" +
"</div>");
$userDiv[0].user = user;
$(".sysNotification .tip-content").append($userDiv);
}
}
});
}); //申请添加好友
function applyToAddFriend(friendUserId) {
let nowTime = commonUtil.getNowTime();
$.post(ctx + "/imsFriend/save", {
userId: userId,
friendId: friendUserId,
friendType: 1,
friendRemark: "",
isAgree: 0,
createdTime: nowTime,
updataTime: nowTime,
}, function (data) {
if (data.flag) {
tip.msg({text:"已为你递交好友申请,对方同意好即可成为好友!",time:3000});
}
});
} //同意好友添加
function agreeAddFriend(id){
let nowTime = commonUtil.getNowTime();
$.post(ctx + "/imsFriend/save", {
id:id,
isAgree: 1,
updataTime: nowTime,
}, function (data) {
if (data.flag) {
$.post(ctx + "/imsFriend/save", {
userId: data.data.friendId,
friendId: data.data.userId,
friendType: 1,
friendRemark: "",
isAgree: 1,
createdTime: nowTime,
updataTime: nowTime,
}, function (data) {
if (data.flag) {
tip.msg({text:"你们已经是好友了,可以开始聊天!",time:2000});
}
});
}
});
} //获取我的申请好友,并做小圆点提示
function getApplyFriend(userId){
$.post(ctx + "/imsFriend/list", {
friendId: userId,
isAgree: 0,
}, function (data) {
if (data.flag && data.data.length > 0) {
$("#sysNotification").find(".hz-badge").css("opacity",1);
$("#sysNotification").find(".hz-badge").text(data.data.length);
}
});
}
socketChart.js
在线、离线提示出来小bug...
2019-05-17更新
问题找到了,是因为我们将关联的好友对象属性名改成了
@OneToOne
@JoinColumn(name = "friendId",referencedColumnName = "id", insertable = false, updatable = false)
@NotFound(action= NotFoundAction.IGNORE)
private ImsUser friendUser;//好友
但在获取在线好友那里还是,getUser();,导致数据错乱,bug修改:改成getFriendUser();即可
/**
* 获取在线好友
*/
@Override
public Result<List<ImsUserVo>> getOnlineList(ImsFriendVo imsFriendVo) {
imsFriendVo.setIsAgree(1);
//好友列表
List<ImsFriendVo> friendList = list(imsFriendVo).getData(); //在线好友列表
ArrayList<ImsUserVo> onlineFriendList = new ArrayList<>(); //遍历friendList
for(ImsFriendVo imsFriendVo1 : friendList){
ImsUserVo imsUserVo = imsFriendVo1.getUser();
if (!StringUtils.isEmpty(WebSocketServer.getSessionMap().get(imsUserVo.getId().toString()))) {
onlineFriendList.add(imsUserVo);
}
}
return Result.of(onlineFriendList);
}
后记
第二版暂时记录到这,第三版持续更新中...
2019-06-18补充:HashMap不支持并发操作,线程不安全,ConcurrentHashMap支持并发操作线程安全,因此,我们应该用后者,而不是前者,今天在这里补充一下,就不再其他地方做补充说明了
PS:ConcurrentHashMap是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
一套简单的web即时通讯——第二版的更多相关文章
- 一套简单的web即时通讯——第一版
前言 我们之前已经实现了 WebSocket+Java 私聊.群聊实例,后面我们模仿layer弹窗,封装了一个自己的web弹窗 自定义web弹窗/层:简易风格的msg与可拖放的dialog,生成博客园 ...
- 一套简单的web即时通讯——第三版
前言 接上版,本次版本做了如下优化: 1.新增同意.拒绝添加好友后做线上提示: 2.新增好友分组,使用工具生成后台API,新增好友分组功能,主要功能有:添加分组.重命名分组名称.删除分组 3.新增好友 ...
- IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯(三)
IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯(三) 后台服务用户与认证 新建一个空的.net core web项目Demo.Chat,端口配置为 ...
- IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯(一)
IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯 前言 .net core 2.1已经正式发布了,signalr core1.0随之发布,是时候写个 ...
- IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯(二)
IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯(二) IdentityServer4 用户中心生成数据库 上文已经创建了所有的数据库上下文迁移代码 ...
- 搭建一套简单的web服务器,记录实验过程
搭建web服务器 一.实验内容: 实验要求: 1.完成一个简单的web服务器,web服务器从mysql里读取数据进行返回 2.Mysql需要有一个单独的数据盘,每个mysql虚拟机的磁盘挂载方式需要都 ...
- web即时通讯2--基于Spring websocket达到web聊天室
如本文所用,Spring4和websocket要构建web聊天室,根据框架SpringMVC+Spring+Hibernate的Maven项目,后台使用spring websocket进行消息转发和聊 ...
- 自己动手写一个简单的MVC框架(第二版)
一.ASP.NET MVC核心机制回顾 在ASP.NET MVC中,最核心的当属“路由系统”,而路由系统的核心则源于一个强大的System.Web.Routing.dll组件. 在这个System.W ...
- Verilog实现IIC通讯第二版
HMC5883三轴磁力传感器IIC通讯模块的VerilogHDL的实现 上一版并没有实现我想要的功能 0.0.1版 正在修订中 2013/9/2 //date :2013/7/7 //desi ...
随机推荐
- 翻译Lanlet2
Here is more information on the basic primitives that make up a Lanelet2 map. Read here for a primer ...
- ios app被自己从应用商店下架后可以再恢復上架吗
好像没有企业能阻挡苹果的下架决定,毕竟这是它的地盘.不管是已经恢复上架的百度.腾讯.优酷.人人游戏,还是至今没有下文的360.金山和PPS,也不管这些企业在中国乃至全球互联网行业的地位如何,下架原因只 ...
- 再次理解WCF以及其通信(附加一個編程小經驗)
一.概述 Windows Communication Foundation(WCF)是由微软发展的一组数据通信的应用程序开发接口,可以翻译为Windows通讯接口,它是.NET框架的一部分.由 .NE ...
- 使用mybatis-generator插件结合tk.mybatis自动生成mapper二三事
本篇文章将介绍使用spring boot框架,引入mybatis-generator插件,结合tk.mybatis自动生成Mapper和Entity的一整套流程,其中包括最重要的踩坑与填坑. ...
- php将一个二维数组按照某个字段值合并成一维数组,如果有重复则将重复的合并成二维数组
版权声明:本文为博主原创文章,未经博主允许不得转载. 最近工作中碰到一个问题,用PHP将一个二维数组按照二维数组中的各个项中的某个特定字段值合并成一维数组,如果有重复则将重复的合并成二维数组,生成的二 ...
- linux命令学习笔记(50):crontab命令
前一天学习了 at 命令是针对仅运行一次的任务,循环运行的例行性计划任务,linux系统则是由 cron (crond) 这个系统服务来控制的.Linux 系统上面原本就有非常多的计划性工作,因此这个 ...
- bzoj1067降雨量
True和False都好搞 Maybe的情况: 1.Y年和X年的降雨量已知,X年的降雨量不超过Y年的降雨量,从Y+1到X-1年中存在至少一年的降雨量未知,从Y+1到X-1年中已知的降雨量都小于X年的降 ...
- ACM学习历程—HDU1041 Computer Transformation(递推 && 大数)
Description A sequence consisting of one digit, the number 1 is initially written into a computer. A ...
- 51nod 1301 集合异或和——异或dp
题目:http://www.51nod.com/onlineJudge/questionCode.html#!problemId=1301 好题!看了TJ才会. 因为是不可重集合,所以当然有前 i 个 ...
- WPF中Xaml编译正常而Designer Time时出错的解决办法
开发wpf时我们经常遇到一个xaml文件在设计时显示解析错误(比如在:VS或者Blend)而编译正常运行正常. 原因是:xaml的在Debug版本下必须为anyCPU. 解决办法: 1.打开工程文件x ...