基于webSocket通信的库主要有 socket.io,SockJS,这次用的是 SockJS。
这里我们使用sockjs-client、stomjs这两个模块,要实现webSocket通信,需要后台配合,也使用相应的模块。
WebSocket
1、http:http超文本传输协议,http有1.0、1.1、 2.0几个版本,从http1.1起,默认都开启了Keep-Alive,保持连接持续性,简单地说,当一个网页打开完成后,客户端和服务器之间用于传输http数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接,这样就降低了资源的消耗优化性能,但是Keep-Alive也是有时间限制的,还有一个客户端只能主动发起请求才能获取返回数据,并不能主动接收后台推送的数据,websocket便应运而生。
2、websocket:是 html5 新增加特性之一,目的是浏览器与服务端建立全双工的通信方式,解决 http 请求-响应带来过多的资源消耗,同时对特殊场景应用提供了全新的实现方式,比如聊天、股票交易、游戏等对对实时性要求较高的行业领域。
http与websocket都是基于TCP(传输控制协议)的,websocket可以看做是对http协议的一个补充。
WebSocket协议提供了通过一个套接字实现全双工通信的功能。除了其他的功能之外,它能够实现Web浏览器和服务器之间的异步通信。全双工意味着服务器可以发送消息给浏览器,浏览器也可以发送消息给服务器。
3、SockJs:SockJS是一个JavaScript库,为了应对许多浏览器不支持WebSocket协议的问题,设计了备选SockJs。SockJS 是 WebSocket 技术的一种模拟。SockJS会尽可能对应 WebSocket API,但如果WebSocket 技术不可用的话,会自动降为轮询的方式。
SockJS会优先选择WebSocket进行连接,但是当服务器或客户端不支持WebSocket时,会自动在 XHR流、XDR流、iFrame事件源、iFrame HTML文件、XHR轮询、XDR轮询、iFrame XHR轮询、JSONP轮询 这几个方案中择优进行连接。
4、Stompjs:STOMP—— Simple Text Oriented Message Protocol——面向消息的简单文本协议。SockJS 为 WebSocket 提供了 备选方案。但无论哪种场景,对于实际应用来说,这种通信形式层级过低。
STOMP协议,来为浏览器 和 server 间的 通信增加适当的消息语义。
5、WebSocket、SockJs、STOMP三者关系
简而言之,WebSocket 是底层协议,SockJS 是WebSocket 的备选方案,也是底层协议,而 STOMP 是基于 WebSocket(SockJS)的上层协议。
(1)HTTP协议解决了 web 浏览器发起请求以及 web 服务器响应请求的细节,假设 HTTP 协议 并不存在,只能使用 TCP 套接字来 编写 web 应用,那将是一件非常痛苦的事情。
(2)直接使用 WebSocket(SockJS) 就很类似于 使用 TCP 套接字来编写 web 应用,因为没有高层协议,就需要我们定义应用间所发送消息的语义,还需要确保连接的两端都能遵循这些语义;
(3)同HTTP在TCP 套接字上添加请求-响应模型层一样,STOMP在WebSocket 之上提供了一个基于帧的线路格式层,用来定义消息语义;
SockJS
WebSocket是一个相对比较新的规范,在Web浏览器和应用服务器上没有得到一致的支持。所以我们需要一种WebSocket的备选方案,而这恰恰是SockJS所擅长的。
SockJS是WebSocket技术的一种模拟,在表面上,它尽可能对应WebSocket API,但是在底层非常智能。如果WebSocket技术不可用的话,就会选择另外的通信方式。
使用SockJS
只需加上withSockJS()方法就能声明我们想要使用SockJS功能,如果WebSocket不可用的话,SockJS的备用方案就会发挥作用。
同时需要注意的是,如果我们不是使用 SockJS,那么后端代码就只需要把这个 withSockJS() 去掉即可,那么就会采用 websocket 来连接。
sockjs-client
sockjs-client是从SockJS中分离出来的用于客户端使用的通信模块,所以我们就直接来看看SockJS。
SockJS是一个浏览器的JavaScript库,它提供了一个类似于网络的对象,SockJS提供了一个连贯的、跨浏览器的JavaScriptAPI,它在浏览器和Web服务器之间创建了一个低延迟、全双工、跨域通信通道。
你可能会问,我为什么不直接用原生的WebSocket而要使用SockJS呢?
这得益于SockJS的一大特性,一些浏览器中缺少对WebSocket的支持,因此回退选项是必要的,而Spring框架提供了基于SockJS协议的透明的回退选项。SockJS提供了浏览器兼容性,优先使用原生的WebSocket,如果某个浏览器不支持WebSocket,SockJS会自动降级为轮询。
stomjs
STOMP(Simple Text-Orientated Messaging Protocol) 面向消息的简单文本协议,WebSocket是一个消息架构,不强制使用任何特定的消息协议,它依赖于应用层解释消息的含义。
与HTTP不同,WebSocket是处在TCP上非常薄的一层,会将字节流转化为文本/二进制消息,因此,对于实际应用来说,WebSocket的通信形式层级过低,因此可以在 WebSocket 之上使用STOMP协议,来为浏览器 和 server间的通信增加适当的消息语义。
STOMP与WebSocket 的关系
HTTP协议解决了web浏览器发起请求以及web服务器响应请求的细节,假设HTTP协议不存在,只能使用TCP套接字来编写web应用,你可能认为这是一件疯狂的事情
直接使用WebSocket(SockJS)就很类似于使用TCP套接字来编写web应用,因为没有高层协议,就需要我们定义应用间发送消息的语义,还需要确保连接的两端都能遵循这些语义。
同HTTP在TCP套接字上添加请求-响应模型层一样,STOMP在WebSocket之上提供了一个基于帧的线路格式层,用来定义消息语义。
前端代码实现:
先安装 sockjs-client 和 stompjs
npm install sockjs-client
npm install stompjs
<script>
import SockJS from 'sockjs-client'
import Stomp from 'stompjs'
export default {
......
methods: {
......
// 连接后台
connection () {
let that = this
// 建立连接对象
let sockUrl = '/api/***?token=' + this.token.substring(7) + '&abc=' + this.$route.params.id
let socket = new SockJS(sockUrl)
// 获取STOMP子协议的客户端对象
this.stompClient = Stomp.over(socket)
// 定义客户端的认证信息,按需求配置
let headers = {
Authorization: ''
}
// 向服务器发起websocket连接
this.stompClient.connect(headers, (res) => {
// 订阅服务端提供的某个topic
this.stompClient.subscribe('/topic/***/' + this.$route.params.id, (frame) => {
that.addBarage(JSON.parse(frame.body))
})
that.sentFirst()
}, (err) => {
console.log('失败:' + err)
})
this.stompClient.debug = null
},
// 断开连接
disconnect () {
if (this.stompClient) {
this.stompClient.disconnect()
}
},
// 初始化websocket
initWebSocket () {
this.connection()
let that = this
// 断开重连机制,尝试发送消息,捕获异常发生时重连
this.timer = setInterval(() => {
try {
that.stompClient.send('connect-test')
} catch (err) {
console.log('断线了: ' + err)
that.connection()
}
}, 5000)
},
// 用户加入发送弹幕
keyDown (e) {
if (e.keyCode === 13) {
if (e.preventDefault) {
e.preventDefault()
} else {
window.event.returnValue = false
}
this.sentBarrage()
}
}
},
mounted () {this.initWebSocket()
},
beforeDestroy () {
if (this.player) {
this.player.dispose()
this.player = null
}
// 页面离开时断开连接,清除定时器
this.disconnect()
clearInterval(this.timer)
}
}
</script>
JavaScript客户端代码
要在客户端使用SockJS,需要确保加载了SockJS客户端库。
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
除了加载SockJS客户端库外,要使用SockJS只需要修改两行代码即可:
var url = 'marco';
var sock = new SockJS(url);
//SockJS所处理的URL是http://或https://,不再是ws://和wss://
运行效果一样,但是客户端–服务器之间通信的方式却有了很大的变化。
使用STOMP消息
STOMP在WebSocket之上提供了一个基于帧的线路格式层,用来定义消息的语义。
STOMP帧由命令、一个或多个头信息以及负载所组成。
STOMP命令是SEND,表明会发送一些内容。紧接着是两个头信息:一个用来表示消息要发送到哪里的目的地,另外一个则包含了负载的大小。然后,紧接着是一个空行,STOMP帧的最后是负载内容。
STOMP帧中最有意思的是destination头信息了。它表明STOMP是一个消息协议。消息会发布到某个目的地,这个目的地实际上可能真的有消息代理作为支撑。另一方面,消息处理器也可以监听这些目的地,接收所发送过来的消息。
启用STOMP消息功能
WebSocketStompConfig 重载了registerStompEndpoints() 方法,将/marcopolo注册为STOMP端点。
这个路径与之前接收和发送消息的目的地路径有所不同。这是一个端点,客户端在订阅或发布消息到目的地前,要连接该端点。
WebSocketStompConfig还通过重载configureMessageBroker() 方法配置了一个简单的消息代理。这个方法是可选的,如果不重载它的话,将会自动配置一个简单的内存消息代理,用它来处理以“/topic”为前缀的消息。
处理来自客户端的STOMP消息
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.js"></script>
<script>
var url = 'http://'+window.location.host+'/yds/marcopolo';
var sock = new SockJS(url); //创建SockJS连接。
var stomp = Stomp.over(sock);//创建STOMP客户端实例。实际上封装了SockJS,这样就能在WebSocket连接上发送STOMP消息。
var payload = JSON.stringify({'message':'Marco!'});
stomp.connect('guest','guest',function(frame){
stomp.send("/app/marco",{},payload);
stomp.subscribe('/app/subscribe', function(message){
});
});
</script>
发送消息到客户端
如果你想要在接收消息的时候,同时在响应中发送一条消息,那么需要做的仅仅是将内容返回就可以了。
stomp.subscribe('/topic/marco', function(message){ // 订阅后将会接收到消息。
});
为目标用户发送消息
为指定用户发送消息
stomp.subscribe('/users/1/message', function(message){
});
客户端接收一对一消息的主题是"/users/“+usersId+”/message",这里的用户Id可以是一个普通字符串,只要每个客户端都使用自己的Id并且服务器端知道每个用户的Id就行了。
具体前端如何实现
创建STOMP客户端
在web浏览器中使用普通的Web Socket
STOMP javascript 客户端会使用 ws://的URL与STOMP 服务端进行交互。
为了创建一个STOMP客户端 js 对象,你需要使用 Stomp.client(url),而这个URL连接着服务端的WebSocket的代理
var url = "ws://localhost:61614/stomp";
var client = Stomp.client(url);
Stomp.client(url, protocols)也可以用来覆盖默认的subprotocols。第二个参数可以是一个字符串或一个字符串数组去指定多个subprotocols。
在web浏览器中使用定制的WebSocket
浏览器提供了不同的WebSocket的协议,一些老的浏览器不支持WebSocket的脚本或者使用别的名字。默认下,stomp.js会使用浏览器原生的WebSocket class去创建WebSocket。
但是利用 Stomp.over(ws)这个方法可以使用其他类型的 WebSockets,这个方法得到一个满足WebSocket定义的对象。例如,可以使用由 SockJS实现的Websocket。
如果使用原生的 Websocket 就使用 Stomp.client(url),如果需要使用其他类型的 Websocket(例如由SockJS包装的Websocket)就使用Stomp.over(ws)。除了初始化有差别,Stomp API 在这两种方式下是相同的。
在 node.js程序中
通过stompjs npm package同样也可以在node.js程序中使用这个库。
// 安装
npm install stompjs
在node.js中,require这个模块:var Stomp = require(‘stompjs’);
为了与建立在TCP socket的STOMP-broker连接,使用Stomp.overTCP(host, port)方法。
var client = Stomp.overTCP('localhost', 61613);
为了与建立在Web Socket的STOMP broker连接,使用Stomp.overWS(url)方法。
var client = Stomp.overWS('ws://localhost:61614/stomp');
除了初始化不同,无论是浏览器还是node.js环境下,Stomp API都是相同的。
连接服务端
一旦 Stomp 客户端建立了,必须调用它的 connect()方法去连接 Stomp 服务端进行验证。这个方法需要两个参数,用户的登录和密码凭证。这种情况下,客户端会使用Websocket打开连接,并发送一个 CONNECT frame。
这个连接是异步进行的:你不能保证当这个方法返回时是有效连接的。为了知道连接的结果,你需要一个回调函数。
var connect_callback = function() {
// called back after the client is connected and authenticated to the STOMP server
};
但是如果连接失败会发生什么呢?
connect()方法接受一个可选的参数(error_callback),当客户端不能连接上服务端时,这个回调函数error_callback会被调用,该函数的参数为对应的错误对象。
var error_callback = function(error) {
// display the error's message header:
alert(error.headers.message);
};
在大多数情况下,connect()方法可接受不同数量的参数来提供简单的API:
client.connect(login, passcode, connectCallback);
client.connect(login, passcode, connectCallback, errorCallback);
client.connect(login, passcode, connectCallback, errorCallback, host);
login和passcode是strings,connectCallback和errorCallback则是functions。(有些brokers(代理)还需要传递一个host(String类型)参数。)
如果你需要附加一个headers头部,connect方法还接受其他两种形式的参数:
client.connect(headers, connectCallback);
client.connect(headers, connectCallback, errorCallback);
header是map形式,connectCallback和errorCallback为functions。
需要注意:如果你使用上述这种方式,你需要自行在headers添加login、passcode(甚至host):
var headers = {
login: 'mylogin',
passcode: 'mypasscode',
// additional header
'client-id': 'my-client-id'
};
client.connect(headers, connectCallback);
断开连接时,调用disconnect方法,这个方法也是异步的,当断开成功后会接收一个额外的回调函数的参数。如下所示。
client.disconnect(function() {
alert("See you next time!");
};
当客户端与服务端断开连接,就不会再发送或接收消息了。
Heart-beating
如果STOMP broker(代理)接收STOMP 1.1版本的帧,heart-beating是默认启用的。
heart-beating也就是频率,incoming是接收频率,outgoing是发送频率。通过改变incoming和outgoing可以更改客户端的heart-beating(默认为10000ms):
client.heartbeat.outgoing = 20000;
// client will send heartbeats every 20000ms
client.heartbeat.incoming = 0;
// client does not want to receive heartbeats
// from the server
heart-beating是利用window.setInterval()去规律地发送heart-beats或者检查服务端的heart-beats。
发送消息
当客户端与服务端连接成功后,可以调用 send()来发送STOMP消息。这个方法必须有一个参数,用来描述对应的STOMP的目的地。另外可以有两个可选的参数:headers,object类型包含额外的信息头部;body,一个String类型的参数。
client.send("/queue/test", {priority: 9}, "Hello, STOMP");
// client会发送一个STOMP发送帧给/queue/test,
这个帧包含一个设置了priority为9的header和内容为“Hello, STOMP”的body。
client.send(destination, {}, body);
如果你想发送一个有body的信息,也必须传递headers参数。如果没有headers需要传递,那么就传{}即可。
订阅(Subscribe)和接收(receive)消息
为了在浏览器中接收消息,STOMP客户端必须先订阅一个目的地 destination。
你可以使用subscribe()去订阅。这个方法有2个必需的参数:目的地(destination),回调函数(callback);还有一个可选的参数headers。其中destination是String类型,对应目的地,回调函数是伴随着一个参数的function类型。
var subscription = client.subscribe("/queue/test", callback);
subscribe()方法返回一个object,这个object包含一个id属性,对应这个这个客户端的订阅ID。
而unsubscribe()可以用来取消客户端对这个目的地destination的订阅。
默认情况下,如果没有在headers额外添加,这个库会默认构建一个独一无二的ID。在传递headers这个参数时,可以使用你自己的ID。
var mysubid = '...';
var subscription = client.subscribe(destination, callback, { id: mysubid });
这个客户端会向服务端发送一个STOMP订阅帧(SUBSCRIBE frame)并注册回调事件。每次服务端向客户端发送消息时,客户端都会轮流调用回调函数,参数为对应消息的STOMP帧对象(Frame object)。
subscribe()方法,接受一个可选的headers参数用来标识附加的头部。
var headers = {ack: 'client', 'selector': "location = 'Europe'"};
client.subscribe("/queue/test", message_callback, headers);
这个客户端指定了它会确认接收的信息,只接收符合这个selector : location = 'Europe’的消息。
如果想让客户端订阅多个目的地,你可以在接收所有信息的时候调用相同的回调函数:
onmessage = function(message) {
// called every time the client receives a message
}
var sub1 = client.subscribe("queue/test", onmessage);
var sub2 = client.subscribe("queue/another", onmessage)
如果要中止接收消息,客户端可以在subscribe()返回的object对象调用unsubscribe()来结束接收。
var subscription = client.subscribe(...);
...
subscription.unsubscribe();
支持JSON
STOMP消息的body必须为字符串。如果你需要发送/接收JSON对象,你可以使用JSON.stringify()和JSON.parse()去转换JSON对象。
Acknowledgment(确认)
默认情况,在消息发送给客户端之前,服务端会自动确认(acknowledged)。
客户端可以选择通过订阅一个目的地时设置一个ack header为client或client-individual来处理消息确认。
在下面这个例子,客户端必须调用message.ack()来通知服务端它已经接收了消息。
var subscription = client.subscribe("/queue/test",
function(message) {
// do something with the message
...
// and acknowledge it
message.ack();
},
{ack: 'client'}
);
ack()接受headers参数用来附加确认消息。例如,将消息作为事务(transaction)的一部分,当要求接收消息时其实代理(broker)已经将ACK STOMP frame处理了。
var tx = client.begin();
message.ack({ transaction: tx.id, receipt: 'my-receipt' });
tx.commit();
nack()也可以用来通知STOMP 1.1.brokers(代理):客户端不能消费这个消息。与ack()方法的参数相同。
事务(Transactions)
可以在将消息的发送和确认接收放在一个事务中。
客户端调用自身的begin()方法就可以开始启动事务了,begin()有一个可选的参数transaction,一个唯一的可标识事务的字符串。如果没有传递这个参数,那么库会自动构建一个。这个方法会返回一个object。这个对象有一个id属性对应这个事务的ID,还有两个方法:
- commit()提交事务
- abort()中止事务
在一个事务中,客户端可以在发送/接受消息时指定transaction id来设置transaction。
// start the transaction
var tx = client.begin();
// send the message in a transaction
client.send("/queue/test", {transaction: tx.id}, "message in a transaction");
// commit the transaction to effectively send the message
tx.commit();
如果你在调用send()方法发送消息的时候忘记添加transction header,那么这不会称为事务的一部分,这个消息会直接发送,不会等到事务完成后才发送。
var txid = "unique_transaction_identifier";
// start the transaction
var tx = client.begin();
// oops! send the message outside the transaction
client.send("/queue/test", {}, "I thought I was in a transaction!");
tx.abort(); // Too late! the message has been sent
调试
有一些测试代码能有助于你知道库发送或接收的是什么,从而来调试程序。
客户端可以将其debug属性设置为一个函数,传递一个字符串参数去观察库所有的debug语句。默认情况,debug消息会被记录在在浏览器的控制台。
client.debug = function(str) {
// append the debug log to a #debug div somewhere in the page using JQuery:
$("#debug").append(str + "\n");
};
使用情况
1、var error_callback = function(error) {};第一次连接失败和连接后断开连接都会调用这个函数
2、关闭控制台调试数据:设置client.debug = null 就可以,stompjs会去检测debug是否是函数,不是函数就不会调用输出。