Bootstrap

WebSocket实现聊天功能

使用SpringBoot + WebSocket 实现模拟QQ聊天功能

源码地址:https://gitee.com/mmolu/ws-chat

登录界面展示

登录界面模仿QQ登录操作,支持拖动、最小化和关闭

聊天界面展示

登录后的右侧显示在线用户,右下方显示在线用户的登录日志
窗口支持拖动、关闭操作


发送消息界面展示

在线用户实现及时聊天功能,可以对指定用户发起聊天(实现点对点的消息推送功能)
支持消息的本地缓存,聊天内容不会因为页面的刷新、关闭、退出登录等操作而丢失(消息只是做了简单缓存),清理浏览器缓存数据时消息数据便会丢失。

点对点消息推送,实现一对一的即时通信功能

Demo的目录代码

该Demo主要使用的是 SpringBoot + WebSocket 技术实现

在这里插入图片描述

下面是Demo的后端代码实现及说明

创建SpringBoot项目,导入依赖

项目pom依赖配置

	<properties>
		<java.version>1.8</java.version>
		<maven.compiler.source>1.8</maven.compiler.source>
		<maven.compiler.target>1.8</maven.compiler.target>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
	</properties>

	<dependencies>
		<!--  springboot-Web  -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<!-- WebSocket组件 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-websocket</artifactId>
		</dependency>

		<!-- 热部署依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<optional>true</optional>
			<scope>true</scope>
		</dependency>
<!-- ****************************以下工具为非必须**************************** -->
		<!--  lang3工具  -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
		</dependency>
        
        <dependency>
			<groupId>cn.hutool</groupId>
			<artifactId>hutool-all</artifactId>
			<version>5.7.16</version>
		</dependency>

		<!-- slf4j日志处理 -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
		</dependency>

		<!--  fastjson,对象和JSON数据转换工具  -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.55</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
		<!-- 打jar包时 如果 不配置该插件,打出来的jar包没有清单文件 -->
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
项目配置文件说明

application.properties

# 项目名称
spring.application.name=websocket-chatroom
# 配置资源路径
spring.resource.static-locations=classpath:/static/
# 视图前缀配置
spring.mvc.view.prefix=/chat/
# 视图后缀配置
spring.mvc.view.suffix=.html
###########################【热部署】#########################
# 重启目录
spring.devtools.restart.additional-paths=src/main/java
# 设置开启热部署
spring.devtools.restart.enabled=true
# 设置字符集
spring.freemarker.charset=utf-8
# 页面不加载缓存,修改后立即生效
spring.freemarker.cache=false
# 服务端口配置
server.port=80

application.yml

spring:
  resource:
    static-locations: classpath:/static/    
  application:
    name: websocket-chatroom
  mvc:
    view:
      suffix: .html
      prefix: /chat/
  devtools: 
    restart:
      additional-paths: src/main/java
      enabled: true
  freemarker: 
    charset: utf-8
    cache: false
server:
  port: 80
项目的配置类说明

WebConfig.java

WebConfig 配置类

配置后项目启动时,访问默认项目路径时,会跳转到该类指定的页面中

/**
 * @title web配置类 设置默认访问页面
 * @author 陌路
 * @date 2022-04-16
 * @apiNote 配置默认页面
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
	/**
	 * 自定义静态路径,spring.resource.static-locations=classpath:/static/
	 */
	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/").setViewName("/chat/login");
	}
}

WebSocketConfig.java

使用WebSocket前首先配置 @ServerEndpoint

  • 首先需要注入 ServerEndpointExporter ,

  • 这个bean会自动注册使用 @ServerEndpoint 注解来声明WebSocket endpoint。

  • 注意:如果使用独立的Servlet容器,而不是直接使用SpringBoot内置容器,就不需要注入 ServerEndpointExporter,因为他将由容器自己提供和管理。

/**
 * @Desc 	首先需要注入 ServerEndpointExporter , 
 * 			这个bean会自动注册使用 @ServerEndpoint 的注解来声明WebSocket endpoint。
 *     注意:如果使用独立的Servlet容器,而不是直接使用SpringBoot内置容器,就不需要注入
 *       	ServerEndpointExporter,因为他将有容器自己提供和管理。
 * @author 陌路
 * @date 2022-04-16
 */
@Configuration
public class WebSocketConfig {
	/**
	 * @title 扫描注册使用 @ServerEndpoint 注解的类
	 */
	@Bean
	public ServerEndpointExporter serverEndpointExporter() {
		return new ServerEndpointExporter();
	}
}

GetHttpSessionConfigurator.java

该类可以在建立连接后存放HttpSession,方便后续的使用

用户建立连接时,通过 EndpointConfig 获取存在 session 中的用户数据

/**
 * @tite 用来获取HttpSession对象.
 * @author 陌路
 * @date 2022-04-16
 */
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
	/**
	 * @title 该配置可以在不手动传入HttpSession的情况下在websocket服务类中使用
	 */
	@Override
	public void modifyHandshake(ServerEndpointConfig sec, 
                                HandshakeRequest request, 
                                HandshakeResponse response) {
		// 获取httpsession对象
		HttpSession httpSession = (HttpSession) request.getHttpSession();
		// 存放httpsession对象
		Map<String, Object> userProperties = sec.getUserProperties();
		userProperties.put(HttpSession.class.getName(), httpSession);
	}
}
创建用户对象实体
/**
 * (User)实体类
 * @author 陌路
 * @since 2022-04-16
 */
@Data
public class User implements Serializable {
    private static final long serialVersionUID = -31513108721728277L;
    /** 用户id */
	private String userId;
    /**  用户名 */
    private String username;
    /** 用户手机号 */
    private String phone;
    /** 用户密码 */
    private String password;
    public User(String userId, String username, String password, String phone) {
		this.userId = userId;
		this.username = username;
		this.password = password;
		this.phone = phone;
	}
    public User() {
    }
}  
创建数据信息对象

使用数据信息对象来模拟数据存储数据,实现用户的登录和注册

import cn.hutool.core.lang.Validator;
import cn.molu.app.pojo.User;
import cn.molu.app.vo.R;
/**
 * @title 模拟数据库user表
 * @Desc 模拟数据库数据,用户登录校验
 * @author 陌路
 * @date 2022-04-16
 */
public class UserData {
	/**
	 * 构造用户数据 模拟数据库用户
	 */
	public static Map<String, User> userMap = new HashMap<String, User>();
	static {
        //userMap.key=phone, userMap.value=user(id, name, password, phone);
		userMap.put("150******67", new User("001", "jack", "123456", "150******67"));
		userMap.put("135******88", new User("002", "mary", "123456", "135******88"));
		userMap.put("136******66", new User("003", "tom", "123456", "136******66"));
		userMap.put("159******21", new User("004", "tim", "123456", "159******21"));
		userMap.put("188******88", new User("005", "jenny", "123456", "188******88"));
		userMap.put("177******14", new User("006", "admin", "123456", "177******14"));
		userMap.put("176******67", new User("007", "lover", "123456", "176******67"));
		userMap.put("150******83", new User("008", "molu", "123456", "150******83"));
	}
	/**
	 * @Title 根据手机号获取用户信息
	 * @return User
	 */
	public static User loginByPhone(String phone) {
		boolean mobile = Validator.isMobile(phone);
		if (!mobile) {
			return null;
		}
		User user = userMap.get(phone);
		if (ObjectUtils.isBlank(user)) {
			return null;
		}
		return user;
	}
	/**
	 * @Title 用户注册
	 * @return R
	 */
	public static R register(HttpServletResponse res, User user) {
		if (null == user) {
			return R.err("请输入手机号、用户名和密码!");
		}
		String phone = user.getPhone();
		String username = user.getUsername();
		String password = user.getPassword();
		ObjectUtils.checkNull(res, "手机号、用户名和密码不能为空!", phone, username, password);
		user.setUserId(String.format("%03d", String.valueOf(userMap.size() + 1)));
		userMap.put(phone, user);
		return R.ok("注册成功!");
	}
}
数据响应实体类

响应数据结果,存储用户基本数据,过滤掉用户的重要信息

/**
 * @Desc 数据的响应类.
 * @author 陌路
 * @date 2022-04-16 上午10:53:15
 */
@Data
public class Result {
	/** 响应标志,成功/失败 */
	private boolean flag;
	/** 响应给前台的消息 */
	private String message;
	/** 用户名 */
	private String username;
	/** 用户id */
	private String userId;
	/** 日期时间 */
	private String dateStr;

	public void setDateStr(Date date) {
		this.dateStr = dateFormat(date);
	}

	public Result() {
		super();
	}

	public static String dateFormat(Date date) {
		return ObjectUtils.dateFormat(date);
	}
}
消息接收和响应类

用于接收和返回数据前台数据

/**
 * @title 服务端发送给客户端的消息.
 */
public class ResultMessage {
	/** 是否是系统消息 */
	private Boolean systemMsgFlag;
	/** 发送方姓名 */
	private String fromName;
	/** 发送方id */
	private String fromId;
	/** 接收方姓名 */
	private String toName;
	/** 接收方id */
	private String toId;
	/** 发送的数据 */
	private Object message;
	/** 接收到消息的日期时间 */
	private String dateStr;

	public void setDateStr(String dateStr) {
		this.dateStr = dateStr;
	}

	public String getDateStr(Date date) {
		if (date == null) {
			date = new Date();
		}
		return dateFormat(date);
	}

	public void setDateStr() {
		this.dateStr = dateFormat(new Date());
	}

	public void setDateStr(Date date) {
		if (date == null) {
			date = new Date();
		}
		this.dateStr = dateFormat(date);
	}

	public ResultMessage() {
		super();
	}
    
	public static String dateFormat(Object date) {
		return ObjectUtils.dateFormat(date);
	}
}
WS核心服务类

webSocket 的核心服务类,建立连接、发送消息、推送通知、关闭连接释放资源、消息处理等。。。

/**
 * @Desc 注解 @ServerEndpoint 配置对外暴露访问地址,外部访问格式为(ws://localhost:80/webSocket/001)
 * @author 陌路
 * @date 2022-04-16
 */
@Component
@ServerEndpoint(value = "/webSocket/{userId}", configurator = GetHttpSessionConfigurator.class)
public class ChatEndpoint {
	private final static Logger LOGGER = LogManager.getLogger(ChatEndpoint.class);
	/** 声明一个线程安全的全局Map对象,用来存储每一个用户的Session对象 */
	private static Map<String, Session> onLineUsers = new ConcurrentHashMap<String, Session>();
	/** 缓存用户数据 */
	public static Map<String, Result> cacheUser = new HashMap<String, Result>();
    
	/** 获取HttpSession中的用户名 */
	private HttpSession httpSession;

	/**
	 * @Title 连接建立时,会调用该方法
	 */
	@OnOpen
	public void onOpen(Session session, EndpointConfig endpointConfig) {
        // 获取GetHttpSessionConfigurator中存放的HttpSession
		Map<String, Object> userProperties = endpointConfig.getUserProperties();
		HttpSession httpSession = (HttpSession) userProperties.get(HttpSession.class.getName());
		this.httpSession = httpSession;
		String username = ObjectUtils.getStr(httpSession.getAttribute("username")) ;
		String userId = ObjectUtils.getStr(httpSession.getAttribute("userId"));
        // 如果用户名和用户id为空,则直接返回
		if (ObjectUtils.isEmpty(username,userId)) {
			return;
		}
        LOGGER.info("{}	建立了连接。。。", username);
        Result res = new Result();
		res.setUserId(userId);
		res.setUsername(username);
		// 缓存数据
		onLineUsers.put(userId, session);
		cacheUser.put(userId, res);
		// 组织消息体数据,将数据推送给所有在线用户
		String message = getSysMessage(getusers());
		// 调用方法进行系统消息的推送
		broadcastAllUsers(message);
		LOGGER.info("推送系统消息:{}	", message);
	}

	/**
	 * @Title 接收到客户端发送的数据时,会调用此方法
	 */
	@OnMessage
	public void onMessage(String message, Session session) throws IOException {
		LOGGER.info("接收到消息:{}", message);
		if (StringUtils.isBlank(message)) {
			return;
		}
        // 心跳检测机制
		if ("PING".toUpperCase().equals(message.toUpperCase())) {
			heartCheck(session);
			return;
		}
		LOGGER.info("接收到好友的消息:{}", message);
		ResultMessage msgObj = JSON.parseObject(message, ResultMessage.class);
        // 消息接收人id
		String toId = msgObj.getToId();
        // 获取消息体内容
		String text = ObjectUtils.getStr(msgObj.getMessage());
		// 获取推送指定用户数据
		String resultMessage = getMessage(msgObj, text);
		LOGGER.info("接收到好友发来的数据:{}", resultMessage);
		// 点对点发送数据(给指定用户发送消息)
		Session toSession = onLineUsers.get(toId);
		if (toSession != null && toSession.isOpen()) {
            Basic basicRemote = toSession.getBasicRemote();
            basicRemote.sendText(resultMessage);
		}
	}

	/**
	 * @title 连接关闭时,调用此方法
	 */
	@OnClose
	public void onClose(Session session) {
		if (httpSession == null) {
			return;
		}
		String username = ObjectUtils.getStr(httpSession.getAttribute("username"));
		String userId = ObjectUtils.getStr(httpSession.getAttribute("userId"));
		if (StringUtils.isBlank(userId)) {
			return;
		}
		LOGGER.info("{}:关闭了连接。。。", username);
		// 移除已关闭连接的用户
		onLineUsers.remove(userId);
        // 移除缓存的用户数据
		cacheUser.remove(userId);
		LOGGER.info("已从onLineUsers移除:{},从cacheUser中移除:{}", username, username);
		String message = getSysMessage(getusers());
		// 设置已关闭连接的用户数据在HttpSession域中有效时间
		httpSession.setMaxInactiveInterval(1800);
		broadcastAllUsers(message);
        LOGGER.info("关闭连接,推送内容:{}", message);
	}

	/**
	 * @title 出现错误时调用改方法
	 */
	@OnError
	public void onError(Session session, Throwable error) {
		LOGGER.info("连接出错了......{}", error);
	}
    
	/**
	 * @title 消息的推送,将消息推送给所有在线用户
	 * @param message
	 */
	private void broadcastAllUsers(String message) {
		try {
			// 将消息推送给所有的在线用户
			Set<String> ids = getIds();
			LOGGER.info("onLineUsers的所有id:{}", ids);
			for (String id : ids) {
				Session session = onLineUsers.get(id);
				Result result = cacheUser.get(id);
				LOGGER.info("获取到用户信息:{}", result);
				// 判断用户是否是连接状态
				if (session.isOpen()) {
					session.getBasicRemote().sendText(message);
				}
			}
			LOGGER.info("给{}推送了{}", getIds(), message);
		} catch (Exception e) {
			LOGGER.error("广播发送系统消息失败!{}", e);
			e.printStackTrace();
		}
	}
    
    /**
	 * @title 组织消息内容
	 */
	public static String getMessage(ResultMessage resultMessage, String message) {
		resultMessage.setSystemMsgFlag(false);
		resultMessage.setMessage(message);
		ObjectMapper objectMapper = new ObjectMapper();
		try {
			return objectMapper.writeValueAsString(resultMessage);
		} catch (JsonProcessingException e) {
			e.printStackTrace();
		}
		return "";
	}
    
    /**
	 * @Title 组织系统推送数据信息
	 * @return String
	 */
	public static String getSysMessage(Collection<Result> collection) {
		ResultMessage resultMessage = new ResultMessage();
		resultMessage.setSystemMsgFlag(true);
		resultMessage.setMessage(collection);
		ObjectMapper objectMapper = new ObjectMapper();
		try {
			return objectMapper.writeValueAsString(resultMessage);
		} catch (JsonProcessingException e) {
			e.printStackTrace();
		}
		return "";
	}
    
    /**
	 * @Title 心跳检测机制
	 */
	public static void heartCheck(Session session) {
		try {
			Map<String, Object> params = new HashMap<String, Object>();
			params.put("type", "PONG");
			session.getAsyncRemote().sendText(JSON.toJSONString(params));
			LOGGER.info("应答客户端的消息:{}", JSON.toJSONString(params));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
    
    
	/**
	 * @title 获取所有在线用户的用户id
	 * @return Set<String>
	 */
	private Set<String> getIds() {
		return ChatEndpoint.onLineUsers.keySet();
	}

	/**
	 * @title 获取所有在线用户,如果是系统消息就返回这个
	 * @return Set<String>
	 */
	private Collection<Result> getusers() {
		return cacheUser.values();
	}

	/**
	 * @title 格式化Date类型的数据
	 * @param date
	 * @return yyyy-MM-dd HH:mm:ss
	 */
	public static String dateFormat(Date date) {
		return ObjectUtils.dateFormat(date);
	}
}
后台控制器接口

后端访问接口

@Controller
@RequestMapping("index")
public class LoginController {

	/**
	 * 处理用户登录请求.
	 * @param user    用户登录数据
	 */
	@ResponseBody
	@PostMapping("/login")
	public Result login(User user, HttpServletRequest req,HttpServletResponse res) {
        HttpSession session = req.getSession();
        ObjectUtils.checkNull(res,"请输入手机号和密码!",user,user.getPhone(),user.getPassword());
		String phone = user.getPhone();
		String password = user.getPassword();
        boolean mobile = Validator.isMobile(phone);
        Result result = new Result();
        if(!mobile){
            result.setMessage("手机号格式错误!");
			result.setFlag(false);
            return result;
        }
		try {
            User userData = UserData.loginByPhone(phone);
			ObjectUtils.checkNull(res,userData,String.format("登录失败,未获取到%s的用户信息!", phone));
			if (password.equals(userData.getPassword())) {
				String username = userData.getUsername();
				result.setFlag(true);
				result.setMessage("登录成功!");
				result.setUsername(username);
				String userId = userData.getUserId();
				result.setUserId(userId);
				result.setDateStr(new Date());
				session.setAttribute("username", username);
				session.setAttribute("userId", userId);
			} else {
				result.setMessage("登录失败,密码输入错误!");
				result.setFlag(false);
			}
		} catch (Exception e) {
			e.printStackTrace();
			result.setMessage("登录失败!",e.getMessage());
			result.setFlag(false);
		}
		return result;
	}

	/**
	 * @Title 登录成功后跳转到聊天页面.
	 * @return 跳转到聊天室,如果没有登录,则返回登录页面
	 */
	@GetMapping("/toChatroom")
	public String toChatroom(HttpSession session) {
		String username =ObjectUtils.getStr(session.getAttribute("username"));
		if (StringUtils.isBlank(username)) {
			return "login";
		}
		return "div";
	}

	/**
	 * 登录成功后,获取session域中username值.
	 * @param session
	 */
	@ResponseBody
	@GetMapping("/getUsername")
	public Map<String, Object> getUsername(HttpSession session) {
		Map<String, Object> map = new HashMap<String, Object>();
		String username = (String) session.getAttribute("username");
		String userId = (String) session.getAttribute("userId");
		map.put("flag", true);
		map.put("username", username);
		map.put("userId", userId);
		return map;
	}
    
    /**
     * @title 返回登录页面
     */
    @GetMapping(value = "/toIndex")
	public String toIndexPage() {
		return "login";
	}
}
登录页面代码
<html lang="zh">
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
		<title>登录</title>
		<link href="/css/login.css" rel="stylesheet" />
		<script src="/js/jquery.js" ></script>
		<script type="text/javascript" src="/js/move.div.js" ></script>
		<script type="text/javascript" src="/js/login.js" ></script>
		<script type="text/javascript" src="/js/molu.js" ></script>
	</head>
	<body>
		<div class="box">
			<div class="title">
				<span class="left_bg">
					<img src="/imgs/bitbug.ico" style="width: 35px;height: 35px;" />
					<span style="font-size: 25px;">WS</span>
				</span>
				&nbsp;
				<span class="title_text" style="display: none;">
					请登录
				</span>
				&nbsp;
				<span class="close" title="关闭">X</span>
				<span class="min_hide" title="最小化"></span>
			</div>
			<span class="img_bg">
				<img src="/imgs/chatIc.png" style="width: 60px;height: 60px;border-radius: 10pc;" />
			</span>
			<div>
				<div id="loginForm">
					<div class="con" style="text-align: center;">
						<input class="u_input phone" type="text" name="phone" autocomplete="on"
							placeholder="手机号..." maxlength="11"/><br />
						<input class="u_input upwd" type="password" name="password" 
                               placeholder="密码..." maxlength="16"/>
					</div>
					<div class="check_box">
						<span style="margin-left: 60px;"></span>
						<label>
                            <input type="checkbox" class="showPwd" name="showPwd" title="显示密码" />
                            <span>显示密码</span>
                        </label>
						<label>
                            <input type="checkbox" class="remPwd" name="rememberMe" title="记住密码" />
                            <span>记住密码</span>
                        </label>
						<span>
                            <a href="javascript:;" style="margin-left: 30px;" title="找回密码">找回密码</a>
                        </span>
					</div>
					<div class="login_btn">
						<button class="btn" title="登录账号" type="submit" id="loginFormBtn">登录</button>
					</div>
				</div>
			</div>
			<div class="bottom_div">
				<span><a href="javascript:;" title="注册账号">注册账号</a></span>
				<span></span>
			</div>
		</div>
	</body>
</html>
登录页面JS代码
$(function() {
    $(".close").click(function() {
        $(".box").hide();
    });
    $("#loginFormBtn").click(function() {
        if (loginCheckData()) {
            login();
        }
    });
    $(".showPwd").bind("input propertychange", function() {
        if ($(this).prop("checked")) {
            $(".upwd").attr("type", "text");
        } else {
            $(".upwd").attr("type", "password");
        }
	})
});

function login() {
    let params = {};
    params.phone = ml.empty($('.phone').val());
    params.password = ml.empty($('.upwd').val());
    params.flag = $("input[name='rememberMe']").prop("checked");
    $.post("/index/login", params, function(res) {
        if (res.flag) {
            ml.msgBox(res.message);
            location.href = "/index/toChatroom";
        } else {
            ml.msgBox(res.message, 5, 5);
        }
    }).error(function(err) {
        ml.msgBoxBtn(err.responseJSON.status + ":" + err.responseJSON.message, "错误提示");
    })
}

function loginCheckData() {
    let phone = ml.empty($('.phone').val());
    if (!phone || phone.length != 11) {
        ml.tips("请输入正确的手机号!", "phone");
        return false;
    }
    // 不可包含中文及中文字符
    let regZh = /[\u4e00-\u9fa5][\u3000-\u301e\ufe10-\ufe19\ufe30-\ufe44\ufe50-\ufe6b\uff01-\uffee]/;
    //1.验证手机号 规则:第一位1,第二位是358中的一个,第三到十一位必须是数字。总长度11
    let reg = /^[1][358][0-9]{9}$/;
    if (!reg.test(phone) || regZh.test(phone)) {
        ml.tips("输入的手机号格式不正确!", "phone");
        return false;
    }
    let password = ml.empty($('.upwd').val());
    if (!password) {
        ml.tips("请输入密码!", "upwd");
        return false;
    }
    if (regZh.test(password)) {
        ml.tips("密码不可包含中文字符!", "upwd");
        return false;
    }
    if (password.length < 6) {
        ml.tips("密码必须为6~16个字符之间!", "upwd");
        return false;
    }
    let regPwd = /[A-Za-z0-9.!?]{12,30}/;
    if (!regPwd.test(password)) {
        ml.tips("密码仅支持数字、大小写字母和.!?符号", "upwd");
        return false;
    }
    //ml.msgBox("校验通过!");
    return true;
}
聊天界面代码
<html>
<head>
<meta charset="UTF-8">
<title></title>
<link href="/css/div.css" rel="stylesheet" />
<script type="text/javascript" src="../js/jquery.js"></script>
<script type="text/javascript" src="../js/move.div.js"></script>
<script type="text/javascript" src="../js/molu.js"></script>
<script type="text/javascript" src="../js/div.js"></script>
</head>
<body>
	<div class="box">
		<div class="div-main-title title" style="text-align: center;"></div>
		<div class="close">
			<a class="close close-click" title="关闭" href="javascript:void(0);">X</a>
		</div>
		<div class="div-main-list">
			<div class="main-list">
				<span>请选择好友聊天</span>
				<ul class="friend-list"></ul>
			</div>
			<div class="main-sys-msg">
				<span>系统消息</span>
				<ul class="sys-msg" style="text-align: left; padding: 5px; margin: 0px;"></ul>
			</div>
		</div>
		<div class="chat-main">
			<ul class="chat-msg">
				<li class="friend-li"><img src="../imgs/chatIc.png" class="img">
					<span class="friend-span">快去找朋友聊聊吧。。。</span>
                </li>
			</ul>
		</div>
		<div class="send-main">
			<textarea class="edit-msg" disabled></textarea>
		</div>
		<div class="div-btn">
			<div style="width: 50%;"></div>
			<div style="width: 50%; text-align: right;">
				<button class="closeDiv close-click" type="button" style="padding: 0px;">关闭</button>
				<button class="sendMsg" type="button">发送</button>
			</div>
		</div>
	</div>
</body>
</html>
聊天界面JS代码
let webObj = null;//全局WebSocket对象
let lockReconnect = false; // 网络断开重连
let wsCreateHandler = null; // 创建连接
let username = null; // 当前登录人姓名
let userId = null; //当亲登录人id
let toName = null; //消息接收人姓名
let toId = null;//消息接收人id
$(function() {
	$(".bg_change_size").remove();
	// 在ajax请求开始前将请求设为同步请求:$.ajaxSettings.async = false。
	// 在ajax请求结束后不要忘了将请求设为异步请求,否则其他的ajax请求也都变成了同步请求 $.ajaxSettings.async = true。
	$.ajax({
		async: false,
		type: 'GET',
		url: "/index/getUsername",
		success: function(res) {
			if (!res.userId || !res.username) {
				location.href = '/index/toIndex';
			}
			username = res.username;
			userId = res.userId;
		}
	});
	// 创建webSocket对象
	createWebSocket();
	// 发送消息到服务器
	$(".sendMsg").on("click", function() {
		sendMessage();
	})
});

/**
 * @title 创建连接
 */
function createWebSocket() {
	try {
		// 获取访问路径,带有端口号:ws://localhost/webSocket/001
		let host = window.location.host;
		// 创建WebSocket连接对象
		webObj = new WebSocket(`ws://${host}/webSocket/${userId}`);
		// 加载组件
		initWsEventHandle();
	} catch (e) {
		ml.msgBox("连接出错,正在尝试重新连接,请稍等。。。");
		// 尝试重新连接服务器
		reconnect();
	}
}

/**
 * @title 初始化组件
 */
function initWsEventHandle() {
	try {
		// 建立连接
		webObj.onOpen = function(evt) {
			onWsOpen(evt);
			// 建立连接之后,开始传输心跳包
			heartCheck.start();
		};
		// 传送消息
		webObj.onmessage = function(evt) {
			// 发送消息
			onWsMessage(evt);
			// 接收消息后 也需要心跳包的传送
			heartCheck.start();
		};
		// 关闭连接
		webObj.onclose = function(evt) {
			// 关闭连接,可能是异常关闭,需要重新连接
			onWsClose(evt);
			// 尝试重新连接
			reconnect();
		};
		// 连接出错
		webObj.onerror = function(evt) {
			// 连接出错
			onWsError(evt);
			// 尝试重新连接
			reconnect();
		}
	} catch (e) {
		if (e) {
			conlog("catch", e);
		}
		ml.msgBox("初始化组件失败,正在重试,请稍后。。。");
		// 尝试重新创建连接
		reconnect();
	}
}

/**
 * @title 建立连接
 */
function onWsOpen(e) {
	if (e.data) {
		conlog("onWsOpen", e.data);
	}
	ml.msgBox("建立连接成功。。。");
}

/**
 * @title 传送消息内容
 */
function onWsMessage(e) {
	if (e.data) {
		conlog("onWsMessage", e.data);
	}
	let jsonStr = e.data;
	//接收到服务器推送的消息后触发事件
	message(e);
}

/**
 * @title 关闭连接
 */
function onWsClose(e) {
	if (e.data) {
		conlog("onWsClose", e.data);
	}
	ml.msgBox("连接关闭,尝试重新连接服务器,请稍侯。。。");
	closeFun(e);
}
/**
 * @title 连接出错
 */
function onWsError(e) {
	ml.msgBox("连接出错,正在尝试重新连接服务器,请稍侯。。。" + e.data);
}

/**
 * @title 输出数据到控制台
 */
function conlog(msg) {
	conlog('', msg);
}

/**
 * @title 输出数据到控制台
 */
function conlog(title, msg) {
	let content = msg;
	if (title) {
		content = `${title}${msg}`;
	}
	console.log(`${content}`);
}

/**
 * @title 异常处理
 * @Desc 处理可以检测到的异常,并尝试重新连接
 */
function reconnect() {
	if (lockReconnect) {
		return;
	}
	conlog("reconnect");
	ml.msgBox("正在尝试重新连接,请稍侯。。。");
	lockReconnect = true;
	// 没链接上会一直连接,设置延迟,避免过多请求
	wsCreateHandler && clearTimeout(wsCreateHandler);
	wsCreateHandler = setTimeout(function() {
		ml.msgBox("正在重新连接。。。");
		createWebSocket();
		lockReconnect = false;
		ml.msgBox("重连完成。。。");
	}, 1000);
}

/**
 * @title 心跳检测
 * @Desc 网络中断,系统无法捕获,需要心跳检测实现重新连接
 */
var heartCheck = {
	// 在15s内若没收到服务端消息,则认为连接断开,需要重新连接
	timeout: 15000, // 心跳检测触发时间
	timeoutObj: null,
	serverTimeoutObj: null,
	// 重新连接
	reset: function() {
		clearTimeout(this.timeoutObj);
		clearTimeout(this.serverTimeoutObj);
		this.start();
	},
	// 开启定时器
	start: function() {
		let self = this;
		this.timeoutObj && clearTimeout(this.timeoutObj);
		this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
		this.timeoutObj = setTimeout(function() {
			ml.msgBox("发送ping到后台服务。。。");
			try {
				webObj.send("PING");
			} catch (e) {
				ml.msgBox("发送ping异常。。。");
			}
			//内嵌定时器
			self.serverTimeoutObj = setTimeout(function() {
				// 若onclose方法会执行reconnect方法,我们只需执行close()就行,
                // 若直接执行reconnect会触发onclose导致重连两次
				ml.msgBox("没有收到后台数据,关闭连接。。。");
				webObj.close();
				//reconnect();
			}, self.timeout);
		}, this.timeout);
	}
};

/**
 * @title 组织并解析数据
 */
function message(e) {
	//获取服务端推送过来的消息
	let message = e.data;
	// 将message转为JSON对象
	let res = JSON.parse(message);
	// 是否为系统消息
	if (res.systemMsgFlag) {
		let allNames = res.message;
		//1. 好友列表展示   2. 系统推广
		let userListStr = "";
		let broadcastListStr = "";
		let imgUrl = "../imgs/chatIc.png";
		for (let user of allNames) {
			if (user.userId != userId) {
				userListStr += `<li class="friend-li" οnclick='chatWith("${user.userId}","${user.username}",this);'><img src="${imgUrl}" class='friendList-img'><span>${user.username}</span></li>`;
				broadcastListStr += `<li style='color:#9d9d9d;font-family:宋体;'>
				<span style='font-size:5px;color:black;'>${user.dateStr}</span><br/>
					好友<span>${user.username}</span>上线了!
				</li>`;
			}
		}
		$(".friend-list").html(userListStr);
		$(".sys-msg").html(broadcastListStr);
	} else {
		// 不是系统消息
		let msgText = res.message;
		let msg = `<li class='friend-msg-li'><img src='${imgUrl}' class='img'/><span class='friend-span'>${msgText}</span></li>`;
		if (toId === res.fromId) {
			$(".chat-msg").append(msg);
		}
		let chatData = sessionStorage.getItem(res.fromId);
		if (chatData) {
			msg = chatData + msg;
		}
		sessionStorage.setItem(res.fromId, msg);
		$(".chat-main")[0].scrollTop = $(".chat-main")[0].scrollHeight;
	}
}

// 关闭连接
function closeFun(e) {
	let tips = `<span style='font-size:5px;color:black;'>${new Date()}</span><br/>`;
	$(".sys-msg").html(`${tips}用户:${username}<span style="float:right;color:red;">离开了</span>`);
}

// 选择好友
function chatWith(id, name, obj) {
	toId = id;
	toName = name;
	$(".edit-msg").attr("disabled", false);
	$(".div-btn").show();
	$(".chat-msg").show();
	$(obj).addClass("selected-li").siblings().removeClass("selected-li");
	let chatNow = `正在和<span style="color: #db41ca;">${name}</span>聊天`;
	$(".div-main-title").html(chatNow);
	$(".chat-msg").html("");
	var chatData = sessionStorage.getItem(toId);
	if (chatData) {
		//渲染聊天数据到聊天区
		$(".chat-msg").html(chatData);
	}
	$(".chat-main")[0].scrollTop = $(".chat-main")[0].scrollHeight;
}

/**
 * @title 组织和发送消息到服务器
 */
function sendMessage() {
	// 发送消息
	if (!toId || !toName) {
		ml.tips("sendMsg", "请选择好友...");
		return;
	}
	let msg = $(".edit-msg").val();
	if (!msg) {
		ml.tips("sendMsg", "请输入内容...");
		return;
	}
	let img = `<img src='${imgUrl}' class='myself-img'/>`;
	let li = $("<li class='myself-li'></li>");
	li.html(`${img}<span class='myself-span'>${msg}</span>`);
	$(".chat-msg").append(li);
	$(".edit-msg").val('');
	$(".chat-main")[0].scrollTop = $(".chat-main")[0].scrollHeight;
	let chatData = sessionStorage.getItem(toId);
	let liStr = `<li class='myself-li'>${img}<span class='myself-span'>${msg}</span></li>`;
	if (chatData) {
		liStr = chatData + liStr;
	}
	let jsonMessage = {
		"fromName": username, //消息发送人姓名
		"fromId": userId, //消息发送人id
		"toName": toName, //消息接收人姓名
		"toId": toId, //消息接收人id
		"message": msg //发送的消息内容
	};
	// 将消息存放到sessionStorage中
	sessionStorage.setItem(toId, liStr);
	// 发送数据给服务器(消息发送格式为JSON格式)
	webObj.send(JSON.stringify(jsonMessage));
}
源码地址:https://gitee.com/mmolu/ws-chat
;