最近公司有一个需求使用服务端推的方式更合适,在这里简单对所学进行记录和总结。
服务器通信方式
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的联系
基础API
- 创建对象
- onopen:WebSocket创建成功时执行
- onmessage:当客户端收到信息时,会触发onmessage事件
- onclose:当客户端收到服务器发来的关闭连接请求时,浏览器会触发onclose事件
- 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)机制以及其它的实时通信方式封装成了通用的接口,并且在服务端实现了这些实时机制的相应代码。
核心原理
应用场景
- 很大一部分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); });
- 广播
- 为了能够向每个人都发射事件,Socket.IO为我们提供了io.emit()方法。
io.emit('some event', { for: 'everyone' });
- 如果指定客户端发送给其他客户端,自己不能收到。
socket.broadcast.emit('some event',hi');
- 为了能够向每个人都发射事件,Socket.IO为我们提供了io.emit()方法。
- 单发
- 使用socket本身emit方法
socket.emit('some event',hi');
- 使用socket本身emit方法
存储数据
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
小伙子加油