mnet 0.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.
Files changed (7) hide show
  1. checksums.yaml +7 -0
  2. data/DESIGN.md +164 -0
  3. data/README.en.md +125 -0
  4. data/README.md +118 -0
  5. data/lib/mnet/version.rb +5 -0
  6. data/lib/mnet.rb +1130 -0
  7. metadata +60 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2024e36584d9a17b0671a60776e00d4cee60c0fae82fc940f293566a07f471c7
4
+ data.tar.gz: 06d1487cb7206d21b62311c89f7e2419785346ebceb86995e79c185f698640e7
5
+ SHA512:
6
+ metadata.gz: 31c53f20db9e19f677e85452477000197e1f615bbf615a95d14faac1d84e9aa3045821eaa67302ebf6cf68f0ca3ed2e02f0af96b5e732affb84babe396296289
7
+ data.tar.gz: f06da08c602b9f4bba472e2778cdba7fd1d4e163c0c09d4aa244f04d6d22ba3cacc29b11939e95d986b199991b2b33af98a6e4f17bd0b5a0cd85705dc73cb3db
data/DESIGN.md ADDED
@@ -0,0 +1,164 @@
1
+ # mnet 设计
2
+
3
+ > 本文介绍 mnet 的包结构、数据传输、可靠性机制,以及「切网不断连」的实现原理。
4
+
5
+ ## 包结构
6
+
7
+ ```
8
+ mnet/
9
+ ├── lib/
10
+ │ ├── mnet.rb # Mnet 模块:Session / KcpSession / Endpoint / Server / SessionIO
11
+ │ └── mnet/version.rb # 版本号
12
+ └── mnet.gemspec # 依赖 kcp gem
13
+
14
+ kcp/(独立 gem,原生 C 扩展)
15
+ └── lib/kcp.rb # Kcp::Engine —— 可靠字节流引擎
16
+ ```
17
+
18
+ ### 模块分层
19
+
20
+ ```
21
+ ┌───────────────────────────────────────────────────┐
22
+ │ 应用层 read / write / readpartial / wait_readable │
23
+ ├───────────────────────────────────────────────────┤
24
+ │ SessionIO —— IO 兼容接口(可直接套 OpenSSL SSLSocket)│
25
+ ├───────────────────────────────────────────────────┤
26
+ │ Session(纯 Ruby ARQ) │ KcpSession(KCP 引擎) │
27
+ ├───────────────────────────────────────────────────┤
28
+ │ Endpoint —— UDP socket 管理 + 会话多路复用 │
29
+ ├───────────────────────────────────────────────────┤
30
+ │ UDPSocket(单个 socket,复用所有会话) │
31
+ └───────────────────────────────────────────────────┘
32
+ ```
33
+
34
+ ## 数据传输流
35
+
36
+ ### 发送端
37
+
38
+ ```
39
+ app.write(data)
40
+
41
+
42
+ Session#write ── 切分(≤ MSS=1200B)── 分配字节偏移序号 seq
43
+
44
+
45
+ 封装 Mnet 头(magic + session_id + seq + ack + type + flags + window)
46
+
47
+
48
+ [可选] AES-256-GCM 加密载荷(header 作为 AAD,头也防篡改)
49
+
50
+
51
+ Endpoint#send_raw ──► UDP socket
52
+ ```
53
+
54
+ ### 接收端
55
+
56
+ ```
57
+ UDP socket
58
+
59
+
60
+ Endpoint#reader_loop(IO.select 批量收包,一次读多个数据报)
61
+
62
+
63
+ 解包 ── 按 session_id 查会话
64
+
65
+
66
+ Session#handle_packet ── [解密] ── 按 seq 重组
67
+
68
+ ├─ seq == next_exp :按序交付 → @recv_buf → 广播唤醒 read
69
+ └─ seq > next_exp :乱序 → 暂存 @reasm(等前面的缺口)
70
+
71
+
72
+ app.read 取出
73
+ ```
74
+
75
+ ## 可靠性机制
76
+
77
+ | 机制 | 纯 Ruby Session | KCP 引擎(KcpSession) |
78
+ |---|---|---|
79
+ | 有序交付 | 字节偏移 seq + 乱序缓冲 `@reasm` | KCP 内置 |
80
+ | 丢包重传 | RTO(SRTT/RTTVAR 计算)超时重传 | KCP 快速重传 + 超时 |
81
+ | 流控 | 接收窗口 `recv_cap`(动态适配内核缓冲) | KCP 窗口 |
82
+ | 拥塞控制 | 无(mosh 式,只靠接收窗口) | KCP 内置(已关 nc=1) |
83
+
84
+ ### 序号与 ACK(纯 Ruby)
85
+
86
+ ```
87
+ 发送端 接收端
88
+ │ │
89
+ │── DATA seq=0,len=1200 ──────────►│
90
+ │── DATA seq=1200,len=1200 ───────►│
91
+ │── DATA seq=2400,len=1200 ───────►│ (乱序:暂存 seq=2400)
92
+ │ │
93
+ │◄── ACK ack=1200 ─────────────────│ (只收到了 seq=0)
94
+ │ │
95
+ │── DATA seq=1200 重传 ───────────►│ (RTO 超时触发)
96
+ │ │
97
+ │◄── ACK ack=3600 ─────────────────│ (缺口补齐,连续后统一 ACK)
98
+ ```
99
+
100
+ ## 切网不断连(漫游)
101
+
102
+ 核心思想:**会话身份 ≠ 源地址**。
103
+
104
+ TCP 用「源IP:源端口 + 目标IP:目标端口」四元组定位连接,源 IP 一变连接就断。
105
+ mnet 用 **128-bit 随机 token(session_id)** 标识会话——对端收到包时只要 token 合法,
106
+ 就更新「这个会话现在从哪来」,于是源地址变化不中断连接。
107
+
108
+ ### 迁移过程
109
+
110
+ ```
111
+ 切网前: client 192.168.1.5:5000 ── token ──► server(记住 client 地址)
112
+
113
+ (WIFI → 蜂窝,源地址变了;socket 绑定 0.0.0.0,内核自动换源)
114
+
115
+ 切网后: client 10.0.0.8:5000 ── token ──► server
116
+
117
+
118
+ update_peer 检测源地址变化
119
+ → 更新指向 10.0.0.8:5000
120
+ → 后续应答直接发往新地址(连接不断)
121
+ ```
122
+
123
+ ### 端口跳变 hop
124
+
125
+ ```
126
+ ┌─────────┐ ┌─────────┐
127
+ │ client │ socket1 │ │ 旧 socket(保留 60s 收延迟包)
128
+ │ ├─────────►│ server │
129
+ │ │ socket2 │ │ 新 socket(hop 后发送走这个)
130
+ └─────────┘ └─────────┘
131
+ ```
132
+
133
+ - **真实切网**:客户端绑定 `0.0.0.0`,内核按路由自动换源地址,无需任何操作
134
+ - **`hop`**:显式开新 socket(新源端口),旧 socket 保留 60s 兜底收延迟包
135
+
136
+ ## 会话状态机
137
+
138
+ ```
139
+ SYN DATA/ACK
140
+ :connecting ────► :established ────► :closed
141
+ │ │
142
+ │ SYNACK │ FIN
143
+ ▼ ▼
144
+ :established :closed
145
+ ```
146
+
147
+ ## 线程模型
148
+
149
+ ```
150
+ Endpoint(每个端点一个)
151
+ ├── reader 线程:IO.select 批量收包 → 解包 → 按 session_id 分发到会话
152
+ └── ticker 线程:每 50ms 驱动会话(重传 / ACK / 保活 / 窗口探测)
153
+
154
+ 每个 Session
155
+ ├── 应用线程:write / read(阻塞在 Monitor + ConditionVariable)
156
+ └── [可选] TLS 桥接线程(socketpair,仅嵌套 TLS 需要;明文用 session 直连)
157
+ ```
158
+
159
+ ## 加密(可选 session_key)
160
+
161
+ - 会话 ID 由密钥派生:`session_id = SHA256(key)[0,16]`
162
+ - 每包 **AES-256-GCM**,明文头作为 AAD(连头部都防篡改)
163
+ - 每次加密用随机 nonce,避免重传时 nonce 复用
164
+ - 密钥需**预共享**(带外分发);密钥不对 → 解密失败 → 丢包 → 连不上
data/README.en.md ADDED
@@ -0,0 +1,125 @@
1
+ # mnet
2
+
3
+ [简体中文](README.md) | English
4
+
5
+ A reliable UDP transport whose connections survive network / egress changes (like mosh).
6
+ It ships a pure-Ruby ARQ engine plus a C engine built on the `kcp` gem.
7
+
8
+ > For the internals (package structure, data flow, roaming) see **[DESIGN.md](DESIGN.md)**.
9
+
10
+ ## Features
11
+
12
+ - **Roaming**: sessions are identified by a 128-bit token (not the TCP source-address 4-tuple),
13
+ so the peer just updates its address when the source changes
14
+ - **Port hopping**: mosh-style `hop`; old sockets are kept for 60s to catch delayed packets
15
+ - **Optional encryption**: per-packet AES-256-GCM (`key`, pre-shared)
16
+ - **Two engines**: `proto: :mnet` (pure Ruby, no C dependency) / `proto: :kcp` (C engine, fast)
17
+ - **Stream sessions**: `Mnet::Session` implements an IO-compatible interface
18
+ (`read / write / readpartial / wait_readable`), so `OpenSSL::SSL::SSLSocket` can wrap it directly
19
+ - Cross-platform: the pure-Ruby part is inherently portable; KCP is a native extension from the `kcp` gem
20
+
21
+ ## Installation
22
+
23
+ ```ruby
24
+ # Gemfile
25
+ gem 'mnet'
26
+ ```
27
+
28
+ ## Quick start
29
+
30
+ ```ruby
31
+ require 'mnet'
32
+
33
+ # ---- server (echo) ----
34
+ server = Mnet::Server.new('0.0.0.0', 9000)
35
+ loop do
36
+ io = server.accept_session
37
+ Thread.new { while (d = io.readpartial(65_536)); io.write(d); end }
38
+ end
39
+
40
+ # ---- client ----
41
+ endpoint = Mnet::Endpoint.new
42
+ sess = endpoint.dial('127.0.0.1', 9000, proto: :kcp) # :mnet or :kcp
43
+ sess.write('hello')
44
+ puts sess.readpartial(1024)
45
+ ```
46
+
47
+ ## Engine choice
48
+
49
+ | `proto` | Description |
50
+ |---|---|
51
+ | `:mnet` | pure-Ruby ARQ, no C dependency, simple and easy to debug |
52
+ | `:kcp` | C engine (via the `kcp` gem), ~8x faster, requires building a native extension |
53
+
54
+ ## Encryption (pre-shared key)
55
+
56
+ Per-packet AES-256-GCM; both ends must pre-share the same 32-byte key:
57
+
58
+ ```ruby
59
+ require 'securerandom'
60
+ key = SecureRandom.random_bytes(32)
61
+
62
+ # server registers the key (one or more)
63
+ server = Mnet::Server.new('0.0.0.0', 9000, keys: [key])
64
+
65
+ # client dials with the key
66
+ sess = endpoint.dial('127.0.0.1', 9000, proto: :kcp, key: key)
67
+ ```
68
+
69
+ The session id is derived from the key (`SHA256(key)[0,16]`); with the wrong key the server fails to
70
+ decrypt and drops the packet, so the client cannot connect.
71
+
72
+ ## TLS (nested)
73
+
74
+ `Mnet::Session` implements an IO-compatible interface, so `SSLSocket` can wrap it directly
75
+ (`SSLServer` usage is compatible):
76
+
77
+ ```ruby
78
+ require 'openssl'
79
+
80
+ server = Mnet::Server.new('0.0.0.0', 9001)
81
+ ssl_server = OpenSSL::SSL::SSLServer.new(server, ssl_context)
82
+ ssl_server.start_immediately = false # do the handshake in worker threads, don't block accept
83
+ loop do
84
+ ssl_sock = ssl_server.accept
85
+ Thread.new do
86
+ ssl_sock.accept
87
+ # ...read/write ssl_sock...
88
+ end
89
+ end
90
+ ```
91
+
92
+ ## Roaming
93
+
94
+ ```ruby
95
+ endpoint.hop # simulate a network change (in reality the client binds 0.0.0.0, and the OS
96
+ # re-picks the source address automatically, so no manual hop is needed)
97
+ ```
98
+
99
+ ## Benchmarks
100
+
101
+ Environment: loopback (127.0.0.1), pipelined echo (concurrent writer/reader, counted by total bytes).
102
+
103
+ | Scheme | 1KB-message throughput |
104
+ |---|---|
105
+ | TCP (raw) | ~49 MB/s |
106
+ | TCP + SSL | ~22 MB/s |
107
+ | **mnet pure Ruby** (`proto: :mnet`) | ~1.8 MB/s |
108
+ | mnet pure Ruby + key (AES-GCM) | ~1.6 MB/s |
109
+ | **mnet + KCP** (`proto: :kcp`) | **~5.0 MB/s** |
110
+ | mnet + KCP + key (AES-GCM) | ~4.1 MB/s |
111
+
112
+ Takeaways:
113
+
114
+ - **The KCP engine is ~2.7x faster than pure Ruby** (5.0 vs 1.8 MB/s)
115
+ - **Encryption overhead is small**: `key` (per-packet AES-GCM) only costs ~15–18%
116
+ - **Plaintext KCP is ~1/10 of TCP**; the bottleneck is the Ruby per-packet glue
117
+ (pack/unpack + thread switching), not the ARQ algorithm
118
+ - These are synthetic loopback numbers; over a real network latency dominates and interactive
119
+ (small-message) workloads feel identical
120
+
121
+ Reproduce: `ruby -I lib -I ../kcp/lib test/bench_compare.rb [msgs] [msg_bytes]`
122
+
123
+ ## License
124
+
125
+ MIT.
data/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # mnet
2
+
3
+ [English](README.en.md) | 简体中文
4
+
5
+ 可靠的 UDP 传输层,连接在**切换网络 / 出口 IP 变化时不断开**(类似 mosh)。
6
+ 提供纯 Ruby ARQ 引擎,以及基于 `kcp` gem 的 C 引擎。
7
+
8
+ > 想了解内部实现原理(包结构、数据传输、切网不断连)见 **[DESIGN.md](DESIGN.md)**。
9
+
10
+ ## 特性
11
+
12
+ - **切网不断连**:会话用 128-bit token 标识(而非 TCP 的源地址四元组),源地址变化时对端自动更新指向
13
+ - **端口跳变**:mosh 式 `hop`,旧 socket 保留 60s 收延迟包
14
+ - **可选加密**:每包 AES-256-GCM(`key`,需预共享)
15
+ - **两种引擎**:`proto: :mnet`(纯 Ruby,无 C 依赖)/ `proto: :kcp`(C 引擎,快)
16
+ - **流式会话**:`Mnet::Session` 实现 IO 兼容接口(`read / write / readpartial / wait_readable`),可直接被 `OpenSSL::SSL::SSLSocket` 包装
17
+ - 跨平台:纯 Ruby 部分天然跨平台,KCP 由 `kcp` gem 提供原生扩展
18
+
19
+ ## 安装
20
+
21
+ ```ruby
22
+ # Gemfile
23
+ gem 'mnet'
24
+ ```
25
+
26
+ ## 快速开始
27
+
28
+ ```ruby
29
+ require 'mnet'
30
+
31
+ # ---- 服务端(回显)----
32
+ server = Mnet::Server.new('0.0.0.0', 9000)
33
+ loop do
34
+ io = server.accept_session
35
+ Thread.new { while (d = io.readpartial(65_536)); io.write(d); end }
36
+ end
37
+
38
+ # ---- 客户端 ----
39
+ endpoint = Mnet::Endpoint.new
40
+ sess = endpoint.dial('127.0.0.1', 9000, proto: :kcp) # :mnet 或 :kcp
41
+ sess.write('hello')
42
+ puts sess.readpartial(1024)
43
+ ```
44
+
45
+ ## 引擎选择
46
+
47
+ | `proto` | 说明 |
48
+ |---|---|
49
+ | `:mnet` | 纯 Ruby ARQ,无 C 依赖,简单、易调试 |
50
+ | `:kcp` | C 引擎(依赖 `kcp` gem),快约 8 倍,需编译原生扩展 |
51
+
52
+ ## 加密(预共享密钥)
53
+
54
+ 每包 AES-256-GCM,两端需预共享同一个 32 字节密钥:
55
+
56
+ ```ruby
57
+ require 'securerandom'
58
+ key = SecureRandom.random_bytes(32)
59
+
60
+ # 服务端注册密钥(可注册多个)
61
+ server = Mnet::Server.new('0.0.0.0', 9000, keys: [key])
62
+
63
+ # 客户端带密钥拨号
64
+ sess = endpoint.dial('127.0.0.1', 9000, proto: :kcp, key: key)
65
+ ```
66
+
67
+ 会话 ID 由密钥派生(`SHA256(key)[0,16]`);密钥不对时服务端解密失败、直接丢弃包,客户端连不上。
68
+
69
+ ## TLS(嵌套)
70
+
71
+ `Mnet::Session` 实现了 IO 兼容接口,可被 `SSLSocket` 直接包装(`SSLServer` 写法兼容):
72
+
73
+ ```ruby
74
+ require 'openssl'
75
+
76
+ server = Mnet::Server.new('0.0.0.0', 9001)
77
+ ssl_server = OpenSSL::SSL::SSLServer.new(server, ssl_context)
78
+ ssl_server.start_immediately = false # 把握手放到工作线程,不阻塞 accept
79
+ loop do
80
+ ssl_sock = ssl_server.accept
81
+ Thread.new do
82
+ ssl_sock.accept
83
+ # ...读写 ssl_sock...
84
+ end
85
+ end
86
+ ```
87
+
88
+ ## 切网迁移
89
+
90
+ ```ruby
91
+ endpoint.hop # 模拟切网(真实场景下客户端绑 0.0.0.0,内核自动换源地址,无需手动 hop)
92
+ ```
93
+
94
+ ## 基准测试
95
+
96
+ 环境:loopback(127.0.0.1),pipelined echo(写端与读端并发,按总字节计)。
97
+
98
+ | 方案 | 1KB 消息吞吐 |
99
+ |---|---|
100
+ | TCP(裸) | ~49 MB/s |
101
+ | TCP + SSL | ~22 MB/s |
102
+ | **mnet 纯 Ruby**(`proto: :mnet`) | ~1.8 MB/s |
103
+ | mnet 纯 Ruby + key(AES-GCM) | ~1.6 MB/s |
104
+ | **mnet + KCP**(`proto: :kcp`) | **~5.0 MB/s** |
105
+ | mnet + KCP + key(AES-GCM) | ~4.1 MB/s |
106
+
107
+ 结论:
108
+
109
+ - **KCP 引擎比纯 Ruby 快约 2.7 倍**(5.0 vs 1.8 MB/s)
110
+ - **加密开销很小**:`key`(每包 AES-GCM)只慢 ~15~18%
111
+ - **明文 KCP 约为 TCP 的 1/10**;瓶颈在 Ruby 层的每包 glue(pack/unpack + 线程切换),不在 ARQ 算法
112
+ - 以上是 loopback 合成基准;真实网络延迟主导时,交互式(小消息)体感无差别
113
+
114
+ 复现:`ruby -I lib -I ../kcp/lib test/bench_compare.rb [消息数] [消息字节]`
115
+
116
+ ## 许可
117
+
118
+ MIT。
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mnet
4
+ VERSION = '0.1.0'
5
+ end