Class: Iodine::Http::WebsocketClient

Inherits:
Object
  • Object
show all
Defined in:
lib/iodine/http/websocket_client.rb

Overview

Create a simple Websocket Client(!).

This should be done from within an Iodine task, or the callbacks will not be called.

Use WebsocketClient.connect to initialize a client with all the callbacks needed.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ WebsocketClient

Returns a new instance of WebsocketClient.

Raises:

  • (TypeError)


13
14
15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/iodine/http/websocket_client.rb', line 13

def initialize options
	@response = nil
	@options = options
	@on_message = @options[:on_message]
	raise "Websocket client must have an #on_message Proc or handler." unless @on_message && @on_message.respond_to?(:call)
	@on_open = @options[:on_open]
	@on_close = @options[:on_close]
	@on_error = @options[:on_error]
	@renew = @options[:renew].to_i
	@options[:url] = URI.parse(@options[:url]) unless @options[:url].is_a?(URI)
	@connection_lock = Mutex.new
	raise TypeError, "Websocket Client `:send` should be either a String or a Proc object." if @options[:send] && !(@options[:send].is_a?(String) || @options[:send].is_a?(Proc))
	on_close && (@io || raise("Connection error, cannot create websocket client")) unless connect
end

Instance Attribute Details

#paramsObject

Returns the value of attribute params.



11
12
13
# File 'lib/iodine/http/websocket_client.rb', line 11

def params
  @params
end

#requestObject

Returns the value of attribute request.



11
12
13
# File 'lib/iodine/http/websocket_client.rb', line 11

def request
  @request
end

#responseObject

Returns the value of attribute response.



11
12
13
# File 'lib/iodine/http/websocket_client.rb', line 11

def response
  @response
end

Instance Method Details

#closeObject

closes the connection, if open



121
122
123
124
# File 'lib/iodine/http/websocket_client.rb', line 121

def close
	@renew = 0
	@io.close if @io
end

#closed?Boolean

checks if the socket is open (if the websocket was terminated abnormally, this might return true for a while after it should be false).

Returns:

  • (Boolean)


127
128
129
# File 'lib/iodine/http/websocket_client.rb', line 127

def closed?
	(@io && @io.io) ? @io.io.closed? : true
end

#cookiesObject

return a Hash with the HTTP cookies recieved during the HTTP’s handshake.



137
138
139
# File 'lib/iodine/http/websocket_client.rb', line 137

def cookies
	@request.cookies
end

#on(event_name, &block) ⇒ Object



28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/iodine/http/websocket_client.rb', line 28

def on event_name, &block
	return false unless block
	case event_name
	when :message
		@on_message = block
	when :close
		@on_close = block
	when :open
		raise 'The on_open even is invalid at this point.'
	end
								
end

#on_close(&block) ⇒ Object



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/iodine/http/websocket_client.rb', line 82

def on_close(&block)
	return @on_close = block if block
	if @renew > 0
		renew_proc = Proc.new do
			@io = nil
			begin
				raise unless connect
			rescue
				@renew -= 1
				if @renew <= 0
					Iodine.fatal "WebsocketClient renewal FAILED for #{@options[:url]}"
					on_close
				else
					Iodine.warn "WebsocketClient renewal failed for #{@options[:url]}, #{@renew} attempts left"
					renew_proc.call
				end
				false
			end
		end
		@connection_lock.synchronize { renew_proc.call }
	else
		begin
			instance_exec(&@on_close) if @on_close
		rescue => e
			@on_error ? @on_error.call(e) : raise(e)
		end
	end
end

#on_error(error = nil, &block) ⇒ Object



110
111
112
113
114
# File 'lib/iodine/http/websocket_client.rb', line 110

def on_error(error = nil, &block)
	return @on_error = block if block
	instance_exec(error, &@on_error) if @on_error
	on_close unless @io # if the connection was initialized, :on_close will be called by Iodine
end

#on_message(data = nil, &block) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
# File 'lib/iodine/http/websocket_client.rb', line 41

def on_message(data = nil, &block)
	unless data
		@on_message = block if block
		return @on_message
	end
	begin
		instance_exec( data, &@on_message) 
	rescue => e
		@on_error ? @on_error.call(e) : raise(e)
	end
end

#on_openObject



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/iodine/http/websocket_client.rb', line 53

def on_open
	raise 'The on_open even is invalid at this point.' if block_given?
	@renew = @options[:renew].to_i
	@io = @request[:io]
	Iodine::Http::Request.parse @request
	begin
		instance_exec(&@on_open) if @on_open
	rescue => e
		@on_error ? @on_error.call(e) : raise(e)
	end
	if @options[:every] && @options[:send]
		Iodine.run_every @options[:every], self, @options do |ws, client_params, timer|
			if ws.closed?
				timer.stop!
				next
			end
			if client_params[:send].is_a?(String)
				ws.write client_params[:send]
			elsif client_params[:send].is_a?(Proc)
				begin
					ws.instance_exec(&client_params[:send])
				rescue => e
					@on_error ? @on_error.call(e) : raise(e)
				end							
			end
		end
	end
end

#on_shutdownObject



116
117
118
# File 'lib/iodine/http/websocket_client.rb', line 116

def on_shutdown
	@renew = 0
end

#ssl?Boolean

checks if this is an SSL websocket connection.

Returns:

  • (Boolean)


132
133
134
# File 'lib/iodine/http/websocket_client.rb', line 132

def ssl?
	@request.ssl?
end

#write(data, op_code = nil, fin = true, ext = 0) ⇒ true, false Also known as: <<

Sends data through the websocket, after client side masking.

Returns:

  • (true, false)

    Returns the true if the data was actually sent or nil if no data was sent.



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/iodine/http/websocket_client.rb', line 144

def write data, op_code = nil, fin = true, ext = 0
	return false if !data || data.empty?
	data = data.dup # needed?
	unless op_code # apply extenetions to the message as a whole
		op_code = (data.encoding == ::Encoding::UTF_8 ? 1 : 2) 
		# @ws_extentions.each { |ex| ext |= ex.edit_message data } if @ws_extentions
	end
	byte_size = data.bytesize
	if byte_size > (::Iodine::Http::Websockets::FRAME_SIZE_LIMIT+2)
		# sections = byte_size/FRAME_SIZE_LIMIT + (byte_size % ::Iodine::Http::Websockets::FRAME_SIZE_LIMIT ? 1 : 0)
		ret = write( data.slice!( 0...::Iodine::Http::Websockets::FRAME_SIZE_LIMIT ), op_code, data.empty?, ext) && (ext = op_code = 0) until data.empty?
		return ret # avoid sending an empty frame.
	end
	# @ws_extentions.each { |ex| ext |= ex.edit_frame data } if @ws_extentions
	header = ( (fin ? 0b10000000 : 0) | (op_code & 0b00001111) | ext).chr.force_encoding(::Encoding::ASCII_8BIT)

	if byte_size < 125
		header << (byte_size | 128).chr
	elsif byte_size.bit_length <= 16					
		header << 254.chr
		header << [byte_size].pack('S>'.freeze)
	else
		header << 255.chr
		header << [byte_size].pack('Q>'.freeze)
	end
	@@make_mask_proc ||= Proc.new {Random.rand(251) + 1}
	mask = Array.new(4, &(@@make_mask_proc))
	header << mask.pack('C*'.freeze)
	@connection_lock.synchronize do
		return false if @io.nil? || @io.closed?
		@io.write header
		i = -1;
		@io.write(data.bytes.map! {|b| (b ^ mask[i = (i + 1)%4]) } .pack('C*'.freeze)) && true
	end
end