network-infra-utility 0.5.0 → 0.8.3

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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/CHANGELOG.md +31 -0
  4. data/GUIDE.md +201 -0
  5. data/README.md +1 -6
  6. data/Rakefile +1 -1
  7. data/bin/dns-query +4 -2
  8. data/bin/gen-get +144 -0
  9. data/bin/geo-api +40 -1
  10. data/bin/geo-get +18 -36
  11. data/bin/geo-update +429 -0
  12. data/bin/ngeo-get +253 -0
  13. data/bin/packbit +227 -0
  14. data/bin/probe +134 -0
  15. data/document/reference/DNS/346/212/200/346/234/257/346/226/207/346/241/243.md +912 -0
  16. data/network-infra-utility.gemspec +6 -4
  17. data/network.rb +19 -1
  18. data/service/geodb/GeoAPI.md +64 -0
  19. data/service/geodb/api.rb +77 -19
  20. data/service/geoquery/GeoQuery.md +309 -0
  21. data/service/geoquery/cache.rb +78 -0
  22. data/service/geoquery/geo_cache.rb +233 -0
  23. data/service/geoquery/geoquery.rb +327 -0
  24. data/service/geoquery/local.rb +151 -0
  25. data/service/geoquery/merge.rb +52 -0
  26. data/service/geoquery/normalize.rb +102 -0
  27. data/service/geoquery/online.rb +136 -0
  28. data/service/simlab/SimLab.md +140 -0
  29. data/service/simlab/bin/simctl +152 -0
  30. data/service/simlab/lib/simlab.rb +124 -0
  31. data/service/simlab/lib/simlab_engine.rb +146 -0
  32. data/service/simlab/lib/simlab_observer.rb +176 -0
  33. data/service/simlab/lib/simlab_router.rb +330 -0
  34. data/service/simlab/lib/simlab_scenario.rb +200 -0
  35. data/service/simlab/lib/simlab_switch.rb +182 -0
  36. data/service/simlab/lib/simlab_topology.rb +196 -0
  37. data/service/simlab/lib/simlab_traffic.rb +167 -0
  38. data/service/ssh/README.md +50 -37
  39. data/service/ssh/ext/ssh_core/src/ssh_infra_sup.erl +14 -1
  40. data/support/basic/packet.rb +167 -0
  41. data/support/routing/bgp.rb +279 -0
  42. data/support/routing/lpm_trie.rb +145 -0
  43. data/support/routing/ospf.rb +243 -0
  44. data/support/routing/prefix.rb +155 -0
  45. data/support/routing/rib.rb +167 -0
  46. data/support/routing/rip.rb +223 -0
  47. data/support/routing/static_route.rb +102 -0
  48. data/support/switching/frame_forward.rb +87 -0
  49. data/support/switching/mac_table.rb +127 -0
  50. data/support/switching/stp.rb +230 -0
  51. data/support/switching/vlan.rb +155 -0
  52. data/tool/packbit/README.md +136 -0
  53. data/tool/packbit/config/five-tuple.yml +25 -0
  54. data/tool/packbit/config/flow.yml +17 -0
  55. data/tool/packbit/config/packbit.yml +41 -0
  56. data/tool/packbit/erl/packbit.erl +588 -0
  57. data/tool/probe/README.md +88 -0
  58. data/tool/probe/lib/probe.rb +54 -0
  59. data/tool/probe/lib/probe_base.rb +98 -0
  60. data/tool/probe/lib/probe_dns.rb +64 -0
  61. data/tool/probe/lib/probe_icmp.rb +71 -0
  62. data/tool/probe/lib/probe_ntp.rb +51 -0
  63. data/tool/probe/lib/probe_radius.rb +58 -0
  64. data/tool/probe/lib/probe_registry.rb +44 -0
  65. data/tool/probe/lib/probe_result.rb +75 -0
  66. data/tool/probe/lib/probe_snmp.rb +114 -0
  67. data/tool/probe/lib/probe_tcp.rb +82 -0
  68. data/tool/probe/lib/probe_version.rb +7 -0
  69. data/version.rb +1 -1
  70. metadata +78 -2
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ # simctl - 路由交换仿真 CLI 工具。
6
+ #
7
+ # 用法:
8
+ # simctl simulate SCENARIO.yml # 运行场景
9
+ # simctl simulate SCENARIO.yml --until 100 # 运行到指定时间
10
+ # simctl dump --rib R1 SCENARIO.yml # 导出 RIB
11
+ # simctl dump --mac S1 SCENARIO.yml # 导出 MAC 表
12
+ # simctl trace --from H1 --to 10.0.0.2 SCENARIO.yml # 路径追踪
13
+
14
+ require "thor"
15
+ require_relative "../lib/simlab"
16
+
17
+ module NetworkInfraUtility
18
+ module SimLab
19
+ class CLI < Thor
20
+ desc "simulate SCENARIO", "运行仿真场景"
21
+ option :until, type: :numeric, default: 100, desc: "运行到指定虚拟时间"
22
+ option :steps, type: :numeric, desc: "执行指定步数"
23
+ option :verbose, type: :boolean, default: false, desc: "详细输出"
24
+
25
+ def simulate(scenario_file)
26
+ sim = Simulation.new
27
+ sim.load(scenario_file)
28
+ sim.build
29
+
30
+ puts "Loading scenario: #{scenario_file}"
31
+ puts "Devices: #{sim.topology.device_names.join(', ')}"
32
+ puts "Links: #{sim.topology.links.size}"
33
+ puts "Running simulation..."
34
+
35
+ if options[:steps]
36
+ sim.run(steps: options[:steps])
37
+ else
38
+ sim.run(final_time: options[:until])
39
+ end
40
+
41
+ puts "Simulation completed at t=#{sim.time}"
42
+
43
+ if options[:verbose]
44
+ sim.topology.devices.each_value do |device|
45
+ if device.is_a?(Device::Router)
46
+ puts sim.observer.format_rib(device)
47
+ elsif device.is_a?(Device::Switch)
48
+ puts sim.observer.format_mac(device)
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ desc "dump SCENARIO", "导出设备表信息"
55
+ option :rib, type: :string, desc: "导出指定路由器的 RIB"
56
+ option :mac, type: :string, desc: "导出指定交换机的 MAC 表"
57
+ option :stp, type: :string, desc: "导出指定交换机的 STP 状态"
58
+ option :all, type: :boolean, default: false, desc: "导出所有设备"
59
+
60
+ def dump(scenario_file)
61
+ sim = Simulation.new
62
+ sim.load(scenario_file)
63
+ sim.build
64
+ sim.run(final_time: 1) # 跑一个 tick 让协议初始化
65
+
66
+ if options[:rib]
67
+ device = sim.topology.devices[options[:rib]]
68
+ if device && device.is_a?(Device::Router)
69
+ puts sim.observer.format_rib(device)
70
+ else
71
+ puts "Device #{options[:rib]} not found or not a router"
72
+ end
73
+ end
74
+
75
+ if options[:mac]
76
+ device = sim.topology.devices[options[:mac]]
77
+ if device && device.is_a?(Device::Switch)
78
+ puts sim.observer.format_mac(device)
79
+ else
80
+ puts "Device #{options[:mac]} not found or not a switch"
81
+ end
82
+ end
83
+
84
+ if options[:stp]
85
+ device = sim.topology.devices[options[:stp]]
86
+ if device && device.is_a?(Device::Switch) && device.stp
87
+ stp_info = device.stp_dump
88
+ puts "=== STP: #{options[:stp]} ==="
89
+ puts " Bridge ID: #{stp_info[:bridge_id]}"
90
+ puts " Root ID: #{stp_info[:root_id]}"
91
+ puts " Root Bridge: #{stp_info[:root_bridge]}"
92
+ stp_info[:ports].each { |port, info| puts " #{port}: role=#{info[:role]} state=#{info[:state]}" }
93
+ else
94
+ puts "Device #{options[:stp]} not found, not a switch, or STP not enabled"
95
+ end
96
+ end
97
+
98
+ if options[:all]
99
+ sim.topology.devices.each_value do |device|
100
+ if device.is_a?(Device::Router)
101
+ puts sim.observer.format_rib(device)
102
+ elsif device.is_a?(Device::Switch)
103
+ puts sim.observer.format_mac(device)
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ desc "trace SCENARIO", "追踪转发路径"
110
+ option :from, type: :string, required: true, desc: "起始设备"
111
+ option :to, type: :string, required: true, desc: "目的 IP"
112
+
113
+ def trace(scenario_file)
114
+ sim = Simulation.new
115
+ sim.load(scenario_file)
116
+ sim.build
117
+ sim.run(final_time: 10)
118
+
119
+ result = sim.trace(from: options[:from], to: options[:to])
120
+ puts sim.observer.format_trace(result)
121
+ end
122
+
123
+ desc "list SCENARIO", "列出场景中的设备和链路"
124
+
125
+ def list(scenario_file)
126
+ sim = Simulation.new
127
+ sim.load(scenario_file)
128
+
129
+ puts "=== Devices ==="
130
+ sim.topology.devices.each_value do |d|
131
+ type = d.is_a?(Device::Router) ? "Router" : "Switch"
132
+ puts " #{d.name} (#{type})"
133
+ if d.is_a?(Device::Router)
134
+ d.interfaces.each { |port, info| puts " #{port}: #{info[:cidr]} (#{info[:status]})" }
135
+ else
136
+ puts " Ports: #{d.ports.keys.join(', ')}"
137
+ end
138
+ end
139
+
140
+ puts "\n=== Links ==="
141
+ sim.topology.links.each do |link|
142
+ puts " #{link.endpoint_a} <-> #{link.endpoint_b} (#{link.status}, bw=#{link.bandwidth}, delay=#{link.delay})"
143
+ end
144
+
145
+ puts "\n=== Hosts ==="
146
+ sim.topology.hosts.each { |name, info| puts " #{name}: #{info[:ip]} gw=#{info[:gateway]}" }
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ NetworkInfraUtility::SimLab::CLI.start(ARGV)
@@ -0,0 +1,124 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ # SimLab 统一入口模块。
5
+ #
6
+ # 负责加载仿真服务的所有子模块,提供统一接口。
7
+ #
8
+ # 用法:
9
+ # require_relative "lib/simlab"
10
+ # simlab = NetworkInfraUtility::SimLab::Simulation.new
11
+ # simlab.load("scenario.yml")
12
+ # simlab.run(final_time: 100)
13
+ # simlab.dump_rib("R1")
14
+
15
+ require_relative "simlab_topology"
16
+ require_relative "simlab_engine"
17
+ require_relative "simlab_router"
18
+ require_relative "simlab_switch"
19
+ require_relative "simlab_traffic"
20
+ require_relative "simlab_observer"
21
+ require_relative "simlab_scenario"
22
+
23
+ module NetworkInfraUtility
24
+ module SimLab
25
+ class Error < StandardError; end
26
+
27
+ class Simulation
28
+ attr_reader :topology, :engine, :observer, :time
29
+
30
+ def initialize
31
+ @topology = Topology.new
32
+ @engine = Engine.new
33
+ @observer = Observer.new
34
+ @traffic = Traffic.new
35
+ @time = 0
36
+ end
37
+
38
+ # 加载场景文件(YAML 或 Ruby DSL)。
39
+ def load(file_path)
40
+ scenario = Scenario.load(file_path)
41
+ scenario.apply_to(self)
42
+ self
43
+ end
44
+
45
+ # 从拓扑构建仿真环境。
46
+ def build
47
+ @topology.devices.each_value(&:build)
48
+ @topology.links.each { |link| link.activate }
49
+ self
50
+ end
51
+
52
+ # 运行仿真直到指定时间。
53
+ def run(final_time: nil, steps: nil)
54
+ if final_time
55
+ @engine.run_until(final_time) do |time, event|
56
+ @time = time
57
+ tick_devices(time)
58
+ event.call
59
+ end
60
+ elsif steps
61
+ @engine.run_steps(steps) do |time, event|
62
+ @time = time
63
+ tick_devices(time)
64
+ event.call
65
+ end
66
+ end
67
+ self
68
+ end
69
+
70
+ # 单步执行。
71
+ def step
72
+ @engine.step do |time, event|
73
+ @time = time
74
+ tick_devices(time)
75
+ event.call
76
+ end
77
+ self
78
+ end
79
+
80
+ # 注入流量/事件。
81
+ def inject(event)
82
+ @traffic.inject(@engine, event)
83
+ end
84
+
85
+ # 导出 RIB 快照。
86
+ def dump_rib(device_name)
87
+ device = @topology.devices[device_name]
88
+ raise Error, "Device #{device_name} not found" unless device
89
+
90
+ @observer.rib_dump(device)
91
+ end
92
+
93
+ # 导出 MAC 表快照。
94
+ def dump_mac(device_name)
95
+ device = @topology.devices[device_name]
96
+ raise Error, "Device #{device_name} not found" unless device
97
+
98
+ @observer.mac_dump(device)
99
+ end
100
+
101
+ # 转发路径追踪。
102
+ def trace(from:, to:)
103
+ @observer.trace(topology: @topology, from: from, to: to)
104
+ end
105
+
106
+ # 收敛检测。
107
+ def converged?(ticks: 5)
108
+ @observer.converged?(topology: @topology, ticks: ticks, engine: @engine)
109
+ end
110
+
111
+ # 快照导出。
112
+ def snapshot
113
+ @observer.snapshot(@topology)
114
+ end
115
+
116
+ private
117
+
118
+ # 全局时钟推进时,所有设备同步 tick。
119
+ def tick_devices(now)
120
+ @topology.devices.each_value { |device| device.tick(now: now) }
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,146 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ # 离散事件仿真核心:全局虚拟时钟 + 事件队列。
5
+ #
6
+ # 设计要点:
7
+ # - 全局虚拟时钟
8
+ # - 事件队列(按时间排序)
9
+ # - 支持延迟事件(链路延迟、计时器、老化)
10
+ # - 收敛检测辅助(统计 N 个 tick 内 RIB/MAC 无变化)
11
+ #
12
+ # support 组件通过 tick(now) 被动推进,不自己计时。
13
+ # device 之间不直接互调方法,由 engine 按链路投递帧/报文(可带延迟)。
14
+ #
15
+ # 用法:
16
+ # engine = Engine.new
17
+ # engine.schedule(at: 10) { puts "tick at 10" }
18
+ # engine.schedule(at: 20, delay: 5) { puts "delayed event" }
19
+ # engine.run_until(30) { |time, event| event.call }
20
+
21
+ module NetworkInfraUtility
22
+ module SimLab
23
+ # 仿真事件。
24
+ Event = Data.define(:at, :priority, :id, :block) do
25
+ include Comparable
26
+
27
+ def <=>(other)
28
+ [at, priority] <=> [other.at, other.priority]
29
+ end
30
+ end
31
+
32
+ # 投递信封:封装设备间传递的报文。
33
+ Envelope = Data.define(:at, :src_node, :src_if, :dst_node, :dst_if, :payload)
34
+
35
+ class Engine
36
+ attr_reader :clock, :event_count
37
+
38
+ def initialize
39
+ @clock = 0 # 虚拟时钟(秒)
40
+ @queue = [] # 事件队列(最小堆模拟)
41
+ @event_count = 0
42
+ @seq = 0 # 事件序号(用于稳定排序)
43
+ @stopped = false
44
+ end
45
+
46
+ # 调度一个事件在指定时间执行。
47
+ # at: 执行时间
48
+ # priority: 优先级(数字小的先执行,默认 0)
49
+ # 返回事件 ID(可用于取消)。
50
+ def schedule(at:, priority: 0, &block)
51
+ @seq += 1
52
+ event = Event.new(at: at, priority: priority, id: @seq, block: block)
53
+ @queue << event
54
+ @queue.sort_by! { |e| [e.at, e.priority, e.id] }
55
+ @event_count += 1
56
+ @seq
57
+ end
58
+
59
+ # 调度一个延迟事件。
60
+ # delay: 从当前时钟起的延迟
61
+ def schedule_delayed(delay:, priority: 0, &block)
62
+ schedule(at: @clock + delay, priority: priority, &block)
63
+ end
64
+
65
+ # 投递信封(带链路延迟)。
66
+ def deliver(envelope, delay: 0)
67
+ schedule(at: envelope.at + delay) do
68
+ yield envelope if block_given?
69
+ end
70
+ end
71
+
72
+ # 取消指定 ID 的事件。
73
+ def cancel(event_id)
74
+ @queue.reject! { |e| e.id == event_id }
75
+ end
76
+
77
+ # 运行到指定时间。
78
+ def run_until(final_time)
79
+ while !@queue.empty? && !@stopped
80
+ event = @queue.first
81
+ break if event.at > final_time
82
+
83
+ @queue.shift
84
+ @clock = event.at
85
+ yield event.at, event.block if block_given?
86
+ end
87
+ @clock = final_time if @clock < final_time
88
+ end
89
+
90
+ # 执行指定步数的事件。
91
+ def run_steps(steps)
92
+ steps.times do
93
+ break if @queue.empty? || @stopped
94
+
95
+ event = @queue.shift
96
+ @clock = event.at
97
+ yield event.at, event.block if block_given?
98
+ end
99
+ end
100
+
101
+ # 执行单步。
102
+ def step
103
+ return nil if @queue.empty? || @stopped
104
+
105
+ event = @queue.shift
106
+ @clock = event.at
107
+ yield event.at, event.block if block_given?
108
+ event
109
+ end
110
+
111
+ # 停止仿真。
112
+ def stop
113
+ @stopped = true
114
+ end
115
+
116
+ # 重置。
117
+ def reset
118
+ @clock = 0
119
+ @queue.clear
120
+ @event_count = 0
121
+ @seq = 0
122
+ @stopped = false
123
+ end
124
+
125
+ # 队列中剩余事件数。
126
+ def pending
127
+ @queue.size
128
+ end
129
+
130
+ # 是否有空闲(队列空)。
131
+ def idle?
132
+ @queue.empty?
133
+ end
134
+
135
+ # 当前时钟。
136
+ def now
137
+ @clock
138
+ end
139
+
140
+ # 查看下一个事件时间。
141
+ def next_event_time
142
+ @queue.first&.at
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,176 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ # 观测导出:RIB/MAC 快照、转发路径 trace、收敛时间统计。
5
+ #
6
+ # 输出 JSON/文本快照,便于断言和教学演示。
7
+ #
8
+ # 用法:
9
+ # observer = Observer.new
10
+ # observer.rib_dump(router)
11
+ # observer.mac_dump(switch)
12
+ # observer.trace(topology: topo, from: "H1", to: "10.0.0.2")
13
+
14
+ require 'json'
15
+ require 'set'
16
+
17
+ module NetworkInfraUtility
18
+ module SimLab
19
+ class Observer
20
+ # RIB 快照。
21
+ def rib_dump(device)
22
+ device.rib_dump
23
+ end
24
+
25
+ # MAC 表快照。
26
+ def mac_dump(device)
27
+ device.mac_dump
28
+ end
29
+
30
+ # 转发路径追踪。
31
+ # 返回 [{device:, port:, decision:}] 的跳转路径。
32
+ def trace(topology:, from:, to:)
33
+ path = []
34
+ visited = Set.new
35
+ current_device = topology.devices[from]
36
+
37
+ return path unless current_device
38
+
39
+ while current_device && !visited.include?(current_device.name)
40
+ visited.add(current_device.name)
41
+
42
+ if current_device.is_a?(Device::Router)
43
+ # 路由器:查 RIB 确定下一跳
44
+ route = current_device.rib.lookup(parse_ip(to))
45
+ break unless route
46
+
47
+ hop = {
48
+ device: current_device.name,
49
+ type: :router,
50
+ port: route.ifindex,
51
+ next_hop: route.next_hop,
52
+ protocol: route.protocol
53
+ }
54
+ path << hop
55
+
56
+ # 找对端设备
57
+ adj = topology.peer_of(current_device.name, route.ifindex)
58
+ break unless adj
59
+
60
+ current_device = topology.devices[adj[:peer_device]]
61
+
62
+ elsif current_device.is_a?(Device::Switch)
63
+ # 交换机:查 MAC 表确定出端口
64
+ mac_entry = current_device.mac_table.lookup(to)
65
+ port = mac_entry ? mac_entry.port : nil
66
+
67
+ hop = {
68
+ device: current_device.name,
69
+ type: :switch,
70
+ port: port || 'flood',
71
+ decision: mac_entry ? :forward : :flood
72
+ }
73
+ path << hop
74
+
75
+ if port
76
+ adj = topology.peer_of(current_device.name, port)
77
+ break unless adj
78
+ current_device = topology.devices[adj[:peer_device]]
79
+ else
80
+ break
81
+ end
82
+ else
83
+ break
84
+ end
85
+ end
86
+
87
+ path
88
+ end
89
+
90
+ # 收敛检测:统计 N 个 tick 内 RIB/MAC 是否无变化。
91
+ def converged?(topology:, ticks:, engine:)
92
+ # 简化实现:检查是否还有 pending 事件
93
+ # 真实实现应比较 RIB/MAC 快照
94
+ snapshots_before = capture_snapshots(topology)
95
+
96
+ ticks.times do
97
+ engine.step do |time, event|
98
+ topology.devices.each_value { |d| d.tick(now: time) }
99
+ event.call
100
+ end
101
+ end
102
+
103
+ snapshots_after = capture_snapshots(topology)
104
+ snapshots_before == snapshots_after
105
+ end
106
+
107
+ # 全局快照。
108
+ def snapshot(topology)
109
+ {
110
+ devices: topology.devices.transform_values { |d| d.to_h },
111
+ links: topology.links.map { |l| { a: l.endpoint_a.to_s, b: l.endpoint_b.to_s, status: l.status } },
112
+ timestamp: engine_now
113
+ }
114
+ end
115
+
116
+ # 导出为 JSON。
117
+ def to_json(topology)
118
+ JSON.pretty_generate(snapshot(topology))
119
+ end
120
+
121
+ # RIB 快照格式化输出。
122
+ def format_rib(device)
123
+ rib_data = device.rib_dump
124
+ lines = ["=== RIB: #{device.name} ==="]
125
+
126
+ rib_data.each do |prefix, routes|
127
+ routes.each do |r|
128
+ lines << " #{prefix} via #{r[:next_hop]} (#{r[:protocol]} metric=#{r[:metric]})"
129
+ end
130
+ end
131
+
132
+ lines.join("\n")
133
+ end
134
+
135
+ # MAC 表快照格式化输出。
136
+ def format_mac(device)
137
+ mac_data = device.mac_dump
138
+ lines = ["=== MAC Table: #{device.name} ==="]
139
+
140
+ mac_data.each do |mac, info|
141
+ lines << " #{mac} -> #{info[:port]}" + (info[:static] ? ' (static)' : '')
142
+ end
143
+
144
+ lines.join("\n")
145
+ end
146
+
147
+ # 路径追踪格式化输出。
148
+ def format_trace(trace_result)
149
+ lines = ["=== Trace ==="]
150
+ trace_result.each do |hop|
151
+ lines << " #{hop[:device]} (#{hop[:type]}) -> port #{hop[:port]}" +
152
+ (hop[:next_hop] ? " next_hop=#{hop[:next_hop]} proto=#{hop[:protocol]}" : '') +
153
+ (hop[:decision] ? " decision=#{hop[:decision]}" : '')
154
+ end
155
+ lines << " (reached destination)" if trace_result.any?
156
+ lines.join("\n")
157
+ end
158
+
159
+ private
160
+
161
+ def capture_snapshots(topology)
162
+ topology.devices.transform_values do |d|
163
+ d.is_a?(Device::Router) ? d.rib_dump : d.mac_dump
164
+ end
165
+ end
166
+
167
+ def parse_ip(ip_str)
168
+ ip_str.include?(':') ? IPv6.new(ip_str) : IPv4.new(ip_str)
169
+ end
170
+
171
+ def engine_now
172
+ Time.now.to_i # 简化
173
+ end
174
+ end
175
+ end
176
+ end