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);