前言

  我们之前已经实现了 WebSocket+Java 私聊、群聊实例,后面我们模仿layer弹窗,封装了一个自己的web弹窗 自定义web弹窗/层:简易风格的msg与可拖放的dialog,生成博客园文章目录弹窗,再后来就产生了将两者结合起来的想法,加上我们之前实现了一套自动生成代码的jpa究极进化版 SpringBoot系列——Spring-Data-JPA(究极进化版) 自动生成单表基础增、删、改、查接口,于是去网上搜索,参考即时通讯系统的消息存储如何建表,看下能不能把我们之前的东西稍微整合一下,将之前的写的东西应用起来学以致用,实现一套简单的web即时通讯,一版一版的升级完善。

  第一版功能

  1、实现简单的登录/注册

  2、将之前的页面改成自定义web弹窗的形式,并且完善群聊、私聊功能

  代码编写

  目前建了三个表,SQL如下:

  

/*
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: 09/05/2019 10:09:11
*/ 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',
`created_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`updata_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '好友表' ROW_FORMAT = Compact; -- ----------------------------
-- Table structure for ims_friend_message
-- ----------------------------
DROP TABLE IF EXISTS `ims_friend_message`;
CREATE TABLE `ims_friend_message` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`from_user_id` int(11) NULL DEFAULT NULL COMMENT '发消息的人的id',
`to_user_id` int(11) NULL DEFAULT NULL COMMENT '收消息的人的id',
`content` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '消息内容',
`is_read` int(11) NULL DEFAULT NULL COMMENT '是否已读,1是0否',
`is_back` int(11) NULL DEFAULT NULL COMMENT '是否撤回,1是0否',
`created_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`updata_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '好友消息表' ROW_FORMAT = Compact; -- ----------------------------
-- Table structure for ims_user
-- ----------------------------
DROP TABLE IF EXISTS `ims_user`;
CREATE TABLE `ims_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '帐号',
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
`nick_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称',
`gender` int(11) NULL DEFAULT 0 COMMENT '性别:0为男,1为女',
`avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像',
`email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '电子邮箱',
`phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号码',
`sign` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '个性签名',
`created_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`updata_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户信息表' ROW_FORMAT = Compact; SET FOREIGN_KEY_CHECKS = 1;

  自动生成代码,运行main方法执行

package cn.huanzi.ims.util;

import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.sql.*;
import java.util.ArrayList;
import java.util.List; /**
* 自动生成代码
*/
public class CodeDOM { /**
* 构造参数,出入表名
*/
private CodeDOM(String tableName) {
this.tableName = tableName;
basePackage_ = "cn\\huanzi\\ims\\";
package_ = basePackage_ + StringUtil.camelCaseName(tableName).toLowerCase() + "\\";
//System.getProperty("user.dir") 获取的是项目所在路径
basePath = System.getProperty("user.dir") + "\\src\\main\\java\\" + package_;
} /**
* 数据连接相关
*/
private static final String URL = "jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&characterEncoding=utf-8";
private static final String USERNAME = "root";
private static final String PASSWORD = "123456";
private static final String DRIVERCLASSNAME = "com.mysql.jdbc.Driver";
/**
* 表名
*/
private String tableName; /**
* 基础路径
*/
private String basePackage_;
private String package_;
private String basePath; /**
* 创建pojo实体类
*/
private void createPojo(List<TableInfo> tableInfos) {
File file = FileUtil.createFile(basePath + "pojo\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ".java");
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(
"package " + package_.replaceAll("\\\\", ".") + "pojo;\n" +
"\n" +
"import lombok.Data;\n" +
"import javax.persistence.*;\n" +
"import java.io.Serializable;\n" +
"import java.util.Date;\n" +
"\n" +
"@Entity\n" +
"@Table(name = \"" + tableName + "\")\n" +
"@Data\n" +
"public class " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + " implements Serializable {\n"
);
//遍历设置属性
for (TableInfo tableInfo : tableInfos) {
//主键
if ("PRI".equals(tableInfo.getColumnKey())) {
stringBuffer.append(" @Id\n");
}
//自增
if ("auto_increment".equals(tableInfo.getExtra())) {
stringBuffer.append(" @GeneratedValue(strategy= GenerationType.IDENTITY)\n");
}
stringBuffer.append(" private " + StringUtil.typeMapping(tableInfo.getDataType()) + " " + StringUtil.camelCaseName(tableInfo.getColumnName()) + ";//" + tableInfo.getColumnComment() + "\n\n");
}
stringBuffer.append("}");
FileUtil.fileWriter(file, stringBuffer);
} /**
* 创建vo类
*/
private void createVo(List<TableInfo> tableInfos) {
File file = FileUtil.createFile(basePath + "vo\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo.java");
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(
"package " + package_.replaceAll("\\\\", ".") + "vo;\n" +
"\n" +
"import lombok.Data;\n" +
"import java.io.Serializable;\n" +
"import java.util.Date;\n" +
"\n" +
"@Data\n" +
"public class " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo implements Serializable {\n"
);
//遍历设置属性
for (TableInfo tableInfo : tableInfos) {
stringBuffer.append(" private " + StringUtil.typeMapping(tableInfo.getDataType()) + " " + StringUtil.camelCaseName(tableInfo.getColumnName()) + ";//" + tableInfo.getColumnComment() + "\n\n");
}
stringBuffer.append("}");
FileUtil.fileWriter(file, stringBuffer);
} /**
* 创建repository类
*/
private void createRepository(List<TableInfo> tableInfos) {
File file = FileUtil.createFile(basePath + "repository\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Repository.java");
StringBuffer stringBuffer = new StringBuffer();
String t = "String";
//遍历属性
for (TableInfo tableInfo : tableInfos) {
//主键
if ("PRI".equals(tableInfo.getColumnKey())) {
t = StringUtil.typeMapping(tableInfo.getDataType());
}
}
stringBuffer.append(
"package " + package_.replaceAll("\\\\", ".") + "repository;\n" +
"\n" +
"import " + basePackage_.replaceAll("\\\\", ".") + "common.repository.*;\n" +
"import " + package_.replaceAll("\\\\", ".") + "pojo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ";\n" +
"import org.springframework.stereotype.Repository;\n" +
"\n" +
"@Repository\n" +
"public interface " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Repository extends CommonRepository<" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ", " + t + "> {"
);
stringBuffer.append("\n");
stringBuffer.append("}");
FileUtil.fileWriter(file, stringBuffer);
} /**
* 创建service类
*/
private void createService(List<TableInfo> tableInfos) {
File file = FileUtil.createFile(basePath + "service\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Service.java");
StringBuffer stringBuffer = new StringBuffer();
String t = "String";
//遍历属性
for (TableInfo tableInfo : tableInfos) {
//主键
if ("PRI".equals(tableInfo.getColumnKey())) {
t = StringUtil.typeMapping(tableInfo.getDataType());
}
}
stringBuffer.append(
"package " + package_.replaceAll("\\\\", ".") + "service;\n" +
"\n" +
"import " + basePackage_.replaceAll("\\\\", ".") + "common.service.*;\n" +
"import " + package_.replaceAll("\\\\", ".") + "pojo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ";\n" +
"import " + package_.replaceAll("\\\\", ".") + "vo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo;\n" +
"\n" +
"public interface " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Service extends CommonService<" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo, " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ", " + t + "> {"
);
stringBuffer.append("\n");
stringBuffer.append("}");
FileUtil.fileWriter(file, stringBuffer); //Impl
File file1 = FileUtil.createFile(basePath + "service\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "ServiceImpl.java");
StringBuffer stringBuffer1 = new StringBuffer();
stringBuffer1.append(
"package " + package_.replaceAll("\\\\", ".") + "service;\n" +
"\n" +
"import " + basePackage_.replaceAll("\\\\", ".") + "common.service.*;\n" +
"import " + package_.replaceAll("\\\\", ".") + "pojo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ";\n" +
"import " + package_.replaceAll("\\\\", ".") + "vo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo;\n" +
"import " + package_.replaceAll("\\\\", ".") + "repository." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Repository;\n" +
"import org.springframework.beans.factory.annotation.Autowired;\n" +
"import org.springframework.stereotype.Service;\n" +
"import org.springframework.transaction.annotation.Transactional;\n" +
"import javax.persistence.EntityManager;\n" +
"import javax.persistence.PersistenceContext;\n" +
"\n" +
"@Service\n" +
"@Transactional\n" +
"public class " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "ServiceImpl extends CommonServiceImpl<" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo, " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ", " + t + "> implements " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Service{"
);
stringBuffer1.append("\n\n");
stringBuffer1.append(
" @PersistenceContext\n" +
" private EntityManager em;\n"); stringBuffer1.append("" +
" @Autowired\n" +
" private " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Repository " + StringUtil.camelCaseName(tableName) + "Repository;\n");
stringBuffer1.append("}");
FileUtil.fileWriter(file1, stringBuffer1);
} /**
* 创建controller类
*/
private void createController(List<TableInfo> tableInfos) {
File file = FileUtil.createFile(basePath + "controller\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Controller.java");
StringBuffer stringBuffer = new StringBuffer();
String t = "String";
//遍历属性
for (TableInfo tableInfo : tableInfos) {
//主键
if ("PRI".equals(tableInfo.getColumnKey())) {
t = StringUtil.typeMapping(tableInfo.getDataType());
}
}
stringBuffer.append(
"package " + package_.replaceAll("\\\\", ".") + "controller;\n" +
"\n" +
"import " + basePackage_.replaceAll("\\\\", ".") + "common.controller.*;\n" +
"import " + package_.replaceAll("\\\\", ".") + "pojo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ";\n" +
"import " + package_.replaceAll("\\\\", ".") + "vo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo;\n" +
"import " + package_.replaceAll("\\\\", ".") + "service." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Service;\n" +
"import org.springframework.beans.factory.annotation.Autowired;\n" +
"import org.springframework.web.bind.annotation.*;\n" +
"\n" +
"@RestController\n" +
"@RequestMapping(\"/" + StringUtil.camelCaseName(tableName) + "/\")\n" +
"public class " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Controller extends CommonController<" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo, " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ", " + t + "> {"
);
stringBuffer.append("\n");
stringBuffer.append("" +
" @Autowired\n" +
" private " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Service " + StringUtil.camelCaseName(tableName) + "Service;\n");
stringBuffer.append("}");
FileUtil.fileWriter(file, stringBuffer);
} /**
* 获取表结构信息
*
* @return list
*/
private List<TableInfo> getTableInfo() {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
ArrayList<TableInfo> list = new ArrayList<>();
try {
conn = DBConnectionUtil.getConnection();
String sql = "select column_name,data_type,column_comment,column_key,extra from information_schema.columns where table_name=?";
ps = conn.prepareStatement(sql);
ps.setString(1, tableName);
rs = ps.executeQuery();
while (rs.next()) {
TableInfo tableInfo = new TableInfo();
//列名,全部转为小写
tableInfo.setColumnName(rs.getString("column_name").toLowerCase());
//列类型
tableInfo.setDataType(rs.getString("data_type"));
//列注释
tableInfo.setColumnComment(rs.getString("column_comment"));
//主键
tableInfo.setColumnKey(rs.getString("column_key"));
//主键类型
tableInfo.setExtra(rs.getString("extra"));
list.add(tableInfo);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
assert rs != null;
DBConnectionUtil.close(conn, ps, rs);
}
return list;
} /**
* file工具类
*/
private static class FileUtil {
/**
* 创建文件
*
* @param pathNameAndFileName 路径跟文件名
* @return File对象
*/
private static File createFile(String pathNameAndFileName) {
File file = new File(pathNameAndFileName);
try {
//获取父目录
File fileParent = file.getParentFile();
if (!fileParent.exists()) {
fileParent.mkdirs();
}
//创建文件
if (!file.exists()) {
file.createNewFile();
}
} catch (Exception e) {
file = null;
System.err.println("新建文件操作出错");
e.printStackTrace();
}
return file;
} /**
* 字符流写入文件
*
* @param file file对象
* @param stringBuffer 要写入的数据
*/
private static void fileWriter(File file, StringBuffer stringBuffer) {
//字符流
try {
FileWriter resultFile = new FileWriter(file, true);//true,则追加写入 false,则覆盖写入
PrintWriter myFile = new PrintWriter(resultFile);
//写入
myFile.println(stringBuffer.toString()); myFile.close();
resultFile.close();
} catch (Exception e) {
System.err.println("写入操作出错");
e.printStackTrace();
}
}
} /**
* 字符串处理工具类
*/
private static class StringUtil {
/**
* 数据库类型->JAVA类型
*
* @param dbType 数据库类型
* @return JAVA类型
*/
private static String typeMapping(String dbType) {
String javaType = "";
if ("int|integer".contains(dbType)) {
javaType = "Integer";
} else if ("float|double|decimal|real".contains(dbType)) {
javaType = "Double";
} else if ("date|time|datetime|timestamp".contains(dbType)) {
javaType = "Date";
} else {
javaType = "String";
}
return javaType;
} /**
* 驼峰转换为下划线
*/
public static String underscoreName(String camelCaseName) {
StringBuilder result = new StringBuilder();
if (camelCaseName != null && camelCaseName.length() > 0) {
result.append(camelCaseName.substring(0, 1).toLowerCase());
for (int i = 1; i < camelCaseName.length(); i++) {
char ch = camelCaseName.charAt(i);
if (Character.isUpperCase(ch)) {
result.append("_");
result.append(Character.toLowerCase(ch));
} else {
result.append(ch);
}
}
}
return result.toString();
} /**
* 首字母大写
*/
public static String captureName(String name) {
char[] cs = name.toCharArray();
cs[0] -= 32;
return String.valueOf(cs); } /**
* 下划线转换为驼峰
*/
public static String camelCaseName(String underscoreName) {
StringBuilder result = new StringBuilder();
if (underscoreName != null && underscoreName.length() > 0) {
boolean flag = false;
for (int i = 0; i < underscoreName.length(); i++) {
char ch = underscoreName.charAt(i);
if ("_".charAt(0) == ch) {
flag = true;
} else {
if (flag) {
result.append(Character.toUpperCase(ch));
flag = false;
} else {
result.append(ch);
}
}
}
}
return result.toString();
}
} /**
* JDBC连接数据库工具类
*/
private static class DBConnectionUtil { {
// 1、加载驱动
try {
Class.forName(DRIVERCLASSNAME);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} /**
* 返回一个Connection连接
*
* @return
*/
public static Connection getConnection() {
Connection conn = null;
// 2、连接数据库
try {
conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
} /**
* 关闭Connection,Statement连接
*
* @param conn
* @param stmt
*/
public static void close(Connection conn, Statement stmt) {
try {
conn.close();
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
} /**
* 关闭Connection,Statement,ResultSet连接
*
* @param conn
* @param stmt
* @param rs
*/
public static void close(Connection conn, Statement stmt, ResultSet rs) {
try {
close(conn, stmt);
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
} } /**
* 表结构行信息实体类
*/
private class TableInfo {
private String columnName;
private String dataType;
private String columnComment;
private String columnKey;
private String extra; TableInfo() {
} String getColumnName() {
return columnName;
} void setColumnName(String columnName) {
this.columnName = columnName;
} String getDataType() {
return dataType;
} void setDataType(String dataType) {
this.dataType = dataType;
} String getColumnComment() {
return columnComment;
} void setColumnComment(String columnComment) {
this.columnComment = columnComment;
} String getColumnKey() {
return columnKey;
} void setColumnKey(String columnKey) {
this.columnKey = columnKey;
} String getExtra() {
return extra;
} void setExtra(String extra) {
this.extra = extra;
}
} /**
* 快速创建,供外部调用,调用之前先设置一下项目的基础路径
*/
private String create() {
List<TableInfo> tableInfo = getTableInfo();
createPojo(tableInfo);
createVo(tableInfo);
createRepository(tableInfo);
createService(tableInfo);
createController(tableInfo);
return tableName + " 后台代码生成完毕!";
} public static void main(String[] args) {
String[] tables = {"ims_user", "ims_friend", "ims_friend_message"};
for (int i = 0; i < tables.length; i++) {
String msg = new CodeDOM(tables[i]).create();
System.out.println(msg);
}
}
}

CodeDOM.java

  工程结构

  工程是一个springboot项目,跟我们之前一样使用lombok、thymeleaf等

  tip.js、tip.css放在static的js、css里面

  我们在ims_user表生成的controller、service层新增登录、登出等几个接口

@RestController
@RequestMapping("/imsUser/")
public class ImsUserController extends CommonController<ImsUserVo, ImsUser, Integer> {
@Autowired
private ImsUserService imsUserService; /**
* 跳转登录、注册页面
*/
@RequestMapping("loginPage.html")
public ModelAndView loginPage() {
return new ModelAndView("login.html");
} /**
* 跳转聊天页面
*/
@RequestMapping("socketChart/{username}.html")
public ModelAndView socketChartPage(@PathVariable String username) {
return new ModelAndView("socketChart.html","username",username);
} /**
* 登录
*/
@PostMapping("login")
public Result<ImsUserVo> login(ImsUserVo userVo) {
//加密后再去对比密文
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.add(imsUserVo.getUserName());
return Result.of(imsUserVo);
}else{
return Result.of(null,false,"账号或密码错误!");
}
} /**
* 登出
*/
@RequestMapping("logout/{username}")
public String loginOut(HttpServletRequest request, @PathVariable String username) {
new WebSocketServer().deleteUserByUsername(username);
return "退出成功!";
} /**
* 获取在线用户
*/
@PostMapping("getOnlineList")
private List<String> getOnlineList(String username) {
List<String> list = new ArrayList<String>();
//遍历webSocketMap
for (Map.Entry<String, Session> entry : WebSocketServer.getSessionMap().entrySet()) {
if (!entry.getKey().equals(username)) {
list.add(entry.getKey());
}
}
return list;
}
}

ImsUserController.java

@Service
@Transactional
public class ImsUserServiceImpl extends CommonServiceImpl<ImsUserVo, ImsUser, Integer> implements ImsUserService{ @PersistenceContext
private EntityManager em;
@Autowired
private ImsUserRepository imsUserRepository; @Override
public Result<ImsUserVo> save(ImsUserVo entityVo) {
//先查询是否已经存在相同账号
ImsUserVo imsUserVo = new ImsUserVo();
imsUserVo.setUserName(entityVo.getUserName());
if(list(imsUserVo).getData().size() > 0){
return Result.of(null,false,"账号已存在!");
}
//存储密文
entityVo.setPassword(MD5Util.getMD5(entityVo.getPassword()));
return super.save(entityVo);
}
}

ImsUserServiceImpl.java

  WebSocketServer也有优化调整

package cn.huanzi.ims.socket;

import cn.huanzi.ims.imsuser.service.ImsUserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry; /**
* WebSocket服务
*/
@RestController
@RequestMapping("/websocket")
@ServerEndpoint(value = "/websocket/{username}", configurator = MyEndpointConfigure.class)
public class WebSocketServer { /**
* 在线人数
*/
private static int onlineCount = 0; /**
* 在线用户的Map集合,key:用户名,value:Session对象
*/
private static Map<String, Session> sessionMap = new HashMap<String, Session>(); /**
* 登录用户集合
*/
public static List<String> loginList = new ArrayList<>(); public static Map<String, Session> getSessionMap(){
return sessionMap;
} /**
* 注入其他类(换成自己想注入的对象)
*/
private ImsUserService imsUserService; /**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("username") String username) {
//在webSocketMap新增上线用户
sessionMap.put(username, session); //在线人数加加
WebSocketServer.onlineCount++; //通知除了自己之外的所有人
sendOnlineCount(username, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + username + "'}");
} /**
* 连接关闭调用的方法
*/
@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);
} /**
* 服务器接收到客户端消息时调用的方法
*/
@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("username").equals(tarUser.get("username"))) {
//群聊
groupChat(session,hashMap);
} else {
//私聊
privateChat(session, tarUser, hashMap);
} //后期要做消息持久化 } catch (IOException e) {
e.printStackTrace();
}
} /**
* 发生错误时调用
*/
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
} /**
* 通知除了自己之外的所有人
*/
private void sendOnlineCount(String username, String message) {
for (Entry<String, Session> entry : sessionMap.entrySet()) {
try {
if (entry.getKey() != username) {
entry.getValue().getBasicRemote().sendText(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
} /**
* 私聊
*/
private void privateChat(Session session, Map tarUser, HashMap hashMap) throws IOException {
//获取目标用户的session
Session tarUserSession = sessionMap.get(tarUser.get("username")); //如果不在线则发送“对方不在线”回来源用户
if (tarUserSession == null) {
session.getBasicRemote().sendText("{\"type\":\"0\",\"message\":\"对方不在线\"}");
} else {
hashMap.put("type", "1");
tarUserSession.getBasicRemote().sendText(new ObjectMapper().writeValueAsString(hashMap));
}
} /**
* 群聊
*/
private void groupChat(Session session, HashMap hashMap) throws IOException {
for (Entry<String, Session> entry : sessionMap.entrySet()) {
//自己就不用再发送消息了
if (entry.getValue() != session) {
hashMap.put("type", "2");
entry.getValue().getBasicRemote().sendText(new ObjectMapper().writeValueAsString(hashMap));
}
}
} /**
删除用户
*/
public void deleteUserByUsername(String username){
//在线人数减减
WebSocketServer.onlineCount--; WebSocketServer.loginList.remove(username); //通知除了自己之外的所有人
sendOnlineCount(username, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + username + "'}");
} }

  先看一下我们的自定义web弹窗的js、css,跟之前相比有一点小升级

/* web弹窗 */
.tip-msg {
background-color: rgba(61, 61, 61, 0.93);
color: #ffffff;
opacity:;
max-width: 200px;
position: fixed;
text-align: center;
line-height: 25px;
border-radius: 30px;
padding: 5px 15px;
display: inline-block;
z-index:;
} .tip-shade {
z-index:;
background-color: rgb(0, 0, 0);
opacity: 0.6;
position: fixed;
top:;
left:;
width: 100%;
height: 100%;
} .tip-dialog {
z-index:;
position: fixed;
display: block;
background: #e9e9e9;
border-radius: 5px;
opacity:;
border: 1px solid #dad8d8;
box-shadow: 0px 1px 20px 2px rgb(255, 221, 221);
} .tip-title {
cursor: move;
padding: 5px;
position: relative;
height: 25px;
border-bottom: 1px solid #dad8d8;
user-select: none;
} .tip-title-text {
margin:;
padding:;
font-size: 15px;
} .tip-title-btn {
position: absolute;
top: 5px;
right: 5px;
} .tip-content {
padding: 8px;
position: relative;
word-break: break-all;
font-size: 14px;
overflow-x: hidden;
overflow-y: auto;
} .tip-resize {
position: absolute;
width: 15px;
height: 15px;
right:;
bottom:;
cursor: se-resize;
}

tip.css

/**
* 自定义web弹窗/层:简易风格的msg与可拖放的dialog
* 依赖jquery
*/
var tip = { /**
* 初始化
*/
init: function () {
var titleDiv = null;//标题元素
var dialogDiv = null;//窗口元素
var titleDown = false;//是否在标题元素按下鼠标
var resizeDown = false;//是否在缩放元素按下鼠标
var offset = {x: 0, y: 0};//鼠标按下时的坐标系/计算后的坐标
/*
使用 on() 方法添加的事件处理程序适用于当前及未来的元素(比如由脚本创建的新元素)。
问题:事件绑定在div上出现div移动速度跟不上鼠标速度,导致鼠标移动太快时会脱离div,从而无法触发事件。
解决:把事件绑定在document文档上,无论鼠标在怎么移动,始终是在文档范围之内。
*/
//鼠标在标题元素按下
$(document).on("mousedown", ".tip-title", function (e) {
var event1 = e || window.event;
titleDiv = $(this);
dialogDiv = titleDiv.parent();
titleDown = true;
offset.x = e.clientX - parseFloat(dialogDiv.css("left"));
offset.y = e.clientY - parseFloat(dialogDiv.css("top"));
});
//鼠标移动
$(document).on("mousemove", function (e) {
var event2 = e || window.event;
var eveX = event2.clientX; // 获取鼠标相对于浏览器x轴的位置
var eveY = event2.clientY; // 获取鼠标相对于浏览器Y轴的位置
// var height = document.body.clientHeight;//表示HTML文档所在窗口的当前高度;
// var width = document.body.clientWidth;//表示HTML文档所在窗口的当前宽度;
var height = window.innerHeight;//浏览器窗口的内部高度;
var width = window.innerWidth;//浏览器窗口的内部宽度; //在标题元素按下
if (titleDown) { //处理滚动条
if (tip.hasXScrollbar()) {
height = height - tip.getScrollbarWidth();
}
if (tip.hasYScrollbar()) {
width = width - tip.getScrollbarWidth();
} //上边
var top = (eveY - offset.y);
if (top <= 0) {
top = 0;
}
if (top >= (height - dialogDiv.height())) {
top = height - dialogDiv.height() - 5;
} //左边
var left = (eveX - offset.x);
if (left <= 0) {
left = 0;
}
if (left >= (width - dialogDiv.width())) {
left = width - dialogDiv.width() - 5;
}
dialogDiv.css({
"top": top + "px",
"left": left + "px"
});
} //在缩放元素按下
if (resizeDown) {
var newWidth = (dialogDiv.resize.width + (eveX - offset.x));
if (dialogDiv.resize.initWidth >= newWidth) {
newWidth = dialogDiv.resize.initWidth;
}
var newHeight = (dialogDiv.resize.height + (eveY - offset.y));
if (dialogDiv.resize.initHeight >= newHeight) {
newHeight = dialogDiv.resize.initHeight;
} dialogDiv.css("width", newWidth + "px");
dialogDiv.find(".tip-content").css("height", newHeight + "px");
}
});
//鼠标弹起
$(document).on("mouseup", function (e) {
//清空对象
titleDown = false;
resizeDown = false;
titleDiv = null;
dialogDiv = null;
offset = {x: 0, y: 0};
});
//阻止按钮事件冒泡
$(document).on("mousedown", ".tip-title-min,.tip-title-max,.tip-title-close", function (e) {
e.stopPropagation();//阻止事件冒泡
});
//最小化
$(document).on("click", ".tip-title-min", function (e) {
// var height = document.body.clientHeight;//表示HTML文档所在窗口的当前高度;
// var width = document.body.clientWidth;//表示HTML文档所在窗口的当前宽度;
var height = window.innerHeight;//浏览器窗口的内部高度;
var width = window.innerWidth;//浏览器窗口的内部宽度;
var $parent = $(this).parents(".tip-dialog");
//显示浏览器滚动条
document.body.parentNode.style.overflowY = "auto"; //当前是否为最大化
if ($parent[0].isMax) {
$parent[0].isMax = false;
$parent.css({
"top": $parent[0].topMin,
"left": $parent[0].leftMin,
"height": $parent[0].heightMin,
"width": $parent[0].widthMin
});
}
//当前是否为最小化
if (!$parent[0].isMin) {
$parent[0].isMin = true;
$parent[0].bottomMin = $parent.css("bottom");
$parent[0].leftMin = $parent.css("left");
$parent[0].heightMin = $parent.css("height");
$parent[0].widthMin = $parent.css("width");
$parent.css({
"top": "",
"bottom": "5px",
"left": 0,
"height": "30px",
"width": "95px"
});
$parent.find(".tip-title-text").css("display", "none");
$parent.find(".tip-content").css("display", "none");
} else {
$parent[0].isMin = false;
$parent.css({
"top": $parent[0].topMin,
"bottom": $parent[0].bottomMin,
"left": $parent[0].leftMin,
"height": $parent[0].heightMin,
"width": $parent[0].widthMin
});
$parent.find(".tip-title-text").css("display", "block");
$parent.find(".tip-content").css("display", "block");
}
});
//最大化
$(document).on("click", ".tip-title-max", function (e) {
// var height = document.body.clientHeight;//表示HTML文档所在窗口的当前高度;
// var width = document.body.clientWidth;//表示HTML文档所在窗口的当前宽度;
var height = window.innerHeight;//浏览器窗口的内部高度;
var width = window.innerWidth;//浏览器窗口的内部宽度;
var $parent = $(this).parents(".tip-dialog");
//当前是否为最小化
if ($parent[0].isMin) {
$parent[0].isMin = false;
$parent.css({
"top": $parent[0].topMin,
"bottom": $parent[0].bottomMin,
"left": $parent[0].leftMin,
"height": $parent[0].heightMin,
"width": $parent[0].widthMin
});
$parent.find(".tip-title h2").css("display", "block");
}
//当前是否为最大化
if (!$parent[0].isMax) {
//隐藏浏览器滚动条
document.body.parentNode.style.overflowY = "hidden";
$parent[0].isMax = true;
$parent[0].topMin = $parent.css("top");
$parent[0].leftMin = $parent.css("left");
$parent[0].heightMin = $parent.css("height");
$parent[0].widthMin = $parent.css("width");
$parent.css({
"top": 0,
"left": 0,
"height": height - 5 + "px",
"width": width - 5 + "px"
});
} else {
//显示浏览器滚动条
document.body.parentNode.style.overflowY = "auto";
$parent[0].isMax = false;
$parent.css({
"top": $parent[0].topMin,
"left": $parent[0].leftMin,
"height": $parent[0].heightMin,
"width": $parent[0].widthMin
});
}
});
//缩放
$(document).on("mousedown", ".tip-resize", function (e) {
var event1 = e || window.event;
dialogDiv = $(this).parent();
resizeDown = true;
offset.x = e.clientX;
offset.y = e.clientY;
//点击时的宽高
dialogDiv.resize.width = dialogDiv.width();
dialogDiv.resize.height = dialogDiv.find(".tip-content").height();
});
//关闭
$(document).on("click", ".tip-title-close", function (e) {
$(this).parents(".tip-dialog").parent().remove();
//显示浏览器滚动条
document.body.parentNode.style.overflowY = "auto";
});
//点击窗口优先显示
$(document).on("click", ".tip-dialog", function (e) {
$(".tip-dialog").css("z-index","9999");
$(this).css("z-index","10000");
});
}, /**
* 是否存在X轴方向滚动条
*/
hasXScrollbar: function () {
return document.body.scrollWidth > (window.innerWidth || document.documentElement.clientWidth);
}, /**
* 是否存在Y轴方向滚动条
*/
hasYScrollbar: function () {
return document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight);
}, /**
* 计算滚动条的宽度
*/
getScrollbarWidth: function () {
/*
思路:生成一个带滚动条的div,分析得到滚动条长度,然后过河拆桥
*/
var scrollDiv = document.createElement("div");
scrollDiv.style.cssText = 'width: 99px; height: 99px; overflow: scroll; position: absolute; top: -9999px;';
document.body.appendChild(scrollDiv);
var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
document.body.removeChild(scrollDiv); return scrollbarWidth; }, /**
* tip提示
* tip.msg("哈哈哈哈哈");
* tip.msg({text:"哈哈哈哈哈",time:5000});
*/
msg: function (setting) {
var time = setting.time || 2000; // 显示时间(毫秒) 默认延迟2秒关闭
var text = setting.text || setting; // 文本内容 //组装HTML
var tip = "<div class='tip tip-msg'>"
+ text +
"</div>"; //删除旧tip
$(".tip-msg").remove(); //添加到body
$("body").append(tip); //获取jq对象
var $tip = $(".tip-msg"); //动画过渡
$tip.animate({opacity: 1}, 500); //计算位置浏览器窗口上下、左右居中
// var height = document.body.clientHeight;//表示HTML文档所在窗口的当前高度;
var width = document.body.clientWidth;//表示HTML文档所在窗口的当前宽度;
var height = window.innerHeight;//浏览器窗口的内部高度;
// var width = window.innerWidth;//浏览器窗口的内部宽度;
width = ((width / 2) - ($tip.css("width").replace("px", "") / 2)) / width;
height = ((height / 2) - ($tip.css("height").replace("px", "") / 2)) / height;
$tip.css({
"top": (height * 100) + "%",
"left": (width * 100) + "%"
}); //延迟删除
setTimeout(function () {
//动画过渡
$tip.animate({opacity: 0}, 500, function () {
$tip.remove();
});
}, time);
}, /**
* 可拖放窗口
* tip.dialog({title:"测试弹窗标题",content:"测试弹窗内容"});
* tip.dialog({title:"测试弹窗标题",class:"myClassName",content:"<h1>测试弹窗内容</h1>",offset: ['100px', '50px'],area:["200px","100px"],shade:0,closeCallBack:function(){console.log('你点击了关闭按钮')}});
*/
dialog: function (setting) {
var title = setting.title || "这里是标题"; // 标题
var clazz = setting.class || ""; // class
var content = setting.content || "这里是内容"; // 内容
var area = setting.area; // 宽高
var offset = setting.offset || "auto"; // 位置 上、左
var shade = setting.shade !== undefined ? setting.shade : 0.7;//遮阴 为0时无遮阴对象 //组装HTML
var tip = "<div>\n" +
" <!-- 遮阴层 -->\n" +
" <div class=\"tip tip-shade\"></div>\n" +
" <!-- 主体 -->\n" +
" <div class=\"tip tip-dialog " + clazz + "\">\n" +
" <!-- 标题 -->\n" +
" <div class=\"tip tip-title\">\n" +
" <h2 class=\"tip tip-title-text\"></h2>\n" +
" <div class=\"tip tip-title-btn\">\n" +
" <button class=\"tip tip-title-min\" title=\"最小化\">--</button>\n" +
" <button class=\"tip tip-title-max\" title=\"最大化\">O</button>\n" +
" <button class=\"tip tip-title-close\" title=\"关闭\">X</button>\n" +
" </div>\n" +
" </div>\n" +
" <!-- 窗口内容 -->\n" +
" <div class=\"tip tip-content\"></div>\n" +
" <!-- 右下角改变窗口大小 -->\n" +
" <div class=\"tip tip-resize\"></div>\n" +
" </div>\n" +
"</div>"; var $tip = $(tip); //添加到body
$("body").append($tip); //设置遮阴
$tip.find(".tip-shade").css("opacity", shade);
if (shade === 0) {
$tip.find(".tip-shade").css({
"width": "0",
"height": "0"
});
} //获取dialog对象
$tip = $tip.find(".tip-dialog"); //标题
$tip.find(".tip-title-text").html(title); //内容
$tip.find(".tip-content").append(content); //设置初始宽高
if (area) {
$tip.css({
"width": area[0],
});
$tip.find(".tip-content").css({
"height": area[1]
});
} //动画过渡
$tip.animate({opacity: 1}, 500); //计算位置浏览器窗口上下、左右居中
if (offset === "auto") {
// var height = document.body.clientHeight;//表示HTML文档所在窗口的当前高度;
var width = document.body.clientWidth;//表示HTML文档所在窗口的当前宽度;
var height = window.innerHeight;//浏览器窗口的内部高度;
// var width = window.innerWidth;//浏览器窗口的内部宽度;
width = ((width / 2) - ($tip.css("width").replace("px", "") / 2)) / width;
height = ((height / 2) - ($tip.css("height").replace("px", "") / 2)) / height;
$tip.css({
"top": (height * 100) + "%",
"left": (width * 100) + "%"
});
} else if (Array.isArray(offset)) {
$tip.css({
"top": offset[0],
"left": offset[1]
});
} //初始值宽高
$tip.resize.initWidth = $tip.width();
$tip.resize.initHeight = $tip.find(".tip-content").height(); //绑定关闭回调
if(setting.closeCallBack){
$(".tip-title-close").click(function (e) {
setting.closeCallBack();
});
}
}
}; //初始化
tip.init();

tip.js

  接下来就是登录/注册页面跟聊天页面的HTML、JS的修改,我们先定义一个head.html作为一个公用head在其他地方引入

<!--此页面用于放置页面的公共片段(fragment)-->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="static">
<head>
<script th:inline="javascript">
//项目根路径
// ctx = /*[[@{/}]]*/'';
ctx = [[${#request.getContextPath()}]];//应用路径
</script>
</head> <head>
<!-- jquery -->
<script th:src="@{/js/jquery.min.js}"></script> <!-- 自定义web弹窗 CSS、JS 文件 -->
<link th:href="@{/css/tip.css}" rel="stylesheet" type="text/css"/>
<script th:src="@{/js/tip.js}"></script>
</head> <head>
<style>
body, html {
margin: 0;
padding: 0;
background: #898f92;
}
</style>
</head> <head>
<script>
/**
* 拓展表单对象:用于将对象序列化为JSON对象
*/
$.fn.serializeObject = function () {
var o = {};
var a = this.serializeArray();
$.each(a, function () {
if (o[this.name]) {
if (!o[this.name].push) {
o[this.name] = [o[this.name]];
}
o[this.name].push(this.value || '');
} else {
o[this.name] = this.value || '';
}
});
return o;
}; /**
* 表单自动回显
* 依赖jqury
* 使用参考:$("#form1").form({"id":"112","username":"ff","password":"111","type":"admin"});
*/
$.fn.form = function (data) {
var form = $(this);
for (var i in data) {
var name = i;
var value = data[i];
if (name !== "" && value !== "") {
valuAtion(name, value);
}
} function valuAtion(name, value) {
if (form.length < 1) {
return;
}
if (form.find("[name='" + name + "']").length < 1) {
return;
}
var input = form.find("[name='" + name + "']")[0];
if ($.inArray(input.type, ["text", "password", "hidden", "select-one", "textarea"]) > -1) {
$(input).val(value);
} else if (input.type == " " || input.type == "checkbox") {
form.find("[name='" + name + "'][value='" + value + "']").attr("checked", true);
}
} }; /**
* 常用工具方法
*/
commonUtil = {
/**
* 获取当前时间,并格式化输出为:2018-05-18 14:21:46
* @returns {string}
*/
getNowTime: function () {
var time = new Date();
var year = time.getFullYear();//获取年
var month = time.getMonth() + 1;//或者月
var day = time.getDate();//或者天 var hour = time.getHours();//获取小时
var minu = time.getMinutes();//获取分钟
var second = time.getSeconds();//或者秒
var data = year + "-";
if (month < 10) {
data += "0";
}
data += month + "-";
if (day < 10) {
data += "0"
}
data += day + " ";
if (hour < 10) {
data += "0"
} data += hour + ":";
if (minu < 10) {
data += "0"
}
data += minu + ":";
if (second < 10) {
data += "0"
}
data += second;
return data;
}
}
</script>
</head> </html>

head.html

<!DOCTYPE >
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>登录/注册</title>
<!-- 引入公用部分 -->
<script th:replace="head::static"></script> <!-- login CSS、JS 文件 -->
<link th:href="@{/css/login.css}" rel="stylesheet" type="text/css"/>
</head>
<body> <script th:inline="javascript">
</script>
<!-- login CSS、JS 文件 -->
<script th:src="@{/js/login.js}"></script>
</body>
</html>

login.html

h3 {
margin: 0 0 5px 0;
text-align: center;
} .login {
width: 250px;
background: #e9e9e9;
border-radius: 5px;
margin: 0 auto;
border: 1px solid #e9e9e9;
padding: 10px;
} .login > form {
margin:;
} .login:focus-within {
border: 1px solid #10b7f3;
background: #caefff;
} .register {
width: 350px;
background: #e9e9e9;
border-radius: 5px;
margin: 0 auto;
border: 1px solid #e9e9e9;
padding: 10px;
display: none;
} .register > form {
margin:;
} .register > table,.login > table {
margin: 0 auto;
} .register:focus-within {
border: 1px solid #10b7f3;
background: #caefff;
}

login.css

let $login = "<div class=\"login\">\n" +
" <h3>登录</h3>\n" +
" <form id=\"loginForm\">\n" +
" <table>\n" +
" <tr>\n" +
" <td>账号:</td>\n" +
" <td><input type=\"text\" name=\"userName\"/></td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td>密码:</td>\n" +
" <td><input type=\"text\" name=\"password\"/></td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td><a href=\"#\" onclick=\"switchover()\">注册</a></td>\n" +
" <td colspan=\"2\"><a href=\"#\" onclick=\"login()\">登录</a></td>\n" +
" </tr>\n" +
" </table>\n" +
" </form>\n" +
"</div>"; let $register = "<!-- 注册 -->\n" +
"<div class=\"register\">\n" +
" <h3>注册</h3>\n" +
"\n" +
" <form id=\"registerForm\">\n" +
" <table>\n" +
" <tr>\n" +
" <td>账号:</td>\n" +
" <td><label for=\"userName\"></label><input type=\"text\" id=\"userName\" name=\"userName\"/></td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td>密码:</td>\n" +
" <td><input type=\"text\" id=\"password\" name=\"password\"/></td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td>昵称:</td>\n" +
" <td><input type=\"text\" id=\"nickName\" name=\"nickName\"/></td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td>性别:</td>\n" +
" <td>\n" +
" 男<input type=\"radio\" name=\"gender\" value=\"1\" checked/>\n" +
" 女<input type=\"radio\" name=\"gender\" value=\"0\"/>\n" +
" </td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td>头像:</td>\n" +
" <td><input type=\"text\" id=\"avatar\" name=\"avatar\"/></td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td>电子邮箱:</td>\n" +
" <td><input type=\"email\" id=\"email\" name=\"email\"/></td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td>手机号码:</td>\n" +
" <td><input type=\"text\" id=\"phone\" name=\"phone\"/></td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td>个性签名:</td>\n" +
" <td><textarea id=\"sign\" name=\"sign\"></textarea></td>\n" +
" </tr>\n" +
" <!-- 两个隐藏时间 -->\n" +
" <input type=\"hidden\" id=\"createdTime\" name=\"createdTime\"/>\n" +
" <input type=\"hidden\" id=\"updataTime\" name=\"updataTime\"/>\n" +
" <tr>\n" +
" <td><a href=\"#\" onclick=\"switchover()\">登录</a></td>\n" +
" <td colspan=\"2\"><a href=\"#\" onclick=\"register()\">注册</a></td>\n" +
" </tr>\n" +
" </table>\n" +
" </form>\n" +
"</div>"; tip.dialog({title: "登录/注册", content: $login + $register, shade: 0}); //切换登录、注册页面
function switchover() {
if ($(".login").css("display") === "none") {
$(".login").show();
$(".register").hide();
} else {
$(".register").show();
$(".login").hide();
}
} //提交注册
function register() {
let newTime = commonUtil.getNowTime();
$("#createdTime").val(newTime);
$("#updataTime").val(newTime);
$("#avatar").val("/image/logo.png"); console.log($("#registerForm").serializeObject());
$.post(ctx + "/imsUser/save", $("#registerForm").serializeObject(), function (data) {
if (data.flag) {
switchover();
}
// tip提示
tip.msg(data.msg);
});
} //提交登录
function login() {
console.log($("#loginForm").serializeObject());
$.post(ctx + "/imsUser/login", $("#loginForm").serializeObject(), function (data) {
if (data.flag) {
window.location.href = ctx + "/imsUser/socketChart/" + data.data.userName + ".html"
} else {
// tip提示
tip.msg(data.msg);
}
});
return false;
}

login.js

<!DOCTYPE>
<!--解决idea thymeleaf 表达式模板报红波浪线-->
<!--suppress ALL -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>聊天页面</title>
<!-- 引入公用部分 -->
<script th:replace="head::static"></script> <!-- socketChart CSS、JS 文件 -->
<link th:href="@{/css/socketChart.css}" rel="stylesheet" type="text/css"/>
</head>
<body> </body>
<script type="text/javascript" th:inline="javascript">
//登录名
var username = /*[[${username}]]*/'';
</script> <!-- socketChart CSS、JS 文件 -->
<script th:src="@{/js/socketChart.js}"></script> <script type="text/javascript">
//老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象
if (navigator.mediaDevices === undefined) {
navigator.mediaDevices = {};
}
//一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia
//因为这样可能会覆盖已有的属性。这里我们只会在没有getUserMedia属性的时候添加它。
if (navigator.mediaDevices.getUserMedia === undefined) {
navigator.mediaDevices.getUserMedia = function (constraints) { // 首先,如果有getUserMedia的话,就获得它
var getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; // 一些浏览器根本没实现它 - 那么就返回一个error到promise的reject来保持一个统一的接口
if (!getUserMedia) {
return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
} // 否则,为老的navigator.getUserMedia方法包裹一个Promise
return new Promise(function (resolve, reject) {
getUserMedia.call(navigator, constraints, resolve, reject);
});
}
}
var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; //开启视频
$(document).on("click", "#videoBut", function (event) {
console.log("开始与" + $("#toUserName").text() + "视频聊天...");
var username = $("#toUserName").text();
// 创建PeerConnection实例 (参数为null则没有iceserver,即使没有stunserver和turnserver,仍可在局域网下通讯)
var pc = new webkitRTCPeerConnection(null); // 发送ICE候选到其他客户端
pc.onicecandidate = function (event) {
if (event.candidate !== null) {
//转json字符串
websocket.send(JSON.stringify({
"event": "_ice_candidate",
"data": {
"candidate": event.candidate
}
}));
websocket.send(JSON.stringify({
"type": "1",
"tarUser": {"username": tarUserName},
"srcUser": {"username": srcUserName},
"message": message
}));
}
};
});
</script>
</html>

socketChart.html

//追加tip页面
let hzGroup = "" +
"<div id=\"hz-group\">\n" +
" 在线人数:<span id=\"onlineCount\">0</span>\n" +
" <!-- 主体 -->\n" +
" <div id=\"hz-group-body\">\n" +
"\n" +
" </div>\n" +
" </div>";
tip.dialog({title: "<span id=\"talks\">" + username + "</span>", content: hzGroup, offset: ["10%", "80%"], shade: 0,
closeCallBack: function () {
console.log("dfasdfasd")
window.location.href = ctx + "/imsUser/logout/" + username;
}}); //聊天页面
let hzMessage = "" +
" <div id=\"hz-message\">\n" +
" <!-- 主体 -->\n" +
" <div id=\"hz-message-body\">\n" +
" </div>\n" +
" <!-- 功能条 -->\n" +
" <div id=\"\">\n" +
" <button>表情</button>\n" +
" <button>图片</button>\n" +
" <button id=\"videoBut\">视频</button>\n" +
" <button onclick=\"send(this)\" style=\"float: right;\">发送</button>\n" +
" </div>\n" +
" <!-- 输入框 -->\n" +
" <div contenteditable=\"true\" id=\"hz-message-input\">\n" +
"\n" +
" </div>\n" +
" </div>"; //消息对象数组
var msgObjArr = []; //websocket对象
var websocket = null; //判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:10086/websocket/" + username);
} else {
console.error("不支持WebSocket");
} //连接发生错误的回调方法
websocket.onerror = function (e) {
console.error("WebSocket连接发生错误");
}; //连接成功建立的回调方法
websocket.onopen = function () {
//获取所有在线用户
$.ajax({
type: 'post',
url: ctx + "/imsUser/getOnlineList",
contentType: 'application/json;charset=utf-8',
dataType: 'json',
data: {username: username},
success: function (data) {
if (data.length) {
//列表
for (let i = 0; i < data.length; i++) {
var userName = data[i];
var text = "<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'>" + userName + "</span><span id=\"" + userName + "-status\" style='color: #497b0f;;'>[在线]</span><div id=\"hz-badge-" + userName + "\" class='hz-badge'>0</div></div>";
//把自己排在第一个
if (username === userName) {
$("#hz-group-body").prepend(text);
} else {
$("#hz-group-body").append(text);
}
} //在线人数
$("#onlineCount").text(data.length);
}
},
error: function (xhr, status, error) {
console.log("ajax错误!");
}
});
}; //接收到消息的回调方法
websocket.onmessage = function (event) {
var messageJson = eval("(" + event.data + ")"); //普通消息(私聊)
if (messageJson.type === "1") {
//来源用户
var srcUser = messageJson.srcUser;
//目标用户
var tarUser = messageJson.tarUser;
//消息
var message = messageJson.message; //最加聊天数据
setMessageInnerHTML(srcUser.username, srcUser.username, message);
} //普通消息(群聊)
if (messageJson.type === "2") {
//来源用户
var srcUser = messageJson.srcUser;
//目标用户
var tarUser = messageJson.tarUser;
//消息
var message = messageJson.message; //最加聊天数据
setMessageInnerHTML(username, tarUser.username, message);
} //对方不在线
if (messageJson.type === "0") {
//消息
var message = messageJson.message; $("#hz-message-body").append(
"<div class=\"hz-message-list\" style='text-align: center;'>" +
"<div class=\"hz-message-list-text\">" +
"<span>" + message + "</span>" +
"</div>" +
"</div>");
} //在线人数
if (messageJson.type === "onlineCount") {
//取出username
var onlineCount = messageJson.onlineCount;
var userName = messageJson.username;
var oldOnlineCount = $("#onlineCount").text(); //新旧在线人数对比
if (oldOnlineCount < onlineCount) {
if ($("#" + userName + "-status").length > 0) {
$("#" + userName + "-status").text("[在线]");
$("#" + userName + "-status").css("color", "#497b0f");
} else {
$("#hz-group-body").append("<div class=\"hz-group-list\"><span class='hz-group-list-username'>" + userName + "</span><span id=\"" + userName + "-status\" style='color: #497b0f;'>[在线]</span><div id=\"hz-badge-" + userName + "\" class='hz-badge'>0</div></div>");
}
} else {
//有人下线
$("#" + userName + "-status").text("[离线]");
$("#" + userName + "-status").css("color", "#9c0c0c");
}
$("#onlineCount").text(onlineCount);
} }; //连接关闭的回调方法
websocket.onclose = function () {
//alert("WebSocket连接关闭");
}; //将消息显示在对应聊天窗口 对于接收消息来说这里的toUserName就是来源用户,对于发送来说则相反
function setMessageInnerHTML(srcUserName, msgUserName, message) {
//判断
var childrens = $("#hz-group-body").children(".hz-group-list");
var isExist = false;
for (var i = 0; i < childrens.length; i++) {
var text = $(childrens[i]).find(".hz-group-list-username").text();
if (text == srcUserName) {
isExist = true;
break;
}
}
if (!isExist) {
//追加聊天对象
msgObjArr.push({
toUserName: srcUserName,
message: [{username: msgUserName, message: message, date: nowTime()}]//封装数据
});
$("#hz-group-body").append("<div class=\"hz-group-list\"><span class='hz-group-list-username'>" + srcUserName + "</span><span id=\"" + srcUserName + "-status\">[在线]</span><div id=\"hz-badge-" + srcUserName + "\" class='hz-badge'>0</div></div>");
} else {
//取出对象
var isExist = false;
for (var i = 0; i < msgObjArr.length; i++) {
var obj = msgObjArr[i];
if (obj.toUserName == srcUserName) {
//保存最新数据
obj.message.push({username: msgUserName, message: message, date: nowTime()});
isExist = true;
break;
}
}
if (!isExist) {
//追加聊天对象
msgObjArr.push({
toUserName: srcUserName,
message: [{username: msgUserName, message: message, date: nowTime()}]//封装数据
});
}
} //刚好有打开的是对应的聊天页面
if ($(".tip" + srcUserName).length > 0) {
$(".tip" + srcUserName + " #hz-message-body").append(
"<div class=\"hz-message-list\">" +
"<p class='hz-message-list-username'>" + msgUserName + "</p>" +
"<img class='left' style='width: 23px;margin: 0 5px 0 0;' src='https://avatars3.githubusercontent.com/u/31408183?s=40&v=4'/>" +
"<div class=\"hz-message-list-text left\">" +
"<span>" + message + "</span>" +
"</div>" +
"<div style=\" clear: both; \"></div>" +
"</div>");
} else {
//小圆点++
var conut = $("#hz-badge-" + srcUserName).text();
$("#hz-badge-" + srcUserName).text(parseInt(conut) + 1);
$("#hz-badge-" + srcUserName).css("opacity", "1");
}
} //发送消息
function send(but) {
//目标用户名
var tarUserName = $(but).parents(".tip-dialog").find("#toUserName").text();
//登录用户名
var srcUserName = $("#talks").text();
//消息
var message = $(but).parents(".tip-dialog").find("#hz-message-input").html(); websocket.send(JSON.stringify({
"type": "1",
"tarUser": {"username": tarUserName},
"srcUser": {"username": srcUserName},
"message": message
}));
$(".tip" + tarUserName + " #hz-message-body").append(
"<div class=\"hz-message-list\">" +
"<img class='right' style='width: 23px;margin: 0 0 0 5px;' src='https://avatars3.githubusercontent.com/u/31408183?s=40&v=4'/>" +
"<div class=\"hz-message-list-text right\">" +
"<span>" + message + "</span>" +
"</div>" +
"</div>");
$(".tip" + tarUserName + " #hz-message-input").html("");
//取出对象
if (msgObjArr.length > 0) {
var isExist = false;
for (var i = 0; i < msgObjArr.length; i++) {
var obj = msgObjArr[i];
if (obj.toUserName == tarUserName) {
//保存最新数据
obj.message.push({username: srcUserName, message: message, date: nowTime()});
isExist = true;
break;
}
}
if (!isExist) {
//追加聊天对象
msgObjArr.push({
toUserName: tarUserName,
message: [{username: srcUserName, message: message, date: nowTime()}]//封装数据[{username:huanzi,message:"你好,我是欢子!",date:2018-04-29 22:48:00}]
});
}
} else {
//追加聊天对象
msgObjArr.push({
toUserName: tarUserName,
message: [{username: srcUserName, message: message, date: nowTime()}]//封装数据[{username:huanzi,message:"你好,我是欢子!",date:2018-04-29 22:48:00}]
});
}
} //监听点击用户
$("body").on("click", ".hz-group-list", function () {
var toUserName = $(this).find(".hz-group-list-username").text();
//弹出聊天页面
tip.dialog({
title: "正在与 <span id=\"toUserName\"></span> 聊天",
class: "tip" + toUserName,
content: hzMessage,
shade: 0
}); // $(".hz-group-list").css("background-color", "");
// $(this).css("background-color", "whitesmoke");
$(".tip" + toUserName + " #toUserName").text(toUserName); //清空小圆点
$("#hz-badge-" + toUserName).text("0");
$("#hz-badge-" + toUserName).css("opacity", "0");
if (msgObjArr.length > 0) {
for (var i = 0; i < msgObjArr.length; i++) {
var obj = msgObjArr[i];
if (obj.toUserName === toUserName) {
//追加数据
var messageArr = obj.message;
if (messageArr.length > 0) {
for (var j = 0; j < messageArr.length; j++) {
var msgObj = messageArr[j];
var leftOrRight = "right";
var message = msgObj.message;
var msgUserName = msgObj.username; //当聊天窗口与msgUserName的人相同,文字在左边(对方/其他人),否则在右边(自己)
if (msgUserName === toUserName) {
leftOrRight = "left";
} //但是如果点击的是自己,群聊的逻辑就不太一样了
if (username === toUserName && msgUserName !== toUserName) {
leftOrRight = "left";
} if (username === toUserName && msgUserName === toUserName) {
leftOrRight = "right";
} var magUserName = leftOrRight === "left" ? "<p class='hz-message-list-username'>" + msgUserName + "</p>" : ""; $(".tip" + toUserName + " #hz-message-body").append(
"<div class=\"hz-message-list\">" +
magUserName +
"<img class='" + leftOrRight + "' style='width: 23px;margin: 0 5px 0 0;' src='https://avatars3.githubusercontent.com/u/31408183?s=40&v=4'/>" +
"<div class=\"hz-message-list-text " + leftOrRight + "\">" +
"<span>" + message + "</span>" +
"</div>" +
"<div style=\" clear: both; \"></div>" +
"</div>");
}
}
break;
}
}
}
}); //获取当前时间
function nowTime() {
var time = new Date();
var year = time.getFullYear();//获取年
var month = time.getMonth() + 1;//或者月
var day = time.getDate();//或者天
var hour = time.getHours();//获取小时
var minu = time.getMinutes();//获取分钟
var second = time.getSeconds();//或者秒
var data = year + "-";
if (month < 10) {
data += "0";
}
data += month + "-";
if (day < 10) {
data += "0"
}
data += day + " ";
if (hour < 10) {
data += "0"
}
data += hour + ":";
if (minu < 10) {
data += "0"
}
data += minu + ":";
if (second < 10) {
data += "0"
}
data += second;
return data;
}

socketChart.js

#hz-main {
width: 700px;
height: 500px;
background-color: red;
margin: 0 auto;
} #hz-message {
width: 500px;
float: left;
background-color: #B5B5B5;
} #hz-message-body {
width: 460px;
height: 340px;
background-color: #E0C4DA;
padding: 10px 20px;
overflow: auto;
} #hz-message-input {
width: 500px;
height: 99px;
background-color: white;
overflow: auto;
} #hz-group {
width: 200px;
height: 500px;
background-color: rosybrown;
float: right;
} .hz-message-list {
min-height: 30px;
margin: 10px 0;
} .hz-message-list-text {
padding: 7px 13px;
border-radius: 15px;
width: auto;
max-width: 85%;
display: inline-block;
} .hz-message-list-username {
margin: 0 0 0 25px;
} .hz-group-body {
overflow: auto;
} .hz-group-list {
padding: 10px;
line-height: 23px;
} .hz-group-list:hover{
background-color: whitesmoke;
} .left {
float: left;
color: #595a5a;
background-color: #ebebeb;
} .right {
float: right;
color: #f7f8f8;
background-color: #919292;
} .hz-badge {
width: 20px;
height: 20px;
background-color: #FF5722;
border-radius: 50%;
float: right;
color: white;
text-align: center;
line-height: 20px;
font-weight: bold;
opacity:;
}

socketChart.css

  演示效果

  注册、登录、登出

  登录三个账号,上下线提示功能

  

  模拟私聊

  

  模拟群聊(目前点击自己头像是群聊频道)

  总结

  第一版先实现到这里。第二版预计实现消息存储、离线推送,好友分组,好友搜索、添加,以及一些其他优化;欢迎大家指出不足之处!

最新文章

  1. codevs 3110 二叉堆练习3
  2. 用markdown简化书写
  3. Register DLL Assembly Gacutil.exe(全局程序集缓存工具)
  4. dfs序 + RMQ = LCA
  5. [翻译]MapReduce: Simplified Data Processing on Large Clusters
  6. socket协议下如何缓存图片--推荐EGOCache
  7. NandFlash读写
  8. leetcode 169
  9. 支付宝Wap支付你了解多少?
  10. leetcode抽风的解决办法
  11. UITableView多选删除
  12. 基于vue2.0的一个分页组件
  13. Is it always safe to call getClass() within the subclass constructor?(转)
  14. AtomicInteger的用法
  15. Java _分页Jdbc 版
  16. 【java】抓取页面内容,提取链接(此方法可以http get无需账号密码的请求)
  17. Qt 5中信号和槽的新语法
  18. CUDA Cuts: Fast Graph Cuts on the GPU
  19. 最新QT4.8+kernel_3.2.5+uboot_2010.06+tslib移植成功-问题小结
  20. oracle11在docker环境下的运行

热门文章

  1. Headless Chrome入门
  2. Touch panel DTS 分析(MSM8994平台,Atmel 芯片)
  3. Angular 请求另一服务的api(请求代理)
  4. WPF 多点触摸开发[2]:WPF触摸的几个手势的执行顺序
  5. WPF 启动唯一程序(项目,exe,实例)
  6. wpf中防止界面卡死的写法
  7. 前端PS常用切图技巧
  8. Pytorch Code积累
  9. Windows 10开发基础——XML和JSON (二)
  10. textblock的LineHeight的调整