前言

  我们之前已经实现了 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

  演示效果

  注册、登录、登出

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

  

  模拟私聊

  

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

  总结

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

一套简单的web即时通讯——第一版的更多相关文章

  1. 一套简单的web即时通讯——第三版

    前言 接上版,本次版本做了如下优化: 1.新增同意.拒绝添加好友后做线上提示: 2.新增好友分组,使用工具生成后台API,新增好友分组功能,主要功能有:添加分组.重命名分组名称.删除分组 3.新增好友 ...

  2. 一套简单的web即时通讯——第二版

    前言 接上一版,这一版的页面与功能都有所优化,具体如下: 1.优化登录拦截 2.登录后获取所有好友并区分显示在线.离线好友,好友上线.下线都有标记 3.将前后端交互的值改成用户id.显示值改成昵称ni ...

  3. IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯(三)

    IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯(三) 后台服务用户与认证 新建一个空的.net core web项目Demo.Chat,端口配置为 ...

  4. IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯(一)

    IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯 前言 .net core 2.1已经正式发布了,signalr core1.0随之发布,是时候写个 ...

  5. IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯(二)

    IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯(二) IdentityServer4 用户中心生成数据库 上文已经创建了所有的数据库上下文迁移代码 ...

  6. 搭建一套简单的web服务器,记录实验过程

    搭建web服务器 一.实验内容: 实验要求: 1.完成一个简单的web服务器,web服务器从mysql里读取数据进行返回 2.Mysql需要有一个单独的数据盘,每个mysql虚拟机的磁盘挂载方式需要都 ...

  7. web即时通讯2--基于Spring websocket达到web聊天室

    如本文所用,Spring4和websocket要构建web聊天室,根据框架SpringMVC+Spring+Hibernate的Maven项目,后台使用spring websocket进行消息转发和聊 ...

  8. Web端即时通讯基础知识补课:一文搞懂跨域的所有问题!

    本文原作者: Wizey,作者博客:http://wenshixin.gitee.io,即时通讯网收录时有改动,感谢原作者的无私分享. 1.引言 典型的Web端即时通讯技术应用场景,主要有以下两种形式 ...

  9. Tomcat学习笔记(一)一个简单的Web服务器

    内容为<深入剖析Tomcat>第一章重点,以及自己的总结,如有描述不清的,可查看原书. 一.HTTP协议: 1.定义:用于服务器与客户端的通讯的协议,允许web服务器和浏览器通过互联网进行 ...

随机推荐

  1. HTML精确定位:scrollLeft,scrollWidth,clientWidth,offsetWidth完全详细的说明

      HTML:scrollLeft,scrollWidth,clientWidth,offsetWidth具体指完全解释究竟哪里的距离scrollHeight: 获取对象的高度滚动. scrollLe ...

  2. [转]完美解决)Tomcat启动提示At least one JAR was scanned for TLDs yet contained no TLDs

    一.文章前言    本文是亲测有效解决At least one JAR was scanned for TLDs yet contained no TLDs问题,绝对不是为了积分随便粘贴复制然后压根都 ...

  3. HDU 2845 Beans(dp)

    Problem Description Bean-eating is an interesting game, everyone owns an M*N matrix, which is filled ...

  4. WPF - 模板查看工具:Show Me The Template及如何查看第三方主题

    原文:WPF - 模板查看工具:Show Me The Template及如何查看第三方主题 在学习WPF的模板(DataTemplate.ItemsPanelTemplate.ControlTemp ...

  5. JS 三个对话框

    <!DOCTYPE html><html lang="en" xmlns="http://www.w3.org/1999/xhtml"> ...

  6. linux虚拟机上svn客户端连接问题

    在虚拟机上搭建好的svn,本地连接居然不行,原来是防火墙端口没开造成的. 修改配置文件:vi /etc/sysconfig/iptables # Generated by iptables-save ...

  7. liunx 桥接 上网 ip配置 外部网络访问

    一.设置VMware 在vmware的[编辑]-->[虚拟网络编辑器]设置:将VMnet0设置为“桥接”,并桥接到宿主机器的网卡(可以是有线或者无线网络).   二.设置虚拟机系统(以cento ...

  8. API Hook基本原理和实现

    API Hook基本原理和实现 2009-03-14 20:09 windows系统下的编程,消息message的传递是贯穿其始终的.这个消息我们可以简单理解为一个有特定意义的整数,正如我们看过的老故 ...

  9. 【Linux】PuTTY----------windows访问Linux 快捷方便

    第一步:百度PuTTY,下载好后直接运行,界面如下: 第二步:后输入IP:10.45.XX.XX,直接点击open按钮 第三步:输入用户名: 第四步:密码~ 现在,您就可以对你访问的linux设备进行 ...

  10. 一个技术人,最重要的是:极客精神(好奇心 + 探索欲)(新de代码)

    一个技术人,最重要的是:极客精神(好奇心 + 探索欲) 初到社会,面对众多的IT企业,我们是陌生与好奇的,认为所有企业都是管理一流并且高大上等的.然而工作多年以后你会发现,国内的IT企业环境良莠不齐, ...