sping 1.0.2 → 1.0.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5ab53904c197ff35bab0d5fc29f26b5ada7c1ded24548b30d42df12d3df6837
4
- data.tar.gz: 8bd57a7d33c5f39f1eb76ad2551550fafecc5836f373b3c3675648e620afebe1
3
+ metadata.gz: 9f7679b3fb31d07d034182114e576f3e272ef8223954f592bb30a98378b98fd5
4
+ data.tar.gz: b8d21d6a58ffb3c57481cc421d61f8fc25da32646fb8d5ae160829017d01fb14
5
5
  SHA512:
6
- metadata.gz: 90e021acc0d54dace745238c35566efacdd5be77d8db2f9ecd6e6aa1ed8d71ee89c552fb4674be23e1adcf244987154e1975364731eff78e76ad5dd7006ec3df
7
- data.tar.gz: 336910fef1e87548f060a4a70547b272d78362828274dd5b21ed011a821a66ed71c7ece447045ca717a169676fa872594cbbd9e649e701731c63cd812c7f7bba
6
+ metadata.gz: a6a2c6897ead8f7371b2f1a83719c1a8f2c07d44957a9c302bc548855d25671858435fe0d6e8162e14737e065f6bbcede37defd2ea21e23561d19e7f8d5ef63f
7
+ data.tar.gz: 2618e70abea52acc38364f7fb431dafa12a4d48ad8f07665f93ac76e316784e6a2f8f5d2b463a55053da88b10f46dc23adf16846abb1a4b1d4677eb9eef09e7f
data/bin/sping CHANGED
@@ -7,10 +7,10 @@ require 'msgpack'
7
7
  require_relative '../lib/session_manager'
8
8
 
9
9
  MessagePack::DefaultFactory.register_type(
10
- MessagePack::Timestamp::TYPE,
11
- Time,
12
- packer: MessagePack::Time::Packer,
13
- unpacker: MessagePack::Time::Unpacker
10
+ MessagePack::Timestamp::TYPE,
11
+ Time,
12
+ packer: MessagePack::Time::Packer,
13
+ unpacker: MessagePack::Time::Unpacker
14
14
  )
15
15
 
16
16
  $logger = Logger.new $stdout
@@ -21,18 +21,18 @@ sess_mgr.run
21
21
  sess_mgr.run_server
22
22
 
23
23
  ARGV.each do |host|
24
- sess_mgr.new_session(host)
24
+ sess_mgr.new_session(host)
25
25
  end
26
26
 
27
27
  Signal.trap('INT') do
28
- puts "Stop server..."
29
- sess_mgr.stop_server
28
+ puts 'Stop server...'
29
+ sess_mgr.stop_server
30
30
 
31
- puts "Stop sessions..."
32
- sess_mgr.stop_sessions
31
+ puts 'Stop sessions...'
32
+ sess_mgr.stop_sessions
33
33
 
34
- puts "Stop client..."
35
- sess_mgr.stop
34
+ puts 'Stop client...'
35
+ sess_mgr.stop
36
36
  end
37
37
 
38
38
  sess_mgr.join
data/lib/errors.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ # sharable_constant_value: literal
3
+
4
+ module SPing
5
+ class SPingError < StandardError; end
6
+
7
+ class SessionManagerError < SPingError; end
8
+ class OutOfSessions < SessionManagerError; end
9
+
10
+ class SessionError < SPingError; end
11
+ class TCPHandshakeError < SessionError; end
12
+
13
+ class PeerError < SPingError; end
14
+ class InvalidPacketError < PeerError; end
15
+ class InvalidMessageError < InvalidPacketError; end
16
+ class UnknownPacketError < InvalidPacketError; end
17
+ class SignalizedError < PeerError; end
18
+ class UnknownVersionError < PeerError; end
19
+ class UnknownSessionError < PeerError; end
20
+ end
data/lib/last_acks.rb CHANGED
@@ -1,21 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
  # sharable_constant_value: literal
3
3
 
4
- class LastAcks
5
-
6
- attr_reader :acks, :size
4
+ module SPing
5
+ class LastAcks
6
+ attr_reader :size
7
7
 
8
8
  def initialize(size = 32)
9
- @size = 32
10
- @acks = []
9
+ @size = size
10
+ @acks = []
11
+ @acks_mutex = Mutex.new
12
+
13
+ @size.times do |index|
14
+ @acks[index] = {
15
+ 'R' => 0,
16
+ 'U' => Time.at(0),
17
+ 'X' => Time.at(0)
18
+ }
19
+ end
20
+ end
21
+
22
+ def acks
23
+ @acks_mutex.synchronize do
24
+ return @acks.dup
25
+ end
11
26
  end
12
27
 
13
28
  def add_ack(ack)
29
+ @acks_mutex.synchronize do
14
30
  @acks << ack
15
31
 
16
- if @acks.length > @size
17
- @acks.shift
18
- end
32
+ @acks.shift if @acks.length > @size
33
+ end
19
34
  end
20
-
35
+ end
21
36
  end
data/lib/session.rb CHANGED
@@ -2,228 +2,242 @@
2
2
  # sharable_constant_value: literal
3
3
 
4
4
  module SPing
5
+ require_relative 'errors'
5
6
 
6
- class SessionError < StandardError; end
7
- class TCPHandshakeError < SessionError; end
7
+ class Session
8
+ attr_reader :created, :last_rx, :madebyme
9
+ attr_accessor :session_id, :tcp_handshake_complete, :udp_handshake_complete
8
10
 
9
- class Session
11
+ require 'socket'
12
+ require 'timeout'
13
+ require 'msgpack'
14
+ require_relative 'last_acks'
10
15
 
11
- attr_reader :created, :last_rx, :madebyme
12
- attr_accessor :session_id, :tcp_handshake_complete, :udp_handshake_complete
16
+ def initialize(host, port, socket, madebyme)
17
+ @host = host
18
+ @port = port
19
+ @socket = socket
20
+ @created = Time.now
21
+ @madebyme = madebyme
13
22
 
14
- require 'socket'
15
- require 'timeout'
16
- require 'msgpack'
17
- require_relative 'last_acks'
23
+ @tcp_handshake_complete = false
24
+ @udp_handshake_complete = false
18
25
 
19
- def initialize(host, port, socket, madebyme)
20
- @host = host
21
- @port = port
22
- @socket = socket
23
- @created = Time.now
24
- @madebyme = madebyme
26
+ @last_acks = SPing::LastAcks.new
27
+ end
25
28
 
26
- @tcp_handshake_complete = false
27
- @udp_handshake_complete = false
29
+ def do_tcp_handshake1(socket, session_id)
30
+ socket.write "sping-0.3-https://codeberg.org/mark22k/sping\r\n"
31
+ socket.flush
28
32
 
29
- @last_acks = LastAcks.new
30
- end
33
+ invite = socket.readpartial 6
31
34
 
32
- def do_tcp_handshake1(socket, session_id)
33
- socket.write "sping-0.3-https://codeberg.org/mark22k/sping\r\n"
34
- socket.flush
35
+ raise TCPHandshakeError, 'Peer didn\'t invite us.' unless invite.chomp == 'INVITE'
35
36
 
36
- invite = socket.readpartial 6
37
+ @session_id = session_id
37
38
 
38
- if invite.chomp != "INVITE"
39
- raise TCPHandshakeError, 'Peer didn\'t invite us.'
40
- end
39
+ socket.write "#{session_id}\r\n"
40
+ socket.flush
41
41
 
42
- @session_id = session_id
42
+ socket.close
43
43
 
44
- socket.write "#{session_id}\r\n"
45
- socket.flush
44
+ @tcp_handshake_complete = true
45
+ end
46
46
 
47
- socket.close
47
+ def do_tcp_handshake2
48
+ socket = TCPSocket.new @host, @port
49
+ banner = socket.readpartial 9001
50
+ if banner.length > 9000
51
+ socket.close
52
+ raise TCPHandshakeError, 'Host banner too big'
53
+ end
48
54
 
49
- @tcp_handshake_complete = true
50
- end
55
+ unless banner.start_with? 'sping-0.3-'
56
+ socket.close
57
+ raise TCPHandshakeError, 'Host banner not sping'
58
+ end
51
59
 
52
- def do_tcp_handshake2
53
- socket = TCPSocket.new @host, @port
54
- banner = socket.readpartial 9001
55
- if banner.length > 9000
56
- socket.close
57
- raise TCPHandshakeError, 'Host banner too big'
58
- end
60
+ @remote_version = banner.chomp
61
+ $logger.info "Peer uses the following program version: #{@remote_version.dump}"
59
62
 
60
- if ! banner.start_with? 'sping-0.3-'
61
- socket.close
62
- raise TCPHandshakeError, 'Host banner not sping'
63
- end
63
+ socket.write "INVITE\r\n"
64
+ socket.flush
64
65
 
65
- @remote_version = banner.chomp
66
- $logger.info "Peer uses the following program version: #{@remote_version.dump}"
66
+ invite_buf = socket.readpartial 32
67
+ if invite_buf.length > 31 || invite_buf.empty?
68
+ socket.close
69
+ raise TCPHandshakeError, 'Invite banner wrong size'
70
+ end
67
71
 
68
- socket.write "INVITE\r\n"
69
- socket.flush
72
+ @session_id = invite_buf.chomp.to_i
70
73
 
71
- inviteBuf = socket.readpartial 32
72
- if inviteBuf.length > 31 || inviteBuf.length.zero?
73
- socket.close
74
- raise TCPHandshakeError, 'Invite banner wrong size'
75
- end
74
+ socket.close
76
75
 
77
- @session_id = inviteBuf.chomp.to_i
76
+ @tcp_handshake_complete = true
77
+ end
78
78
 
79
- socket.close
79
+ def set_endpoint(host, port)
80
+ @host = host
81
+ @port = port
82
+ end
80
83
 
81
- @tcp_handshake_complete = true
82
- end
84
+ def start_udp_handshake_sender(send_interval = 5)
85
+ raise 'UDP handshake sender is already running.' if @udp_handshake_sender
83
86
 
84
- def set_endpoint(host, port)
85
- @host = host
86
- @port = port
87
+ @udp_handshake_sender = Thread.new(send_interval) do |th_send_interval|
88
+ loop do
89
+ send_udp_handshake
90
+ sleep th_send_interval
87
91
  end
92
+ end
93
+ end
88
94
 
89
- def start_udp_handshake_sender(send_interval = 5)
90
- raise 'UDP handshake sender is already running.' if @udp_handshake_sender
95
+ def send_udp_handshake
96
+ packet = {
97
+ 'Y' => 'h'.ord,
98
+ 'M' => 11_181,
99
+ 'V' => 3,
100
+ 'S' => @session_id
101
+ }.to_msgpack
91
102
 
92
- @udp_handshake_sender = Thread.new(send_interval) do |send_interval|
93
- loop do
94
- self.send_udp_handshake
95
- sleep send_interval
96
- end
97
- end
98
- end
103
+ $logger.debug "Send UDP handshake to #{@host} port #{@port}."
104
+ @socket.send packet, 0, @host, @port
105
+ end
99
106
 
100
- def send_udp_handshake
101
- packet = {
102
- 'Y' => 'h'.ord,
103
- 'M' => 11181,
104
- 'V' => 3,
105
- 'S' => @session_id
106
- }.to_msgpack
107
+ def stop_udp_handshake_sender
108
+ raise 'UDP handshake sender is not running and therefore cannot be terminated.' unless @udp_handshake_sender
107
109
 
108
- $logger.debug "Send UDP handshake to #{@host} port #{@port}."
109
- @socket.send packet, 0, @host, @port
110
- end
110
+ @udp_handshake_sender.kill
111
+ @udp_handshake_sender = nil
112
+ end
111
113
 
112
- def stop_udp_handshake_sender
113
- raise 'UDP handshake sender is not running and therefore cannot be terminated.' unless @udp_handshake_sender
114
- @udp_handshake_sender.kill
115
- @udp_handshake_sender = nil
116
- end
114
+ def udp_handshake_recived
115
+ stop_udp_handshake_sender if @madebyme
116
+ @udp_handshake_complete = true
117
+ end
117
118
 
118
- def udp_handshake_recived
119
- stop_udp_handshake_sender if @madebyme
120
- @udp_handshake_complete = true
119
+ def start_pinger
120
+ raise 'Pinger is already running.' if @pinger
121
+
122
+ @pinger = Thread.new do
123
+ loop do
124
+ ping
125
+ sleep 1
121
126
  end
127
+ end
128
+ end
122
129
 
123
- def start_pinger
124
- raise 'Pinger is already running.' if @pinger
130
+ def stop_pinger
131
+ raise 'Pinger sender is not running and therefore cannot be terminated.' unless @pinger
125
132
 
126
- @pinger = Thread.new do
127
- loop do
128
- self.ping
129
- sleep 1
130
- end
131
- end
132
- end
133
+ @pinger.kill
134
+ @pinger = nil
135
+ end
133
136
 
134
- def stop_pinger
135
- raise 'Pinger sender is not running and therefore cannot be terminated.' unless @pinger
136
- @pinger.kill
137
- @pinger = nil
138
- end
137
+ def ping
138
+ current_id = (Time.now.to_i % 255) + 1
139
+ data = {
140
+ 'Y' => 't'.ord,
141
+ 'M' => 11_181,
142
+ 'S' => @session_id,
143
+ 'I' => current_id,
144
+ 'T' => Time.now,
145
+ 'E' => 0,
146
+ 'A' => @last_acks.acks
147
+ }.to_msgpack
148
+
149
+ $logger.debug "Send ping to #{@host} port #{@port}."
150
+ @socket.send data, 0, @host, @port
151
+ end
139
152
 
140
- def ping
141
- current_id = Time.now.to_i % 255 + 1
142
- data = {
143
- 'Y' => 't'.ord,
144
- 'M' => 11181,
145
- 'S' => @session_id,
146
- 'I' => current_id,
147
- 'T' => Time.now,
148
- 'E' => 0,
149
- 'A' => @last_acks.acks
150
- }.to_msgpack
151
-
152
- $logger.debug "Send ping to #{@host} port #{@port}."
153
- @socket.send data, 0, @host, @port
153
+ def stop
154
+ @pinger&.kill
155
+ @udp_handshake_sender&.kill
156
+ end
157
+
158
+ def handle_ping(packet, rxtime, peeraddr)
159
+ if !(packet.keys - %w[M Y E I T A S]).empty? ||
160
+ # M, Y are already checked in handle_packet from session manager
161
+ !packet['E'].is_a?(Integer) ||
162
+ !packet['I'].is_a?(Integer) ||
163
+ !packet['T'].is_a?(Time) ||
164
+ !packet['A'].is_a?(Array)
165
+ raise InvalidPacketError, 'The peer has sent an invalid message. The ping packet is incorrectly coded.'
166
+ end
167
+
168
+ raise SignalizedError, "Package displays an error message: #{packet['E']} Processing aborted." if packet['E'] != 0
169
+
170
+ id = packet['I']
171
+ txtime = packet['T']
172
+ remote_last_acks = packet['A']
173
+
174
+ if remote_last_acks.length != 32
175
+ raise InvalidMessageError, 'The peer has sent an invalid message. It does not contain any 32-Acks.'
176
+ end
177
+
178
+ remote_last_acks.each do |block_ack|
179
+ if !(block_ack.keys - %w[R U X]).empty? ||
180
+ !block_ack['R'].is_a?(Integer) ||
181
+ !block_ack['U'].is_a?(Time) ||
182
+ !block_ack['X'].is_a?(Time)
183
+ raise InvalidMessageError, 'The peer has sent an invalid message. An Ack is formatted invalid.'
154
184
  end
185
+ end
186
+
187
+ ack = {
188
+ 'R' => id,
189
+ 'U' => txtime,
190
+ 'X' => rxtime
191
+ }
192
+
193
+ @last_acks.add_ack(ack)
194
+ @last_rx = rxtime
195
+
196
+ # This is the A
197
+ # The peer is B
198
+ # TX => Time from A to B
199
+ # RX => Time from B to A
200
+
201
+ # Remove zero acks
202
+ remote_last_acks.reject! { |block_ack| block_ack['R'].zero? }
203
+
204
+ # Sort by TX time
205
+ remote_last_acks.sort_by! { |block_ack| block_ack['U'] }
206
+
207
+ newest_remote_ack = remote_last_acks.last.to_h
208
+
209
+ # Calculate loss
210
+ tx_loss = 0
211
+ rx_loss = 0
212
+ exchanges = 0
213
+
214
+ if remote_last_acks.length == 32
215
+ # We have enough data
216
+ exchanges = 32
217
+ remote_acks = remote_last_acks.map { |ack| ack['R'] }
218
+ local_acks = @last_acks.acks.map { |ack| ack['R'] }
219
+
220
+ tip_id = (Time.now.to_i % 255) + 1
221
+ starting = tip_id - 32
222
+
223
+ last_ids = if tip_id > 32
224
+ (starting..tip_id).to_a
225
+ else
226
+ ((255 + starting)..255).to_a + (1..tip_id).to_a
227
+ end
155
228
 
156
- def stop
157
- @pinger&.kill
158
- @udp_handshake_sender&.kill
229
+ last_ids[0...-1].each do |id|
230
+ tx_loss += 1 unless remote_acks.include? id
159
231
  end
160
232
 
161
- def handle_ping packet, rxtime, peeraddr
162
- if packet['E'] != 0
163
- $logger.error "Package displays an error message: #{packet['E']} Processing aborted."
164
- return
165
- end
166
-
167
- id = packet['I']
168
- txtime = packet['T']
169
- lastAcks = packet['A']
170
-
171
- ack = {
172
- 'R' => id,
173
- 'U' => txtime,
174
- 'X' => rxtime
175
- }
176
-
177
- @last_acks.add_ack(ack)
178
- @last_rx = rxtime
179
-
180
- # This is the A
181
- # The peer is B
182
- # TX => Time from A to B
183
- # RX => Time from B to A
184
-
185
- # Remove zero acks
186
- lastAcks.reject! { |ack| ack['R'] == 0 }
187
-
188
- # Sort by TX time
189
- lastAcks.sort_by! { |ack| ack['U'] }
190
-
191
- newest_remote_ack = lastAcks.last.to_h
192
-
193
- # Calculate loss
194
- tx_loss = 0
195
- rx_loss = 0
196
- exchanges = 0
197
-
198
- if lastAcks.length == 32
199
- # We have enough data
200
- exchanges = 32
201
- remote_acks = lastAcks.map { |ack| ack['R'] }
202
- local_acks = @last_acks.acks.map { |ack| ack['R'] }
203
-
204
- tip_id = Time.now.to_i % 255 + 1
205
- starting = tip_id - 32
206
-
207
- last_ids = nil
208
- if tip_id > 32
209
- last_ids = (starting..tip_id).to_a
210
- else
211
- last_ids = ((255 + starting)..255).to_a + (1..tip_id).to_a
212
- end
213
-
214
- last_ids[0...-1].each do |id|
215
- tx_loss += 1 unless remote_acks.include? id
216
- end
217
-
218
- last_ids[1..-1].each do |id|
219
- rx_loss += 1 unless local_acks.include? id
220
- end
221
- end
222
-
223
- tx_latency = ((newest_remote_ack['X'].to_f - newest_remote_ack['U'].to_f) * 1000.0).round(6)
224
- rx_latency = ((rxtime - txtime) * 1000.0).round(6)
225
- puts "[#{peeraddr}] RX: #{rx_latency}ms TX: #{tx_latency}ms [Loss RX: #{rx_loss}/#{exchanges} | Loss TX: #{tx_loss}/#{exchanges}]"
233
+ last_ids[1..].each do |id|
234
+ rx_loss += 1 unless local_acks.include? id
226
235
  end
236
+ end
227
237
 
238
+ tx_latency = ((newest_remote_ack['X'].to_f - newest_remote_ack['U'].to_f) * 1000.0).round(6)
239
+ rx_latency = ((rxtime - txtime) * 1000.0).round(6)
240
+ puts "[#{peeraddr}] RX: #{rx_latency}ms TX: #{tx_latency}ms [Loss RX: #{rx_loss}/#{exchanges} | Loss TX: #{tx_loss}/#{exchanges}]"
228
241
  end
242
+ end
229
243
  end
@@ -2,261 +2,285 @@
2
2
  # sharable_constant_value: literal
3
3
 
4
4
  module SPing
5
+ require_relative 'errors'
5
6
 
6
- class SessionManagerError < StandardError; end
7
- class OutOfSessions < SessionManagerError; end
7
+ class SessionManager
8
+ require 'socket'
9
+ require 'timeout'
10
+ require_relative 'session'
8
11
 
9
- class SessionManager
12
+ def initialize(host = '::', port = 6924)
13
+ @host = host
14
+ @port = port
10
15
 
11
- require 'socket'
12
- require 'timeout'
13
- require_relative 'session'
16
+ @sessions = {}
17
+ @sessions_mutex = Mutex.new
14
18
 
15
- def initialize(port = 6924)
16
- @port = port
19
+ @socket = UDPSocket.new(Socket::AF_INET6)
20
+ @socket.bind @host, @port
21
+ end
17
22
 
18
- @sessions = {}
19
- @pingers = {}
23
+ def new_session(host, port = 6924)
24
+ $logger.info "Add new session for host #{host} port #{port}."
25
+ session = SPing::Session.new host, port, @socket, true
20
26
 
21
- @socket = UDPSocket.new(Socket::AF_INET6)
22
- @socket.bind "::", @port
23
- end
27
+ counter = 0
28
+ loop do
29
+ session_id = request_session session
30
+ break if session_id
24
31
 
25
- def new_session(host, port = 6924)
26
- $logger.info "Add new session for host #{host} port #{port}."
27
- session = SPing::Session.new host, port, @socket, true
28
-
29
- begin
30
- counter = 0
31
- loop do
32
- begin
33
- session.do_tcp_handshake2
34
- rescue Errno::ECONNRESET, EOFError, IO::TimeoutError, TCPHandshakeError => e
35
- $logger.warn "TCP handshake failed: #{e.message}"
36
- return
37
- end
38
- if ! @sessions.key? session.session_id
39
- break
40
- end
41
- if counter > 5
42
- raise OutOfSessions, 'The peer could not name a session ID that had not yet been assigned.'
43
- end
44
- counter += 1
45
- end
46
- rescue OutOfSessions => e
47
- $logger.error "Out of session: #{e.message}"
48
- return
49
- end
50
-
51
- $logger.info "TCP handshake for host #{host} port #{port} successful. Session ID is #{session.session_id}."
52
-
53
- $logger.info "Perform UDP handshake for session ID #{session.session_id}."
54
- session.start_udp_handshake_sender
55
-
56
- @sessions[session.session_id] = session
32
+ # rubocop:disable Style/IfUnlessModifier
33
+ if counter > 5
34
+ raise OutOfSessions, 'The peer could not name a session ID that had not yet been assigned.'
57
35
  end
36
+ # rubocop:enable Style/IfUnlessModifier
58
37
 
59
- def del_session(session_id)
60
- $logger.debug "Delete session with session id #{session_id}"
38
+ counter += 1
39
+ end
61
40
 
62
- @sessions[session_id].stop
63
- @sessions.delete(session_id)
64
- end
41
+ $logger.info "TCP handshake for host #{host} port #{port} successful. Session ID is #{session.session_id}."
65
42
 
66
- def stop_sessions
67
- @sessions.each do |session_id, session|
68
- session.stop
69
- end
70
- end
43
+ $logger.info "Perform UDP handshake for session ID #{session.session_id}."
44
+ session.start_udp_handshake_sender
45
+ rescue Errno::ECONNRESET, EOFError, IO::TimeoutError, TCPHandshakeError => e
46
+ $logger.warn "TCP handshake failed: #{e.message}"
47
+ rescue OutOfSessions => e
48
+ $logger.error "Out of session: #{e.message}"
49
+ end
71
50
 
72
- def join
73
- @runner&.join
74
- @gc_thread&.join
75
- @server_thread&.join
76
- end
51
+ def del_session(session_id)
52
+ $logger.debug "Delete session with session id #{session_id}"
77
53
 
78
- def run
79
- raise 'Client is already running.' if @runner
80
- raise 'GC is already running.' if @gc
81
-
82
- @runner = Thread.new do
83
- loop do
84
- Thread.new(@socket.recvfrom(10000)) do |buf|
85
- handle_packet buf
86
- end
87
- end
88
- end
89
- @gc_thread = Thread.new do
90
- loop do
91
- do_gc
92
- sleep 10
93
- end
94
- end
95
- end
54
+ @sessions_mutex.synchronize do
55
+ @sessions[session_id].stop
56
+ @sessions.delete(session_id)
57
+ end
58
+ end
96
59
 
97
- def run_server
98
- raise 'Server is already running.' if @server_thread
99
- raise 'Server already exist.' if @server
60
+ def stop_sessions
61
+ @sessions.each do |_session_id, session|
62
+ session.stop
63
+ end
64
+ end
100
65
 
101
- @server = TCPServer.new '::', @port
66
+ def join
67
+ @runner&.join
68
+ @gc_thread&.join
69
+ @server_thread&.join
70
+ end
102
71
 
103
- @server_thread = Thread.new do
104
- loop do
105
- Thread.new(@server.accept) do |client|
106
- handle_client client
107
- end
108
- end
109
- end
72
+ def run
73
+ raise 'Client is already running.' if @runner
74
+ raise 'GC is already running.' if @gc
110
75
 
111
- return @server_thread
76
+ @runner = Thread.new do
77
+ loop do
78
+ Thread.new(@socket.recvfrom(10_000)) do |buf|
79
+ handle_packet buf
80
+ end
112
81
  end
82
+ end
83
+ @gc_thread = Thread.new do
84
+ loop do
85
+ do_gc
86
+ sleep 10
87
+ end
88
+ end
89
+ end
113
90
 
114
- def stop_server
115
- raise 'Server thread is not running.' unless @server_thread
116
-
117
- @server_thread.kill
118
- @server_thread = nil
91
+ def run_server
92
+ raise 'Server is already running.' if @server_thread
93
+ raise 'Server already exist.' if @server
119
94
 
120
- raise 'Server is not running.' unless @server
95
+ @server = TCPServer.new @host, @port
121
96
 
122
- @server.close
123
- @server = nil
97
+ @server_thread = Thread.new do
98
+ loop do
99
+ Thread.new(@server.accept) do |client|
100
+ handle_client client
101
+ end
124
102
  end
103
+ end
125
104
 
126
- def stop
127
- raise 'Session manager is not running.' unless @runner
105
+ return @server_thread
106
+ end
128
107
 
129
- @runner.kill
130
- @runner = nil
108
+ def stop_server
109
+ raise 'Server thread is not running.' unless @server_thread
131
110
 
132
- raise 'Session GC is not running.' unless @gc_thread
111
+ @server_thread.kill
112
+ @server_thread = nil
133
113
 
134
- @gc_thread.kill
135
- @gc_thread = nil
136
- end
114
+ raise 'Server is not running.' unless @server
137
115
 
138
- def generate_session_id
139
- Timeout::timeout(30, OutOfSessions, 'No session ID could be generated which has not yet been used.') do
140
- session_id = rand(2 ** 32 - 1)
141
- while @sessions.key? session_id
142
- session_id = rand(2 ** 32 - 1)
143
- end
116
+ @server.close
117
+ @server = nil
118
+ end
144
119
 
145
- return session_id
146
- end
147
- end
120
+ def stop
121
+ raise 'Session manager is not running.' unless @runner
148
122
 
149
- private
150
-
151
- def do_gc
152
- $logger.debug 'Remove outdated sessions.'
153
- @sessions.keys.each do |session_id|
154
- session = @sessions[session_id]
155
- # It is possible that sessions no longer exist here, as we may have session IDs that have already been deleted.
156
- # However, we can ignore this aspect here, as we are the only function that deletes sessions.
157
- if ! (session.tcp_handshake_complete && session.udp_handshake_complete)
158
- # Handshake incomplete
159
- if (Time.now.to_i - session.created.to_i) > 60
160
- # TCP and/or UDP take more than 60 seconds.
161
- $logger.debug "UDP handshake for session id #{session_id} timed out."
162
- del_session session_id
163
- end
164
- elsif (Time.now.to_i - session.last_rx.to_i) > 30
165
- # 30 seconds have elapsed since the last ping from the peer was received. The peer is probably dead.
166
- $logger.debug "Session id #{session_id} without activity for over thirty seconds."
167
- del_session session_id
168
- end
169
- end
170
- end
123
+ @runner.kill
124
+ @runner = nil
171
125
 
172
- def handle_packet buf
173
- rxtime = Time.now
174
-
175
- peeraddr = buf[1][2]
176
- peerport = buf[1][1]
177
- $logger.debug "Packet received from #{peeraddr} port #{peerport}."
178
-
179
- packet = MessagePack.unpack(buf[0])
180
-
181
- # Check whether the Magic Number is the correct one.
182
- if packet['M'] != 11181
183
- $logger.warn 'Package contains incorrect magic number. Processing is canceled.'
184
- return
185
- end
186
-
187
- type = packet['Y'].chr
188
- case type
189
- when 'h'
190
- handle_udp_handshake packet, peeraddr, peerport
191
- when 't'
192
- handle_ping packet, rxtime, peeraddr, peerport
193
- else
194
- $logger.warn "Package is of unknown type: #{type}"
195
- end
196
- end
126
+ raise 'Session GC is not running.' unless @gc_thread
197
127
 
198
- def handle_udp_handshake packet, peeraddr, peerport
199
- session_id = packet['S']
200
- session = @sessions[session_id]
201
- if session
202
- if packet['V'] != 3
203
- $logger.warn "UDP handshake uses an unsupported version: #{packet['V']}"
204
- return
205
- end
206
-
207
- session.set_endpoint peeraddr, peerport
208
-
209
- if ! session.madebyme
210
- # If the session was not started by me, the other peer expects a
211
- # confirmation of the handshake in which the same is sent back.
212
- $logger.debug "Send UDP Handshake back for session id #{session_id}."
213
- session.send_udp_handshake
214
- end
215
-
216
- if session.udp_handshake_complete
217
- $logger.warn 'UDP handshake is received, although a previous one was already successful.'
218
- return
219
- end
220
-
221
- session.udp_handshake_recived
222
- $logger.info "UDP handshake for session ID #{session_id} was successful."
223
-
224
- session.start_pinger
225
- else
226
- $logger.warn 'UDP handshake received for uninitiated session.'
227
- return
228
- end
128
+ @gc_thread.kill
129
+ @gc_thread = nil
130
+ end
131
+
132
+ def generate_session_id
133
+ Timeout.timeout(30, OutOfSessions, 'No session ID could be generated which has not yet been used.') do
134
+ loop do
135
+ session_id = rand (2**32) - 1
136
+ return session_id unless @sessions.key? session_id
229
137
  end
138
+ end
139
+ end
230
140
 
231
- def handle_ping packet, rxtime, peeraddr, peerport
232
- session_id = packet['S']
233
- session = @sessions[session_id]
234
- if session && session.tcp_handshake_complete && session.udp_handshake_complete
235
- session.set_endpoint peeraddr, peerport
236
- session.handle_ping packet, rxtime, peeraddr
237
- else
238
- $logger.warn "Ping packet received for non-initiated session id #{session_id}."
239
- end
141
+ private
142
+
143
+ def do_gc
144
+ $logger.debug 'Remove outdated sessions.'
145
+ # 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,
147
+ # a snapshot of the key variable is run through.
148
+ @sessions.keys.each do |session_id|
149
+ # rubocop:enable Style/HashEachMethods
150
+
151
+ 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.
154
+ if !(session.tcp_handshake_complete && session.udp_handshake_complete)
155
+ # Handshake incomplete
156
+ if (Time.now.to_i - session.created.to_i) > 60
157
+ # TCP and/or UDP take more than 60 seconds.
158
+ $logger.debug "UDP handshake for session id #{session_id} timed out."
159
+ del_session session_id
160
+ end
161
+ 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.
163
+ $logger.debug "Session id #{session_id} without activity for over thirty seconds."
164
+ del_session session_id
240
165
  end
166
+ end
167
+ end
168
+
169
+ def request_session(session)
170
+ session.do_tcp_handshake2
241
171
 
242
- def handle_client(client)
243
- host = client.peeraddr[3]
244
- port = client.peeraddr[1]
245
- session = SPing::Session.new host, port, @socket, false
246
-
247
- $logger.debug "Session request from host #{host} port #{port}."
248
-
249
- begin
250
- session.do_tcp_handshake1 client, self.generate_session_id
251
- rescue Errno::ECONNRESET, EOFError, IO::TimeoutError, TCPHandshakeError => e
252
- $logger.warn "Could not establish a new session with Peer: #{e.message}"
253
- return
254
- rescue OutOfSessions => e
255
- $logger.error "Could not establish a new session with Peer: #{e.message}"
256
- return
257
- end
258
-
259
- @sessions[session.session_id] = session
172
+ @sessions_mutex.synchronize do
173
+ unless @sessions.key? session.session_id
174
+ @sessions[session.session_id] = session
175
+ return session.session_id
260
176
  end
177
+ end
178
+
179
+ return nil
180
+ end
181
+
182
+ def handle_packet(buf)
183
+ rxtime = Time.now
184
+
185
+ peeraddr = buf[1][2]
186
+ peerport = buf[1][1]
187
+ $logger.debug "Packet received from #{peeraddr} port #{peerport}."
188
+
189
+ packet = MessagePack.unpack(buf[0]).to_h
190
+
191
+ if !packet.key?('M') ||
192
+ !packet.key?('Y') ||
193
+ !packet.key?('S') ||
194
+ !packet['M'].is_a?(Integer) ||
195
+ !packet['Y'].is_a?(Integer) ||
196
+ !packet['S'].is_a?(Integer)
197
+ raise InvalidMessageError, 'The peer has sent an invalid message. The packet is incorrectly coded.'
198
+ end
199
+
200
+ # Check whether the Magic Number is the correct one.
201
+ if packet['M'] != 11_181
202
+ raise UnknownPacketError, 'Package contains incorrect magic number. Processing is canceled.'
203
+ end
204
+
205
+ type = packet['Y'].chr
206
+ case type
207
+ when 'h'
208
+ handle_udp_handshake packet, peeraddr, peerport
209
+ when 't'
210
+ handle_ping packet, rxtime, peeraddr, peerport
211
+ else
212
+ raise UnknownPacketError, "Package is of unknown type: #{type}"
213
+ end
214
+ rescue MessagePack::MalformedFormatError => e
215
+ $logger.error "Packet cannot be decoded: #{e.message}"
216
+ rescue PeerError => e
217
+ $logger.error "Perr error: #{e.message}"
218
+ end
219
+
220
+ def handle_udp_handshake(packet, peeraddr, peerport)
221
+ if !(packet.keys - %w[M Y V S]).empty? ||
222
+ # M, Y are already checked in handle_packet from session manager
223
+ !packet['V'].is_a?(Integer)
224
+ raise InvalidMessageError, 'The peer has sent an invalid message. The handshake packet is incorrectly coded.'
225
+ end
226
+
227
+ session_id = packet['S']
228
+ session = @sessions[session_id]
229
+ 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
231
+
232
+ session.set_endpoint peeraddr, peerport
233
+
234
+ unless session.madebyme
235
+ # If the session was not started by me, the other peer expects a
236
+ # confirmation of the handshake in which the same is sent back.
237
+ $logger.debug "Send UDP Handshake back for session id #{session_id}."
238
+ session.send_udp_handshake
239
+ end
240
+
241
+ if session.udp_handshake_complete
242
+ $logger.warn 'UDP handshake is received, although a previous one was already successful.'
243
+ return
244
+ end
245
+
246
+ session.udp_handshake_recived
247
+ $logger.info "UDP handshake for session ID #{session_id} was successful."
248
+
249
+ session.start_pinger
250
+ end
251
+
252
+ def handle_ping(packet, rxtime, peeraddr, peerport)
253
+ session_id = packet['S']
254
+ session = @sessions[session_id]
255
+ unless session&.tcp_handshake_complete && session&.udp_handshake_complete
256
+ raise UnknownSessionError, "Ping packet received for non-initiated session id #{session_id}."
257
+ end
258
+
259
+ session.set_endpoint peeraddr, peerport
260
+ session.handle_ping packet, rxtime, peeraddr
261
+ end
262
+
263
+ def handle_client(client)
264
+ host = client.peeraddr[3]
265
+ port = client.peeraddr[1]
266
+ session = SPing::Session.new host, port, @socket, false
267
+
268
+ $logger.debug "Session request from host #{host} port #{port}."
269
+
270
+ session_id = nil
271
+
272
+ @sessions_mutex.synchronize do
273
+ session_id = generate_session_id
274
+ @sessions[session_id] = session
275
+ end
276
+
277
+ session.do_tcp_handshake1 client, session_id
278
+ rescue Errno::ECONNRESET, EOFError, IO::TimeoutError, TCPHandshakeError => e
279
+ $logger.warn "Could not establish a new session with Peer: #{e.message}"
280
+ del_session session_id
281
+ rescue OutOfSessions => e
282
+ $logger.error "Could not establish a new session with Peer: #{e.message}"
283
+ del_session session_id
261
284
  end
285
+ end
262
286
  end
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.2
4
+ version: 1.0.4
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-07 00:00:00.000000000 Z
11
+ date: 2023-11-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack
@@ -43,6 +43,7 @@ files:
43
43
  - LICENSE
44
44
  - README.md
45
45
  - bin/sping
46
+ - lib/errors.rb
46
47
  - lib/last_acks.rb
47
48
  - lib/session.rb
48
49
  - lib/session_manager.rb
@@ -68,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
68
69
  - !ruby/object:Gem::Version
69
70
  version: '0'
70
71
  requirements: []
71
- rubygems_version: 3.4.21
72
+ rubygems_version: 3.4.22
72
73
  signing_key:
73
74
  specification_version: 4
74
75
  summary: Reimplementation of sping in Ruby.