diff --git a/.gitignore b/.gitignore index 5b3ef4d..6499eff 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ /.idea/ /.settings/ .classpath -.project -_book +.project \ No newline at end of file diff --git a/docs/Build Real-Time Web App with SSE.md b/docs/Build Real-Time Web App with SSE.md index a4dc3db..8980f24 100644 --- a/docs/Build Real-Time Web App with SSE.md +++ b/docs/Build Real-Time Web App with SSE.md @@ -290,7 +290,9 @@ EventSource 的用法与发布-订阅模式类似。而 send(message) 方法是 ![](../images/sse-real-time-web-08.jpg) -## 错误解决 +## 相关问题 + +### 异步请求 报如下错误: @@ -365,8 +367,114 @@ EventSource 的用法与发布-订阅模式类似。而 send(message) 方法是 +### 跨域请求 +由于浏览器同源策略,凡是发送请求url的协议、域名、端口三者之间任意一与当前页面地址不同即为跨域。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
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
同一域名,不同协议不允许
http://www.a.com/a.js
+http://70.32.92.74/b.js
域名和域名对应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
不同域名不允许
+ + +出于安全考虑,默认是不允许跨域访问的,会报如下异常: + +![](../images/sse-real-time-web-10.jpg) + +解决是服务器启动 [CORS](http://www.w3.org/TR/cors/)。 + +先是做一个过滤器 CrossDomainFilter.java,将响应头“Access-Control-Allow-Origin”设置为“*” + + @Override + public void filter(ContainerRequestContext requestContext, + ContainerResponseContext responseContext) throws IOException { + + // 响应头添加了对允许访问的域,* 代表是全部域 + 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 站下的资源 + +![](../images/sse-real-time-web-11.jpg) + + +## 源码 + +见 `sse-real-time-web` 项目 + ##参考: * Data Push Apps with HTML5 SSE(by Darren Cook) * [Jersey 2.x 用户指南](https://github.com/waylau/Jersey-2.x-User-Guide) -* \ No newline at end of file +* +* diff --git a/images/sse-real-time-web-10.jpg b/images/sse-real-time-web-10.jpg new file mode 100644 index 0000000..589b5b1 Binary files /dev/null and b/images/sse-real-time-web-10.jpg differ diff --git a/images/sse-real-time-web-11.jpg b/images/sse-real-time-web-11.jpg new file mode 100644 index 0000000..531ad1c Binary files /dev/null and b/images/sse-real-time-web-11.jpg differ diff --git a/samples/sse-real-time-web/.gitignore b/samples/sse-real-time-web/.gitignore index 5b3ef4d..6499eff 100644 --- a/samples/sse-real-time-web/.gitignore +++ b/samples/sse-real-time-web/.gitignore @@ -2,5 +2,4 @@ /.idea/ /.settings/ .classpath -.project -_book +.project \ No newline at end of file diff --git a/samples/sse-real-time-web/src/main/java/com/waylau/rest/RestApplication.java b/samples/sse-real-time-web/src/main/java/com/waylau/rest/RestApplication.java index e96c2db..4d23c45 100644 --- a/samples/sse-real-time-web/src/main/java/com/waylau/rest/RestApplication.java +++ b/samples/sse-real-time-web/src/main/java/com/waylau/rest/RestApplication.java @@ -3,6 +3,8 @@ import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.server.ResourceConfig; +import com.waylau.rest.filter.CrossDomainFilter; + /** * REST 主应用 * @@ -12,10 +14,13 @@ public class RestApplication extends ResourceConfig { public RestApplication() { - //资源类所在的包路径 + // 资源类所在的包路径 packages("com.waylau.rest.resource"); - //注册 MultiPart + // 注册 MultiPart register(MultiPartFeature.class); + + // 注册CORS过滤器 + register(CrossDomainFilter.class); } } diff --git a/samples/sse-real-time-web/src/main/java/com/waylau/rest/filter/CrossDomainFilter.java b/samples/sse-real-time-web/src/main/java/com/waylau/rest/filter/CrossDomainFilter.java new file mode 100644 index 0000000..48f3a3e --- /dev/null +++ b/samples/sse-real-time-web/src/main/java/com/waylau/rest/filter/CrossDomainFilter.java @@ -0,0 +1,35 @@ +package com.waylau.rest.filter; + +import java.io.IOException; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; + +/** + * 说明:支持 Cross-domain 请求 + * + * @author waylau.com 2015年8月22日 + */ +public class CrossDomainFilter implements ContainerResponseFilter { + + /** + * + */ + public CrossDomainFilter() { + // TODO Auto-generated constructor stub + } + + /* (non-Javadoc) + * @see javax.ws.rs.container.ContainerResponseFilter#filter(javax.ws.rs.container.ContainerRequestContext, javax.ws.rs.container.ContainerResponseContext) + */ + @Override + public void filter(ContainerRequestContext requestContext, + ContainerResponseContext responseContext) throws IOException { + + // 响应头添加了对允许访问的域,* 代表是全部域 + responseContext.getHeaders().add("Access-Control-Allow-Origin", "*"); + + } + +} diff --git a/samples/sse-real-time-web/src/main/java/com/waylau/rest/filter/CrossDomainFilter2.java b/samples/sse-real-time-web/src/main/java/com/waylau/rest/filter/CrossDomainFilter2.java new file mode 100644 index 0000000..00bc405 --- /dev/null +++ b/samples/sse-real-time-web/src/main/java/com/waylau/rest/filter/CrossDomainFilter2.java @@ -0,0 +1,17 @@ +package com.waylau.rest.filter; + +/** + * 说明:支持 Cross-domain 请求 + * + * @author waylau.com 2015年8月22日 + */ +public class CrossDomainFilter2 { + + /** + * + */ + public CrossDomainFilter2() { + // TODO Auto-generated constructor stub + } + +} diff --git a/samples/sse-real-time-web/src/main/webapp/index.html b/samples/sse-real-time-web/src/main/webapp/index.html index 3033dc5..21c6973 100644 --- a/samples/sse-real-time-web/src/main/webapp/index.html +++ b/samples/sse-real-time-web/src/main/webapp/index.html @@ -9,6 +9,11 @@

Jersey RESTful Web Application!

进入聊天室 +

+ IE 浏览器的 SSE +

+ SSE 的 CORS +

Get time from server

Initializing...

diff --git a/samples/sse-real-time-web/src/main/webapp/scripts/sse_real_time_cors.js b/samples/sse-real-time-web/src/main/webapp/scripts/sse_real_time_cors.js new file mode 100644 index 0000000..529bbfb --- /dev/null +++ b/samples/sse-real-time-web/src/main/webapp/scripts/sse_real_time_cors.js @@ -0,0 +1,41 @@ + +/** + * Created by waylau.com on 2015/8/22 + */ + +//判断浏览器是否支持 EventSource +if (typeof (EventSource) !== "undefined") { + var source = new EventSource("http://192.168.11.125:8080/webapi/see-events"); + + // 当通往服务器的连接被打开 + source.onopen = function(event) { + console.log("连接开启!"); + + }; + + // 当接收到消息。只能是事件名称是 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; + }); + */ + + // 当错误发生 + source.onerror = function(event) { + console.log("连接错误!"); + + }; +} else { + document.getElementById("result").innerHTML = "Sorry, your browser does not support server-sent events..." +} \ No newline at end of file diff --git a/samples/sse-real-time-web/src/main/webapp/scripts/vendor/eventsource.js b/samples/sse-real-time-web/src/main/webapp/scripts/vendor/eventsource.js new file mode 100644 index 0000000..a351950 --- /dev/null +++ b/samples/sse-real-time-web/src/main/webapp/scripts/vendor/eventsource.js @@ -0,0 +1,537 @@ +/** @license + * eventsource.js + * Available under MIT License (MIT) + * https://github.com/Yaffle/EventSource/ + */ + +/*jslint indent: 2, vars: true, plusplus: true */ +/*global setTimeout, clearTimeout */ + +(function (global) { + "use strict"; + + var setTimeout = global.setTimeout; + var clearTimeout = global.clearTimeout; + + function Map() { + this.data = {}; + } + + Map.prototype.get = function (key) { + return this.data[key + "~"]; + }; + Map.prototype.set = function (key, value) { + this.data[key + "~"] = value; + }; + Map.prototype["delete"] = function (key) { + delete this.data[key + "~"]; + }; + + function EventTarget() { + this.listeners = new Map(); + } + + function throwError(e) { + setTimeout(function () { + throw e; + }, 0); + } + + EventTarget.prototype.dispatchEvent = function (event) { + event.target = this; + var type = event.type.toString(); + var listeners = this.listeners; + var typeListeners = listeners.get(type); + if (typeListeners == undefined) { + return; + } + var length = typeListeners.length; + var i = -1; + var listener = undefined; + while (++i < length) { + listener = typeListeners[i]; + try { + listener.call(this, event); + } catch (e) { + throwError(e); + } + } + }; + EventTarget.prototype.addEventListener = function (type, callback) { + type = type.toString(); + var listeners = this.listeners; + var typeListeners = listeners.get(type); + if (typeListeners == undefined) { + typeListeners = []; + listeners.set(type, typeListeners); + } + var i = typeListeners.length; + while (--i >= 0) { + if (typeListeners[i] === callback) { + return; + } + } + typeListeners.push(callback); + }; + EventTarget.prototype.removeEventListener = function (type, callback) { + type = type.toString(); + var listeners = this.listeners; + var typeListeners = listeners.get(type); + if (typeListeners == undefined) { + return; + } + var length = typeListeners.length; + var filtered = []; + var i = -1; + while (++i < length) { + if (typeListeners[i] !== callback) { + filtered.push(typeListeners[i]); + } + } + if (filtered.length === 0) { + listeners["delete"](type); + } else { + listeners.set(type, filtered); + } + }; + + function Event(type) { + this.type = type; + this.target = undefined; + } + + function MessageEvent(type, options) { + Event.call(this, type); + this.data = options.data; + this.lastEventId = options.lastEventId; + } + + MessageEvent.prototype = Event.prototype; + + var XHR = global.XMLHttpRequest; + var XDR = global.XDomainRequest; + var isCORSSupported = XHR != undefined && (new XHR()).withCredentials != undefined; + var Transport = isCORSSupported || (XHR != undefined && XDR == undefined) ? XHR : XDR; + + var WAITING = -1; + var CONNECTING = 0; + var OPEN = 1; + var CLOSED = 2; + var AFTER_CR = 3; + var FIELD_START = 4; + var FIELD = 5; + var VALUE_START = 6; + var VALUE = 7; + var contentTypeRegExp = /^text\/event\-stream;?(\s*charset\=utf\-8)?$/i; + + var MINIMUM_DURATION = 1000; + var MAXIMUM_DURATION = 18000000; + + function getDuration(value, def) { + var n = value; + if (n !== n) { + n = def; + } + return (n < MINIMUM_DURATION ? MINIMUM_DURATION : (n > MAXIMUM_DURATION ? MAXIMUM_DURATION : n)); + } + + function fire(that, f, event) { + try { + if (typeof f === "function") { + f.call(that, event); + } + } catch (e) { + throwError(e); + } + } + + function EventSource(url, options) { + url = url.toString(); + + var withCredentials = isCORSSupported && options != undefined && Boolean(options.withCredentials); + var initialRetry = getDuration(1000, 0); + var heartbeatTimeout = getDuration(45000, 0); + + var lastEventId = ""; + var that = this; + var retry = initialRetry; + var wasActivity = false; + var CurrentTransport = options != undefined && options.Transport != undefined ? options.Transport : Transport; + var xhr = new CurrentTransport(); + var isXHR = CurrentTransport !== XDR; + var timeout = 0; + var timeout0 = 0; + var charOffset = 0; + var currentState = WAITING; + var dataBuffer = []; + var lastEventIdBuffer = ""; + var eventTypeBuffer = ""; + var onTimeout = undefined; + + var state = FIELD_START; + var field = ""; + var value = ""; + + function close() { + currentState = CLOSED; + if (xhr != undefined) { + xhr.abort(); + xhr = undefined; + } + if (timeout !== 0) { + clearTimeout(timeout); + timeout = 0; + } + if (timeout0 !== 0) { + clearTimeout(timeout0); + timeout0 = 0; + } + that.readyState = CLOSED; + } + + function onEvent(type) { + var responseText = ""; + if (currentState === OPEN || currentState === CONNECTING) { + try { + responseText = xhr.responseText; + } catch (error) { + // IE 8 - 9 with XMLHttpRequest + } + } + var event = undefined; + var isWrongStatusCodeOrContentType = false; + + if (currentState === CONNECTING) { + var status = 0; + var statusText = ""; + var contentType = undefined; + if (isXHR) { + try { + status = xhr.status; + statusText = xhr.statusText; + contentType = xhr.getResponseHeader("Content-Type"); + } catch (error) { + // https://bugs.webkit.org/show_bug.cgi?id=29121 + status = 0; + statusText = ""; + contentType = undefined; + // FF < 14, WebKit + // https://bugs.webkit.org/show_bug.cgi?id=29658 + // https://bugs.webkit.org/show_bug.cgi?id=77854 + } + } else if (type !== "" && type !== "error") { + status = 200; + statusText = "OK"; + contentType = xhr.contentType; + } + if (contentType == undefined) { + contentType = ""; + } + if (status === 0 && statusText === "" && type === "load" && responseText !== "") { + status = 200; + statusText = "OK"; + if (contentType === "") { // Opera 12 + var tmp = (/^data\:([^,]*?)(?:;base64)?,[\S]*$/).exec(url); + if (tmp != undefined) { + contentType = tmp[1]; + } + } + } + if (status === 200 && contentTypeRegExp.test(contentType)) { + currentState = OPEN; + wasActivity = true; + retry = initialRetry; + that.readyState = OPEN; + event = new Event("open"); + that.dispatchEvent(event); + fire(that, that.onopen, event); + if (currentState === CLOSED) { + return; + } + } else { + // Opera 12 + if (status !== 0 && (status !== 200 || contentType !== "")) { + var message = ""; + if (status !== 200) { + message = "EventSource's response has a status " + status + " " + statusText.replace(/\s+/g, " ") + " that is not 200. Aborting the connection."; + } else { + message = "EventSource's response has a Content-Type specifying an unsupported type: " + contentType.replace(/\s+/g, " ") + ". Aborting the connection."; + } + setTimeout(function () { + throw new Error(message); + }, 0); + isWrongStatusCodeOrContentType = true; + } + } + } + + if (currentState === OPEN) { + if (responseText.length > charOffset) { + wasActivity = true; + } + var i = charOffset - 1; + var length = responseText.length; + var c = "\n"; + while (++i < length) { + c = responseText.charAt(i); + if (state === AFTER_CR && c === "\n") { + state = FIELD_START; + } else { + if (state === AFTER_CR) { + state = FIELD_START; + } + if (c === "\r" || c === "\n") { + if (field === "data") { + dataBuffer.push(value); + } else if (field === "id") { + lastEventIdBuffer = value; + } else if (field === "event") { + eventTypeBuffer = value; + } else if (field === "retry") { + initialRetry = getDuration(Number(value), initialRetry); + retry = initialRetry; + } else if (field === "heartbeatTimeout") { + heartbeatTimeout = getDuration(Number(value), heartbeatTimeout); + if (timeout !== 0) { + clearTimeout(timeout); + timeout = setTimeout(onTimeout, heartbeatTimeout); + } + } + value = ""; + field = ""; + if (state === FIELD_START) { + if (dataBuffer.length !== 0) { + lastEventId = lastEventIdBuffer; + if (eventTypeBuffer === "") { + eventTypeBuffer = "message"; + } + event = new MessageEvent(eventTypeBuffer, { + data: dataBuffer.join("\n"), + lastEventId: lastEventIdBuffer + }); + that.dispatchEvent(event); + if (eventTypeBuffer === "message") { + fire(that, that.onmessage, event); + } + if (currentState === CLOSED) { + return; + } + } + dataBuffer.length = 0; + eventTypeBuffer = ""; + } + state = c === "\r" ? AFTER_CR : FIELD_START; + } else { + if (state === FIELD_START) { + state = FIELD; + } + if (state === FIELD) { + if (c === ":") { + state = VALUE_START; + } else { + field += c; + } + } else if (state === VALUE_START) { + if (c !== " ") { + value += c; + } + state = VALUE; + } else if (state === VALUE) { + value += c; + } + } + } + } + charOffset = length; + } + + if ((currentState === OPEN || currentState === CONNECTING) && + (type === "load" || type === "error" || isWrongStatusCodeOrContentType || (charOffset > 1024 * 1024) || (timeout === 0 && !wasActivity))) { + if (isWrongStatusCodeOrContentType) { + close(); + } else { + if (type === "" && timeout === 0 && !wasActivity) { + setTimeout(function () { + throw new Error("No activity within " + heartbeatTimeout + " milliseconds. Reconnecting."); + }, 0); + } + currentState = WAITING; + xhr.abort(); + if (timeout !== 0) { + clearTimeout(timeout); + timeout = 0; + } + if (retry > initialRetry * 16) { + retry = initialRetry * 16; + } + if (retry > MAXIMUM_DURATION) { + retry = MAXIMUM_DURATION; + } + timeout = setTimeout(onTimeout, retry); + retry = retry * 2 + 1; + + that.readyState = CONNECTING; + } + event = new Event("error"); + that.dispatchEvent(event); + fire(that, that.onerror, event); + } else { + if (timeout === 0) { + wasActivity = false; + timeout = setTimeout(onTimeout, heartbeatTimeout); + } + } + } + + function onProgress() { + onEvent("progress"); + } + + function onLoad() { + onEvent("load"); + } + + function onError() { + onEvent("error"); + } + + function onReadyStateChange() { + if (xhr.readyState === 4) { + if (xhr.status === 0) { + onEvent("error"); + } else { + onEvent("load"); + } + } else { + onEvent("progress"); + } + } + + if (isXHR && global.opera != undefined) { + // workaround for Opera issue with "progress" events + timeout0 = setTimeout(function f() { + if (xhr.readyState === 3) { + onEvent("progress"); + } + timeout0 = setTimeout(f, 500); + }, 0); + } + + onTimeout = function () { + timeout = 0; + if (currentState !== WAITING) { + onEvent(""); + return; + } + + // loading indicator in Safari, Chrome < 14 + if (isXHR && !("onloadend" in xhr) && global.document != undefined && global.document.readyState != undefined && global.document.readyState !== "complete") { + timeout = setTimeout(onTimeout, 4); + return; + } + + // XDomainRequest#abort removes onprogress, onerror, onload + xhr.onload = onLoad; + xhr.onerror = onError; + + if (isXHR) { + // improper fix to match Firefox behaviour, but it is better than just ignore abort + // see https://bugzilla.mozilla.org/show_bug.cgi?id=768596 + // https://bugzilla.mozilla.org/show_bug.cgi?id=880200 + // https://code.google.com/p/chromium/issues/detail?id=153570 + xhr.onabort = onError; + } + if (isXHR && !("ontimeout" in xhr) || !("onloadend" in xhr)) { + // Firefox 3.5 - 3.6 - ? < 9.0 + // onprogress is not fired sometimes or delayed + // IE 8-9 (XMLHTTPRequest) + xhr.onreadystatechange = onReadyStateChange; + } + + // loading indicator in Firefox + // https://bugzilla.mozilla.org/show_bug.cgi?id=736723 + if (xhr.sendAsBinary == undefined) { + xhr.onprogress = onProgress; + } + + wasActivity = false; + timeout = setTimeout(onTimeout, heartbeatTimeout); + + charOffset = 0; + currentState = CONNECTING; + dataBuffer.length = 0; + eventTypeBuffer = ""; + lastEventIdBuffer = lastEventId; + value = ""; + field = ""; + state = FIELD_START; + + var s = url.slice(0, 5); + if (s !== "data:" && s !== "blob:") { + s = url + ((url.indexOf("?", 0) === -1 ? "?" : "&") + "lastEventId=" + encodeURIComponent(lastEventId) + "&r=" + (Math.random() + 1).toString().slice(2)); + } else { + s = url; + } + xhr.open("GET", s, true); + + if (isXHR) { + // withCredentials should be set after "open" for Safari and Chrome (< 19 ?) + xhr.withCredentials = withCredentials; + + xhr.responseType = "text"; + + // Request header field Cache-Control is not allowed by Access-Control-Allow-Headers. + // "Cache-control: no-cache" are not honored in Chrome and Firefox + // https://bugzilla.mozilla.org/show_bug.cgi?id=428916 + //xhr.setRequestHeader("Cache-Control", "no-cache"); + xhr.setRequestHeader("Accept", "text/event-stream"); + // Request header field Last-Event-ID is not allowed by Access-Control-Allow-Headers. + //xhr.setRequestHeader("Last-Event-ID", lastEventId); + } + + xhr.send(undefined); + }; + + EventTarget.call(this); + this.close = close; + this.url = url; + this.readyState = CONNECTING; + this.withCredentials = withCredentials; + + this.onopen = undefined; + this.onmessage = undefined; + this.onerror = undefined; + + onTimeout(); + } + + function F() { + this.CONNECTING = CONNECTING; + this.OPEN = OPEN; + this.CLOSED = CLOSED; + } + F.prototype = EventTarget.prototype; + + EventSource.prototype = new F(); + F.call(EventSource); + if (isCORSSupported) { + EventSource.prototype.withCredentials = undefined; + } + + var isEventSourceSupported = function () { + // Opera 12 fails this test, but this is fine. + return global.EventSource != undefined && ("withCredentials" in global.EventSource.prototype); + }; + + if (Transport != undefined && (global.EventSource == undefined || (isCORSSupported && !isEventSourceSupported()))) { + // Why replace a native EventSource ? + // https://bugzilla.mozilla.org/show_bug.cgi?id=444328 + // https://bugzilla.mozilla.org/show_bug.cgi?id=831392 + // https://code.google.com/p/chromium/issues/detail?id=260144 + // https://code.google.com/p/chromium/issues/detail?id=225654 + // ... + global.NativeEventSource = global.EventSource; + global.EventSource = EventSource; + } + +}(this)); \ No newline at end of file diff --git a/samples/sse-real-time-web/src/main/webapp/sse_broadcast.html b/samples/sse-real-time-web/src/main/webapp/sse_broadcast.html index e810a1b..981335a 100644 --- a/samples/sse-real-time-web/src/main/webapp/sse_broadcast.html +++ b/samples/sse-real-time-web/src/main/webapp/sse_broadcast.html @@ -8,8 +8,7 @@

Jersey RESTful Web Application!

Index -

SSE Chat

- +

SSE 聊天室:

diff --git a/samples/sse-real-time-web/src/main/webapp/sse_cors.html b/samples/sse-real-time-web/src/main/webapp/sse_cors.html new file mode 100644 index 0000000..254ce5c --- /dev/null +++ b/samples/sse-real-time-web/src/main/webapp/sse_cors.html @@ -0,0 +1,21 @@ + + + + + +SSE 的 CORS| www.waylau.com + + +

SSE 的 CORS

+ +

+ Index +

Get time from server

+
Initializing...
+

+ Visit www.waylau.com for more information + on Jersey! + + + + diff --git a/samples/sse-real-time-web/src/main/webapp/sse_ie.html b/samples/sse-real-time-web/src/main/webapp/sse_ie.html new file mode 100644 index 0000000..87a5209 --- /dev/null +++ b/samples/sse-real-time-web/src/main/webapp/sse_ie.html @@ -0,0 +1,21 @@ + + + + + +SSE 发布-订阅模式,支持IE| www.waylau.com + + +

SSE 发布-订阅模式,支持IE

+ +

+ Index +

Get time from server

+
Initializing...
+

+ Visit www.waylau.com for more information + on Jersey! + + + +