REST 用SSE構(gòu)建實(shí)時(shí)Web應(yīng)用

2018-08-08 14:57 更新

在講Server-Sent Events (SSE) 之前,我們先來看看 HTTP 請求- 響應(yīng)。一個(gè)標(biāo)準(zhǔn)的 HTTP 請求- 響應(yīng),需要客戶端打開一個(gè)連接,將一個(gè) HTTP 請求(如 HTTP GET 請求)發(fā)送到服務(wù)端,然后接收到 HTTP 回來的響應(yīng),如果該響應(yīng)被完全發(fā)送或者接收,服務(wù)端就會(huì)把連接關(guān)閉。通常是由某個(gè)客戶發(fā)起,客戶端才會(huì)需要請求所有數(shù)據(jù)。

sse-real-time-web-00

然而, Server-Sent Events (SSE) 與 HTTP 請求- 響應(yīng)背道而馳,它是一種機(jī)制,客戶端一旦建立起客戶機(jī)-服務(wù)器的連接,就能讓服務(wù)端將數(shù)據(jù)以異步的方式從服務(wù)器推到客戶端。當(dāng)連接由客戶端建立完成,服務(wù)端就提供數(shù)據(jù),并決定新數(shù)據(jù)“塊"可用時(shí)將其發(fā)送到客戶端。當(dāng)一個(gè)新的數(shù)據(jù)事件發(fā)生在服務(wù)端時(shí),這個(gè)事件被服務(wù)端發(fā)送到客戶端。因此,名稱被稱為 Server-Sent Events(服務(wù)器推送事件)。下面是支持服務(wù)端到客戶端交互的技術(shù)總覽:

  • 插件提供 socket 方式:比如利用 Flash XMLSocket,Java Applet 套接口,Activex 包裝的 socket。

    • 優(yōu)點(diǎn):原生 socket 的支持,與 PC 端的實(shí)現(xiàn)方式相似;
    • 缺點(diǎn):瀏覽器端需要裝相應(yīng)的插件;與 js 進(jìn)行交互時(shí)復(fù)雜
  • Polling:輪詢,重復(fù)發(fā)送新的請求到服務(wù)端。如果服務(wù)端沒有新的數(shù)據(jù),就發(fā)送適當(dāng)?shù)闹甘静㈥P(guān)閉連接。然后客戶端等待一段時(shí)間后,發(fā)送另一個(gè)請求(例如,一秒后)

    • 優(yōu)點(diǎn):實(shí)現(xiàn)簡單,無需做過多的更改
    • 缺點(diǎn):輪詢的間隔過長,會(huì)導(dǎo)致用戶不能及時(shí)接收到更新的數(shù)據(jù);輪詢的間隔過短,會(huì)導(dǎo)致查詢請求過多,增加服務(wù)器端的負(fù)擔(dān)。

sse-real-time-web-01

  • Long-polling:長輪詢,客戶端發(fā)送一個(gè)請求到服務(wù)端,如果服務(wù)端沒有新的數(shù)據(jù),就保持住這個(gè)連接直到有數(shù)據(jù)。一旦服務(wù)端有了數(shù)據(jù)(消息)給客戶端,它就使用這個(gè)連接發(fā)送數(shù)據(jù)給客戶端。接著連接關(guān)閉。
    • 優(yōu)點(diǎn):比 Polling 做了優(yōu)化,有較好的時(shí)效性
    • 缺點(diǎn):需第三方庫支持,實(shí)現(xiàn)較為復(fù)雜;每次連接只能發(fā)送一個(gè)數(shù)據(jù),多個(gè)數(shù)據(jù)發(fā)送時(shí)耗費(fèi)服務(wù)器性能

sse-real-time-web-02

  • 基于 iframe 及 htmlfile 的流(streaming)方式:iframe 流方式是在頁面中插入一個(gè)隱藏的 iframe,利用其src屬性在服務(wù)器和客戶端之間創(chuàng)建一條長鏈接,服務(wù)器向 iframe 傳輸數(shù)據(jù)(通常是 HTML,內(nèi)有負(fù)責(zé)插入信息的 javascript),來實(shí)時(shí)更新頁面。
    • 優(yōu)點(diǎn):消息能夠?qū)崟r(shí)到達(dá);
    • 缺點(diǎn):服務(wù)器維持著長連接期會(huì)消耗資源;iframe 不規(guī)范的用法;數(shù)據(jù)推送過程會(huì)有加載進(jìn)度條顯示,界面體驗(yàn)不好

sse-real-time-web-09

  • Server-Sent events:SSE 與 長輪詢機(jī)制類似,區(qū)別是每個(gè)連接不只發(fā)送一個(gè)消息??蛻舳税l(fā)送一個(gè)請求,服務(wù)端就保持這個(gè)連接直到有一個(gè)新的消息已經(jīng)準(zhǔn)備好了,那么它將消息發(fā)送回客戶端,同時(shí)仍然保持這個(gè)連接是打開,這樣這個(gè)連接就可以用于另一個(gè)可用消息的發(fā)送。一旦準(zhǔn)備好了一個(gè)新消息,通過同一初始連接發(fā)送回客戶端??蛻舳藛为?dú)處理來自服務(wù)端傳回的消息后不關(guān)閉連接。所以,SSE 通常重用一個(gè)連接處理多個(gè)消息(稱為事件)。SSE 還定義了一個(gè)專門的媒體類型 text/event-stream,描述一個(gè)從服務(wù)端發(fā)送到客戶端的簡單格式。SSE 還提供在大多數(shù)現(xiàn)代瀏覽器里的標(biāo)準(zhǔn) javascript 客戶端 API 實(shí)現(xiàn)。關(guān)于 SSE 的更多信息,請參見 SSE API 規(guī)范
    • 優(yōu)點(diǎn):HTML5 標(biāo)準(zhǔn);實(shí)現(xiàn)較為簡單;一個(gè)連接可以發(fā)送多個(gè)數(shù)據(jù)
    • 缺點(diǎn):IE 不支持 EventSource(可以使用第三方的 js 庫來解決,具體可以本章中的源碼) ;服務(wù)器只能單向推送數(shù)據(jù)到客戶端

sse-real-time-web-09

sse-real-time-web-05

  • WebSocket: WebSocket 與上述技術(shù)都不同,因?yàn)樗峁┝艘粋€(gè)真正的全雙工連接。發(fā)起者是一個(gè)客戶端,發(fā)送一個(gè)帶特殊 HTTP 頭的請求到服務(wù)端,通知服務(wù)器, HTTP 連接可能“升級(jí)”到一個(gè)全雙工的 TCP/IP WebSocket 連接。如果服務(wù)端支持 WebSocket,它可能會(huì)選擇升級(jí)到 WebSocket。一旦建立 WebSocket 連接,它可用于客戶機(jī)和服務(wù)器之間的雙向通信。客戶端和服務(wù)器可以隨意向?qū)Ψ桨l(fā)送數(shù)據(jù)。此時(shí),新的 WebSocket 連接上的交互不再是基于 HTTP 協(xié)議了。 WebSocket 可以用于需要快速在兩個(gè)方向上交換小塊數(shù)據(jù)的在線游戲或任何其他應(yīng)用程序。(示例可以參考http://www.waylau.com/netty-websocket-chat/)
    • 優(yōu)點(diǎn):HTML5 標(biāo)準(zhǔn);大多數(shù)瀏覽器支持;真正全雙工;性能強(qiáng)
    • 缺點(diǎn):實(shí)現(xiàn)相對復(fù)雜;ws 協(xié)議

sse-real-time-web-04

sse-real-time-web-06

SSE vs. WebSocket

用比較籠統(tǒng)的一個(gè)說法,就是WebSocket能做的,SSE也能做,反之亦然,但是它們還是有差別的,特別是在完成某些任務(wù)方面。

WebSocket 是一種更為復(fù)雜的服務(wù)端實(shí)現(xiàn)技術(shù),但它是真正的雙向傳輸技術(shù),既能從服務(wù)端向客戶端推送數(shù)據(jù),也能從客戶端向服務(wù)端推送數(shù)據(jù)。

WebSocket 和 SSE 的瀏覽器支持率差不多,除了IE。IE是個(gè)例外,即便IE11都還不支持原生 SSE,IE10 添加了WebSocket 支持,可見上圖。

與 WebSocket 相比,SSE 有一些顯著的優(yōu)勢。我認(rèn)為它最大的優(yōu)勢就是便利:不需要添加任何新組件,用任何你習(xí)慣的后端語言和框架就能繼續(xù)使用。你不用為新建虛擬機(jī)、弄一個(gè)新的IP或新的端口號(hào)而勞神,就像在現(xiàn)有網(wǎng)站中新增一個(gè)頁面那樣簡單。我喜歡把這稱為既存基礎(chǔ)設(shè)施優(yōu)勢。

SSE 的第二個(gè)優(yōu)勢是服務(wù)端的簡潔。我們將在下節(jié)中看到,服務(wù)端代碼只需幾行。相對而言,WebSocket 則很復(fù)雜,不借助輔助類庫基本搞不定。

因?yàn)?SSE 能在現(xiàn)有的 HTTP/HTTPS 協(xié)議上運(yùn)作,所以它能直接運(yùn)行于現(xiàn)有的代理服務(wù)器和認(rèn)證技術(shù)。而對 WebSocket 而言,代理服務(wù)器需要做一些開發(fā)(或其他工作)才能支持,在寫這本書時(shí),很多服務(wù)器還沒有(雖然這種狀況會(huì)改善)。SSE還有一個(gè)優(yōu)勢:它是一種文本協(xié)議,腳本調(diào)試非常容易。事實(shí)上,在本書中,我們會(huì)在開發(fā)和測試時(shí)用 curl,甚至直接在命令行中運(yùn)行后端腳本。

不過,這就引出了 WebSocket 相較 SSE 的一個(gè)潛在優(yōu)勢:WebSocket 是二進(jìn)制協(xié)議,而 SSE 是文本協(xié)議(通常使用UTF-8編碼)。當(dāng)然,我們可以通過SSE連接傳輸二進(jìn)制數(shù)據(jù):在 SSE 中,只有兩個(gè)具有特殊意義的字符,它們是 CR 和LF,而對它們進(jìn)行轉(zhuǎn)碼并不難。但用 SSE 傳輸二進(jìn)制數(shù)據(jù)時(shí)數(shù)據(jù)會(huì)變大,如果需要從服務(wù)端到客戶端傳輸大量的二進(jìn)制數(shù)據(jù),最好還是用 WebSocket。

WebSocket 相較 SSE 最大的優(yōu)勢在于它是雙向交流的,這意味向服務(wù)端發(fā)送數(shù)據(jù)就像從服務(wù)端接收數(shù)據(jù)一樣簡單。用 SSE時(shí),一般通過一個(gè)獨(dú)立的 Ajax 請求從客戶端向服務(wù)端傳送數(shù)據(jù)。相對于 WebSocket,這樣使用 Ajax 會(huì)增加開銷,但也就多一點(diǎn)點(diǎn)而已。如此一來,問題就變成了“什么時(shí)候需要關(guān)心這個(gè)差異?”如果需要以1次/秒或者更快的頻率向服務(wù)端傳輸數(shù)據(jù),那應(yīng)該用 WebSocket。0.2次/秒到1次/秒的頻率是一個(gè)灰色地帶,用 WebSocket 和用 SSE 差別不大;但如果你期望重負(fù)載,那就有必要確定基準(zhǔn)點(diǎn)。頻率低于0.2次/秒左右時(shí),兩者差別不大。

從服務(wù)端向客戶端傳輸數(shù)據(jù)的性能如何?如果是文本數(shù)據(jù)而非二進(jìn)制數(shù)據(jù)(如前文所提到的),SSE和WebSocket沒什么區(qū)別。它們都用TCP/IP套接字,都是輕量級(jí)協(xié)議。延遲、帶寬、服務(wù)器負(fù)載等都沒有區(qū)別。

在舊版本瀏覽器上的兼容,WebSocket 難兼容,SSE 易兼容。

SSE 的應(yīng)用場景

看了上述的定義,可以知道 SSE 適合應(yīng)用于服務(wù)端單向推送信息到客戶端的場景。 Jersey 的 SSE 大致可以分為發(fā)布-訂閱模式和廣播模式。

為使用 Jersey SSE, 添加如下依賴:

<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-sse</artifactId>
</dependency> 

發(fā)布-訂閱模式

服務(wù)端代碼:

@Path("see-events")
public class SseResource {

    private EventOutput eventOutput = new EventOutput();
    private OutboundEvent.Builder eventBuilder;
    private OutboundEvent event ;

    /**
     * 提供 SSE 事件輸出通道的資源方法
     * @return eventOutput
     */
     @GET
    @Produces(SseFeature.SERVER_SENT_EVENTS)
    public EventOutput getServerSentEvents() {

         // 不斷循環(huán)執(zhí)行
        while (true) {
            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//設(shè)置日期格式
            String now =  df.format(new Date()); //獲取當(dāng)前系統(tǒng)時(shí)間
            String message = "Server Time:" + now;
            System.out.println( message );

            eventBuilder = new OutboundEvent.Builder();
            eventBuilder.id(now);
            eventBuilder.name("message");
            eventBuilder.data(String.class,
                    message );  // 推送服務(wù)器時(shí)間的信息給客戶端
            event = eventBuilder.build();
            try {
                eventOutput.write(event);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    eventOutput.close();
                    return eventOutput;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

上面的代碼定義了資源部署在 URI "see-events"。這個(gè)資源有一個(gè) @GET 資源方法返回作為一個(gè)實(shí)體 EventOutput ——通用 Jersey ChunkedOutput API 的擴(kuò)展用于輸出分塊消息處理。

客戶端代碼:

//判斷瀏覽器是否支持 EventSource
if (typeof (EventSource) !== "undefined") {
    var source = new EventSource("webapi/see-events");

    // 當(dāng)通往服務(wù)器的連接被打開
    source.onopen = function(event) {
        console.log("連接開啟!");

    };

    // 當(dāng)接收到消息。只能是事件名稱是 message
    source.onmessage = function(event) {
        console.log(event.data);
        var data = event.data;
        var lastEventId = event.lastEventId;
        document.getElementById("x").innerHTML += "\n" + 'lastEventId:'+lastEventId+';data:'+data;
    };

    //可以是任意命名的事件名稱
    /*
    source.addEventListener('message', function(event) {
        console.log(event.data);
        var data = event.data;
        var lastEventId = event.lastEventId;
        document.getElementById("x").innerHTML += "\n" + 'lastEventId:'+lastEventId+';data:'+data;
    });
    */

    // 當(dāng)錯(cuò)誤發(fā)生
    source.onerror = function(event) {
        console.log("連接錯(cuò)誤!");

    };
} else {
    document.getElementById("result").innerHTML = "Sorry, your browser does not support server-sent events..."
}

首先要判斷瀏覽器是否支持 EventSource,而后,EventSource 對象分別監(jiān)聽 onopen、onmessage、onerror 事件。其中, source.onmessage = function(event) {} 和 source.addEventListener('message', function(event) {} 是一樣的,區(qū)別是,后者可以支持監(jiān)聽不同名稱的事件,而 onmessage 屬性只支持一個(gè)事件處理方法。。

效果

運(yùn)行項(xiàng)目

mvn jetty:run

瀏覽器訪問 http://localhost:8080

sse-real-time-web-07

廣播模式

服務(wù)端代碼:

@Singleton
@Path("sse-chat")
public class SseChatResource {

    private SseBroadcaster broadcaster = new SseBroadcaster();

    /**
     * 提供 SSE 事件輸出通道的資源方法
     * @return eventOutput
     */
    @GET
    @Produces(SseFeature.SERVER_SENT_EVENTS)
    public EventOutput listenToBroadcast() {
        EventOutput eventOutput = new EventOutput();
        this.broadcaster.add(eventOutput);
        return eventOutput;
    }

    /**
     * 提供 寫入 SSE 事件通道的資源方法
     * @param message
     * @param name
     */
    @POST
    @Produces(MediaType.TEXT_PLAIN)
    public void broadcastMessage(@DefaultValue("waylau.com") @QueryParam("message")  String message,
            @DefaultValue("waylau") @QueryParam("name")  String name) {
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//設(shè)置日期格式
        String now =  df.format(new Date()); //獲取當(dāng)前系統(tǒng)時(shí)間
        message = now +":"+ name +":"+ message;  // 發(fā)送的消息帶上當(dāng)前的時(shí)間

        OutboundEvent.Builder eventBuilder = new OutboundEvent.Builder();
        OutboundEvent event = eventBuilder.name("message")
            .mediaType(MediaType.TEXT_PLAIN_TYPE)
            .data(String.class, message)
            .build();

        // 發(fā)送廣播
        broadcaster.broadcast(event);
     }
}

其中,SseChatResource 資源類用 @Singleton 注解,告訴 Jersey 運(yùn)行時(shí),資源類只有一個(gè)實(shí)例,用于所有傳入/sse-chat路徑的請求。應(yīng)用程序引用私有的 broadcaster 字段,這樣我們?yōu)樗姓埱罂梢允褂孟嗤膶?shí)例。客戶端想監(jiān)聽 SSE 事件,先發(fā)送 GET 請求到sse-chat的 listenToBroadcast() 資源方法處理。方法創(chuàng)建一個(gè)新的 EventOutput 用于展示請求的客戶端的連接,并通過 add(EventOutput) 注冊 eventOutput 實(shí)例到單例 broadcaster。方法返回 eventOutput 導(dǎo)致 Jersey 使請求的客戶端事件與 eventOutput 實(shí)例綁定,向客戶機(jī)發(fā)送響應(yīng) HTTP 頭??蛻舳诉B接保持開放,客戶端等待準(zhǔn)備接收新的 SSE 事件。所有的事件通過 broadcaster 寫入 eventOutput。這樣開發(fā)人員可以方便地處理發(fā)送新的事件到所有訂閱的客戶端。

當(dāng)客戶端想要廣播新消息給所有的已經(jīng)監(jiān)聽 SSE 連接的客戶端時(shí),它先發(fā)送一個(gè) POST 請求將消息內(nèi)容發(fā)到 SseChatResource 資源。 SseChatResource 資源調(diào)用方法 broadcastMessage,消息內(nèi)容作為輸入?yún)?shù)。一個(gè)新的 SSE 出站事件是建立在標(biāo)準(zhǔn)方法上并傳遞給 broadcaster。broadcaster 內(nèi)部在所有注冊了的 EventOutput 上調(diào)用 write(OutboundEvent) 。當(dāng)該方法只返回一個(gè)標(biāo)準(zhǔn)文本響應(yīng)給客戶端,來通知客戶端已經(jīng)成功廣播了消息。正如您可以看到的, broadcastMessage 資源方法只是一個(gè)簡單的 JAX-RS 資源的方法。

您可能已經(jīng)注意到,Jersey SseBroadcaster 完成該用例不是強(qiáng)制性的。每個(gè) EventOutput 可以只是存儲(chǔ)在收集器里,在 broadcastMessage 方法里面迭代。然而,SseBroadcaster 內(nèi)部會(huì)識(shí)別和處理客戶端斷開連接。當(dāng)客戶端關(guān)閉了連接,broadcaster 可檢測并刪除過期的在內(nèi)部收集器里面注冊了 EventOutput 的連接,以及釋放所有服務(wù)器端關(guān)聯(lián)了陳舊連接的資源。此外,SseBroadcaster 的實(shí)現(xiàn)是線程安全的,這樣客戶端可以在任何時(shí)間連接和斷開, SseBroadcaster 總是廣播消息給最近收集的注冊和活躍的客戶端。

客戶端代碼:

//判斷瀏覽器是否支持 EventSource
if (typeof (EventSource) !== "undefined") {
    var source = new EventSource("webapi/sse-chat");

    // 當(dāng)通往服務(wù)器的連接被打開
    source.onopen = function(event) {
        var ta = document.getElementById('response_text');
        ta.value = '連接開啟!';
    };

    // 當(dāng)接收到消息。只能是事件名稱是 message
    source.onmessage = function(event) {
        var ta = document.getElementById('response_text');
        ta.value = ta.value + '\n' + event.data;
    };

    //可以是任意命名的事件名稱
    /*
    source.addEventListener('message', function(event) {
         var ta = document.getElementById('response_text');
         ta.value = ta.value + '\n' + event.data;
    });
    */

    // 當(dāng)錯(cuò)誤發(fā)生
    source.onerror = function(event) {
        var ta = document.getElementById('response_text');
        ta.value = ta.value + '\n' + "連接出錯(cuò)!";

    };
} else {
    alert("Sorry, your browser does not support server-sent events");
}

function send(message) {
    var xmlhttp;
    var name = document.getElementById('name_id').value;

    if (window.XMLHttpRequest)
    {// code for IE7+, Firefox, Chrome, Opera, Safari
        xmlhttp=new XMLHttpRequest();
    }
    else
    {// code for IE6, IE5
        xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
    }

    xmlhttp.open("POST","webapi/sse-chat?message=" + message +'&name=' + name ,true);
    xmlhttp.send();
}

EventSource 的用法與發(fā)布-訂閱模式類似。而 send(message) 方法是將消息以 POST 請求發(fā)送給服務(wù)端,而后將該消息進(jìn)行廣播,從而達(dá)到了聊天室的效果。

效果

sse-real-time-web-08

相關(guān)問題

異步請求

報(bào)如下錯(cuò)誤:

八月 18, 2015 7:48:28 下午 org.glassfish.jersey.servlet.internal.ResponseWriter suspend
WARNING: Attempt to put servlet request into asynchronous mode has failed. Please check your servlet configuration - all Servlet instances and Servlet filters involved in the request processing must explicitly declare support for asynchronous request processing.
java.lang.IllegalStateException: !asyncSupported
    at org.eclipse.jetty.server.Request.startAsync(Request.java:2072)
    at org.glassfish.jersey.servlet.async.AsyncContextDelegateProviderImpl$ExtensionImpl.getAsyncContext(AsyncContextDelegateProviderImpl.java:112)
    at org.glassfish.jersey.servlet.async.AsyncContextDelegateProviderImpl$ExtensionImpl.suspend(AsyncContextDelegateProviderImpl.java:96)
    at org.glassfish.jersey.servlet.internal.ResponseWriter.suspend(ResponseWriter.java:121)
    at org.glassfish.jersey.server.ServerRuntime$Responder.writeResponse(ServerRuntime.java:747)
    at org.glassfish.jersey.server.ServerRuntime$Responder.processResponse(ServerRuntime.java:424)
    at org.glassfish.jersey.server.ServerRuntime$Responder.process(ServerRuntime.java:414)
    at org.glassfish.jersey.server.ServerRuntime$2.run(ServerRuntime.java:312)
    at org.glassfish.jersey.internal.Errors$1.call(Errors.java:271)
    at org.glassfish.jersey.internal.Errors$1.call(Errors.java:267)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:315)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:297)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:267)
    at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:317)
    at org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:292)
    at org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:1139)
    at org.glassfish.jersey.servlet.WebComponent.service(WebComponent.java:460)
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:386)
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:334)
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:221)
    at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:808)
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:587)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:577)
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:223)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1127)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:515)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:185)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1061)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
    at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:215)
    at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:110)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:97)
    at org.eclipse.jetty.server.Server.handle(Server.java:497)
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:310)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:257)
    at org.eclipse.jetty.io.AbstractConnection$2.run(AbstractConnection.java:540)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:635)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:555)
    at java.lang.Thread.run(Thread.java:722)

是指服務(wù)器不支持異步請求。解決方法是在 web.xml 中添加

<async-supported>true</async-supported>

最后的 web.xml 為:

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
    id="WebApp_ID" version="3.1">

    <servlet>
        <servlet-name>Jersey Web Application</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>com.waylau.rest.RestApplication</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported>
    </servlet>
    <servlet-mapping>
        <servlet-name>Jersey Web Application</servlet-name>
        <url-pattern>/webapi/*</url-pattern>
    </servlet-mapping>
</web-app>

跨域請求

由于瀏覽器同源策略,凡是發(fā)送請求url的協(xié)議、域名、端口三者之間任意一與當(dāng)前頁面地址不同即為跨域。

URL說明是否允許通信
http://www.a.com/a.js
http://www.a.com/b.js
同一域名下允許
http://www.a.com/lab/a.js
http://www.a.com/script/b.js
同一域名下不同文件夾允許
http://www.a.com:8000/a.js
http://www.a.com/b.js
同一域名,不同端口不允許
http://www.a.com/a.js
https://www.a.com/b.js
同一域名,不同協(xié)議不允許
http://www.a.com/a.js
http://70.32.92.74/b.js
域名和域名對應(yīng)ip不允許
http://www.a.com/a.js
http://script.a.com/b.js
主域相同,子域不同不允許
http://www.a.com/a.js
http://a.com/b.js
同一域名,不同二級(jí)域名(同上)不允許(cookie這種情況下也不允許訪問)
http://www.cnblogs.com/a.js
http://www.a.com/b.js
不同域名不允許

出于安全考慮,默認(rèn)是不允許跨域訪問的,會(huì)報(bào)如下異常:

sse-real-time-web-10

解決是服務(wù)器啟動(dòng) CORS。

先是做一個(gè)過濾器 CrossDomainFilter.java,將響應(yīng)頭“Access-Control-Allow-Origin”設(shè)置為“*”

@Override
public void filter(ContainerRequestContext requestContext,
        ContainerResponseContext responseContext) throws IOException {

    // 響應(yīng)頭添加了對允許訪問的域,* 代表是全部域
    responseContext.getHeaders().add("Access-Control-Allow-Origin", "*"); 

}

在 RestApplication 里,注冊該過濾器即可。

public class RestApplication extends ResourceConfig {

    public RestApplication() {
        // 資源類所在的包路徑  
        packages("com.waylau.rest.resource");

        // 注冊 MultiPart
        register(MultiPartFeature.class);

        // 注冊CORS過濾器
        register(CrossDomainFilter.class);
    }
}

這樣,就能跨域訪問了,如下,192.168.11.103 可以訪問 192.168.11.125 站下的資源

sse-real-time-web-11

源碼

見 sse-real-time-web 項(xiàng)目

參考:


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)