使用SpringBoot + WebSocket 实现模拟QQ聊天功能
源码地址:https://gitee.com/mmolu/ws-chat
登录界面展示
登录界面模仿QQ登录操作,支持拖动、最小化和关闭
聊天界面展示
登录后的右侧显示在线用户,右下方显示在线用户的登录日志
窗口支持拖动、关闭操作
发送消息界面展示
在线用户实现及时聊天功能,可以对指定用户发起聊天(实现点对点的消息推送功能)
支持消息的本地缓存,聊天内容不会因为页面的刷新、关闭、退出登录等操作而丢失(消息只是做了简单缓存),清理浏览器缓存数据时消息数据便会丢失。
点对点消息推送,实现一对一的即时通信功能
Demo的目录代码
该Demo主要使用的是
SpringBoot + WebSocket
技术实现
下面是Demo的后端代码实现及说明
创建SpringBoot项目,导入依赖
项目pom依赖配置
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<!-- springboot-Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- WebSocket组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 热部署依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<scope>true</scope>
</dependency>
<!-- ****************************以下工具为非必须**************************** -->
<!-- lang3工具 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.16</version>
</dependency>
<!-- slf4j日志处理 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<!-- fastjson,对象和JSON数据转换工具 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.55</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打jar包时 如果 不配置该插件,打出来的jar包没有清单文件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
项目配置文件说明
application.properties
# 项目名称
spring.application.name=websocket-chatroom
# 配置资源路径
spring.resource.static-locations=classpath:/static/
# 视图前缀配置
spring.mvc.view.prefix=/chat/
# 视图后缀配置
spring.mvc.view.suffix=.html
###########################【热部署】#########################
# 重启目录
spring.devtools.restart.additional-paths=src/main/java
# 设置开启热部署
spring.devtools.restart.enabled=true
# 设置字符集
spring.freemarker.charset=utf-8
# 页面不加载缓存,修改后立即生效
spring.freemarker.cache=false
# 服务端口配置
server.port=80
application.yml
spring:
resource:
static-locations: classpath:/static/
application:
name: websocket-chatroom
mvc:
view:
suffix: .html
prefix: /chat/
devtools:
restart:
additional-paths: src/main/java
enabled: true
freemarker:
charset: utf-8
cache: false
server:
port: 80
项目的配置类说明
WebConfig.java
WebConfig 配置类
配置后项目启动时,访问默认项目路径时,会跳转到该类指定的页面中
/**
* @title web配置类 设置默认访问页面
* @author 陌路
* @date 2022-04-16
* @apiNote 配置默认页面
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 自定义静态路径,spring.resource.static-locations=classpath:/static/
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("/chat/login");
}
}
WebSocketConfig.java
使用WebSocket前首先配置 @ServerEndpoint
首先需要注入 ServerEndpointExporter ,
这个bean会自动注册使用 @ServerEndpoint 注解来声明WebSocket endpoint。
注意:如果使用独立的Servlet容器,而不是直接使用SpringBoot内置容器,就不需要注入 ServerEndpointExporter,因为他将由容器自己提供和管理。
/**
* @Desc 首先需要注入 ServerEndpointExporter ,
* 这个bean会自动注册使用 @ServerEndpoint 的注解来声明WebSocket endpoint。
* 注意:如果使用独立的Servlet容器,而不是直接使用SpringBoot内置容器,就不需要注入
* ServerEndpointExporter,因为他将有容器自己提供和管理。
* @author 陌路
* @date 2022-04-16
*/
@Configuration
public class WebSocketConfig {
/**
* @title 扫描注册使用 @ServerEndpoint 注解的类
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
GetHttpSessionConfigurator.java
该类可以在建立连接后存放HttpSession,方便后续的使用
用户建立连接时,通过
EndpointConfig
获取存在session
中的用户数据
/**
* @tite 用来获取HttpSession对象.
* @author 陌路
* @date 2022-04-16
*/
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
/**
* @title 该配置可以在不手动传入HttpSession的情况下在websocket服务类中使用
*/
@Override
public void modifyHandshake(ServerEndpointConfig sec,
HandshakeRequest request,
HandshakeResponse response) {
// 获取httpsession对象
HttpSession httpSession = (HttpSession) request.getHttpSession();
// 存放httpsession对象
Map<String, Object> userProperties = sec.getUserProperties();
userProperties.put(HttpSession.class.getName(), httpSession);
}
}
创建用户对象实体
/**
* (User)实体类
* @author 陌路
* @since 2022-04-16
*/
@Data
public class User implements Serializable {
private static final long serialVersionUID = -31513108721728277L;
/** 用户id */
private String userId;
/** 用户名 */
private String username;
/** 用户手机号 */
private String phone;
/** 用户密码 */
private String password;
public User(String userId, String username, String password, String phone) {
this.userId = userId;
this.username = username;
this.password = password;
this.phone = phone;
}
public User() {
}
}
创建数据信息对象
使用数据信息对象来模拟数据存储数据,实现用户的登录和注册
import cn.hutool.core.lang.Validator;
import cn.molu.app.pojo.User;
import cn.molu.app.vo.R;
/**
* @title 模拟数据库user表
* @Desc 模拟数据库数据,用户登录校验
* @author 陌路
* @date 2022-04-16
*/
public class UserData {
/**
* 构造用户数据 模拟数据库用户
*/
public static Map<String, User> userMap = new HashMap<String, User>();
static {
//userMap.key=phone, userMap.value=user(id, name, password, phone);
userMap.put("150******67", new User("001", "jack", "123456", "150******67"));
userMap.put("135******88", new User("002", "mary", "123456", "135******88"));
userMap.put("136******66", new User("003", "tom", "123456", "136******66"));
userMap.put("159******21", new User("004", "tim", "123456", "159******21"));
userMap.put("188******88", new User("005", "jenny", "123456", "188******88"));
userMap.put("177******14", new User("006", "admin", "123456", "177******14"));
userMap.put("176******67", new User("007", "lover", "123456", "176******67"));
userMap.put("150******83", new User("008", "molu", "123456", "150******83"));
}
/**
* @Title 根据手机号获取用户信息
* @return User
*/
public static User loginByPhone(String phone) {
boolean mobile = Validator.isMobile(phone);
if (!mobile) {
return null;
}
User user = userMap.get(phone);
if (ObjectUtils.isBlank(user)) {
return null;
}
return user;
}
/**
* @Title 用户注册
* @return R
*/
public static R register(HttpServletResponse res, User user) {
if (null == user) {
return R.err("请输入手机号、用户名和密码!");
}
String phone = user.getPhone();
String username = user.getUsername();
String password = user.getPassword();
ObjectUtils.checkNull(res, "手机号、用户名和密码不能为空!", phone, username, password);
user.setUserId(String.format("%03d", String.valueOf(userMap.size() + 1)));
userMap.put(phone, user);
return R.ok("注册成功!");
}
}
数据响应实体类
响应数据结果,存储用户基本数据,过滤掉用户的重要信息
/**
* @Desc 数据的响应类.
* @author 陌路
* @date 2022-04-16 上午10:53:15
*/
@Data
public class Result {
/** 响应标志,成功/失败 */
private boolean flag;
/** 响应给前台的消息 */
private String message;
/** 用户名 */
private String username;
/** 用户id */
private String userId;
/** 日期时间 */
private String dateStr;
public void setDateStr(Date date) {
this.dateStr = dateFormat(date);
}
public Result() {
super();
}
public static String dateFormat(Date date) {
return ObjectUtils.dateFormat(date);
}
}
消息接收和响应类
用于接收和返回数据前台数据
/**
* @title 服务端发送给客户端的消息.
*/
public class ResultMessage {
/** 是否是系统消息 */
private Boolean systemMsgFlag;
/** 发送方姓名 */
private String fromName;
/** 发送方id */
private String fromId;
/** 接收方姓名 */
private String toName;
/** 接收方id */
private String toId;
/** 发送的数据 */
private Object message;
/** 接收到消息的日期时间 */
private String dateStr;
public void setDateStr(String dateStr) {
this.dateStr = dateStr;
}
public String getDateStr(Date date) {
if (date == null) {
date = new Date();
}
return dateFormat(date);
}
public void setDateStr() {
this.dateStr = dateFormat(new Date());
}
public void setDateStr(Date date) {
if (date == null) {
date = new Date();
}
this.dateStr = dateFormat(date);
}
public ResultMessage() {
super();
}
public static String dateFormat(Object date) {
return ObjectUtils.dateFormat(date);
}
}
WS核心服务类
webSocket 的核心服务类,建立连接、发送消息、推送通知、关闭连接释放资源、消息处理等。。。
/**
* @Desc 注解 @ServerEndpoint 配置对外暴露访问地址,外部访问格式为(ws://localhost:80/webSocket/001)
* @author 陌路
* @date 2022-04-16
*/
@Component
@ServerEndpoint(value = "/webSocket/{userId}", configurator = GetHttpSessionConfigurator.class)
public class ChatEndpoint {
private final static Logger LOGGER = LogManager.getLogger(ChatEndpoint.class);
/** 声明一个线程安全的全局Map对象,用来存储每一个用户的Session对象 */
private static Map<String, Session> onLineUsers = new ConcurrentHashMap<String, Session>();
/** 缓存用户数据 */
public static Map<String, Result> cacheUser = new HashMap<String, Result>();
/** 获取HttpSession中的用户名 */
private HttpSession httpSession;
/**
* @Title 连接建立时,会调用该方法
*/
@OnOpen
public void onOpen(Session session, EndpointConfig endpointConfig) {
// 获取GetHttpSessionConfigurator中存放的HttpSession
Map<String, Object> userProperties = endpointConfig.getUserProperties();
HttpSession httpSession = (HttpSession) userProperties.get(HttpSession.class.getName());
this.httpSession = httpSession;
String username = ObjectUtils.getStr(httpSession.getAttribute("username")) ;
String userId = ObjectUtils.getStr(httpSession.getAttribute("userId"));
// 如果用户名和用户id为空,则直接返回
if (ObjectUtils.isEmpty(username,userId)) {
return;
}
LOGGER.info("{} 建立了连接。。。", username);
Result res = new Result();
res.setUserId(userId);
res.setUsername(username);
// 缓存数据
onLineUsers.put(userId, session);
cacheUser.put(userId, res);
// 组织消息体数据,将数据推送给所有在线用户
String message = getSysMessage(getusers());
// 调用方法进行系统消息的推送
broadcastAllUsers(message);
LOGGER.info("推送系统消息:{} ", message);
}
/**
* @Title 接收到客户端发送的数据时,会调用此方法
*/
@OnMessage
public void onMessage(String message, Session session) throws IOException {
LOGGER.info("接收到消息:{}", message);
if (StringUtils.isBlank(message)) {
return;
}
// 心跳检测机制
if ("PING".toUpperCase().equals(message.toUpperCase())) {
heartCheck(session);
return;
}
LOGGER.info("接收到好友的消息:{}", message);
ResultMessage msgObj = JSON.parseObject(message, ResultMessage.class);
// 消息接收人id
String toId = msgObj.getToId();
// 获取消息体内容
String text = ObjectUtils.getStr(msgObj.getMessage());
// 获取推送指定用户数据
String resultMessage = getMessage(msgObj, text);
LOGGER.info("接收到好友发来的数据:{}", resultMessage);
// 点对点发送数据(给指定用户发送消息)
Session toSession = onLineUsers.get(toId);
if (toSession != null && toSession.isOpen()) {
Basic basicRemote = toSession.getBasicRemote();
basicRemote.sendText(resultMessage);
}
}
/**
* @title 连接关闭时,调用此方法
*/
@OnClose
public void onClose(Session session) {
if (httpSession == null) {
return;
}
String username = ObjectUtils.getStr(httpSession.getAttribute("username"));
String userId = ObjectUtils.getStr(httpSession.getAttribute("userId"));
if (StringUtils.isBlank(userId)) {
return;
}
LOGGER.info("{}:关闭了连接。。。", username);
// 移除已关闭连接的用户
onLineUsers.remove(userId);
// 移除缓存的用户数据
cacheUser.remove(userId);
LOGGER.info("已从onLineUsers移除:{},从cacheUser中移除:{}", username, username);
String message = getSysMessage(getusers());
// 设置已关闭连接的用户数据在HttpSession域中有效时间
httpSession.setMaxInactiveInterval(1800);
broadcastAllUsers(message);
LOGGER.info("关闭连接,推送内容:{}", message);
}
/**
* @title 出现错误时调用改方法
*/
@OnError
public void onError(Session session, Throwable error) {
LOGGER.info("连接出错了......{}", error);
}
/**
* @title 消息的推送,将消息推送给所有在线用户
* @param message
*/
private void broadcastAllUsers(String message) {
try {
// 将消息推送给所有的在线用户
Set<String> ids = getIds();
LOGGER.info("onLineUsers的所有id:{}", ids);
for (String id : ids) {
Session session = onLineUsers.get(id);
Result result = cacheUser.get(id);
LOGGER.info("获取到用户信息:{}", result);
// 判断用户是否是连接状态
if (session.isOpen()) {
session.getBasicRemote().sendText(message);
}
}
LOGGER.info("给{}推送了{}", getIds(), message);
} catch (Exception e) {
LOGGER.error("广播发送系统消息失败!{}", e);
e.printStackTrace();
}
}
/**
* @title 组织消息内容
*/
public static String getMessage(ResultMessage resultMessage, String message) {
resultMessage.setSystemMsgFlag(false);
resultMessage.setMessage(message);
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.writeValueAsString(resultMessage);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return "";
}
/**
* @Title 组织系统推送数据信息
* @return String
*/
public static String getSysMessage(Collection<Result> collection) {
ResultMessage resultMessage = new ResultMessage();
resultMessage.setSystemMsgFlag(true);
resultMessage.setMessage(collection);
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.writeValueAsString(resultMessage);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return "";
}
/**
* @Title 心跳检测机制
*/
public static void heartCheck(Session session) {
try {
Map<String, Object> params = new HashMap<String, Object>();
params.put("type", "PONG");
session.getAsyncRemote().sendText(JSON.toJSONString(params));
LOGGER.info("应答客户端的消息:{}", JSON.toJSONString(params));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* @title 获取所有在线用户的用户id
* @return Set<String>
*/
private Set<String> getIds() {
return ChatEndpoint.onLineUsers.keySet();
}
/**
* @title 获取所有在线用户,如果是系统消息就返回这个
* @return Set<String>
*/
private Collection<Result> getusers() {
return cacheUser.values();
}
/**
* @title 格式化Date类型的数据
* @param date
* @return yyyy-MM-dd HH:mm:ss
*/
public static String dateFormat(Date date) {
return ObjectUtils.dateFormat(date);
}
}
后台控制器接口
后端访问接口
@Controller
@RequestMapping("index")
public class LoginController {
/**
* 处理用户登录请求.
* @param user 用户登录数据
*/
@ResponseBody
@PostMapping("/login")
public Result login(User user, HttpServletRequest req,HttpServletResponse res) {
HttpSession session = req.getSession();
ObjectUtils.checkNull(res,"请输入手机号和密码!",user,user.getPhone(),user.getPassword());
String phone = user.getPhone();
String password = user.getPassword();
boolean mobile = Validator.isMobile(phone);
Result result = new Result();
if(!mobile){
result.setMessage("手机号格式错误!");
result.setFlag(false);
return result;
}
try {
User userData = UserData.loginByPhone(phone);
ObjectUtils.checkNull(res,userData,String.format("登录失败,未获取到%s的用户信息!", phone));
if (password.equals(userData.getPassword())) {
String username = userData.getUsername();
result.setFlag(true);
result.setMessage("登录成功!");
result.setUsername(username);
String userId = userData.getUserId();
result.setUserId(userId);
result.setDateStr(new Date());
session.setAttribute("username", username);
session.setAttribute("userId", userId);
} else {
result.setMessage("登录失败,密码输入错误!");
result.setFlag(false);
}
} catch (Exception e) {
e.printStackTrace();
result.setMessage("登录失败!",e.getMessage());
result.setFlag(false);
}
return result;
}
/**
* @Title 登录成功后跳转到聊天页面.
* @return 跳转到聊天室,如果没有登录,则返回登录页面
*/
@GetMapping("/toChatroom")
public String toChatroom(HttpSession session) {
String username =ObjectUtils.getStr(session.getAttribute("username"));
if (StringUtils.isBlank(username)) {
return "login";
}
return "div";
}
/**
* 登录成功后,获取session域中username值.
* @param session
*/
@ResponseBody
@GetMapping("/getUsername")
public Map<String, Object> getUsername(HttpSession session) {
Map<String, Object> map = new HashMap<String, Object>();
String username = (String) session.getAttribute("username");
String userId = (String) session.getAttribute("userId");
map.put("flag", true);
map.put("username", username);
map.put("userId", userId);
return map;
}
/**
* @title 返回登录页面
*/
@GetMapping(value = "/toIndex")
public String toIndexPage() {
return "login";
}
}
登录页面代码
<html lang="zh">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>登录</title>
<link href="/css/login.css" rel="stylesheet" />
<script src="/js/jquery.js" ></script>
<script type="text/javascript" src="/js/move.div.js" ></script>
<script type="text/javascript" src="/js/login.js" ></script>
<script type="text/javascript" src="/js/molu.js" ></script>
</head>
<body>
<div class="box">
<div class="title">
<span class="left_bg">
<img src="/imgs/bitbug.ico" style="width: 35px;height: 35px;" />
<span style="font-size: 25px;">WS</span>
</span>
<span class="title_text" style="display: none;">
请登录
</span>
<span class="close" title="关闭">X</span>
<span class="min_hide" title="最小化">一</span>
</div>
<span class="img_bg">
<img src="/imgs/chatIc.png" style="width: 60px;height: 60px;border-radius: 10pc;" />
</span>
<div>
<div id="loginForm">
<div class="con" style="text-align: center;">
<input class="u_input phone" type="text" name="phone" autocomplete="on"
placeholder="手机号..." maxlength="11"/><br />
<input class="u_input upwd" type="password" name="password"
placeholder="密码..." maxlength="16"/>
</div>
<div class="check_box">
<span style="margin-left: 60px;"></span>
<label>
<input type="checkbox" class="showPwd" name="showPwd" title="显示密码" />
<span>显示密码</span>
</label>
<label>
<input type="checkbox" class="remPwd" name="rememberMe" title="记住密码" />
<span>记住密码</span>
</label>
<span>
<a href="javascript:;" style="margin-left: 30px;" title="找回密码">找回密码</a>
</span>
</div>
<div class="login_btn">
<button class="btn" title="登录账号" type="submit" id="loginFormBtn">登录</button>
</div>
</div>
</div>
<div class="bottom_div">
<span><a href="javascript:;" title="注册账号">注册账号</a></span>
<span></span>
</div>
</div>
</body>
</html>
登录页面JS代码
$(function() {
$(".close").click(function() {
$(".box").hide();
});
$("#loginFormBtn").click(function() {
if (loginCheckData()) {
login();
}
});
$(".showPwd").bind("input propertychange", function() {
if ($(this).prop("checked")) {
$(".upwd").attr("type", "text");
} else {
$(".upwd").attr("type", "password");
}
})
});
function login() {
let params = {};
params.phone = ml.empty($('.phone').val());
params.password = ml.empty($('.upwd').val());
params.flag = $("input[name='rememberMe']").prop("checked");
$.post("/index/login", params, function(res) {
if (res.flag) {
ml.msgBox(res.message);
location.href = "/index/toChatroom";
} else {
ml.msgBox(res.message, 5, 5);
}
}).error(function(err) {
ml.msgBoxBtn(err.responseJSON.status + ":" + err.responseJSON.message, "错误提示");
})
}
function loginCheckData() {
let phone = ml.empty($('.phone').val());
if (!phone || phone.length != 11) {
ml.tips("请输入正确的手机号!", "phone");
return false;
}
// 不可包含中文及中文字符
let regZh = /[\u4e00-\u9fa5][\u3000-\u301e\ufe10-\ufe19\ufe30-\ufe44\ufe50-\ufe6b\uff01-\uffee]/;
//1.验证手机号 规则:第一位1,第二位是358中的一个,第三到十一位必须是数字。总长度11
let reg = /^[1][358][0-9]{9}$/;
if (!reg.test(phone) || regZh.test(phone)) {
ml.tips("输入的手机号格式不正确!", "phone");
return false;
}
let password = ml.empty($('.upwd').val());
if (!password) {
ml.tips("请输入密码!", "upwd");
return false;
}
if (regZh.test(password)) {
ml.tips("密码不可包含中文字符!", "upwd");
return false;
}
if (password.length < 6) {
ml.tips("密码必须为6~16个字符之间!", "upwd");
return false;
}
let regPwd = /[A-Za-z0-9.!?]{12,30}/;
if (!regPwd.test(password)) {
ml.tips("密码仅支持数字、大小写字母和.!?符号", "upwd");
return false;
}
//ml.msgBox("校验通过!");
return true;
}
聊天界面代码
<html>
<head>
<meta charset="UTF-8">
<title></title>
<link href="/css/div.css" rel="stylesheet" />
<script type="text/javascript" src="../js/jquery.js"></script>
<script type="text/javascript" src="../js/move.div.js"></script>
<script type="text/javascript" src="../js/molu.js"></script>
<script type="text/javascript" src="../js/div.js"></script>
</head>
<body>
<div class="box">
<div class="div-main-title title" style="text-align: center;"></div>
<div class="close">
<a class="close close-click" title="关闭" href="javascript:void(0);">X</a>
</div>
<div class="div-main-list">
<div class="main-list">
<span>请选择好友聊天</span>
<ul class="friend-list"></ul>
</div>
<div class="main-sys-msg">
<span>系统消息</span>
<ul class="sys-msg" style="text-align: left; padding: 5px; margin: 0px;"></ul>
</div>
</div>
<div class="chat-main">
<ul class="chat-msg">
<li class="friend-li"><img src="../imgs/chatIc.png" class="img">
<span class="friend-span">快去找朋友聊聊吧。。。</span>
</li>
</ul>
</div>
<div class="send-main">
<textarea class="edit-msg" disabled></textarea>
</div>
<div class="div-btn">
<div style="width: 50%;"></div>
<div style="width: 50%; text-align: right;">
<button class="closeDiv close-click" type="button" style="padding: 0px;">关闭</button>
<button class="sendMsg" type="button">发送</button>
</div>
</div>
</div>
</body>
</html>
聊天界面JS代码
let webObj = null;//全局WebSocket对象
let lockReconnect = false; // 网络断开重连
let wsCreateHandler = null; // 创建连接
let username = null; // 当前登录人姓名
let userId = null; //当亲登录人id
let toName = null; //消息接收人姓名
let toId = null;//消息接收人id
$(function() {
$(".bg_change_size").remove();
// 在ajax请求开始前将请求设为同步请求:$.ajaxSettings.async = false。
// 在ajax请求结束后不要忘了将请求设为异步请求,否则其他的ajax请求也都变成了同步请求 $.ajaxSettings.async = true。
$.ajax({
async: false,
type: 'GET',
url: "/index/getUsername",
success: function(res) {
if (!res.userId || !res.username) {
location.href = '/index/toIndex';
}
username = res.username;
userId = res.userId;
}
});
// 创建webSocket对象
createWebSocket();
// 发送消息到服务器
$(".sendMsg").on("click", function() {
sendMessage();
})
});
/**
* @title 创建连接
*/
function createWebSocket() {
try {
// 获取访问路径,带有端口号:ws://localhost/webSocket/001
let host = window.location.host;
// 创建WebSocket连接对象
webObj = new WebSocket(`ws://${host}/webSocket/${userId}`);
// 加载组件
initWsEventHandle();
} catch (e) {
ml.msgBox("连接出错,正在尝试重新连接,请稍等。。。");
// 尝试重新连接服务器
reconnect();
}
}
/**
* @title 初始化组件
*/
function initWsEventHandle() {
try {
// 建立连接
webObj.onOpen = function(evt) {
onWsOpen(evt);
// 建立连接之后,开始传输心跳包
heartCheck.start();
};
// 传送消息
webObj.onmessage = function(evt) {
// 发送消息
onWsMessage(evt);
// 接收消息后 也需要心跳包的传送
heartCheck.start();
};
// 关闭连接
webObj.onclose = function(evt) {
// 关闭连接,可能是异常关闭,需要重新连接
onWsClose(evt);
// 尝试重新连接
reconnect();
};
// 连接出错
webObj.onerror = function(evt) {
// 连接出错
onWsError(evt);
// 尝试重新连接
reconnect();
}
} catch (e) {
if (e) {
conlog("catch", e);
}
ml.msgBox("初始化组件失败,正在重试,请稍后。。。");
// 尝试重新创建连接
reconnect();
}
}
/**
* @title 建立连接
*/
function onWsOpen(e) {
if (e.data) {
conlog("onWsOpen", e.data);
}
ml.msgBox("建立连接成功。。。");
}
/**
* @title 传送消息内容
*/
function onWsMessage(e) {
if (e.data) {
conlog("onWsMessage", e.data);
}
let jsonStr = e.data;
//接收到服务器推送的消息后触发事件
message(e);
}
/**
* @title 关闭连接
*/
function onWsClose(e) {
if (e.data) {
conlog("onWsClose", e.data);
}
ml.msgBox("连接关闭,尝试重新连接服务器,请稍侯。。。");
closeFun(e);
}
/**
* @title 连接出错
*/
function onWsError(e) {
ml.msgBox("连接出错,正在尝试重新连接服务器,请稍侯。。。" + e.data);
}
/**
* @title 输出数据到控制台
*/
function conlog(msg) {
conlog('', msg);
}
/**
* @title 输出数据到控制台
*/
function conlog(title, msg) {
let content = msg;
if (title) {
content = `${title}:${msg}`;
}
console.log(`${content}`);
}
/**
* @title 异常处理
* @Desc 处理可以检测到的异常,并尝试重新连接
*/
function reconnect() {
if (lockReconnect) {
return;
}
conlog("reconnect");
ml.msgBox("正在尝试重新连接,请稍侯。。。");
lockReconnect = true;
// 没链接上会一直连接,设置延迟,避免过多请求
wsCreateHandler && clearTimeout(wsCreateHandler);
wsCreateHandler = setTimeout(function() {
ml.msgBox("正在重新连接。。。");
createWebSocket();
lockReconnect = false;
ml.msgBox("重连完成。。。");
}, 1000);
}
/**
* @title 心跳检测
* @Desc 网络中断,系统无法捕获,需要心跳检测实现重新连接
*/
var heartCheck = {
// 在15s内若没收到服务端消息,则认为连接断开,需要重新连接
timeout: 15000, // 心跳检测触发时间
timeoutObj: null,
serverTimeoutObj: null,
// 重新连接
reset: function() {
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
this.start();
},
// 开启定时器
start: function() {
let self = this;
this.timeoutObj && clearTimeout(this.timeoutObj);
this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
this.timeoutObj = setTimeout(function() {
ml.msgBox("发送ping到后台服务。。。");
try {
webObj.send("PING");
} catch (e) {
ml.msgBox("发送ping异常。。。");
}
//内嵌定时器
self.serverTimeoutObj = setTimeout(function() {
// 若onclose方法会执行reconnect方法,我们只需执行close()就行,
// 若直接执行reconnect会触发onclose导致重连两次
ml.msgBox("没有收到后台数据,关闭连接。。。");
webObj.close();
//reconnect();
}, self.timeout);
}, this.timeout);
}
};
/**
* @title 组织并解析数据
*/
function message(e) {
//获取服务端推送过来的消息
let message = e.data;
// 将message转为JSON对象
let res = JSON.parse(message);
// 是否为系统消息
if (res.systemMsgFlag) {
let allNames = res.message;
//1. 好友列表展示 2. 系统推广
let userListStr = "";
let broadcastListStr = "";
let imgUrl = "../imgs/chatIc.png";
for (let user of allNames) {
if (user.userId != userId) {
userListStr += `<li class="friend-li" οnclick='chatWith("${user.userId}","${user.username}",this);'><img src="${imgUrl}" class='friendList-img'><span>${user.username}</span></li>`;
broadcastListStr += `<li style='color:#9d9d9d;font-family:宋体;'>
<span style='font-size:5px;color:black;'>${user.dateStr}</span><br/>
好友<span>${user.username}</span>上线了!
</li>`;
}
}
$(".friend-list").html(userListStr);
$(".sys-msg").html(broadcastListStr);
} else {
// 不是系统消息
let msgText = res.message;
let msg = `<li class='friend-msg-li'><img src='${imgUrl}' class='img'/><span class='friend-span'>${msgText}</span></li>`;
if (toId === res.fromId) {
$(".chat-msg").append(msg);
}
let chatData = sessionStorage.getItem(res.fromId);
if (chatData) {
msg = chatData + msg;
}
sessionStorage.setItem(res.fromId, msg);
$(".chat-main")[0].scrollTop = $(".chat-main")[0].scrollHeight;
}
}
// 关闭连接
function closeFun(e) {
let tips = `<span style='font-size:5px;color:black;'>${new Date()}</span><br/>`;
$(".sys-msg").html(`${tips}用户:${username}<span style="float:right;color:red;">离开了</span>`);
}
// 选择好友
function chatWith(id, name, obj) {
toId = id;
toName = name;
$(".edit-msg").attr("disabled", false);
$(".div-btn").show();
$(".chat-msg").show();
$(obj).addClass("selected-li").siblings().removeClass("selected-li");
let chatNow = `正在和<span style="color: #db41ca;">${name}</span>聊天`;
$(".div-main-title").html(chatNow);
$(".chat-msg").html("");
var chatData = sessionStorage.getItem(toId);
if (chatData) {
//渲染聊天数据到聊天区
$(".chat-msg").html(chatData);
}
$(".chat-main")[0].scrollTop = $(".chat-main")[0].scrollHeight;
}
/**
* @title 组织和发送消息到服务器
*/
function sendMessage() {
// 发送消息
if (!toId || !toName) {
ml.tips("sendMsg", "请选择好友...");
return;
}
let msg = $(".edit-msg").val();
if (!msg) {
ml.tips("sendMsg", "请输入内容...");
return;
}
let img = `<img src='${imgUrl}' class='myself-img'/>`;
let li = $("<li class='myself-li'></li>");
li.html(`${img}<span class='myself-span'>${msg}</span>`);
$(".chat-msg").append(li);
$(".edit-msg").val('');
$(".chat-main")[0].scrollTop = $(".chat-main")[0].scrollHeight;
let chatData = sessionStorage.getItem(toId);
let liStr = `<li class='myself-li'>${img}<span class='myself-span'>${msg}</span></li>`;
if (chatData) {
liStr = chatData + liStr;
}
let jsonMessage = {
"fromName": username, //消息发送人姓名
"fromId": userId, //消息发送人id
"toName": toName, //消息接收人姓名
"toId": toId, //消息接收人id
"message": msg //发送的消息内容
};
// 将消息存放到sessionStorage中
sessionStorage.setItem(toId, liStr);
// 发送数据给服务器(消息发送格式为JSON格式)
webObj.send(JSON.stringify(jsonMessage));
}