eventmachine websocket 實戰

87
Eventmachine Websocket實戰 慕凡@Ruby Tuesday-28 2014/1/7

Upload: mu-fan-teng

Post on 19-May-2015

4.851 views

Category:

Technology


7 download

DESCRIPTION

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

TRANSCRIPT

Page 1: Eventmachine Websocket 實戰

Eventmachine Websocket實戰

慕凡@Ruby Tuesday-28 2014/1/7

Page 2: Eventmachine Websocket 實戰

講者⾃自介• Tomlan Workshop(http://tomlan.tw)創辦⼈人

• RubyConf Taiwan/Ruby Taiwan社群主辦⼈人

• Rails Girls Taiwan社群主辦⼈人

• Ruby經驗7年

• Github/Twitter: ryudoawaru

• http://ryudo.tw

Page 3: Eventmachine Websocket 實戰

主題• 什麼是Websocket

• Ruby的Websocket解決⽅方案介紹

• EventMachine::Websocket實戰

• Deploy平台介紹

• 多⾏行程伺服器實作

Page 4: Eventmachine Websocket 實戰

HTTP 1.0Client Server

open

close

open

close

Page 5: Eventmachine Websocket 實戰

HTTP 1.1Client Server

open

close

Page 6: Eventmachine Websocket 實戰

http特性• 無狀態(stateless)

• 每次request間無關聯

• 單向連線

• 從client端發request, 單向

Page 7: Eventmachine Websocket 實戰

不適⽤用場合• 頻率很⾼高, 但每次response都很⼩小

• 需要由server主動push的場合

Page 8: Eventmachine Websocket 實戰

以前的解決⽅方案

Page 9: Eventmachine Websocket 實戰

Client Interval Polling都2014年了,你不覺得這樣很 ____ 嗎?

(by 製了2年杖的⼈人)

Page 10: Eventmachine Websocket 實戰

Long-Polling• 最⾼高相容性

• ⻑⾧長時間連線容易佔⽤用Server端資源

• 衍⽣生物

• Rails 4 Live Streaming(Server sent Events)

• Comet

Page 11: Eventmachine Websocket 實戰

Flash Socket們• 沒有flash不能⽤用→iOS GG

• SSL相容性問題

• server端通常需要依賴3rd party元件

• 地雷機率⾼高 • http://juggernaut.rubyforge.org/ →被炸過

• 可是瑞凡我不會寫flash

Page 12: Eventmachine Websocket 實戰

Websocket (RFC6455)

Page 13: Eventmachine Websocket 實戰

真‧Server Push

Page 14: Eventmachine Websocket 實戰

W3C規範 (⺫⽬目前版本Draft20發展中)

http://www.w3.org/TR/2012/CR-websockets-20120920/

Page 15: Eventmachine Websocket 實戰

廣泛的瀏覽器⽀支援 (除了IE, 但是有相容解決⽅方案)

Page 16: Eventmachine Websocket 實戰

有SSL (雖然我沒試過)

Page 17: Eventmachine Websocket 實戰

幾乎所有語⾔言/平台都有⾄至少Client端實作

Page 18: Eventmachine Websocket 實戰

EventMachine ⼀一個基於Reactor設計模式的、⽤用於網絡編程和並發

編程的框架

Page 19: Eventmachine Websocket 實戰

EventMachine

• 事件驅動式 • 延遲執⾏行

• 抽象Thread或Fiber

• 抽象socket處理

• 通⽤用CRuby/JRuby(我沒試過)

Page 20: Eventmachine Websocket 實戰

EventMachine::Websocket

Page 21: Eventmachine Websocket 實戰

Why EventMachine

Page 22: Eventmachine Websocket 實戰

C10K issue https://github.com/shokai/sinatra-websocketio/wiki/

C10K

Page 23: Eventmachine Websocket 實戰

經實測證明,普通的實體伺服器上單⼀一⾏行程可負擔超過10k的同

時連線 (請按上⾴頁連結指⽰示服⽤用,請勿在MacOS試)

Page 24: Eventmachine Websocket 實戰

EM:Websocket實戰

Page 25: Eventmachine Websocket 實戰

Hello Websocket!

Page 26: Eventmachine 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

Page 27: Eventmachine Websocket 實戰
Page 28: Eventmachine Websocket 實戰

Serverrequire '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

連線池

Page 29: Eventmachine Websocket 實戰

特性• Client/Server端具備相同的事件

• 原⽣生Javascript物件⽀支援

• 事件驅動

Page 30: Eventmachine Websocket 實戰

事件名稱 作⽤用 C/S端

onopen 連接成功時 共通

onmessage 接收到訊息時 共通

onclose 連線關閉 共通

onerror 發⽣生錯誤時 Server only

Page 31: Eventmachine Websocket 實戰

methods of WS object

• send(msg)

• 送訊息

• ping

• 測試對⽅方⽣生存狀況

• close

• 關閉連線

Page 32: Eventmachine Websocket 實戰

handshake object on open

• 相當於⼀一般http的request object

• 有path/host/user-agent/cookie等內容

• 根據瀏覽器/draft版本不同會有些許差異

Page 33: Eventmachine Websocket 實戰

連接流程說明1. 從http端得到html和javascript

2. Javascript發起WS連線 3. 連線成功 4. Client端送訊息給WS端 5. WS端收到後把訊息送給所有Client端

Page 34: Eventmachine Websocket 實戰

流程onopen

Server

onopen

Client

onmessage

onmessage

start

send

closeonclose

onclose

•invoke to every clients in room •iteration

Page 35: Eventmachine Websocket 實戰

這樣就結束了?

Page 36: Eventmachine Websocket 實戰

案情有這麼單純嗎?

Page 37: Eventmachine Websocket 實戰

Production issues

• IE issue

• authentication

• channel identification

• non-block message passing

Page 38: Eventmachine Websocket 實戰

IE問題• Flash Websocket

• https://github.com/gimite/web-socket-js

• 完全相容所有原⽣生Javascript API

• 需要policy file在port 843, EM已內建

• Flash版本需>10

• 連線前置有點慢,⼀一次傳太多資料會卡

Page 39: Eventmachine Websocket 實戰

authentication issue• 通常無法和http⼀一起掛在port 80(後⾯面說明)

• 即使掛在相同port,有的瀏覽器會視為corss domain不給cookie

• WS連線固定為GET,無法POST

• WS有提供連線時可選的 protocols參數,但不是所有瀏覽器都⽀支援

• 唯⼀一能安全地做⽂文章的地⽅方剩下WS連線的URL

Page 40: Eventmachine Websocket 實戰

典型連線⽅方式1. 連線http端時已驗證⾝身份 2. http端在render⾴頁⾯面時,提供特定的WS

URL供連線⽤用,並在此時於後端告知WS端URL和user⾝身份對應

3. client端使⽤用這個URL連線 4. WS端確認⾝身份,接受連線 5. 將該連線加到「連線池」內

Page 41: Eventmachine Websocket 實戰

⽰示例-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_1-f938455d50b0228fee500f1a15b8bc2f42974a0e38a1e71c

Page 42: Eventmachine Websocket 實戰

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

Page 43: Eventmachine Websocket 實戰

non-block message passing

Page 44: Eventmachine Websocket 實戰

由於單⼀一⾏行程要負擔過萬連線,如何在傳遞訊息時不阻塞其它處理變成最重要的事項。

Page 45: Eventmachine Websocket 實戰

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排⼊入背景執⾏行

場景:在收到某個client端的訊息後,將該訊息傳給所有client端

Page 46: Eventmachine Websocket 實戰

Reactor程式撰寫原則

Page 47: Eventmachine Websocket 實戰

事件處理要⼩小不要卡

Page 48: Eventmachine Websocket 實戰

會卡的事丟到next_tick或defer

Page 49: Eventmachine Websocket 實戰

會卡I/O的最好找有EM包裝的相關實作

Page 50: Eventmachine Websocket 實戰

Architecture / Framework Issue

Page 51: Eventmachine Websocket 實戰

Websocket整合⽅方案分類

Page 52: Eventmachine Websocket 實戰

Websocket App⾓角⾊色1. http frontend

• 負責串接後端與client端的proxy⾓角⾊色 • 處理「髒連線」等任務

2. http app server

• 處理普通的http

3. websocket app server

• 主要差別在附屬在http內或是分開

Page 53: Eventmachine Websocket 實戰

純websocket⾓角⾊色• EM:Websocket

• faye-websocket

Page 54: Eventmachine Websocket 實戰

整合現有http framework

• Sinatra-Websocket

• Rack-Websocket

• Goliath

• Cramp

Page 55: Eventmachine Websocket 實戰

前後端整合• 除了web framework外,封裝包括

Javascript

• Websocket-Rails

• Rocket-IO

Page 56: Eventmachine Websocket 實戰

你該選擇哪⼀一種?

Page 57: Eventmachine Websocket 實戰

考量點• scalability

• 套件更新頻率與熱⾨門度

• 套件和app server的綁定問題

Page 58: Eventmachine Websocket 實戰

scalability

• http和WS是否住在同⼀一個⾏行程?

• 是否可依需要分別調整WS或http端的⾏行程數

Page 59: Eventmachine Websocket 實戰

套件熱⾨門度• WS在Ruby圈不是主流

• 除了EM以外的套件⼤大都更新緩慢或是學術研究性質產品

Page 60: Eventmachine Websocket 實戰

app server綁定• EM:其實⾃自⼰己就是App server

• 多數會綁定thin

• ex:sinatra-websocket

• 有的會⾃自⼰己另外⽤用EM產⽣生WS專⽤用⾏行程

• ex:Rocket-IO

Page 61: Eventmachine Websocket 實戰

Production Environment Deployment

Page 62: Eventmachine Websocket 實戰

Http Frontend Proxy

Page 63: Eventmachine Websocket 實戰

為何需要• Backend Load Balance

• app server未必能處理request buffer / dirty connection等問題

Page 64: Eventmachine Websocket 實戰

⺫⽬目前主流• nginx

• 注意:要1.4版以上

• haproxy

• 最早開始⽀支援

Page 65: Eventmachine Websocket 實戰

nginxserver { 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; } }

Page 66: Eventmachine Websocket 實戰

haproxyfrontend 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

Page 67: Eventmachine Websocket 實戰

主要差異• nginx無法在同⼀一vhost & location內分別

reverse proxy http & WS

- 其實可以⽤用if辨認header達成,但是會有ifisEvil問題(http://wiki.nginx.org/IfIsEvil)

• haproxy無法做為http file server,等於是多⼀一層

Page 68: Eventmachine Websocket 實戰

Port issue

• 80 port

• transparent proxy issue

• 443 port

• 最安全,幾乎不會被擋,也不會被proxy

• Port > 1000

• 有機會被client端防⽕火牆檔

Page 69: Eventmachine Websocket 實戰

結論• 即使和http在同vhost & port,由於cookie

不⼀一定能被瀏覽器認可,故不需強求

• nginx / haproxy 的穩定性都很好,視需求與現有環境狀況決定

• listen在443也不⼀一定要有SSL,所以443最安全

Page 70: Eventmachine Websocket 實戰

同場加映: Websocket Load

Balancer

Page 71: Eventmachine Websocket 實戰

如果事業做很⼤大 同時連線達數萬?

Page 72: Eventmachine Websocket 實戰

解決之道• Multi-Process

• fork

• ⼀一個⼀一個開 • Multi-Thread

• Ruby有GIL/GVL,無法使⽤用超過⼀一個核⼼心 • JRuby OK!

Page 73: Eventmachine Websocket 實戰

Multi-Process issue

Worker A

Worker B

Worker C

Massive Clients

Page 74: Eventmachine Websocket 實戰

• 每個⾏行程有⾃自⼰己的連線池 • 每個⾏行程不知道其它⾏行程的連線

• 當⾏行程A收到訊息,B/C不會知道

問題

Page 75: Eventmachine Websocket 實戰

Master-Workers (fork model)

Worker A

Worker B

Worker C

Master fork

fork

fork

Page 76: Eventmachine Websocket 實戰

fork model

• for Unix Like OS only

• 由⽗父⾏行程fork出⼦子⾏行程

• ⼦子⾏行程有和⽗父⾏行程⼀一樣的記憶體內容 • ⾏行程間的記憶體不互通

• ⾏行程間共享fork前已開啟的IO

• 由Copy on Write⽅方式節省未變更記憶體使⽤用(Ruby 2.0+ feature)

Page 77: Eventmachine Websocket 實戰

onmessage流程變更1. Client傳訊給Woker A,Worker A收到訊息 2. Worker A通知Master有新訊息 3. Master通知所有Workers

4. Workers對各⾃自所屬連線發出訓息

Page 78: Eventmachine Websocket 實戰

程序說明

Worker AWorker BWorker C

Master

Massive Clients A Client

1

23

44 4

Page 79: Eventmachine Websocket 實戰

傳統⾏行程間傳遞訊息的⽅方式 in unix like system

• pipe

• unix socket

• shared memory

Page 80: Eventmachine Websocket 實戰

以上都不是事件驅動式,撰寫不易

Page 81: Eventmachine Websocket 實戰

解法1:Websocket on Websocket

Page 82: Eventmachine Websocket 實戰

解法2.Redis Pub-Sub

• 頻道 (Channel) 和訂閱者(Subscriber)的概念

• 和Websocket本質類似,但更⽅方便使⽤用

• 訂閱單位為頻道

• ⼀一旦有⼈人向頻道發出訊息(Publish),訂閱者會收到通知

Page 83: Eventmachine Websocket 實戰

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

Page 84: Eventmachine Websocket 實戰

訂閱⽅方式• Master頻道

• 訂閱者:Master

• 由Worker發訊息通知Master

• Child頻道

• 訂閱者:Workers

• Master由Master頻道收到訊息後,從此處發給Worker們

Page 85: Eventmachine Websocket 實戰

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

Page 86: Eventmachine Websocket 實戰

要點• Publisher需要⽤用普通Redis Client

• Subscriber要⽤用EM::HiRedis Client

• Master頻道訂閱需在fork之後

• Child頻道需要在fork內訂閱

Page 87: Eventmachine Websocket 實戰

End http://ryudo.tw