文本根據ActionCable 5.1.0版本的代碼進行講解。
ActionCable可以在Rails5中實現集成WebSocket通訊功能。其實都得益于它所依賴的三個第三方庫:websocket-driver nio4r concurrent-ruby 其中websocket-driver 負責Websocket通訊,nio4r處理IO,concurrent-ruby 處理并發worker。
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請求啟動流程:
- websocket接受數據處理流程: