easy_sockets 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6cda3ce2892a6052d57ed242708f762febe57481
4
+ data.tar.gz: fa52646d15d1aede1b25c6a612aab869c767d8da
5
+ SHA512:
6
+ metadata.gz: 4160e8c1d10c54a8998c4d071de8b9f91215c157925814a36f5d097cf0804db2a1e76d58a5c6bb75117ed747d9735d2b4655873438686160a312b17684745e27
7
+ data.tar.gz: 3324c1006ccc13d583dce4d1aa6d037a94e7e8cacadc0d58aaf411ce8a73ceb88b0f642aadb25d1abaa803c9cafd5ced03b71a2d3aa1a97b41294c1ab59b70ac
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ *.gem
2
+ .ruby-version
3
+ /.bundle/
4
+ /.yardoc
5
+ /Gemfile.lock
6
+ /_yardoc/
7
+ /coverage/
8
+ /doc/
9
+ /pkg/
10
+ /spec/reports/
11
+ /tmp/
12
+ /test_logs/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.0
5
+ before_install: gem install bundler -v 1.12.5
6
+ notifications:
7
+ email: false
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --no-private
2
+ --protected
3
+ --markup="markdown" lib/**/*.rb
4
+ --main README.md
5
+ -M redcarpet
6
+ --exclude (tcp_server.rb|unix_server.rb|server_utils.rb)
data/CHANGELOG.rb ADDED
@@ -0,0 +1,4 @@
1
+ ## 0.1.0 (2016-06-28)
2
+ Features:
3
+ - EasySockets::TcpSocket now reads and writes in a non blocking way.
4
+ - Adding support to unix sockets.
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'rspec'
7
+ gem "codeclimate-test-reporter", require: nil
8
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Marcos Ortiz
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,280 @@
1
+ # Easy Sockets
2
+
3
+ [![Gem Version][GV img]][Gem Version]
4
+ [![Build Status][BS img]][Build Status]
5
+ [![Dependency Status][DS img]][Dependency Status]
6
+ [![Code Climate][CC img]][Code Climate]
7
+ [![Coverage Status][CS img]][Coverage Status]
8
+
9
+ [Gem Version]: https://rubygems.org/gems/easy_sockets
10
+ [Build Status]: https://travis-ci.org/marcosortiz/easy_sockets
11
+ [Dependency Status]: https://gemnasium.com/marcosortiz/easy_sockets
12
+ [Code Climate]: https://codeclimate.com/github/marcosortiz/easy_sockets
13
+ [Coverage Status]: https://codeclimate.com/github/marcosortiz/easy_sockets/coverage
14
+
15
+ [GV img]: https://badge.fury.io/rb/easy_sockets.svg
16
+ [BS img]: https://travis-ci.org/marcosortiz/easy_sockets.svg?branch=master
17
+ [DS img]: https://gemnasium.com/marcosortiz/easy_sockets.svg
18
+ [CC img]: https://codeclimate.com/github/marcosortiz/easy_sockets/badges/gpa.svg
19
+ [CS img]: https://codeclimate.com/github/marcosortiz/easy_sockets/badges/coverage.svg
20
+
21
+ ## Description
22
+
23
+ Over and over I see developers struggling to implement basic sockets with featues available on ruby socket stdlib.
24
+
25
+ easy_sockets, takes care of basic details that usually are overlooked by developers when implementing TCP/Unix sockets from scratch.
26
+
27
+ I also strongly recommend [the following book](http://www.jstorimer.com/products/working-with-tcp-sockets) if you want to learn more about TCP sockets.
28
+
29
+ > UDP socket support is comming soon.
30
+
31
+ ### Dependencies
32
+
33
+ easy_sockets only uses the following ruby stdlib gems:
34
+
35
+ - sockets
36
+ - logger
37
+ - timeout (just to raise Timeout::Error)
38
+
39
+ ### Transparent idempotent connect and disconnect operations
40
+
41
+ You don't needneed to worry about connecting your socket (you can still call it if you want). All you need to do is call `send_msg`. If the socket object is not connected yet, it will automatically try to connect the socket before sending the message. You still need to disconnect your socket after using it.
42
+
43
+ The `connect` and `disconnect` methods are idempotent methods. That means you can call them over and over again and they will only try to do something (connect and disconnect respectively) when the instance of your socket object is disconnected and connected respectively.
44
+
45
+ The code bellow illustrates this:
46
+
47
+ ```ruby
48
+ irb(main):001:0> require 'easy_sockets'
49
+ => true
50
+ irb(main):002:0> s = EasySockets::TcpSocket.new
51
+ => #<EasySockets::TcpSocket:0x007fd16bb69308 @logger=nil, @timeout=0.5, @separator="\r\n", @connected=false, @port=2000, @host="127.0.0.1">
52
+ irb(main):003:0> s.connected
53
+ => false
54
+ irb(main):004:0> s.connect
55
+ => true
56
+ irb(main):005:0> s.connected
57
+ => true
58
+ irb(main):006:0> s.connect
59
+ => nil
60
+ irb(main):007:0> s.connect
61
+ => nil
62
+ irb(main):008:0> s.connected
63
+ => true
64
+ irb(main):009:0> s.disconnect
65
+ => true
66
+ irb(main):010:0> s.connected
67
+ => false
68
+ irb(main):011:0> s.disconnect
69
+ => nil
70
+ irb(main):012:0> s.disconnect
71
+ => nil
72
+ ```
73
+
74
+ ### Safe connect, read and write timeout implementation
75
+
76
+ There is a lot of material on the internet saying [why you should not use ruby timeout stdlib](http://www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/). However, over and over I see developers using the timeout stdlib for production code!
77
+
78
+ easy_sockets implements connect, read and write timeouts using [IO.select](http://ruby-doc.org/core-2.3.1/IO.html#method-c-select).
79
+
80
+ ### Framing Messages
81
+
82
+ Usually, if you don't want to open and close a new connection everytime you need to send something to the server, you need to implement some sort of message framing. Openning (and closing) a new connection for each message, generates unnecessary overhead. While this might be ok for some communications where the messsage exchange rate is low, it might be a show stopper when this rate needs to be bigger.
83
+
84
+ Message framing is an agreement between client and server on the message format. That way clients and server can signal that one message is ending and another on is beginning.
85
+
86
+ There are numerous ways of framing messages. easy_sockets support 2:
87
+
88
+ 1. **Message separators:** When pass the `:separator` option when creating your socket, easy_sockets will add it to the end of the message. For instance, if you setup `separator: "\r\n"` and you call `send_msg("some_message")`, the server will receive `"some_message\r\n"`.
89
+
90
+ 2. **No separators**: When you pass the option `no_separator: true` when creating your socket, easy_sockets will not add anything to the end of the message. This is useful when both client and server uses a more specific protocol. For instance, both client and server know that the first 4 bytes of the message represent and little ending integer, and depending on the value of that integer, the message will have a specific size and format.
91
+
92
+ Whether to use separators or not, is totally up to what both client and server expects.
93
+
94
+ > If you decide to use new lines as the message separator, remember that it is `\n` on Unix systems but `\r\n` on Windows. So, be sure that both client and server are using the same separator.
95
+
96
+ ## Installation
97
+
98
+ Add this line to your application's Gemfile:
99
+ ```ruby
100
+ gem 'easy_sockets'
101
+ ```
102
+
103
+ And then execute:
104
+
105
+ $ bundle
106
+
107
+ Or install it yourself as:
108
+
109
+ $ gem install easy_sockets
110
+
111
+ ## Usage
112
+
113
+ Make sure you have netcat installed on your system. We will use it to emulate our servers.
114
+
115
+ ### TCP Sockets
116
+
117
+ Open up a terminal window and type the following to start a TCP server:
118
+ ```bash
119
+ nc -ckl 2500
120
+ ```
121
+
122
+ On another terminal window, run the following [code](https://github.com/marcosortiz/easy_sockets/blob/master/examples/tcp_socket.rb) to start the client:
123
+ ```ruby
124
+ require 'easy_sockets'
125
+
126
+ host = ARGV[0] || '127.0.0.1'
127
+
128
+ port = ARGV[1].to_i
129
+ port = 2500 if port <= 0
130
+
131
+ opts = {
132
+ host: host,
133
+ port: port,
134
+ timeout: 300,
135
+ separator: "\r\n",
136
+ logger: Logger.new(STDOUT),
137
+ }
138
+ s = EasySockets::TcpSocket.new(opts)
139
+ [:INT, :QUIT, :TERM].each do |signal|
140
+ Signal.trap(signal) do
141
+ exit
142
+ end
143
+ end
144
+
145
+ loop do
146
+ puts "Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:"
147
+ msg = gets.chomp
148
+ s.send_msg(msg)
149
+ end
150
+ ```
151
+
152
+ Then typing `sample_request` in the client terminal, you should see:
153
+ ```
154
+ $ bundle exec ruby examples/tcp_socket.rb
155
+ Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:
156
+ sample_request
157
+ D, [2016-06-30T15:17:39.648945 #90385] DEBUG -- : Successfully connected to tcp://127.0.0.1:2500.
158
+ D, [2016-06-30T15:17:39.649044 #90385] DEBUG -- : Sending "sample_request\r\n"
159
+ ```
160
+
161
+ And the server terminal window should display:
162
+ ```
163
+ $ nc -ckl 2500
164
+ sample_request
165
+
166
+ ```
167
+
168
+ Then type `sample_response` on the server terminal window, and you should see:
169
+ ```
170
+ $ nc -ckl 2500
171
+ sample_request
172
+ sample_response
173
+
174
+ ```
175
+
176
+ And the client window should show:
177
+ ```
178
+ $ bundle exec ruby examples/tcp_socket.rb
179
+ Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:
180
+ sample_request
181
+ D, [2016-06-30T15:17:39.648945 #90385] DEBUG -- : Successfully connected to tcp://127.0.0.1:2500.
182
+ D, [2016-06-30T15:17:39.649044 #90385] DEBUG -- : Sending "sample_request\r\n"
183
+ D, [2016-06-30T15:19:52.494791 #90385] DEBUG -- : Got "sample_response\r\n"
184
+ Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:
185
+
186
+ ```
187
+
188
+ Press `Ctrl+c` on the client and server terminal windows to terminate both.
189
+
190
+ ### Unix Sockets
191
+
192
+ Open up a terminal window and type the following to start a Unix server:
193
+ ```bash
194
+ nc -Ul /tmp/test_socket
195
+ ```
196
+
197
+ On another terminal window, run the following [code](https://github.com/marcosortiz/easy_sockets/blob/master/examples/unix_socket.rb) to start the client:
198
+ ```ruby
199
+ require 'easy_sockets'
200
+
201
+ host = ARGV[0] || '127.0.0.1'
202
+
203
+ socket_path = ARGV[1]
204
+ socket_path ||= '/tmp/test_socket'
205
+
206
+ opts = {
207
+ host: host,
208
+ socket_path: socket_path,
209
+ timeout: 300,
210
+ separator: "\n",
211
+ logger: Logger.new(STDOUT),
212
+ }
213
+ s = EasySockets::UnixSocket.new(opts)
214
+ [:INT, :QUIT, :TERM].each do |signal|
215
+ Signal.trap(signal) do
216
+ exit
217
+ end
218
+ end
219
+
220
+ loop do
221
+ puts "Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:"
222
+ msg = gets.chomp
223
+ s.send_msg(msg)
224
+ end
225
+ ```
226
+
227
+ Then typing `sample_request` in the client terminal, you should see:
228
+ ```
229
+ marcosortiz@~/dev/easy_sockets$ bundle exec ruby examples/unix_socket.rb
230
+ Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:
231
+ sample_request
232
+ D, [2016-06-30T15:38:10.303188 #96993] DEBUG -- : Successfully connected to /tmp/test_socket.
233
+ D, [2016-06-30T15:38:10.303265 #96993] DEBUG -- : Sending "sample_request\n"
234
+ ```
235
+
236
+ And the server terminal window should display:
237
+ ```
238
+ $ nc -Ul /tmp/test_socket
239
+ sample_request
240
+
241
+ ```
242
+
243
+ Then type `sample_response` on the server terminal window, and you should see:
244
+ ```
245
+ $ nc -Ul /tmp/test_socket
246
+ sample_request
247
+ sample_response
248
+
249
+ ```
250
+
251
+ And the client window should show:
252
+ ```
253
+ $ bundle exec ruby examples/unix_socket.rb
254
+ Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:
255
+ sample_request
256
+ D, [2016-06-30T15:38:10.303188 #96993] DEBUG -- : Successfully connected to /tmp/test_socket.
257
+ D, [2016-06-30T15:38:10.303265 #96993] DEBUG -- : Sending "sample_request\n"
258
+ D, [2016-06-30T15:38:23.503411 #96993] DEBUG -- : "sample_response\n"
259
+ D, [2016-06-30T15:38:23.503488 #96993] DEBUG -- : Got "sample_response\n"
260
+ Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:
261
+
262
+ ```
263
+
264
+ Press `Ctrl+c` on the client and server terminal windows to terminate both. Also, type `rm -rf /tmp/test_socket` to remove the socket file.
265
+
266
+ ## Development
267
+
268
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
269
+
270
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
271
+
272
+ ## 5. Contributing
273
+
274
+ Bug reports and pull requests are welcome on GitHub at https://github.com/marcosortiz/easy_sockets.
275
+
276
+
277
+ ## 6. License
278
+
279
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
280
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "easy_sockets"
5
+
6
+ require "irb"
7
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'easy_sockets/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "easy_sockets"
8
+ spec.version = EasySockets::VERSION
9
+ spec.authors = ["Marcos Ortiz"]
10
+ spec.email = ["marcos.ortiz@icloud.com"]
11
+
12
+ spec.summary = %q{Wrapper around ruby socket stdlib to make developer's life easier'.}
13
+ spec.description = %q{Over and over I see developers struggling to implement basic sockets with featues available on ruby socket stdlib. easy_sockets, takes care of basic details that usually are overlooked by developers when implementing TCP/Unix sockets from scratch.}
14
+ spec.homepage = "https://github.com/marcosortiz/easy_sockets"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ # if spec.respond_to?(:metadata)
20
+ # spec.metadata['allowed_push_host'] = 'http://rubygems.org'
21
+ # else
22
+ # raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
+ # end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "bundler", "~> 1.12"
31
+ spec.add_development_dependency "rake", "~> 10.0"
32
+ spec.add_development_dependency "rspec", "~> 3.0"
33
+ end
@@ -0,0 +1,26 @@
1
+ require 'easy_sockets'
2
+
3
+ host = ARGV[0] || '127.0.0.1'
4
+
5
+ port = ARGV[1].to_i
6
+ port = 2500 if port <= 0
7
+
8
+ opts = {
9
+ host: host,
10
+ port: port,
11
+ timeout: 300,
12
+ separator: "\r\n",
13
+ logger: Logger.new(STDOUT),
14
+ }
15
+ s = EasySockets::UnixSocket.new(opts)
16
+ [:INT, :QUIT, :TERM].each do |signal|
17
+ Signal.trap(signal) do
18
+ exit
19
+ end
20
+ end
21
+
22
+ loop do
23
+ puts "Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:"
24
+ msg = gets.chomp
25
+ s.send_msg(msg)
26
+ end
@@ -0,0 +1,26 @@
1
+ require 'easy_sockets'
2
+
3
+ host = ARGV[0] || '127.0.0.1'
4
+
5
+ socket_path = ARGV[1]
6
+ socket_path ||= '/tmp/test_socket'
7
+
8
+ opts = {
9
+ host: host,
10
+ socket_path: socket_path,
11
+ timeout: 300,
12
+ separator: "\n",
13
+ logger: Logger.new(STDOUT),
14
+ }
15
+ s = EasySockets::UnixSocket.new(opts)
16
+ [:INT, :QUIT, :TERM].each do |signal|
17
+ Signal.trap(signal) do
18
+ exit
19
+ end
20
+ end
21
+
22
+ loop do
23
+ puts "Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:"
24
+ msg = gets.chomp
25
+ s.send_msg(msg)
26
+ end
@@ -0,0 +1,154 @@
1
+ require 'logger'
2
+ require 'socket'
3
+ require 'timeout'
4
+ require 'easy_sockets/constants'
5
+ require 'easy_sockets/utils'
6
+
7
+ module EasySockets
8
+ #
9
+ # @author Marcos Ortiz
10
+ # @abstract Please check the following subclasses: {EasySockets::TcpSocket} and {EasySockets::UnixSocket}.
11
+ #
12
+ class BasicSocket
13
+ include EasySockets::Utils
14
+
15
+ DEFAULT_TIMEOUT = 0.5
16
+
17
+ attr_reader :logger, :connected
18
+ alias_method :connected?, :connected
19
+
20
+ #
21
+ # @param [Hash] opts the options to create a socket with.
22
+ # @option opts [Logger] :logger (nil) An instance of Logger.
23
+ # @option opts [Float] :timeout (0.5) Timeout in seconds for socket connect, read and write operations.
24
+ # @option opts [String] :separator ("\r\n") Message separator.
25
+ # @option opts [Boolean] :no_msg_separator (nil) If true, the socket will not use message separators.
26
+ def initialize(opts={})
27
+ setup_opts(opts)
28
+ @connected = false
29
+ end
30
+
31
+ #
32
+ # Connects to the server. This is an idempotent operation.
33
+ #
34
+ def connect
35
+ return if @connected && (@socket && !@socket.closed?)
36
+ on_connect
37
+ @connected = true
38
+ end
39
+
40
+ #
41
+ # Disconnects to the server. This is an idempotent operation.
42
+ #
43
+ def disconnect
44
+ return unless @connected
45
+ if @socket && !@socket.closed?
46
+ @socket.close
47
+ log(:debug, "Socket successfully disconnected")
48
+ @connected = false
49
+ return true
50
+ end
51
+ end
52
+
53
+ #
54
+ # Sends the message to the server, and reads and return the response if read_response=true.
55
+ # If you call this method and the socket is not connected yet, it will automatically connect the socket.
56
+ # @param [String] msg The message to send.
57
+ # @param [Boolean] read_response Whether or not to read from the server after sending the message. Defaul to true.
58
+ #
59
+ #
60
+ def send_msg(msg, read_response=true)
61
+ msg_to_send = msg.dup
62
+ msg_to_send << @separator unless @separator.nil? || msg.end_with?(@separator)
63
+
64
+ # This is an idempotent operation
65
+ connect
66
+
67
+ log(:debug, "Sending #{msg_to_send.inspect}")
68
+ send_non_block(msg_to_send)
69
+
70
+ if read_response
71
+ resp = receive_non_block
72
+ log(:debug, "Got #{resp.inspect}")
73
+ resp
74
+ end
75
+ # Raised by some IO operations when reaching the end of file. Many IO methods exist in two forms,
76
+ # one that returns nil when the end of file is reached, the other raises EOFError EOFError.
77
+ # EOFError is a subclass of IOError.
78
+ rescue EOFError => e
79
+ log(:info, "Server disconnected.")
80
+ self.disconnect
81
+ raise e
82
+ # "Connection reset by peer" is the TCP/IP equivalent of slamming the phone back on the hook.
83
+ # It's more polite than merely not replying, leaving one hanging.
84
+ # But it's not the FIN-ACK expected of the truly polite TCP/IP converseur.
85
+ rescue Errno::ECONNRESET => e
86
+ log(:info, 'Connection reset by peer.')
87
+ self.disconnect
88
+ raise e
89
+ rescue Errno::EPIPE => e
90
+ log(:info, 'Broken pipe.')
91
+ self.disconnect
92
+ raise e
93
+ rescue Errno::ECONNREFUSED => e
94
+ log(:info, 'Connection refused by peer.')
95
+ self.disconnect
96
+ raise e
97
+ rescue Exception => e
98
+ @socket.close if @socket && !@socket.closed?
99
+ raise e
100
+ end
101
+
102
+ private
103
+
104
+ def setup_opts(opts)
105
+ @logger = opts[:logger]
106
+ @timeout = opts[:timeout].to_f || DEFAULT_TIMEOUT
107
+ @timeout = DEFAULT_TIMEOUT if @timeout <= 0
108
+ @separator = opts[:separator] || CRLF
109
+ @separator = nil if opts[:no_msg_separator] == true
110
+ end
111
+
112
+ def on_connect
113
+ end
114
+
115
+ def send_non_block(msg)
116
+ begin
117
+ loop do
118
+ bytes = @socket.write_nonblock(msg)
119
+ break if bytes >= msg.size
120
+ msg.slice!(0, bytes)
121
+ IO.select(nil, [@socket])
122
+ end
123
+ rescue Errno::EAGAIN
124
+ IO.select(nil, [@socket])
125
+ end
126
+ end
127
+
128
+ def receive_non_block
129
+ resp = ''
130
+ begin
131
+ resp << @socket.read_nonblock(CHUNK_SIZE)
132
+ while @separator && !resp.end_with?(@separator) do
133
+ resp << @socket.read_nonblock(CHUNK_SIZE)
134
+ end
135
+ resp
136
+ rescue Errno::EAGAIN
137
+ if IO.select([@socket], nil, nil, @timeout)
138
+ retry
139
+ else
140
+ self.disconnect
141
+ raise Timeout::Error, "No response in #{@timeout} seconds."
142
+ end
143
+ rescue EOFError => e
144
+ log(:info, "Server disconnected.")
145
+ self.disconnect
146
+ raise e
147
+ rescue Errno::EPIPE => e
148
+ log(:info, "Broken pipe.")
149
+ self.disconnect
150
+ raise e
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,8 @@
1
+ module EasySockets
2
+ CR = "\r"
3
+ LF = "\n"
4
+ CRLF = CR+LF
5
+ CHUNK_SIZE = 1024 * 16
6
+ DEFAULT_HOST = '127.0.0.1'
7
+ DEFAULT_PORT = 2000
8
+ end
@@ -0,0 +1,83 @@
1
+ require 'logger'
2
+ require 'socket'
3
+ require 'easy_sockets/constants'
4
+ require 'easy_sockets/utils/server_utils'
5
+
6
+ module EasySockets
7
+ #
8
+ # This class was created for testing purposes only. It should not be used
9
+ # in production.
10
+ #
11
+ class TcpServer
12
+ include EasySockets::ServerUtils
13
+
14
+ attr_reader :connections
15
+
16
+ DEFAULT_TIMEOUT = 0.5 # seconds
17
+
18
+ def initialize(opts={})
19
+ set_opts(opts)
20
+ @started = false
21
+ @stop_requested = false
22
+ @connections = []
23
+ register_shutdown_signals
24
+ end
25
+
26
+ def start
27
+ return if @started
28
+ @started = true
29
+ @server = TCPServer.new(@port)
30
+ @logger.info "Listening on tcp://127.0.0.1:#{@port}"
31
+ loop do
32
+ shutdown if @stop_requested
33
+ connection = accept_non_block(@server)
34
+ @connections << connection
35
+ handle(connection)
36
+ end
37
+ end
38
+
39
+ def stop
40
+ return unless @started
41
+ @stop_requested = true
42
+ end
43
+
44
+ private
45
+
46
+ def set_opts(opts)
47
+ @port = opts[:port].to_i
48
+ @port = DEFAULT_PORT if @port <= 0
49
+
50
+ @logger = opts[:logger] || Logger.new(STDOUT)
51
+
52
+ @separator = opts[:separator]
53
+ @separator ||= EasySockets::CRLF
54
+
55
+ @sleep_time = opts[:sleep_time].to_f
56
+ @sleep_time = 0.0001 if @sleep_time <= 0.0
57
+
58
+ @timeout = opts[:timeout].to_f
59
+ @timeout = DEFAULT_TIMEOUT if @timeout <= 0.0
60
+ end
61
+
62
+ def handle(connection)
63
+ loop do
64
+ shutdown if @stop_requested
65
+ begin
66
+ msg = read_non_block(connection)
67
+ next if msg.nil? || msg.empty?
68
+ sleep @sleep_time
69
+ write_non_block(connection, msg)
70
+ if msg.chomp == 'simulate_crash'
71
+ connection.close
72
+ break
73
+ end
74
+ rescue EOFError, Errno::ECONNRESET
75
+ connection.close
76
+ @logger.info 'Client disconnected.'
77
+ break
78
+ end
79
+ end
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,56 @@
1
+ require 'easy_sockets/basic_socket'
2
+
3
+ module EasySockets
4
+ #
5
+ # @author Marcos Ortiz
6
+ # Subclass of {EasySockets::BasicSocket} that implement a TCP socket.
7
+ #
8
+ class TcpSocket < EasySockets::BasicSocket
9
+
10
+ #
11
+ # @param [Hash] opts the options to create a socket with.
12
+ # @option opts [Integer] :port (2000) The tcp port the server is running on.
13
+ # @option opts [String] :host ('127.0.0.1') The hostname or IP address the server is running on.
14
+ #
15
+ # It also accepts all options that {EasySockets::BasicSocket#initialize} accepts
16
+ def initialize(opts={})
17
+ super(opts)
18
+
19
+ @port = opts[:port].to_i || '127.0.0.1'
20
+ @port = DEFAULT_PORT if @port <= 0
21
+ @host = opts[:host] || DEFAULT_HOST
22
+ end
23
+
24
+ private
25
+
26
+ def on_connect
27
+ @socket = Socket.new(:INET, :STREAM)
28
+ begin
29
+ # Initiate a nonblocking connection
30
+ remote_addr = Socket.pack_sockaddr_in(@port, @host)
31
+ @socket.connect_nonblock(remote_addr)
32
+
33
+ rescue Errno::EINPROGRESS
34
+ # Indicates that the connect is in progress. We monitor the
35
+ # socket for it to become writable, signaling that the connect
36
+ # is completed.
37
+ #
38
+ # Once it retries the above block of code it
39
+ # should fall through to the EISCONN rescue block and end up
40
+ # outside this entire begin block where the socket can be used.
41
+ if IO.select(nil, [@socket], nil, @timeout)
42
+ retry
43
+ else
44
+ @socket.close if @socket && !@socket.closed?
45
+ raise Timeout::Error.new("Timeout is set to #{@timeout} seconds.")
46
+ end
47
+ rescue Errno::EISCONN
48
+ # Indicates that the connect is completed successfully.
49
+ end
50
+ log(:debug, "Successfully connected to tcp://#{@host}:#{@port}.")
51
+ rescue Exception => e
52
+ @socket.close if @socket && !@socket.closed?
53
+ raise e
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,83 @@
1
+ require 'logger'
2
+ require 'socket'
3
+ require 'easy_sockets/constants'
4
+ require 'easy_sockets/utils/server_utils'
5
+
6
+ module EasySockets
7
+ #
8
+ # This class was created for testing purposes only. It should not be used
9
+ # in production.
10
+ #
11
+ class UnixServer
12
+ include EasySockets::ServerUtils
13
+
14
+ attr_reader :connections
15
+
16
+ DEFAULT_TIMEOUT = 0.5 # seconds
17
+
18
+ def initialize(opts={})
19
+ set_opts(opts)
20
+ @started = false
21
+ @stop_requested = false
22
+ @connections = []
23
+ register_shutdown_signals
24
+ end
25
+
26
+ def start
27
+ return if @started
28
+ @started = true
29
+ @server = UNIXServer.new(@socket_path)
30
+ @logger.info "Listening on #{@socket_path}"
31
+ loop do
32
+ shutdown if @stop_requested
33
+ connection = accept_non_block(@server)
34
+ @connections << connection
35
+ handle(connection)
36
+ end
37
+ end
38
+
39
+ def stop
40
+ return unless @started
41
+ @stop_requested = true
42
+ end
43
+
44
+ private
45
+
46
+ def set_opts(opts)
47
+ @socket_path = opts[:socket_path]
48
+ @socket_path ||= '/tmp/unix_server'
49
+
50
+ @logger = opts[:logger] || Logger.new(STDOUT)
51
+
52
+ @separator = opts[:separator]
53
+ @separator ||= EasySockets::CRLF
54
+
55
+ @sleep_time = opts[:sleep_time].to_f
56
+ @sleep_time = 0.0001 if @sleep_time <= 0.0
57
+
58
+ @timeout = opts[:timeout].to_f
59
+ @timeout = DEFAULT_TIMEOUT if @timeout <= 0.0
60
+ end
61
+
62
+ def handle(connection)
63
+ loop do
64
+ shutdown if @stop_requested
65
+ begin
66
+ msg = read_non_block(connection)
67
+ next if msg.nil? || msg.empty?
68
+ sleep @sleep_time
69
+ write_non_block(connection, msg)
70
+ if msg.chomp == 'simulate_crash'
71
+ connection.close
72
+ break
73
+ end
74
+ rescue EOFError, Errno::ECONNRESET, Errno::EPIPE
75
+ connection.close
76
+ @logger.info 'Client disconnected.'
77
+ break
78
+ end
79
+ end
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,56 @@
1
+ require 'easy_sockets/basic_socket'
2
+
3
+ module EasySockets
4
+ #
5
+ # @author Marcos Ortiz
6
+ # Subclass of {EasySockets::BasicSocket} that implement a Unix socket.
7
+ #
8
+ class UnixSocket < EasySockets::BasicSocket
9
+
10
+ DEFAULT_SOCKET_PATH = '/tmp/unix_socket'
11
+
12
+ #
13
+ # @param [Hash] opts the options to create a socket with.
14
+ # @option opts [Integer] :socket_path ('/tmp/unix_socket') The unix socket file path.
15
+ #
16
+ # It also accepts all options that {EasySockets::BasicSocket#initialize} accepts
17
+ def initialize(opts={})
18
+ super(opts)
19
+ @socket_path = opts[:socket_path] || DEFAULT_SOCKET_PATH
20
+ end
21
+
22
+ private
23
+
24
+ def on_connect
25
+ @socket = Socket.new(:UNIX, :STREAM)
26
+ begin
27
+ # Initiate a nonblocking connection
28
+ remote_addr = Socket.pack_sockaddr_un(@socket_path)
29
+ @socket.connect_nonblock(remote_addr)
30
+
31
+ rescue Errno::EINPROGRESS
32
+ # Indicates that the connect is in progress. We monitor the
33
+ # socket for it to become writable, signaling that the connect
34
+ # is completed.
35
+ #
36
+ # Once it retries the above block of code it
37
+ # should fall through to the EISCONN rescue block and end up
38
+ # outside this entire begin block where the socket can be used.
39
+ if IO.select(nil, [@socket], nil, @timeout)
40
+ retry
41
+ else
42
+ @socket.close if @socket && !@socket.closed?
43
+ raise Timeout::Error.new("Timeout is set to #{@timeout} seconds.")
44
+ end
45
+ rescue Errno::EISCONN
46
+ # Indicates that the connect is completed successfully.
47
+ end
48
+
49
+ log(:debug, "Successfully connected to #{@socket_path}.")
50
+ rescue Exception => e
51
+ @socket.close if @socket && !@socket.closed?
52
+ raise e
53
+ end
54
+ end
55
+
56
+ end
@@ -0,0 +1,69 @@
1
+ module EasySockets
2
+ module ServerUtils
3
+
4
+ def register_shutdown_signals
5
+ [:INT, :QUIT, :TERM].each do |signal|
6
+ Signal.trap(signal) do
7
+ t = Thread.new do
8
+ stop
9
+ end
10
+ t.join
11
+ end
12
+ end
13
+ end
14
+
15
+ def accept_non_block(server)
16
+ begin
17
+ connection = server.accept_nonblock
18
+ rescue Errno::EAGAIN
19
+ shutdown if @stop_requested
20
+ retry
21
+ end
22
+ end
23
+
24
+ def read_non_block(connection)
25
+ msg = ''
26
+ begin
27
+ msg << connection.read_nonblock(EasySockets::CHUNK_SIZE)
28
+ while !msg.end_with?(@separator) do
29
+ msg << connection.read_nonblock(EasySockets::CHUNK_SIZE)
30
+ end
31
+ rescue Errno::EAGAIN
32
+ if IO.select([connection], nil, nil, @timeout)
33
+ retry
34
+ end
35
+ end
36
+ @logger.info "Got: #{msg.inspect}" unless msg.nil? || msg.empty?
37
+ msg
38
+ end
39
+
40
+ def write_non_block(connection, msg)
41
+ return 0 unless msg && msg.is_a?(String)
42
+ total_bytes = 0
43
+ begin
44
+ loop do
45
+ bytes = connection.write_nonblock(msg)
46
+ total_bytes += bytes
47
+ break if bytes >= msg.size
48
+ msg.slice!(0, bytes)
49
+ IO.select(nil, [connection])
50
+ end
51
+ @logger.info "Sent: #{msg.inspect}"
52
+ total_bytes
53
+ rescue Errno::EAGAIN
54
+ IO.select(nil, [connection], nil, @timeout)
55
+ end
56
+ end
57
+
58
+ def shutdown
59
+ if @stop_requested
60
+ @connections.each do |c|
61
+ c.close
62
+ @logger.info "Server shutting down: closed connection #{c}."
63
+ end
64
+ exit
65
+ end
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,7 @@
1
+ module EasySockets
2
+ module Utils
3
+ def log(level, msg)
4
+ logger.send(level, msg) unless logger.nil?
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module EasySockets
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,7 @@
1
+ require "easy_sockets/version"
2
+ require "easy_sockets/tcp/tcp_socket"
3
+ require "easy_sockets/unix/unix_socket"
4
+
5
+ module EasySockets
6
+ # Your code goes here...
7
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: easy_sockets
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Marcos Ortiz
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-06-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: Over and over I see developers struggling to implement basic sockets
56
+ with featues available on ruby socket stdlib. easy_sockets, takes care of basic
57
+ details that usually are overlooked by developers when implementing TCP/Unix sockets
58
+ from scratch.
59
+ email:
60
+ - marcos.ortiz@icloud.com
61
+ executables: []
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - ".gitignore"
66
+ - ".rspec"
67
+ - ".travis.yml"
68
+ - ".yardopts"
69
+ - CHANGELOG.rb
70
+ - Gemfile
71
+ - LICENSE.txt
72
+ - README.md
73
+ - Rakefile
74
+ - bin/console
75
+ - bin/setup
76
+ - easy_sockets.gemspec
77
+ - examples/tcp_socket.rb
78
+ - examples/unix_socket.rb
79
+ - lib/easy_sockets.rb
80
+ - lib/easy_sockets/basic_socket.rb
81
+ - lib/easy_sockets/constants.rb
82
+ - lib/easy_sockets/tcp/tcp_server.rb
83
+ - lib/easy_sockets/tcp/tcp_socket.rb
84
+ - lib/easy_sockets/unix/unix_server.rb
85
+ - lib/easy_sockets/unix/unix_socket.rb
86
+ - lib/easy_sockets/utils.rb
87
+ - lib/easy_sockets/utils/server_utils.rb
88
+ - lib/easy_sockets/version.rb
89
+ homepage: https://github.com/marcosortiz/easy_sockets
90
+ licenses:
91
+ - MIT
92
+ metadata: {}
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubyforge_project:
109
+ rubygems_version: 2.5.1
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: Wrapper around ruby socket stdlib to make developer's life easier'.
113
+ test_files: []