sping 1.0.4 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f7679b3fb31d07d034182114e576f3e272ef8223954f592bb30a98378b98fd5
4
- data.tar.gz: b8d21d6a58ffb3c57481cc421d61f8fc25da32646fb8d5ae160829017d01fb14
3
+ metadata.gz: 7dc97d43dd1c7805574269a6ebbe8113c49c431bdfa46783b8e43ebef14063a5
4
+ data.tar.gz: ffb03def610179c609ffd4046cd5a7da5659e73b83ae867a123a2fcf62309b72
5
5
  SHA512:
6
- metadata.gz: a6a2c6897ead8f7371b2f1a83719c1a8f2c07d44957a9c302bc548855d25671858435fe0d6e8162e14737e065f6bbcede37defd2ea21e23561d19e7f8d5ef63f
7
- data.tar.gz: 2618e70abea52acc38364f7fb431dafa12a4d48ad8f07665f93ac76e316784e6a2f8f5d2b463a55053da88b10f46dc23adf16846abb1a4b1d4677eb9eef09e7f
6
+ metadata.gz: 637dc51c01d6f89bb26a6175d3ac0f0a16921398db3244164efea4b1de457cd3ef1329f3b4ee6eed69a1a57961a6c0d64c31fab7f4fb1fed150183212d830b96
7
+ data.tar.gz: 5bc1661058eb425277ed0d37429b5b0e8376690a6665aeb85e8bda35b96b85fd3a4025f0b4cc2020aef7b9e1f847b24845b6db9e841705749a7db3a20e4e9ee3
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
1
  # sping
2
2
 
3
- This is a reimplementation of [sping](https://github.com/benjojo/sping/) in Ruby.
3
+ Program for measuring asymmetric latencies.
4
+
5
+ This is a reimplementation in Ruby of the reference implementation of the sping protocol in Go. The program provides both the client and server part to measure asymmetric latencies between two peers.
data/lib/errors.rb CHANGED
@@ -1,20 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
  # sharable_constant_value: literal
3
3
 
4
+ # SPing / Splitted Ping is a protocol for measuring asymmetric latencies.
4
5
  module SPing
6
+ # Generic SPing error.
5
7
  class SPingError < StandardError; end
6
8
 
9
+ # Error that occurred while managing the sessions.
7
10
  class SessionManagerError < SPingError; end
11
+
12
+ # Error that is thrown when there are not enough session IDs available to establish a
13
+ # connection to a peer or to accept new ones.
14
+ # This indicates either a programmer error or an overload of the server.
8
15
  class OutOfSessions < SessionManagerError; end
9
16
 
17
+ # Error that occurs in connection with a session.
10
18
  class SessionError < SPingError; end
19
+
20
+ # Error during the TCP handshake
11
21
  class TCPHandshakeError < SessionError; end
12
22
 
23
+ # Error due to the peer (i.e. not self-inflicted).
13
24
  class PeerError < SPingError; end
25
+
26
+ # Package was not coded correctly.
14
27
  class InvalidPacketError < PeerError; end
28
+
29
+ # The package was coded correctly, but the content is invalid.
15
30
  class InvalidMessageError < InvalidPacketError; end
31
+
32
+ # A packet has been received which cannot be clearly assigned to SPing
33
+ # and therefore cannot be processed further.
16
34
  class UnknownPacketError < InvalidPacketError; end
35
+
36
+ # The peer has signaled an error.
17
37
  class SignalizedError < PeerError; end
18
- class UnknownVersionError < PeerError; end
38
+
39
+ # The peer uses a version of sping that is not supported.
40
+ class UnsupportedVersionError < PeerError; end
41
+
42
+ # The package could not be assigned to a current session.
19
43
  class UnknownSessionError < PeerError; end
20
44
  end
data/lib/last_acks.rb CHANGED
@@ -1,16 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
  # sharable_constant_value: literal
3
3
 
4
+ # SPing / Splitted Ping is a protocol for measuring asymmetric latencies.
4
5
  module SPing
6
+ # A kind of circular buffer, which consists of 32 acks at any given time. If no acks are available,
7
+ # empty ones are created. If a new ack is added (which would result in 33 acks), one is removed.
5
8
  class LastAcks
6
- attr_reader :size
7
-
8
- def initialize(size = 32)
9
- @size = size
9
+ # Creates a new circular buffer for acks
10
+ def initialize
11
+ # Array in which the acks are stored.
10
12
  @acks = []
13
+ # Mutex, which ensures that the array is not accessed simultaneously.
11
14
  @acks_mutex = Mutex.new
12
15
 
13
- @size.times do |index|
16
+ # Initiallize the circular buffer with 32 empty acks.
17
+ 32.times do |index|
14
18
  @acks[index] = {
15
19
  'R' => 0,
16
20
  'U' => Time.at(0),
@@ -19,17 +23,20 @@ module SPing
19
23
  end
20
24
  end
21
25
 
26
+ # Returns a copy of the last 32 acks.
27
+ # @return [Array] Last 32 Acks
22
28
  def acks
23
29
  @acks_mutex.synchronize do
24
30
  return @acks.dup
25
31
  end
26
32
  end
27
33
 
34
+ # Adds a new ack and deletes the oldest one
35
+ # @param ack [Hash] New ack to be added
28
36
  def add_ack(ack)
29
37
  @acks_mutex.synchronize do
30
38
  @acks << ack
31
-
32
- @acks.shift if @acks.length > @size
39
+ @acks.shift
33
40
  end
34
41
  end
35
42
  end
data/lib/session.rb CHANGED
@@ -1,68 +1,118 @@
1
1
  # frozen_string_literal: true
2
2
  # sharable_constant_value: literal
3
3
 
4
+ # SPing / Splitted Ping is a protocol for measuring asymmetric latencies.
4
5
  module SPing
6
+ # Inclusion of all possible SPing-specific errors.
5
7
  require_relative 'errors'
6
8
 
9
+ # Models or represents a session with a peer.
7
10
  class Session
8
- attr_reader :created, :last_rx, :madebyme
9
- attr_accessor :session_id, :tcp_handshake_complete, :udp_handshake_complete
11
+ # Time when the session was created. This is necessary so that the session GC knows when a
12
+ # non-initialized session can be deleted.
13
+ # @return [Time]
14
+ attr_reader :created
15
+ # Time when the last ping packet was received by the peer. This is important so that the session
16
+ # GC knows when a session is considered inactive and can be deleted.
17
+ # @return [Time]
18
+ attr_reader :last_rx
19
+ # Indicates whether the session has been initialized. true if we have initiated it.
20
+ # false if the peer has initiated it.
21
+ # @return [TrueClass, FalseClass]
22
+ attr_reader :madebyme
23
+ # Indicates whether a TCP handshake has been carried out (successfully).
24
+ # @return [TrueClass, FalseClass]
25
+ attr_reader :tcp_handshake_complete
26
+ # Indicates whether a UDP handshake has been carried out (successfully).
27
+ # @return [TrueClass, FalseClass]
28
+ attr_reader :udp_handshake_complete
29
+ # The session ID in the range from 1 to 2**32 - 1.
30
+ # @return [Integer]
31
+ attr_reader :session_id
10
32
 
11
33
  require 'socket'
12
34
  require 'timeout'
13
35
  require 'msgpack'
14
36
  require_relative 'last_acks'
15
37
 
38
+ # Creates a new session
39
+ # @param host [#to_s] Host of the peer
40
+ # @param port [#to_i] Port of the peer
41
+ # @param socket [UDPSocket] UDP socket via which the pings and the UDP handshake are to be sent.
42
+ # @param madebyme [TrueClass, FalseClass] Indicates whether we have initiated the session.
16
43
  def initialize(host, port, socket, madebyme)
17
- @host = host
18
- @port = port
44
+ # Assign the passed parameters to instance variables.
45
+ @host = host.to_s
46
+ @port = port.to_i
19
47
  @socket = socket
20
- @created = Time.now
21
48
  @madebyme = madebyme
22
49
 
50
+ # Initialize assignment of other instance variables
51
+ @created = Time.now
52
+
23
53
  @tcp_handshake_complete = false
24
54
  @udp_handshake_complete = false
25
55
 
26
56
  @last_acks = SPing::LastAcks.new
27
57
  end
28
58
 
59
+ # Initiates a TCP handshake.
60
+ # @param socket [TCPSocket] Socket via which the handshake is to be sent
61
+ # (and responses are to be expected).
62
+ # @param session_id [Integer] Session ID which is to be sent to the peer
63
+ # and which is to be used for the current session.
29
64
  def do_tcp_handshake1(socket, session_id)
65
+ # Send the banner
30
66
  socket.write "sping-0.3-https://codeberg.org/mark22k/sping\r\n"
67
+ # and make sure that it is no longer in the send buffer.
31
68
  socket.flush
32
69
 
70
+ # See if the peer invites us to create a session with them.
33
71
  invite = socket.readpartial 6
34
-
35
72
  raise TCPHandshakeError, 'Peer didn\'t invite us.' unless invite.chomp == 'INVITE'
36
73
 
74
+ # Send the session ID
37
75
  @session_id = session_id
38
-
39
76
  socket.write "#{session_id}\r\n"
77
+ # and make sure that it is no longer in the send buffer.
40
78
  socket.flush
41
79
 
80
+ # This means that the TCP handshake is successful. The socket can be closed
81
+ # and the corresponding instance variable can be set.
42
82
  socket.close
43
83
 
44
84
  @tcp_handshake_complete = true
45
85
  end
46
86
 
87
+ # Receives a TCP handshake from a peer.
88
+ # The peer specified when the session is created is consulted for this purpose.
47
89
  def do_tcp_handshake2
90
+ # Establish a connection and receive the banner.
48
91
  socket = TCPSocket.new @host, @port
92
+
93
+ # If the banner is too large, close the socket and throw an error.
94
+ # The handshake was not successful.
49
95
  banner = socket.readpartial 9001
50
96
  if banner.length > 9000
51
97
  socket.close
52
98
  raise TCPHandshakeError, 'Host banner too big'
53
99
  end
54
100
 
101
+ # If the banner does not match the SPing service, close the socket and
102
+ # throw an error. The handshake was not successful.
55
103
  unless banner.start_with? 'sping-0.3-'
56
104
  socket.close
57
- raise TCPHandshakeError, 'Host banner not sping'
105
+ raise TCPHandshakeError, 'Host banner not sping or unsupported version of sping.'
58
106
  end
59
107
 
60
108
  @remote_version = banner.chomp
61
109
  $logger.info "Peer uses the following program version: #{@remote_version.dump}"
62
110
 
111
+ # Invite the peer to start a session with us.
63
112
  socket.write "INVITE\r\n"
64
113
  socket.flush
65
114
 
115
+ # If the session ID is too long or none was received, close the socket and throw an error.
66
116
  invite_buf = socket.readpartial 32
67
117
  if invite_buf.length > 31 || invite_buf.empty?
68
118
  socket.close
@@ -76,15 +126,21 @@ module SPing
76
126
  @tcp_handshake_complete = true
77
127
  end
78
128
 
129
+ # Sets the endpoint consisting of the host and port of the remote end. This is done
130
+ # each time a new packet is received and ensures that the current endpoint is always available.
131
+ # @param host [#to_s]
132
+ # @param port [#to_i]
79
133
  def set_endpoint(host, port)
80
- @host = host
81
- @port = port
134
+ @host = host.to_s
135
+ @port = port.to_i
82
136
  end
83
137
 
138
+ # Starts a thread which sends the UDP handshake at regular intervals.
139
+ # @param send_interval [#to_i] The interval at which the UDP handshakes are to be sent.
84
140
  def start_udp_handshake_sender(send_interval = 5)
85
141
  raise 'UDP handshake sender is already running.' if @udp_handshake_sender
86
142
 
87
- @udp_handshake_sender = Thread.new(send_interval) do |th_send_interval|
143
+ @udp_handshake_sender = Thread.new(send_interval.to_i) do |th_send_interval|
88
144
  loop do
89
145
  send_udp_handshake
90
146
  sleep th_send_interval
@@ -92,6 +148,7 @@ module SPing
92
148
  end
93
149
  end
94
150
 
151
+ # Send a single UDP handshake
95
152
  def send_udp_handshake
96
153
  packet = {
97
154
  'Y' => 'h'.ord,
@@ -104,6 +161,7 @@ module SPing
104
161
  @socket.send packet, 0, @host, @port
105
162
  end
106
163
 
164
+ # Stop the UDP handshake sender. UDP handshakes are then no longer sent at any interval.
107
165
  def stop_udp_handshake_sender
108
166
  raise 'UDP handshake sender is not running and therefore cannot be terminated.' unless @udp_handshake_sender
109
167
 
@@ -111,11 +169,13 @@ module SPing
111
169
  @udp_handshake_sender = nil
112
170
  end
113
171
 
172
+ # Informs the session that a UDP handshake has been received.
114
173
  def udp_handshake_recived
115
174
  stop_udp_handshake_sender if @madebyme
116
175
  @udp_handshake_complete = true
117
176
  end
118
177
 
178
+ # Starts a thread which sends ping or time messages to the peer at one-second intervals.
119
179
  def start_pinger
120
180
  raise 'Pinger is already running.' if @pinger
121
181
 
@@ -127,6 +187,7 @@ module SPing
127
187
  end
128
188
  end
129
189
 
190
+ # Stops the thread, which sends ping or time messages to the peer at regular intervals.
130
191
  def stop_pinger
131
192
  raise 'Pinger sender is not running and therefore cannot be terminated.' unless @pinger
132
193
 
@@ -134,6 +195,7 @@ module SPing
134
195
  @pinger = nil
135
196
  end
136
197
 
198
+ # Sends a ping message to the peer.
137
199
  def ping
138
200
  current_id = (Time.now.to_i % 255) + 1
139
201
  data = {
@@ -150,11 +212,16 @@ module SPing
150
212
  @socket.send data, 0, @host, @port
151
213
  end
152
214
 
215
+ # Stops all threads associated with the session. This means that the session to the peer is as good as dead.
153
216
  def stop
154
217
  @pinger&.kill
155
218
  @udp_handshake_sender&.kill
156
219
  end
157
220
 
221
+ # Handler that receives and processes a receiving ping packet or time message. The current statistics are output.
222
+ # @param packet [Hash]
223
+ # @param rxtime [Time]
224
+ # @param peeraddr [#to_s]
158
225
  def handle_ping(packet, rxtime, peeraddr)
159
226
  if !(packet.keys - %w[M Y E I T A S]).empty? ||
160
227
  # M, Y are already checked in handle_packet from session manager
@@ -1,17 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
  # sharable_constant_value: literal
3
3
 
4
+ # SPing / Splitted Ping is a protocol for measuring asymmetric latencies.
4
5
  module SPing
6
+ # Inclusion of all possible SPing-specific errors.
5
7
  require_relative 'errors'
6
8
 
9
+ # Container, which contains a collection of sessions and manages them.
7
10
  class SessionManager
8
11
  require 'socket'
9
12
  require 'timeout'
10
13
  require_relative 'session'
11
14
 
15
+ # Creates a new Session Manager, which can (and will) manage a number of sessions.
16
+ # @param host [#to_s] Host to which messages are to be bound and from which messages are sent accordingly.
17
+ # @param port [#to_i] Port
12
18
  def initialize(host = '::', port = 6924)
13
- @host = host
14
- @port = port
19
+ @host = host.to_s
20
+ @port = port.to_i
15
21
 
16
22
  @sessions = {}
17
23
  @sessions_mutex = Mutex.new
@@ -20,9 +26,12 @@ module SPing
20
26
  @socket.bind @host, @port
21
27
  end
22
28
 
29
+ # Initiates a new session with a peer.
30
+ # @param host [#to_s]
31
+ # @param port [#to_s, #to_i]
23
32
  def new_session(host, port = 6924)
24
33
  $logger.info "Add new session for host #{host} port #{port}."
25
- session = SPing::Session.new host, port, @socket, true
34
+ session = SPing::Session.new host.to_s, port.to_i, @socket, true
26
35
 
27
36
  counter = 0
28
37
  loop do
@@ -48,6 +57,8 @@ module SPing
48
57
  $logger.error "Out of session: #{e.message}"
49
58
  end
50
59
 
60
+ # Stops all threads connected to a session ID and removes the session from the Session Manager administration.
61
+ # @param session_id [Integer] Session ID of the session to be removed.
51
62
  def del_session(session_id)
52
63
  $logger.debug "Delete session with session id #{session_id}"
53
64
 
@@ -57,18 +68,21 @@ module SPing
57
68
  end
58
69
  end
59
70
 
71
+ # Stops all sessions or the threads that are connected to them.
60
72
  def stop_sessions
61
73
  @sessions.each do |_session_id, session|
62
74
  session.stop
63
75
  end
64
76
  end
65
77
 
78
+ # Waits and blocks until the Session Manager is no longer running.
66
79
  def join
67
80
  @runner&.join
68
81
  @gc_thread&.join
69
82
  @server_thread&.join
70
83
  end
71
84
 
85
+ # Starts the Session Manager.
72
86
  def run
73
87
  raise 'Client is already running.' if @runner
74
88
  raise 'GC is already running.' if @gc
@@ -88,6 +102,7 @@ module SPing
88
102
  end
89
103
  end
90
104
 
105
+ # Starts the server functionality of the Session Manager.
91
106
  def run_server
92
107
  raise 'Server is already running.' if @server_thread
93
108
  raise 'Server already exist.' if @server
@@ -105,6 +120,7 @@ module SPing
105
120
  return @server_thread
106
121
  end
107
122
 
123
+ # Stops the server functionality of the Session Manager.
108
124
  def stop_server
109
125
  raise 'Server thread is not running.' unless @server_thread
110
126
 
@@ -117,6 +133,7 @@ module SPing
117
133
  @server = nil
118
134
  end
119
135
 
136
+ # Stops the Session Manager. The server functionality must be stopped separately.
120
137
  def stop
121
138
  raise 'Session manager is not running.' unless @runner
122
139
 
@@ -129,6 +146,12 @@ module SPing
129
146
  @gc_thread = nil
130
147
  end
131
148
 
149
+ private
150
+
151
+ # Generates a new session ID that was not yet in use at the time of the check.
152
+ # If none could be generated within half a minute - for example because all
153
+ # session IDs have already been assigned - an error is thrown.
154
+ # @return [Integer]
132
155
  def generate_session_id
133
156
  Timeout.timeout(30, OutOfSessions, 'No session ID could be generated which has not yet been used.') do
134
157
  loop do
@@ -138,19 +161,23 @@ module SPing
138
161
  end
139
162
  end
140
163
 
141
- private
142
-
164
+ # Removes obsolete sessions. This includes sessions that are older than one minute but
165
+ # have not been activated a double time. And sessions that have not
166
+ # received any packets for half a minute.
143
167
  def do_gc
144
168
  $logger.debug 'Remove outdated sessions.'
145
169
  # rubocop:disable Style/HashEachMethods
146
- # You cannot run through a map and edit it at the same time. Since this is necessary due to the threading,
170
+ # You cannot run through a map and edit it at the same time. Since this is
171
+ # necessary due to the threading,
147
172
  # a snapshot of the key variable is run through.
148
173
  @sessions.keys.each do |session_id|
149
174
  # rubocop:enable Style/HashEachMethods
150
175
 
151
176
  session = @sessions[session_id]
152
- # It is possible that sessions no longer exist here, as we may have session IDs that have already been deleted.
153
- # However, we can ignore this aspect here, as we are the only function that deletes sessions.
177
+ # It is possible that sessions no longer exist here, as we may have
178
+ # session IDs that have already been deleted.
179
+ # However, we can ignore this aspect here, as we are the only
180
+ # function that deletes sessions.
154
181
  if !(session.tcp_handshake_complete && session.udp_handshake_complete)
155
182
  # Handshake incomplete
156
183
  if (Time.now.to_i - session.created.to_i) > 60
@@ -159,13 +186,16 @@ module SPing
159
186
  del_session session_id
160
187
  end
161
188
  elsif (Time.now.to_i - session.last_rx.to_i) > 30
162
- # 30 seconds have elapsed since the last ping from the peer was received. The peer is probably dead.
189
+ # 30 seconds have elapsed since the last ping from the peer was received.
190
+ # The peer is probably dead.
163
191
  $logger.debug "Session id #{session_id} without activity for over thirty seconds."
164
192
  del_session session_id
165
193
  end
166
194
  end
167
195
  end
168
196
 
197
+ # Receives a TCP handshake for a session and then has it
198
+ # managed by the Session Manager.
169
199
  def request_session(session)
170
200
  session.do_tcp_handshake2
171
201
 
@@ -179,6 +209,9 @@ module SPing
179
209
  return nil
180
210
  end
181
211
 
212
+ # Handler for a received packet. The function catches errors and outputs them.
213
+ # It also forwards the packet to the corresponding handler according to the
214
+ # session ID and type of packet.
182
215
  def handle_packet(buf)
183
216
  rxtime = Time.now
184
217
 
@@ -217,6 +250,7 @@ module SPing
217
250
  $logger.error "Perr error: #{e.message}"
218
251
  end
219
252
 
253
+ # Handler for a receiving UDP handshake.
220
254
  def handle_udp_handshake(packet, peeraddr, peerport)
221
255
  if !(packet.keys - %w[M Y V S]).empty? ||
222
256
  # M, Y are already checked in handle_packet from session manager
@@ -227,7 +261,7 @@ module SPing
227
261
  session_id = packet['S']
228
262
  session = @sessions[session_id]
229
263
  raise UnknownSessionError, 'UDP handshake received for uninitiated session.' unless session
230
- raise UnknownVersionError, "UDP handshake uses an unsupported version: #{packet['V']}" if packet['V'] != 3
264
+ raise UnsupportedVersionError, "UDP handshake uses an unsupported version: #{packet['V']}" if packet['V'] != 3
231
265
 
232
266
  session.set_endpoint peeraddr, peerport
233
267
 
@@ -249,6 +283,7 @@ module SPing
249
283
  session.start_pinger
250
284
  end
251
285
 
286
+ # Handler for a receiving ping or a receiving time message.
252
287
  def handle_ping(packet, rxtime, peeraddr, peerport)
253
288
  session_id = packet['S']
254
289
  session = @sessions[session_id]
@@ -260,6 +295,7 @@ module SPing
260
295
  session.handle_ping packet, rxtime, peeraddr
261
296
  end
262
297
 
298
+ # Handler to establish a session for the request of a peer.
263
299
  def handle_client(client)
264
300
  host = client.peeraddr[3]
265
301
  port = client.peeraddr[1]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sping
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marek Küthe
@@ -30,8 +30,9 @@ dependencies:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
32
  version: 1.7.2
33
- description: This is a reimplementation of [sping](https://github.com/benjojo/sping/)
34
- in Ruby.
33
+ description: This is a reimplementation in Ruby of the reference implementation of
34
+ the sping protocol in Go. The program provides both the client and server part to
35
+ measure asymmetric latencies between two peers.
35
36
  email: m.k@mk16.de
36
37
  executables:
37
38
  - sping
@@ -72,5 +73,5 @@ requirements: []
72
73
  rubygems_version: 3.4.22
73
74
  signing_key:
74
75
  specification_version: 4
75
- summary: Reimplementation of sping in Ruby.
76
+ summary: Program for measuring asymmetric latencies.
76
77
  test_files: []