# 9. actioncable 实现重新连接功能
#### 1. 介绍
当我们在一个网页聊天室聊天的时候,我们有一个情况,会显示是在线,还是离线状态,还有,有一些网页型的项目管理工具,在离线状态的时候,可能是网络连不上的时候,也能够进行操作,不过它是把数据暂时保存到浏览器本地,等网络通了,就会自动同步到服务器中,这些又是如何办到的呢,这篇文章就来讲这个原理。
![](https://box.kancloud.cn/8478918b4ffab89229d2611a6509656b_399x248.png)
#### 2. 使用
其实要实现整个功能,我只是用了几行代码,不过最重要的是理解了actioncable中的javascript部分源码,通过复写其中的方法来实现的。
现在就来看看这部分的源码。
##### 2.1 cable.coffee
我们先从项目中的入口文件`app/assets/javascripts/cable.coffee`看起,其内容大约是这样的:
```
@App ||= {}
App.cable = ActionCable.createConsumer()
```
相关的源码是这样的:
```
# https://github.com/rails/rails/blob/52ce6ece8c8f74064bb64e0a0b1ddd83092718e1/actioncable/app/assets/javascripts/action_cable.coffee.erb#L4
@ActionCable =
INTERNAL: <%= ActionCable::INTERNAL.to_json %>
createConsumer: (url) ->
url ?= @getConfig("url") ? @INTERNAL.default_mount_path
new ActionCable.Consumer @createWebSocketURL(url)
```
##### 2.2 ActionCable.createConsumer
这个`ActionCable.createConsumer`方法我们之前也有讲过,它就是通过一些参数,找到就正确的服务器地址,然后执行`new WebSocket`的。
而且你还在源码文件`action_cable.coffee.erb`中看到下面几行。
```
startDebugging: ->
@debugging = true
stopDebugging: ->
@debugging = null
log: (messages...) ->
if @debugging
messages.push(Date.now())
console.log("[ActionCable]", messages...)
```
`@debugging`变量是开启日志的开关,我们把它设为true。
```
# app/assets/javascripts/cable.coffee
@App ||= {}
App.cable = ActionCable.createConsumer()
ActionCable.startDebugging()
```
现在我们就可以在chrome等浏览器的开发者工具的console标签看到更为详尽的日志内容了。
![](https://box.kancloud.cn/aac2596c43ee48a381c687c7ceddbcd7_974x205.png)
##### 2.3 ActionCable.Consumer
现在来看一下`new ActionCable.Consumer`的内容:
```
# https://github.com/rails/rails/blob/52ce6ece8c8f74064bb64e0a0b1ddd83092718e1/actioncable/app/assets/javascripts/action_cable/consumer.coffee#L17
class ActionCable.Consumer
constructor: (@url) ->
@subscriptions = new ActionCable.Subscriptions this
@connection = new ActionCable.Connection this
send: (data) ->
@connection.send(data)
ensureActiveConnection: ->
unless @connection.isActive()
@connection.open()
```
很早之前我们就介绍过,如何用javascript给后台发送websocket请求。
首先是使用`new WebSocket`发起连接指令。
然后使用`send`命令发送具体的消息给后台,比如`ws.send("Hello");`。
上面的源码中,`new ActionCable.Connection this`管的就是`new WebSocket`类似的内容。当然它除了有open方法,还有close方法用于关闭连接,reopen方法用于重连。
`new ActionCable.Subscriptions this`的内容主要就是管理如何向服务器端发送具体的消息的,这部分我们先不管。
##### 2.4 ActionCable.Connection
我们来看看`ActionCable.Connection`相关的内容。
关于它的内容主要是下面两个文件:
- connection.coffee
- connection\_monitor.coffee
`connection.coffee`文件主要是记录了open、close,reopen方法,还有连接的活动状态等。最重要的是reopen方法。
```
class ActionCable.Connection
@reopenDelay: 500
constructor: (@consumer) ->
{@subscriptions} = @consumer
@monitor = new ActionCable.ConnectionMonitor this
@disconnected = true
send: (data) ->
if @isOpen()
@webSocket.send(JSON.stringify(data))
true
else
false
open: =>
if @isActive()
ActionCable.log("Attempted to open WebSocket, but existing socket is #{@getState()}")
throw new Error("Existing connection must be closed before opening")
else
ActionCable.log("Opening WebSocket, current state is #{@getState()}, subprotocols: #{protocols}")
@uninstallEventHandlers() if @webSocket?
@webSocket = new WebSocket(@consumer.url, protocols)
@installEventHandlers()
@monitor.start()
true
close: ({allowReconnect} = {allowReconnect: true}) ->
@monitor.stop() unless allowReconnect
@webSocket?.close() if @isActive()
reopen: ->
ActionCable.log("Reopening WebSocket, current state is #{@getState()}")
if @isActive()
try
@close()
catch error
ActionCable.log("Failed to reopen WebSocket", error)
finally
ActionCable.log("Reopening WebSocket in #{@constructor.reopenDelay}ms")
setTimeout(@open, @constructor.reopenDelay)
else
@open()
```
这个文件只是定义了如何reopen的方法,默认reopen的时间延迟是500ms。
假如说,突然服务器挂了,这个时候掉线了,客户端应该重新连接,但重新连接的次数总是有限的吧,不能一直进行下去。
这个就是`connection_monitor.coffee`所发挥的作用。
`ActionCable.Connection`也是会调用这个文件的内容的。
比如`constructor`方法中的`@monitor = new ActionCable.ConnectionMonitor this`,还有`open`方法中的`@monitor.start()`。
##### 2.5 ActionCable.ConnectionMonitor
我们来看看`connection_monitor.coffee`的部分源码:
```
class ActionCable.ConnectionMonitor
@pollInterval:
min: 3
max: 30
@staleThreshold: 6 # Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
constructor: (@connection) ->
@reconnectAttempts = 0
start: ->
unless @isRunning()
@startedAt = now()
delete @stoppedAt
@startPolling()
document.addEventListener("visibilitychange", @visibilityDidChange)
ActionCable.log("ConnectionMonitor started. pollInterval = #{@getPollInterval()} ms")
```
在阅读源码的时候,可以紧密结合日志来一起看的。
##### 2.6 测试
现在我们来做一个实验,把服务器停掉,看看浏览器的日志会输出什么内容。
一关掉服务器,浏览器就马上探测出服务器关闭了,并输出了下面的信息:
```
[ActionCable] WebSocket onclose event 1462006781945
[ActionCable] ConnectionMonitor recorded disconnect 1462006781954
```
除此之外,接着输出类似下面的信息:
```
[ActionCable] ConnectionMonitor detected stale connection. reconnectAttempts = 0, pollInterval = 3000 ms, time disconnected = 5.156 s, stale threshold = 6 s 1462007045080
[ActionCable] ConnectionMonitor detected stale connection. reconnectAttempts = 1, pollInterval = 3466 ms, time disconnected = 8.624 s, stale threshold = 6 s 1462007048548
[ActionCable] ConnectionMonitor detected stale connection. reconnectAttempts = 2, pollInterval = 5493 ms, time disconnected = 14.124 s, stale threshold = 6 s 1462007054048
```
##### 2.7 改写
其实这些输出信息都在`connection_monitor.coffee`文件中可以找到,主要就是`reconnectIfStale`这个方法:
```
reconnectIfStale: ->
if @connectionIsStale()
ActionCable.log("ConnectionMonitor detected stale connection. reconnectAttempts = #{@reconnectAttempts}, pollInterval = #{@getPollInterval()} ms, time disconnected = #{secondsSince(@disconnectedAt)} s, stale threshold = #{@constructor.staleThreshold} s")
@reconnectAttempts++
if @disconnectedRecently()
ActionCable.log("ConnectionMonitor skipping reopening recent disconnect")
else
ActionCable.log("ConnectionMonitor reopening")
@connection.reopen()
```
主要改写这个方法,把日志输出的部分效果改成在页面上提示即可。
我是这样改写的:
```
App.cable.connection.monitor.reconnectIfStale = ->
if App.cable.connection.monitor.connectionIsStale()
$.notify("正在重新连接")
App.cable.connection.monitor.reconnectAttempts++
if App.cable.connection.monitor.disconnectedRecently()
else
App.cable.connection.reopen(
```
至于`$.notify`是使用了[notifyjs](https://notifyjs.com/)这个库,你当然可以用你自己喜欢的库,或者干脆自己修改样式。
这样就算完成了,不过为了圆满,当连上服务器的时候,或掉线的时候也总有提示吧。
```
# app/assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
connected: ->
$.notify("已连接到服务器", "success")
disconnected: ->
$.notify("已掉线", "warn")
```
![](https://box.kancloud.cn/01214918bc41011025c449432caf687b_403x244.png)
![](https://box.kancloud.cn/98291f3c783f9f6c73555de36898001a_401x239.png)
本篇完结。
下一篇:[websocket之部署(十)](http://www.rails365.net/articles/websocket-zhi-bu-shu-shi)