Java Spring WebSocket消息交互

Spring 4.0的一個最大更新是增加了對Websocket的支持。Websocket提供了一個在web應(yīng)用中實(shí)現(xiàn)高效、雙向通訊,需考慮客戶端(瀏覽器)和服務(wù)端之間高頻和低延時消息交換的機(jī)制。一般的應(yīng)用場景有:在線交易、網(wǎng)頁聊天、游戲、協(xié)作、數(shù)據(jù)可視化等。

1. Websocket原理

  • Websocket協(xié)議本質(zhì)上是一個基于TCP的獨(dú)立協(xié)議,能夠在瀏覽器和服務(wù)器之間建立雙向連接,以基于消息的機(jī)制,賦予瀏覽器和服務(wù)器間實(shí)時通信能力。
  • WebSocket資源URI采用了自定義模式:ws表示純文本通信,其連接地址寫法為“ws://**”,占用與http相同的80端口;wss表示使用加密信道通信(TCP+TLS),基于SSL的安全傳輸,占用與TLS相同的443端口。

2. Websocket與HTTP比較

Websocket和HTTP都是基于TCP協(xié)議
TCP是傳輸層協(xié)議,Websocket和HTTP是應(yīng)用層協(xié)議

HTTP是用于文檔傳輸、簡單同步請求的響應(yīng)式協(xié)議,本質(zhì)上是無狀態(tài)的應(yīng)用層協(xié)議,半雙工的連接特性。Websocket與 HTTP 之間的唯一關(guān)系就是它的握手請求可以作為一個升級請求(Upgrade request)經(jīng)由 HTTP 服務(wù)器解釋(也就是可以使用Nginx反向代理一個WebSocket)。

聯(lián)系:

客戶端建立WebSocket連接時發(fā)送一個header,標(biāo)記了Upgrade的HTTP請求,表示請求協(xié)議升級。
服務(wù)器直接在現(xiàn)有的HTTP服務(wù)器軟件和端口上實(shí)現(xiàn)Websocket,重用現(xiàn)有代碼(比如解析和認(rèn)證這個HTTP請求),然后再回一個狀態(tài)碼為101(協(xié)議轉(zhuǎn)換)的HTTP響應(yīng)完成握手,之后發(fā)送數(shù)據(jù)就跟HTTP沒關(guān)系了。

區(qū)別:

  • 持久性:

HTTP協(xié)議:
HTTP是非持久的協(xié)議(長連接、循環(huán)連接除外)
Websocket協(xié)議:
Websocket是持久化的協(xié)議

  • 生命周期:

HTTP的生命周期通過Request來界定,也就是一個Request 一個Response
HTTP1.0中,這次HTTP請求就結(jié)束了;
HTTP1.1中進(jìn)行了改進(jìn),使得有一個keep-alive,也就是說,在一個HTTP連接中,可以發(fā)送多個Request,并接收多個Respouse。<br />
在HTTP中永遠(yuǎn)都是一個Request只有一個Respouse,而且這個Respouse是被動的,不能主動發(fā)起。

3. Spring Websocket項(xiàng)目搭建

3.1 Websocket服務(wù)端

Spring4.0新增一個對Websocket提供廣泛支持的Spring-Websocket模塊,其兼容于JAVA Websocket API標(biāo)準(zhǔn)(JSR-356)。

3.1.1 pom.xml配置

在此基于maven搭建項(xiàng)目,引入Spring Websocket所需的jar包,以及對傳輸?shù)南Ⅲw進(jìn)行JSON序列化所需的jar包。

<properties>
    <!-- Spring -->
    <spring-framework.version>4.1.6.RELEASE</spring-framework.version>
    <!-- Jackson -->
    <jackson.version>2.6.0</jackson.version>
</properties>

<dependencies>
    <!-- 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>       
</dependencies>    

3.1.2 web.xml配置

配置Spring過濾器,并設(shè)置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>
    <async-supported>true</async-supported>
</filter>  
<filter-mapping>  
    <filter-name>characterEncodingFilter</filter-name>  
    <url-pattern>/*</url-pattern>  
</filter-mapping>    

其中的關(guān)鍵為org.springframework.web.filter.CharacterEncodingFilter,此過濾器對請求按指定的字符編碼方式進(jìn)行編碼。因?yàn)椋词乖贖TML頁面中指定了字符編碼方式,通常瀏覽器也并未指定請求的字符編碼方式。CharacterEncodingFilter重寫了doFilterInternal方法,不僅可以指定請求的編碼方式,同時也可使得響應(yīng)的編碼方式與請求一致。

@Override
protected void doFilterInternal(
        HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

    if (this.encoding != null && (this.forceEncoding || request.getCharacterEncoding() == null)) {
        request.setCharacterEncoding(this.encoding);
        if (this.forceEncoding) {
            response.setCharacterEncoding(this.encoding);
        }
    }
    filterChain.doFilter(request, response);
}

3.1.3 WebSocketHandler接口實(shí)現(xiàn)

實(shí)現(xiàn)WebSocketHandler接口并重寫接口中的方法,為消息的處理實(shí)現(xiàn)定制化。Spring Websocket通過WebSocketSession建立會話,發(fā)送消息或關(guān)閉會話。Websocket可發(fā)送兩類消息體,分別為文本消息TextMessage和二進(jìn)制消息BinaryMessage,兩類消息都實(shí)現(xiàn)了WebSocketMessage接口(A message that can be handled or sent on a WebSocket connection.)

/**
 * @desp Socket處理類
 * @author hejun
 * @date 2016-07-25
 *
 */
@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);
    }

    // Websocket連接建立
    @Override
    public void afterConnectionEstablished(WebSocketSession session)
        throws Exception {
        logger.info("成功建立Websocket連接");
        users.add(session);
        String username = session.getAttributes().get("user").toString();
        // 判斷session中用戶信息 
        if(username!=null){
            session.sendMessage(new TextMessage("已成功建立Websocket通信"));
        }       
    }

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

    // 當(dāng)連接出錯時,主動關(guān)閉當(dāng)前連接,并從會話列表中刪除該會話
    @Override
    public void handleTransportError(WebSocketSession session, Throwable error)
        throws Exception {
        if(session.isOpen()){
            session.close();
        }
        logger.error("連接出現(xiàn)錯誤:"+error.toString());
        users.remove(session);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus arg1)
        throws Exception {
        logger.debug("Websocket連接已關(guān)閉");
        users.remove(session);
    }

    @Override
    public boolean supportsPartialMessages() {
        return false;
    }

    /**
     * 給所有在線用戶發(fā)送消息
     *
     * @param message
     */
    public void sendMessageToUsers(TextMessage message) {
        for (WebSocketSession user : users) {
            try {
                if (user.isOpen()) {
                    user.sendMessage(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 給某個用戶發(fā)送消息
     *
     * @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.1.4 WebSocket激活

現(xiàn)在最重要的是,在Spring中激活Websocket。

Websocket配置類:
/**
 * @desp websocket激活配置
 * @author hejun
 * @date 2016-07-25
 *
 */
@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();
    }
}

如代碼所見
① 配置類添加注解@EnableWebSocket,因?yàn)镋nableWebSocket引入了DelegatingWebSocketConfiguration配置(@Import(DelegatingWebSocketConfiguration.class)),且DelegatingWebSocketConfiguration繼承了WebSocketConfigurationSupport的配置特性,所以@EnableWebSocket實(shí)現(xiàn)對Websocket請求的高效處理。

DelegatingWebSocketConfiguration:
@Configuration
public class DelegatingWebSocketConfiguration extends WebSocketConfigurationSupport {

    private final List<WebSocketConfigurer> configurers = new ArrayList<WebSocketConfigurer>();

    @Autowired(required = false)
    public void setConfigurers(List<WebSocketConfigurer> configurers) {
        if (!CollectionUtils.isEmpty(configurers)) {
            this.configurers.addAll(configurers);
        }
    }


    @Override
    protected void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        for (WebSocketConfigurer configurer : this.configurers) {
            configurer.registerWebSocketHandlers(registry);
        }
    }
}

此類主要是添加Websocket的默認(rèn)配置,那Websocket的默認(rèn)配置具體是什么樣呢?那就得看WebSocketConfigurationSupport了。

WebSocketConfigurationSupport:
/**
 * Configuration support for WebSocket request handling.
 *
 * @author Rossen Stoyanchev
 * @since 4.0
 */
public class WebSocketConfigurationSupport {

    @Bean
    public HandlerMapping webSocketHandlerMapping() {
        ServletWebSocketHandlerRegistry registry = new ServletWebSocketHandlerRegistry(defaultSockJsTaskScheduler());
        registerWebSocketHandlers(registry);
        return registry.getHandlerMapping();
    }

    protected void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    }

    @Bean
    public ThreadPoolTaskScheduler defaultSockJsTaskScheduler() {
            ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setThreadNamePrefix("SockJS-");
        scheduler.setPoolSize(Runtime.getRuntime().availableProcessors());
        scheduler.setRemoveOnCancelPolicy(true);
        return scheduler;
    }
}

注意查看Websocket的默認(rèn)配置中,SockJS默認(rèn)的線程管理defaultSockJsTaskScheduler()便可知道scheduler.setPoolSize(Runtime.getRuntime().availableProcessors());Websocket的線程池容量可根據(jù)當(dāng)前運(yùn)行環(huán)境可用的CPU核數(shù)動態(tài)配置,充分利用CPU資源;scheduler.setRemoveOnCancelPolicy(true);Websocket的本地線程池可將cancelled tasks自動踢出work queue at time of cancellation,釋放線程資源。注意:removeOnCancel屬性默認(rèn)為false,且setRemoveOnCancelPolicy(true);屬性設(shè)置僅在JDK1.7之上支持。
② extends WebMvcConfigurerAdapter重寫addInterceptors()方法,在Spring MVC中實(shí)現(xiàn)對請求握手的攔截處理,具體的處理方法見Websocket攔截器類代碼(此方案可借鑒于其他應(yīng)用項(xiàng)目中!)。
③ 可用兩種方法注冊處理攔截器,一種為通過Websocket的通信方式,一種為降級的通過SockJS的通信方式(SockJS是一個JavaScript庫,提供跨瀏覽器JavaScript的API,創(chuàng)建了一個低延遲、全雙工的瀏覽器和web服務(wù)器之間通信通道)。

Websocket攔截器類:
/**
 * @desp websocket攔截器
 * @author hejun
 * @date 2016-07-25
 *
 */
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){
                //區(qū)分socket連接以定向發(fā)送消息
                map.put("user", session.getAttribute("user"));
            }
        }
        return true;
    }
}

該攔截器實(shí)現(xiàn)了HandshakeInterceptor接口,HandshakeInterceptor可攔截Websocket的握手請求(通過HTTP協(xié)議)并可設(shè)置與Websocket session建立連接的HTTP握手連接的屬性值。實(shí)例中配置重寫了beforeHandshake方法,將HttpSession中對象放入WebSocketSession中,實(shí)現(xiàn)后續(xù)通信。

3.2 Websocket客戶端

當(dāng)Websocket的服務(wù)端配置完成后, 使用sockjs-client創(chuàng)建一個JSP頁面與服務(wù)端消息系統(tǒng)交互,創(chuàng)建connect方法建立連接,sendMessage方法發(fā)送消息,disconnect方法關(guān)閉連接。


Wesocket_Project.png

3.2.1 頁面代碼編寫

因?yàn)楫?dāng)Websocket建立通信后,客戶端和服務(wù)端即可實(shí)現(xiàn)了雙向通信。此Demo中home頁面模擬客戶端接收服務(wù)端發(fā)送的消息,message頁面模擬服務(wù)端發(fā)送消息,當(dāng)然也可以作為客戶端接收服務(wù)端消息。

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!
        </h1>
        <div id="message">
        </div>
        <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(){
                //通過HTTP協(xié)議自動建立socket連接,服務(wù)端對"/socketServer"和"/sockjs/socketServer"進(jìn)行攔截
                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> 
message.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>message</title>
    </head>
    <body>
        <h1>已經(jīng)發(fā)送消息了</h1>
    </body>
</html>   

3.2.2 Spring MVC Controller編寫

Controller用于通過"/login"發(fā)送Spring MVC的HTTP請求,建立Websocket連接(之后就沒有該HTTP的什么事了);使用Spring DI方式獲取所需SocketHandler對象,模擬服務(wù)端發(fā)送消息(此段應(yīng)該為服務(wù)端代碼)。

/**
 * @desp Socket控制器
 * @author hejun
 * @date 2016-07-25
 *
 */
@Controller
public class SocketController{

    private static final Logger logger = LoggerFactory.getLogger(SocketController.class);

    @Autowired
    private SocketHandler socketHandler;

    // 服務(wù)端Spring MVC攔截該HTTP請求,將HTTP Session載入Websocket Session中,建立會話 
    @RequestMapping(value="/login")
    public String login(HttpSession session){
        logger.info("用戶登錄建立Websocket連接");       
        session.setAttribute("user", "hejun");
        return "home";
    }
      
    // 模擬服務(wù)端發(fā)送消息,其中可實(shí)現(xiàn)消息的廣發(fā)或指定對象發(fā)送
    @RequestMapping(value = "/message", method = RequestMethod.GET)
    public String sendMessage(){        
        double rand = Math.ceil(Math.random()*100);
        socketHandler.sendMessageToUser("hejun", new TextMessage("Websocket測試消息" + rand));      
        return "message";
    }
}

3.2.3 Websocket應(yīng)用

下面到驗(yàn)證實(shí)驗(yàn)成果的時候,運(yùn)行應(yīng)用程序,分別
打開登錄頁消息發(fā)送頁登錄頁可創(chuàng)建Websocket連接,然后每次刷新消息發(fā)送頁時即發(fā)送一條新的消息,登錄頁自動收到該消息并在頁面顯示,如下截圖所示。

Websocket_Demo.png

4. Websocket總結(jié)

當(dāng)然,上述展示的只是一個小小的Demo,但按照上述思路即可將Websocket運(yùn)用于其它項(xiàng)目中,為項(xiàng)目錦上添花???,不知大家有沒有注意到一個,上述Websocket協(xié)議我們使用的都是ws協(xié)議,那什么時候會用到wss協(xié)議呢?當(dāng)我們的通信協(xié)議為HTTPS協(xié)議的時候,此時需要在服務(wù)端應(yīng)用服務(wù)器中安裝SSL證書,不然服務(wù)端是沒法解析wss協(xié)議的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容