长轮询与短轮询
短轮询
其实就是普通的轮询,在特定的时间间隔内,由浏览器向服务器发出HTTP请求,然后服务器返回最新的数据给客户端。
var xhr = new XMLHttpRequest();
setInterval(function() {
xhr.open('GET','/user');
xhr.onreadystatechange = function() {
// your code ...
};
xhr.send();
}, 1000);
缺点: 请求中有大半是无用的,浪费带宽和服务器资源;因为是异步请求,响应的结果没有顺序。
实例: 适用于小型应用。
长轮询
客户端向服务器发送HTTP请求,但服务端不立即返回响应,而是hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后才能向服务器发送新的请求。
function ajax(){
var xhr = new XMLHttpRequest();
xhr.open('GET','/user');
xhr.onreadystatechange = function(){
ajax();
};
xhr.send();
}
优点:在无消息的情况下不会频繁的请求,耗费资源小
缺点:服务器hold连接会消耗资源
实例:WebQQ、Hi网页版、Facebook IM
长连接与短连接
HTTP协议是基于请求/响应模式的,因此只要服务端给了响应,本次HTTP连接(请求)就结束了。也就是说,HTTP本身根本没有长连接与短连接这一说。
所谓的长连接与短连接,其实指的是TCP连接,TCP是一个双向通道,可以保持一段时间不关闭,因此TCP连接才有真正的长连接和短连接这一说。
只要客户端和服务端的头部都设置了connection: keep-alive
,就是启用了长连接,为了通道的复用。HTTP/1.1默认启用长连接。
注意: 长连接是为了通道复用,并不意味着永久连接,在一段时间内没有HTTP请求的话,这个连接就会被关闭。
WebSocket
WebSocket
是HTML5
开始提供的一种在单个TCP
连接上进行全双工通讯的协议。WebSocket
使客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API
中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。
-
通常,应用层协议都是完全基于网络层协议
TCP/UDP
来实现,例如HTTP、SMTP、POP3
,而Websocket
是同时基于HTTP
与TCP
来实现。- 先用带有
Upgrade:Websocket
请求头的特殊HTTP request
来实现与服务端握手HandShake
; - 握手成功后,协议升级成
Websocket
,进行长连接通讯; - 整个过程可理解为:小锤抠缝,大锤搞定。
- 先用带有
-
为什么不使用
HTTP
长连接来实现即时通讯?事实上,在Websocket
之前就是使用HTTP
长连接这种方式,如Comet
。但是它有如下弊端:-
HTTP 1.1
规范中规定,客户端不应该与服务器端建立超过两个的HTTP
连接, 新的连接会被阻塞; - 对于服务端来说,每个长连接都占有一个用户线程,在
NIO
或者异步编程之前,服务端开销太大; -
HTTP
不能完成服务端推送,新出的HTTP/2
只能推送静态资源,无法推送即时消息。 -
HTTP/2
所谓的server push
其实是当服务器接收一个请求时,可以响应多个资源。
-
-
为什么不直接使用
Socket
编程,基于TCP
直接保持长连接,实现即时通讯?-
Socket
编程针对C/S
模式的,而浏览器是B/S
模式,浏览器无法发起Socket
请求。正因如此,W3C
最后还是给出了浏览器的Socket -- Websocket
。
-
实际上,
HTTP
协议也是建立在TCP
协议之上的,TCP
协议本身就是全双工通信,但HTTP
协议的请求-
应答机制限制了全双工通信。WebSocket
其实也只是简单规定了一下:接下来咱们就不使用HTTP
协议了,直接互相发数据吧。
HTTP
和webSocket
其实是个交集,他们都是建立在TCP
链接之上的。
使用方式
对于浏览器端,WebSocket API
的使用非常简单。
// 首先new一个websocket对象,发起建立连接的请求
var ws = new WebSocket("wss://webchat-bj-test5.clink.cn");
// 连接成功后的回调函数
ws.onopen = function(evt) {
console.log("Connection open ...");
// 向服务器发送数据
ws.send("Hello WebSockets!");
};
// 接收服务器数据后的回调函数
ws.onmessage = function(evt) {
console.log( "Received: ", evt.data);
// ws.close(); // 主动关闭websocket连接
};
// 服务器连接关闭后的回调函数
ws.onclose = function(evt) {
console.log("Connection closed.");
};
- 首先,
WebSocket
连接必须由浏览器发起,因为请求协议是一个标准的HTTP
请求,格式如下:GET wss://webchat-bj-test5.clink.cn&province= HTTP/1.1 //请求地址 Connection: Upgrade Upgrade: websocket Sec-WebSocket-Version: 13 Sec-WebSocket-Key: O5GLCYKZVQi2jTLENobvtg== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits // ...
-
GET
请求的地址使用ws/wss
协议,也是webSocket
使用的协议; -
Upgrade:websocket
和Connection:Upgrade
表示将这个连接升级为websocket
连接; -
Sec-WebSocket-Key
是一个Base64 encode
的值,由浏览器随机生成,用于标识这个连接,与服务器做身份验证; -
Sec-WebSocket-Version
告诉服务器所使用的Websocke
协议版本。
-
- 随后,如果服务器接受该请求,则返回响应:
HTTP/1.1 101 // 状态码101 Server: nginx/1.13.9 // 服务器 Connection: upgrade Upgrade: websocket Sec-WebSocket-Accept: uZpmP+PDDvSeKsEg9vkAsWcqPzE= Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15 // ...
- 响应码
101
表示本次连接的HTTP
协议将被更改,更改后的协议就是Upgrade:websocket
; -
Sec-WebSocket-Accept
经过服务器确认、并且加密过后的Sec-WebSocket-Key
; -
Sec-WebSocket-Extensions
WebSocket
的扩展; -
Sec-WebSocket-Protocol
数据交换协议,列出的客户端请求的子协议。如果指定了这个字段,服务器需要包含相同的字段,并且按照优先顺序从子协议中选择一个值作为建立连接的响应。
- 响应码
如此之后,我们建立了一个websocket
链接。这个过程通常称为握手。浏览器和服务器随时可以主动发送消息给对方。消息有两种:文本 和 二进制数据
-
WebSocket
是为了在web
应用上进行全双工通信而产生的协议,相比于轮询HTTP
请求的方式,WebSocket
有节省服务器资源,效率高等优点; -
WebSocket
中的掩码是为了防止早期版本中存在中间缓存污染攻击等问题而设置的,客户端向服务端发送数据需要掩码,而服务端向客户端发送数据不需要掩码; -
WebSocket
中Sec-WebSocket-Key
的生成算法是拼接服务端和客户端生成的字符串,进行SHA1
哈希算法,再用base64
编码; -
WebSocket
协议握手是依靠HTTP
协议的,依靠于HTTP
响应101
进行协议升级转换。
SocketIO
SocketIO
将WebSocket、AJAX
和其它的通信方式全部封装成了统一的通信接口。也就是说,我们在使用SocketIO
时,不用担心兼容问题,底层会自动选用最佳的通信方式。所以说WebSocket
是SocketIO
的一个子集。
Websocket API
并不是所有浏览器都完美支持,而当浏览器不支持Websocket
时,应该自动切换成Ajax
长轮询,SSE
等备用解决方案。所以在实际开发中我们通常采用封装了Websocket
及其备用方案的库----SockJS(Java) / Socket.IO(Node)
。
- 如果使用
Java
做服务端,同时又恰好使用Spring
作为框架,那么推荐使用SockJS
,因为Spring
本身就是SockJS
推荐的Java Server
实现,同时也提供了Java
的Client
实现。 - 如果使用·Node.js·做服务端,那么毫无疑问选择
Socket.IO
,它本省就是从Node.js
开始的,当然服务端也提供了engine.io-server-java
实现。甚至可以使用netty-socketio
。
注意:不管你使用哪一种,都必须保证客户端与服务端同时支持。
# node端
// 引入koa
var app = require('koa')();
//创建http服务
var server = require('http').createServer(app.callback());
//给http封装成io对象
var io = require('socket.io')(server);
// 建立链接
io.on('connection', function(socket){
// io.emit代表广播,socket.emit代表私发
socket.on('eventB', function(socket){ /* */ });
socket.emit('eventA', /* */);
});
server.listen(3000);
# 前端
<script src="./lib/socket.io.js"></script>
<script>
//创建个服务
var socket = new io()
// 用 on 监听
socket.on('eventA', function (res) {
console.log('⽤户1接收到信息了了')
})
socket.emit('eventB', data)
</script>
websocket的特点
- 建立在
TCP
协议之上,服务器端的实现比较容易; - 与
HTTP
协议有着良好的兼容性。默认端口也是80
和443
,并且握手阶段采用HTTP
协议,因此握手时不容易屏蔽,能通过各种HTTP
代理服务器; - 数据格式比较轻量,性能开销小,通信高效;
- 可以发送文本,也可以发送二进制数据;
- 没有同源限制,客户端可以与任意服务器通信。
SSE
所谓
SSE(Sever-Sent Event)
,就是浏览器向服务器发送一个HTTP
请求,保持长连接,服务器不断单向地向浏览器推送消息,这么做是为了节约网络资源,不用一直发请求,建立新连接。
它其实类似长轮询,有一个浏览器内置EventSource
对象来操作
//进建立链接
var source = new EventSource();
//关闭链接
source.close();
缺点:无法实现双向消息。
StompJS
在探讨StompJS
之前,让我们先了解一下STOMP -- Simple (or Streaming) Text Orientated Messaging Protocol
,一个面向消息/流的简单文本协议。它提供了一个可互操作的连接格式,允许STOMP
客户端与任意STOMP
消息代理(Broker
)进行交互。从而为多语言、多平台和Brokers
集群提供简单且普遍的消息协作。
STOMP
可用于任何可靠的双向流网络协议之上,如TCP
和WebSocket
。 虽然STOMP
是面向文本的协议,但消息有效负载可以是文本或二进制。
STOMP
是一种基于帧的协议,帧的结构是效仿HTTP报文格式,如下:
COMMAND
header1:value1
header2:value2
Body^@
WebSocket
实现客户端看起来比较简单,但是需要与后台进行很好的配合和调试才能达到最佳效果。通过SockJS/SocketIO 、Stomp
来进行浏览器兼容,可以增加消息语义和可用性。简而言之,WebSocket
是底层协议,SockJS
是WebSocket
的备选方案,也是底层协议,而 STOMP
是基于 WebSocket(SockJS)
的上层协议。
WebSocket
协议定义了两种类型的消息,文本和二进制,但它们的内容是未定义的。
如果说
Socket
是C/S
的TCP
编程,那么Websocket
就是Web(B/S)
的TCP
编程,所以需要在客户端与服务端之间定义一个机制去协商一个子协议(更高级别的消息协议),将它使用在Websocket
之上去定义每次发送消息的类别、格式和内容等等。
子协议的使用是可选的,但无论哪种方式,客户端和服务器都需要就一些定义消息内容的协议达成一致。
于是,通常选择在Websocket
协议上使用STOMP
协议来定义内容格式。
-
创建
STOMP
客户端
在web
浏览器中,可以通过两种方式进行客户端的创建- 使用普通的
WebSocket
let url = "ws://localhost:61614/stomp"; let client = Stomp.client(url);
- 使用定制的
WebSocket
let url = "ws://localhost:61614/stomp"; let socket = new SockJS(url); let client = Stomp.over(socket);
虽然客户端的创建方式不同,但后续的连接等操作都是一样的。
- 使用普通的
-
连接服务端
client.connect(login,passcode,successCallback,errorCallback);
-
login
和passcode
都是字符串,相当于是用户的登录名和密码凭证。 -
successCallback、errorCallback
分别是连接成功、失败的回调函数。
还可以这样连接服务器:
const loginForm = { login:'admin', passcode:'666', 'token':'2333' } client.connect(loginForm , successCallback, errorCallback);
-
-
断开连接:
client.disconnect(() => { console.log("disconnect") })
-
Heart-beating(心跳)
Heart-beating
也就是消息传送的频率,incoming
是接收频率,outgoing
是发送频率,其默认值都为10000ms
// 手动设置 client.heartbeat.outgoing = 5000; client.heartbeat.incoming = 0;
-
发送消息
客户端向服务端发送消息:send(serverAddr, [options], [message])
-
serverAddr
字符串,发送消息的目的地; -
options
可选对象,包含了额外的头部信息; -
message
字符串,发送的消息。
-
订阅消息
-
订阅消息:客户端接收服务端发送的消息;
subscribe(serverAddr, callback, [options])
-
serverAddr
字符串,接收消息的目的地; -
callback
回调函数,接收消息; -
options
可选对象,包含额外的头部信息。
-
-
客户端可以订阅广播
client.subscribe('/topic/msg',function(messages){ console.log(messages); })
-
一对一消息的接收
// 第一种方式 const uId = 888; client.subscribe(`/user/${uId}/msg`, msg => { console.log(msg); }) // 第二种方式 client.subscribe('/msg', msg => { console.log(messages); }, {userId : uId })
客户端采用的写法要根据服务端代码来做选择。
-
取消订阅:
unsubscribe()
constsub = client.subscribe; sub .unsubscribe();