服務器推送消息方法總結及實現(java)
最近在進行web開發時,有用到服務端推送消息這個功能,相信大家在平常開發時,也經常會有這種需求。本文對常用的幾種服務器推送消息方法進行整理和總結,並實現使用流的方式推送消息(java)。
服務器推送消息主要有一下幾種方法:
- 輪詢
- http流
- websocket
- http2.0
下面對各個方法一一進行介紹。
輪詢
輪詢分爲短輪詢和長輪詢。
短輪詢即瀏覽器定時向服務器發送請求,以此來更新數據的方法。如下圖所示
(圖片來自javascript高級程序設計第三版)
瀏覽器每隔一段時間向服務器發送一次請求,請求瀏覽器想要的數據。嚴格意義上講:短輪詢不是服務器推送的消息,獲取的數據也不是實時的。
實現原理:
在瀏覽器使用定時器setTimeout或setInterval即可。不進行講解了。
長輪詢長輪詢是短輪詢的一個翻版,或者叫改進版。瀏覽器向服務器發送一個請求看有沒有數據,有數據就響應,沒數據就保持該請求,知道有數據再返回。瀏覽器在服務器返回數據時再發送一個請求。這樣瀏覽器就可以一直獲取到最新的數據。長輪詢的時間線如下圖所示
(圖片來自javascript高級程序設計第三版)
實現原理:
在請求響應時,再次發送一個數據請求即可。
http流
流不同於上述兩種輪詢,因爲它在頁面的整個生命週期內只使用一個 HTTP 連接。具體來說,就是瀏覽器向服務器發送一個請求,而服務器一直保持連接打開,然後週期性地向瀏覽器發送數據。
實現:
本例以spring boot框架爲基礎,github地址如下:https://github.com/xubaodian/JAVA-SSE
下載該實例,並啓動。該實例端口號爲10000。
我們先對實例進行驗證測試,然後再講解代碼。
測試頁面地址爲:http://localhost:10000/subscribe.html
測試步驟如下:
1、進入http://localhost:10000/subscribe.html,頁面如下圖所示:
左側是訂閱消息的操作和展示頁面,右側是發佈內容的頁面。
2、左側輸入訂閱消息主題,點擊訂閱,訂閱相關主題消息,例如:輸入財經新聞主題FinancialNews,點擊訂閱,這樣就訂閱了財經新聞了。
3、在右側發佈內容頁面輸入主題和內容,點擊發布。這樣就可以發佈內容了。
測試結果如下:
訂閱了財經主題新聞,右側發佈了5條新聞,3條財經新聞,一條天氣新聞,一條時政新聞,訂閱者收到了3條財經新聞推送信息,證明我們工程已經跑起來了,實現了http流推送的最基本功能。
下面,對工程代碼進行分析:
java代碼
該工程使用spring boot框架,項目端口號爲10000,接口代碼如下:
package com.xbd.pushdata.controller;
import com.xbd.pushdata.Utils.ReqContextUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@RestController
public class SubscribeController {
@RequestMapping("/subscribe")
public void subscribe(HttpServletRequest req, HttpServletResponse res, @RequestParam("topic") String topic) {
ReqContextUtils.addSubscrib(topic, req, res);
}
@RequestMapping("/publish")
public void publish(@RequestParam("topic") String topic, @RequestParam("content") String content) {
ReqContextUtils.publishMessage(topic, content);
}
}
有兩個接口:
"/subscribe"接口:用於消息訂閱,該接口有一個參數topic,即訂閱的消息主題。
"/publish"接口:發佈消息接口,有兩個參數,topic是發佈消息主題,content是發佈消息內容。
訂閱和發佈消息的才做都封裝在ReqContextUtils類中,ReqContextUtils的代碼如下,代碼中註釋比較多,不再講解了:
package com.xbd.pushdata.Utils;
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
public class ReqContextUtils {
//超時時間
private static int DEFAULT_TIME_OUT = 60*60*1000;
//訂閱列表,存儲所有主題的訂閱請求,每個topic對應一個ArrayList,ArrayList裏該topic的所有訂閱請求
private static HashMap<String, ArrayList<AsyncContext>> subscribeArray = new LinkedHashMap<>();
//添加訂閱消息
public static void addSubscrib(String topic, HttpServletRequest request, HttpServletResponse response) {
if (null == topic || "".equals(topic)) {
return;
}
//設置響應頭ContentType
response.setContentType("text/event-stream");
//設置響應編碼類型
response.setCharacterEncoding("UTF-8");
//request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
//支持異步響應
//異步這個概念很多地方都有,就像處理文件時,不是一直等待文件讀完,而是讓它去讀,cpu做其它事情,讀完通知cpu來處理即可。
AsyncContext actx = request.startAsync(request, response);
actx.setTimeout(DEFAULT_TIME_OUT);
//添加一些監聽函數
actx.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) throws IOException {
System.out.println("推送結束");
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
System.out.println("推送超時");
}
@Override
public void onError(AsyncEvent event) throws IOException {
System.out.println("推送錯誤");
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
System.out.println("推送開始");
}
});
//將異步請求存入列表
ArrayList<AsyncContext> actxList = subscribeArray.get(topic);
if (null == actxList) {
actxList = new ArrayList<AsyncContext>();
subscribeArray.put(topic, actxList);
}
actxList.add(actx);
}
//獲取訂閱列表
public static ArrayList<AsyncContext> getSubscribList(String topic) {
return subscribeArray.get(topic);
}
//推送消息
public static void publishMessage(String topic, String content) {
//獲取對應topic的訂閱列表
ArrayList<AsyncContext> actxList = subscribeArray.get(topic);
if (null != actxList) {
for(AsyncContext actx :actxList) {
try {
PrintWriter out = actx.getResponse().getWriter();
out.print(content);
actx.getResponse().flushBuffer();
//out.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
前端代碼
前端代碼如下,主要就是2個請求,代碼中有註釋,不再講解了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>訂閱消息</title>
<style>
.left-container {
float: left;
width: 350px;
min-height: 300px;
border-right: 3px solid #4b4b4b;
}
.left-container li{
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.right-container{
padding-left: 30px;
float: left;
width: 350px;
}
</style>
</head>
<body>
<div class="left-container">
<label>訂閱主題</label>
<input type="text" id="topic">
<button onclick="subscribe()">訂閱</button>
<div>收到消息如下:</div>
<ul id="message"></ul>
</div>
<div class="right-container">
<div>
<label>消息主題</label>
<input type="text" id="pub_topic">
</div>
<div>
<label>消息內容</label>
<input type="text" id="pub_content">
</div>
<button onclick="publish()">發佈</button>
<div>發佈消息和內容如下:</div>
<ul id="pub_message"></ul>
</div>
<script>
function subscribe() {
let topic = document.getElementById('topic').value;
let url = location.origin + '/subscribe?topic=' + topic;
send(url, null, process);
}
//發送訂閱消息
function send(url, data, callback) {
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
//http流的響應時,xhr.readyState爲3
if (xhr.readyState == 3 || xhr.readyState == 4){
if (callback) {
callback(xhr.responseText);
}
}
};
xhr.open('get', url, true);
xhr.send(data);
}
let len = 0;
//處理訂閱消息
function process(messsage) {
let li = document.createElement('li');
li.innerHTML = messsage.substr(len);
len = messsage.length;
let ul = document.getElementById('message');
ul.appendChild(li);
}
//發佈消息
function publish() {
let topic = document.getElementById('pub_topic').value;
let content = document.getElementById('pub_content').value;
let url = location.origin + '/publish?topic=' + topic + '&content=' + content;
send(url, null, null);
let li = document.createElement('li');
li.innerHTML = `發佈主題:${topic}; 發佈內容:${content}`;
let ul = document.getElementById('pub_message');
ul.appendChild(li);
}
</script>
</body>
</html>
webSocket推送消息
Web Sockets 的是在一個單獨的持久連接上提供全雙工、雙向通信。在 JavaScript 中創建了 Web Socket 之後,會有一個 HTTP 請求發送到瀏覽器以發起連接。在取得服務器響應後,建立的連接會從 HTTP 協議升級爲 Web Socket 協議。
使用spring框架可以很容易實現websocket,這是spring實現websocket的官方教程(非常詳細)地址:https://spring.io/guides/gs/messaging-stomp-websocket/ ,需要的可以移步官方網頁學習。
http2.0
http2.0的特點是首部壓縮,多路複用,請求響應管線化,服務器推送等等,這些特點是建立在http2.0流的基礎上的。
具體想要學習http2.0的同學可以上網找下資料,這裏只是提一下,不做過多描述了。