WebSocket實踐——Java實現WebSocket的兩種方式

什麼是 WebSocket? 

  隨着互聯網的發展,傳統的HTTP協議已經很難滿足Web應用日益複雜的需求了。近年來,隨着HTML5的誕生,WebSocket協議被提出,它實現了瀏覽器與服務器的全雙工通信,擴展了瀏覽器與服務端的通信功能,使服務端也能主動向客戶端發送數據。
  我們知道,傳統的HTTP協議是無狀態的,每次請求(request)都要由客戶端(如 瀏覽器)主動發起,服務端進行處理後返回response結果,而服務端很難主動向客戶端發送數據;這種客戶端是主動方,服務端是被動方的傳統Web模式 對於信息變化不頻繁的Web應用來說造成的麻煩較小,而對於涉及實時信息的Web應用卻帶來了很大的不便,如帶有即時通信、實時數據、訂閱推送等功能的應 用。在WebSocket規範提出之前,開發人員若要實現這些實時性較強的功能,經常會使用折衷的解決方法:輪詢(polling)和Comet技術。其實後者本質上也是一種輪詢,只不過有所改進。
輪詢是最原始的實現實時Web應用的解決方案。輪詢技術要求客戶端以設定的時間間隔週期性地向服務端發送請求,頻繁地查詢是否有新的數據改動。明顯地,這種方法會導致過多不必要的請求,浪費流量和服務器資源。
  Comet技術又可以分爲長輪詢和流技術。長輪詢改進了上述的輪詢技術,減小了無用的請求。它會爲某些數據設定過期時間,當數據過期後纔會向服務端發送請求;這種機制適合數據的改動不是特別頻繁的情況。流技術通常是指客戶端使用一個隱藏的窗口與服務端建立一個HTTP長連接,服務端會不斷更新連接狀態以保持HTTP長連接存活;這樣的話,服務端就可以通過這條長連接主動將數據發送給客戶端;流技術在大併發環境下,可能會考驗到服務端的性能。
這兩種技術都是基於請求-應答模式,都不算是真正意義上的實時技術;它們的每一次請求、應答,都浪費了一定流量在相同的頭部信息上,並且開發複雜度也較大。
伴隨着HTML5推出的WebSocket,真正實現了Web的實時通信,使B/S模式具備了C/S模式的實時通信能力。WebSocket的工作流程是這 樣的:瀏覽器通過JavaScript向服務端發出建立WebSocket連接的請求,在WebSocket連接建立成功後,客戶端和服務端就可以通過 TCP連接傳輸數據。因爲WebSocket連接本質上是TCP連接,不需要每次傳輸都帶上重複的頭部數據,所以它的數據傳輸量比輪詢和Comet技術小 了很多。本文不詳細地介紹WebSocket規範,主要介紹下WebSocket在Java Web中的實現。
  JavaEE 7中出了JSR-356:Java API for WebSocket規範。不少Web容器,如Tomcat,Nginx,Jetty等都支持WebSocket。Tomcat從7.0.27開始支持 WebSocket,從7.0.47開始支持JSR-356,下面的Demo代碼也是需要部署在Tomcat7.0.47以上的版本才能運行。
  WebSocket是HTML5開始提供的一種在單個 TCP 連接上進行全雙工通訊的協議。
在WebSocket API中,瀏覽器和服務器只需要做一個握手的動作,然後,瀏覽器和服務器之間就形成了一條快速通道。兩者之間就直接可以數據互相傳送。
瀏覽器通過 JavaScript 向服務器發出建立 WebSocket 連接的請求,連接建立以後,客戶端和服務器端就可以通過 TCP 連接直接交換數據。
當你獲取 Web Socket 連接後,你可以通過 send() 方法來向服務器發送數據,並通過 onmessage 事件來接收服務器返回的數據。
以下 API 用於創建 WebSocket 對象。
  var Socket = new WebSocket(url, [protocol] );
以上代碼中的第一個參數 url, 指定連接的 URL。第二個參數 protocol 是可選的,指定了可接受的子協議。

實現方式

  1. 常用的 Node 實現有以下三種。

  1. Tomcat實現websocket方法
  2. spring整合websocket方法

具體實現

  • Tomcat實現websocket方法

使用這種方式無需別的任何配置,只需服務端一個處理類

 

服務端

/**
 * 服務器
 * @ClassName: WebSocket 
 * @Description: TODO
 * @author huangk
 * @Date 2018年8月16日 下午2:46:54  
 *
 */
@ServerEndpoint("/webSocketByTomcat/{username}")  
public class WebSocket {  
    private static int onlineCount = 0;  
    private static Map<String, WebSocket> clients = new ConcurrentHashMap<String, WebSocket>();  
    private Session session;  
    private String username;  
      
    @OnOpen  
    public void onOpen(@PathParam("username") String username, Session session) throws IOException {  
  
        this.username = username;  
        this.session = session;  
          
        addOnlineCount();  
        clients.put(username, this);  
        System.out.println("已連接");  
        
    }  
  
    @OnClose  
    public void onClose() throws IOException {  
        clients.remove(username);  
        subOnlineCount();  
    }  
  
    @OnMessage  
    public void onMessage(String message) throws IOException {  
        JSONObject jsonTo = JSONObject.parseObject(message);  
        System.out.println(jsonTo.getString("to") +":"+ jsonTo.getString("msg"));
          
        if (!jsonTo.getString("to").toLowerCase().equals("all")){  
            sendMessageTo(jsonTo.getString("msg"), jsonTo.getString("to"));  
        }else{  
            sendMessageAll(jsonTo.getString("msg"));  
        }  
    }  
  
    @OnError  
    public void onError(Session session, Throwable error) {  
        error.printStackTrace();  
    }  
  
    public void sendMessageTo(String message, String To) throws IOException {  
        // session.getBasicRemote().sendText(message);  
        //session.getAsyncRemote().sendText(message);  
        for (WebSocket item : clients.values()) {  
            if (item.username.equals(To) )  
                item.session.getAsyncRemote().sendText(message);  
        }  
    }  
      
    public void sendMessageAll(String message) throws IOException {  
        for (WebSocket item : clients.values()) {  
            item.session.getAsyncRemote().sendText(message);  
        }  
    }  
      
      
  
    public static synchronized int getOnlineCount() {  
        return onlineCount;  
    }  
  
    public static synchronized void addOnlineCount() {  
        WebSocket.onlineCount++;  
    }  
  
    public static synchronized void subOnlineCount() {  
        WebSocket.onlineCount--;  
    }  
  
    public static synchronized Map<String, WebSocket> getClients() {  
        return clients;  
    }  
}  

 

客戶端

<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<c:set var="ctx" value="${pageContext.request.contextPath}" />
<c:set var="ctxpath"
    value="${pageContext.request.scheme}${'://'}${pageContext.request.serverName}${':'}${pageContext.request.serverPort}${pageContext.request.contextPath}" />
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta charset=UTF-8">
<title>登錄測試</title>
</head>
<body>
    <h2>Hello World!</h2>
    <div>
        <span>sessionId:</span>
        <% 
            HttpSession s= request.getSession(); 
            out.println(s.getId());
        %>
    </div>
    
    <input id="sessionId" type="hidden" value="<%=session.getId() %>" />
    <input id="text" type="text" />
    <button οnclick="send()">發送消息</button>
    <hr />
    <button οnclick="closeWebSocket()">關閉WebSocket連接</button>
    <hr />
    <div id="message"></div>
</body>
<script type="text/javascript" src="http://localhost:8088/static/js/sockjs-0.3.min.js"></script> 
<script type="text/javascript">  
        var websocket = null;  
        if('WebSocket' in window) {
            websocket = new WebSocket("ws://localhost:8088/websocket/webSocketByTomcat/"+document.getElementById('sessionId').value);  
        } else if('MozWebSocket' in window) {
            websocket = new MozWebSocket("ws://localhost:8088/websocket/webSocketByTomcat/"+document.getElementById('sessionId').value);
        } else {
            websocket = new SockJS("localhost:8088/websocket/webSocketByTomcat/"+document.getElementById('sessionId').value);
        }
      
        //連接發生錯誤的回調方法  
        websocket.onerror = function () {  
            setMessageInnerHTML("WebSocket連接發生錯誤");  
        };  
      
        //連接成功建立的回調方法  
        websocket.onopen = function () {  
            setMessageInnerHTML("WebSocket連接成功");  
        }  
      
        //接收到消息的回調方法  
        websocket.onmessage = function (event) {  
            setMessageInnerHTML(event.data);  
        }  
      
        //連接關閉的回調方法  
        websocket.onclose = function () {  
            setMessageInnerHTML("WebSocket連接關閉");  
        }  
      
        //監聽窗口關閉事件,當窗口關閉時,主動去關閉websocket連接,防止連接還沒斷開就關閉窗口,server端會拋異常。  
        window.onbeforeunload = function () {  
            closeWebSocket();  
        }  
      
        //將消息顯示在網頁上  
        function setMessageInnerHTML(innerHTML) {  
            document.getElementById('message').innerHTML += innerHTML + '<br/>';  
        }  
      
        //關閉WebSocket連接  
        function closeWebSocket() {  
            websocket.close();  
        }  
      
        //發送消息  
        function send() {  
            var message = document.getElementById('text').value;  
            websocket.send(message);  
        }  
    </script>
</html>

注意導入socketjs時要使用地址全稱,並且連接使用的是http而不是websocket的ws

服務端如何向客戶端推送消息呢?

代碼如下

/**
 * 服務端推送消息對客戶端
 * @ClassName: ServiceClientController 
 * @Description: TODO
 * @author huangk
 * @Date 2018年8月16日 下午2:45:22  
 *
 */
@Controller
@RequestMapping(value="webSocketByTomcat/serviceToClient")
public class ServiceClientByTomcatController {
    private WebSocket websocket = new WebSocket();
    
    @RequestMapping
    public void sendMsg(HttpServletRequest request, HttpServletResponse response) throws IOException {
        JSONObject json = new JSONObject();
        json.put("to", request.getSession().getId());
        json.put("msg", "歡迎連接WebSocket!!!!");
        websocket.onMessage(json.toJSONString());
    }
}

效果如下圖所示

 

 

 

 

  • spring整合websocket方法

springboot對websocket支持很友好,只需要繼承webSocketHandler類,重寫幾個方法就可以了

這個類是對消息的一些處理,比如是發給一個人,還是發給所有人,並且前端連接時觸發的一些動作

/**
 * 創建一個WebSocket server
 * 
 * @ClassName: CustomWebSocketHandler
 * @Description: TODO
 * @author huangk
 * @Date 2018年8月16日 下午3:17:34
 *
 */
@Service
public class CustomWebSocketHandler extends TextWebSocketHandler implements WebSocketHandler {
    private Logger logger = LoggerFactory.getLogger(CustomWebSocketHandler.class);
    // 在線用戶列表
    private static final Map<String, WebSocketSession> users;
    // 用戶標識
    private static final String CLIENT_ID = "mchNo";

    static {
        users = new HashMap<>();
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        logger.info("成功建立websocket-spring連接");
        String mchNo = getMchNo(session);
        if (StringUtils.isNotEmpty(mchNo)) {
            users.put(mchNo, session);
            session.sendMessage(new TextMessage("成功建立websocket-spring連接"));
            logger.info("用戶標識:{},Session:{}", mchNo, session.toString());
        }
    }

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) {
        logger.info("收到客戶端消息:{}", message.getPayload());
        JSONObject msgJson = JSONObject.parseObject(message.getPayload());
        String to = msgJson.getString("to");
        String msg = msgJson.getString("msg");
        WebSocketMessage<?> webSocketMessageServer = new TextMessage("server:" +message);
        try {
            session.sendMessage(webSocketMessageServer);
            if("all".equals(to.toLowerCase())) {
                sendMessageToAllUsers(new TextMessage(getMchNo(session) + ":" +msg));
            }else {
                sendMessageToUser(to, new TextMessage(getMchNo(session) + ":" +msg));
            }
        } catch (IOException e) {
            logger.info("handleTextMessage method error:{}", e);
        }
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        if (session.isOpen()) {
            session.close();
        }
        logger.info("連接出錯");
        users.remove(getMchNo(session));
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        logger.info("連接已關閉:" + status);
        users.remove(getMchNo(session));
    }

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

    public void sendMessage(String jsonData) {
        logger.info("收到客戶端消息sendMessage:{}", jsonData);
        JSONObject msgJson = JSONObject.parseObject(jsonData);
        String mchNo = StringUtils.isEmpty(msgJson.getString(CLIENT_ID)) ? "陌生人" : msgJson.getString(CLIENT_ID);
        String to = msgJson.getString("to");
        String msg = msgJson.getString("msg");
        if("all".equals(to.toLowerCase())) {
            sendMessageToAllUsers(new TextMessage(mchNo + ":" +msg));
        }else {
            sendMessageToUser(to, new TextMessage(mchNo + ":" +msg));
        }
    }
    
    /**
     * 發送信息給指定用戶
     * @Title: sendMessageToUser 
     * @Description: TODO
     * @Date 2018年8月21日 上午11:01:08 
     * @author huangk
     * @param mchNo
     * @param message
     * @return
     */
    public boolean sendMessageToUser(String mchNo, TextMessage message) {
        if (users.get(mchNo) == null)
            return false;
        WebSocketSession session = users.get(mchNo);
        logger.info("sendMessage:{} ,msg:{}", session, message.getPayload());
        if (!session.isOpen()) {
            logger.info("客戶端:{},已斷開連接,發送消息失敗", mchNo);
            return false;
        }
        try {
            session.sendMessage(message);
        } catch (IOException e) {
            logger.info("sendMessageToUser method error:{}", e);
            return false;
        }
        return true;
    }

    /**
     * 廣播信息
     * @Title: sendMessageToAllUsers 
     * @Description: TODO
     * @Date 2018年8月21日 上午11:01:14 
     * @author huangk
     * @param message
     * @return
     */
    public boolean sendMessageToAllUsers(TextMessage message) {
        boolean allSendSuccess = true;
        Set<String> mchNos = users.keySet();
        WebSocketSession session = null;
        for (String mchNo : mchNos) {
            try {
                session = users.get(mchNo);
                if (session.isOpen()) {
                    session.sendMessage(message);
                }else {
                    logger.info("客戶端:{},已斷開連接,發送消息失敗", mchNo);
                }
            } catch (IOException e) {
                logger.info("sendMessageToAllUsers method error:{}", e);
                allSendSuccess = false;
            }
        }

        return allSendSuccess;
    }
    
    /**
     * 獲取用戶標識
     * @Title: getMchNo 
     * @Description: TODO
     * @Date 2018年8月21日 上午11:01:01 
     * @author huangk
     * @param session
     * @return
     */
    private String getMchNo(WebSocketSession session) {
        try {
            String mchNo = session.getAttributes().get(CLIENT_ID).toString();
            return mchNo;
        } catch (Exception e) {
            return null;
        }
    }
}

 

這個類的作用就是在連接成功前和成功後增加一些額外的功能

我們希望能夠把websocketSession和httpsession對應起來,這樣就能根據當前不同的session,定向對websocketSession進行數據返回;在查詢資料之後,發現spring中有一個攔截器接口,HandshakeInterceptor,可以實現這個接口,來攔截握手過程,向其中添加屬性

/**
 * WebSocket握手時的攔截器
 * @ClassName: CustomWebSocketInterceptor 
 * @Description: TODO
 * @author huangk
 * @Date 2018年8月16日 下午3:17:04  
 *
 */
public class CustomWebSocketInterceptor implements HandshakeInterceptor {
    private Logger logger = LoggerFactory.getLogger(CustomWebSocketInterceptor.class);
    /**
     * 關聯HeepSession和WebSocketSession,
     * beforeHandShake方法中的Map參數 就是對應websocketSession裏的屬性
     */
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Map<String, Object> map) throws Exception {
        if (request instanceof ServletServerHttpRequest) {
            logger.info("*****beforeHandshake******");
            HttpServletRequest httpServletRequest = ((ServletServerHttpRequest) request).getServletRequest();
            HttpSession session = httpServletRequest.getSession(true);
            
            logger.info("mchNo:{}", httpServletRequest.getParameter("mchNo"));
            if (session != null) {
                
                map.put("sessionId",session.getId());
                map.put("mchNo", httpServletRequest.getParameter("mchNo"));
            }
        }
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
        logger.info("******afterHandshake******");
    }
}

 

這個類是配置類向Spring中注入handler

 

/**
 * websocket的配置類
 * @ClassName: CustomWebSocketConfig 
 * @Description: TODO
 * @author huangk
 * @Date 2018年8月16日 下午3:17:26  
 *
 */
@Configuration
@EnableWebSocket
public class CustomWebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(customWebSocketHandler(), "/webSocketBySpring/customWebSocketHandler").addInterceptors(new CustomWebSocketInterceptor()).setAllowedOrigins("*");
        registry.addHandler(customWebSocketHandler(), "/sockjs/webSocketBySpring/customWebSocketHandler").addInterceptors(new CustomWebSocketInterceptor()).setAllowedOrigins("*").withSockJS();
    }

    @Bean
    public WebSocketHandler customWebSocketHandler() {
        return new CustomWebSocketHandler();
    }
}

補充說明:

setAllowedOrigins("*")一定要加上,不然只有訪問localhost,其他的不予許訪問

setAllowedOrigins(String[] domains),允許指定的域名或IP(含端口號)建立長連接,如果只允許自家域名訪問,這裏輕鬆設置。如果不限時使用"*"號,如果指定了域名,則必須要以http或https開頭

經查閱官方文檔springwebsocket 4.1.5版本前默認支持跨域訪問,之後的版本默認不支持跨域,需要設置

 

使用withSockJS()的原因:

  一些瀏覽器中缺少對WebSocket的支持,因此,回退選項是必要的,而Spring框架提供了基於SockJS協議的透明的回退選項。

SockJS的一大好處在於提供了瀏覽器兼容性。優先使用原生WebSocket,如果在不支持websocket的瀏覽器中,會自動降爲輪詢的方式。 
除此之外,spring也對socketJS提供了支持。

如果代碼中添加了withSockJS()如下,服務器也會自動降級爲輪詢。

registry.addEndpoint("/coordination").withSockJS();

SockJS的目標是讓應用程序使用WebSocket API,但在運行時需要在必要時返回到非WebSocket替代,即無需更改應用程序代碼。

SockJS是爲在瀏覽器中使用而設計的。它使用各種各樣的技術支持廣泛的瀏覽器版本。對於SockJS傳輸類型和瀏覽器的完整列表,可以看到SockJS客戶端頁面。 
傳輸分爲3類:WebSocket、HTTP流和HTTP長輪詢(按優秀選擇的順序分爲3類)

 

客戶端

<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<c:set var="ctx" value="${pageContext.request.contextPath}" />
<c:set var="ctxpath"
    value="${pageContext.request.scheme}${'://'}${pageContext.request.serverName}${':'}${pageContext.request.serverPort}${pageContext.request.contextPath}" />
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta charset=UTF-8">
<title>登錄測試</title>
</head>
<body>
    <h2>Hello World! Web Socket by Spring</h2>
    <div>
        <span>sessionId:</span>
        <% 
            HttpSession s= request.getSession(); 
            out.println(s.getId());
        %>
    </div>
    
    <input id="sessionId" type="hidden" value="<%=session.getId() %>" />
    <input id="text" type="text" />
    <button οnclick="send()">發送消息</button>
    <hr />
    <button οnclick="closeWebSocket()">關閉WebSocket連接</button>
    <hr />
    <div id="message"></div>
</body>
<script type="text/javascript" src="http://localhost:8088/static/js/sockjs-0.3.min.js"></script>
<script type="text/javascript">  
        var websocket = null;  
        //判斷當前瀏覽器是否支持WebSocket  
        //判斷當前瀏覽器是否支持WebSocket  
        if('WebSocket' in window) {
            websocket = new WebSocket("ws://localhost:8088/websocket/webSocketBySpring/customWebSocketHandler?mchNo="+ 123);  
        } else if('MozWebSocket' in window) {
            websocket = new MozWebSocket("ws://localhost:8088/websocket/webSocketBySpring/customWebSocketHandler?mchNo="+ 123);
        } else {
            websocket = new SockJS("http://localhost:8088/websocket/sockjs/webSocketBySpring/customWebSocketHandler?mchNo="+ 123);
        }
        //連接發生錯誤的回調方法  
        websocket.onerror = function () {  
            setMessageInnerHTML("WebSocket連接發生錯誤");  
        };  
      
        //連接成功建立的回調方法  
        websocket.onopen = function () {  
            setMessageInnerHTML("WebSocket連接成功");  
        }  
      
        //接收到消息的回調方法  
        websocket.onmessage = function (event) {  
            setMessageInnerHTML(event.data);  
        }  
      
        //連接關閉的回調方法  
        websocket.onclose = function () {  
            setMessageInnerHTML("WebSocket連接關閉");  
        }  
      
        //監聽窗口關閉事件,當窗口關閉時,主動去關閉websocket連接,防止連接還沒斷開就關閉窗口,server端會拋異常。  
        window.onbeforeunload = function () {  
            closeWebSocket();  
        }  
      
        //將消息顯示在網頁上  
        function setMessageInnerHTML(innerHTML) {  
            document.getElementById('message').innerHTML += innerHTML + '<br/>';  
        }  
      
        //關閉WebSocket連接  
        function closeWebSocket() {  
            websocket.close();  
        }  
      
        //發送消息  
        function send() {  
            var message = document.getElementById('text').value;  
            websocket.send(message);  
        }  
    </script>
</html>

效果如圖所示

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章