Eventmachine Websocket 實戰

Post on 19-May-2015

4.840 views 7 download

description

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

Transcript of Eventmachine Websocket 實戰

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.0Client Server

open

close

open

close

HTTP 1.1Client Server

open

close

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

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

連線池

特性• 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端

流程onopen

Server

onopen

Client

onmessage

onmessage

start

send

closeonclose

onclose

•invoke to every clients in room •iteration

這樣就結束了?

案情有這麼單純嗎?

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(後⾯面說明)

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

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

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

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

典型連線⽅方式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_1-f938455d50b0228fee500f1a15b8bc2f42974a0e38a1e71c

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

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

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端

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

• 最早開始⽀支援

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

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

主要差異• 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

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

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

Multi-Process issue

Worker A

Worker B

Worker C

Massive Clients

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

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

問題

Master-Workers (fork model)

Worker A

Worker B

Worker C

Master fork

fork

fork

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對各⾃自所屬連線發出訓息

程序說明

Worker AWorker BWorker C

Master

Massive Clients A Client

1

23

44 4

傳統⾏行程間傳遞訊息的⽅方式 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