• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
 

Eventmachine Websocket 實戰

on

  • 3,340 views

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

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

Statistics

Views

Total Views
3,340
Views on SlideShare
2,385
Embed Views
955

Actions

Likes
13
Downloads
20
Comments
3

5 Embeds 955

http://rubytaiwan.tumblr.com 930
http://outcircle.com 12
http://feedly.com 6
http://feeds.feedburner.com 6
http://www.linkedin.com 1

Accessibility

Upload Details

Uploaded via as Adobe PDF

Usage Rights

CC Attribution License

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel

13 of 3 previous next Post a comment

  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

    Eventmachine Websocket 實戰 Eventmachine Websocket 實戰 Presentation Transcript

    • Eventmachine Websocket實戰 慕凡@Ruby Tuesday-28 2014/1/7
    • 講者⾃自介 • Tomlan Workshop(http://tomlan.tw)創辦⼈人 • RubyConf Taiwan/Ruby Taiwan社群主辦⼈人 • Rails Girls Taiwan社群主辦⼈人 • Ruby經驗7年 • Github/Twitter: ryudoawaru • http://ryudo.tw
    • 主題 • 什麼是Websocket • Ruby的Websocket解決⽅方案介紹 • EventMachine::Websocket實戰 • Deploy平台介紹 • 多⾏行程伺服器實作
    • HTTP 1.0 Client open close open close Server
    • HTTP 1.1 Client open close Server
    • http特性 • 無狀態(stateless) • 每次request間無關聯 • 單向連線 • 從client端發request, 單向
    • 不適⽤用場合 • 頻率很⾼高, 但每次response都很⼩小 • 需要由server主動push的場合
    • 以前的解決⽅方案
    • Client Interval Polling 都2014年了,你不覺得這樣很 ____ 嗎? (by 製了2年杖的⼈人)
    • Long-Polling • • • 最⾼高相容性 ⻑⾧長時間連線容易佔⽤用Server端資源 衍⽣生物 • • Rails 4 Live Streaming(Server sent Events) Comet
    • Flash Socket們 • 沒有flash不能⽤用→iOS GG • SSL相容性問題 • server端通常需要依賴3rd party元件 • 地雷機率⾼高 • http://juggernaut.rubyforge.org/ →被炸過 • 可是瑞凡我不會寫flash
    • Websocket (RFC6455)
    • 真‧Server Push
    • W3C規範 (⺫⽬目前版本Draft20發展中) http://www.w3.org/TR/2012/CR-websockets-20120920/
    • 廣泛的瀏覽器⽀支援 (除了IE, 但是有相容解決⽅方案)
    • 有SSL (雖然我沒試過)
    • 幾乎所有語⾔言/平台都有⾄至 少Client端實作
    • EventMachine ⼀一個基於Reactor設計模式的、⽤用於網絡編程和並發 編程的框架
    • EventMachine • 事件驅動式 • 延遲執⾏行 • 抽象Thread或Fiber • 抽象socket處理 • 通⽤用CRuby/JRuby(我沒試過)
    • EventMachine::Websocket
    • Why EventMachine
    • C10K issue https://github.com/shokai/sinatra-websocketio/wiki/ C10K
    • 經實測證明,普通的 實體伺服器上單⼀一⾏行 程可負擔超過10k的同 時連線 (請按上⾴頁連結指⽰示服⽤用,請勿在MacOS試)
    • EM:Websocket實戰
    • Hello Websocket!
    • 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
    • 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 連線池
    • 特性 • Client/Server端具備相同的事件 • 原⽣生Javascript物件⽀支援 • 事件驅動
    • 事件 名稱 作⽤用 C/S端 onopen 連接成功時 共通 onmessage 接收到訊息時 共通 onclose 連線關閉 共通 onerror 發⽣生錯誤時 Server only
    • methods of WS object • send(msg) • 送訊息 • ping • 測試對⽅方⽣生存狀況 • close • 關閉連線
    • handshake object on open • 相當於⼀一般http的request object • 有path/host/user-agent/cookie等內容 • 根據瀏覽器/draft版本不同會有些許差異
    • 連接流程說明 1. 從http端得到html和javascript 2. Javascript發起WS連線 3. 連線成功 4. Client端送訊息給WS端 5. WS端收到後把訊息送給所有Client端
    • 流程 Server Client onopen start onopen onmessage •invoke to every clients in room •iteration send onmessage onclose close onclose
    • 這樣就結束了?
    • 案情有這麼單純嗎?
    • Production issues • IE issue • authentication • channel identification • non-block message passing
    • IE問題 • Flash Websocket • https://github.com/gimite/web-socket-js • 完全相容所有原⽣生Javascript API • 需要policy file在port 843, EM已內建 • Flash版本需>10 • 連線前置有點慢,⼀一次傳太多資料會卡
    • authentication issue • • 通常無法和http⼀一起掛在port 80(後⾯面說明) • • WS連線固定為GET,無法POST • 唯⼀一能安全地做⽂文章的地⽅方剩下WS連線的URL 即使掛在相同port,有的瀏覽器會視為corss domain不給cookie WS有提供連線時可選的 protocols參數,但不是 所有瀏覽器都⽀支援
    • 典型連線⽅方式 1. 連線http端時已驗證⾝身份 2. http端在render⾴頁⾯面時,提供特定的WS URL供連線⽤用,並在此時於後端告知WS 端URL和user⾝身份對應 3. client端使⽤用這個URL連線 4. WS端確認⾝身份,接受連線 5. 將該連線加到「連線池」內
    • ⽰示例-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
    • 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
    • non-block message passing
    • 由於單⼀一⾏行程要負擔過萬 連線,如何在傳遞訊息時 不阻塞其它處理變成最重 要的事項。
    • 場景:在收到某個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排⼊入 背景執⾏行
    • Reactor程式撰寫原則
    • 事件處理要⼩小不要卡
    • 會卡的事丟到 next_tick或defer
    • 會卡I/O的最好找有EM 包裝的相關實作
    • Architecture / Framework Issue
    • Websocket整合⽅方案分類
    • Websocket App⾓角⾊色 1. http frontend • 負責串接後端與client端的proxy⾓角⾊色 • 處理「髒連線」等任務 2. http app server • 處理普通的http 3. websocket app server • 主要差別在附屬在http內或是分開
    • 純websocket⾓角⾊色 • EM:Websocket • faye-websocket
    • 整合現有http framework • Sinatra-Websocket • Rack-Websocket • Goliath • Cramp
    • 前後端整合 • 除了web framework外,封裝包括 Javascript • Websocket-Rails • Rocket-IO
    • 你該選擇哪⼀一種?
    • 考量點 • scalability • 套件更新頻率與熱⾨門度 • 套件和app server的綁定問題
    • scalability • http和WS是否住在同⼀一個⾏行程? • 是否可依需要分別調整WS或http端的⾏行 程數
    • 套件熱⾨門度 • WS在Ruby圈不是主流 • 除了EM以外的套件⼤大都更新緩慢或是學 術研究性質產品
    • app server綁定 • EM:其實⾃自⼰己就是App server • 多數會綁定thin • ex:sinatra-websocket • 有的會⾃自⼰己另外⽤用EM產⽣生WS專⽤用⾏行程 • ex:Rocket-IO
    • Production Environment Deployment
    • Http Frontend Proxy
    • 為何需要 • Backend Load Balance • app server未必能處理request buffer / dirty connection等問題
    • ⺫⽬目前主流 • nginx • 注意:要1.4版以上 • haproxy • 最早開始⽀支援
    • 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; } }
    • 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
    • 主要差異 • nginx無法在同⼀一vhost & location內分別 reverse proxy http & WS - 其實可以⽤用if辨認header達成,但是會有ifisEvil 問題(http://wiki.nginx.org/IfIsEvil) • haproxy無法做為http file server,等於是 多⼀一層
    • Port issue • 80 port • transparent proxy issue • 443 port • 最安全,幾乎不會被擋,也不會被 proxy • Port > 1000 • 有機會被client端防⽕火牆檔
    • 結論 • 即使和http在同vhost & port,由於cookie 不⼀一定能被瀏覽器認可,故不需強求 • nginx / haproxy 的穩定性都很好,視需求 與現有環境狀況決定 • listen在443也不⼀一定要有SSL,所以443最 安全
    • 同場加映: Websocket Load Balancer
    • 如果事業做很⼤大 同時連線達數萬?
    • 解決之道 • Multi-Process • • fork ⼀一個⼀一個開 • • Ruby有GIL/GVL,無法使⽤用超過⼀一個核⼼心 • Multi-Thread JRuby OK!
    • Multi-Process issue Worker A Worker B Worker C Massive Clients
    • 問題 • 每個⾏行程有⾃自⼰己的連線池 • 每個⾏行程不知道其它⾏行程的連線 • 當⾏行程A收到訊息,B/C不會知道
    • Master-Workers (fork model) fork Master fork Worker A Worker B fork Worker C
    • fork model • for Unix Like OS only • 由⽗父⾏行程fork出⼦子⾏行程 • ⼦子⾏行程有和⽗父⾏行程⼀一樣的記憶體內容 • ⾏行程間的記憶體不互通 • ⾏行程間共享fork前已開啟的IO • 由Copy on Write⽅方式節省未變更記憶體 使⽤用(Ruby 2.0+ feature)
    • onmessage流程變更 1. Client傳訊給Woker A,Worker A收到訊息 2. Worker A通知Master有新訊息 3. Master通知所有Workers 4. Workers對各⾃自所屬連線發出訓息
    • 程序說明 Master 3 Worker C 4 Worker B 4 2 Worker A 1 4 Massive Clients A Client
    • 傳統⾏行程間傳遞訊息的⽅方式 in unix like system • pipe • unix socket • shared memory
    • 以上都不是事件驅動 式,撰寫不易
    • 解法1:Websocket on Websocket
    • 解法2.Redis Pub-Sub • 頻道 (Channel) 和訂閱者(Subscriber)的概念 • 和Websocket本質類似,但更⽅方便使⽤用 • 訂閱單位為頻道 • ⼀一旦有⼈人向頻道發出訊息(Publish),訂閱者 會收到通知
    • 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
    • 訂閱⽅方式 • Master頻道 • 訂閱者:Master • 由Worker發訊息通知Master • Child頻道 • 訂閱者:Workers • Master由Master頻道收到訊息後,從此 處發給Worker們
    • 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
    • 要點 • Publisher需要⽤用普通Redis Client • Subscriber要⽤用EM::HiRedis Client • Master頻道訂閱需在fork之後 • Child頻道需要在fork內訂閱
    • End http://ryudo.tw