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

2018-08-08 14:57 更新

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

sse-real-time-web-00

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

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

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

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

sse-real-time-web-01

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

sse-real-time-web-02

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

sse-real-time-web-09

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

sse-real-time-web-09

sse-real-time-web-05

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

sse-real-time-web-04

sse-real-time-web-06

SSE vs. WebSocket

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

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

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

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

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

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

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

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

從服務(wù)端向客戶端傳輸數(shù)據(jù)的性能如何?如果是文本數(shù)據(jù)而非二進制數(shù)據(jù)(如前文所提到的),SSE和WebSocket沒什么區(qū)別。它們都用TCP/IP套接字,都是輕量級協(xié)議。延遲、帶寬、服務(wù)器負載等都沒有區(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)時間
            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ù)器時間的信息給客戶端
            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"。這個資源有一個 @GET 資源方法返回作為一個實體 EventOutput ——通用 Jersey ChunkedOutput API 的擴展用于輸出分塊消息處理。

客戶端代碼:

//判斷瀏覽器是否支持 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)錯誤發(fā)生
    source.onerror = function(event) {
        console.log("連接錯誤!");

    };
} 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 屬性只支持一個事件處理方法。。

效果

運行項目

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)時間
        message = now +":"+ name +":"+ message;  // 發(fā)送的消息帶上當(dāng)前的時間

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

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

您可能已經(jīng)注意到,Jersey SseBroadcaster 完成該用例不是強制性的。每個 EventOutput 可以只是存儲在收集器里,在 broadcastMessage 方法里面迭代。然而,SseBroadcaster 內(nèi)部會識別和處理客戶端斷開連接。當(dāng)客戶端關(guān)閉了連接,broadcaster 可檢測并刪除過期的在內(nèi)部收集器里面注冊了 EventOutput 的連接,以及釋放所有服務(wù)器端關(guān)聯(lián)了陳舊連接的資源。此外,SseBroadcaster 的實現(xiàn)是線程安全的,這樣客戶端可以在任何時間連接和斷開, 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)錯誤發(fā)生
    source.onerror = function(event) {
        var ta = document.getElementById('response_text');
        ta.value = ta.value + '\n' + "連接出錯!";

    };
} 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ù)端,而后將該消息進行廣播,從而達到了聊天室的效果。

效果

sse-real-time-web-08

相關(guān)問題

異步請求

報如下錯誤:

八月 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
同一域名,不同二級域名(同上)不允許(cookie這種情況下也不允許訪問)
http://www.cnblogs.com/a.js
http://www.a.com/b.js
不同域名不允許

出于安全考慮,默認是不允許跨域訪問的,會報如下異常:

sse-real-time-web-10

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

先是做一個過濾器 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 項目

參考:


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號