networking 0.0.8
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/lib/networking/tcp.rb +217 -0
- data/lib/networking/version.rb +5 -0
- data/lib/networking.rb +7 -0
- data/spec/networking/tcp_spec.rb +96 -0
- metadata +65 -0
@@ -0,0 +1,217 @@
|
|
1
|
+
require 'log'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module BBFS
|
5
|
+
module Networking
|
6
|
+
|
7
|
+
Params.integer('client_retry_delay', 60, 'Number of seconds before trying to reconnect.')
|
8
|
+
|
9
|
+
# TODO(kolman): To get robustness, just use try catch + return 0 bytes on write +
|
10
|
+
# return false on status on read.
|
11
|
+
def Networking.write_to_stream(stream, obj)
|
12
|
+
Log.debug3('Writing to stream.')
|
13
|
+
marshal_data = Marshal.dump(obj)
|
14
|
+
Log.debug2("Writing data size: #{marshal_data.length}")
|
15
|
+
data_size = [marshal_data.length].pack("l")
|
16
|
+
if data_size.nil? || marshal_data.nil?
|
17
|
+
Log.debug3 'Send data size is nil!'
|
18
|
+
end
|
19
|
+
begin
|
20
|
+
bytes_written = stream.write data_size
|
21
|
+
bytes_written += stream.write marshal_data
|
22
|
+
return bytes_written
|
23
|
+
rescue Exception => e
|
24
|
+
Log.warning("Could not write tcp/ip stream, #{e.to_s}")
|
25
|
+
begin
|
26
|
+
stream.close()
|
27
|
+
rescue IOError => e
|
28
|
+
Log.warning("Could not close stream, #{e.to_s}.")
|
29
|
+
end
|
30
|
+
return 0
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns pair [status(true/false), obj]
|
35
|
+
def Networking.read_from_stream(stream)
|
36
|
+
Log.debug3('Read from stream.')
|
37
|
+
begin
|
38
|
+
return [false, nil] unless size_of_data = stream.read(4)
|
39
|
+
size_of_data = size_of_data.unpack("l")[0]
|
40
|
+
Log.debug2("Reading data size:#{size_of_data}")
|
41
|
+
data = stream.read(size_of_data)
|
42
|
+
rescue Exception => e
|
43
|
+
Log.warning("Could not read tcp/ip stream, #{e.to_s}.")
|
44
|
+
begin
|
45
|
+
stream.close()
|
46
|
+
rescue IOError => e
|
47
|
+
Log.warning("Could not close stream, #{e.to_s}.")
|
48
|
+
end
|
49
|
+
return [false, nil]
|
50
|
+
end
|
51
|
+
|
52
|
+
unmarshalled_data = Marshal.load(data)
|
53
|
+
Log.debug3('Read good.')
|
54
|
+
return [true, unmarshalled_data]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Limit on number of concurrent connections?
|
58
|
+
# The TCP server is not responsible for reconnection.
|
59
|
+
class TCPServer
|
60
|
+
attr_reader :tcp_thread
|
61
|
+
|
62
|
+
def initialize(port, obj_clb, new_clb=nil, closed_clb=nil, max_parallel=1)
|
63
|
+
@port = port
|
64
|
+
@obj_clb = obj_clb
|
65
|
+
@new_clb = new_clb
|
66
|
+
@closed_clb = closed_clb
|
67
|
+
# Max parallel connections is not implemented yet.
|
68
|
+
@max_parallel = max_parallel
|
69
|
+
@sockets = {}
|
70
|
+
@tcp_thread = run_server
|
71
|
+
@tcp_thread.abort_on_exception = true
|
72
|
+
end
|
73
|
+
|
74
|
+
def send_obj(obj, addr_info=nil)
|
75
|
+
Log.debug3("addr_info=#{addr_info}")
|
76
|
+
unless addr_info.nil?
|
77
|
+
if @sockets.key?(addr_info)
|
78
|
+
Networking.write_to_stream(@sockets[addr_info], obj)
|
79
|
+
else
|
80
|
+
Log.warning("Could not find client socket: #{addr_info}")
|
81
|
+
return 0
|
82
|
+
end
|
83
|
+
else
|
84
|
+
out = {}
|
85
|
+
@sockets.each { |key, sock| out[key] = Networking.write_to_stream(sock, obj) }
|
86
|
+
return out
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
# Creates new thread/pool
|
92
|
+
def run_server
|
93
|
+
Log.debug3('run_server')
|
94
|
+
return Thread.new do
|
95
|
+
Log.debug3('run_server2')
|
96
|
+
loop {
|
97
|
+
begin
|
98
|
+
Socket.tcp_server_loop(@port) do |sock, addr_info|
|
99
|
+
Log.debug3("----- #{@port} -----")
|
100
|
+
Log.debug3("tcp_server_loop... #{sock} #{addr_info.inspect}")
|
101
|
+
@sockets[addr_info] = sock
|
102
|
+
@new_clb.call(addr_info) if @new_clb != nil
|
103
|
+
loop do
|
104
|
+
# Blocking read.
|
105
|
+
Log.debug3('read_from_stream')
|
106
|
+
stream_ok, obj = Networking.read_from_stream(sock)
|
107
|
+
Log.debug3("Server returned from read: #{stream_ok}")
|
108
|
+
@obj_clb.call(addr_info, obj) if @obj_clb != nil && stream_ok
|
109
|
+
break if !stream_ok
|
110
|
+
end
|
111
|
+
Log.warning("Connection broken, #{addr_info.inspect}")
|
112
|
+
begin
|
113
|
+
@sockets[addr_info].close() unless @sockets[addr_info].nil?
|
114
|
+
rescue IOError => e
|
115
|
+
Log.warning("Could not close socket, #{e.to_s}.")
|
116
|
+
end
|
117
|
+
@sockets.delete(addr_info)
|
118
|
+
@closed_clb.call(addr_info) if @closed_clb != nil
|
119
|
+
end
|
120
|
+
# !!! note this break is needed for tests only !!!! server loop should never end.
|
121
|
+
# and if it ends, it will fail, and rescue will work.
|
122
|
+
break
|
123
|
+
rescue IOError => e
|
124
|
+
Log.info("Connection broken during tcp_server_loop. Restarting server loop " \
|
125
|
+
"port:#{port}.")
|
126
|
+
end
|
127
|
+
}
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end # TCPServer
|
131
|
+
|
132
|
+
class TCPClient
|
133
|
+
attr_reader :tcp_thread
|
134
|
+
|
135
|
+
def initialize(host, port, obj_clb=nil, reconnected_clb=nil)
|
136
|
+
@host = host
|
137
|
+
@port = port
|
138
|
+
@tcp_socket = nil
|
139
|
+
@obj_clb = obj_clb
|
140
|
+
@reconnected_clb = reconnected_clb
|
141
|
+
Log.debug1('TCPClient init.')
|
142
|
+
Log.debug1("TCPClient init...#{@obj_clb}...")
|
143
|
+
if @obj_clb != nil
|
144
|
+
@tcp_thread = start_reading
|
145
|
+
@tcp_thread.abort_on_exception = true
|
146
|
+
end
|
147
|
+
# Variable to signal when remote server is ready.
|
148
|
+
@remote_server_available = ConditionVariable.new
|
149
|
+
@remote_server_available_mutex = Mutex.new
|
150
|
+
open_socket unless socket_good?
|
151
|
+
end
|
152
|
+
|
153
|
+
def send_obj(obj)
|
154
|
+
Log.debug1('send_obj')
|
155
|
+
open_socket unless socket_good?
|
156
|
+
Log.debug1('after open')
|
157
|
+
if !socket_good?
|
158
|
+
Log.warning('Socket not opened for writing, skipping send.')
|
159
|
+
return false
|
160
|
+
end
|
161
|
+
Log.debug1('writing...')
|
162
|
+
#Log.debug3("socket port: #{@tcp_socket.peeraddr}")
|
163
|
+
bytes_written = Networking.write_to_stream(@tcp_socket, obj)
|
164
|
+
return bytes_written
|
165
|
+
end
|
166
|
+
|
167
|
+
# This function may be executed only from one thread!!! or in synchronized manner.
|
168
|
+
# private
|
169
|
+
def open_socket
|
170
|
+
Log.debug1("Connecting to content server #{@host}:#{@port}.")
|
171
|
+
begin
|
172
|
+
@tcp_socket = TCPSocket.new(@host, @port)
|
173
|
+
rescue Errno::ECONNREFUSED
|
174
|
+
Log.warning('Connection refused')
|
175
|
+
end
|
176
|
+
Log.debug1("Reconnect clb: '#{@reconnected_clb.nil? ? 'nil' : @reconnected_clb}'")
|
177
|
+
if socket_good?
|
178
|
+
@remote_server_available_mutex.synchronize {
|
179
|
+
@remote_server_available.signal
|
180
|
+
}
|
181
|
+
@reconnected_clb.call if @reconnected_clb != nil && socket_good?
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
def socket_good?
|
187
|
+
Log.debug1 "socket_good? #{@tcp_socket != nil && !@tcp_socket.closed?}"
|
188
|
+
return @tcp_socket != nil && !@tcp_socket.closed?
|
189
|
+
end
|
190
|
+
|
191
|
+
private
|
192
|
+
def start_reading
|
193
|
+
Log.debug1('start_reading (TCPClient).')
|
194
|
+
return Thread.new do
|
195
|
+
loop do
|
196
|
+
Log.debug3('Start blocking read (TCPClient).')
|
197
|
+
# Blocking read.
|
198
|
+
if !socket_good?
|
199
|
+
Log.warning("Socket not good, waiting for reconnection with " \
|
200
|
+
"#{Params['client_retry_delay']} seconds timeout.")
|
201
|
+
@remote_server_available_mutex.synchronize {
|
202
|
+
@remote_server_available.wait(@remote_server_available_mutex,
|
203
|
+
Params['client_retry_delay'])
|
204
|
+
}
|
205
|
+
else
|
206
|
+
read_ok, obj = Networking.read_from_stream(@tcp_socket)
|
207
|
+
Log.debug3("Client returned from read: #{read_ok}")
|
208
|
+
# Handle case when socket is closed in middle.
|
209
|
+
# In that case we should not call obj_clb.
|
210
|
+
@obj_clb.call(obj) if (read_ok && @obj_clb != nil)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end # TCPClient
|
216
|
+
end
|
217
|
+
end
|
data/lib/networking.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'log'
|
2
|
+
require 'rspec'
|
3
|
+
require 'socket'
|
4
|
+
require 'stringio'
|
5
|
+
|
6
|
+
require_relative '../../lib/networking/tcp'
|
7
|
+
|
8
|
+
# Uncomment to debug spec.
|
9
|
+
BBFS::Params['log_write_to_console'] = true
|
10
|
+
BBFS::Params['log_debug_level'] = 0
|
11
|
+
BBFS::Log.init
|
12
|
+
|
13
|
+
module BBFS
|
14
|
+
module Networking
|
15
|
+
module Spec
|
16
|
+
|
17
|
+
describe 'TCPClient' do
|
18
|
+
it 'should warn when destination host+port are bad' do
|
19
|
+
data = 'kuku!!!'
|
20
|
+
stream = StringIO.new
|
21
|
+
stream.close()
|
22
|
+
::TCPSocket.stub(:new).and_return(stream)
|
23
|
+
BBFS::Log.should_receive(:warning).with('Socket not opened for writing, skipping send.')
|
24
|
+
|
25
|
+
# Send data first.
|
26
|
+
tcp_client = TCPClient.new('kuku', 5555)
|
27
|
+
# Send has to fail.
|
28
|
+
tcp_client.send_obj(data).should be(false)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should send data and server should receive callback function' do
|
32
|
+
info = 'info'
|
33
|
+
data = 'kuku!!!'
|
34
|
+
stream = StringIO.new
|
35
|
+
::Socket.stub(:tcp_server_loop).and_yield(stream, info)
|
36
|
+
::TCPSocket.stub(:new).and_return(stream)
|
37
|
+
|
38
|
+
# Send data first.
|
39
|
+
tcp_client = TCPClient.new('kuku', 5555)
|
40
|
+
# Send has to be successful.
|
41
|
+
tcp_client.send_obj(data).should be(21)
|
42
|
+
|
43
|
+
# Note this is very important so that reading the stream from beginning.
|
44
|
+
stream.rewind
|
45
|
+
|
46
|
+
func = lambda { |info, data| Log.info("info, data: #{info}, #{data}") }
|
47
|
+
# Check data is received.
|
48
|
+
func.should_receive(:call).with(info, data)
|
49
|
+
|
50
|
+
tcp_server = TCPServer.new(5555, func)
|
51
|
+
# Wait on server thread.
|
52
|
+
tcp_server.tcp_thread.join
|
53
|
+
end
|
54
|
+
|
55
|
+
# TODO(kolman): Don't work, missing client send_obj/open_socket execution in
|
56
|
+
# the correct place.
|
57
|
+
it 'should connect and receive callback from server' do
|
58
|
+
info = 'info'
|
59
|
+
data = 'kuku!!!'
|
60
|
+
stream = StringIO.new
|
61
|
+
::Socket.stub(:tcp_server_loop).once.and_yield(stream, info)
|
62
|
+
::TCPSocket.stub(:new).and_return(stream)
|
63
|
+
|
64
|
+
tcp_server = nil
|
65
|
+
new_clb = lambda { |i|
|
66
|
+
Log.info("#2 - After after @sockets is filled. Send has to be successful.")
|
67
|
+
tcp_server.send_obj(data).should eq({info => 21})
|
68
|
+
|
69
|
+
stream.rewind
|
70
|
+
|
71
|
+
func = lambda { |d|
|
72
|
+
Log.info("#6, After client initialized, it should send object.")
|
73
|
+
Log.info("data: #{d}")
|
74
|
+
# Validate received data.
|
75
|
+
d.should eq(data)
|
76
|
+
# Exit tcp client reading loop thread.
|
77
|
+
Thread.exit
|
78
|
+
}
|
79
|
+
|
80
|
+
Log.info("#4 - Create client and wait for read data.")
|
81
|
+
tcp_client = TCPClient.new('kuku', 5555, func)
|
82
|
+
tcp_client.tcp_thread.abort_on_exception = true
|
83
|
+
tcp_client.tcp_thread.join()
|
84
|
+
# We can finish now. Exit tcp server listening loop.
|
85
|
+
Thread.exit
|
86
|
+
}
|
87
|
+
|
88
|
+
Log.info("#1 - Create server, send data and wait.")
|
89
|
+
tcp_server = TCPServer.new(5555, nil, new_clb)
|
90
|
+
tcp_server.tcp_thread.abort_on_exception = true
|
91
|
+
tcp_server.tcp_thread.join()
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: networking
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.8
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Kolman Vornovitsky
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-09-02 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: params
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: Easy to use simple tools for networking (tcp/ip, ect,...).
|
31
|
+
email: kolmanv@gmail.com
|
32
|
+
executables: []
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- lib/networking.rb
|
37
|
+
- lib/networking/tcp.rb
|
38
|
+
- lib/networking/version.rb
|
39
|
+
- spec/networking/tcp_spec.rb
|
40
|
+
homepage: http://github.com/kolmanv/bbfs
|
41
|
+
licenses: []
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ! '>='
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ! '>='
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
requirements: []
|
59
|
+
rubyforge_project:
|
60
|
+
rubygems_version: 1.8.23
|
61
|
+
signing_key:
|
62
|
+
specification_version: 3
|
63
|
+
summary: Easy to use simple tools for networking (tcp/ip, ect,...).
|
64
|
+
test_files:
|
65
|
+
- spec/networking/tcp_spec.rb
|