勿以恶小而为之,勿以善小而不为--------------------------刘备
劝诸君,多行善事积福报,莫作恶
上一章简单介绍了 Servlet实现WebSocket的简单聊天室(二),如果没有看过,请观看上一章
本文代码参考了黑马教程视频的 “即时通信技术-Websocket实现在线聊天室” 的代码实例。
一. Spring 整合 WebSocket
一.一 提升点
相对于上一章节的代码实例,主要有以下几个增强点:
一.二 整合流程讲解
Spring 整合 WebSocket, 也是针对 onopen,onclose,onmessage,onerror 四个事件,进行相应处理。
需要多添加一个拦截器和处理器,并将拦截器和处理器,进行注册,需要一个注册工厂。
一.二.一 拦截器 HandshakeInterceptor
开发者需要自定义拦截器, 如 MyHandshakeInterceptor, 实现 org.springframework.web.socket.server.HandshakeInterceptor 接口。
该HandshakeInterceptor 接口提供了两个方法:
public abstract interface HandshakeInterceptor
{
public abstract boolean beforeHandshake(ServerHttpRequest paramServerHttpRequest,
ServerHttpResponse paramServerHttpResponse, WebSocketHandler paramWebSocketHandler,
Map<String, Object> paramMap) throws Exception;
public abstract void afterHandshake(ServerHttpRequest paramServerHttpRequest,
ServerHttpResponse paramServerHttpResponse, WebSocketHandler paramWebSocketHandler,
Exception paramException);
}
beforeHandshake() 方法,是请求连接之前的处理方法。
注意,方法的参数 paramServerHttpRequest 并不是以前的 HttpServletRequest 对象,而是 ServletServerHttpRequest 对象, WebSocket 对其进行了扩展。
参数 paramMap 是集体Map, 放置于 WebSocketSession里面, 通过 WebSocketSession对象的 getAttributes() 方法来获取这个Map.
当请求连接时,需要把对象放置到 paramMap 里面进行保存。
afterHandshake() 方法,是请求连接成功之后的处理方法。
一.二.二 处理器 WebSocketHandler
开发者需要自定义处理器, 如 MyWebSocketHandler, 实现 org.springframework.web.socket.WebSocketHandler 接口。
一.二.二.一 接口方法解释
该接口 WebSocketHandler 提供了五个方法。
public abstract interface WebSocketHandler
{
public abstract void afterConnectionEstablished(WebSocketSession paramWebSocketSession)
throws Exception;
public abstract void handleMessage(WebSocketSession paramWebSocketSession, WebSocketMessage<?> paramWebSocketMessage)
throws Exception;
public abstract void handleTransportError(WebSocketSession paramWebSocketSession, Throwable paramThrowable)
throws Exception;
public abstract void afterConnectionClosed(WebSocketSession paramWebSocketSession, CloseStatus paramCloseStatus)
throws Exception;
public abstract boolean supportsPartialMessages();
}
一.二.二.二 接口方法处理操作
在执行 afterConnectionEstablished()时,需要将该Session的对象,放置到在线用户列表里面, 并且向客户端发送’欢迎Xxx进来’ 类似提示。
在执行 handleMessage()时,需要向客户端发送 ‘XXX 说 输入内容’ 类似提示,通过 paramWebSocketMessage.getPayload().toString() 来获取传递过来的内容,通过 paramWebSocketMessage.getPayloadLength() 来判断传递过来的内容是否为空。
在执行 handleTransportError()时,需要从在线用户列表里面移除该Session 对象,并且向客户端发送 ‘Xxx退出聊天室’ 类似提示
在执行 afterConnectionClosed()时,也需要从在线用户列表里面移除该Session 对象,并且向客户端发送 ‘Xxx有事离开了’ 类似提示。
一.二.二.三 服务器向浏览器发送消息
消息 需要封装在 TextMessage 对象里面, 通过
TextMessage message=new TextMessage(内容主体字符串);
进行实例化。
通过调用 WebSocketSession 对象的 sendMessage(WebSocketMessage<?> message) 方法,进行发送消息。
//发送消息
webSocketSession.sendMessage(message);
一.二.三 注册工厂 WebSocketConfigurer
开发者需要手动实现 注册工厂,来将 HandshakeInterceptor 拦截器和 WebSocketHandler 处理器注册进来, 让系统框架能够通过前台的url地址找到对应的 拦截器和处理器。 如 WebSocketConfig 类。
需要实现org.springframework.web.socket.config.annotation.WebSocketConfigurer 接口
public interface WebSocketConfigurer {
//注册进来
void registerWebSocketHandlers(WebSocketHandlerRegistry registry);
}
registry.addHandler(new MyWebSocketHandler(),"/ws").addInterceptors(new MyHandshakeInterceptor());
其中, “/ws” 是前台传入过来的路径。 当前台传入的路径是 ws时,就执行 MyWebSocketHandler 处理器和 MyHandshakeInterceptor 拦截器。
可配置多个。
registry.addHandler(new MyWebSocketHandler(),"/ws").addInterceptors(new MyHandshakeInterceptor());
registry.addHandler(new MyWebSocketHandler2(),"/ws2").addInterceptors(new MyHandshakeInterceptor2());
表示前台路径是 ws时,去执行MyWebSocketHandler,MyHandshakeInterceptor,
当前台路径是 ws2 时,去执行 MyWebSocketHandler2,MyHandshakeInterceptor2, 可以区分性处理。
不要忘记,该注册工厂 WebSocketConfig 需要添加一个注解, @EnableWebSocket, 来表示该类由WebSocket 进行处理。
二. Spring 整合 WebSocket 的详细步骤
二.一 基本环境搭建 (Maven 管理项目)
二.一.一 pom.xml 依赖管理
<dependencies>
<!-- 添加webmvc的依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<!-- 添加websocket的依赖,不能忘记 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<!-- 添加 message依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>4.3.14.RELEASE</version>
</dependency>
<!-- tomcat中的 servlet-api和 jsp-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jsp-api</artifactId>
<version>2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- jstl 与 standard -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.22</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<!-- jackson 依赖,用于处理json -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.2.3</version>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.11</version>
</dependency>
</dependencies>
<!-- 构建信息管理 -->
<build>
<plugins>
<!-- 编译的jdk版本 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<!--tomcat的插件名, tomcat7-maven-plugin, 用的是tomcat7版本 -->
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>8080</port> <!--tomcat的端口号 -->
<path>/chatroom</path> <!--tomcat的项目名 -->
<uriEncoding>UTF-8</uriEncoding> <!-- 防止get 提交时乱码 -->
</configuration>
</plugin>
</plugins>
</build>
二.一.二 配置 web.xml 文件
比平常多了一个 defaultHtmlEscape ,防止 XSS 注入。
<!-- UTF-8 编码过滤器 -->
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- spring mvc前端控制器 -->
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- 防 XSS -->
<context-param>
<param-name>defaultHtmlEscape</param-name>
<param-value>true</param-value>
</context-param>
二.一.三 springmvc.xml 配置文件
放置在 src/main/resources 目录下。
采用 json 进行转换, 静态资源在 /static 目录下。
<!-- bean组件扫描 -->
<context:component-scan base-package="com.yjl.websocket" />
<mvc:annotation-driven>
<mvc:message-converters>
<bean
class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>application/json;charset=UTF-8</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!-- 静态资源 -->
<mvc:resources location="/static/" mapping="/static/**" />
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/" />
<property name="suffix" value=".jsp" />
</bean>
二.二 前端页面处理
二.二.一 前端 static 静态目录
里面存放的是 bootstrap 框架和 jquery,sockjs 的js 文件
二.二.二 index.jsp 页面
主页,去跳转到 登录页面
<body>
<jsp:forward page="User/toLogin"></jsp:forward>
</body>
二.二.三 登录页面 /pages/login.jsp
<body>
<div class="col-sm-6 col-sm-offset-3">
<div class="col-sm-offset-2" >
<h2>聊天室登录页面</h2>
</div>
<div >
<form action="/User/login" method="post" class="form-horizontal" role="form">
<div class="form-group">
<label for="firstPass" class="col-md-2 control-label">昵称:</label>
<div class="col-md-4">
<input type="text" class="form-control" id="nickName"
placeholder="请输入你的昵称" name="nickName" value=""/>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3">
<input type="submit" value="进入聊天室" class="btn btn-success"/>
</div>
</div>
</form>
</div>
</div>
</body>
展示大致效果如下所示:
二.二.四 主页展示 /pages/main.jsp
<%
//项目路径
String path = request.getContextPath();
//ip地址+端口+项目路径, 即请求前路径
String basePath = request.getServerName() + ":" + request.getServerPort() + path + "/";
//协议+basePath
String baseUrlPath = request.getScheme() + "://" + basePath;
%>
<body>
<div class="container">
<div class="row col-sm-offset-4" >
<div class="col-sm-7" >欢迎进入 '两个蝴蝶飞' 聊天室</div>
<div class="col-sm-4 col-sm-offset-1">
<p>当前登录用户: <button id="exitBtn" class="btn btn-default">退出或重新登录</button></p>
</div>
</div>
<div class="row" >
<div class="col-sm-3">
<div>在线人员列表(<span id="onlineNum">0</span>)人</div>
<ul id="online" class="list-unstyled">
</ul>
</div>
<div class="col-sm-9">
<div class="showText" id="up">
<ul id="contentUl" class="list-unstyled">
</ul>
</div>
<div class="inputText hr">
<div class="form-group">
<textarea class="form-control" id="msg" name="msg" placeholder="请输入你想发送的消息" ></textarea>
</div>
<div class="form-group col-sm-offset-9" >
<input type="button" value="发送消息" id="sendBtn" name="sendBtn" class="btn btn-success"/>
</div>
</div>
</div>
</div>
</div>
</body>
展示大致效果如下所示:
二.三 实体封装处理和控制器
二.三.一 封装用户信息 User
里面存入 id (uuid自动生成,避免重复) 和 nickName (用户输入昵称) 。 可以用数据库替换这个用户信息
package com.yjl.websocket.pojo;
/**
* 封装用户信息对象
* @author 两个蝴蝶飞
*
*/
public class User {
/**
* @param id 编号,是uuid
* @param nickName 昵称,由用户自己输入
*/
private String id;
private String nickName;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
@Override
public String toString() {
return "User [id=" + id + ", nickName=" + nickName + "]";
}
}
二.三.二 封装消息内容 MyMessage
里面有 发送者,接收者,发送内容,发送时间等重要的信息
package com.yjl.websocket.bean;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
* 封装消息对象
* @author 两个蝴蝶飞
*
*/
public class MyMessage {
/**
* @param fromId 发送者
* @param fromNickName 发送者的昵称
* @param toId 接收者,如果是群发的话,为空
* @param text 发送的内容
* @param date 发送的时间,具体到秒
*/
private String fromId;
private String fromNickName;
private String toId;
private String text;
//格式化成 yyyy-MM-dd HH:mm:ss的格式
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
private Date date;
public String getFromId() {
return fromId;
}
public void setFromId(String fromId) {
this.fromId = fromId;
}
public String getFromNickName() {
return fromNickName;
}
public void setFromNickName(String fromNickName) {
this.fromNickName = fromNickName;
}
public String getToId() {
return toId;
}
public void setToId(String toId) {
this.toId = toId;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
@Override
public String toString() {
return "MyMessage [fromId=" + fromId + ", fromNickName=" + fromNickName + ", toId=" + toId + ", text=" + text
+ ", date=" + date + "]";
}
}
二.三.三 封装在线用户列表 MyOnLineUserMap
package com.yjl.websocket.bean;
import java.util.HashMap;
import java.util.Map;
import org.springframework.web.socket.WebSocketSession;
import com.yjl.websocket.pojo.User;
/**
* 封装在线用户列表信息
* @author 两个蝴蝶飞
*
*/
public class MyOnLineUserMap {
//定义id 与 session的集合,用于发送消息
private static Map<String,WebSocketSession> USER_ONLINE_SESSION_MAP;
//定义id 与 user的集合,用于查询在线用户
private static Map<String,User> USER_ONLINE_MAP;
static {
//初始化,长度为16
USER_ONLINE_SESSION_MAP=new HashMap<String,WebSocketSession>(16);
USER_ONLINE_MAP=new HashMap<String,User>(16);
}
public static Map<String, WebSocketSession> getUSER_ONLINE_SESSION_MAP() {
return USER_ONLINE_SESSION_MAP;
}
public static Map<String, User> getUSER_ONLINE_MAP() {
return USER_ONLINE_MAP;
}
}
二.三.四 编写控制器 UserAction
用于跳转到登录,登录方法和查询在线用户列表的方法
package com.yjl.websocket.action;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.yjl.websocket.bean.MyOnLineUserMap;
import com.yjl.websocket.pojo.User;
/**
*
* 处理用户跳转逻辑
* @author 两个蝴蝶飞
*
*/
@Controller
@RequestMapping("/User")
public class UserAction {
/**
* 跳转到登录的页面
* @return
*/
@RequestMapping("/toLogin")
public String toLogin(HttpSession session){
//需要清空session中的loginUser, 如果有的话
if(session.getAttribute("loginUser")!=null){
//移除
session.removeAttribute("loginUser");
}
return "login";
}
/**
* 登录操作
* @param nickName
* @param req
* @param session
* @return
*/
@RequestMapping("/login")
public String login(String nickName,HttpServletRequest req,HttpSession session){
//当前浏览器已经登录过了,那么就清空,保证每一个浏览器只能登录一个用户。
if(session.getAttribute("loginUser")!=null){
//移除
session.removeAttribute("loginUser");
}
//编号为uuid
User user=new User();
user.setId(UUID.randomUUID().toString());
user.setNickName(nickName);
//放置到session 里面
session.setAttribute("loginUser",user);
System.out.println("**********新用户nickeName["+nickName+"]登录*****************");
return "redirect:toMain.action";
}
/**
* 跳转到主页
* @param session
* @return
*/
@RequestMapping("/toMain")
public String toMain(HttpSession session){
//如果未登录,就跳转到登录页面
if(session.getAttribute("loginUser")==null){
return "redirect:toLogin";
}
return "main";
}
/**
* 查询在线用户列表
* @return
*/
@RequestMapping(value="/onlineList")
@ResponseBody
public Map<String,Object> getOnlineUserList(){
Map<String,Object> resultMap=new HashMap<String,Object>();
List<User> allList=new ArrayList<User>();
allList.addAll(MyOnLineUserMap.getUSER_ONLINE_MAP().values());
resultMap.put("onlineList",allList);
return resultMap;
}
}
上面,都是正常的逻辑操作,与WebSocket 无关。
可以发现,Spring 整合 WebSocket 时,没有很大的侵入性,是松耦合的。
二.四 WebSocket的三大件编写
基本与 一.二 讲解的内容差不多。
二.四.一 编写拦截器 MyHandshakeInterceptor
package com.yjl.websocket.websocket;
import java.util.Map;
import javax.servlet.http.HttpSession;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import com.yjl.websocket.pojo.User;
/**
* 配置拦截器,需要继承 HandshakeInterceptor
* @author 两个蝴蝶飞
*
*/
@Component("myHandshakeInterceptor")
public class MyHandshakeInterceptor implements HandshakeInterceptor{
/**
* 先发送一个请求,请求连接
*/
@Override
public boolean beforeHandshake(ServerHttpRequest req, ServerHttpResponse resp, WebSocketHandler handler,
Map<String, Object> attribute) throws Exception {
if(req instanceof ServletServerHttpRequest){
System.out.println("属于ServletServerHttpRequest");
//先进行转换
ServletServerHttpRequest servletRequest=(ServletServerHttpRequest)req;
//得到Session
HttpSession session=servletRequest.getServletRequest().getSession(false);
//取出里面的 loginUser 的登录用户
if(session.getAttribute("loginUser")!=null){
User user=(User)session.getAttribute("loginUser");
//放置到 map里面,这个map是 WebSocketSession的对象
attribute.put("loginUser",user);
System.out.println("连接一个新用户:[id:"+user.getId()+",nickName:"+user.getNickName());
}else{
System.out.println("***********用户未登录,握手失败*****************");
return false;
}
}else{
System.out.println("不属于ServletServerHttpRequest");
}
System.out.println("*********发送请求握手*************");
return true;
}
/**
* 请求连接成功
*/
@Override
public void afterHandshake(ServerHttpRequest arg0, ServerHttpResponse arg1, WebSocketHandler arg2, Exception arg3) {
System.out.println("*********握手成功*************");
}
}
二.四.二 编写处理器 MyWebSocketHandler
package com.yjl.websocket.websocket;
import java.io.IOException;
import java.util.Date;
import java.util.Map;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.util.HtmlUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yjl.websocket.bean.MyMessage;
import com.yjl.websocket.bean.MyOnLineUserMap;
import com.yjl.websocket.pojo.User;
/**
* 配置处理器
* @author 两个蝴蝶飞
*
*/
@Component("myWebSocketHandler")
public class MyWebSocketHandler implements WebSocketHandler{
/**
* 当连接成功之后,进行的处理操作,对应 @OnOpen
* wsSession 指的是 连接的那个浏览器用户信息
*/
@Override
public void afterConnectionEstablished(WebSocketSession wsSession) throws Exception {
System.out.println("进来了:onOpen");
//获取存于attribute的那个map
Map<String,Object> attributes=wsSession.getAttributes();
//刚刚登录成功的那个user 信息
User user=(User)attributes.get("loginUser");
//将这个信息,放置到在线的map里面
MyOnLineUserMap.getUSER_ONLINE_SESSION_MAP().put(user.getId(),wsSession);
MyOnLineUserMap.getUSER_ONLINE_MAP().put(user.getId(), user);
//构建消息 MyMessage
MyMessage message=new MyMessage();
message.setText("风骚的【"+user.getNickName()+"】进入了聊天室,大家欢迎");
message.setDate(new Date());
//构建TextMessage 对象,然后发送对象信息
ObjectMapper objMapper=new ObjectMapper();
String textResult=objMapper.writeValueAsString(message);
System.out.println("输出消息内容:"+textResult.toString());
TextMessage textMessage=new TextMessage(textResult);
//发送消息给所有人
sendMessageToAll(textMessage);
}
/**
* 主动断开连接后的事件, 对应 @OnClose
*/
@Override
public void afterConnectionClosed(WebSocketSession wsSession, CloseStatus closeStatus) throws Exception {
System.out.println("进来了:onClose");
//获取该 wsSession 对应的那个User 信息
User closeUser=(User)wsSession.getAttributes().get("loginUser");
//构建 Message
MyMessage message=new MyMessage();
message.setFromId(closeUser.getId());
message.setFromNickName(closeUser.getNickName());
message.setText("万众嘱目的【"+closeUser.getNickName()+"】有事先走了,大家继续聊...");
message.setDate(new Date());
//在线列表里面,去除掉这个人的信息
MyOnLineUserMap.getUSER_ONLINE_SESSION_MAP().remove(closeUser.getId());
MyOnLineUserMap.getUSER_ONLINE_MAP().remove(closeUser.getId());
//信息移除
wsSession.getAttributes().remove("loginUser");
ObjectMapper objMapper=new ObjectMapper();
String textResult=objMapper.writeValueAsString(message);
TextMessage textMessage=new TextMessage(textResult);
//发送消息给所有人
sendMessageToAll(textMessage);
}
/**
* 浏览器发送消息之后,进行的处理操作, 对应 @OnMessage
*/
@Override
public void handleMessage(WebSocketSession wsSession, WebSocketMessage<?> message) throws Exception {
System.out.println("进来了:onMessage");
// 接收的消息,长度如果是0,表示没有消息,直接返回
if(message.getPayloadLength()==0){
return ;
}
ObjectMapper objMapper=new ObjectMapper();
MyMessage inputMessage=objMapper.readValue(message.getPayload().toString(),MyMessage.class);
//设置日期
inputMessage.setDate(new Date());
//接收到的消息
String inputMsg=inputMessage.getText();
System.out.println("【"+inputMessage.getFromNickName()+"】发送的消息是:"+inputMsg);
//将这个消息,进行转义
String escapeHTML=HtmlUtils.htmlEscape(inputMsg);
//重新设置转义好的字符串
inputMessage.setText(escapeHTML);
//定义Message
TextMessage textMessage=new TextMessage(objMapper.writeValueAsString(inputMessage));
//接收到的消息, 看是群发,还是私发
if(inputMessage.getToId()==null||"-1".equals(inputMessage.getToId())){
//是群发
sendMessageToAll(textMessage);
}else{
//是私发
sendMessageToOne(inputMessage.getToId(), textMessage);
}
}
/**
* 错误时的消息, 对应的是 @OnError
*/
@Override
public void handleTransportError(WebSocketSession wsSession, Throwable throwable) throws Exception {
System.out.println("进来了:onError");
//如果目前开启,那么执行关闭
if(wsSession.isOpen()){
wsSession.close();
}
//获取该 wsSession 对应的那个User 信息
User closeUser=(User)wsSession.getAttributes().get("loginUser");
//构建 Message
MyMessage message=new MyMessage();
message.setFromId(closeUser.getId());
message.setFromNickName(closeUser.getNickName());
message.setText("万众嘱目的【"+closeUser.getNickName()+"】退出聊天室");
message.setDate(new Date());
//在线列表里面,去除掉这个人的信息
MyOnLineUserMap.getUSER_ONLINE_SESSION_MAP().remove(closeUser.getId());
MyOnLineUserMap.getUSER_ONLINE_MAP().remove(closeUser.getId());
//信息移除
wsSession.getAttributes().remove("loginUser");
ObjectMapper objMapper=new ObjectMapper();
String textResult=objMapper.writeValueAsString(message);
TextMessage textMessage=new TextMessage(textResult);
//发送消息给所有人
sendMessageToAll(textMessage);
}
/**
* 是否支持处理拆分消息,返回true返回拆分消息
*/
//是否支持部分消息:如果设置为true,那么一个大的或未知尺寸的消息将会被分割,并会收到多次消息(会通过多次调用方法handleMessage(WebSocketSession, WebSocketMessage). )
//如果分为多条消息,那么可以通过一个api:org.springframework.web.socket.WebSocketMessage.isLast() 是否是某条消息的最后一部分。
//默认一般为false,消息不分割
@Override
public boolean supportsPartialMessages() {
return false;
}
/**
* 发送给单个用户
* @param toId 用户编号
* @param textMessage 发送消息
*/
private void sendMessageToOne(String toId,final TextMessage textMessage){
//没有接收人,则发送给全部的在线用户
if(toId==null){
sendMessageToAll(textMessage);
}
WebSocketSession toSession=MyOnLineUserMap.getUSER_ONLINE_SESSION_MAP().get(toId);
//如果不存在,或者是未开启
if(toSession==null||!toSession.isOpen()){
return ;
}
try {
toSession.sendMessage(textMessage);
} catch (IOException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
/**
* 发送给全部的用户
* @param textMessage 发送消息
*/
private void sendMessageToAll(final TextMessage textMessage) {
//遍历所有的在线用户,包括自己
for(Map.Entry<String,WebSocketSession> wsSession:MyOnLineUserMap.getUSER_ONLINE_SESSION_MAP().entrySet()){
//获取 WebSocketSession
WebSocketSession onLineSession=wsSession.getValue();
//是打开的状态
if(onLineSession.isOpen()){
//开启线程
new Thread(new Runnable() {
@Override
public void run() {
if(onLineSession.isOpen()){
//发送消息
try {
onLineSession.sendMessage(textMessage);
System.out.println("发送消息成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
}
}
二.四.三 编写 注册工厂 WebSocketConfig
package com.yjl.websocket.websocket;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
/**
* 注册处理器和拦截器
* @author 两个蝴蝶飞
*
*/
@Component(value="webSocketConfig")
//通过注解 EnableWebSocket
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{
/**
* 注册服务
*/
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyWebSocketHandler(),"/ws").addInterceptors(new MyHandshakeInterceptor());
/*
* 在这里我们用到.withSockJS(),SockJS是spring用来处理浏览器对websocket的兼容性,
* 目前浏览器支持websocket还不是很好,特别是IE11以下.
* SockJS能根据浏览器能否支持websocket来提供三种方式用于websocket请求,
* 三种方式分别是 WebSocket, HTTP Streaming以及 HTTP Long Polling
*/
registry.addHandler(new MyWebSocketHandler(),"ws/sockjs").addInterceptors(new MyHandshakeInterceptor())
.withSockJS();
}
}
后端的处理,算是基本完成了。
二.五 处理前端的 main.jsp 页面
添加 js 脚本。 (不要忘记添加相应的 js 和样式表)
基本的东西,就不讲解了。
<script>
var path='<%=basePath%>';
//定义MyMessage 需要用到的属性信息
//当前进入的id信息
var uid="{sessionScope.loginUser.id}";
var fromId=uid;
var fromNickName='';
//默认是-1, 表示全部接收
var toId=-1;
// 创建一个Socket实例
//参数为URL,ws表示WebSocket协议。onopen、onclose和onmessage方法把事件连接到Socket实例上。每个方法都提供了一个事件,以表示Socket的状态。
var webSocket;
//不同浏览器的WebSocket对象类型不同
if ('WebSocket' in window) {
webSocket = new WebSocket("ws://" + path + "ws");
//火狐
} else if ('MozWebSocket' in window) {
webSocket = new MozWebSocket("ws://" + path + "ws");
} else {
webSocket = new SockJS("http://" + path + "ws/sockjs");
}
//定义四个事件, onopen,onclose,onmessage,onerror
//打开Socket,
webSocket.onopen = function(event) {
//console.log("WebSocket:已连接");
refreshOnLineList();
}
// 监听消息
//onmessage事件提供了一个data属性,它可以包含消息的Body部分。消息的Body部分必须是一个字符串,可以进行序列化/反序列化操作,以便传递更多的数据。
webSocket.onmessage = function(event) {
var data=JSON.parse(event.data);
//console.log("WebSocket:收到一条消息",data);
//2种推送的消息
//1.用户聊天信息:发送消息触发
//2.系统消息:登录和退出触发
//判断是否是欢迎消息(没用户编号的就是欢迎消息)
if(data.fromId==undefined||data.fromId==null||data.fromId==""){
//===系统消息
$("#contentUl").append("<li><b class='dateStyle'>"+data.date+"</b><em class='sysStyle'>系统消息:</em><span class='sysTextStyle'>"+data.text+"</span></li>");
}else{
//===普通消息
//处理一下个人信息的显示:
if(data.fromNickName==fromNickName){
data.fromNickName="我 :";
$("#contentUl").append("<li><span style='display:block; float:right;'><em class='nickNameStyle'>"+data.fromNickName+"</em><span class='textStyle'>"+data.text+"</span><b class='dateStyle'>"+data.date+"</b></span></li><br/>");
}else{
$("#contentUl").append("<li><b class='dateStyle'>"+data.date+"</b><em class='nickNameStyle'>"+data.fromNickName+"</em><span class='textStyle'>"+data.text+"</span></li><br/>");
}
}
//刷新在线用户列表
refreshOnLineList();
scrollToBottom();
};
// 监听WebSocket的关闭
webSocket.onclose = function(event) {
refreshOnLineList();
$("#contentUl").append("<li><b>"+new Date().Format("yyyy-MM-dd hh:mm:ss")+"</b><em>系统消息:</em><span>连接已断开!</span></li>");
scrollToBottom();
};
//监听异常
webSocket.onerror = function(event) {
refreshOnLineList();
$("#contentUl").append("<li><b>"+new Date().Format("yyyy-MM-dd hh:mm:ss")+"</b><em>系统消息:</em><span>连接异常,建议重新登录</span></li>");
scrollToBottom();
};
//onload初始化
$(function(){
//发送消息
$("#sendBtn").on("click",function(){
sendMsg();
});
//给退出聊天绑定事件
$("#exitBtn").on("click",function(){
closeWebsocket();
//跳转到主页
location.href="https://www.ctyun.cn/portal/link.html?target=%24%7BpageContext.request.contextPath%7D%2Findex.jsp";
});
//给输入框绑定事件
$("#msg").on("keydown",function(event){
keySend(event);
});
//初始化时如果有消息,则滚动条到最下面:
scrollToBottom();
});
//使用ctrl+回车快捷键发送消息
function keySend(e) {
var theEvent = window.event || e;
var code = theEvent.keyCode || theEvent.which;
if (theEvent.ctrlKey && code == 13) {
var msg=$("#msg");
if (msg.innerHTML == "") {
msg.focus();
return false;
}
sendMsg();
}
}
//发送消息
function sendMsg(){
//对象为空了
if(webSocket==undefined||webSocket==null){
//alert('WebSocket connection not established, please connect.');
alert('您的连接已经丢失,请退出聊天重新进入');
return;
}
//获取用户要发送的消息内容
var msg=$("#msg").val();
if(msg==""){
return;
}else{
var data={};
data["fromId"]=fromId;
data["fromNickName"]=fromNickName;
data["toId"]=toId;
data["text"]=msg;
//发送消息
webSocket.send(JSON.stringify(data));
//发送完消息,清空输入框
$("#msg").val("");
}
}
//关闭Websocket连接
function closeWebsocket(){
if (webSocket != null) {
webSocket.close();
webSocket = null;
}
}
//div滚动条(scrollbar)保持在最底部
function scrollToBottom(){
//var div = document.getElementById('chatCon');
var div = document.getElementById('up');
div.scrollTop = div.scrollHeight;
}
//格式化日期
Date.prototype.Format = function (fmt) { //author: meizz
var o = {
"M+": this.getMonth() + 1, //月份
"d+": this.getDate(), //日
"h+": this.getHours(), //小时
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
"S": this.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
for (var k in o)
if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
return fmt;
}
/*
刷新在线用户列表
*/
function refreshOnLineList(){
$.ajax({
type : "post",
url : "../User/onlineList",
dataType : "json",
data : {} ,
success : function(data) {
var onlineList=data.onlineList;
//有值的话
if(onlineList){
$("#onlineNum").text(onlineList.length);
$("#online").empty();
$.each(onlineList,function(idx,item){
var $li=$("<li><a href='https://www.ctyun.cn/portal/link.html?target=javascript%3Avoid%280%29%3B' data-id='"+item.id+"'>"+item.nickName+"</a></li>");
$("#online").append($li);
})
addAClickEvent($("#online li a"));
}
}
});
/*
点击私聊事件,暂未处理
*/
function addAClickEvent(target){
target.click(function(){
var clickId=target.attr("data-id");
if(clickId==fromId){
alert("自己不能跟自己聊天");
return ;
}
alert("你要私聊的人的id是:"+clickId);
//打开模态框,输入私聊的信息,进行私聊。
//不在讲解范围之内,可看后续的聊天室项目。
return ;
})
}
}
</script>
三. 运行服务器,测试
火狐浏览器打开网址: http://localhost:8080/chatroom/
输入昵称,“两个蝴蝶飞”
谷歌浏览器打开网址: http://localhost:8080/chatroom/
输入昵称, “岳泽霖”
这个时候, 两个蝴蝶飞的火狐浏览器显示:
经上一章节的各种测试行为,包括退出登录等,服务器均可以推送消息到客户端, WebSocket 功能整合成功。
本章节代码链接为:
链接:https://pan.baidu.com/s/1Tsobj24_ELCjtkoX3_DsnA
提取码:j72d
谢谢您的观看!!!