如今,不少網站爲了實現推送技術,所用的技術都是 Ajax 輪詢或者long poll 。這種傳統的模式帶來很明顯的缺點,即瀏覽器須要不斷的向服務器發出請求,然而HTTP請求可能包含較長的頭部,其中真正有效的數據可能只是很小的一部分,顯然這樣會浪費不少的帶寬等資源。javascript
ajax輪詢 的原理很是簡單,讓瀏覽器隔個幾秒就發送一次請求,詢問服務器是否有新信息。
場景再現:
客戶端:啦啦啦,有沒有新信息(Request)
服務端:沒有(Response)
客戶端:啦啦啦,有沒有新消息(Request)
服務端:好啦好啦,有啦給你。(Response)
客戶端:啦啦啦,有沒有新消息(Request)
服務端:。。。。。沒。。。。沒。。。沒有(Response) ---- loop前端
long poll 其實原理跟 ajax輪詢 差很少,都是採用輪詢的方式,不過採起的是阻塞模型(一直打電話,沒收到就不掛電話),也就是說,客戶端發起鏈接後,若是沒消息,就一直不返回Response給客戶端。直到有消息才返回,返回完以後,客戶端再次創建鏈接,周而復始。
場景再現:
客戶端:啦啦啦,有沒有新信息,沒有的話就等有了才返回給我吧(Request)
服務端:額。。 等待到有消息的時候。。來 給你(Response)
客戶端:啦啦啦,有沒有新信息,沒有的話就等有了才返回給我吧(Request) -loopjava
從上面能夠看出其實這兩種方式,都是在不斷地創建HTTP鏈接,而後等待服務端處理,能夠體現HTTP協議的另一個特色,被動性。何爲被動性呢,其實就是,服務端不能主動聯繫客戶端,只能有客戶端發起。web
從上面很容易看出來,無論怎麼樣,上面這兩種都是很是消耗資源的。
ajax輪詢 須要服務器有很快的處理速度和資源。(速度)
long poll 須要有很高的併發,也就是說同時接待客戶的能力。(場地大小)
因此ajax輪詢 和long poll 缺點很是明顯。ajax
WebSocket是HTML5出的東西(協議),也就是說HTTP協議沒有變化,或者說不要緊,但HTTP是不支持持久鏈接的(長鏈接,循環鏈接的不算)json
首先HTTP有1.1和1.0之說,也就是所謂的keep-alive,把多個HTTP請求合併爲一個,可是Websocket實際上是一個新協議,跟HTTP協議基本沒有關係,只是爲了兼容現有瀏覽器的握手規範而已,也就是說它是HTTP協議上的一種補充,能夠經過這樣一張圖理解瀏覽器
首先,相對於HTTP這種非持久的協議來講,Websocket是一個持久化的協議。安全
因此在這種狀況下出現了,Websocket出現了。他解決了HTTP的難題。服務器
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請求有幾點不一樣:
/path/
,而是以ws://
開頭的地址;Upgrade: websocket
和Connection: Upgrade
表示這個鏈接將要被轉換爲WebSocket鏈接;Sec-WebSocket-Key
是用於標識這個鏈接,並不是用於加密數據;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最後負責的區域了,告訴客戶端,已經成功切換協議啦~
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."); };
@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); } } }
websockt心跳機制,不得不說很形象;那何爲心跳機制,就是代表client與server的鏈接是否還在的檢測機制;
若是不存在檢測,那麼網絡忽然斷開,形成的後果就是client、server可能還在傻乎乎的發送無用的消息,浪費了資源;怎樣檢測呢?原理就是定時向server發送消息,若是接收到server的響應就代表鏈接依舊存在;
這個心跳機制在分佈式中也很常見,
(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。
對比聊天室的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的高併發,能夠考慮。