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::Session implements an IO-compatible interface (read / write / readpartial / wait_readable), so OpenSSL::SSL::SSLSocket can wrap it directly
  • Cross-platform: the pure-Ruby part is inherently portable; KCP is a native extension from the kcp gem

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.