Flyaway
LiuLichao的技术随笔

Java Spring WebSocket实现后端消息主动推送(下)

接上文:Java Spring WebSocket实现后端消息主动推送(上)

上面一篇文章主要介绍了传统Web实现消息通信的方式以及WebSocket的基本原理,这篇文章将介绍如何构建一个简单的WebSocket消息推送Demo

 

3、Spring WebSocket服务器端实现:

 

3.1 引入项目依赖

使用eclipse建立maven项目后引入相关的依赖jar包,如下:

<properties>
  <java-version>1.6</java-version>
  <org.aspectj-version>1.6.10</org.aspectj-version>
  <org.slf4j-version>1.6.6</org.slf4j-version>
  <!-- Spring -->
  <spring-framework.version>4.1.6.RELEASE</spring-framework.version>
  <!-- Junit -->
  <junit.version>4.11</junit.version>
  <!-- Jackson -->
  <jackson.version>2.6.0</jackson.version>
 </properties>
 <dependencies>
  <!-- Spring -->
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context</artifactId>
   <version>${spring-framework.version}</version>
   <exclusions>
    <!-- Exclude Commons Logging in favor of SLF4j -->
    <exclusion>
     <groupId>commons-logging</groupId>
     <artifactId>commons-logging</artifactId>
     </exclusion>
   </exclusions>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-webmvc</artifactId>
   <version>${spring-framework.version}</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-tx</artifactId>
   <version>${spring-framework.version}</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-beans</artifactId>
   <version>${spring-framework.version}</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-jdbc</artifactId>
   <version>${spring-framework.version}</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-aop</artifactId>
   <version>${spring-framework.version}</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-aspects</artifactId>
   <version>${spring-framework.version}</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-orm</artifactId>
   <version>${spring-framework.version}</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-expression</artifactId>
   <version>${spring-framework.version}</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-test</artifactId>
   <version>${spring-framework.version}</version>
  </dependency>
  
  <!-- Spring WebSocket -->
  <dependency>  
   <groupId>org.springframework</groupId>  
   <artifactId>spring-websocket</artifactId>  
   <version>${spring-framework.version}</version>  
  </dependency>  
  <dependency>  
   <groupId>org.springframework</groupId>  
   <artifactId>spring-messaging</artifactId>  
   <version>${spring-framework.version}</version>  
  </dependency>  
 <!-- jackson -->
 <dependency>
   <groupId>com.fasterxml.jackson.core</groupId>
   <artifactId>jackson-core</artifactId>
   <version>${jackson.version}</version>
 </dependency>
 <dependency>
   <groupId>com.fasterxml.jackson.core</groupId>
   <artifactId>jackson-databind</artifactId>
   <version>${jackson.version}</version>
 </dependency>
 <dependency>
   <groupId>com.fasterxml.jackson.core</groupId>
   <artifactId>jackson-annotations</artifactId>
   <version>${jackson.version}</version>
 </dependency>
    
  <!-- AspectJ -->
  <dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjrt</artifactId>
   <version>${org.aspectj-version}</version>
  </dependency> 
  
  <!-- Logging -->
  <dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>slf4j-api</artifactId>
   <version>${org.slf4j-version}</version>
  </dependency>
  <dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>jcl-over-slf4j</artifactId>
   <version>${org.slf4j-version}</version>
   <scope>runtime</scope>
  </dependency>
  <dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>slf4j-log4j12</artifactId>
   <version>${org.slf4j-version}</version>
   <scope>runtime</scope>
  </dependency>
  <dependency>
   <groupId>log4j</groupId>
   <artifactId>log4j</artifactId>
   <version>1.2.15</version>
   <exclusions>
    <exclusion>
     <groupId>javax.mail</groupId>
     <artifactId>mail</artifactId>
    </exclusion>
    <exclusion>
     <groupId>javax.jms</groupId>
     <artifactId>jms</artifactId>
    </exclusion>
    <exclusion>
     <groupId>com.sun.jdmk</groupId>
     <artifactId>jmxtools</artifactId>
    </exclusion>
    <exclusion>
     <groupId>com.sun.jmx</groupId>
     <artifactId>jmxri</artifactId>
    </exclusion>
   </exclusions>
   <scope>runtime</scope>
  </dependency>

  <!-- @Inject -->
  <dependency>
   <groupId>javax.inject</groupId>
   <artifactId>javax.inject</artifactId>
   <version>1</version>
  </dependency>
    
  <!-- Servlet -->
  <dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>servlet-api</artifactId>
   <version>2.5</version>
   <scope>provided</scope>
  </dependency>
  <dependency>
   <groupId>javax.servlet.jsp</groupId>
   <artifactId>jsp-api</artifactId>
   <version>2.1</version>
   <scope>provided</scope>
  </dependency>
  <dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>jstl</artifactId>
   <version>1.2</version>
  </dependency>
 
  <!-- Test -->
  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>${junit.version}</version>
   <scope>test</scope>
  </dependency>   
  
</dependencies>

123

 

项目依赖如上所示

3.2 、WebSocket相关类:

我们需要编写三个WebSocket的处理类,这里是利用了Spring WebSocket模块,三个类分别是WebSocket处理类SocketHandler、WebSocket配置类WebSocketConfig以及WebSocket拦截器WebSocketInterceptor。

在开始编写之前记得在Spring配置文件中添加Spring注解支持:

<context:component-scan base-package="com.accenture.demo" />

WebSocketConfig.java:

package com.accenture.socket;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

/**
 * @desp websocket配置
 * @author liulichao@ruc.edu.cn
 *
 */
@Configuration
@EnableWebMvc
@EnableWebSocket
public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{
  
  @Autowired
  private SocketHandler socketHandler;

  @Override
  public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    //注册处理拦截器,拦截url为socketServer的请求
    registry.addHandler(socketHandler, "/socketServer").addInterceptors(new WebSocketInterceptor());
    
    //注册SockJs的处理拦截器,拦截url为/sockjs/socketServer的请求
    registry.addHandler(socketHandler, "/sockjs/socketServer").addInterceptors(new WebSocketInterceptor()).withSockJS();
  }

}

如上所示,在WebSocketConfig.java中,我们为SocketHandler注册了两个url请求,并应用了我们所定义的WebSocketInterceptor()拦截器。

WebSocketInterceptor.java:

package com.accenture.socket;

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.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

/**
 * @desp websocket拦截器
 * @author liulichao
 *
 */
public class WebSocketInterceptor implements HandshakeInterceptor{

  @Override
  public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
      WebSocketHandler handler, Exception exception) {
    
  }
  
  /**
   * @desp 将HttpSession中对象放入WebSocketSession中
   */
  @Override
  public boolean beforeHandshake(ServerHttpRequest request,
      ServerHttpResponse response, WebSocketHandler handler,
      Map<String, Object> map) throws Exception {
    if(request instanceof ServerHttpRequest){
      ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
      HttpSession session = servletRequest.getServletRequest().getSession();
      if(session!=null){
        //区分socket连接以定向发送消息
        map.put("user", session.getAttribute("user"));
      }
    }
    return true;
  }

}

如上所示,在执行客户端服务器端握手之前,也就是在beforeHandshake()方法中,我们将HttpSession中我们登录后存储的对象放到WebSocketSession中,以此实现定向发送消息。

SocketHandler.java:

package com.accenture.socket;

import java.io.IOException;
import java.util.ArrayList;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
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;

/**
 * @desp Socket处理类
 * @author liulichao@ruc.edu.cn
 *
 */
@Service
public class SocketHandler implements WebSocketHandler{

  private static final Logger logger;
  private static final ArrayList<WebSocketSession> users;
  
  static{
    users = new ArrayList<WebSocketSession>();
    logger = LoggerFactory.getLogger(SocketHandler.class);
  }
  
  @Override
  public void afterConnectionEstablished(WebSocketSession session)
      throws Exception {
    logger.info("成功建立socket连接");
    users.add(session);
    String username = session.getAttributes().get("user").toString();
    if(username!=null){
      session.sendMessage(new TextMessage("我们已经成功建立soket通信了"));
    }
    
  }

  @Override
  public void handleMessage(WebSocketSession arg0, WebSocketMessage<?> arg1)
      throws Exception {
    // TODO Auto-generated method stub
    
  }

  @Override
  public void handleTransportError(WebSocketSession session, Throwable error)
      throws Exception {
    if(session.isOpen()){
      session.close();
    }
    logger.error("连接出现错误:"+error.toString());
    users.remove(session);
  }
  
  @Override
  public void afterConnectionClosed(WebSocketSession session, CloseStatus arg1)
      throws Exception {
    logger.debug("连接已关闭");
    users.remove(session);
  }

  @Override
  public boolean supportsPartialMessages() {
    return false;
  }
  
  /**
     * 给所有在线用户发送消息
     *
     * @param message
     */
    public void sendMessageToUsers(TextMessage message) {
        for (WebSocketSession user : users) {
            try {
                if (user.isOpen()) {
                    user.sendMessage(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
 
    /**
     * 给某个用户发送消息
     *
     * @param userName
     * @param message
     */
    public void sendMessageToUser(String userName, TextMessage message) {
        for (WebSocketSession user : users) {
            if (user.getAttributes().get("user").equals(userName)) {
                try {
                    if (user.isOpen()) {
                        user.sendMessage(message);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
                break;
            }
        }
    }

}

 

3.3 SpringMVC Controller类:

SocketController.java

package com.accenture.controller;

import javax.servlet.http.HttpSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.socket.TextMessage;

import com.accenture.socket.SocketHandler;

/**
 * @desp Socket控制器
 * @author liulichao@ruc.edu.cn
 * @date 2016-5-6
 *
 */
@Controller
public class SocketController{
  
  private static final Logger logger = LoggerFactory.getLogger(SocketController.class);
  
  @Autowired
  private SocketHandler socketHandler;
  
  @RequestMapping(value="/login")
  public String login(HttpSession session){
    logger.info("用户登录了建立连接啦");
    
    session.setAttribute("user", "liulichao");
    
    return "home";
  }

  @RequestMapping(value = "/message", method = RequestMethod.GET)
  public String sendMessage(){
    
    socketHandler.sendMessageToUser("liulichao", new TextMessage("这是一条测试的消息"));
    
    return "message";
  }

}

如上,在SocketController中我们定义了两个请求处理方法,首先执行login()后再session中存入user对象模拟用户已登录,而sendMessage()方法则是调用了sendMessageToUser()实现向某一个用户推送消息。

整个包结构如下:

833A80BB-9A4F-4471-B7A7-377318FE66AD

 

同时,在web.xml中需要所有servlet与filter中需要加入异步支持:

<async-supported>true</async-supported>

web.xml如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3.1.xsd">

   <!-- 文件编码过滤器 -->
   <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>
        <async-supported>true</async-supported>
    </filter>  
    <filter-mapping>  
        <filter-name>characterEncodingFilter</filter-name>  
        <url-pattern>/*</url-pattern>  
    </filter-mapping>
    
   <!-- Creates the Spring Container shared by all Servlets and Filters -->
   <listener>
      <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
   </listener>
   
   <!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
   <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:spring/application-config.xml</param-value>
   </context-param>
   

   <!-- Processes application requests -->
   <servlet>
      <servlet-name>appServlet</servlet-name>
      <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
      <init-param>
         <param-name>contextConfigLocation</param-name>
         <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
      </init-param>
      <load-on-startup>1</load-on-startup>
      <async-supported>true</async-supported>
   </servlet>
   
   <!-- Creates the Spring Container shared by all Servlets and Filters -->
   <listener>
      <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
   </listener>
      
   <servlet-mapping>
      <servlet-name>appServlet</servlet-name>
      <url-pattern>/</url-pattern>
   </servlet-mapping>

</web-app>

 

4、前端页面实现:

 

4.1 SockJS

因为部分浏览器不支持WebSocket,因此我们需要应用SockJS。SockJS 是一个浏览器上运行的 JavaScript 库,如果浏览器不支持 WebSocket,该库可以模拟对 WebSocket 的支持,实现浏览器和 Web 服务器之间低延迟、全双工、跨域的通讯通道。

GitHub:https://github.com/sockjs/sockjs-client

 

4.2 前端页面

模拟登录页面home.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
String wsPath = "ws://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<html>
<head>
  <title>Home</title>
</head>
<body>
<h1>
  Hello world!  This is a WebSocket demo!
  <div id="message">
    
  </div>
</h1>

<script type="text/javascript" src="js/jquery-1.12.2.min.js"></script>
<script type="text/javascript" src="js/sockjs.min.js"></script>


<script type="text/javascript">
  
  $(function(){
    //建立socket连接
    var sock;
    if ('WebSocket' in window) {
      sock = new WebSocket("<%=wsPath%>socketServer");
      } else if ('MozWebSocket' in window) {
        sock = new MozWebSocket("<%=wsPath%>socketServer");
      } else {
        sock = new SockJS("<%=basePath%>sockjs/socketServer");
      }
    sock.onopen = function (e) {
      console.log(e);
      };
      sock.onmessage = function (e) {
        console.log(e)
          $("#message").append("<p><font color='red'>"+e.data+"</font>")
      };
      sock.onerror = function (e) {
        console.log(e);
      };
      sock.onclose = function (e) {
        console.log(e);
      }
  });
  
</script>

</body>
</html>

如上所示,我们访问login模拟登录后之后将跳到home.jsp,在页面中通过sockjs与服务器端我们注册的WebSocket访问接口实现握手建立socket连接。

 

推送消息页面message.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>message</title>
</head>
<body>
  <h1>已经发送消息了</h1>
  
</body>
</html>

在访问/message过后将调用消息发送的函数实现后端消息向前端的推送。

 

4、测试

首先,我们访问login函数,将跳至home.jsp视图,出现如下的页面:

1212

 

在这一步中,我们已经在后台模拟登录,session中保存了user的对象,紧接着,我们再在新页面中访问message函数:

 

76E4291B-57D2-4829-A792-8B0CC3265258

 

这时候我们再回去看home.jsp的页面:

 

3434

可以看到,页面实现了更新,同时在socketServer握手请求之后并没有发生http的请求,同时我们可以在console中看到打印出来的通信的数据:

 

3232

 

至此,我们成功实现了服务器端向客户端的消息推送。

整个maven项目的代码在附件中提供下载,如有任何问题可以通过liulichaoruc@gmail.com联系我

if you have any problem,you can contact me via liulichaoruc@gmail.com

版权所有,转载注明出处

 

附件: webSocket-1.zip (下载489)

 

未经允许不得转载:Flyaway的技术随笔 » Java Spring WebSocket实现后端消息主动推送(下)

分享到:更多 ()

随便写的东西