WEB即時通訊/訊息推送
寫在前面
通常進行的Web開發都是由客戶端主動發起的請求,然後伺服器對請求做出響應並返回給客戶端。但是在很多情況下,你也許會希望由伺服器主動對客戶端傳送一些資料。
那麼,該如何實現這個需求,或者說該如何向網頁推送訊息呢?
一、推送方式
我們知道,HTTP/HTTPS
協議是被設計基於“請求-相應”模型的,儘管HTTP/HTTPS
可以在任何網際網路協議或網路上實現,但這裡我們只討論在Internet網上的全球資訊網中的情況。
由於在Internet中,HTTP
協議在傳輸層使用的是TCP
協議。由此可知,只要我們能保持TCP
連線不隨一次“請求-響應”結束而結束,使得伺服器可以主動傳送資料,那麼我們就能夠實現向網頁的資料推送。事與願違,在2011年WebSocket
不過,在那時雖然不能直接實現推送,但是還是有曲線救國路線的,基本上有4類這種間接方式。當然現在我們還有了1種直接方式-WebSocket ,接下來我來依次介紹下。
模擬推送
1. 輪詢(Polling)
AJAX 定時(可以使用JS的 setTimeout 函式)去伺服器查詢是否有新訊息,從而進行增量式的更新。這種方式間隔多長時間再查詢是個問題,因為效能和即時性是反比關係。間隔太短,海量的請求會拖垮伺服器,間隔太長,伺服器上的新資料就需要更長的時間才能到達客戶機。
- 優點:服務端邏輯簡單;
- 缺點:大多數請求是無效請求,在輪詢很頻繁的情況下對伺服器的壓力很大;
所以,除了一些簡單練習專案外,這種方式不能被用於生產。
Comet
2和3屬於:Comet (web技術),是廣大開發者想出來的比較可行的推送技術。
2. 長輪詢(Long-Polling)
客戶端向伺服器傳送AJAX請求,伺服器接到請求後hold住連線,直到有新訊息或超時(設定)才返回響應資訊並關閉連線,客戶端處理完響應資訊後再向伺服器傳送新的請求。
- 優點:任意瀏覽器都可用;實時性好,無訊息的情況下不會進行頻繁的請求;
- 缺點:連線建立銷燬操作還是比較頻繁,伺服器維持著連線比較消耗資源;
微信網頁版使用的就是這種方式,據我觀察:
- 微信把25秒作為超時時間;
- 用兩個請求來完成長輪詢,一個用於25秒超時獲取是否有新訊息,當有新訊息時會用另一個AJAX請求來獲取具體資料。
這種方式是可以被用於生產的,並且已經被實踐檢驗有比較高的可用性。
3. 基於iframe的方式
iframe 是很早就存在的一種 HTML 標記, 通過在 HTML 頁面裡嵌入一個隱蔵幀,然後將這個隱蔵幀的 src 屬性設為對一個長連線的請求,伺服器端就能源源不斷地往客戶端輸入資料。
iframe 伺服器端並不返回直接顯示在頁面的資料,而是返回對客戶端 Javascript 函式的呼叫,如<script type="text/javascript">js_func("data from server")</script>
。伺服器端將返回的資料作為客戶端 JavaScript 函式的引數傳遞;客戶端瀏覽器的 Javascript 引擎在收到伺服器返回的 JavaScript 呼叫時就會去執行程式碼。
每次資料傳送不會關閉連線,連線只會在通訊出現錯誤時,或是連線重建時關閉(一些防火牆常被設定為丟棄過長的連線, 伺服器端可以設定一個超時時間, 超時後通知客戶端重新建立連線,並關閉原來的連線)。
- 優點:訊息能夠實時到達;
- 缺點:使用 iframe 請求一個長連線有一個很明顯的不足之處:IE、Morzilla Firefox 下端的進度欄都會顯示載入沒有完成,而且 IE 上方的圖示會不停的轉動,表示載入正在進行;
Google公司在一些產品中使用了iframe流,如Google Talk。
侷限性方式
4. 外掛提供的Socket方式
利用Flash XMLSocket,Java Applet套介面,Activex包裝的socket。
- 優點:原生socket的支援,和PC端和移動端的實現方式相似;
- 缺點:瀏覽器端需要裝相應的外掛;
5. WebSocket
2011年,WebSocket被IETF定為標準RFC 6455,WebSocket API也被W3C定為標準。WebSocket 使得客戶端和伺服器之間的資料交換變得更加簡單,允許服務端主動向客戶端推送資料。在 WebSocket API 中,瀏覽器和伺服器只需要完成一次握手,兩者之間就直接可以建立永續性的連線,並進行雙向資料傳輸。
WebSocket自然是極好的,更多細節我在下一節詳細說明。
到這裡,我們已經對WEB上的訊息推送機制有了一個整體的瞭解。不過,僅僅只有了解對於我們來說顯然還不夠,由於我是Java程式設計師,接下來我將繼續介紹WebSocket,並且用Java做服務端來做一個例子。
二、WebSocket
WebSocket 是獨立的、建立在 TCP 上的協議。Websocket 通過 HTTP/1.1 協議的101狀態碼進行握手。為了建立Websocket連線,需要通過瀏覽器發出請求,之後伺服器進行迴應,這個過程通常稱為“握手”(handshaking)。
1. ws請求
一個典型的WebSocket請求如下:
GET wss://xxx.xxx.com/push/ HTTP/1.1
Host: xxx.xxx.com:port
Connection:Upgrade
Upgrade:websocket
Sec-WebSocket-Extensions:permessage-deflate; client_max_window_bits
Sec-WebSocket-Key:rZGX8zZKTrdkhIJTCuW54Q==
Sec-WebSocket-Version:13
// Connection必須為:Upgrade,表示client希望升級連線;
// Upgrade必須為:websocket,表示client希望升級到Websocket協議;
// Sec-WebSocket-Key:是隨機字串,服務端會將其做一定運算,最後在Response中返回“Sec-WebSocket-Accept”頭的值。用於避免普通http請求被當做WebSocket協議。
// Sec-WebSocket-Version:表示支援的Websocket版本。RFC6455要求使用的版本是13,之前草案的版本均應當被棄用。
響應如下:
HTTP/1.1 101 Switching Protocols
Upgrade:websocket
Connection:upgrade
Sec-WebSocket-Accept:QJsTRym36zHnArQ7FCmSdPhuK78=
// Connection:upgrade 升級被伺服器同意
// Upgrade:websocket 指示客戶端升級到websocket
// Sec-WebSocket-Accept:參考上面請求的Sec-WebSocket-Key的註釋
2. WebSocket在Java中
JavaEE 7的JSR-356:Java API for WebSocket,已經對WebSocket做了支援。不少Web容器,如Tomcat、Jetty等都支援WebSocket。Tomcat從7.0.27開始支援WebSocket,從7.0.47開始支援JSR-356。
但是如果使用Java EE的WebSocket API的話,還有很多自己需要封裝的地方。所以接下來我要說的並不是Java官方的API,而是目前正在接觸的一種推送框架:Socket.IO以及其Server端的Java實現netty-socketio。這個框架不僅支援WebSocket,還支援Long-Polling模式。
注意Socket.IO並不是一個標準的WebSocket的實現,只是說Socket.IO使用並很好的支援了WebSocket協議而已。
下面就說一下這兩個框架。
3. SOCKET.IO
Socket.IO enables real-time bidirectional event-based communication. It consists in:
- a Node.js server (this repository)
由於其Server端是用Node.js實現的,又沒有提供Java版本的Server,所以我找到了一個比較流行的第三方實現:netty-socketio。
4. netty-socketio
This project is an open-source Java implementation of Socket.IO server. Based on Netty server framework.
netty-socketio是一個開源的Socket.IO Server的Java實現,基於Netty。
接下來我就使用netty-socketio來做一個demo。
三、netty-socketio例項
建議先大致讀一下Socket.IO和netty-socketio的官方網站相關資訊,以有個整體的概念,然後再做Demo,我就不把那些搬過來了。
Socket.IO中的一些重要概念。
Server
:代表一個服務端伺服器;Namespace
:一個Server
中可以包含多個Namespace
。見名知意,Namespace
代表一個個獨立的空間。Socket
/Client
:基本上這兩個詞是一個概念。- 在
JavaScript
客戶端叫Socket
,在建立時必須確定加入哪個Namespace
,使用Socket
可以讓你和伺服器通訊。注意這個和伯克利Socket
是不同的,只是開發者借用了一樣的名字、功能相似。 - 在
Java
服務端用Client
來表示連線上伺服器的連結,它就代表了JavaScript
連線時建立的那個Socket
。
- 在
room
:在服務端,一個Namespace
中你可以建立任意個房間,房間就是給Client
進行分組,以進行組範圍的通訊。Client
可以選擇加入某個房間,也可以不加入。
程式碼例項:兩個Namespace,廣播通訊。
Java服務端
public static void main(String[] args) throws InterruptedException { Configuration config = new Configuration(); config.setHostname("localhost"); config.setPort(9092); // 可重用地址,防止處於重啟時處於TIME_WAIT的TCP影響服務啟動 final SocketConfig socketConfig = new SocketConfig(); socketConfig.setReuseAddress(true); config.setSocketConfig(socketConfig); final SocketIOServer server = new SocketIOServer(config); final SocketIONamespace chat1namespace = server.addNamespace("/chat1"); chat1namespace.addEventListener("message", ChatObject.class, new DataListener<ChatObject>() { @Override public void onData(SocketIOClient client, ChatObject data, AckRequest ackRequest) { // broadcast messages to all clients chat1namespace.getBroadcastOperations().sendEvent("message", data); } }); final SocketIONamespace chat2namespace = server.addNamespace("/chat2"); chat2namespace.addEventListener("message", ChatObject.class, new DataListener<ChatObject>() { @Override public void onData(SocketIOClient client, ChatObject data, AckRequest ackRequest) { // broadcast messages to all clients chat2namespace.getBroadcastOperations().sendEvent("message", data); } }); server.start(); Thread.sleep(Integer.MAX_VALUE); server.stop(); }
JS客戶端
引用到的JS檔案:
<!DOCTYPE html> <html> <head> <title>Demo Chat</title> <link href="bootstrap.css" rel="stylesheet"> <style> body { padding: 20px; } .console { height: 400px; overflow: auto; } .username-msg { color: orange; } .connect-msg { color: green; } .disconnect-msg { color: red; } .send-msg { color: #888 } </style> <script src="js/socket.io/socket.io.js"></script> <script src="js/moment.min.js"></script> <script src="http://code.jquery.com/jquery-1.10.1.min.js"></script> <script> var userName1 = 'user1_' + Math.floor((Math.random() * 1000) + 1); var userName2 = 'user2_' + Math.floor((Math.random() * 1000) + 1); var chat1Socket = io.connect('http://localhost:9092/chat1'); var chat2Socket = io.connect('http://localhost:9092/chat2'); function connectHandler(parentId) { return function() { output('<span class="connect-msg">Client has connected to the server!</span>', parentId); } } function messageHandler(parentId) { return function(data) { output('<span class="username-msg">' + data.userName + ':</span> ' + data.message, parentId); } } function disconnectHandler(parentId) { return function() { output('<span class="disconnect-msg">The client has disconnected!</span>', parentId); } } function sendMessageHandler(parentId, userName, chatSocket) { var message = $(parentId + ' .msg').val(); $(parentId + ' .msg').val(''); var jsonObject = {'@class': 'com.ddupa.service.push.model.ChatObject', userName: userName, message: message}; chatSocket.json.send(jsonObject); } chat1Socket.on('connect', connectHandler('#chat1')); chat2Socket.on('connect', connectHandler('#chat2')); chat1Socket.on('message', messageHandler('#chat1')); chat2Socket.on('message', messageHandler('#chat2')); chat1Socket.on('disconnect', disconnectHandler('#chat1')); chat2Socket.on('disconnect', disconnectHandler('#chat2')); function sendDisconnect1() { chat1Socket.disconnect(); } function sendDisconnect2() { chat2Socket.disconnect(); } function sendMessage1() { sendMessageHandler('#chat1', userName1, chat1Socket); } function sendMessage2() { sendMessageHandler('#chat2', userName2, chat2Socket); } function output(message, parentId) { var currentTime = "<span class='time'>" + moment().format('HH:mm:ss.SSS') + "</span>"; var element = $("<div>" + currentTime + " " + message + "</div>"); $(parentId + ' .console').prepend(element); } $(document).keydown(function(e) { if (e.keyCode == 13) { $('#send').click(); } }); </script> </head> <body> <h1>Namespaces demo chat</h1> <br /> <div id="chat1" style="width: 49%; float: left;"> <h4>chat1</h4> <div class="console well"></div> <form class="well form-inline" onsubmit="return false;"> <input class="msg input-xlarge" type="text" placeholder="Type something..." /> <button type="button" onClick="sendMessage1()" class="btn" id="send">Send</button> <button type="button" onClick="sendDisconnect1()" class="btn">Disconnect</button> </form> </div> <div id="chat2" style="width: 49%; float: right;"> <h4>chat2</h4> <div class="console well"></div> <form class="well form-inline" onsubmit="return false;"> <input class="msg input-xlarge" type="text" placeholder="Type something..." /> <button type="button" onClick="sendMessage2()" class="btn" id="send">Send</button> <button type="button" onClick="sendDisconnect2()" class="btn">Disconnect</button> </form> </div> </body> </html>
到這裡,我們學習了一個能用於生產的推送框架的基本使用。不過,以上只是一個簡單例子,僅做引路入門,更多參考可以直接去官方網站找到,我再寫就是贅述了:
例外的一點是,由於分散式netty-socketio的部署方式文件中描述的不太清晰,且這部分實際中比較重要,我會在下面再繼續描述下。
四、分散式伺服器例項
1. 分散式環境下的問題
在分散式部署環境下假設有3臺伺服器分別為:PushServer001
、PushServer002
和PushServer003
。有3個Client
連線上了伺服器且他們都在一個名稱空間下的同一個room
中(叫room1
)。連線關係如下:
Client1
<———>PushServer001
Client2
<———>PushServer001
Client3
<———>PushServer003
此時Client1
傳送了一條訊息,PushServer叢集
收到訊息後顯然需要將其推到Client2
和Client3
上。
Client2
好說:它和Client1
連線的是同一個PushServer001
,PushServer001
通過Client1
可以獲取到room
,繼而通過room
獲取到其下的所有Clients
(其中必有Client2
),然後推送即可。Client3
怎麼辦呢?它連線的是PushServer003
,而003
並沒有收到Client1
的推送事件。
2. 解決方案
其實解決方案也很簡單,就是用釋出/訂閱 模式。
首先需要引入一個第三方的釋出/訂閱系統,比如這裡使用Redis-PUB/SUB。(如果Redis是主從複製的,注意PUB只能由Master做,SUB則Master和Slaves都行)
其次,每當伺服器需要傳送訊息時:
- 先將訊息傳送給
本Server
儲存的某room
中的所有Client
; - 接著再立即釋出一個通知,例如叫
PubSubStore.DISPATCH
,並將訊息內容放入其中。
// 本伺服器推送 try { Iterable<SocketIOClient> clients = pushNamespace.getRoomClients(room); for (SocketIOClient socketIOClient : clients) { socketIOClient.send(packet); } } catch (Exception e) { logger.error("當前服務直接推送失敗", e); } // 分發訊息(當前服務不會向client推送自己分發出去的訊息) try { pubSubStore.publish(PubSubStore.DISPATCH, new DispatchMessage(userId, packet, pushNamespace.getName())); } catch (Exception e) { logger.error("分發訊息失敗", e); }
- 先將訊息傳送給
最後,每臺伺服器啟動時都訂閱通知
PubSubStore.DISPATCH
。每當當前伺服器收到此類訂閱通知時,就將其中的訊息分發到同一個房間名的所有Client
去。在com.corundumstudio.socketio.store.pubsub.BaseStoreFactory.init(*)
時:pubSubStore().subscribe(PubSubStore.DISPATCH, new PubSubListener<DispatchMessage>() { @Override public void onMessage(DispatchMessage msg) { String room = msg.getRoom(); namespacesHub.get(msg.getNamespace()).dispatch(room, msg.getPacket()); } }, DispatchMessage.class);
其它一些事
1. HTTP持久連線
所謂HTTP持久連線即是:HTTP persistent connection,意即TCP連線重用技術。HTTP 1.0 的連線本來是“短連線”:建立一次TCP做完請求-響應即關閉,這樣頻繁的建立、關閉TCP連線顯然是很低效比較浪費資源。
所以HTTP協議後來就做了升級,允許使用一個請求和響應頭Connection:keep-alive
,來祈使伺服器能夠保持連線不中斷。如此,一個TCP連線就能在你對同一個網站進行訪問的時候被多次複用,請求網頁HTML本身、網頁中的JS、CSS和圖片等都用這一個連線。
不過,到了HTTP 1.1 以上連線預設就是持久化的了。
值得注意的是HTTP伺服器一般都有超時機制,伺服器不可能容忍你一直不釋放連線的。例如:Apache httpd 1.3/2.0是15秒、2.2是5秒。
持久連線做的是連線複用的工作,並不是解決全雙工通訊、推送的。
相關推薦
WEB即時通訊/訊息推送
寫在前面 通常進行的Web開發都是由客戶端主動發起的請求,然後伺服器對請求做出響應並返回給客戶端。但是在很多情況下,你也許會希望由伺服器主動對客戶端傳送一些資料。 那麼,該如何實現這個需求,或者說該如何向網頁推送訊息呢? 一、推送方式 我們知道,
即時通訊-Android推送方案(MQTT)
這篇文章是居於前面的幾篇部落格,如果還不知道ActiveMQ伺服器的請看:即時通訊-ActiveMQ環境搭建 1.什麼是MQTT協議 MQTT(Message Queuing Telemetry Transport,訊息佇列遙測傳輸)是IBM開發的一個即
淺析web端的訊息推送原理
開發十年,就只剩下這套架構體系了! >>>
Web端即時通訊、訊息推送的實現
在瀏覽某些網頁的時候,例如 WebQQ、京東線上客服服務、CSDN私信訊息等類似的情況下,我們可以在網頁上進行線上聊天,或者即時訊息的收取與回覆,可見,這種功能的需求由來已久,並且應用廣泛。 網上關於這方面的文章也能搜到一大堆,不過基本上都是理論,真正能夠執行
基於Netty實現的Android 訊息推送(即時通訊)的解決方案
根據Netty框架實現訊息推送(即時聊天)功能. Netty框架,TCP長連線,心跳,阻塞訊息佇列,執行緒池處理訊息傳送, 基於Google ProtoBuf自定義的訊息協議, TCP粘包/拆包.... 客戶端通過TCP連線到伺服器,並建立TCP長連線;當伺服器端收到新訊息後通過TCP連線推送給
Web Socket 多個使用者之間實現時時訊息推送
1個月不寫部落格了,最近挺忙的,剛用了2天寫了個預約的小程式和大家分享下~首先大家看下介面:1.祕書端 - 專門新增預約的內容,新增以後立馬在 “市長端” 彈出有一個新的預約2.市長端 - 專門看最新的預約 ,看看要不要接待,接待或不接待點選按鈕以後以後立馬 回覆祕書其實挺簡
SignalR SelfHost實時訊息,整合到web中,實現伺服器訊息推送
先前用過兩次SignalR,但是中途有段時間沒弄了,今天重新弄,發現已經忘得差不多了,做個筆記! 首先建立一個控制檯專案Nuget新增引用聯機搜尋:Microsoft.AspNet.SignalR.SelfHostMicrosoft.Owin.Cors 在Program.cs新增程式碼新增一個
SignalR快速入門 ~ 仿QQ即時聊天,訊息推送,單聊,群聊,多群公聊(基礎=》提升)
SignalR快速入門 ~ 仿QQ即時聊天,訊息推送,單聊,群聊,多群公聊(基礎=》提升,5個Demo貫徹全篇,感興趣的玩才是真的學) 應用情景之一: 沒太多連續的時間來研究SignalR,所以我把這篇文章分了三個階段: 第一個階段,簡單使用,熟悉並認識SignalR 第二個階段,實現
通過Socket.IO與nodeJs實現即時訊息推送
很早開始就想用WebSocket完成即時訊息推送功能。之前本打算用WebSocket + C#實現的,結果人上了年紀變笨了,弄了一天也沒弄好 ⊙﹏⊙ 今天參考了幾篇資料,終於搞定了一個Socket.IO結合nodeJs的Demo。 用Socket.IO有個很大的好處就是開
Nodejs和一個簡單的web頁面訊息推送服務
前言: 英語能力有限,所以不能叫做純翻譯,大概比例是70%翻譯,20%理解,10%自由發揮 原文: http://www.gianlucaguarini.com/blog/nodejs-and-a-simple-push-notification-server/ 簡述: 用
Workerman之WEB訊息推送框架使用筆記【一】
伺服器使用的時候需要注意雲盾和360埠攔截 下載解壓到任意目錄 cd到目錄下執行 start.php linux :php start.php start -d win:直接執行start_
android socket通訊demo (本篇服務於android訊息推送)
本文系作者原創,轉載請附原文地址,謝謝。 文章末尾提供本文中的原始碼下載連結,需資源積分1分,人艱不拆,下載後評論資源可獲系統返回積分=無損! 前言: 關於什麼是socket通訊,本篇文件中不進行解釋,不甚清楚的可以去百科查詢,日後得空我也會整理相關的內容。 本文
web+java+js的GoEasy的訊息推送
原文http://blog.csdn.net/xiqincai9/article/details/52535275以前都是使用ajax定時傳送請求到後臺,這種方式非常消耗系統資源。在大併發情況時如果不對執行緒進行控制的話,還會重複取資料,造成資料錯誤。 鑑於這種情況,使
spring boot 整合activemq 進行服務端訊息推送(web頁面)
最近公司的專案裡有需要服務端向web端實時推送訊息的需求,網上搜索了一番,有前端頁面通過定時任務向後臺傳送ajax請求重新整理,有使用第三方提供的訊息服務(GoEasy),前者因為會有很多請求是無用的,容易加大伺服器負荷造成宕機,後者現在收費了(免費的也只能用一
利用Hessian10分鐘配置出一個簡單的跨Web服務消息推送
開始 mage from 分鐘 啟動 hessian accept 就是 pattern 筆者,之前對Web跨服務推送數據一無所知,今天研究了一下。其實有些事物,在不理解的時候完全覺得好似天外來物。但了解一點點之後,又會覺得十分有趣。每天閑扯一下很開心,下面一個簡單的實例1
Android 基於Netty的訊息推送方案之物件的傳遞(四)
在上一篇文章中《Android 基於Netty的訊息推送方案之字串的接收和傳送(三)》我們介紹了Netty的字串傳遞,我們知道了Netty的訊息傳遞都是基於流,通過ChannelBuffer傳遞的,那麼自然,Object也需要轉換成ChannelBuffer來傳遞。好在Netty本身已經給我們寫好了
Android 基於Netty的訊息推送方案之字串的接收和傳送(三)
在上一篇文章中《Android 基於Netty的訊息推送方案之概念和工作原理(二)》 ,我們介紹過一些關於Netty的概念和工作原理的內容,今天我們先來介紹一個叫做ChannelBuffer的東東。 ChannelBuffer Netty中的訊息傳遞,都必須以位元
Android 基於Netty的訊息推送方案之概念和工作原理(二)
上一篇文章中我講述了關於訊息推送的方案以及一個基於Netty實現的一個簡單的Hello World,為了更好的理解Hello World中的程式碼,今天我來講解一下關於Netty中一些概念和工作原理的內容,如果你覺得本篇文章有些枯燥,請先去閱讀《Android 基於Netty的訊息推送方案之Hell
IOS APNS訊息推送框架介紹(pushy)以及詳細使用方法
最近公司需要做IOS訊息推送的功能,我負責後臺推送,IOS端資料處理以及回撥我不負責,本篇文章主要介紹基於java的apns訊息推送,使用框架為pushy。 宣告:我先前也沒有接觸過這個IOS推送,自己研究了兩天,通過百度,對比各個框架的優缺點,最後選擇了這個框架,有說的不對的地方,還
android 實現mqtt訊息推送,以及不停斷線重連的問題解決
前段時間專案用到mqtt的訊息推送,整理一下程式碼,程式碼的原型是網上找的,具體哪個地址已經忘記了。 程式碼的實現是新建了一個MyMqttService,全部功能都在裡面實現,包括連伺服器,斷線重連,訂閱訊息,處理訊息,釋出訊息等基本操作。 首先新增依賴: dependencies { &