Better

Ethan的博客,欢迎访问交流

Socket.IO学习与延伸

最近公司有一个需求使用服务端推的方式更合适,在这里简单对所学进行记录和总结。

服务器通信方式

Ajax轮询

每次都要建立HTTP连接,即使需要传输的数据非常少,所以这样很浪费带宽;同时,这个过程是被动性的,即不是服务器主动推送的。

长连接(long pull)

  • 以即时通信为代表的web应用程序对数据的Low Latency要求,传统的基于轮询的方式已经无法满足,而且也会带来不好的用户体验。于是一种基于http长连接的“服务器推”技术便被hack出来。这种技术被命名为Comet。
  • 客户端发起请求,服务端把这个连接攥在手里不回复,等有消息了再回,如果超时了客户端就再请求一次——其实大家也懂,这只是个减少了请求次数、实时性更好的轮询,本质没变。
  • 同ajax轮询一样,也是每次都要建立HTTP连接,同样也都是被动的。而且这种方法对服务器的并行要求比较大,因为在没有消息的时候,服务器拽着连接不放,而这时需要其它信息时又要建立新的连接。

Websocket

Websocket是HTML5中提出的新的协议,注意,这里是协议,可以实现客户端与服务器端的全双工通信,实现服务器的推送功能。

Websocket

简介

  • 本质上就是为了解决HTTP协议本身的单向性问题。先通过HTTP/HTTPS协议进行握手后创建一个用于交换数据的TCP连接,服务端与客户端通过此TCP连接进行实时通信。
  • 它借用了Web的端口和消息头来创建连接,后续的数据传输又和基于TCP的Socket几乎完全一样,但封装了好多原本在Socket开发时需要我们手动去做的功能。
  • WebSocket协议的URL使用WS://开头,另外安全的WebSocket协议使用WSS://开头
  • WebSocket和http,tcp的联系 0_1330330261W6TS.gif

基础API

  1. 创建对象
  2. onopen:WebSocket创建成功时执行
  3. onmessage:当客户端收到信息时,会触发onmessage事件
  4. onclose:当客户端收到服务器发来的关闭连接请求时,浏览器会触发onclose事件
  5. onerror:当出现连接、处理、接收、发送数据失败的时候就会触发onerror事件

更多

优势

我们可以看出所以的操作都是采用事件的方式触发,如此就不会阻塞UI,使得UI更快的响应时间,得到更好的用户体验

websocket与TCP,HTTP的关系

WebSocket与http协议一样都是基于TCP的,所以他们都是可靠的协议,Web开发者调用的WebSocket的send函数在browser 的实现中最终都是通过TCP的系统接口进行传输的。WebSocket和Http协议一样都属于应用层的协议,那么他们之间有没有什么关系呢?答案是肯定 的,WebSocket在建立握手连接时,数据是通过http协议传输的,正如我们上一节所看到的“GET/chat HTTP/1.1”,这里面用到的只是http协议一些简单的字段。但是在建立连接之后,真正的数据传输阶段是不需要http协议参与的。

websocket server

  • PyWebSocket:PyWebSocket采用Python语言编写,可以很好的跨平台,扩展起来也比较简单,目前WebKit采用它搭建WebSocket服务器来做LayoutTest。
  • WebSocket-Node:WebSocket-Node采用JavaScript语言编写,这个库是建立在nodejs之上的,对于熟悉JavaScript的朋友可参考一下,另外Html5和Web应用程序受欢迎的程度越来越高,nodejs也正受到广泛的关注。
  • LibWebSockets:LibWebSockets采用C/C++语言编写,可定制化的力度更大,从TCP监听开始到封包的完成我们都可以参与编程。

Socket.IO

背景

整合多种双向推送消息方式的库,当初最大的卖点就是兼容所有浏览器版本,自动识别旧版浏览器并采取不同的连接方式,现在也渐渐失去了优势,所有新版浏览器都兼容WebSocket,直接用原生的就行了。

node.js提供了高效的服务端运行环境,但是由于浏览器端对HTML5的支持不一,为了兼容所有浏览器,提供卓越的实时的用户体验,并且为程序员提供客户端与服务端一致的编程体验,于是socket.io诞生。Socket.io将Websocket和轮询 (Polling)机制以及其它的实时通信方式封装成了通用的接口,并且在服务端实现了这些实时机制的相应代码。

核心原理

socket.io.jpg

应用场景

  • 很大一部分AJAX的使用场景仍然是传统的请求-响应形式,比如获取json数据、post表单之类。这些功能虽然靠WebSocket也能实现,但就像在原本传输数据流的TCP之上定义了基于请求的HTTP协议一样,我们也要在WebSocket之上重新定义一种新的协议,最少也要加个request id用来区分每次响应数据对应的请求.但是何苦重复造轮子呢。
  • 传输大文件、图片、媒体流的时候,最好还是老老实实用HTTP来传。如果一定要用WebSocket的话,至少也专门为这些数据专门开辟个新通道,而别去占用那条用于推送消息、对实时性要求很强的连接。否则会把串行的WebSocket彻底堵死.
  • 所以说WebSocket在用于双向传输、推送消息方面能够做到灵活、简便、高效,但在普通的Request-Response过程中并没有太大用武之地,比起普通的HTTP请求来反倒麻烦了许多,甚至更为低效。

每项技术都有自身的优缺点,在适合它的地方能发挥出最大长处,而看到它的几个优点就不分场合地全方位推广的话,可能会适得其反。

服务端API

备注:使用Node作为服务端。

创建对象

var io = require('socket.io')(http);

内置事件

  • connection事件
    io.on('connection', function(socket){
    console.log('a user connected');
    });
    
  • disconnect事件,每个socket离线时会发射特殊的disconnect事件
  • message事件,客户端通过socket.send来传送消息时触发此事件,message为传输的消息,callback是收到消息后要执行的回调
    socket.on(‘message’, function(message, callback) {})
    
  • anything,收到任何事件时触发

事件注册和监听

数据的交流通过事件的方式,因此事件的发射,监听,和广播才是关键

  • 监听,例子:客户端发送消息,服务端接受消息
    //client
    socket.emit('chat message', $('#m').val());
    // server
    socket.on('chat message', function(msg){
    console.log('message: ' + msg);
    });
    
  • 广播
    1. 为了能够向每个人都发射事件,Socket.IO为我们提供了io.emit()方法。
      io.emit('some event', { for: 'everyone' });
      
    2. 如果指定客户端发送给其他客户端,自己不能收到。
      socket.broadcast.emit('some event',hi');
      
  • 单发
    1. 使用socket本身emit方法
      socket.emit('some event',hi');
      

存储数据

socket.set('nickname', name, function () {
 socket.emit('ready');
});
socket.get('nickname', function (err, name) {
 console.log('Chat message by ', name);
});

设置命名空间

有的时候要一个程序支持多个应用,如果使用默认的 “/” 命名空间可能会比较混乱。如果想让一个连接可以支持多个连接,可以使用如下的命名空间的方法:

io.of('/chat').on('connection', function (socket) {});

发送获取数据

很多时候,我们可以在发送的时候直接获取数据,在emit的时候,我们可以传递第三个参数作为回调函数,服务端接受回调函数。

// client
socket.emit('evtnanme', 'clientdata', function (data) {
 console.log(data); // data will be 'serverdata'
});
// server
socket.on('evtnanme', function (name, fn) {
  // name will be 'clientdata'
  fn('serverdata');
});

客户端API

加载JS

暴露了io作为全局对象,然后进行连接,调用io()得到socket对象,没有声明任何URL,默认是尝试连接提供网页的主机。

emit & on

  • 非常重要:特别注意socket.io使用on绑定事件,可以理解成一个多播事件,如果代码运行多次,会对事件绑定多次,这样的话一旦事件发生了,事件处理函数会执行多次,这里一定要注意,看代码运行多次对自己的项目会不会有影响,一般同样的代码执行多次肯定是有问题,融云连接的问题就在于此,因为必须注意这个问题。
  • once:这个方法不是说不会重复绑定,而是事件触发一次之后就自动解除绑定了,之后不会在响应。

内置事件

  • connect
  • connecting
  • disconnect
  • connect_failed
  • error:错误发生,并且无法被其他事件类型所处理
  • anything:服务器端anything事件
  • reconnect_failed
  • reconnect
  • reconnecting

高级功能

这里只简单列举功能点,具体查看官方文档

服务端

  • Server 配置path,serverClient,adapter,origins,parser,pingTimeout,pingInterval,cookie等参数。
  • Namespace
    • 命名名称:namespace.name
    • 连接在此空间的socket集合:namespace.connected
    • 适配器:eg:redis
    • 指定房间:namespace.to(room),指定多个可以使用多次to,to也可以使用in代替
    • 得到连接到这个空间下的客户端:namespace.clients(callback)
    • 注册中间件:namespace.use(fn)
  • Socket
    • socket的标识符:socket.id
    • 加入的房间:socket.rooms
    • 得到客户端:socket.client
    • 请求对象:socket.request
    • socket.handshake
    • 注册中间件:socket.use(fn)
    • 发送message事件:socket.send([...args][, ack])
    • socket.on(eventName, listener)
    • socket.once(eventName, listener)
    • socket.removeListener(eventName, listener)
    • socket.removeAllListeners([eventName])
    • socket.eventNames()
    • 加入指定房间:socket.join(room[, callback])
    • 加入多个房间:socket.join(rooms[, callback])
    • 从指定房间移除:socket.leave(room[, callback])
    • 达到指定房间:socket.to(room)或者使用in
    • 是否压缩:socket.compress(value)
    • 断开连接:socket.disconnect(close)
    • Flag:broadcast,volatile
    • Event:disconnect,error,disconnecting
  • Client

客户端

  • io.protocol
  • io([url][, options])
    • io({autoConnect: false});
  • Manager
    • new Manager(url[, options])
    • 主管配置
    • ...
  • Socket
    • socket.id
    • socket.open() = socket.connect()
    • 触发message事件,socket.send([...args][, ack])
    • socket.on(eventName, callback)
    • socket.compress(value)
    • socket.close() = socket.disconnect()
    • event:connect,connect_error,connect_timeout,error,disconnect,reconnect,reconnect_attempt,reconnecting,reconnect_error,reconnect_failed,ping,pong

Namespaces and Rooms

  • Namespaces
    • 创建命名空间io.of('/my-namespace');
  • Rooms
    • 在每个名称空间中,您还可以定义任意频道,套接字可以加入和离开。
    • socket.join('some room');
    • io.to('some room').emit('some event');
    • 默认房间:套接字在被创建时,通过一个唯一的Socket#id确定,为了方便使用,每个套接字都会被自动加入通过自己id命名的房间。

附录1-事件发送示例

事件emit示例

// sending to the client
  socket.emit('hello', 'can you hear me?', 1, 2, 'abc');

  // sending to all clients except sender
  socket.broadcast.emit('broadcast', 'hello friends!');

  // sending to all clients in 'game' room except sender
  socket.to('game').emit('nice game', "let's play a game");

  // sending to all clients in 'game1' and/or in 'game2' room, except sender
  socket.to('game1').to('game2').emit('nice game', "let's play a game (too)");

  // sending to all clients in 'game' room, including sender
  io.in('game').emit('big-announcement', 'the game will start soon');

  // sending to all clients in namespace 'myNamespace', including sender
  io.of('myNamespace').emit('bigger-announcement', 'the tournament will start soon');

  // sending to individual socketid (private message)
  socket.to(<socketid>).emit('hey', 'I just met you');

  // sending with acknowledgement
  socket.emit('question', 'do you think so?', function (answer) {});

  // sending without compression
  socket.compress(false).emit('uncompressed', "that's rough");

  // sending a message that might be dropped if the client is not ready to receive messages
  socket.volatile.emit('maybe', 'do you really need it?');

  // sending to all clients on this node (when using multiple nodes)
  io.local.emit('hi', 'my lovely babies');

保留事件,请勿暂用

error
connect
disconnect
disconnecting
newListener
removeListener
ping
pong

应用

维护在线列表

一开始想在服务端connection事件中直接将用户加入在线列表中,但是那样无法传递数据,应该另起一个事件,具体步骤如下:

  • 比如add user专门用来新增用户
  • 服务端监听用户disconnect离线事件,用来删除用户
  • 以上两个看上去完成了,但是有个隐藏的大问题,在应用期间socket离线时,如果网络恢复,会自动重连,而这时候服务器已经将它删除,这样就会造成用户不在线的错误,因此还有第三步,重连新增用户。
    socket.on('reconnect',function(){
     socket.emit('add user',data);
    })
    

问题汇总

连接不上服务器

具体表现为无限Ajax请求,但始终无法连接上服务器,在自己的Demo上是没有问题的,移到公司的Ionic App上就出现问题这种问题,一开始考虑是不是在Angular中使用方法不一样?实在找不到原因,但使用最新的问题的socket.io的CDN,问题就不出现了。

由于项目上之前存在一个socket的应用是可以正常运行的,那么为什么现在的会出现问题呢?猜测:应该是服务端和客户端的socket.io的版本有对应关系

事件重复响应

应用到Ionic App上时,客户端出现重复响应事件的问题,一开始考虑到会不会会服务器多处推的问题,排除掉之后才发现是客户端的问题。谨记以下几点:

  • on绑定事件属于多播绑定,使用多次就会响应多次,这样设计是有道理的,给开发者更高的自由度且这是必须的,因为我们需要在监听同一个事件用来处理不同的业务,因此肯定不能采用单播的设计,但是这样带来一个问题,就是开发中不注意,同一个事件处理绑定了多次就会响应多次。
  • 在Angular中,即使controller被销毁,但是其中绑定的事件并不会自动解绑,需要手动解除绑定,不仅仅是Socket的事件,angular中事件也是同样的道理。socket解除事件绑定方法如下:
    socket.removeListener(name,handler);
    socket.removeAllListeners();
    
  • 事件解除绑定时机,这里设计到Ionic的声明周期如下:
    • $ionicView.loaded:该观点已经加载。此事件仅一次按次序被创建并添加到DOM发生。如果视图离开,但被缓存,那么这个事件将不会再在触发。类似于Android的activity中的onCreate()方法。
    • $ionicView.enter:该观点已经全面进入,现在是活动视图。此事件将触发,无论是第一次视图或缓存的视图。类似于Android的activity中的onStart()方法。
    • $ionicView.leave:该观点已经完成离开,不再是积极的看法。此事件将触发,无论是缓存或销毁。类似于Android的activity中的onStop()方法。
    • $ionicView.beforeEnter:视图是即将进入并成为活动视图。类似于Android的activity中的onResume()方法。
    • $ionicView.beforeLeave:视图是即将离开,不再是活动视图。类似于Android的activity中的onPause()方法。
    • $ionicView.afterEnter:该观点已经全面进入,现在是活动视图。
    • $ionicView.afterLeave:该观点已经完成离开,不再是积极的看法。
    • $ionicView.unloaded:该视图的控制器已经被破坏,它的元素已经从DOM中删除。 类似于Android的activity中的onDestroy()方法。
    • $destroy:常用来进行资源释放,清理监听,取消请求,经过思考,取消socket事件绑定的可以放置在此事件中。

socket.io updated at 20220221

socket.io 提供两种层级的抽象

  • low-level:使用 Engine.IO + Manager 对象
  • high-level:使用 Socket.IO

transport 两种方式

  • websocket
  • polling
  • 推荐使用默认值,也就是 polling 建立连接、websocket 交换数据,但不利于调试,可选择在开发环境直接启用 websocket

socket 实例有三个特别事件

  • connect:连接上或者重连上时触发
  • connect_error
    • 底层连接建立失败
    • 服务端中间件阻止了
  • disconnecting
  • disconnect
    • io server disconnect 服务端手动关闭
    • io client disconnect 客户端手动关闭
    • ping timeout 心跳超时
    • transport close 连接关闭,比如断开网络、网络切换等
    • transport error 连接出现错误
    • 前两种原因不会自动重连,其他情况会尝试重连

离线模式

  • 默认情况下,没有连接时发送的消息,会被存到缓存区,直到重连成功,但这可能导致链接激增,当瞬间重连上时
  • 你可以通过 socket.connected 确保连接才发送消息
  • 或者使用 socket.volatile.emit 表示消息允许丢失

服务端多次 emit,客户端同时收到,以及客户端会莫名自动重连

  • 主要是服务端的原因,线程被重计算卡住后,导致多次 emit 最后一次性收到
  • 由于服务端线程被卡住,从而导致心跳超时,客户端自动触发重连

socket.emit 和 socket.send 是不一样的,前者是发送自定义事件,后者是发送内置 message 事件

关于配置踩的坑,传输文件时,会导致连接断开,reason 内容为 transport close,但当我降级到 2.3 的版本时,确是正确的,这让人很困惑,原因如下

  • maxHttpBufferSize 设置过小的原因,而正好我的文件比较大,从而导致连接断开
  • 至于为什么 2.3 可以工作,是因为默认值的不同,2.x 默认值 10e7,而 4.x 默认值 1e6

ClientOptions 需要注意的配置

  • forceBase64
    • 使用 websocket 时,是否强制对二进制内容进行 base64 编码
    • 在 long-polling 模式时,总是开启的
  • closeOnBeforeunload
    • 默认为 true,当浏览器 beforeunload 事件触发时候,自动关闭链接
  • autoConnect 是否自动重连
  • auth 认证相关

ServeOptions

  • connectTimeout
  • maxHttpBufferSize 设置最大传输大小,对应 websocket 的 maxPayload 选项
  • httpCompression 是否为 HTTP long-polling 传输开启压缩
  • cors 相关
  • cookie 往客户端写 cookie


留言

瑶哈哈
2017-07-27 23:19

小伙子加油