SockJS
SockJS是一個客戶端JavaScript庫和協議,SockJS提供類似WebSocket的接口,此接口允許與SockJS服務器建立連接,而不論瀏覽器或者網絡允許真實的WebSockets。SockJS通過支持瀏覽器與服務間的多樣傳輸來完成這樣工作。並在運行時根據瀏覽器和網絡的能邊選擇其中之一。所有這一切對你都是透明的,你只簡單看到類似WebSocket接口的工作方式。
爲了獲取更多關於SockJS的信息,請參考SockJS網站。
SockJS處理器
Vert.x提供一個創新的處理器叫SockJSHandler,便於在Vert.x-Web應用中使用SockJS。用SockJSHandler.create方法爲每個SockJS應用創建一個處理器。在創建實例時也可以指定配置選項。配置選項由SockJSHandlerOptions類來描述。
Router router = Router.router(vertx);
SockJSHandlerOptions options = newSockJSHandlerOptions().setHeartbeatInterval(2000);
SockJSHandler sockJSHandler = SockJSHandler.create(vertx,options);
router.route("/myapp/*").handler(sockJSHandler);
處理SockJS的Socket
在服務端,設置一個SockJS處理器,此處理器在每個客戶端連接時都會被調用。調用時SockJSSocket類會被傳入處理器。此類有着與socket類似的接口,與NetSocket或者WebSocket一樣,可以讀取和寫入。其也實現了ReadStream和WriteStream兩種流,所以可以將其泵接到其他讀/寫流。這是一個簡單的SocketJS處理器的例子,簡單地將讀到的數據寫回。
Router router = Router.router(vertx);
SockJSHandlerOptions options = newSockJSHandlerOptions().setHeartbeatInterval(2000);
SockJSHandler sockJSHandler = SockJSHandler.create(vertx,options);
sockJSHandler.socketHandler(sockJSSocket -> {
// Just echo thedata back
sockJSSocket.handler(sockJSSocket::write);
});
router.route("/myapp/*").handler(sockJSHandler);
客戶端
客戶端,使用SockJS客戶端JavaScript庫進行連接。可以從這裏下載http://cdn.sockjs.org/sockjs-0.3.4.js,最小化版本可以從這裏下載http://cdn.sockjs.org/sockjs-0.3.4.min.js。使用SockJS JavaScript客戶端的詳細信息可以參見SockJS官網https://github.com/sockjs/sockjs-client,總得來說,基本象下面代碼一樣使用:
var sock = new SockJS('http://mydomain.com/myapp');
sock.onopen = function() {
console.log('open');
};
sock.onmessage = function(e) {
console.log('message',e.data);
};
sock.onclose = function() {
console.log('close');
};
sock.send('test');
sock.close();
配置SockJS處理器
使用SockJSHandlerOptions可以將多個選項配置給處理器。
-
insertJSESSIONID
插入一個JSESSIONIDR的cokkie,確保負載均衡器將指定SockJS會話的請求總是路由到正確的會話服務器。默認值爲true.
-
sessionTimeout
在客戶端收到請求,服務器很長時間沒有得到反饋時,服務 器發送一個關閉(close)事件。此處的延遲由此設置。在一個接收的連接在5秒種沒有反映,默認的關閉事件將會被髮送。
-
heartbeatInterval
爲了避免代理和負載均衡器關閉長時間運行的HTTP請求,需要保證連接是活的並且過一段時間發送心跳包。此設置用於設定發送心跳包的間隔。默認每隔25秒發送一個心跳包。
-
maxBytesStreaming
大 多數流傳輸將響應保持在客戶端並且不會釋放投送消息的內存。這樣的傳輸過一段時間需要進行垃圾回收。max_bytes_streaming設置了在流關閉時,單一http流請求所能傳遞的最小字節數。當到達此最小字節數時,客戶端需要打開一個新請求。爲了有效禁用流並且將流傳輸表現爲輪詢傳輸,請設置此值。默認值爲128kb。
-
libraryURL
對於不支持跨域通信的傳輸,使用天然的iframe技巧。一個簡單頁面由SockJS服務器提供(使用外部域)並且將頁面放置在不可見的iframe中。在此iframe中運行的代碼不用擔心跨域問題,因爲由本地或運行,連接到SockJS服務器。此iframe不需要加載SockJS客戶端庫,此選項讓你指定SockJS JavaScript庫的url(如果不確定,默認地使用最新的最小化的socketJS客戶端發行版)。默認值是http://cdn.sockjs.org/sockjs-0.3.4.min.js
-
disabledTransports
這是禁用傳輸的列表。可能的值是WEBSOCKET,EVENT_SOURCE,HTML_FILE,JSON_P,XHR.
SockJS 事件總線橋
隨同編譯進Vert.x-Web的SockJS的socket處理器叫事件總線橋,此處理器有效擴展服務端vert.x事件總線到客戶端JavaScript中。這就創建了分佈式事件總線,此事件總線不能跨越多個服務端Vert.x實例,但包括了運行在瀏覽器端的客戶端JavaScript。因此,可以創建大的分佈式總線壓縮一些瀏覽器與服務器,只要服務器可連接,瀏覽器不必連接到相同的服務器。這是通過提供一個客戶端JavaScript庫(vertx-eventbus.js)實現的,此JavaScript庫提供了與服務端Vert.x事件總線相似的API,使用此API,可以發送和發佈信息到事件總線並且可以註冊處理器接收消息。此JavaScript庫使用JavaScript SockJS客戶端在SockJS連接上建立傳輸通道,在某個服務器端的處理器(SockJSHandler)終此連接。
一個特殖的SockJS socket處理器會添加到SockJSHandler,此處理器處理SockJS數據,或者將數據橋接到/自服務器端事件總線。爲了激活格橋接,只要在SockJS處理器上簡單調用bridge方法。
Router router =Router.router(vertx);
SockJSHandler sockJSHandler =SockJSHandler.create(vertx);
BridgeOptions options = newBridgeOptions();
sockJSHandler.bridge(options);
router.route("/eventbus/*").handler(sockJSHandler);
在客戶端JavaScript中,需要使用 'vertx-eventbus.js`庫創建到事件總線的連接發送和接收消息。
<scriptsrc="http://cdn.sockjs.org/sockjs-0.3.4.min.js"></script>
<scriptsrc='vertx-eventbus.js'></script>
<script>
var eb = newEventBus('http://localhost:8080/eventbus');
eb.onopen = function() {
// set a handler to receive a message
eb.registerHandler('some-address', function(error, message) {
console.log('received a message: ' +JSON.stringify(message));
});
// send a message
eb.send('some-address', {name: 'tim', age: 587});
}
</script>
此例子做的第一件事是創建一個事件總線實例:
var eb = new EventBus('http://localhost:8080/eventbus');
構造函數的參數是需要連接到的URI。因爲我們創建帶eventbus前綴的橋將會連到那裏。在連接打開前,它實際還不能做任何事。打開連接時onopen處理器會被調用。
可以用依賴管理器獲取客戶端包。
Maven(pom.xml)
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
<version>3.2.1</version>
<classifier>client</classifier>
</dependency>
Gradle ( build.gradle):
compile 'io.vertx:vertx-web:3.2.1:client'
此庫也可以用NPM和Bower獲取。
注意,在3.0.0和3.1.0版本之間API已經改變了。請查看changelog。前一個版本的客戶端被兼容仍然能被使用,但是新和客戶端提供了更多特性並與vert.x事件總線API更接近。
保護SockJS橋
如果像上面例子一樣打開一個沒有保護的橋,並通過期試圖發送消息,你將發現消息神奇地消失了。這發生了什麼?
對於大多數應用,你或許不想客戶端JavaScript發送一些消息到任何服務器端處理器或者其他所有的瀏覽器。例如,你在事件總線上有一個服務,此服務可訪問或刪除數據。我們不想錯誤行爲或者惡意客戶端刪除所有數據庫所有數據。我們也不想任意客戶端偵聽任意事件總線地址。爲了處理上面問題,一個SockJS橋將默認拒絕傳輸任何消息。這取決於你告訴橋什麼消息可以通過傳輸。對於總是能夠通過的響應消息這是一個異常。換句話說,橋充當了防火牆角色,默認是拒絕所有(deny-all)策略。什麼消息可以在橋上傳輸,只需對橋進行簡單配置。
在調用橋時,可用傳入BridgeOptions對出站和入站允許的流量進行匹配。每個匹配是一個PermittedOptions對象:
setAddress
這代表了一個確切的消息發送到的地址。如果想讓消息基於此確切地址,可以使用此字段。
setAddressReges
這是一個匹配地址的正則表達式。如果讓想消息基於正則表達式地址,可以使用此字段,如果設置了address字段,將忽視此字段。
setMatch
此允許消息基於其消息結構。匹配的任何字段必須存在於消息中,同時值是允許的。此僅能用於JSON消息。
如果是傳入的消息(如,由客戶端javaScript發送到服務器)在服務器收到時,Vert.x-Web會查看所有入站許可匹配。如果匹配,將允許通過。
如是是出站肖息在被髮送到客戶端之前,Vert.x-Web會查看出站許可匹配,如果匹配,將允許通過。
實際匹配規則如下:
如果設置了address字段,消息地址必須與指定的address地址精確匹配才被認爲是匹配的。
如果不設置address字段而是設置addressRegex字段,消息地址的格式必須與address_re中的正則表達式匹配。
如果設置了match字段,消息結構必須匹配,結構匹配是檢查看消息是否有存在匹配對象的所有字段和值。
這是一個例子:
Router router =Router.router(vertx);
SockJSHandler sockJSHandler =SockJSHandler.create(vertx);
// Let through any messages sentto 'demo.orderMgr' from the client
PermittedOptionsinboundPermitted1 = new PermittedOptions().setAddress("demo.orderMgr");
// Allow calls to the address'demo.persistor' from the client as long as the messages
// have an action field withvalue 'find' and a collection field with value
// 'albums'
PermittedOptions inboundPermitted2= new PermittedOptions().setAddress("demo.persistor")
.setMatch(newJsonObject().put("action", "find")
.put("collection","albums"));
// Allow through any message witha field `wibble` with value `foo`.
PermittedOptionsinboundPermitted3 = new PermittedOptions().setMatch(newJsonObject().put("wibble", "foo"));
// First let's define what we'regoing to allow from server -> client
// Let through any messagescoming from address 'ticker.mystock'
PermittedOptionsoutboundPermitted1 = new PermittedOptions().setAddress("ticker.mystock");
// Let through any messages fromaddresses starting with "news." (e.g. news.europe, news.usa, etc)
PermittedOptionsoutboundPermitted2 = newPermittedOptions().setAddressRegex("news\\..+");
// Let's define what we're goingto allow from client -> server
BridgeOptions options = newBridgeOptions().
addInboundPermitted(inboundPermitted1).
addInboundPermitted(inboundPermitted1).
addInboundPermitted(inboundPermitted3).
addOutboundPermitted(outboundPermitted1).
addOutboundPermitted(outboundPermitted2);
sockJSHandler.bridge(options);
router.route("/eventbus/*").handler(sockJSHandler);
消息必須的認證信息
事件總線橋可用Vert.x-Web認證功能配置成要求消息必須認證,不論是橋的入站還是出站。
爲了完成此工作,如前節所描述的,需要添加額外的match字段,此字段段決定針對匹配的消息必須提供什麼權利。用setRequiredAuthority設置登錄用戶需要特定訪問消息的權利。
這是一個例子:
PermittedOptions inboundPermitted= new PermittedOptions().setAddress("demo.orderService");
// But only if the user is loggedin and has the authority "place_orders"
inboundPermitted.setRequiredAuthority("place_orders");
BridgeOptions options = newBridgeOptions().addInboundPermitted(inboundPermitted);
對於被授權的用戶,首先需要登錄,然後需要特定權利。爲了處理登錄和確切授權,可以爲Vert.x配置正常的認證處理器(auth handler)。例如:
Router router =Router.router(vertx);
// Let through any messages sentto 'demo.orderService' from the client
PermittedOptions inboundPermitted= new PermittedOptions().setAddress("demo.orderService");
// But only if the user is loggedin and has the authority "place_orders"
inboundPermitted.setRequiredAuthority("place_orders");
SockJSHandler sockJSHandler =SockJSHandler.create(vertx);
sockJSHandler.bridge(newBridgeOptions().
addInboundPermitted(inboundPermitted));
// Now set up some basic authhandling:
router.route().handler(CookieHandler.create());
router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx)));
AuthHandler basicAuthHandler =BasicAuthHandler.create(authProvider);
router.route("/eventbus/*").handler(basicAuthHandler);
router.route("/eventbus/*").handler(sockJSHandler);
處理事件總線橋事件
如果在橋的事件出現後想得到事件通知,可以在調用bridge方法時提供一個處理器。不論什麼時間橋事件出下,事件將會傳給處理器進行處理,用BridgeEvet實例對橋事件進行描述。事件可以是下面任一類型:
SOCKET_CREATED
在創建SockJS socket時,產生此事件
SOCKET_CLOSED
在 SockJS socket關閉時,產生此事件
SEND
在一個消息試圖被從客戶端發送到服務器時,產生此事件。
PUBLISH
在一個消息被圖被從客戶端發佈到服務器時,產生此事件。
RECEIVE
在消息從服務器傳送到客戶端時,產生RECEIVE事件。
REGISTER
在一個客戶端試圖註冊一個處理器時,產生此事件。
UNREGISTER
在一個客戶端試圖解註冊一個處理器時,產生此事件。
用事件的type方法獲取事件類型,並且用getRawMessage方法查看原生事件消息。
原事消息是一個JSON對象,俱有以下結構:
{
"type":"send"|"publish"|"receive"|"register"|"unregister",
"address": the event bus address beingsent/published/registered/unregistered
"body": the body of the message
}
事件也是一個Future對象。在完成事件處理,可以用true來結束Future,以便進一步進行處理。如果不想讓事件被處理,可以用false來結束Future。這對於過濾橋上傳輸的消息或者進行細粒度的授權和度量很有用。這是一個例子,在這個例子中,我們拒絕了所有包售“Armadillos”單詞的所有消息。
Router router =Router.router(vertx);
// Let through any messages sentto 'demo.orderMgr' from the client
PermittedOptions inboundPermitted= new PermittedOptions().setAddress("demo.someService");
SockJSHandler sockJSHandler =SockJSHandler.create(vertx);
BridgeOptions options = newBridgeOptions().addInboundPermitted(inboundPermitted);
sockJSHandler.bridge(options, be-> {
if (be.type() == BridgeEventType.PUBLISH || be.type() ==BridgeEventType.RECEIVE) {
if(be.getRawMessage().getString("body").equals("armadillos")){
// Reject it
be.complete(false);
return;
}
}
be.complete(true);
});
router.route("/eventbus").handler(sockJSHandler);
也可以修改原始信息,例如改變消息體。對於從客戶端流入的消息,也可以向消息添加頭,這是一個例子:
Router router =Router.router(vertx);
// Let through any messages sentto 'demo.orderService' from the client
PermittedOptions inboundPermitted= new PermittedOptions().setAddress("demo.orderService");
SockJSHandler sockJSHandler =SockJSHandler.create(vertx);
BridgeOptions options = newBridgeOptions().addInboundPermitted(inboundPermitted);
sockJSHandler.bridge(options, be-> {
if (be.type() == BridgeEventType.PUBLISH || be.type() ==BridgeEventType.SEND) {
// Add some headers
JsonObject headers = newJsonObject().put("header1", "val").put("header2","val2");
JsonObject rawMessage = be.getRawMessage();
rawMessage.put("headers",headers);
be.setRawMessage(rawMessage);
}
be.complete(true);
});
router.route("/eventbus").handler(sockJSHandler);