sping 1.0.3 → 1.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 +4 -4
- data/README.md +3 -1
- data/lib/errors.rb +44 -0
- data/lib/last_acks.rb +32 -23
- data/lib/session.rb +102 -17
- data/lib/session_manager.rb +89 -43
- metadata +8 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7dc97d43dd1c7805574269a6ebbe8113c49c431bdfa46783b8e43ebef14063a5
|
|
4
|
+
data.tar.gz: ffb03def610179c609ffd4046cd5a7da5659e73b83ae867a123a2fcf62309b72
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 637dc51c01d6f89bb26a6175d3ac0f0a16921398db3244164efea4b1de457cd3ef1329f3b4ee6eed69a1a57961a6c0d64c31fab7f4fb1fed150183212d830b96
|
|
7
|
+
data.tar.gz: 5bc1661058eb425277ed0d37429b5b0e8376690a6665aeb85e8bda35b96b85fd3a4025f0b4cc2020aef7b9e1f847b24845b6db9e841705749a7db3a20e4e9ee3
|
data/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
# sping
|
|
2
2
|
|
|
3
|
-
|
|
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
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# sharable_constant_value: literal
|
|
3
|
+
|
|
4
|
+
# SPing / Splitted Ping is a protocol for measuring asymmetric latencies.
|
|
5
|
+
module SPing
|
|
6
|
+
# Generic SPing error.
|
|
7
|
+
class SPingError < StandardError; end
|
|
8
|
+
|
|
9
|
+
# Error that occurred while managing the sessions.
|
|
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.
|
|
15
|
+
class OutOfSessions < SessionManagerError; end
|
|
16
|
+
|
|
17
|
+
# Error that occurs in connection with a session.
|
|
18
|
+
class SessionError < SPingError; end
|
|
19
|
+
|
|
20
|
+
# Error during the TCP handshake
|
|
21
|
+
class TCPHandshakeError < SessionError; end
|
|
22
|
+
|
|
23
|
+
# Error due to the peer (i.e. not self-inflicted).
|
|
24
|
+
class PeerError < SPingError; end
|
|
25
|
+
|
|
26
|
+
# Package was not coded correctly.
|
|
27
|
+
class InvalidPacketError < PeerError; end
|
|
28
|
+
|
|
29
|
+
# The package was coded correctly, but the content is invalid.
|
|
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.
|
|
34
|
+
class UnknownPacketError < InvalidPacketError; end
|
|
35
|
+
|
|
36
|
+
# The peer has signaled an error.
|
|
37
|
+
class SignalizedError < 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.
|
|
43
|
+
class UnknownSessionError < PeerError; end
|
|
44
|
+
end
|
data/lib/last_acks.rb
CHANGED
|
@@ -1,34 +1,43 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# sharable_constant_value: literal
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
# SPing / Splitted Ping is a protocol for measuring asymmetric latencies.
|
|
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.
|
|
8
|
+
class LastAcks
|
|
9
|
+
# Creates a new circular buffer for acks
|
|
10
|
+
def initialize
|
|
11
|
+
# Array in which the acks are stored.
|
|
12
|
+
@acks = []
|
|
13
|
+
# Mutex, which ensures that the array is not accessed simultaneously.
|
|
14
|
+
@acks_mutex = Mutex.new
|
|
6
15
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
'U' => Time.at(0),
|
|
16
|
-
'X' => Time.at(0)
|
|
17
|
-
}
|
|
16
|
+
# Initiallize the circular buffer with 32 empty acks.
|
|
17
|
+
32.times do |index|
|
|
18
|
+
@acks[index] = {
|
|
19
|
+
'R' => 0,
|
|
20
|
+
'U' => Time.at(0),
|
|
21
|
+
'X' => Time.at(0)
|
|
22
|
+
}
|
|
23
|
+
end
|
|
18
24
|
end
|
|
19
|
-
end
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
@
|
|
23
|
-
|
|
26
|
+
# Returns a copy of the last 32 acks.
|
|
27
|
+
# @return [Array] Last 32 Acks
|
|
28
|
+
def acks
|
|
29
|
+
@acks_mutex.synchronize do
|
|
30
|
+
return @acks.dup
|
|
31
|
+
end
|
|
24
32
|
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def add_ack(ack)
|
|
28
|
-
@acks_mutex.synchronize do
|
|
29
|
-
@acks << ack
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
# Adds a new ack and deletes the oldest one
|
|
35
|
+
# @param ack [Hash] New ack to be added
|
|
36
|
+
def add_ack(ack)
|
|
37
|
+
@acks_mutex.synchronize do
|
|
38
|
+
@acks << ack
|
|
39
|
+
@acks.shift
|
|
40
|
+
end
|
|
32
41
|
end
|
|
33
42
|
end
|
|
34
43
|
end
|
data/lib/session.rb
CHANGED
|
@@ -1,69 +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
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
# Inclusion of all possible SPing-specific errors.
|
|
7
|
+
require_relative 'errors'
|
|
7
8
|
|
|
9
|
+
# Models or represents a session with a peer.
|
|
8
10
|
class Session
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
11
32
|
|
|
12
33
|
require 'socket'
|
|
13
34
|
require 'timeout'
|
|
14
35
|
require 'msgpack'
|
|
15
36
|
require_relative 'last_acks'
|
|
16
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.
|
|
17
43
|
def initialize(host, port, socket, madebyme)
|
|
18
|
-
|
|
19
|
-
@
|
|
44
|
+
# Assign the passed parameters to instance variables.
|
|
45
|
+
@host = host.to_s
|
|
46
|
+
@port = port.to_i
|
|
20
47
|
@socket = socket
|
|
21
|
-
@created = Time.now
|
|
22
48
|
@madebyme = madebyme
|
|
23
49
|
|
|
50
|
+
# Initialize assignment of other instance variables
|
|
51
|
+
@created = Time.now
|
|
52
|
+
|
|
24
53
|
@tcp_handshake_complete = false
|
|
25
54
|
@udp_handshake_complete = false
|
|
26
55
|
|
|
27
|
-
@last_acks = LastAcks.new
|
|
56
|
+
@last_acks = SPing::LastAcks.new
|
|
28
57
|
end
|
|
29
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.
|
|
30
64
|
def do_tcp_handshake1(socket, session_id)
|
|
65
|
+
# Send the banner
|
|
31
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.
|
|
32
68
|
socket.flush
|
|
33
69
|
|
|
70
|
+
# See if the peer invites us to create a session with them.
|
|
34
71
|
invite = socket.readpartial 6
|
|
35
|
-
|
|
36
72
|
raise TCPHandshakeError, 'Peer didn\'t invite us.' unless invite.chomp == 'INVITE'
|
|
37
73
|
|
|
74
|
+
# Send the session ID
|
|
38
75
|
@session_id = session_id
|
|
39
|
-
|
|
40
76
|
socket.write "#{session_id}\r\n"
|
|
77
|
+
# and make sure that it is no longer in the send buffer.
|
|
41
78
|
socket.flush
|
|
42
79
|
|
|
80
|
+
# This means that the TCP handshake is successful. The socket can be closed
|
|
81
|
+
# and the corresponding instance variable can be set.
|
|
43
82
|
socket.close
|
|
44
83
|
|
|
45
84
|
@tcp_handshake_complete = true
|
|
46
85
|
end
|
|
47
86
|
|
|
87
|
+
# Receives a TCP handshake from a peer.
|
|
88
|
+
# The peer specified when the session is created is consulted for this purpose.
|
|
48
89
|
def do_tcp_handshake2
|
|
90
|
+
# Establish a connection and receive the banner.
|
|
49
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.
|
|
50
95
|
banner = socket.readpartial 9001
|
|
51
96
|
if banner.length > 9000
|
|
52
97
|
socket.close
|
|
53
98
|
raise TCPHandshakeError, 'Host banner too big'
|
|
54
99
|
end
|
|
55
100
|
|
|
101
|
+
# If the banner does not match the SPing service, close the socket and
|
|
102
|
+
# throw an error. The handshake was not successful.
|
|
56
103
|
unless banner.start_with? 'sping-0.3-'
|
|
57
104
|
socket.close
|
|
58
|
-
raise TCPHandshakeError, 'Host banner not sping'
|
|
105
|
+
raise TCPHandshakeError, 'Host banner not sping or unsupported version of sping.'
|
|
59
106
|
end
|
|
60
107
|
|
|
61
108
|
@remote_version = banner.chomp
|
|
62
109
|
$logger.info "Peer uses the following program version: #{@remote_version.dump}"
|
|
63
110
|
|
|
111
|
+
# Invite the peer to start a session with us.
|
|
64
112
|
socket.write "INVITE\r\n"
|
|
65
113
|
socket.flush
|
|
66
114
|
|
|
115
|
+
# If the session ID is too long or none was received, close the socket and throw an error.
|
|
67
116
|
invite_buf = socket.readpartial 32
|
|
68
117
|
if invite_buf.length > 31 || invite_buf.empty?
|
|
69
118
|
socket.close
|
|
@@ -77,15 +126,21 @@ module SPing
|
|
|
77
126
|
@tcp_handshake_complete = true
|
|
78
127
|
end
|
|
79
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]
|
|
80
133
|
def set_endpoint(host, port)
|
|
81
|
-
@host = host
|
|
82
|
-
@port = port
|
|
134
|
+
@host = host.to_s
|
|
135
|
+
@port = port.to_i
|
|
83
136
|
end
|
|
84
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.
|
|
85
140
|
def start_udp_handshake_sender(send_interval = 5)
|
|
86
141
|
raise 'UDP handshake sender is already running.' if @udp_handshake_sender
|
|
87
142
|
|
|
88
|
-
@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|
|
|
89
144
|
loop do
|
|
90
145
|
send_udp_handshake
|
|
91
146
|
sleep th_send_interval
|
|
@@ -93,6 +148,7 @@ module SPing
|
|
|
93
148
|
end
|
|
94
149
|
end
|
|
95
150
|
|
|
151
|
+
# Send a single UDP handshake
|
|
96
152
|
def send_udp_handshake
|
|
97
153
|
packet = {
|
|
98
154
|
'Y' => 'h'.ord,
|
|
@@ -105,6 +161,7 @@ module SPing
|
|
|
105
161
|
@socket.send packet, 0, @host, @port
|
|
106
162
|
end
|
|
107
163
|
|
|
164
|
+
# Stop the UDP handshake sender. UDP handshakes are then no longer sent at any interval.
|
|
108
165
|
def stop_udp_handshake_sender
|
|
109
166
|
raise 'UDP handshake sender is not running and therefore cannot be terminated.' unless @udp_handshake_sender
|
|
110
167
|
|
|
@@ -112,11 +169,13 @@ module SPing
|
|
|
112
169
|
@udp_handshake_sender = nil
|
|
113
170
|
end
|
|
114
171
|
|
|
172
|
+
# Informs the session that a UDP handshake has been received.
|
|
115
173
|
def udp_handshake_recived
|
|
116
174
|
stop_udp_handshake_sender if @madebyme
|
|
117
175
|
@udp_handshake_complete = true
|
|
118
176
|
end
|
|
119
177
|
|
|
178
|
+
# Starts a thread which sends ping or time messages to the peer at one-second intervals.
|
|
120
179
|
def start_pinger
|
|
121
180
|
raise 'Pinger is already running.' if @pinger
|
|
122
181
|
|
|
@@ -128,6 +187,7 @@ module SPing
|
|
|
128
187
|
end
|
|
129
188
|
end
|
|
130
189
|
|
|
190
|
+
# Stops the thread, which sends ping or time messages to the peer at regular intervals.
|
|
131
191
|
def stop_pinger
|
|
132
192
|
raise 'Pinger sender is not running and therefore cannot be terminated.' unless @pinger
|
|
133
193
|
|
|
@@ -135,6 +195,7 @@ module SPing
|
|
|
135
195
|
@pinger = nil
|
|
136
196
|
end
|
|
137
197
|
|
|
198
|
+
# Sends a ping message to the peer.
|
|
138
199
|
def ping
|
|
139
200
|
current_id = (Time.now.to_i % 255) + 1
|
|
140
201
|
data = {
|
|
@@ -151,21 +212,45 @@ module SPing
|
|
|
151
212
|
@socket.send data, 0, @host, @port
|
|
152
213
|
end
|
|
153
214
|
|
|
215
|
+
# Stops all threads associated with the session. This means that the session to the peer is as good as dead.
|
|
154
216
|
def stop
|
|
155
217
|
@pinger&.kill
|
|
156
218
|
@udp_handshake_sender&.kill
|
|
157
219
|
end
|
|
158
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]
|
|
159
225
|
def handle_ping(packet, rxtime, peeraddr)
|
|
160
|
-
if packet[
|
|
161
|
-
|
|
162
|
-
|
|
226
|
+
if !(packet.keys - %w[M Y E I T A S]).empty? ||
|
|
227
|
+
# M, Y are already checked in handle_packet from session manager
|
|
228
|
+
!packet['E'].is_a?(Integer) ||
|
|
229
|
+
!packet['I'].is_a?(Integer) ||
|
|
230
|
+
!packet['T'].is_a?(Time) ||
|
|
231
|
+
!packet['A'].is_a?(Array)
|
|
232
|
+
raise InvalidPacketError, 'The peer has sent an invalid message. The ping packet is incorrectly coded.'
|
|
163
233
|
end
|
|
164
234
|
|
|
235
|
+
raise SignalizedError, "Package displays an error message: #{packet['E']} Processing aborted." if packet['E'] != 0
|
|
236
|
+
|
|
165
237
|
id = packet['I']
|
|
166
238
|
txtime = packet['T']
|
|
167
239
|
remote_last_acks = packet['A']
|
|
168
240
|
|
|
241
|
+
if remote_last_acks.length != 32
|
|
242
|
+
raise InvalidMessageError, 'The peer has sent an invalid message. It does not contain any 32-Acks.'
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
remote_last_acks.each do |block_ack|
|
|
246
|
+
if !(block_ack.keys - %w[R U X]).empty? ||
|
|
247
|
+
!block_ack['R'].is_a?(Integer) ||
|
|
248
|
+
!block_ack['U'].is_a?(Time) ||
|
|
249
|
+
!block_ack['X'].is_a?(Time)
|
|
250
|
+
raise InvalidMessageError, 'The peer has sent an invalid message. An Ack is formatted invalid.'
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
169
254
|
ack = {
|
|
170
255
|
'R' => id,
|
|
171
256
|
'U' => txtime,
|
data/lib/session_manager.rb
CHANGED
|
@@ -1,18 +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
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
# Inclusion of all possible SPing-specific errors.
|
|
7
|
+
require_relative 'errors'
|
|
7
8
|
|
|
9
|
+
# Container, which contains a collection of sessions and manages them.
|
|
8
10
|
class SessionManager
|
|
9
11
|
require 'socket'
|
|
10
12
|
require 'timeout'
|
|
11
13
|
require_relative 'session'
|
|
12
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
|
|
13
18
|
def initialize(host = '::', port = 6924)
|
|
14
|
-
@host = host
|
|
15
|
-
@port = port
|
|
19
|
+
@host = host.to_s
|
|
20
|
+
@port = port.to_i
|
|
16
21
|
|
|
17
22
|
@sessions = {}
|
|
18
23
|
@sessions_mutex = Mutex.new
|
|
@@ -21,9 +26,12 @@ module SPing
|
|
|
21
26
|
@socket.bind @host, @port
|
|
22
27
|
end
|
|
23
28
|
|
|
29
|
+
# Initiates a new session with a peer.
|
|
30
|
+
# @param host [#to_s]
|
|
31
|
+
# @param port [#to_s, #to_i]
|
|
24
32
|
def new_session(host, port = 6924)
|
|
25
33
|
$logger.info "Add new session for host #{host} port #{port}."
|
|
26
|
-
session = SPing::Session.new host, port, @socket, true
|
|
34
|
+
session = SPing::Session.new host.to_s, port.to_i, @socket, true
|
|
27
35
|
|
|
28
36
|
counter = 0
|
|
29
37
|
loop do
|
|
@@ -49,6 +57,8 @@ module SPing
|
|
|
49
57
|
$logger.error "Out of session: #{e.message}"
|
|
50
58
|
end
|
|
51
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.
|
|
52
62
|
def del_session(session_id)
|
|
53
63
|
$logger.debug "Delete session with session id #{session_id}"
|
|
54
64
|
|
|
@@ -58,18 +68,21 @@ module SPing
|
|
|
58
68
|
end
|
|
59
69
|
end
|
|
60
70
|
|
|
71
|
+
# Stops all sessions or the threads that are connected to them.
|
|
61
72
|
def stop_sessions
|
|
62
73
|
@sessions.each do |_session_id, session|
|
|
63
74
|
session.stop
|
|
64
75
|
end
|
|
65
76
|
end
|
|
66
77
|
|
|
78
|
+
# Waits and blocks until the Session Manager is no longer running.
|
|
67
79
|
def join
|
|
68
80
|
@runner&.join
|
|
69
81
|
@gc_thread&.join
|
|
70
82
|
@server_thread&.join
|
|
71
83
|
end
|
|
72
84
|
|
|
85
|
+
# Starts the Session Manager.
|
|
73
86
|
def run
|
|
74
87
|
raise 'Client is already running.' if @runner
|
|
75
88
|
raise 'GC is already running.' if @gc
|
|
@@ -89,6 +102,7 @@ module SPing
|
|
|
89
102
|
end
|
|
90
103
|
end
|
|
91
104
|
|
|
105
|
+
# Starts the server functionality of the Session Manager.
|
|
92
106
|
def run_server
|
|
93
107
|
raise 'Server is already running.' if @server_thread
|
|
94
108
|
raise 'Server already exist.' if @server
|
|
@@ -106,6 +120,7 @@ module SPing
|
|
|
106
120
|
return @server_thread
|
|
107
121
|
end
|
|
108
122
|
|
|
123
|
+
# Stops the server functionality of the Session Manager.
|
|
109
124
|
def stop_server
|
|
110
125
|
raise 'Server thread is not running.' unless @server_thread
|
|
111
126
|
|
|
@@ -118,6 +133,7 @@ module SPing
|
|
|
118
133
|
@server = nil
|
|
119
134
|
end
|
|
120
135
|
|
|
136
|
+
# Stops the Session Manager. The server functionality must be stopped separately.
|
|
121
137
|
def stop
|
|
122
138
|
raise 'Session manager is not running.' unless @runner
|
|
123
139
|
|
|
@@ -130,6 +146,12 @@ module SPing
|
|
|
130
146
|
@gc_thread = nil
|
|
131
147
|
end
|
|
132
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]
|
|
133
155
|
def generate_session_id
|
|
134
156
|
Timeout.timeout(30, OutOfSessions, 'No session ID could be generated which has not yet been used.') do
|
|
135
157
|
loop do
|
|
@@ -139,19 +161,23 @@ module SPing
|
|
|
139
161
|
end
|
|
140
162
|
end
|
|
141
163
|
|
|
142
|
-
|
|
143
|
-
|
|
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.
|
|
144
167
|
def do_gc
|
|
145
168
|
$logger.debug 'Remove outdated sessions.'
|
|
146
169
|
# rubocop:disable Style/HashEachMethods
|
|
147
|
-
# You cannot run through a map and edit it at the same time. Since this is
|
|
170
|
+
# You cannot run through a map and edit it at the same time. Since this is
|
|
171
|
+
# necessary due to the threading,
|
|
148
172
|
# a snapshot of the key variable is run through.
|
|
149
173
|
@sessions.keys.each do |session_id|
|
|
150
174
|
# rubocop:enable Style/HashEachMethods
|
|
151
175
|
|
|
152
176
|
session = @sessions[session_id]
|
|
153
|
-
# It is possible that sessions no longer exist here, as we may have
|
|
154
|
-
#
|
|
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.
|
|
155
181
|
if !(session.tcp_handshake_complete && session.udp_handshake_complete)
|
|
156
182
|
# Handshake incomplete
|
|
157
183
|
if (Time.now.to_i - session.created.to_i) > 60
|
|
@@ -160,13 +186,16 @@ module SPing
|
|
|
160
186
|
del_session session_id
|
|
161
187
|
end
|
|
162
188
|
elsif (Time.now.to_i - session.last_rx.to_i) > 30
|
|
163
|
-
# 30 seconds have elapsed since the last ping from the peer was received.
|
|
189
|
+
# 30 seconds have elapsed since the last ping from the peer was received.
|
|
190
|
+
# The peer is probably dead.
|
|
164
191
|
$logger.debug "Session id #{session_id} without activity for over thirty seconds."
|
|
165
192
|
del_session session_id
|
|
166
193
|
end
|
|
167
194
|
end
|
|
168
195
|
end
|
|
169
196
|
|
|
197
|
+
# Receives a TCP handshake for a session and then has it
|
|
198
|
+
# managed by the Session Manager.
|
|
170
199
|
def request_session(session)
|
|
171
200
|
session.do_tcp_handshake2
|
|
172
201
|
|
|
@@ -180,6 +209,9 @@ module SPing
|
|
|
180
209
|
return nil
|
|
181
210
|
end
|
|
182
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.
|
|
183
215
|
def handle_packet(buf)
|
|
184
216
|
rxtime = Time.now
|
|
185
217
|
|
|
@@ -187,12 +219,20 @@ module SPing
|
|
|
187
219
|
peerport = buf[1][1]
|
|
188
220
|
$logger.debug "Packet received from #{peeraddr} port #{peerport}."
|
|
189
221
|
|
|
190
|
-
packet = MessagePack.unpack(buf[0])
|
|
222
|
+
packet = MessagePack.unpack(buf[0]).to_h
|
|
223
|
+
|
|
224
|
+
if !packet.key?('M') ||
|
|
225
|
+
!packet.key?('Y') ||
|
|
226
|
+
!packet.key?('S') ||
|
|
227
|
+
!packet['M'].is_a?(Integer) ||
|
|
228
|
+
!packet['Y'].is_a?(Integer) ||
|
|
229
|
+
!packet['S'].is_a?(Integer)
|
|
230
|
+
raise InvalidMessageError, 'The peer has sent an invalid message. The packet is incorrectly coded.'
|
|
231
|
+
end
|
|
191
232
|
|
|
192
233
|
# Check whether the Magic Number is the correct one.
|
|
193
234
|
if packet['M'] != 11_181
|
|
194
|
-
|
|
195
|
-
return
|
|
235
|
+
raise UnknownPacketError, 'Package contains incorrect magic number. Processing is canceled.'
|
|
196
236
|
end
|
|
197
237
|
|
|
198
238
|
type = packet['Y'].chr
|
|
@@ -202,54 +242,60 @@ module SPing
|
|
|
202
242
|
when 't'
|
|
203
243
|
handle_ping packet, rxtime, peeraddr, peerport
|
|
204
244
|
else
|
|
205
|
-
|
|
245
|
+
raise UnknownPacketError, "Package is of unknown type: #{type}"
|
|
206
246
|
end
|
|
247
|
+
rescue MessagePack::MalformedFormatError => e
|
|
248
|
+
$logger.error "Packet cannot be decoded: #{e.message}"
|
|
249
|
+
rescue PeerError => e
|
|
250
|
+
$logger.error "Perr error: #{e.message}"
|
|
207
251
|
end
|
|
208
252
|
|
|
253
|
+
# Handler for a receiving UDP handshake.
|
|
209
254
|
def handle_udp_handshake(packet, peeraddr, peerport)
|
|
255
|
+
if !(packet.keys - %w[M Y V S]).empty? ||
|
|
256
|
+
# M, Y are already checked in handle_packet from session manager
|
|
257
|
+
!packet['V'].is_a?(Integer)
|
|
258
|
+
raise InvalidMessageError, 'The peer has sent an invalid message. The handshake packet is incorrectly coded.'
|
|
259
|
+
end
|
|
260
|
+
|
|
210
261
|
session_id = packet['S']
|
|
211
262
|
session = @sessions[session_id]
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
$logger.warn "UDP handshake uses an unsupported version: #{packet['V']}"
|
|
215
|
-
return
|
|
216
|
-
end
|
|
263
|
+
raise UnknownSessionError, 'UDP handshake received for uninitiated session.' unless session
|
|
264
|
+
raise UnsupportedVersionError, "UDP handshake uses an unsupported version: #{packet['V']}" if packet['V'] != 3
|
|
217
265
|
|
|
218
|
-
|
|
266
|
+
session.set_endpoint peeraddr, peerport
|
|
219
267
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if session.udp_handshake_complete
|
|
228
|
-
$logger.warn 'UDP handshake is received, although a previous one was already successful.'
|
|
229
|
-
return
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
session.udp_handshake_recived
|
|
233
|
-
$logger.info "UDP handshake for session ID #{session_id} was successful."
|
|
268
|
+
unless session.madebyme
|
|
269
|
+
# If the session was not started by me, the other peer expects a
|
|
270
|
+
# confirmation of the handshake in which the same is sent back.
|
|
271
|
+
$logger.debug "Send UDP Handshake back for session id #{session_id}."
|
|
272
|
+
session.send_udp_handshake
|
|
273
|
+
end
|
|
234
274
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
$logger.warn 'UDP handshake received for uninitiated session.'
|
|
275
|
+
if session.udp_handshake_complete
|
|
276
|
+
$logger.warn 'UDP handshake is received, although a previous one was already successful.'
|
|
238
277
|
return
|
|
239
278
|
end
|
|
279
|
+
|
|
280
|
+
session.udp_handshake_recived
|
|
281
|
+
$logger.info "UDP handshake for session ID #{session_id} was successful."
|
|
282
|
+
|
|
283
|
+
session.start_pinger
|
|
240
284
|
end
|
|
241
285
|
|
|
286
|
+
# Handler for a receiving ping or a receiving time message.
|
|
242
287
|
def handle_ping(packet, rxtime, peeraddr, peerport)
|
|
243
288
|
session_id = packet['S']
|
|
244
289
|
session = @sessions[session_id]
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
session.handle_ping packet, rxtime, peeraddr
|
|
248
|
-
else
|
|
249
|
-
$logger.warn "Ping packet received for non-initiated session id #{session_id}."
|
|
290
|
+
unless session&.tcp_handshake_complete && session&.udp_handshake_complete
|
|
291
|
+
raise UnknownSessionError, "Ping packet received for non-initiated session id #{session_id}."
|
|
250
292
|
end
|
|
293
|
+
|
|
294
|
+
session.set_endpoint peeraddr, peerport
|
|
295
|
+
session.handle_ping packet, rxtime, peeraddr
|
|
251
296
|
end
|
|
252
297
|
|
|
298
|
+
# Handler to establish a session for the request of a peer.
|
|
253
299
|
def handle_client(client)
|
|
254
300
|
host = client.peeraddr[3]
|
|
255
301
|
port = client.peeraddr[1]
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sping
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Marek Küthe
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-11-
|
|
11
|
+
date: 2023-11-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: msgpack
|
|
@@ -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
|
|
34
|
-
in
|
|
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
|
|
@@ -43,6 +44,7 @@ files:
|
|
|
43
44
|
- LICENSE
|
|
44
45
|
- README.md
|
|
45
46
|
- bin/sping
|
|
47
|
+
- lib/errors.rb
|
|
46
48
|
- lib/last_acks.rb
|
|
47
49
|
- lib/session.rb
|
|
48
50
|
- lib/session_manager.rb
|
|
@@ -68,8 +70,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
68
70
|
- !ruby/object:Gem::Version
|
|
69
71
|
version: '0'
|
|
70
72
|
requirements: []
|
|
71
|
-
rubygems_version: 3.4.
|
|
73
|
+
rubygems_version: 3.4.22
|
|
72
74
|
signing_key:
|
|
73
75
|
specification_version: 4
|
|
74
|
-
summary:
|
|
76
|
+
summary: Program for measuring asymmetric latencies.
|
|
75
77
|
test_files: []
|