websocket_sequential_client 1.0.0
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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +127 -0
- data/Rakefile +1 -0
- data/lib/websocket_sequential_client.rb +11 -0
- data/lib/websocket_sequential_client/exception.rb +15 -0
- data/lib/websocket_sequential_client/read_queue.rb +50 -0
- data/lib/websocket_sequential_client/version.rb +3 -0
- data/lib/websocket_sequential_client/web_socket.rb +349 -0
- data/lib/websocket_sequential_client/write_queue.rb +65 -0
- data/websocket_sequential_client.gemspec +30 -0
- metadata +128 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c86e2bfd2241079309ed7daceed67c861a0e6e27
|
4
|
+
data.tar.gz: f1f3b91c65126b6885bd5c6b735e44c350ada22c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 85b89614c39101a15684018881338f6aa747faeeb34e909f112d6fbc2762bd7000c04e9902ad42ed93bb9b35222ab668d2c84553c74adfbb42e5fba8e4f43c3c
|
7
|
+
data.tar.gz: e43c3706bef4b80a410ad6946958e608e59a77bff13b79a582a3a1b35b57fb0b83ba4293e254b7db59d487237ec34233c3f775f0f695b77c9c0a4fdaab32cecb
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
language: ruby
|
2
|
+
script: bundle exec rspec spec/websocket_sequential_client_spec.rb
|
3
|
+
before_install:
|
4
|
+
- gem update --system
|
5
|
+
- gem --version
|
6
|
+
- gem update bundler
|
7
|
+
rvm:
|
8
|
+
- 2.0.0
|
9
|
+
- 2.1
|
10
|
+
- 2.2
|
11
|
+
- ruby-head
|
12
|
+
- rbx
|
13
|
+
- jruby
|
14
|
+
matrix:
|
15
|
+
allow_failures:
|
16
|
+
- rvm: ruby-head
|
17
|
+
- rvm: rbx
|
18
|
+
- rvm: jruby
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Masamitsu MURASE
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
# WebsocketSequentialClient [](https://travis-ci.org/masamitsu-murase/websocket_sequential_client)
|
2
|
+
|
3
|
+
This ruby gem library allows you to access a WebSocket server in a **sequential** way instead of an event-driven way.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
* Provides access to a WebSocket server in a *sequential* way.
|
8
|
+
Many gem libraries for WebSocket client use *event-driven* style.
|
9
|
+
This style has many advantages, but it's not so easy to understand.
|
10
|
+
This gem library provides a *simple* and *sequential* access to a WebSocket server.
|
11
|
+
|
12
|
+
* Independent from eventmachine and other frameworks.
|
13
|
+
You can use this gem library with/without eventmachine and other frameworks.
|
14
|
+
Eventmachine is a great and useful library, but it uses some global functions, such as `EM.run`, `EM.stop` and so on, so it might conflicts with *your* environment.
|
15
|
+
This gem library does not depends on any frameworks, so you can use it in your environment.
|
16
|
+
|
17
|
+
If you need performance and/or scalability, you should use the other great libraries, such as em-websocket.
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
Add this line to your application's Gemfile:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem 'websocket_sequential_client'
|
25
|
+
```
|
26
|
+
|
27
|
+
And then execute:
|
28
|
+
|
29
|
+
$ bundle
|
30
|
+
|
31
|
+
Or install it yourself as:
|
32
|
+
|
33
|
+
$ gem install websocket_sequential_client
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
Call `send` and `recv` to communicate with the server.
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
require "websocket_sequential_client"
|
41
|
+
|
42
|
+
WebSocketSequentialClient::WebSocket.open "ws://server-url" do |ws|
|
43
|
+
ws.send "Hello"
|
44
|
+
puts ws.recv
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
**Note**: `recv` method is a *blocking* method. It will wait until a message is sent from the server.
|
49
|
+
|
50
|
+
## More examples
|
51
|
+
|
52
|
+
### Send and receive *text* and *binary* messages.
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
# coding: utf-8
|
56
|
+
|
57
|
+
require "websocket_sequential_client"
|
58
|
+
|
59
|
+
ws = WebSocketSequentialClient::WebSocket.new "ws://server-url"
|
60
|
+
|
61
|
+
# `send` method guesses whether the argument is text or binary based on the encoding.
|
62
|
+
# The encoding of "Hello" is UTF-8, so the messages is sent as text.
|
63
|
+
ws.send "Hello"
|
64
|
+
|
65
|
+
# You can also use `send_text` method.
|
66
|
+
ws.send_text "World"
|
67
|
+
|
68
|
+
|
69
|
+
# The following lines send binary messages
|
70
|
+
# because the argument has ASCII-8BIT encoding.
|
71
|
+
ws.send [0,1,2,3].pack("C*")
|
72
|
+
ws.send "example".b
|
73
|
+
|
74
|
+
# Of course, `send_binary` is supported.
|
75
|
+
ws.send_binary "binary message"
|
76
|
+
|
77
|
+
|
78
|
+
# You can receive messages from the server with `recv`.
|
79
|
+
# If the server sent "text" message, msg has UTF-8 encoding.
|
80
|
+
# In the case of "binary" message, msg has ASCII-8BIT encoding.
|
81
|
+
msg = ws.recv
|
82
|
+
|
83
|
+
ws.close
|
84
|
+
```
|
85
|
+
|
86
|
+
### Ping and pong.
|
87
|
+
|
88
|
+
`ping` and `pong` frames are sent in background, so you need not to handle these frames.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
require "websocket_sequential_client"
|
92
|
+
|
93
|
+
# `ping` is enabled as default. The interval is 20s.
|
94
|
+
ws = WebSocketSequentialClient::WebSocket.new "ws://server-url"
|
95
|
+
#...
|
96
|
+
ws.close
|
97
|
+
|
98
|
+
# You can disable `ping`.
|
99
|
+
ws = WebSocketSequentialClient::WebSocket.new "ws://server-url", ping: false
|
100
|
+
#...
|
101
|
+
ws.close
|
102
|
+
|
103
|
+
# You can change the interval of `ping` to 10s.
|
104
|
+
ws = WebSocketSequentialClient::WebSocket.new "ws://server-url", ping: { interval: 10 }
|
105
|
+
#...
|
106
|
+
ws.close
|
107
|
+
```
|
108
|
+
|
109
|
+
### Send additional headers.
|
110
|
+
|
111
|
+
You can send additional headers with the request of HTTP handshake.
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
require "websocket_sequential_client"
|
115
|
+
|
116
|
+
# Add "Cookie" field to the HTTP request.
|
117
|
+
ws = WebSocketSequentialClient::WebSocket.new "ws://server-url", headers: { "Cookie" => "value1" }
|
118
|
+
#...
|
119
|
+
ws.close
|
120
|
+
```
|
121
|
+
|
122
|
+
|
123
|
+
## Versions
|
124
|
+
|
125
|
+
* 1.0.0
|
126
|
+
Initial release
|
127
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require "websocket_sequential_client/version"
|
2
|
+
require "websocket_sequential_client/web_socket"
|
3
|
+
require "websocket_sequential_client/write_queue"
|
4
|
+
require "websocket_sequential_client/read_queue"
|
5
|
+
require "websocket_sequential_client/exception"
|
6
|
+
|
7
|
+
module WebsocketSequentialClient
|
8
|
+
end
|
9
|
+
|
10
|
+
WebSocketSequentialClient = WebsocketSequentialClient
|
11
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require "thread"
|
4
|
+
|
5
|
+
module WebsocketSequentialClient
|
6
|
+
class ReadQueue
|
7
|
+
def initialize
|
8
|
+
@mutex = Mutex.new
|
9
|
+
@cond_var = ConditionVariable.new
|
10
|
+
@frame_list = []
|
11
|
+
@closed_value = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def close value
|
15
|
+
@mutex.synchronize do
|
16
|
+
@closed_value = value
|
17
|
+
@cond_var.broadcast
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def available?
|
22
|
+
@mutex.synchronize do
|
23
|
+
return !(@frame_list.empty?)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def push frame
|
28
|
+
@mutex.synchronize do
|
29
|
+
return if @closed_value
|
30
|
+
|
31
|
+
@frame_list.push frame
|
32
|
+
@cond_var.broadcast
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def pop
|
37
|
+
@mutex.synchronize do
|
38
|
+
until !(@frame_list.empty?) or @closed_value
|
39
|
+
@cond_var.wait @mutex
|
40
|
+
end
|
41
|
+
|
42
|
+
if @frame_list.empty?
|
43
|
+
return @closed_value
|
44
|
+
else
|
45
|
+
return @frame_list.shift
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,349 @@
|
|
1
|
+
require("websocket")
|
2
|
+
require("uri")
|
3
|
+
require("socket")
|
4
|
+
require("thread")
|
5
|
+
|
6
|
+
module WebsocketSequentialClient
|
7
|
+
class WebSocket
|
8
|
+
DEFAULT_PING_INTERVAL = 20
|
9
|
+
DEFAULT_CLOSE_CODE = 1000
|
10
|
+
DEFAULT_CLOSE_TIMEOUT = 20
|
11
|
+
|
12
|
+
RECV_SIZE = 1024
|
13
|
+
|
14
|
+
WS_PORT = 80
|
15
|
+
|
16
|
+
def self.open *args, &block
|
17
|
+
if block
|
18
|
+
ws = self.new *args
|
19
|
+
begin
|
20
|
+
block.call ws
|
21
|
+
ensure
|
22
|
+
ws.close
|
23
|
+
end
|
24
|
+
else
|
25
|
+
self.new *args
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize url, opt = { ping: true, headers: {} }
|
30
|
+
@read_queue = ReadQueue.new
|
31
|
+
@write_queue = WriteQueue.new
|
32
|
+
|
33
|
+
@closed_status_mutex = Mutex.new
|
34
|
+
@closed_status_cond_var = ConditionVariable.new
|
35
|
+
@closed_status = nil
|
36
|
+
@close_timeout = DEFAULT_CLOSE_TIMEOUT
|
37
|
+
|
38
|
+
@close_code = nil
|
39
|
+
@close_reason = nil
|
40
|
+
|
41
|
+
@ping_th = nil
|
42
|
+
|
43
|
+
url = URI.parse url.to_s unless url.kind_of? URI
|
44
|
+
@url = url
|
45
|
+
|
46
|
+
case url.scheme
|
47
|
+
when "ws"
|
48
|
+
@socket = TCPSocket.new(url.host, url.port || WS_PORT)
|
49
|
+
else
|
50
|
+
raise ArgumentError, "URL scheme must be 'ws'."
|
51
|
+
end
|
52
|
+
|
53
|
+
hs = handshake(opt[:headers])
|
54
|
+
|
55
|
+
@version = hs.version
|
56
|
+
|
57
|
+
Thread.start{ send_background }
|
58
|
+
Thread.start{ receive_background }
|
59
|
+
start_ping_thread opt[:ping] if opt[:ping]
|
60
|
+
|
61
|
+
rescue
|
62
|
+
close_socket(SocketAlreadyClosed.new, SocketAlreadyClosed.new)
|
63
|
+
raise
|
64
|
+
end
|
65
|
+
attr_reader :close_code, :close_reason
|
66
|
+
|
67
|
+
def available?
|
68
|
+
@read_queue.available?
|
69
|
+
end
|
70
|
+
alias data_available? available?
|
71
|
+
|
72
|
+
def recv
|
73
|
+
data = @read_queue.pop
|
74
|
+
raise data if data.kind_of? StandardError
|
75
|
+
|
76
|
+
case data.type
|
77
|
+
when :text
|
78
|
+
data.to_s.force_encoding Encoding::UTF_8
|
79
|
+
when :binary
|
80
|
+
data.to_s.force_encoding Encoding::BINARY
|
81
|
+
else
|
82
|
+
raise NotImplementedError
|
83
|
+
end
|
84
|
+
end
|
85
|
+
alias receive recv
|
86
|
+
|
87
|
+
def send data, type = :guess
|
88
|
+
case type
|
89
|
+
when :guess
|
90
|
+
if data.encoding == Encoding::BINARY
|
91
|
+
type = :binary
|
92
|
+
elsif Encoding.compatible?(data.encoding, Encoding::UTF_8)
|
93
|
+
type = :text
|
94
|
+
else
|
95
|
+
raise ArgumentError, "Invalid encoding."
|
96
|
+
end
|
97
|
+
when :text
|
98
|
+
unless Encoding.compatible?(data.encoding, Encoding::UTF_8)
|
99
|
+
raise ArgumentError, "Invalid encoding"
|
100
|
+
end
|
101
|
+
when :binary
|
102
|
+
#
|
103
|
+
else
|
104
|
+
raise ArgumentError, "Invalid type specified"
|
105
|
+
end
|
106
|
+
|
107
|
+
# data.b is necessary for the current implementation of websocket gem library.
|
108
|
+
frame = ::WebSocket::Frame::Outgoing::Client.new(data: data.b, type: type, version: @version)
|
109
|
+
|
110
|
+
result = @write_queue.process_frame frame
|
111
|
+
raise result if result.kind_of? StandardError
|
112
|
+
end
|
113
|
+
|
114
|
+
def send_text data
|
115
|
+
send data, :text
|
116
|
+
end
|
117
|
+
|
118
|
+
def send_binary data
|
119
|
+
send data, :binary
|
120
|
+
end
|
121
|
+
|
122
|
+
def close code = nil, reason = nil, opt = { timeout: DEFAULT_CLOSE_TIMEOUT, wait_for_response: true }
|
123
|
+
code ||= DEFAULT_CLOSE_CODE
|
124
|
+
param = {
|
125
|
+
code: code,
|
126
|
+
type: :close,
|
127
|
+
version: @version
|
128
|
+
}
|
129
|
+
param[:data] = reason if reason
|
130
|
+
frame = ::WebSocket::Frame::Outgoing::Client.new(param)
|
131
|
+
|
132
|
+
@close_timeout = (opt.key?(:timeout) ? opt[:timeout] : DEFAULT_CLOSE_CODE)
|
133
|
+
@write_queue.process_frame frame
|
134
|
+
|
135
|
+
wait_for_response = (opt.key?(:wait_for_response) ? opt[:wait_for_response] : true)
|
136
|
+
if wait_for_response
|
137
|
+
@closed_status_mutex.synchronize do
|
138
|
+
@closed_status_cond_var.wait @closed_status_mutex until @closed_status == :closed
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
def handshake headers
|
145
|
+
hs = ::WebSocket::Handshake::Client.new(url: @url.to_s, headers: headers)
|
146
|
+
|
147
|
+
@socket.send(hs.to_s, 0)
|
148
|
+
|
149
|
+
begin
|
150
|
+
data = @socket.recv(1)
|
151
|
+
raise HandshakeFailed if data.empty?
|
152
|
+
hs << data
|
153
|
+
end until hs.finished?
|
154
|
+
|
155
|
+
raise HandshakeFailed unless hs.valid?
|
156
|
+
|
157
|
+
hs
|
158
|
+
end
|
159
|
+
|
160
|
+
def start_ping_thread ping_opt
|
161
|
+
interval = (ping_opt[:interval] rescue DEFAULT_PING_INTERVAL)
|
162
|
+
raise ArgumentError, "opt[:ping][:interval] must be a positive number." if interval <= 0
|
163
|
+
@ping_th = Thread.start(interval) do |i|
|
164
|
+
while true
|
165
|
+
sleep i
|
166
|
+
return if @closed_status
|
167
|
+
|
168
|
+
frame = ::WebSocket::Frame::Outgoing::Client.new(type: :ping, version: @version)
|
169
|
+
@write_queue.push frame
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def kill_ping_thread
|
175
|
+
return unless @ping_th
|
176
|
+
|
177
|
+
@ping_th.kill rescue nil
|
178
|
+
@ping_th = nil
|
179
|
+
end
|
180
|
+
|
181
|
+
def receive_next_data
|
182
|
+
data = @socket.recv(RECV_SIZE)
|
183
|
+
if data.empty?
|
184
|
+
# Unexpected close
|
185
|
+
close_socket(SocketAlreadyClosed.new, SocketAlreadyClosed.new)
|
186
|
+
return nil
|
187
|
+
end
|
188
|
+
return data
|
189
|
+
rescue => e
|
190
|
+
@read_queue.push e
|
191
|
+
close_socket(SocketAlreadyClosed.new, SocketAlreadyClosed.new)
|
192
|
+
return nil
|
193
|
+
end
|
194
|
+
|
195
|
+
def receive_background
|
196
|
+
input = ::WebSocket::Frame::Incoming::Client.new
|
197
|
+
|
198
|
+
data = receive_next_data
|
199
|
+
return unless data
|
200
|
+
|
201
|
+
input << data
|
202
|
+
|
203
|
+
while true
|
204
|
+
error = false
|
205
|
+
begin
|
206
|
+
next_frame = input.next
|
207
|
+
error = true if input.error
|
208
|
+
rescue
|
209
|
+
error = true
|
210
|
+
end
|
211
|
+
if error
|
212
|
+
@read_queue.push InvalidDataReceived.new
|
213
|
+
close_socket(SocketAlreadyClosed.new, SocketAlreadyClosed.new)
|
214
|
+
return
|
215
|
+
end
|
216
|
+
|
217
|
+
unless next_frame
|
218
|
+
data = receive_next_data
|
219
|
+
return unless data
|
220
|
+
|
221
|
+
input << data
|
222
|
+
next
|
223
|
+
end
|
224
|
+
|
225
|
+
case next_frame.type
|
226
|
+
when :text, :binary
|
227
|
+
@read_queue.push next_frame
|
228
|
+
|
229
|
+
when :close
|
230
|
+
@read_queue.close SocketAlreadyClosed.new
|
231
|
+
@close_code = next_frame.code
|
232
|
+
@close_reason = (next_frame.data && next_frame.data.to_s.force_encoding(Encoding::UTF_8))
|
233
|
+
|
234
|
+
@closed_status_mutex.synchronize do
|
235
|
+
unless @closed_status
|
236
|
+
@closed_status = :close_frame_received
|
237
|
+
@closed_status_cond_var.broadcast
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# After close_frame_received is set to true, all frames are ignored.
|
242
|
+
kill_ping_thread
|
243
|
+
|
244
|
+
# If @write_queue is already closed, frame will be ignored.
|
245
|
+
frame = ::WebSocket::Frame::Outgoing::Client.new(code: next_frame.code,
|
246
|
+
type: :close, version: @version)
|
247
|
+
@write_queue.push frame # Ignore result.
|
248
|
+
|
249
|
+
return
|
250
|
+
|
251
|
+
when :ping
|
252
|
+
f = ::WebSocket::Frame::Outgoing::Client.new(data: next_frame.to_s,
|
253
|
+
type: :pong, version: @version)
|
254
|
+
@write_queue.push f
|
255
|
+
|
256
|
+
when :pong
|
257
|
+
# Ignore
|
258
|
+
|
259
|
+
else
|
260
|
+
# Unknown packet.
|
261
|
+
@read_queue.push InvalidDataReceived.new
|
262
|
+
close_socket(SocketAlreadyClosed.new, SocketAlreadyClosed.new)
|
263
|
+
return
|
264
|
+
|
265
|
+
end
|
266
|
+
end
|
267
|
+
rescue SignalException, StandardError
|
268
|
+
critical_close
|
269
|
+
end
|
270
|
+
|
271
|
+
def send_background
|
272
|
+
while true
|
273
|
+
frame = @write_queue.pop
|
274
|
+
|
275
|
+
begin
|
276
|
+
@socket.send(frame.to_s, 0)
|
277
|
+
rescue => e
|
278
|
+
@write_queue.push_result frame, e
|
279
|
+
close_socket(SocketAlreadyClosed.new, SocketAlreadyClosed.new)
|
280
|
+
return
|
281
|
+
end
|
282
|
+
|
283
|
+
case frame.type
|
284
|
+
when :text, :binary
|
285
|
+
@write_queue.push_result frame, true
|
286
|
+
when :close
|
287
|
+
@write_queue.push_result frame, true
|
288
|
+
close_process
|
289
|
+
return
|
290
|
+
else
|
291
|
+
# ping and pong are ignored.
|
292
|
+
end
|
293
|
+
end
|
294
|
+
rescue SignalException, StandardError
|
295
|
+
critical_close
|
296
|
+
end
|
297
|
+
|
298
|
+
def critical_close
|
299
|
+
begin
|
300
|
+
close_socket(SocketAlreadyClosed.new, SocketAlreadyClosed.new)
|
301
|
+
rescue
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def close_process
|
306
|
+
kill_ping_thread
|
307
|
+
|
308
|
+
# Always start with shutdown of write.
|
309
|
+
@socket.shutdown Socket::SHUT_WR rescue nil
|
310
|
+
@write_queue.close SocketAlreadyClosed.new
|
311
|
+
# Then, try to receive a close frame from the server.
|
312
|
+
@closed_status_mutex.synchronize do
|
313
|
+
unless @closed_status
|
314
|
+
@closed_status_cond_var.wait(@closed_status_mutex, @close_timeout)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
case @closed_status
|
319
|
+
when nil
|
320
|
+
# An error occurs and a close frame is not sent from the server,
|
321
|
+
# so close anyway.
|
322
|
+
@socket.shutdown Socket::SHUT_RD
|
323
|
+
close_socket(SocketAlreadyClosed.new, SocketAlreadyClosed.new)
|
324
|
+
when :close_frame_received
|
325
|
+
begin
|
326
|
+
true until @socket.recv(RECV_SIZE).empty?
|
327
|
+
@socket.shutdown Socket::SHUT_RD
|
328
|
+
rescue
|
329
|
+
end
|
330
|
+
close_socket(nil, nil)
|
331
|
+
when :closed
|
332
|
+
# Nothing to do.
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def close_socket(wr_queue_error, rd_queue_error)
|
337
|
+
@write_queue.close wr_queue_error if wr_queue_error
|
338
|
+
@read_queue.close rd_queue_error if rd_queue_error
|
339
|
+
|
340
|
+
@socket.close rescue nil
|
341
|
+
@closed_status_mutex.synchronize do
|
342
|
+
@closed_status = :closed
|
343
|
+
@closed_status_cond_var.broadcast
|
344
|
+
end
|
345
|
+
|
346
|
+
kill_ping_thread # Fail safe
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require "thread"
|
4
|
+
|
5
|
+
module WebsocketSequentialClient
|
6
|
+
class WriteQueue
|
7
|
+
def initialize
|
8
|
+
@mutex = Mutex.new
|
9
|
+
@cond_var = ConditionVariable.new
|
10
|
+
@result_mutex = Mutex.new
|
11
|
+
@result_cond_var = ConditionVariable.new
|
12
|
+
@frame_list = []
|
13
|
+
@frame_result = {}
|
14
|
+
@closed_value = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def close value
|
18
|
+
@result_mutex.synchronize do
|
19
|
+
@closed_value = value
|
20
|
+
@result_cond_var.broadcast
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def push frame
|
25
|
+
return if @closed_value
|
26
|
+
|
27
|
+
@mutex.synchronize do
|
28
|
+
@frame_list.push frame
|
29
|
+
@cond_var.broadcast
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def pop
|
34
|
+
@mutex.synchronize do
|
35
|
+
@cond_var.wait @mutex while @frame_list.empty?
|
36
|
+
|
37
|
+
return @frame_list.shift
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def push_result frame, result
|
42
|
+
@result_mutex.synchronize do
|
43
|
+
return if @closed_value
|
44
|
+
|
45
|
+
@frame_result[frame.object_id] = result
|
46
|
+
@result_cond_var.broadcast
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def pop_result frame
|
51
|
+
@result_mutex.synchronize do
|
52
|
+
until @frame_result.key?(frame.object_id) or @closed_value
|
53
|
+
@result_cond_var.wait @result_mutex
|
54
|
+
end
|
55
|
+
|
56
|
+
return (@frame_result.delete(frame.object_id) || @closed_value)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def process_frame frame
|
61
|
+
push frame
|
62
|
+
pop_result frame
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'websocket_sequential_client/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "websocket_sequential_client"
|
8
|
+
spec.version = WebsocketSequentialClient::VERSION
|
9
|
+
spec.authors = ["Masamitsu MURASE"]
|
10
|
+
spec.email = ["masamitsu.murase@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{A simple WebSocket client to access a server in a sequential way}
|
13
|
+
spec.description = %q{This gem library allows you to access a WebSocket server in a sequential way instead of an event-driven way.}
|
14
|
+
spec.homepage = "https://github.com/masamitsu-murase/websocket_sequential_client"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.required_ruby_version = ">= 2.0"
|
23
|
+
|
24
|
+
spec.add_dependency "websocket"
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.9"
|
27
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
28
|
+
spec.add_development_dependency "rspec", "~> 3.2"
|
29
|
+
spec.add_development_dependency "em-websocket"
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: websocket_sequential_client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Masamitsu MURASE
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: websocket
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.9'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.9'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.2'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.2'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: em-websocket
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: This gem library allows you to access a WebSocket server in a sequential
|
84
|
+
way instead of an event-driven way.
|
85
|
+
email:
|
86
|
+
- masamitsu.murase@gmail.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- ".travis.yml"
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- lib/websocket_sequential_client.rb
|
98
|
+
- lib/websocket_sequential_client/exception.rb
|
99
|
+
- lib/websocket_sequential_client/read_queue.rb
|
100
|
+
- lib/websocket_sequential_client/version.rb
|
101
|
+
- lib/websocket_sequential_client/web_socket.rb
|
102
|
+
- lib/websocket_sequential_client/write_queue.rb
|
103
|
+
- websocket_sequential_client.gemspec
|
104
|
+
homepage: https://github.com/masamitsu-murase/websocket_sequential_client
|
105
|
+
licenses:
|
106
|
+
- MIT
|
107
|
+
metadata: {}
|
108
|
+
post_install_message:
|
109
|
+
rdoc_options: []
|
110
|
+
require_paths:
|
111
|
+
- lib
|
112
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '2.0'
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
requirements: []
|
123
|
+
rubyforge_project:
|
124
|
+
rubygems_version: 2.4.6
|
125
|
+
signing_key:
|
126
|
+
specification_version: 4
|
127
|
+
summary: A simple WebSocket client to access a server in a sequential way
|
128
|
+
test_files: []
|