勿以恶小而为之,勿以善小而不为--------------------------刘备

劝诸君,多行善事积福报,莫作恶

上一章简单介绍了 Servlet实现WebSocket的简单聊天室(二),如果没有看过,​​请观看上一章​​

本文代码参考了黑马教程视频的 “即时通信技术-Websocket实现在线聊天室” 的代码实例。

一. Spring 整合 WebSocket

一.一 提升点

相对于上一章节的代码实例,主要有以下几个增强点:

  1. 将WebSocket 与Spring 进行整合,扩展性好。
  2. 聊天室人员的名称由用户自已输入,而不是系统指定。
  3. 增加了在线人员列表展示的功能
  4. 转义字符如 <,>等处理。
  5. 界面和功能优化了很多。

一.二 整合流程讲解

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();
}
  1. 方法 afterConnectionEstablished(),是连接之后进行的操作,类似于以前的 onopen 方法。 里面有一个参数 WebSocketSession,表示连接进来的那一个 Session. 可以通过 getAttributes() 方法,获取 HandshakeInterceptor 拦截器放置的 paramMap 集合。
  2. 方法 handleMessage(), 是服务器接收浏览器发送过来的消息进行的操作,类似于以前的 onmessage 方法。 WebSocketSession 对象表示 发送消息的那一个Session, WebSocketMessage 表示发送的消息主体。
  3. 方法 handleTransportError()是出现异常时进行的操作,类似于以前的 onerror 方法。 WebSocketSession 对象表示哪一个Session 出现了错误异常。
  4. 方法 afterConnectionClosed(),是浏览器断开连接或者服务器断开连接的操作,类似于以前的 onclose 方法,WebSocketSession 表示 断开的是哪一个Session
  5. 方法 supportsPartialMessages() 表示是否支持拆分。 当浏览器输入的内容过多时,允不允许将接收到的内容,进行拆分处理。通常不允许拆分, 返回 false 即可。
一.二.二.二 接口方法处理操作

在执行 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>

二.二 前端页面处理

Spring整合WebSocket(三)_实现聊天室

二.二.一 前端 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" style="color:#D33;margin-top:30px;">
<h2>聊天室登录页面</h2>
</div>
<div style="margin-top:40px;">
<form action="${pageContext.request.contextPath}/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>

展示大致效果如下所示:

Spring整合WebSocket(三)_spring_02

二.二.四 主页展示 /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" style="color:#D33;margin-top:30px;padding-left: 60px;">
<div class="col-sm-7" style="font-size:26px;">欢迎进入 '两个蝴蝶飞' 聊天室</div>
<div class="col-sm-4 col-sm-offset-1">
<p>当前登录用户:${sessionScope.loginUser!=null?sessionScope.loginUser.nickName:"请登录" }   <button id="exitBtn" class="btn btn-default">退出或重新登录</button></p>
</div>

</div>
<div class="row" style="margin-top:30px;">
<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="请输入你想发送的消息" style="min-width: 50%;width:90%;"></textarea>
</div>
<div class="form-group col-sm-offset-9" style="margin-top:30px;">
<input type="button" value="发送消息" id="sendBtn" name="sendBtn" class="btn btn-success"/>
</div>

</div>
</div>
</div>
</div>
</body>

展示大致效果如下所示:

Spring整合WebSocket(三)_整合WebSocket_03

二.三 实体封装处理和控制器

二.三.一 封装用户信息 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='${sessionScope.loginUser.nickName}';

//默认是-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="${pageContext.request.contextPath}/index.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='javascript:void(0);' 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/

输入昵称,“两个蝴蝶飞”

Spring整合WebSocket(三)_整合WebSocket_04

点击进入

Spring整合WebSocket(三)_WebSocket实现聊天室_05

谷歌浏览器打开网址: http://localhost:8080/chatroom/

输入昵称, “岳泽霖”

Spring整合WebSocket(三)_spring_06

点击进入

Spring整合WebSocket(三)_整合WebSocket_07

这个时候, 两个蝴蝶飞的火狐浏览器显示:

Spring整合WebSocket(三)_spring_08

经上一章节的各种测试行为,包括退出登录等,服务器均可以推送消息到客户端, WebSocket 功能整合成功。

本章节代码链接为:

链接:https://pan.baidu.com/s/1Tsobj24_ELCjtkoX3_DsnA 
提取码:j72d

谢谢您的观看!!!