支撐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閱讀 227,250評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,923評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,041評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,475評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,253評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,801評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,023評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,530評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,494評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,639評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,890評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,289評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,552評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,242評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,626評論 2 370

推薦閱讀更多精彩內容

  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,704評論 18 399
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,574評論 25 707
  • 這是今天一個分享課的討論主題,如果沒有做楷媽,我現在會是什么狀態... 在二胎還沒出來之前我就設想過辭職在...
    莫奈小姐閱讀 3,577評論 0 0