WebSocket的介紹


WebSocket

關於websocket的一個小demo,是聊天室,源代碼地址:

聊天室的github源代碼

websocket的背景

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

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) -loop

從上面可以看出其實這兩種方式,都是在不斷地建立HTTP連接,然后等待服務端處理,可以體現HTTP協議的另外一個特點,被動性。何為被動性呢,其實就是,服務端不能主動聯系客戶端,只能有客戶端發起。

從上面很容易看出來,不管怎么樣,上面這兩種都是非常消耗資源的。
ajax輪詢 需要服務器有很快的處理速度和資源。(速度)
long poll 需要有很高的並發,也就是說同時接待客戶的能力。(場地大小)
所以ajax輪詢 和long poll 缺點非常明顯。

websocket 與Http的關系

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

首先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連接必須由瀏覽器發起,因為請求協議是一個標准的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的高並發,可以考慮。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM