Eventmachine Websocket 實戰

  • 3,981 views
Uploaded on

簡介如何製作EventMachine的Websocket程式與注意事項

簡介如何製作EventMachine的Websocket程式與注意事項

More in: Technology , Design
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
No Downloads

Views

Total Views
3,981
On Slideshare
0
From Embeds
0
Number of Embeds
5

Actions

Shares
Downloads
25
Comments
3
Likes
17

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. Eventmachine Websocket實戰 慕凡@Ruby Tuesday-28 2014/1/7
  • 2. 講者⾃自介 • Tomlan Workshop(http://tomlan.tw)創辦⼈人 • RubyConf Taiwan/Ruby Taiwan社群主辦⼈人 • Rails Girls Taiwan社群主辦⼈人 • Ruby經驗7年 • Github/Twitter: ryudoawaru • http://ryudo.tw
  • 3. 主題 • 什麼是Websocket • Ruby的Websocket解決⽅方案介紹 • EventMachine::Websocket實戰 • Deploy平台介紹 • 多⾏行程伺服器實作
  • 4. HTTP 1.0 Client open close open close Server
  • 5. HTTP 1.1 Client open close Server
  • 6. http特性 • 無狀態(stateless) • 每次request間無關聯 • 單向連線 • 從client端發request, 單向
  • 7. 不適⽤用場合 • 頻率很⾼高, 但每次response都很⼩小 • 需要由server主動push的場合
  • 8. 以前的解決⽅方案
  • 9. Client Interval Polling 都2014年了,你不覺得這樣很 ____ 嗎? (by 製了2年杖的⼈人)
  • 10. Long-Polling • • • 最⾼高相容性 ⻑⾧長時間連線容易佔⽤用Server端資源 衍⽣生物 • • Rails 4 Live Streaming(Server sent Events) Comet
  • 11. Flash Socket們 • 沒有flash不能⽤用→iOS GG • SSL相容性問題 • server端通常需要依賴3rd party元件 • 地雷機率⾼高 • http://juggernaut.rubyforge.org/ →被炸過 • 可是瑞凡我不會寫flash
  • 12. Websocket (RFC6455)
  • 13. 真‧Server Push
  • 14. W3C規範 (⺫⽬目前版本Draft20發展中) http://www.w3.org/TR/2012/CR-websockets-20120920/
  • 15. 廣泛的瀏覽器⽀支援 (除了IE, 但是有相容解決⽅方案)
  • 16. 有SSL (雖然我沒試過)
  • 17. 幾乎所有語⾔言/平台都有⾄至 少Client端實作
  • 18. EventMachine ⼀一個基於Reactor設計模式的、⽤用於網絡編程和並發 編程的框架
  • 19. EventMachine • 事件驅動式 • 延遲執⾏行 • 抽象Thread或Fiber • 抽象socket處理 • 通⽤用CRuby/JRuby(我沒試過)
  • 20. EventMachine::Websocket
  • 21. Why EventMachine
  • 22. C10K issue https://github.com/shokai/sinatra-websocketio/wiki/ C10K
  • 23. 經實測證明,普通的 實體伺服器上單⼀一⾏行 程可負擔超過10k的同 時連線 (請按上⾴頁連結指⽰示服⽤用,請勿在MacOS試)
  • 24. EM:Websocket實戰
  • 25. Hello Websocket!
  • 26. Client <html> <head> <script src="http://code.jquery.com/jquery-1.10.2.js"></script> <script> $(document).ready(function(){ ws = new WebSocket("ws://localhost:28080"); ws.onmessage = function(evt) { $("#msg").append("<p> NEW message: "+evt.data+"</p>"); }; ws.onclose = function() { console.log("socket closed"); }; ws.onopen = function() { console.log("connected..."); ws.send("hello server"); }; }); </script> </head> <body> <div id="debug"></div> <div id="msg"></div> </body> </html> WS URL
  • 27. Server require 'em-websocket' trap(:INT){EM.stop} EM.run do connections = [] EM::WebSocket.run(:host => "0.0.0.0", :port => 28080) do |ws| ws.onopen do |handshake| puts "New connection" connections << ws end ws.onclose do connections.delete(ws) end ws.onmessage do |msg| puts "Message coming: #{msg}" connections.each{|conn| conn.send(msg) } end end end 連線池
  • 28. 特性 • Client/Server端具備相同的事件 • 原⽣生Javascript物件⽀支援 • 事件驅動
  • 29. 事件 名稱 作⽤用 C/S端 onopen 連接成功時 共通 onmessage 接收到訊息時 共通 onclose 連線關閉 共通 onerror 發⽣生錯誤時 Server only
  • 30. methods of WS object • send(msg) • 送訊息 • ping • 測試對⽅方⽣生存狀況 • close • 關閉連線
  • 31. handshake object on open • 相當於⼀一般http的request object • 有path/host/user-agent/cookie等內容 • 根據瀏覽器/draft版本不同會有些許差異
  • 32. 連接流程說明 1. 從http端得到html和javascript 2. Javascript發起WS連線 3. 連線成功 4. Client端送訊息給WS端 5. WS端收到後把訊息送給所有Client端
  • 33. 流程 Server Client onopen start onopen onmessage •invoke to every clients in room •iteration send onmessage onclose close onclose
  • 34. 這樣就結束了?
  • 35. 案情有這麼單純嗎?
  • 36. Production issues • IE issue • authentication • channel identification • non-block message passing
  • 37. IE問題 • Flash Websocket • https://github.com/gimite/web-socket-js • 完全相容所有原⽣生Javascript API • 需要policy file在port 843, EM已內建 • Flash版本需>10 • 連線前置有點慢,⼀一次傳太多資料會卡
  • 38. authentication issue • • 通常無法和http⼀一起掛在port 80(後⾯面說明) • • WS連線固定為GET,無法POST • 唯⼀一能安全地做⽂文章的地⽅方剩下WS連線的URL 即使掛在相同port,有的瀏覽器會視為corss domain不給cookie WS有提供連線時可選的 protocols參數,但不是 所有瀏覽器都⽀支援
  • 39. 典型連線⽅方式 1. 連線http端時已驗證⾝身份 2. http端在render⾴頁⾯面時,提供特定的WS URL供連線⽤用,並在此時於後端告知WS 端URL和user⾝身份對應 3. client端使⽤用這個URL連線 4. WS端確認⾝身份,接受連線 5. 將該連線加到「連線池」內
  • 40. ⽰示例-http端 get '/rooms/:room_name.html' do @ws_url = gen_ws_url ........... erb "index.html".to_sym end def gen_ws_url hkey = SecureRandom.hex(24) #亂數產⽣生URL後綴 #在REDIS中設定URL對應USER redis.hsetnx("keys-#{current_room.db_name}".to_sym, hkey, current_member.uid) sprintf 'ws://%s:%d%s', request.host, params[:ws_port], "/update-service/ #{current_room.db_name}-#{hkey}" end #產⽣生:ws://hostanme:port/update-service/group_item_1f938455d50b0228fee500f1a15b8bc2f42974a0e38a1e71c
  • 41. WS端 #GET /update-service/group_item_1-f938455d50b0228fee500f1a15b8bc2f42974a0e38a1e71c EventMachine::WebSocket.start(:host => options[:host], :port => 8080) do |ws| ws.onopen do |request| if request.path =~ //update-service/(.+)-(.+)/ #$1 為 current_room = Room.where(:db_name => $1).first rhkey = "keys-#{current_room.db_name}".to_sym #由之前設好的Redis Hash中由正確的key對應拉出UID, ⾝身份辨認完成 current_member = Discuz::Member.find_by_uid(redis.hget(rhkey,$2)) unless current_member ws.close end redis.hdel rhkey, params[:hkey] #⽴立刻刪key以避免被重複使⽤用 $connections << ws #將連線加到connections pool內 ws.close end end end
  • 42. non-block message passing
  • 43. 由於單⼀一⾏行程要負擔過萬 連線,如何在傳遞訊息時 不阻塞其它處理變成最重 要的事項。
  • 44. 場景:在收到某個client端的訊息後, 將該訊息傳給所有client端 ws.onmessage do |ws_msg| msg = current_room.messages.create(JSON.parse(ws_msg)) EM.next_tick do $connections.each do |conn| conn.send(render_messages([msg])) end end end EM.next_tick將send排⼊入 背景執⾏行
  • 45. Reactor程式撰寫原則
  • 46. 事件處理要⼩小不要卡
  • 47. 會卡的事丟到 next_tick或defer
  • 48. 會卡I/O的最好找有EM 包裝的相關實作
  • 49. Architecture / Framework Issue
  • 50. Websocket整合⽅方案分類
  • 51. Websocket App⾓角⾊色 1. http frontend • 負責串接後端與client端的proxy⾓角⾊色 • 處理「髒連線」等任務 2. http app server • 處理普通的http 3. websocket app server • 主要差別在附屬在http內或是分開
  • 52. 純websocket⾓角⾊色 • EM:Websocket • faye-websocket
  • 53. 整合現有http framework • Sinatra-Websocket • Rack-Websocket • Goliath • Cramp
  • 54. 前後端整合 • 除了web framework外,封裝包括 Javascript • Websocket-Rails • Rocket-IO
  • 55. 你該選擇哪⼀一種?
  • 56. 考量點 • scalability • 套件更新頻率與熱⾨門度 • 套件和app server的綁定問題
  • 57. scalability • http和WS是否住在同⼀一個⾏行程? • 是否可依需要分別調整WS或http端的⾏行 程數
  • 58. 套件熱⾨門度 • WS在Ruby圈不是主流 • 除了EM以外的套件⼤大都更新緩慢或是學 術研究性質產品
  • 59. app server綁定 • EM:其實⾃自⼰己就是App server • 多數會綁定thin • ex:sinatra-websocket • 有的會⾃自⼰己另外⽤用EM產⽣生WS專⽤用⾏行程 • ex:Rocket-IO
  • 60. Production Environment Deployment
  • 61. Http Frontend Proxy
  • 62. 為何需要 • Backend Load Balance • app server未必能處理request buffer / dirty connection等問題
  • 63. ⺫⽬目前主流 • nginx • 注意:要1.4版以上 • haproxy • 最早開始⽀支援
  • 64. nginx server { server_name xxx.tw; listen 443; location / { access_log logs/xxx-ws-access.log; proxy_pass http://localhost:12850; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_connect_timeout 30; proxy_read_timeout 600; proxy_send_timeout 600; } }
  • 65. haproxy frontend public bind *:443 timeout client 1800s acl is_websocket hdr(Upgrade) -i WebSocket use_backend ws if is_websocket default_backend www ! backend www option forwardfor timeout server 30s server ws1 127.0.0.1 ! backend ws option forwardfor timeout server 1800s server ws2 127.0.0.1:12850
  • 66. 主要差異 • nginx無法在同⼀一vhost & location內分別 reverse proxy http & WS - 其實可以⽤用if辨認header達成,但是會有ifisEvil 問題(http://wiki.nginx.org/IfIsEvil) • haproxy無法做為http file server,等於是 多⼀一層
  • 67. Port issue • 80 port • transparent proxy issue • 443 port • 最安全,幾乎不會被擋,也不會被 proxy • Port > 1000 • 有機會被client端防⽕火牆檔
  • 68. 結論 • 即使和http在同vhost & port,由於cookie 不⼀一定能被瀏覽器認可,故不需強求 • nginx / haproxy 的穩定性都很好,視需求 與現有環境狀況決定 • listen在443也不⼀一定要有SSL,所以443最 安全
  • 69. 同場加映: Websocket Load Balancer
  • 70. 如果事業做很⼤大 同時連線達數萬?
  • 71. 解決之道 • Multi-Process • • fork ⼀一個⼀一個開 • • Ruby有GIL/GVL,無法使⽤用超過⼀一個核⼼心 • Multi-Thread JRuby OK!
  • 72. Multi-Process issue Worker A Worker B Worker C Massive Clients
  • 73. 問題 • 每個⾏行程有⾃自⼰己的連線池 • 每個⾏行程不知道其它⾏行程的連線 • 當⾏行程A收到訊息,B/C不會知道
  • 74. Master-Workers (fork model) fork Master fork Worker A Worker B fork Worker C
  • 75. fork model • for Unix Like OS only • 由⽗父⾏行程fork出⼦子⾏行程 • ⼦子⾏行程有和⽗父⾏行程⼀一樣的記憶體內容 • ⾏行程間的記憶體不互通 • ⾏行程間共享fork前已開啟的IO • 由Copy on Write⽅方式節省未變更記憶體 使⽤用(Ruby 2.0+ feature)
  • 76. onmessage流程變更 1. Client傳訊給Woker A,Worker A收到訊息 2. Worker A通知Master有新訊息 3. Master通知所有Workers 4. Workers對各⾃自所屬連線發出訓息
  • 77. 程序說明 Master 3 Worker C 4 Worker B 4 2 Worker A 1 4 Massive Clients A Client
  • 78. 傳統⾏行程間傳遞訊息的⽅方式 in unix like system • pipe • unix socket • shared memory
  • 79. 以上都不是事件驅動 式,撰寫不易
  • 80. 解法1:Websocket on Websocket
  • 81. 解法2.Redis Pub-Sub • 頻道 (Channel) 和訂閱者(Subscriber)的概念 • 和Websocket本質類似,但更⽅方便使⽤用 • 訂閱單位為頻道 • ⼀一旦有⼈人向頻道發出訊息(Publish),訂閱者 會收到通知
  • 82. Example #Publisher $redis = Redis.new data = {"user" => ARGV[1]} loop do msg = STDIN.gets $redis.publish ARGV[0], data.merge('msg' => msg.strip).to_json end #Subscriber $redis = Redis.new(:timeout => 0) $redis.subscribe('rubyonrails', 'ruby-lang') do |on| puts "開始subscribe" on.message do |channel, msg| data = JSON.parse(msg) puts "##{channel} - [#{data['user']}]: #{data['msg']}" end end
  • 83. 訂閱⽅方式 • Master頻道 • 訂閱者:Master • 由Worker發訊息通知Master • Child頻道 • 訂閱者:Workers • Master由Master頻道收到訊息後,從此 處發給Worker們
  • 84. r2p = Redis.new #Publisher⽤用連線 EventMachine.run do emredis = EM::Hiredis.connect# Subscriber⽤用 pids = 2.times do |pi| pid = fork do#############FORK START##################### EventMachine::WebSocket.start(.....) do |ws| ws.onopen do |request| ... ws.onmessage do |ws_msg| #1. Client傳訊給worker msg = current_room.messages.create(...) #2. 向master channel通知有新訊息 r2p.publish 'master', msg.id.to_s end emredis.pubsub.subscribe('child') do |mid|/ msg = Message.find(mid) EM.next_tick do#4. worker們得到master傳來的新訊息,傳給⾃自⼰己的clients $connections[current_room.db_name.to_sym].each do |s| s.send(render_messages([msg])) end end end end end end#############FORK END##################### end emredis.pubsub.subscribe('master') do |msg| r2p.publish('child', msg)#3. MASTER得到訊息,通知workers end end
  • 85. 要點 • Publisher需要⽤用普通Redis Client • Subscriber要⽤用EM::HiRedis Client • Master頻道訂閱需在fork之後 • Child頻道需要在fork內訂閱
  • 86. End http://ryudo.tw