1. 介绍

    当我们在一个网页聊天室聊天的时候,我们有一个情况,会显示是在线,还是离线状态,还有,有一些网页型的项目管理工具,在离线状态的时候,可能是网络连不上的时候,也能够进行操作,不过它是把数据暂时保存到浏览器本地,等网络通了,就会自动同步到服务器中,这些又是如何办到的呢,这篇文章就来讲这个原理。
    10、actioncable 实现重新连接功能 - 图1

    1. 使用

    其实要实现整个功能,我只是用了几行代码,不过最重要的是理解了actioncable中的javascript部分源码,通过复写其中的方法来实现的。
    现在就来看看这部分的源码。
    2.1 cable.coffee

    我们先从项目中的入口文件app/assets/javascripts/cable.coffee看起,其内容大约是这样的:

    1. @App ||= {}
    2. App.cable = ActionCable.createConsumer()

    相关的源码是这样的:

    1. # https://github.com/rails/rails/blob/52ce6ece8c8f74064bb64e0a0b1ddd83092718e1/actioncable/app/assets/javascripts/action_cable.coffee.erb#L4
    2. @ActionCable =
    3. INTERNAL: <%= ActionCable::INTERNAL.to_json %>
    4. createConsumer: (url) ->
    5. url ?= @getConfig("url") ? @INTERNAL.default_mount_path
    6. new ActionCable.Consumer @createWebSocketURL(url)

    2.2 ActionCable.createConsumer

    这个ActionCable.createConsumer方法我们之前也有讲过,它就是通过一些参数,找到就正确的服务器地址,然后执行new WebSocket的。
    而且你还在源码文件action_cable.coffee.erb中看到下面几行。

    1. startDebugging: ->
    2. @debugging = true
    3. stopDebugging: ->
    4. @debugging = null
    5. log: (messages...) ->
    6. if @debugging
    7. messages.push(Date.now())
    8. console.log("[ActionCable]", messages...)

    @debugging变量是开启日志的开关,我们把它设为true。

    1. # app/assets/javascripts/cable.coffee
    2. @App ||= {}
    3. App.cable = ActionCable.createConsumer()
    4. ActionCable.startDebugging()

    现在我们就可以在chrome等浏览器的开发者工具的console标签看到更为详尽的日志内容了。
    10、actioncable 实现重新连接功能 - 图2
    2.3 ActionCable.Consumer

    现在来看一下new ActionCable.Consumer的内容:

    1. # https://github.com/rails/rails/blob/52ce6ece8c8f74064bb64e0a0b1ddd83092718e1/actioncable/app/assets/javascripts/action_cable/consumer.coffee#L17
    2. class ActionCable.Consumer
    3. constructor: (@url) ->
    4. @subscriptions = new ActionCable.Subscriptions this
    5. @connection = new ActionCable.Connection this
    6. send: (data) ->
    7. @connection.send(data)
    8. ensureActiveConnection: ->
    9. unless @connection.isActive()
    10. @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相关的内容。
    关于它的内容主要是下面两个文件:

    1. connection.coffee
    2. connection_monitor.coffee

    connection.coffee文件主要是记录了open、close,reopen方法,还有连接的活动状态等。最重要的是reopen方法。

    1. class ActionCable.Connection
    2. @reopenDelay: 500
    3. constructor: (@consumer) ->
    4. {@subscriptions} = @consumer
    5. @monitor = new ActionCable.ConnectionMonitor this
    6. @disconnected = true
    7. send: (data) ->
    8. if @isOpen()
    9. @webSocket.send(JSON.stringify(data))
    10. true
    11. else
    12. false
    13. open: =>
    14. if @isActive()
    15. ActionCable.log("Attempted to open WebSocket, but existing socket is #{@getState()}")
    16. throw new Error("Existing connection must be closed before opening")
    17. else
    18. ActionCable.log("Opening WebSocket, current state is #{@getState()}, subprotocols: #{protocols}")
    19. @uninstallEventHandlers() if @webSocket?
    20. @webSocket = new WebSocket(@consumer.url, protocols)
    21. @installEventHandlers()
    22. @monitor.start()
    23. true
    24. close: ({allowReconnect} = {allowReconnect: true}) ->
    25. @monitor.stop() unless allowReconnect
    26. @webSocket?.close() if @isActive()
    27. reopen: ->
    28. ActionCable.log("Reopening WebSocket, current state is #{@getState()}")
    29. if @isActive()
    30. try
    31. @close()
    32. catch error
    33. ActionCable.log("Failed to reopen WebSocket", error)
    34. finally
    35. ActionCable.log("Reopening WebSocket in #{@constructor.reopenDelay}ms")
    36. setTimeout(@open, @constructor.reopenDelay)
    37. else
    38. @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的部分源码:

    1. class ActionCable.ConnectionMonitor
    2. @pollInterval:
    3. min: 3
    4. max: 30
    5. @staleThreshold: 6 # Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
    6. constructor: (@connection) ->
    7. @reconnectAttempts = 0
    8. start: ->
    9. unless @isRunning()
    10. @startedAt = now()
    11. delete @stoppedAt
    12. @startPolling()
    13. document.addEventListener("visibilitychange", @visibilityDidChange)
    14. ActionCable.log("ConnectionMonitor started. pollInterval = #{@getPollInterval()} ms")

    在阅读源码的时候,可以紧密结合日志来一起看的。
    2.6 测试

    现在我们来做一个实验,把服务器停掉,看看浏览器的日志会输出什么内容。
    一关掉服务器,浏览器就马上探测出服务器关闭了,并输出了下面的信息:

    1. [ActionCable] WebSocket onclose event 1462006781945
    2. [ActionCable] ConnectionMonitor recorded disconnect 1462006781954

    除此之外,接着输出类似下面的信息:

    1. [ActionCable] ConnectionMonitor detected stale connection. reconnectAttempts = 0, pollInterval = 3000 ms, time disconnected = 5.156 s, stale threshold = 6 s 1462007045080
    2. [ActionCable] ConnectionMonitor detected stale connection. reconnectAttempts = 1, pollInterval = 3466 ms, time disconnected = 8.624 s, stale threshold = 6 s 1462007048548
    3. [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这个方法:

    1. reconnectIfStale: ->
    2. if @connectionIsStale()
    3. ActionCable.log("ConnectionMonitor detected stale connection. reconnectAttempts = #{@reconnectAttempts}, pollInterval = #{@getPollInterval()} ms, time disconnected = #{secondsSince(@disconnectedAt)} s, stale threshold = #{@constructor.staleThreshold} s")
    4. @reconnectAttempts++
    5. if @disconnectedRecently()
    6. ActionCable.log("ConnectionMonitor skipping reopening recent disconnect")
    7. else
    8. ActionCable.log("ConnectionMonitor reopening")
    9. @connection.reopen()

    主要改写这个方法,把日志输出的部分效果改成在页面上提示即可。
    我是这样改写的:

    1. App.cable.connection.monitor.reconnectIfStale = ->
    2. if App.cable.connection.monitor.connectionIsStale()
    3. $.notify("正在重新连接")
    4. App.cable.connection.monitor.reconnectAttempts++
    5. if App.cable.connection.monitor.disconnectedRecently()
    6. else
    7. App.cable.connection.reopen(

    至于$.notify是使用了notifyjs这个库,你当然可以用你自己喜欢的库,或者干脆自己修改样式。
    这样就算完成了,不过为了圆满,当连上服务器的时候,或掉线的时候也总有提示吧。

    1. # app/assets/javascripts/channels/room.coffee
    2. App.room = App.cable.subscriptions.create "RoomChannel",
    3. connected: ->
    4. $.notify("已连接到服务器", "success")
    5. disconnected: ->
    6. $.notify("已掉线", "warn")

    10、actioncable 实现重新连接功能 - 图3

    本篇完结。