UDN-企业互联网技术人气社区

板块导航

浏览  : 1345
回复  : 0

[讨论交流] java使用websocket,并且获取HttpSession,源码分析

[复制链接]
htmlman的头像 楼主
发表于 2017-1-10 15:28:57 | 显示全部楼层 |阅读模式
  一:本文使用范围

  此文不仅仅局限于spring boot,普通的spring工程,甚至是servlet工程,都是一样的,只不过配置一些监听器的方法不同而已。

  二:Spring boot使用websocket

  2.1:依赖包

  websocket本身是servlet容器所提供的服务,所以需要在web容器中运行,像我们所使用的tomcat,当然,spring boot中已经内嵌了tomcat。

  websocket遵循了javaee规范,所以需要引入javaee的包
  1. <dependency>
  2.       <groupId>javax</groupId>
  3.       <artifactId>javaee-API</artifactId>
  4.       <version>7.0</version>
  5.       <scope>provided</scope>
  6.     </dependency>
复制代码

  当然,其实tomcat中已经自带了这个包。

  如果是在spring boot中,还需要加入websocket的starter
  1.         <dependency>
  2.             <groupId>org.springframework.boot</groupId>
  3.             <artifactId>spring-boot-starter-websocket</artifactId>
  4.             <version>1.4.0.RELEASE</version>
  5.         </dependency>
复制代码

  2.2:配置websocket

  如果不是spring boot项目,那就不需要进行这样的配置,因为如果在tomcat中运行的话,tomcat会扫描带有@ServerEndpoint的注解成为websocket,而spring boot项目中需要由这个bean来提供注册管理。
  1. @Configuration
  2. public class WebSocketConfig {
  3.     @Bean
  4.     public ServerEndpointExporter serverEndpointExporter() {
  5.         return new ServerEndpointExporter();
  6.     }

  7. }
复制代码

  2.3:websocket的java代码

  使用websocket的核心,就是一系列的websocket注解,@ServerEndpoint是注册在类上面开启。
  1. @ServerEndpoint(value = "/websocket")
  2. @Component
  3. public class MyWebSocket {


  4.     //与某个客户端的连接会话,需要通过它来给客户端发送数据
  5.     private Session session;

  6.     /**
  7.      * 连接成功*/
  8.     @OnOpen
  9.     public void onOpen(Session session) {
  10.         this.session = session;

  11.     }

  12.     /**
  13.      * 连接关闭调用的方法
  14.      */
  15.     @OnClose
  16.     public void onClose() {

  17.     }

  18.     /**
  19.      * 收到消息
  20.      *
  21.      * @param message
  22.     */
  23.     @OnMessage
  24.     public void onMessage(String message, Session session) {
  25.         System.out.println("来自浏览器的消息:" + message);

  26.         //群发消息
  27.         for (MyWebSocket item : webSocketSet) {
  28.             try {
  29.                 item.sendMessage(message);
  30.             } catch (IOException e) {
  31.                 e.printStackTrace();
  32.             }
  33.         }
  34.     }

  35.     /**
  36.      * 发生错误时调用
  37.      */
  38.     @OnError
  39.     public void onError(Session session, Throwable error) {
  40.         System.out.println("发生错误");
  41.         error.printStackTrace();
  42.     }

  43.     public void sendMessage(String message) throws IOException {
  44.         this.session.getBasicRemote().sendText(message);//同步
  45.         //this.session.getAsyncRemote().sendText(message);//异步
  46.     }




  47. }
复制代码

  其实我也感觉很奇怪,为什么不使用接口来规范。即使是因为@ServerEndpoint注解中其它属性中可以定义出一些额外的参数,但相信也是可以抽象出来的,不过想必javaee这样做,应该是有它的用意吧。

  2.4:浏览器端的代码

  浏览器端的代码需要浏览器支持websocket,当然,也有socket.js可以支持到ie7,但是这个我没用过。毕竟ie基本上没人用的,市面上的浏览器基本上全部都支持websocket。
  1. <!DOCTYPE HTML>
  2. <html>
  3. <head>

  4. </head>

  5. <body>

  6. </body>

  7. <script type="text/javascript">
  8.     var websocket = null;

  9.     //判断当前浏览器是否支持WebSocket
  10.     if('WebSocket' in window){
  11.         websocket = new WebSocket("ws://localhost:9999/websocket");
  12.     }
  13.     else{
  14.         alert('不支持websocket')
  15.     }

  16.     //连接发生错误
  17.     websocket.onerror = function(){
  18.         
  19.     };

  20.     //连接成功
  21.     websocket.onopen = function(event){
  22.         
  23.     }

  24.     //接收到消息
  25.     websocket.onmessage = function(event){
  26.         var msg = event.data;
  27.         alert("收到消息:" + msg);
  28.     }

  29.     //连接关闭
  30.     websocket.onclose = function(){
  31.         
  32.     }

  33.     //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
  34.     window.onbeforeunload = function(){
  35.         websocket.close();
  36.     }

  37.    

  38.   

  39.     //发送消息
  40.     function send(message){
  41.         websocket.send(message);
  42.     }
  43. </script>
  44. </html>
复制代码

  如此就连接成功了。

  三:获取HttpSession,源码分析

  获取HttpSession是一个很有必要讨论的问题,因为java后台需要知道当前是哪个用户,用以处理该用户的业务逻辑,或者是对该用户进行授权之类的,但是由于websocket的协议与Http协议是不同的,所以造成了无法直接拿到session。但是问题总是要解决的,不然这个websocket协议所用的场景也就没了。

  3.1:获取HttpSession的工具类,源码详细分析

  我们先来看一下@ServerEndpoint注解的源码
  1. package javax.websocket.server;

  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;

  6. import javax.websocket.Decoder;
  7. import javax.websocket.Encoder;

  8. @Retention(RetentionPolicy.RUNTIME)
  9. @Target(ElementType.TYPE)
  10. public @interface ServerEndpoint {

  11.     /**
  12.      * URI or URI-template that the annotated class should be mapped to.
  13.      * @return The URI or URI-template that the annotated class should be mapped
  14.      *         to.
  15.      */
  16.     String value();

  17.     String[] subprotocols() default {};

  18.     Class<? extends Decoder>[] decoders() default {};

  19.     Class<? extends Encoder>[] encoders() default {};

  20.     public Class<? extends ServerEndpointConfig.Configurator> configurator()
  21.             default ServerEndpointConfig.Configurator.class;
  22. }
复制代码

  我们看到最后的一个方法,也就是加粗的方法。可以看到,它要求返回一个ServerEndpointConfig.Configurator的子类,我们写一个类去继承它。
  1. import javax.servlet.http.HttpSession;
  2. import javax.websocket.HandshakeResponse;
  3. import javax.websocket.server.HandshakeRequest;
  4. import javax.websocket.server.ServerEndpointConfig;
  5. import javax.websocket.server.ServerEndpointConfig.Configurator;


  6. public class HttpSessionConfigurator extends Configurator {

  7.     @Override
  8.     public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {

  9.     //怎么搞?
  10.     }
  11. }
复制代码

  当我们覆盖modifyHandshake方法时,可以看到三个参数,其中后面两个参数让我们感觉有点见过的感觉,我们查看一HandshakeRequest的源码
  1. package javax.websocket.server;

  2. import java.net.URI;
  3. import java.security.Principal;
  4. import java.util.List;
  5. import java.util.Map;

  6. /**
  7. * Represents the HTTP request that asked to be upgraded to WebSocket.
  8. */
  9. public interface HandshakeRequest {

  10.     static final String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key";
  11.     static final String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol";
  12.     static final String SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version";
  13.     static final String SEC_WEBSOCKET_EXTENSIONS= "Sec-WebSocket-Extensions";

  14.     Map<String,List<String>> getHeaders();

  15.     Principal getUserPrincipal();

  16.     URI getRequestURI();

  17.     boolean isUserInRole(String role);

  18.     /**
  19.      * Get the HTTP Session object associated with this request. Object is used
  20.      * to avoid a direct dependency on the Servlet API.
  21.      * @return The javax.servlet.http.HttpSession object associated with this
  22.      *         request, if any.
  23.      */
  24.     Object getHttpSession();

  25.     Map<String, List<String>> getParameterMap();

  26.     String getQueryString();
  27. }
复制代码

  我们发现它是一个接口,接口中规范了这样的一个方法
  1.     /**
  2.      * Get the HTTP Session object associated with this request. Object is used
  3.      * to avoid a direct dependency on the Servlet API.
  4.      * @return The javax.servlet.http.HttpSession object associated with this
  5.      *         request, if any.
  6.      */
  7.     Object getHttpSession();
复制代码

  上面有相应的注释,说明可以从Servlet API中获取到相应的HttpSession。

  当我们发现这个方法的时候,其实已经松了一口气了。

  那么我们就可以补全未完成的代码
  1. import javax.servlet.http.HttpSession;
  2. import javax.websocket.HandshakeResponse;
  3. import javax.websocket.server.HandshakeRequest;
  4. import javax.websocket.server.ServerEndpointConfig;
  5. import javax.websocket.server.ServerEndpointConfig.Configurator;

  6. /**
  7. * 从websocket中获取用户session
  8. *
  9. *
  10. */
  11. public class HttpSessionConfigurator extends Configurator {

  12.     @Override
  13.     public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {

  14.           HttpSession httpSession = (HttpSession) request.getHttpSession();
  15.       
  16.           sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
  17.     }
  18. }
复制代码

  其实通过上面的源码分析,你们应该知道了HttpSession的获取。但是下面又多了一行代码
  1. sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
复制代码

  这行代码又是什么意思呢?

  我们看一下ServerEnpointConfig的声明
  1. public interface ServerEndpointConfig extends EndpointConfig
复制代码

  我们发现这个接口继承了EndpointConfig的接口,好,我们看一下EndpointConfig的源码:
  1. package javax.websocket;

  2. import java.util.List;
  3. import java.util.Map;

  4. public interface EndpointConfig {

  5.     List<Class<? extends Encoder>> getEncoders();

  6.     List<Class<? extends Decoder>> getDecoders();

  7.     Map<String,Object> getUserProperties();
  8. }
复制代码

  我们发现了这样的一个方法定义
  1. Map<String,Object> getUserProperties();
复制代码

  可以看到,它是一个map,从方法名也可以理解到,它是用户的一些属性的存储,那既然它提供了get方法,那么也就意味着我们可以拿到这个map,并且对这里面的值进行操作,

  所以就有了上面的
  1. sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
复制代码

  那么到此,获取HttpSession的源码分析,就完成了。

  3.2:设置HttpSession的类

  我们之前有说过,由于HTTP协议与websocket协议的不同,导致没法直接从websocket中获取协议,然后在3.1中我们已经写了获取HttpSession的代码,但是如果真的放出去执行,那么会报空指值异常,因为这个HttpSession并没有设置进去。

  好,这一步,我们来设置HttpSession。这时候我们需要写一个监听器。
  1. import javax.servlet.ServletRequestEvent;
  2. import javax.servlet.ServletRequestListener;
  3. import javax.servlet.http.HttpServletRequest;

  4. import org.springframework.stereotype.Component;

  5. @Component
  6. public class RequestListener implements ServletRequestListener {

  7.     public void requestInitialized(ServletRequestEvent sre)  {
  8.         //将所有request请求都携带上httpSession
  9.         ((HttpServletRequest) sre.getServletRequest()).getSession();

  10.     }
  11.     public RequestListener() {
  12.     }

  13.     public void requestDestroyed(ServletRequestEvent arg0)  {
  14.     }
  15. }
复制代码

  然后我们需要把这个类注册为监听器 ,如果是普通的Spring工程,或者是servlet工程,那么要么在web.xml中配置,要么使用 @WebListener注解。

  因为本文是以Spring boot工程来演示,所以这里只写Spring boot配置Listener的代码,其它的配置方式,请自行百度。

  这是使用@Bean注解的方式
  1. @Autowird
  2. private RequestListener requestListener;
  3.   

  4.     @Bean
  5.     public ServletListenerRegistrationBean<RequestListener> servletListenerRegistrationBean() {
  6.         ServletListenerRegistrationBean<RequestListener> servletListenerRegistrationBean = new ServletListenerRegistrationBean<>();
  7.         servletListenerRegistrationBean.setListener(requestListener);
  8.         return servletListenerRegistrationBean;
  9.     }
复制代码

  或者也可以使用@WebListener注解

  然后使用@ServletComponentScan注解,配置在启动方法上面。

  3.3:在websocket中获取用户的session

  然后刚才我们通过源码分析,是知道@ServerEndpoint注解中是有一个参数可以配置我们刚才继承的类。好的,我们现在进行配置。
  1. @ServerEndpoint(value = "/websocket" , configurator = HttpSessionConfigurator.class)
复制代码

  接下来就可以在@OnOpen注解中所修饰的方法中,拿到EndpointConfig对象,并且通过这个对象,拿到之前我们设置进去的map
  1. @OnOpen
  2.     public void onOpen(Session session,EndpointConfig config){
  3.         HttpSession httpSession= (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
  4.         User user = (User)httpSession.getAttribute(SessionName.USER);
  5.         if(user != null){
  6.             this.session = session;
  7.             this.httpSession = httpSession;
  8.         }else{
  9.             //用户未登陆
  10.             try {
  11.                 session.close();
  12.             } catch (IOException e) {
  13.                 e.printStackTrace();
  14.             }
  15.         }
  16.     }
复制代码

  这下我们就从java的webscoket中拿到了用户的session。

相关帖子

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部