支撑ActionCable的底层库

文本根据ActionCable 5.1.0版本的代码进行讲解。
ActionCable可以在Rails5中实现集成WebSocket通讯功能。其实都得益于它所依赖的三个第三方库:websocket-driver nio4r concurrent-ruby 其中websocket-driver 负责Websocket通讯,nio4r处理IO,concurrent-ruby 处理并发worker。

image.png

nio4r

nio4r是Java 领域中NIO的Ruby实现版本,最早它是作为celluloid的底层I/O库,后来被作为底层I/O库单独开源它的底层I/O是使用了libev(一种精简高效的C/C++ I/O库)。

nio4r实现了三大特性:

  • selectors: 使用Monitors监听多个I/O对象的就绪状态。
  • Monitors: 追踪注册在监听器上的特定I/O事件。
  • ByteBuffers: 内建的堆外缓存区,支持零复制i/o操作。

ActionCable::Connection::StreamEventLoop 流数据的事件循环类,nio4r在其中被用在与Websocket监听socket的I/O控制。Ruby标准库其实是提供I/O多路复用的,但是它的功能比较弱,仅支持select/poll系统调用。而使用nio4r能够提供epoll/kqueue等在时间复杂度为O(1)的系统调用函数。

module ActionCable
  module Connection
    class StreamEventLoop

      def attach(io, stream)
        @todo << lambda do
          @map[io] = @nio.register(io, :r)
          @map[io].value = stream
        end
        wakeup
      end

      def run
        loop do
          next unless monitors = @nio.select
          # ....
          monitors.each do |monitor|
            io = monitor.io
            stream = monitor.value

            incoming = io.read_nonblock(4096, exception: false)
            case incoming
            when :wait_readable
              next
            when nil
              stream.close
            else
              # 读取后的数据就将交由 Stream
              stream.receive incoming
            end
          end
        end
      end
    end
  end
end

上面的代码就是经过简化后的事件循环类的代码,在ActionCable中在每创建一个连接后就要持续保存I/O的监听状态,这一部分的就是交由nior4r来完成的,可以看到attach方法将Rack闯入的hijack_io对象注册到nio中,然后在run方法的循环中通过nio.select不断监听是否有新的数据流入,如果有的话,就会使用Ruby IO对象的read_nonblock方法读出缓冲区的数据。最后在将读好的数据交给 websocket-driver进行处理。

concurrent-ruby

在ActionCable中每一个Channel中的action或者是callback的执行都是异步的,这样就使得在主线程中执行这些操作就会中断和阻塞的情况,所以ActionCable的解决方案就是使用线程池,concurrent-ruby就是提供了这一部分的功能,在ActionCable的主类,Server::Base中有定义:

require "monitor"

module ActionCable
  module Server
    class Base
     # ......
      # 线程池的大小在默认的情况下是4个,如果需要定制可以通过worker_pool_size参数进行配置。
      def worker_pool
        @worker_pool || @mutex.synchronize do
          @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) 
        end
      end
     # ......
  end
end

上面代码中的使用的ActionCable::Server::Worker就是ActionCable中的线程池类,它的内部封装了concurrent-ruby的线程池实现。

module ActionCable
  module Server
    # 每一条ActionCable的消息都是使用Worker线程池中的一个线程单独运行。
    class Worker      
      def initialize(max_size: 5)
        @executor = Concurrent::ThreadPoolExecutor.new(
          min_threads: 1,
          max_threads: max_size,
          max_queue: 0,
        )
      end
    end
  end
end

websocket-driver

websocket-driver见名知意,就是为ActionCable提供websocket处理能力的驱动程序,要知道在ActionCable的5.0版本中是使用faye-websocket-ruby这个库进行处理websocket请求,后来因为EventMachine的I/O处理性能不如nio4r好,所以就将原本faye-websocket-ruby的工作拆分成了使用nio4r处理i/o,用websocket-driver专门处理websocket的相关细节,其实也就是websocket协议的处理和响应。

在ActionCable中使用ClientSocket类对websocket-driver进行了封装:

module ActionCable
  module Connection
    class ClientSocket
      def initialize(env, event_target, event_loop, protocols)
        @env          = env
        @event_target = event_target
        @event_loop   = event_loop

        @url = ClientSocket.determine_url(@env)

        @driver = @driver_started = nil
        @close_params = ["", 1006]

        @ready_state = CONNECTING

        # The driver calls +env+, +url+, and +write+
        @driver = ::WebSocket::Driver.rack(self, protocols: protocols)

        @driver.on(:open)    { |e| open }
        @driver.on(:message) { |e| receive_message(e.data) }
        @driver.on(:close)   { |e| begin_close(e.reason, e.code) }
        @driver.on(:error)   { |e| emit_error(e.message) }

        @stream = ActionCable::Connection::Stream.new(@event_loop, self)
      end

    end
  end
end

上面的代码可以看到driver使用了 on message回调处理接受到的请求,其中receive_message方法是在ClientSocket类中将接受到的数据传送给MessageBuffer,数据缓冲完成后最好将交由Connection类调用线程池中的线程处理数据。

接受数据的方法其实不在websocket-driver中处理,是由我们上面讲到的nio4r读取i/o对象传送的流数据然后通过driver.parse方法传入websocket-driver进行websocket协议的相关处理最后转换数据。

最后

上面介绍了三个底层库分别被使用在ActionCable中的各个组件当中,下面的顺序图就是这些组件的调用流程。

  • websocket请求启动流程:
image.png
  • websocket接受数据处理流程:
image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,141评论 5 461
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,101评论 2 372
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,318评论 0 321
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,357评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,205评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,194评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,595评论 3 384
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,285评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,578评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,642评论 2 311
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,402评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,266评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,641评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,933评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,220评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,591评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,780评论 2 335

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,507评论 18 399
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,095评论 25 707
  • 这是今天一个分享课的讨论主题,如果没有做楷妈,我现在会是什么状态... 在二胎还没出来之前我就设想过辞职在...
    莫奈小姐阅读 3,544评论 0 0