一. Spring 整合 WebSocket
一.一 提升点
相对于上一章节的代码实例,主要有以下几个增强点:
- 将WebSocket 与Spring 进行整合,扩展性好。
- 聊天室人员的名称由用户自已输入,而不是系统指定。
- 增加了在线人员列表展示的功能
- 转义字符如 <,>等处理。
- 界面和功能优化了很多。
一.二 整合流程讲解
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(),是连接之后进行的操作,类似于以前的 onopen 方法。 里面有一个参数 WebSocketSession,表示连接进来的那一个 Session. 可以通过 getAttributes() 方法,获取 HandshakeInterceptor 拦截器放置的 paramMap 集合。
- 方法 handleMessage(), 是服务器接收浏览器发送过来的消息进行的操作,类似于以前的 onmessage 方法。 WebSocketSession 对象表示 发送消息的那一个Session, WebSocketMessage 表示发送的消息主体。
- 方法 handleTransportError()是出现异常时进行的操作,类似于以前的 onerror 方法。 WebSocketSession 对象表示哪一个Session 出现了错误异常。
- 方法 afterConnectionClosed(),是浏览器断开连接或者服务器断开连接的操作,类似于以前的 onclose 方法,WebSocketSession 表示 断开的是哪一个Session
- 方法 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>
二.二 前端页面处理
二.二.一 前端 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>
展示大致效果如下所示:
二.二.四 主页展示 /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>
展示大致效果如下所示: