发布时间:2022-03-10 21:05:33来源:腾讯IMWeb前端团队
本系列第一篇《》,没看过的同学可以看看,看过的同学也可以回顾一把。
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocketAPI中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
Socket.IO在Socket.IOserver(Node.js)和Socket.IOclient(browser,Node.js,oranotherprogramminglanguage)之间,基于WebSocket(不支持WebSocket的情况下,退化成HTTPlong-polling)建立一条全双工实时通信通道.
Engine.IO是一个Socket.IO的抽象实现,作为Socket.IO的服务器和浏览器之间交换的数据的传输层。它不会取代Socket.IO,它只是抽象出固有的复杂性,支持多种浏览器,设备和网络的实时数据交换。Engine.IO使用了Websocket和HTTPlong-polling方式封装了一套socket协议。为了兼容不支持Websocket的低版本浏览器,使用长轮询(polling)替代WebSocket。
Engine.IO负责在服务器和客户端之间建立底层连接。包括以下功能:
多种传输通道及升级机制
断连检测
现在主要有2种传输通道实现
HTTPlong-polling
WebSocket
HTTPlong-pollingtransport(也简称"polling")由连续的HTTPrequests组成:
long-runningGETrequests,forreceivingdatafromtheserver
short-runningPOSTrequests,forsendingdatatotheserver
基于HTTPlong-pollingtransport的特性,连续的emits可能合并在一个HTTPRequest中发送。
TheWebSocket传输通道包含一条WebSocket连接,WebSocket提供了服务端和客户端之间双向通信及低时延的通信通道。
基于传输通道特性,每个emit会以一个WebSocket数据帧发送,有时候会分为2个不同的数据帧发送。
Engine.IO连接建立的时候,Server端会发送一些消息到客户端:
{"sid":"FSDjX-WRwSA4zTZMALqx","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":20000}sid:是session的ID,在所有的子序列HTTPRequest中都会在参数带上这个sid.
upgrades:upgradesarray包含了服务端可以支持的更好的transport.
pingInterval和pingTimeout:用于心跳机制.
默认的情况下,客户端先建立HTTPlong-polling通信通道。
为什么呢?
WebSocket无疑是最好的双向通道,但是由于公司的代理、个人的防火墙、杀毒软件等,它并不是在什么情况下都能成功建立。
从用户的角度来看,如果WebSocket连接建立失败,那么用户至少要等10S才能开始真正的数据传输,这无疑伤害了用户的体验。
总的来说,Engine.IO首先关注可靠性和用户体验,其次才是服务器性能。
升级的时候,客户端会做如下动作:
保证要发送的队列中是空的
把当前的传输通道设为只读
使用另外的transport建立新的连接
如果新传输通道建立成功,关掉第一条传输通道
可以在浏览器抓包看到如下网络连接:
握手协议(containsthesessionID—here,zBjrh...AAAK—thatisusedinsubsequentrequests)
发送数据(HTTPlong-polling)
接收数据(HTTPlong-polling)
升级协议(WebSocket)
接收数据(HTTPlong-polling,closedoncetheWebSocketconnectionin4.issuccessfullyestablished)
当以下情况出现时,Engine.IO的连接会判断为关闭。
一次HTTPrequest(eitherGETorPOST)失败(比如服务器挂了)
WebSocket连接关闭(比如用户关闭了浏览器的tab)
在服务端或者客户端调用socket.disconnect()
还有一个心跳机制用来检测服务端和客户端的连接是否正常在运行。
服务端会以pingInterval的间隔发送PING数据包,客户端收到后在pingTimeout时间之内需要发送PONG数据包给服务端,如果服务端在pingTimeout时间内没有收到,那么就认为这条连接关闭了。相反,客户端如果在pingInterval+pingTimeout时间内没有收到PING数据包,客户端也判断连接关闭。
服务端触发断连事件的原因有:
Reason
Description
servernamespacedisconnect
Thesocketwasforcefullydisconnectedwithsocket.disconnect
clientnamespacedisconnect
Theclienthasmanuallydisconnectedthesocketusingsocket.disconnect()
servershuttingdown
Theserveris,well,shuttingdown
pingtimeout
TheclientdidnotsendaPONGpacketinthepingTimeoutdelay
transportclose
Theconnectionwasclosed(example:theuserhaslostconnection,orthenetworkwaschangedfromWiFito4G)
transporterror
Theconnectionhasencounteredanerror
客户端触发断连事件的原因有:
Reason
Description
ioserverdisconnect
Theserverhasforcefullydisconnectedthesocketwithsocket.disconnect()
ioclientdisconnect
Thesocketwasmanuallydisconnectedusingsocket.disconnect()
pingtimeout
TheserverdidnotsendaPINGwithinthepingInterval+pingTimeoutrange
transportclose
Theconnectionwasclosed(example:theuserhaslostconnection,orthenetworkwaschangedfromWiFito4G)
transporterror
Theconnectionhasencounteredanerror(example:theserverwaskilledduringaHTTPlong-pollingcycle)
传输通道通过Engine.IOURL进行连接建立
连接建立之后,服务端会发一个JSON格式的握手数据
sid:会话id(string)
upgrades:允许升级的传输通道(ArrayofString)
pingTimeout:服务端配置的ping超时时间,发送给客户端,客户端用来检测服务端是否还正常响应(Number)
pingInterval:服务端配置的心跳间隔,客户端用来检测服务端是否还正常响应(Number)
客户端收到服务端定时的pingpacket之后,需要回复客户端pongpacket
客户端和服务端之间可以传输messagepackets
Pollingtransports可以发送closepacket来关闭socket
Requestn°1(openpacket)
GET/engine.io/?EIO=4&transport=polling&t=N8hyd6w
0=>"open"packettype{"sid":...=>thehandshakedataNote:query参数中的t是用来防止浏览器缓存请求.
Requestn°2(messagein)
服务端执行socket.send('hey'):
GET/engine.io/?EIO=4&transport=polling&t=N8hyd7H&sid=lv_VI97HAXpY6yYWAAAC 4=>"message"packettypehey=>theactualmessageNote:query中的sid是握手协议中sid. Requestn°3(messageout) 客户端执行:socket.send('hello');socket.send('world'); POST/engine.io/?EIO=4&transport=polling&t=N8hzxke&sid=lv_VI97HAXpY6yYWAAAC>Content-Type:text/plain;charset=UTF-84hello\x1e4world 4=>"message"packettypehello=>the1stmessage\x1e=>separator4=>"message"messagetypeworld=>the2ndmessageRequestn°4(WebSocketupgrade) GET/engine.io/?EIO=4&transport=websocket&sid=lv_VI97HAXpY6yYWAAAC <2probe=>proberequest>3probe=>proberesponse<5=>"upgrade"packettype>4hello=>message(notconcatenated)>4world>2=>"ping"packettype<3=>"pong"packettype>1=>"close"packettype只有WebSocket连接的会话在这个例子中,客户端只开启了WebSocket传输通道(withoutHTTPpolling). GET/engine.io/?EIO=4&transport=websocket <0{"sid":"lv_VI97HAXpY6yYWAAAC","pingInterval":25000,"pingTimeout":5000}=>handshake<4hey>4hello=>message(notconcatenated)>4world<2=>"ping"packettype>3=>"pong"packettype>1=>"close"packettype3.2URLsEngine.IOurl包含了以下内容 /engine.io/[? querystring是可选的,有6个保留的key: transport:指定的transport,默认为polling,websocket. j:如果需要JSONP响应,j必须与JSONP响应索引一起设置。 sid:如果客户端已经收到sessionid,那么每次请求的querystring中都必须带上sid EIO:协议的版本 t:用来防止浏览器缓存 有两种不同类型的编码 packet payload 一个编码的数据包可以是UTF-8字符串或者二进制数据。字符串的数据包编码格式如下: 4hello对于二进制数据,不包括数据包类型(packettype),因为只有“message”数据包类型可以包括二进制数据。 0open 新传输通道建立的时候,从服务端发送Sentfromtheserverwhenanewtransportisopened(recheck) 1close 请求关闭此传输,但不关闭连接本身。 2ping 由服务器发送。客户应该用pong数据包应答。 example serversends:2clientsends:33pong 由客户端发送以响应ping数据包。 4message 实际传输的消息 example1 serversends:4HelloWorldclientreceivesandcallscallbacksocket.on('message',function(data){console.log(data);});example2 clientsends:4HelloWorldserverreceivesandcallscallbacksocket.on('message',function(data){console.log(data);});5upgrade 在engine.io切换传输通道之前,它测试服务器和客户端是否可以通过该传输进行通信。如果此测试成功,客户端将发送一个升级包,请求服务器刷新旧传输上的缓存,并切换到新传输通道。 6noop 一个noop包。主要用于建立websocket连接之后关闭长轮询。 example clientconnectsthroughnewtransportclientsends2probeserverreceivesandsends3probeclientreceivesandsends5serverflushesandclosesoldtransportandswitchestonew.3.3.2PayloadPayload是捆绑在一起的一系列encodedpackets。Payload编码格式如下: 当有效负载中包含二进制数据时,它将作为base64编码字符串发送。为了解码的目的,将标识符b置于包含二进制数据的分组编码之前。可以发送任意数量的字符串和base64编码字符串的组合。下面是base64编码消息的示例: 不包含二进制的例子: [{"type":"message","data":"hello"},{"type":"message","data":"€"}]编码后: 4hello\x1e4€包含二进制的例子: [{"type":"message","data":"€"},{"type":"message","data":buffer<01020304>}]编码后: 4€\x1ebAQIDBA==分解: 4=>"message"packettype€\x1e=>recordseparatorb=>indicatesabase64packetAQIDBA===>buffercontentencodedinbase643.4传输通道engine.ioserver必须支持三种传输通道: websocket server-sentevents(SSE) polling jsonp xhr 轮询传输包括客户端向服务器发送周期性GET请求以获取数据,以及将带有有效负载的请求从客户端发送到服务器以发送数据。 服务器必须支持CORS响应。 服务器实现必须使用有效的JavaScript进行响应。在响应中需要使用URL中query中的j参数。j是一个整数。 JSONP数据包的格式。 `___eio[` 服务器返回的JSONP数据帧的例子 ___eio[4]("packetdata");Postingdata 客户端通过隐藏的iframe发送数据。数据以URI编码格式发送给服务器,如下所示 d= 客户端使用EventSource对象接收数据,使用XMLHttpRequest对象发送数据。 上面的对payloads的编码方式并不用于WebSocket通道,WebSocket通道本身已有轻量级的数据帧机制。 发送消息的时候,对数据包进行单独编码,然后依次调用send()进行发送。 连接总是以轮询(XHR或JSONP)开始。WebSocket通过发送探针在侧面进行测试(2probe)。如果探测由服务器响应(3probe),则客户端会发送一个升级包(5)。 为了确保没有消息丢失,只有在刷新现有传输的所有缓冲区并认为传输已暂停后,才会发送升级数据包。 当服务器收到升级包时,它必须假定这是新的传输通道,并将所有现有缓冲区(如果有的话)发送给它。 客户端发送的探测器是一个ping+probe作为数据发送。(2probe)服务端发送的探测器是一个pong+probe作为数据发送。(3probe) 客户端必须使用握手中发送的pingTimeout和pingInterval来确定服务器是否无响应。 服务器发送一个ping数据包。如果在pingTimeout内未收到任何数据包类型,服务器将认为套接字已断开连接。如果收到了pong数据包,服务器将在等待pingInterval之后再次发送ping数据包。 由于这两个值在服务器和客户端之间共享,当客户端在pingTimeout+pingInterval内没有接收到任何数据时,客户端也能探测到服务器是否变得无响应。 Engine.IO是Socket.IO的底层传输通道实现。 Engine.IO、Socket.IO在上层均有自己的协议,因此服务端和客户端必须搭配才能使用。也就是说Socket.IO的客户端必须搭配Socket.IO的服务端才能正常交互数据。 在浏览器中message中的能抓到的数据包,属于WebSocket协议中的message类型数据,WebSocket的PING,PONG是和message类型是并列的,因此浏览器中的devTools并不能抓到,而Engine.IO的心跳机制的实现(下图中的2和3),是message数据之上的协议定义,是Engine.IO用WebSocket的message类型消息发送的。 服务端代码 constengine=require('engine.io');constserver=engine.listen(3000,{cors:{origin:"*"}});server.on('listen',()=>{console.log('listeningon3000')})server.on('connection',socket=>{console.log('newconnection')socket.send('utf8string');socket.send(Buffer.from('helloworld'));//binarydata});客户端代码 const{Socket}=require('engine.io-client');constsocket=newSocket('ws://localhost:3000');socket.on('open',()=>{socket.emit('messagefromclient')socket.on('message',(data)=>{console.log('receivemessage:'+data);socket.send('ackfromclient.');});socket.on('close',(e)=>{console.log('socketclose',e)});});浏览器请求抓包 1、Polling传输通道握手 Request: Response: 2、发起长轮询请求服务端数据 Request: Response: 3、POST方式发送数据到服务端 Request: Requestpayload: Response: 4、服务端告诉客户端传输通道已升级,回复一个6 Request: Response: 5、WebSocket通道建立之后,切换为WebSocket传输数据 Connect: Message: 也可以在客户端指定传输通道为websocket,那么就不会先建立Polling传输通道,直接用WebSocket传输通道进行握手。 constsocket=newSocket('ws://localhost:3000',{transports:['websocket'[}); 紧追技术前沿,深挖专业领域 扫码关注我们吧!