mnet
简体中文 | English
A reliable UDP transport whose connections survive network / egress changes (like mosh).
It ships a pure-Ruby ARQ engine plus a C engine built on the kcp gem.
For the internals (package structure, data flow, roaming) see DESIGN.md.
Features
- Roaming: sessions are identified by a 128-bit token (not the TCP source-address 4-tuple), so the peer just updates its address when the source changes
- Port hopping: mosh-style
hop; old sockets are kept for 60s to catch delayed packets - Optional encryption: per-packet AES-256-GCM (
key, pre-shared) - Two engines:
proto: :mnet(pure Ruby, no C dependency) /proto: :kcp(C engine, fast) - Stream sessions:
Mnet::Sessionimplements an IO-compatible interface (read / write / readpartial / wait_readable), soOpenSSL::SSL::SSLSocketcan wrap it directly - Cross-platform: the pure-Ruby part is inherently portable; KCP is a native extension from the
kcpgem
Installation
# Gemfile
gem 'mnet'
Quick start
require 'mnet'
# ---- server (echo) ----
server = Mnet::Server.new('0.0.0.0', 9000)
loop do
io = server.accept_session
Thread.new { while (d = io.readpartial(65_536)); io.write(d); end }
end
# ---- client ----
endpoint = Mnet::Endpoint.new
sess = endpoint.dial('127.0.0.1', 9000, proto: :kcp) # :mnet or :kcp
sess.write('hello')
puts sess.readpartial(1024)
Engine choice
proto |
Description |
|---|---|
:mnet |
pure-Ruby ARQ, no C dependency, simple and easy to debug |
:kcp |
C engine (via the kcp gem), ~8x faster, requires building a native extension |
Encryption (pre-shared key)
Per-packet AES-256-GCM; both ends must pre-share the same 32-byte key:
require 'securerandom'
key = SecureRandom.random_bytes(32)
# server registers the key (one or more)
server = Mnet::Server.new('0.0.0.0', 9000, keys: [key])
# client dials with the key
sess = endpoint.dial('127.0.0.1', 9000, proto: :kcp, key: key)
The session id is derived from the key (SHA256(key)[0,16]); with the wrong key the server fails to
decrypt and drops the packet, so the client cannot connect.
TLS (nested)
Mnet::Session implements an IO-compatible interface, so SSLSocket can wrap it directly
(SSLServer usage is compatible):
require 'openssl'
server = Mnet::Server.new('0.0.0.0', 9001)
ssl_server = OpenSSL::SSL::SSLServer.new(server, ssl_context)
ssl_server.start_immediately = false # do the handshake in worker threads, don't block accept
loop do
ssl_sock = ssl_server.accept
Thread.new do
ssl_sock.accept
# ...read/write ssl_sock...
end
end
Roaming
endpoint.hop # simulate a network change (in reality the client binds 0.0.0.0, and the OS
# re-picks the source address automatically, so no manual hop is needed)
Benchmarks
Environment: loopback (127.0.0.1), pipelined echo (concurrent writer/reader, counted by total bytes).
| Scheme | 1KB-message throughput |
|---|---|
| TCP (raw) | ~49 MB/s |
| TCP + SSL | ~22 MB/s |
mnet pure Ruby (proto: :mnet) |
~1.8 MB/s |
| mnet pure Ruby + key (AES-GCM) | ~1.6 MB/s |
mnet + KCP (proto: :kcp) |
~5.0 MB/s |
| mnet + KCP + key (AES-GCM) | ~4.1 MB/s |
Takeaways:
- The KCP engine is ~2.7x faster than pure Ruby (5.0 vs 1.8 MB/s)
- Encryption overhead is small:
key(per-packet AES-GCM) only costs ~15–18% - Plaintext KCP is ~1/10 of TCP; the bottleneck is the Ruby per-packet glue (pack/unpack + thread switching), not the ARQ algorithm
- These are synthetic loopback numbers; over a real network latency dominates and interactive (small-message) workloads feel identical
Reproduce: ruby -I lib -I ../kcp/lib test/bench_compare.rb [msgs] [msg_bytes]
License
MIT.