lynn_tcp 2.0.0

Lightweight asynchronous TCP framework
Documentation

English | 简体中文

Lynn_tcp is a lightweight, high-performance asynchronous TCP framework built on Tokio.

It adopts a DDD (Domain-Driven Design) + Onion Architecture, separating core domain logic from infrastructure concerns for better maintainability and extensibility.


Keywords

  • Lightweight: Concise code that is easy to learn and use
  • Concurrent & Performance: Built on Tokio's excellent async runtime for high-concurrency multi-user connections
  • Low Latency: Read-write separation design for minimal latency
  • Security: Strong typing and memory safety guarantees of Rust
  • Production Ready: Prometheus metrics, connection limiting, rate limiting, and heartbeat management built-in

Tips: Lynn_tcp is designed for message forwarding and long-lived TCP game servers.

Quickly develop business scenarios with the framework — customize message parsing, encryption, routing, and more.


Architecture

Lynn_tcp v2.0 is organized into three clean layers following the Onion Architecture pattern:

┌─────────────────────────────────────┐
│         Interface Layer             │  ← Public API (no breaking changes)
│   (lynn_server / lynn_client / ...) │
├─────────────────────────────────────┤
│         Application Layer           │  ← Orchestration: server/client startup
│   (LynnServer / LynnClient)         │
├─────────────────────────────────────┤
│          Domain Layer               │  ← Pure business logic
│   (Router / Handler / Model)        │
├─────────────────────────────────────┤
│       Infrastructure Layer          │  ← Concrete implementations
│   (TCP / Metrics / Validation)      │
└─────────────────────────────────────┘

Quick Start

Dependencies

Add to your Cargo.toml:

Full features (recommended):

[dependencies]
lynn_tcp = "2"
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread"] }
tracing-subscriber = "0.3"

Server only:

[dependencies]
lynn_tcp = { version = "2", features = ["server"] }
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread"] }
tracing-subscriber = "0.3"

Client only:

[dependencies]
lynn_tcp = { version = "2", features = ["client"] }
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread"] }
tracing-subscriber = "0.3"

Minimal Server

use lynn_tcp::{lynn_server::*, lynn_tcp_dependents::*};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let _ = LynnServer::new()
        .await
        .add_router(1, ping_handler)
        .add_router(2, echo_handler)
        .add_router(3, broadcast_handler)
        .start()
        .await;

    Ok(())
}

pub async fn ping_handler() -> HandlerResult {
    println!("📡 Ping received");
    HandlerResult::new_without_send()
}

pub async fn echo_handler(input_buf_vo: InputBufVO) -> HandlerResult {
    let addr = input_buf_vo.get_input_addr().unwrap();
    println!("📨 Echo from: {}", addr);
    HandlerResult::new_without_send()
}

pub async fn broadcast_handler(clients_context: ClientsContext) -> HandlerResult {
    let addrs = clients_context.get_all_clients_addrs().await;
    HandlerResult::new_with_send(3, "hello everyone!".into(), addrs)
}

Server with Custom Config

use lynn_tcp::{lynn_server::*, lynn_tcp_dependents::*};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let config = LynnServerConfigBuilder::new()
        .with_addr("0.0.0.0:9876")?
        .with_server_max_connections(Some(&500))
        .with_server_max_taskpool_size(&256)
        .with_tcp_nodelay(&true)
        .with_tcp_keepalive_enabled(&true)
        .with_tcp_keepalive_time_secs(&120)
        .build();

    let _ = LynnServer::new_with_config(config)
        .await
        .add_router(1, my_handler)
        .start()
        .await;

    Ok(())
}

pub async fn my_handler() -> HandlerResult {
    HandlerResult::new_without_send()
}

Minimal Client

use lynn_tcp::{
    lynn_client::LynnClient,
    lynn_tcp_dependents::*,
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let mut client = LynnClient::new_with_addr("127.0.0.1:9177")
            .await
            .start()
            .await;

    // Send a message
    client.send_data(
        HandlerResult::new_with_send_to_server(1, "hello".into())
    ).await?;

    // Receive response
    if let Some(response) = client.get_receive_data().await {
        println!("Received: {:?}", response.get_all_bytes());
    }

    Ok(())
}

Examples

All examples are located in the examples/ directory. Run them with:

Example Command Description
basic_server cargo run --example basic_server Default server with 3 handler signatures
custom_config_server cargo run --example custom_config_server Server with custom Builder configuration
custom_protocol cargo run --example custom_protocol Custom message header/tail marks
echo_server_client cargo run --example echo_server_client Full request-response cycle (Client ↔ Server)
multi_route_service cargo run --example multi_route_service Multi-route dispatch and verification
custom_protocol_full cargo run --example custom_protocol_full Custom protocol with client support
state_example cargo run --example state_example Global state injection (AppState<T>)
tls_example cargo run --example tls_example --features tls TLS 1.3 encrypted server ↔ client
metrics_example cargo run --example metrics_example --features metrics Prometheus metrics integration

The echo_server_client and multi_route_service examples are the best starting points for understanding Client ↔ Server communication.


Features

Feature Description Default
server TCP server with multi-route async handlers, heartbeat, connection management
client TCP client for sending/receiving messages, automatic reconnection
metrics Prometheus integration (17 production metrics, HTTP /metrics endpoint) ✅ (via server)
tls TLS 1.3 transport encryption for server & client (rustls/ring) ❌ opt-in
seaorm Built-in SeaORM support: with_db(...) + DbConn state handle ❌ opt-in

Note: metrics is automatically enabled when server is selected. To use it independently: features = ["metrics"]. tls and seaorm are disabled by default and must be enabled explicitly.


Available Configuration (LynnServerConfigBuilder)

Method Description Default
with_addr() Server listen address 0.0.0.0:9177
with_server_max_connections() Max concurrent connections 100_000
with_server_max_taskpool_size() Async task pool size (throughput) 512
with_server_single_processs_permit() Max concurrent processing tasks 1024
with_server_check_heart_interval() Heartbeat check interval (s) 60
with_server_check_heart_timeout_time() Heartbeat timeout (s) 180
with_tcp_nodelay() TCP_NODELAY (disable Nagle) true
with_tcp_keepalive_enabled() TCP keep-alive true
with_tcp_keepalive_time_secs() Keep-alive interval (s) 120
with_message_header_mark() Custom message header mark 0x23D9
with_message_tail_mark() Custom message tail mark 0x1E27
with_max_connections_per_ip() Max connections per IP 100
with_connection_rate_limit() Connection rate (per second) 0 (disabled)
with_read_timeout_secs() Read timeout (s, 0 = disabled) 0
with_write_timeout_secs() Write timeout (s, 0 = disabled) 0
with_recv_buffer_size() Receive buffer (bytes) 65535
with_send_buffer_size() Send buffer (bytes) 65535
with_tls() (feature tls) Enable TLS 1.3 with a TlsServerConfig off
with_tls_cert_paths() (feature tls) Enable TLS 1.3 from PEM cert/key paths off

Available Configuration (LynnClientConfigBuilder)

Method Description Default
with_server_addr() Server address to connect to
with_server_single_channel_size() Per-connection channel capacity 64
with_server_check_heart_interval() Heartbeat send interval (s) 5
with_message_header_mark() / with_message_tail_mark() Custom message marks 9177 / 7719
with_reconnect_max_attempts() Connect attempts per session (initial connect & each reconnect) 3
with_reconnect_interval_secs() Delay between attempts (s) 1
with_connect_timeout_secs() Per-attempt connect/TLS-handshake timeout (s) 3
with_tls() (feature tls) Enable TLS 1.3 with a TlsClientConfig (CA, SNI, mTLS) off

TLS 1.3 Encryption (feature tls)

TLS is disabled by default. Enable the tls feature and attach certificates — TLS 1.3 only (rustls + ring):

[dependencies]
lynn_tcp = { version = "2", features = ["tls"] }
use lynn_tcp::{lynn_server::*, lynn_tcp_dependents::*, lynn_tls::*};

// Server: opt in with a certificate chain + private key (PEM files).
let config = LynnServerConfigBuilder::new()
    .with_addr("0.0.0.0:9177")?
    .with_tls_cert_paths("cert.pem", "key.pem") // or: .with_tls(TlsServerConfig::new(...))
    .build();
// Mutual TLS: TlsServerConfig::new("cert.pem", "key.pem").with_client_ca("client_ca.pem")
use lynn_tcp::{lynn_client::*, lynn_tls::*};

// Client: verify the server against a CA trust anchor.
let config = LynnClientConfigBuilder::new()
    .with_server_addr("127.0.0.1:9177")?
    .with_tls(
        TlsClientConfigBuilder::new()
            .with_ca_cert_path("ca.pem")
            .with_server_name("localhost") // optional SNI override
            .build(),
    )?
    .build();

Miss-configured TLS fails fast (missing files, invalid pairs) and handshake failures simply drop the offending connection. Run the runnable demo: cargo run --example tls_example --features tls.


Global State (Dependency Injection)

Register any Send + Sync + 'static value once and extract it in handler parameters through AppState<T> (axum-style). One value per type; several types can coexist. SeaORM users can enable the seaorm feature and use with_db(...) + DbConn:

use lynn_tcp::{lynn_server::*, lynn_tcp_dependents::*, lynn_state::AppState};

#[tokio::main]
async fn main() {
    let _ = LynnServer::new()
        .await
        .with_state(UserRepo::new()) // or .with_db(db) with feature "seaorm"
        .add_router(1, find_user)
        .start()
        .await;
}

// `repo` derefs to &UserRepo — no global statics, no manual Arc plumbing.
async fn find_user(repo: AppState<UserRepo>, input: InputBufVO) -> HandlerResult {
    let name = repo.find_user(42);
    HandlerResult::new_with_send(1, name.into(), vec![input.get_input_addr().unwrap()])
}

State is resolved per request, so with_state may be called before or after add_router — just register before start(). Run the runnable demo: cargo run --example state_example.


Client Automatic Reconnection

The client supervises its own connection: when it drops, it reconnects automatically — 3 attempts by default, 1s apart (configurable). User-facing channels survive reconnections, stale queued frames are discarded, and the live state is one call away:

let mut client = LynnClient::new_with_addr("127.0.0.1:9177").await.start().await;
if !client.is_connected() {
    eprintln!("server unreachable");
}

Road Map

✅ Core Features (v1.0.0+)

  • ✅ TCP Server with multi-route async handler dispatch
  • ✅ TCP Client with message send/receive
  • ✅ Custom message header/tail marks
  • ✅ Automatic client heartbeat & cleanup
  • ✅ Asynchronous task routing service
  • ✅ Prometheus + Grafana monitoring (v1.2.5)
  • ✅ DDD + Onion Architecture refactoring (v2.0.0)
  • ✅ 7 runnable examples covering all scenarios
  • ✅ TLS 1.3 transport encryption for server & client (v2.0.0)
  • ✅ Client automatic reconnection with configurable attempts (v2.0.0)
  • ✅ Global state injection (AppState<T>) + built-in SeaORM support (v2.0.0)

🔜 Planned

Feature Target Version Status
Middleware support v2.2.0 📝 Design
Scheduled tasks TBD 💡 Idea

Flow Chart

v2.x — DDD + Onion Architecture

FlowChart v2

FlowChart


Release Notes

See docs/version.md for full version history, and per-version changelogs under docs/update_logs/.


Benchmarks

Starting with v2.0.0-rc.3, Lynn_tcp ships a standardized, reusable benchmark harness (benches/benchmark.rs + the standalone bench_echo_server binary). The server runs as an independent process (like a real deployment), every concurrency level starts from a fresh server process, and each cell measures a fixed window after a discarded warmup. The pre-2.0 tables were produced by an unrecoverable one-off script and have been retired.

Traffic models

  • Model 1 — request/response (ping-pong): each client keeps exactly one request in flight and measures every round-trip; reports throughput and RTT percentiles.
  • Model 2 — concurrent send/receive (pipelined): each client runs one sending and one receiving task with a bounded in-flight window (8); reports sustained throughput.

Standard matrix: models 1+2 × client concurrency 64 / 256 / 1024 / 4096 (logarithmic 4× steps, covering the latency-bound and saturation regimes), echo payload 128 B, 3 s warmup + 10 s measured per cell.

Reproduce

cargo bench --bench benchmark -- --json results.json
# customize: --model all|1|2  --clients 64,256  --duration 15  --warmup 3  --payload 128

Standard run for a new version / machine (maintainers)

git pull && cargo bench --bench benchmark -- --json docs/benchmark/v<version>-<machine>.json

Before running, make sure the file-descriptor limit allows the highest concurrency level (ulimit -n 65535; the harness also auto-raises it up to the hard limit). Archive the JSON under docs/benchmark/ named v<version>-<machine>.json, then update the results table in both READMEs in the same commit (see AGENTS.md §10.3).

raw JSON

Model Clients Payload (B) Throughput (resp/s) Avg RTT (ms) p50 (ms) p95 (ms) p99 (ms)
1 (ping-pong) 64 128 135,473 0.472 0.439 0.791 1.046
1 (ping-pong) 256 128 142,735 1.793 1.634 3.295 4.435
1 (ping-pong) 1024 128 147,568 6.938 5.827 15.200 22.410
1 (ping-pong) 4096 128 138,073 29.640 28.416 35.220 64.218
2 (pipelined) 64 128 151,644
2 (pipelined) 256 128 191,607
2 (pipelined) 1024 128 143,569
2 (pipelined) 4096 128 31,956

📊 Numbers are loopback, single-machine measurements where client load generation shares the CPU with the server — treat them as relative comparisons between versions, not absolute capacity. Model-1 RTT grows with concurrency as queues form (classic queueing behavior). Model-2 peaks near 256 clients on this machine; its 4096-client cell was degraded by connection-phase retries (see the requests ≫ responses gap in the JSON), so read that point as a lower bound rather than steady state.

raw JSON

Model Clients Payload (B) Throughput (resp/s) Avg RTT (ms) p50 (ms) p95 (ms) p99 (ms)
1 (ping-pong) 64 128 128,206 0.499 0.232 0.988 5.099
1 (ping-pong) 256 128 140,197 1.826 0.835 3.504 35.898
1 (ping-pong) 1024 128 140,187 7.303 2.602 45.081 55.153
1 (ping-pong) 4096 128 130,003 31.445 6.305 111.846 167.948
2 (pipelined) 64 128 35,277
2 (pipelined) 256 128 142,922
2 (pipelined) 1024 128 360,433
2 (pipelined) 4096 128 7,125

📊 Notable: Model-1 throughput is flat from 64 up to 4096 clients on just two cores (128k–140k), though the RTT tail grows sharply once queues form. Model-2 bursts to 360k @1024, but its 4096-client cell collapsed (7,125 resp/s, connection-phase retries) — on a 2 GB host that level is bounded by memory (kernel socket buffers + connection state), so read it as a host resource limit rather than a framework regression.

The v1.x numbers were produced by an unrecoverable one-off script on Debian 12 (4-core / 4 GB server); the v2.0 numbers come from the standardized harness above on Apple M1 Pro (8 logical cores / 16 GB). Hardware, OS and methodology all differ — read the tables as trends, not absolutes.

Model 1 — request/response (resp/s)

Clients v1.1.x v1.2.x v2.0.0-rc.3
256 182,879 64,630 142,735
1024 232,861 163,307 147,568
4096 160,318 124,645 138,073

Model 2 — concurrent send/receive (resp/s)

Clients v1.1.x v1.2.x v2.0.0-rc.3
256 80,499 492,889 191,607
1024 23,143 158,056 143,569
4096 13,557 52,163 31,956

Takeaways

  • Model-1 stays in the same order of magnitude as v1.x — no recognizable regression — and again degrades least at 4096 clients (peak→4096: −6% versus −36% / −32% for v1.x).
  • Model-2 holds 143k–192k through 1024 clients, where v1.1.x had already collapsed to 23k (6.2×). Its 4096-client point in the archived run was degraded by connection-phase retries (requests ≫ responses in the JSON), so read it as a lower bound; the v1.x numbers at that level came from unbounded bursts and are not directly comparable.
  • Model-2 semantics differ (v1.x appears to burst without in-flight limits; v2.0 uses a fixed 8-deep pipeline), so treat that table as directional.
  • v2.0 additionally reports RTT percentiles, which v1.x never measured.

FAQ

Q: Is v2.0.0 backward compatible with v1.x?

A: Yes. All public API signatures remain unchanged. You only need to update the version in Cargo.toml: lynn_tcp = "2".

Q: Do I need to change my code after upgrading?

A: No. The refactoring was purely structural — no behavior was modified. Your existing code will compile and run as before.

Q: Why jump from v1.2.x to v2.0.0?

A: The DDD + Onion Architecture restructuring represents a significant architectural improvement. The major version bump reflects this depth of change, even though the public API is fully compatible.

Q: How do I run the examples?

A: See the Examples section above. Each example can be run with cargo run --example <name>.

Q: How do I enable metrics?

A: Metrics are automatically enabled with the server feature. To run the metrics example: cargo run --example metrics_example --features metrics. See METRICS.md for detailed documentation.

Q: How do I enable TLS?

A: Enable the tls feature and call .with_tls_cert_paths("cert.pem", "key.pem") on the server builder and .with_tls(...) on the client builder — TLS 1.3 only, off by default. See the TLS 1.3 Encryption section.

Q: How do I share a database handle with handlers?

A: Call .with_state(db) (or .with_db(db) with the seaorm feature) on LynnServer and take an AppState<T> parameter in the handler. See Global State.

Q: Does the client reconnect automatically?

A: Yes. After a disconnect it retries 3 times (1s apart) by default, configurable via with_reconnect_max_attempts / with_reconnect_interval_secs. Check the live state with client.is_connected().


License

This project is dual-licensed under either of:

at your option.

Contribution

  • Please read AGENTS.md first — it is the project's development constitution, defining commit conventions, CI standards, formatting/lint rules, testing requirements (coverage ≥ 80% via cargo-llvm-cov), and the release workflow. All pull requests and code reviews are checked against it.
  • Before submitting, run locally: cargo fmt --all, cargo clippy --all-targets -- -D warnings, cargo test.
  • Keep README.md and README_ZH.md in sync when changing documentation.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Lynn_tcp by you shall be licensed as MIT OR Apache-2.0, without any additional terms or conditions.