Browser connection limitations解決方案
現象
Web界面訂閱Server端長鏈接接口時,當訂閱數量達到必定時,新建長鏈接將處於pending狀態html
環境
瀏覽器:Google Chrome 84.0.4147.135前端
創建長鏈接方法:new EventSource()web
通信方式:SSEspring
SSE實現:org.springframework.web.servlet.mvc.method.annotation.SseEmitter後端
緣由
瀏覽器限制 具備相同域名的HTTP鏈接的數量。此限制在HTTP規範(RFC2616)中定義。大多數現代瀏覽器每一個域容許六個鏈接。大多數較舊的瀏覽器每一個域僅容許兩個鏈接。瀏覽器
該HTTP 1.1協議規定,單用戶的客戶端不該該維持與任何服務器或代理多於兩個鏈接。這就是瀏覽器限制的緣由。有關更多信息,請參見RFC 2616 –超文本傳輸協議,第8節–鏈接。服務器
現代瀏覽器的限制不那麼嚴格,容許更多的鏈接。RFC沒有指定如何防止超出限制。能夠阻止打開鏈接,也能夠關閉現有鏈接。mvc
源碼
前端
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>SseEmitter</title> </head> <body> <button onclick="closeSse()">關閉鏈接</button> <div id="message"></div> </body> <script> let source = null; // 擬登陸用戶 if (!!window.EventSource) { // 創建鏈接 source = new EventSource('http://域/sseTest/sub?tId=1111114'); /** * 鏈接一旦創建,就會觸發open事件 * 另外一種寫法:source.onopen = function (event) {} */ source.addEventListener('open', function (e) { setMessageInnerHTML("創建鏈接。。。"); }, false); /** * 客戶端收到服務器發來的數據 * 另外一種寫法:source.onmessage = function (event) {} */ source.addEventListener('test', function (e) { setMessageInnerHTML(e.data); }); /** * 若是發生通訊錯誤(好比鏈接中斷),就會觸發error事件 * 或者: * 另外一種寫法:source.onerror = function (event) {} */ source.addEventListener('error', function (e) { if (e.readyState === EventSource.CLOSED) { setMessageInnerHTML("鏈接關閉"); } else { console.log(e); } }, false); } else { setMessageInnerHTML("你的瀏覽器不支持SSE"); } // 監聽窗口關閉事件,主動去關閉sse鏈接,若是服務端設置永不過時,瀏覽器關閉後手動清理服務端數據 window.onbeforeunload = function () { closeSse(); }; // 關閉Sse鏈接 function closeSse() { source.close(); const httpRequest = new XMLHttpRequest(); httpRequest.open('GET', 'http://域/sseTest/close?tId=1111', true); httpRequest.send(); console.log("close"); } // 將消息顯示在網頁上 function setMessageInnerHTML(innerHTML) { document.getElementById('message').innerHTML += innerHTML + '<br/>'; } </script> </html>
後端
/** * @author senkyouku * @ClassName: TestDemoController * @Description: sseDemo * @date 2020-08-28 */ @Slf4j @CrossOrigin @RestController @RequestMapping("/sseTest") public class TestDemoController { private final ConcurrentHashMap<String, SseEmitter> sseContainer = new ConcurrentHashMap<>(); /*** * sse訂閱 * * @param sub 訂閱參數 * @return SseEmitter */ @RequestMapping("/sub") public Object sub(SseSubDTO sub) { try { SseEmitter sseEmitter = new SseEmitter(180000L); sseContainer.put(sub.getTId(), sseEmitter); return sseEmitter; } catch (Exception e) { log.error("sse訂閱失敗,參數:{}", sub); return e.getMessage(); } } /*** * sse取消訂閱 * * @param sub 訂閱參數 * @return String */ @RequestMapping("/close") public String close(SseSubDTO sub) { try { // 二、建立SseEmitter 超時 3min SseEmitter sseEmitter = sseContainer.get(sub.getTId()); sseEmitter.complete(); sseContainer.remove(sub.getTId()); return "ok"; } catch (Exception e) { log.error("sse訂閱失敗,參數:{}", sub); return e.getMessage(); } } /** * 推送 */ @Scheduled(fixedRate = 2000) public void sseScheduled() { sseContainer.entrySet().parallelStream().forEach(p -> { // 推送失敗,移除該訂閱sse SseEmitter sseEmitter = p.getValue(); SseEmitter.SseEventBuilder builder = SseEmitter .event() .data("ok") .name("test"); try { sseEmitter.send(builder); } catch (IOException e) { log.error("推送失敗,訂閱信息:{},e:{}", p.getKey(), e); } }); }
解決方案
方案一:使用多個子域
解決瀏覽器限制一種方法是提供多個子域。每一個子域均容許最大鏈接數。經過使用編號的子域,客戶端能夠選擇一個隨機的子域進行鏈接。若是DNS服務器容許將與模式匹配的子域解析爲同一服務器。app
如上述前端代碼中【域】,可使用不一樣的域名或者使用不一樣的IP+端口,都可以解決限制問題,不過根據HTTP 1.1協議規定,單用戶的客戶端不該該維持與任何服務器或代理多於兩個鏈接,咱們能夠參考方案二ui
方案二:同一個界面使用同一個長鏈接
由前端訂閱通知業務類型,服務端根據不一樣的業務類型推送不一樣的事件,即不一樣的eventName,代碼片斷以下,SseEmitter.name 根據不一樣業務類型設置不一樣的eventName
try { SseEmitter.SseEventBuilder builder = SseEmitter .event() .data("ok") .name("test"); sseEmitter.send(builder); } catch (IOException e) { log.error("推送失敗,訂閱信息:{},e:{}", p.getKey(), e); }
推薦
方案一和方案二同時使用,在一些交易服務中,個別用戶可能會打開多個界面,一個界面使用一個長鏈接,以Chrome來講最多創建6個長鏈接,當使用多子域方案時能夠進行水平擴展,打破這個限制。
附錄
各個瀏覽器長鏈接個數
瀏覽器及版本 | 最大鏈接數 |
---|---|
Internet Explorer的® 7.0 | 2 |
Internet Explorer 8.0和9.0 | 6 |
Internet Explorer 10.0 | 8 |
Internet Explorer 11.0 | 13 |
火狐® | 6 |
Chrome™ | 6 |
Safari瀏覽器® | 6 |
歌劇® | 6 |
iOS版® | 6 |
Android™ | 6 |