Browser connection limitations(瀏覽器長鏈接個數限制)解決方案

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
相關文章
相關標籤/搜索