WebSocket的介紹

WebSocket

websocket的背景

如今,不少網站爲了實現推送技術,所用的技術都是 Ajax 輪詢或者long poll 。這種傳統的模式帶來很明顯的缺點,即瀏覽器須要不斷的向服務器發出請求,然而HTTP請求可能包含較長的頭部,其中真正有效的數據可能只是很小的一部分,顯然這樣會浪費不少的帶寬等資源。javascript

websocket的特色

  • WebSocket 是 HTML5 開始提供的一種在單個 TCP 鏈接上進行全雙工通信的協議。能更好的節省服務器資源和帶寬,而且可以更實時地進行通信。
  • WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,容許服務端主動向客戶端推送數據。
  • 在 WebSocket API 中,瀏覽器和服務器只須要完成一次握手,二者之間就直接能夠建立持久性的鏈接,並進行雙向數據傳輸。
  • 瀏覽器經過 JavaScript 向服務器發出創建 WebSocket 鏈接的請求,鏈接創建之後,客戶端和服務器端就能夠經過 TCP 鏈接直接交換數據。

Ajax輪詢

ajax輪詢 的原理很是簡單,讓瀏覽器隔個幾秒就發送一次請求,詢問服務器是否有新信息。
場景再現:
客戶端:啦啦啦,有沒有新信息(Request)
服務端:沒有(Response)
客戶端:啦啦啦,有沒有新消息(Request)
服務端:好啦好啦,有啦給你。(Response)
客戶端:啦啦啦,有沒有新消息(Request)
服務端:。。。。。沒。。。。沒。。。沒有(Response) ---- loop前端

long poll

long poll 其實原理跟 ajax輪詢 差很少,都是採用輪詢的方式,不過採起的是阻塞模型(一直打電話,沒收到就不掛電話),也就是說,客戶端發起鏈接後,若是沒消息,就一直不返回Response給客戶端。直到有消息才返回,返回完以後,客戶端再次創建鏈接,周而復始。
場景再現:
客戶端:啦啦啦,有沒有新信息,沒有的話就等有了才返回給我吧(Request)
服務端:額。。 等待到有消息的時候。。來 給你(Response)
客戶端:啦啦啦,有沒有新信息,沒有的話就等有了才返回給我吧(Request) -loopjava

從上面能夠看出其實這兩種方式,都是在不斷地創建HTTP鏈接,而後等待服務端處理,能夠體現HTTP協議的另一個特色,被動性。何爲被動性呢,其實就是,服務端不能主動聯繫客戶端,只能有客戶端發起。web

從上面很容易看出來,無論怎麼樣,上面這兩種都是很是消耗資源的。
ajax輪詢 須要服務器有很快的處理速度和資源。(速度)
long poll 須要有很高的併發,也就是說同時接待客戶的能力。(場地大小)
因此ajax輪詢 和long poll 缺點很是明顯。ajax

websocket 與Http的關係

WebSocket是HTML5出的東西(協議),也就是說HTTP協議沒有變化,或者說不要緊,但HTTP是不支持持久鏈接的(長鏈接,循環鏈接的不算)json

首先HTTP有1.1和1.0之說,也就是所謂的keep-alive,把多個HTTP請求合併爲一個,可是Websocket實際上是一個新協議,跟HTTP協議基本沒有關係,只是爲了兼容現有瀏覽器的握手規範而已,也就是說它是HTTP協議上的一種補充,能夠經過這樣一張圖理解瀏覽器

首先,相對於HTTP這種非持久的協議來講,Websocket是一個持久化的協議。安全

  • HTTP仍是一個無狀態協議。通俗的說就是,服務器由於天天要接待太多客戶了,是個健忘鬼,你一掛電話,他就把你的東西全忘光了,把你的東西全丟掉了。你第二次還得再告訴服務器一遍。
  • HTTP的生命週期經過Request來界定,也就是一個Request 一個Response,那麼HTTP1.0,此次HTTP請求就結束了。
  • 在HTTP1.1中進行了改進,使得有一個keep-alive,也就是說,在一個HTTP鏈接中,能夠發送多個Request,接收多個Response。
  • 可是 Request = Response , 在HTTP中永遠是這樣,也就是說一個request只能有一個response。並且這個response也是被動的,不能主動發起。

因此在這種狀況下出現了,Websocket出現了。他解決了HTTP的難題。服務器

websocket協議創建

WebSocket並非全新的協議,而是利用了HTTP協議來創建鏈接。咱們來看看WebSocket鏈接是如何建立的。websocket

首先,WebSocket鏈接必須由瀏覽器發起,由於請求協議是一個標準的HTTP請求,格式以下:

GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13

該請求和普通的HTTP請求有幾點不一樣:

  1. GET請求的地址不是相似/path/,而是以ws://開頭的地址;
  2. 請求頭Upgrade: websocketConnection: Upgrade表示這個鏈接將要被轉換爲WebSocket鏈接;
  3. Sec-WebSocket-Key是用於標識這個鏈接,並不是用於加密數據;
  4. Sec-WebSocket-Version指定了WebSocket的協議版本。

服務器若是接受該請求,就會返回以下響應:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string

該響應代碼101表示本次鏈接的HTTP協議即將被更改,更改後的協議就是Upgrade: websocket指定的WebSocket協議。

這裏開始就是HTTP最後負責的區域了,告訴客戶端,已經成功切換協議啦~

websocket的客戶端簡單實例

var ws = new WebSocket("wss://localhost:8080/ws/asset");

//鏈接創建成功調用的方法
ws.onopen = function(evt) { 
  console.log("Connection open ..."); 
  //向服務端發送消息
  ws.send("Hello WebSockets!");
};
//收到服務端消息後調用的方法
ws.onmessage = function(evt) {
  console.log( "Received Message: " + evt.data);
  ws.close();
};
//鏈接關閉調用的方法
ws.onclose = function(evt) {
  console.log("Connection closed.");
};

webSocket的服務端簡單實例

@ServerEndpoint(value = "/ws/asset")
@Component
@Slf4j
public class WebSocketServer {
    

    private static AtomicInteger OnlineCount = new AtomicInteger(0);
    // concurrent包的線程安全Set,用來存放每一個客戶端對應的Session對象。
    private static CopyOnWriteArraySet<Session> SessionSet = new CopyOnWriteArraySet<Session>();


    /**
     * 鏈接創建成功調用的方法
     */
    @OnOpen
    public void onOpen(Session session) {
        SessionSet.add(session);
        int cnt = OnlineCount.incrementAndGet(); // 在線數加1
        log.info("有鏈接加入,當前鏈接數爲:{}", cnt);
        SendMessage(session, "鏈接成功");
    }

    /**
     * 鏈接關閉調用的方法
     */
    @OnClose
    public void onClose(Session session) {
        SessionSet.remove(session);
        int cnt = OnlineCount.decrementAndGet();
        log.info("有鏈接關閉,當前鏈接數爲:{}", cnt);
    }

    /**
     * 收到客戶端消息後調用的方法
     *
     * @param message
     *            客戶端發送過來的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("來自客戶端的消息:{}",message);
        System.out.println(session.toString());
        SendMessage(session, "收到消息,消息內容:"+message+session.getId());

    }

    /**
     * 出現錯誤
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("發生錯誤:{},Session ID: {}",error.getMessage(),session.getId());
        error.printStackTrace();
    }

    /**
     * 發送消息,實踐代表,每次瀏覽器刷新,session會發生變化。
     * @param session
     * @param message
     */
    public static void SendMessage(Session session, String message) {
        try {
            session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            log.error("發送消息出錯:{}", e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 羣發消息
     * @param message
     * @throws IOException
     */
    public static void BroadCastInfo(String message) throws IOException {
        for (Session session : SessionSet) {
            if(session.isOpen()){
                SendMessage(session, message);
            }
        }
    }

    /**
     * 指定Session發送消息
     * @param sessionId
     * @param message
     * @throws IOException
     */
    public static void SendMessage(String message,String sessionId) throws IOException {
        Session session = null;
        for (Session s : SessionSet) {
            if(s.getId().equals(sessionId)){
                session = s;
                break;
            }
        }
        if(session!=null){
            SendMessage(session, message);
        }
        else{
            log.warn("沒有找到你指定ID的會話:{}",sessionId);
        }
    }
}

websocket的心跳機制

websockt心跳機制,不得不說很形象;那何爲心跳機制,就是代表client與server的鏈接是否還在的檢測機制;

若是不存在檢測,那麼網絡忽然斷開,形成的後果就是client、server可能還在傻乎乎的發送無用的消息,浪費了資源;怎樣檢測呢?原理就是定時向server發送消息,若是接收到server的響應就代表鏈接依舊存在;
這個心跳機制在分佈式中也很常見,

demo

聊天室demo

(1)Client:客戶端說明

​ 客戶端的代碼主要是使用H5的WebSocket進行實現,在前端網頁中使用WebSocket進行鏈接服務端,而後創建Socket鏈接進行通信。

(2)Server:服務端說明

​ 服務端主要是創建多個客戶端的關係,進行消息的中轉等。客戶端成功鏈接到服務端以後,就能夠經過創建的通道進行發送消息到服務端,服務端接收到消息以後在羣發給全部的客戶端。

(3)客戶端和服務端鏈接

var websocket = new WebSocket("ws://localhost:8080/myWs");

(4)客戶端和服務端怎麼發送消息?

客戶端可使用webSocket提供的send()方法,以下代碼:

var message = document.getElementById('text').value;  
websocket.send(message);

服務端怎麼發送消息呢?主要是使用在成功創建鏈接的時候,建立的Session對象進行發送,以下代碼:

session.getAsyncRemote().sendText("恭喜您成功鏈接上WebSocket");

(5)客戶端和服務端怎麼接受消息?

客戶端接收消息消息使用的是websocket的onmessage回調方法,以下代碼:

websocket.onmessage = function(event) {  
           //文本信息直接顯示,若是是json信息,須要轉換下在顯示.  
       var data = event.data;  
       document.getElementById('message').innerHTML += data;  
}

服務端:

@OnMessage  
public void onMessage(String message, Session session) {  
        System.out.println("來自客戶端的消息:" + message);  
}

(6)羣聊原理(羣發消息)

服務端在和客戶端創建鏈接的時候,會建立一個webSocket對象,咱們會將每一個鏈接建立的對象進行報錯到一個列表中,好比:CopyOnWriteArraySet(這是線程安全的);在要進行羣發的時候,編寫咱們的列表對象進行羣發消息。

(7)單聊原理(一對一消息)

聊的時候,就無需遍歷列表,而是須要知道發送者和接受者各自的Session對象,這個Session對象怎麼獲取呢?Session能夠獲取到sessionId,發送者在發送消息的時候,攜帶接收消息的sessionId,那麼問題就演變成了:發送者怎麼知道接受者的sessionId,那就是加入一個在線用戶列表便可,在線用戶列表中有用戶的基本信息,包括sessionId。

websocket的實時推送

對比聊天室的demo,不一樣之處在於,客戶端連入服務器時候,會開啓一個線程,在線程中對客戶端進行推送數據。

關鍵代碼:

/**
     * 接收到消息
     *
     * @param text
     */
    @OnMessage
    public void onMsg(Session session,@PathParam("param") String param) throws IOException {
        //記錄客戶端
        webSocketMaps.put(session, param);
        //實例化工做任務
        Operater operater =new Operater(session,param);
        //開啓線程
        Thread thread = new Thread(operater);
        thread.start();
        logger.info("發送線程啓動成功");
    }

目前業務還不是很複雜,後期功能添加時候,再進行擴展,關於這個實時推送,大概開了50個窗口就鏈接失敗了。關於websocket的高併發,能夠考慮。