Websocket

长轮询与短轮询

短轮询

其实就是普通的轮询,在特定的时间间隔内,由浏览器向服务器发出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

WebSocketHTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket使客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。

  1. 通常,应用层协议都是完全基于网络层协议TCP/UDP来实现,例如HTTP、SMTP、POP3,而Websocket是同时基于HTTPTCP来实现。

    • 先用带有 Upgrade:Websocket 请求头的特殊HTTP request来实现与服务端握手HandShake
    • 握手成功后,协议升级成Websocket,进行长连接通讯;
    • 整个过程可理解为:小锤抠缝,大锤搞定。
  2. 为什么不使用HTTP长连接来实现即时通讯?事实上,在Websocket之前就是使用HTTP长连接这种方式,如Comet。但是它有如下弊端:

    • HTTP 1.1 规范中规定,客户端不应该与服务器端建立超过两个的 HTTP 连接, 新的连接会被阻塞;
    • 对于服务端来说,每个长连接都占有一个用户线程,在NIO或者异步编程之前,服务端开销太大;
    • HTTP不能完成服务端推送,新出的HTTP/2只能推送静态资源,无法推送即时消息。
    • HTTP/2所谓的server push其实是当服务器接收一个请求时,可以响应多个资源。
  3. 为什么不直接使用Socket编程,基于TCP直接保持长连接,实现即时通讯?

    • Socket编程针对C/S模式的,而浏览器是B/S模式,浏览器无法发起Socket请求。正因如此, W3C最后还是给出了浏览器的Socket -- Websocket
  4. 实际上,HTTP协议也是建立在TCP协议之上的,TCP协议本身就是全双工通信,但HTTP协议的请求-应答机制限制了全双工通信。WebSocket其实也只是简单规定了一下:接下来咱们就不使用HTTP协议了,直接互相发数据吧。

HTTPwebSocket其实是个交集,他们都是建立在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:websocketConnection: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 中的掩码是为了防止早期版本中存在中间缓存污染攻击等问题而设置的,客户端向服务端发送数据需要掩码,而服务端向客户端发送数据不需要掩码;
  • WebSocketSec-WebSocket-Key 的生成算法是拼接服务端和客户端生成的字符串,进行SHA1哈希算法,再用base64编码;
  • WebSocket 协议握手是依靠 HTTP 协议的,依靠于 HTTP 响应101进行协议升级转换。

SocketIO

SocketIOWebSocket、AJAX和其它的通信方式全部封装成了统一的通信接口。也就是说,我们在使用SocketIO时,不用担心兼容问题,底层会自动选用最佳的通信方式。所以说 WebSocketSocketIO的一个子集。

Websocket API 并不是所有浏览器都完美支持,而当浏览器不支持Websocket时,应该自动切换成Ajax长轮询,SSE等备用解决方案。所以在实际开发中我们通常采用封装了Websocket及其备用方案的库----SockJS(Java) / Socket.IO(Node)

  • 如果使用Java做服务端,同时又恰好使用Spring作为框架,那么推荐使用SockJS,因为Spring本身就是SockJS推荐的Java Server实现,同时也提供了JavaClient实现。
  • 如果使用·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协议有着良好的兼容性。默认端口也是 80443,并且握手阶段采用 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可用于任何可靠的双向流网络协议之上,如TCPWebSocket。 虽然STOMP是面向文本的协议,但消息有效负载可以是文本或二进制。

STOMP是一种基于帧的协议,帧的结构是效仿HTTP报文格式,如下:

COMMAND
header1:value1
header2:value2

Body^@

WebSocket实现客户端看起来比较简单,但是需要与后台进行很好的配合和调试才能达到最佳效果。通过SockJS/SocketIO 、Stomp来进行浏览器兼容,可以增加消息语义和可用性。简而言之,WebSocket是底层协议,SockJSWebSocket 的备选方案,也是底层协议,而 STOMP 是基于 WebSocket(SockJS)的上层协议。

WebSocket协议定义了两种类型的消息,文本二进制,但它们的内容是未定义的。

如果说SocketC/STCP编程,那么Websocket就是Web(B/S)TCP编程,所以需要在客户端与服务端之间定义一个机制去协商一个子协议(更高级别的消息协议),将它使用在Websocket之上去定义每次发送消息的类别、格式和内容等等。
子协议的使用是可选的,但无论哪种方式,客户端和服务器都需要就一些定义消息内容的协议达成一致。
于是,通常选择在Websocket协议上使用STOMP协议来定义内容格式。

  1. 创建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);
      

    虽然客户端的创建方式不同,但后续的连接等操作都是一样的。

  2. 连接服务端

    client.connect(login,passcode,successCallback,errorCallback);
    
    • loginpasscode都是字符串,相当于是用户的登录名和密码凭证。
    • successCallback、errorCallback 分别是连接成功、失败的回调函数。

    还可以这样连接服务器:

    const loginForm = { login:'admin', passcode:'666', 'token':'2333' }
    client.connect(loginForm , successCallback, errorCallback);
    
  3. 断开连接:

    client.disconnect(() => {
       console.log("disconnect")
    })
    
  4. Heart-beating(心跳)
    Heart-beating也就是消息传送的频率,incoming是接收频率,outgoing是发送频率,其默认值都为10000ms

    // 手动设置
    client.heartbeat.outgoing = 5000; 
    client.heartbeat.incoming = 0;
    
  5. 发送消息
    客户端向服务端发送消息:send(serverAddr, [options], [message])

    • serverAddr 字符串,发送消息的目的地;
    • options 可选对象,包含了额外的头部信息;
    • message 字符串,发送的消息。
订阅消息
  1. 订阅消息:客户端接收服务端发送的消息;
    subscribe(serverAddr, callback, [options])

    • serverAddr 字符串,接收消息的目的地;
    • callback 回调函数,接收消息;
    • options 可选对象,包含额外的头部信息。
  2. 客户端可以订阅广播

    client.subscribe('/topic/msg',function(messages){
        console.log(messages);
    })
    
  3. 一对一消息的接收

    // 第一种方式
    const uId = 888;
    client.subscribe(`/user/${uId}/msg`, msg => {
        console.log(msg);
    })
    // 第二种方式
    client.subscribe('/msg', msg => {
        console.log(messages);
    }, {userId : uId  })
    

    客户端采用的写法要根据服务端代码来做选择。

  4. 取消订阅:unsubscribe()

    constsub =  client.subscribe;
    sub .unsubscribe();
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341