dripdrop 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.0
1
+ 0.7.1
data/dripdrop.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{dripdrop}
8
- s.version = "0.6.0"
8
+ s.version = "0.7.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Andrew Cholakian"]
12
- s.date = %q{2010-12-17}
12
+ s.date = %q{2011-01-30}
13
13
  s.description = %q{Evented framework for ZeroMQ and EventMachine Apps. }
14
14
  s.email = %q{andrew@andrewvc.com}
15
15
  s.extra_rdoc_files = [
@@ -27,21 +27,14 @@ Gem::Specification.new do |s|
27
27
  "dripdrop.gemspec",
28
28
  "example/agent_test.rb",
29
29
  "example/combined.rb",
30
+ "example/complex/README",
31
+ "example/complex/client.rb",
32
+ "example/complex/server.rb",
33
+ "example/complex/service.rb",
34
+ "example/complex/websocket.rb",
30
35
  "example/http.rb",
31
36
  "example/pubsub.rb",
32
37
  "example/pushpull.rb",
33
- "example/stats_app/core.rb",
34
- "example/stats_app/public/.sass-cache/b48b4299d80c05f528daf63fe51d85e5e3c10d98/stats.scssc",
35
- "example/stats_app/public/backbone.js",
36
- "example/stats_app/public/build_templates.rb",
37
- "example/stats_app/public/json2.js",
38
- "example/stats_app/public/protovis-r3.2.js",
39
- "example/stats_app/public/stats.css",
40
- "example/stats_app/public/stats.haml",
41
- "example/stats_app/public/stats.html",
42
- "example/stats_app/public/stats.js",
43
- "example/stats_app/public/stats.scss",
44
- "example/stats_app/public/underscore.js",
45
38
  "example/subclass.rb",
46
39
  "example/xreq_xrep.rb",
47
40
  "js/dripdrop.html",
@@ -74,16 +67,16 @@ Gem::Specification.new do |s|
74
67
  s.rubygems_version = %q{1.3.7}
75
68
  s.summary = %q{Evented framework for ZeroMQ and EventMachine Apps.}
76
69
  s.test_files = [
77
- "spec/gimite-websocket.rb",
78
- "spec/message_spec.rb",
79
- "spec/node_spec.rb",
70
+ "spec/node_spec.rb",
80
71
  "spec/spec_helper.rb",
81
- "spec/node/http_spec.rb",
72
+ "spec/gimite-websocket.rb",
73
+ "spec/message_spec.rb",
82
74
  "spec/node/nodelet_spec.rb",
75
+ "spec/node/zmq_pushpull_spec.rb",
76
+ "spec/node/zmq_xrepxreq_spec.rb",
83
77
  "spec/node/routing_spec.rb",
84
78
  "spec/node/websocket_spec.rb",
85
- "spec/node/zmq_pushpull_spec.rb",
86
- "spec/node/zmq_xrepxreq_spec.rb"
79
+ "spec/node/http_spec.rb"
87
80
  ]
88
81
 
89
82
  if s.respond_to? :specification_version then
@@ -0,0 +1,22 @@
1
+ This example creates an async, evented app that can do the following.
2
+
3
+ * Broadcast messages to all connected websockets originating from a
4
+ master control server.
5
+ * Proxy requests sent via websocket to HTTP through a master
6
+ control server
7
+
8
+ It's broken into three parts
9
+ * service.rb: The async core, written in dripdrop/eventmachine/zeromq
10
+ * client.rb: A Test websocket client
11
+ * service.rb: A test HTTP web-service that could beb used to control messages
12
+
13
+ To run.
14
+
15
+ In one terminal (in dripdrop root)
16
+ ruby -I lib/ example/complex/server.rb
17
+
18
+ In another terminal (Websocket client)
19
+ cd example/complex && ruby client.rb
20
+
21
+ In a third terminal (Minimal webapp in sinatra)
22
+ cd example/complex && ruby service.rb
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ require 'websocket'
3
+ require 'dripdrop/message'
4
+
5
+ Thread.abort_on_exception = true
6
+
7
+ client = WebSocket.new('ws://127.0.0.1:8080')
8
+
9
+ Thread.new do
10
+ while data = client.receive
11
+ puts data
12
+ end
13
+ end
14
+
15
+ i = 0
16
+ while sleep 1
17
+ i += 1
18
+ puts '.'
19
+ client.send(DripDrop::Message.new('Client Broadcast', :body => i).json_encoded)
20
+ end
@@ -0,0 +1,115 @@
1
+ require 'dripdrop'
2
+ Thread.abort_on_exception = true
3
+
4
+ class ComplexExample < DripDrop::Node
5
+ def initialize(mode=:all)
6
+ super()
7
+ @mode = mode
8
+ end
9
+
10
+ def action
11
+ if [:all, :websockets].include?(@mode)
12
+ route :ws_listener, :websocket, 'ws://127.0.0.1:8080'
13
+ route :broadcast_in, :zmq_subscribe, 'tcp://127.0.0.1:2200', :connect
14
+ route :reqs_out, :zmq_xreq, 'tcp://127.0.0.1:2201', :connect
15
+
16
+ WSListener.new(:ws => ws_listener, :broadcast_in => broadcast_in, :reqs_out => reqs_out).run
17
+ end
18
+
19
+ if [:all, :coordinator].include?(@mode)
20
+ route :broadcast_out, :zmq_publish, 'tcp://127.0.0.1:2200', :bind
21
+ route :reqs_in, :zmq_xrep, 'tcp://127.0.0.1:2201', :bind
22
+ route :reqs_htout, :http_client, 'tcp://127.0.0.1:3000/endpoint'
23
+
24
+ Coordinator.new(:broadcast_out => broadcast_out, :reqs_in => reqs_in, :reqs_htout => reqs_htout).run
25
+ end
26
+ end
27
+ end
28
+
29
+ class Coordinator
30
+ def initialize(opts={})
31
+ @bc_out = opts[:broadcast_out]
32
+ @reqs_in = opts[:reqs_in]
33
+ @reqs_htout = opts[:reqs_htout]
34
+ end
35
+
36
+ def run
37
+ proxy_reqs
38
+ heartbeat
39
+ end
40
+
41
+ def proxy_reqs
42
+ @reqs_in.on_recv do |message, response|
43
+ puts "Proxying #{message.inspect} to htout"
44
+ @reqs_htout.send_message(message) do |http_response|
45
+ puts "Received http response #{http_response.inspect} sending back"
46
+ response.send_message(http_response)
47
+ end
48
+ end
49
+ end
50
+
51
+ def heartbeat
52
+ EM::PeriodicTimer.new(1) do
53
+ @bc_out.send_message :name => 'tick', :body => Time.now.to_s
54
+ end
55
+ end
56
+ end
57
+
58
+ class WSListener
59
+ def initialize(opts={})
60
+ @ws = opts[:ws]
61
+ @bc_in = opts[:broadcast_in]
62
+ @reqs_out = opts[:reqs_out]
63
+ @client_channel = EM::Channel.new
64
+ end
65
+ def run
66
+ proxy_websockets
67
+ broadcast_to_websockets
68
+ end
69
+
70
+ def broadcast_to_websockets
71
+ # Receives messages from Broadcast Out
72
+ @bc_in.on_recv do |message|
73
+ puts "Broadcast In recv: #{message.inspect}"
74
+ @client_channel.push(message)
75
+ end
76
+ end
77
+
78
+ def proxy_websockets
79
+ ws = @ws
80
+
81
+ sigs_sids = {} #Map connection signatures to subscriber IDs
82
+
83
+ ws.on_open do |conn|
84
+ puts "WS Connected"
85
+ conn.send_message(DripDrop::Message.new('test'))
86
+
87
+ sid = @client_channel.subscribe do |message|
88
+ puts message.inspect
89
+ conn.send_message(message)
90
+ end
91
+
92
+ sigs_sids[conn.signature] = sid
93
+ end
94
+ ws.on_close do |conn|
95
+ puts "Closed #{conn.signature}"
96
+ @client_channel.unsubscribe sigs_sids[conn.signature]
97
+ end
98
+ ws.on_error do |reason,conn|
99
+ puts "Errored #{reason.inspect}, #{conn.signature}"
100
+ @client_channel.unsubscribe sigs_sids[conn.signature]
101
+ end
102
+
103
+ ws.on_recv do |message,conn|
104
+ puts "WS Recv #{message.name}"
105
+ @reqs_out.send_message(message) do |resp_message|
106
+ puts "Recvd resp_message #{resp_message.inspect}, sending back to client"
107
+ conn.send_message(resp_message)
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+
114
+ puts "Starting..."
115
+ ComplexExample.new.start!
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'sinatra'
3
+ require 'dripdrop/message'
4
+
5
+ post '/endpoint' do
6
+ puts DripDrop::Message.decode_json(request.body.read).inspect
7
+ DripDrop::Message.new('ack').json_encoded
8
+ end
@@ -0,0 +1,442 @@
1
+ # Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
2
+ # Lincense: New BSD Lincense
3
+ # Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol
4
+
5
+ require "socket"
6
+ require "uri"
7
+ require "digest/md5"
8
+ require "openssl"
9
+
10
+
11
+ class WebSocket
12
+
13
+ class << self
14
+
15
+ attr_accessor(:debug)
16
+
17
+ end
18
+
19
+ class Error < RuntimeError
20
+
21
+ end
22
+
23
+ def initialize(arg, params = {})
24
+ if params[:server] # server
25
+
26
+ @server = params[:server]
27
+ @socket = arg
28
+ line = gets().chomp()
29
+ if !(line =~ /\AGET (\S+) HTTP\/1.1\z/n)
30
+ raise(WebSocket::Error, "invalid request: #{line}")
31
+ end
32
+ @path = $1
33
+ read_header()
34
+ if @header["sec-websocket-key1"] && @header["sec-websocket-key2"]
35
+ @key3 = read(8)
36
+ else
37
+ # Old Draft 75 protocol
38
+ @key3 = nil
39
+ end
40
+ if !@server.accepted_origin?(self.origin)
41
+ raise(WebSocket::Error,
42
+ ("Unaccepted origin: %s (server.accepted_domains = %p)\n\n" +
43
+ "To accept this origin, write e.g. \n" +
44
+ " WebSocketServer.new(..., :accepted_domains => [%p]), or\n" +
45
+ " WebSocketServer.new(..., :accepted_domains => [\"*\"])\n") %
46
+ [self.origin, @server.accepted_domains, @server.origin_to_domain(self.origin)])
47
+ end
48
+ @handshaked = false
49
+
50
+ else # client
51
+
52
+ uri = arg.is_a?(String) ? URI.parse(arg) : arg
53
+
54
+ if uri.scheme == "ws"
55
+ default_port = 80
56
+ elsif uri.scheme = "wss"
57
+ default_port = 443
58
+ else
59
+ raise(WebSocket::Error, "unsupported scheme: #{uri.scheme}")
60
+ end
61
+
62
+ @path = (uri.path.empty? ? "/" : uri.path) + (uri.query ? "?" + uri.query : "")
63
+ host = uri.host + (uri.port == default_port ? "" : ":#{uri.port}")
64
+ origin = params[:origin] || "http://#{uri.host}"
65
+ key1 = generate_key()
66
+ key2 = generate_key()
67
+ key3 = generate_key3()
68
+
69
+ socket = TCPSocket.new(uri.host, uri.port || default_port)
70
+
71
+ if uri.scheme == "ws"
72
+ @socket = socket
73
+ else
74
+ @socket = ssl_handshake(socket)
75
+ end
76
+
77
+ write(
78
+ "GET #{@path} HTTP/1.1\r\n" +
79
+ "Upgrade: WebSocket\r\n" +
80
+ "Connection: Upgrade\r\n" +
81
+ "Host: #{host}\r\n" +
82
+ "Origin: #{origin}\r\n" +
83
+ "Sec-WebSocket-Key1: #{key1}\r\n" +
84
+ "Sec-WebSocket-Key2: #{key2}\r\n" +
85
+ "\r\n" +
86
+ "#{key3}")
87
+ flush()
88
+
89
+ line = gets().chomp()
90
+ raise(WebSocket::Error, "bad response: #{line}") if !(line =~ /\AHTTP\/1.1 101 /n)
91
+ read_header()
92
+ if (@header["sec-websocket-origin"] || "").downcase() != origin.downcase()
93
+ raise(WebSocket::Error,
94
+ "origin doesn't match: '#{@header["sec-websocket-origin"]}' != '#{origin}'")
95
+ end
96
+ reply_digest = read(16)
97
+ expected_digest = security_digest(key1, key2, key3)
98
+ if reply_digest != expected_digest
99
+ raise(WebSocket::Error,
100
+ "security digest doesn't match: %p != %p" % [reply_digest, expected_digest])
101
+ end
102
+ @handshaked = true
103
+
104
+ end
105
+ @received = []
106
+ @buffer = ""
107
+ @closing_started = false
108
+ end
109
+
110
+ attr_reader(:server, :header, :path)
111
+
112
+ def handshake(status = nil, header = {})
113
+ if @handshaked
114
+ raise(WebSocket::Error, "handshake has already been done")
115
+ end
116
+ status ||= "101 Web Socket Protocol Handshake"
117
+ sec_prefix = @key3 ? "Sec-" : ""
118
+ def_header = {
119
+ "#{sec_prefix}WebSocket-Origin" => self.origin,
120
+ "#{sec_prefix}WebSocket-Location" => self.location,
121
+ }
122
+ header = def_header.merge(header)
123
+ header_str = header.map(){ |k, v| "#{k}: #{v}\r\n" }.join("")
124
+ if @key3
125
+ digest = security_digest(
126
+ @header["Sec-WebSocket-Key1"], @header["Sec-WebSocket-Key2"], @key3)
127
+ else
128
+ digest = ""
129
+ end
130
+ # Note that Upgrade and Connection must appear in this order.
131
+ write(
132
+ "HTTP/1.1 #{status}\r\n" +
133
+ "Upgrade: WebSocket\r\n" +
134
+ "Connection: Upgrade\r\n" +
135
+ "#{header_str}\r\n#{digest}")
136
+ flush()
137
+ @handshaked = true
138
+ end
139
+
140
+ def send(data)
141
+ if !@handshaked
142
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
143
+ end
144
+ data = force_encoding(data.dup(), "ASCII-8BIT")
145
+ write("\x00#{data}\xff")
146
+ flush()
147
+ end
148
+
149
+ def receive()
150
+ if !@handshaked
151
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
152
+ end
153
+ packet = gets("\xff")
154
+ return nil if !packet
155
+ if packet =~ /\A\x00(.*)\xff\z/nm
156
+ return force_encoding($1, "UTF-8")
157
+ elsif packet == "\xff" && read(1) == "\x00" # closing
158
+ if @server
159
+ @socket.close()
160
+ else
161
+ close()
162
+ end
163
+ return nil
164
+ else
165
+ raise(WebSocket::Error, "input must be either '\\x00...\\xff' or '\\xff\\x00'")
166
+ end
167
+ end
168
+
169
+ def tcp_socket
170
+ return @socket
171
+ end
172
+
173
+ def host
174
+ return @header["host"]
175
+ end
176
+
177
+ def origin
178
+ return @header["origin"]
179
+ end
180
+
181
+ def location
182
+ return "ws://#{self.host}#{@path}"
183
+ end
184
+
185
+ # Does closing handshake.
186
+ def close()
187
+ return if @closing_started
188
+ write("\xff\x00")
189
+ @socket.close() if !@server
190
+ @closing_started = true
191
+ end
192
+
193
+ def close_socket()
194
+ @socket.close()
195
+ end
196
+
197
+ private
198
+
199
+ NOISE_CHARS = ("\x21".."\x2f").to_a() + ("\x3a".."\x7e").to_a()
200
+
201
+ def read_header()
202
+ @header = {}
203
+ while line = gets()
204
+ line = line.chomp()
205
+ break if line.empty?
206
+ if !(line =~ /\A(\S+): (.*)\z/n)
207
+ raise(WebSocket::Error, "invalid request: #{line}")
208
+ end
209
+ @header[$1] = $2
210
+ @header[$1.downcase()] = $2
211
+ end
212
+ if !(@header["upgrade"] =~ /\AWebSocket\z/i)
213
+ raise(WebSocket::Error, "invalid Upgrade: " + @header["upgrade"])
214
+ end
215
+ if !(@header["connection"] =~ /\AUpgrade\z/i)
216
+ raise(WebSocket::Error, "invalid Connection: " + @header["connection"])
217
+ end
218
+ end
219
+
220
+ def gets(rs = $/)
221
+ line = @socket.gets(rs)
222
+ $stderr.printf("recv> %p\n", line) if WebSocket.debug
223
+ return line
224
+ end
225
+
226
+ def read(num_bytes)
227
+ str = @socket.read(num_bytes)
228
+ $stderr.printf("recv> %p\n", str) if WebSocket.debug
229
+ return str
230
+ end
231
+
232
+ def write(data)
233
+ if WebSocket.debug
234
+ data.scan(/\G(.*?(\n|\z))/n) do
235
+ $stderr.printf("send> %p\n", $&) if !$&.empty?
236
+ end
237
+ end
238
+ @socket.write(data)
239
+ end
240
+
241
+ def flush()
242
+ @socket.flush()
243
+ end
244
+
245
+ def security_digest(key1, key2, key3)
246
+ bytes1 = websocket_key_to_bytes(key1)
247
+ bytes2 = websocket_key_to_bytes(key2)
248
+ return Digest::MD5.digest(bytes1 + bytes2 + key3)
249
+ end
250
+
251
+ def generate_key()
252
+ spaces = 1 + rand(12)
253
+ max = 0xffffffff / spaces
254
+ number = rand(max + 1)
255
+ key = (number * spaces).to_s()
256
+ (1 + rand(12)).times() do
257
+ char = NOISE_CHARS[rand(NOISE_CHARS.size)]
258
+ pos = rand(key.size + 1)
259
+ key[pos...pos] = char
260
+ end
261
+ spaces.times() do
262
+ pos = 1 + rand(key.size - 1)
263
+ key[pos...pos] = " "
264
+ end
265
+ return key
266
+ end
267
+
268
+ def generate_key3()
269
+ return [rand(0x100000000)].pack("N") + [rand(0x100000000)].pack("N")
270
+ end
271
+
272
+ def websocket_key_to_bytes(key)
273
+ num = key.gsub(/[^\d]/n, "").to_i() / key.scan(/ /).size
274
+ return [num].pack("N")
275
+ end
276
+
277
+ def force_encoding(str, encoding)
278
+ if str.respond_to?(:force_encoding)
279
+ return str.force_encoding(encoding)
280
+ else
281
+ return str
282
+ end
283
+ end
284
+
285
+ def ssl_handshake(socket)
286
+ ssl_context = OpenSSL::SSL::SSLContext.new()
287
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
288
+ ssl_socket.sync_close = true
289
+ ssl_socket.connect()
290
+ return ssl_socket
291
+ end
292
+
293
+ end
294
+
295
+
296
+ class WebSocketServer
297
+
298
+ def initialize(params_or_uri, params = nil)
299
+ if params
300
+ uri = params_or_uri.is_a?(String) ? URI.parse(params_or_uri) : params_or_uri
301
+ params[:port] ||= uri.port
302
+ params[:accepted_domains] ||= [uri.host]
303
+ else
304
+ params = params_or_uri
305
+ end
306
+ @port = params[:port] || 80
307
+ @accepted_domains = params[:accepted_domains]
308
+ if !@accepted_domains
309
+ raise(ArgumentError, "params[:accepted_domains] is required")
310
+ end
311
+ if params[:host]
312
+ @tcp_server = TCPServer.open(params[:host], @port)
313
+ else
314
+ @tcp_server = TCPServer.open(@port)
315
+ end
316
+ end
317
+
318
+ attr_reader(:tcp_server, :port, :accepted_domains)
319
+
320
+ def run(&block)
321
+ while true
322
+ Thread.start(accept()) do |s|
323
+ begin
324
+ ws = create_web_socket(s)
325
+ yield(ws) if ws
326
+ rescue => ex
327
+ print_backtrace(ex)
328
+ ensure
329
+ begin
330
+ ws.close_socket() if ws
331
+ rescue
332
+ end
333
+ end
334
+ end
335
+ end
336
+ end
337
+
338
+ def accept()
339
+ return @tcp_server.accept()
340
+ end
341
+
342
+ def accepted_origin?(origin)
343
+ domain = origin_to_domain(origin)
344
+ return @accepted_domains.any?(){ |d| File.fnmatch(d, domain) }
345
+ end
346
+
347
+ def origin_to_domain(origin)
348
+ if origin == "null" || origin == "file://" # local file
349
+ return "null"
350
+ else
351
+ return URI.parse(origin).host
352
+ end
353
+ end
354
+
355
+ def create_web_socket(socket)
356
+ ch = socket.getc()
357
+ if ch == ?<
358
+ # This is Flash socket policy file request, not an actual Web Socket connection.
359
+ send_flash_socket_policy_file(socket)
360
+ return nil
361
+ else
362
+ socket.ungetc(ch)
363
+ return WebSocket.new(socket, :server => self)
364
+ end
365
+ end
366
+
367
+ private
368
+
369
+ def print_backtrace(ex)
370
+ $stderr.printf("%s: %s (%p)\n", ex.backtrace[0], ex.message, ex.class)
371
+ for s in ex.backtrace[1..-1]
372
+ $stderr.printf(" %s\n", s)
373
+ end
374
+ end
375
+
376
+ # Handles Flash socket policy file request sent when web-socket-js is used:
377
+ # http://github.com/gimite/web-socket-js/tree/master
378
+ def send_flash_socket_policy_file(socket)
379
+ socket.puts('<?xml version="1.0"?>')
380
+ socket.puts('<!DOCTYPE cross-domain-policy SYSTEM ' +
381
+ '"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">')
382
+ socket.puts('<cross-domain-policy>')
383
+ for domain in @accepted_domains
384
+ next if domain == "file://"
385
+ socket.puts("<allow-access-from domain=\"#{domain}\" to-ports=\"#{@port}\"/>")
386
+ end
387
+ socket.puts('</cross-domain-policy>')
388
+ socket.close()
389
+ end
390
+
391
+ end
392
+
393
+
394
+ if __FILE__ == $0
395
+ Thread.abort_on_exception = true
396
+
397
+ if ARGV[0] == "server" && ARGV.size == 3
398
+
399
+ server = WebSocketServer.new(
400
+ :accepted_domains => [ARGV[1]],
401
+ :port => ARGV[2].to_i())
402
+ puts("Server is running at port %d" % server.port)
403
+ server.run() do |ws|
404
+ puts("Connection accepted")
405
+ puts("Path: #{ws.path}, Origin: #{ws.origin}")
406
+ if ws.path == "/"
407
+ ws.handshake()
408
+ while data = ws.receive()
409
+ printf("Received: %p\n", data)
410
+ ws.send(data)
411
+ printf("Sent: %p\n", data)
412
+ end
413
+ else
414
+ ws.handshake("404 Not Found")
415
+ end
416
+ puts("Connection closed")
417
+ end
418
+
419
+ elsif ARGV[0] == "client" && ARGV.size == 2
420
+
421
+ client = WebSocket.new(ARGV[1])
422
+ puts("Connected")
423
+ Thread.new() do
424
+ while data = client.receive()
425
+ printf("Received: %p\n", data)
426
+ end
427
+ end
428
+ $stdin.each_line() do |line|
429
+ data = line.chomp()
430
+ client.send(data)
431
+ printf("Sent: %p\n", data)
432
+ end
433
+
434
+ else
435
+
436
+ $stderr.puts("Usage:")
437
+ $stderr.puts(" ruby web_socket.rb server ACCEPTED_DOMAIN PORT")
438
+ $stderr.puts(" ruby web_socket.rb client ws://HOST:PORT/")
439
+ exit(1)
440
+
441
+ end
442
+ end