gbnet-hs-0.2.0.0: README.md
<div align="center">
<h1>gbnet-hs</h1>
<p><strong>Transport-Level Networking for Haskell</strong></p>
<p>Zero-copy Storable serialization. Reliable UDP transport. Effect-abstracted design for pure testing.</p>
<p><a href="#quick-start">Quick Start</a> · <a href="#networking">Networking</a> · <a href="#serialization">Serialization</a> · <a href="#testing">Testing</a> · <a href="#architecture">Architecture</a></p>
<p>
[](https://github.com/gondola-bros-entertainment/gbnet-hs/actions/workflows/ci.yml)
[](https://hackage.haskell.org/package/gbnet-hs)


</p>
</div>
---
## What is gbnet-hs?
A transport-level networking library providing:
- **Zero-copy serialization** — Storable-based, C-level speed (14ns per type)
- **Reliable UDP** — Connection-oriented with ACKs, retransmits, and ordering
- **AEAD encryption** — Optional ChaCha20-Poly1305 with anti-replay nonce tracking
- **Unified Peer API** — Same code for client, server, or P2P mesh
- **Effect abstraction** — `MonadNetwork` typeclass enables pure deterministic testing
- **Congestion control** — Dual-layer: binary mode + TCP New Reno window, with application-level backpressure
- **Zero-poll receive** — Dedicated receive thread via GHC IO manager (epoll/kqueue), STM TQueue delivery
- **IPv4 and IPv6** — Automatic address family detection
- **Connection migration** — Seamless IP address change handling
---
## Quick Start
Add to your `.cabal` file:
```cabal
build-depends:
gbnet-hs
```
### Simple Game Loop
```haskell
import GBNet
import Control.Monad.IO.Class (liftIO)
main :: IO ()
main = do
-- Create peer (binds UDP socket)
let addr = anyAddr 7777
now <- getMonoTimeIO
Right (peer, sock) <- newPeer addr defaultNetworkConfig now
-- Wrap socket in NetState (starts dedicated receive thread)
netState <- newNetState sock addr
-- Run game loop inside NetT IO
evalNetT (gameLoop peer) netState
gameLoop :: NetPeer -> NetT IO ()
gameLoop peer = do
-- Single call: receive, process, broadcast, send
let outgoing = [(ChannelId 0, encodeMyState myState)]
(events, peer') <- peerTick outgoing peer
-- Handle events
liftIO $ mapM_ handleEvent events
gameLoop peer'
handleEvent :: PeerEvent -> IO ()
handleEvent = \case
PeerConnected pid dir -> putStrLn $ "Connected: " ++ show pid
PeerDisconnected pid _ -> putStrLn $ "Disconnected: " ++ show pid
PeerMessage pid ch msg -> handleMessage pid ch msg
PeerMigrated old new -> putStrLn "Peer address changed"
```
### Connecting to a Remote Peer
```haskell
-- Initiate connection (handshake happens automatically)
let peer' = peerConnect (peerIdFromAddr remoteAddr) now peer
-- The PeerConnected event fires when handshake completes
```
---
## Networking
### The `peerTick` Function
The recommended API for game loops — handles receive, process, and send in one call:
```haskell
peerTick
:: MonadNetwork m
=> [(ChannelId, ByteString)] -- Messages to broadcast (channel, data)
-> NetPeer -- Current peer state
-> m ([PeerEvent], NetPeer) -- Events and updated state
```
### Peer Events
```haskell
data PeerEvent
= PeerConnected !PeerId !ConnectionDirection -- Inbound or Outbound
| PeerDisconnected !PeerId !DisconnectReason
| PeerMessage !PeerId !ChannelId !ByteString -- channel, data
| PeerMigrated !PeerId !PeerId -- old address, new address
```
### Channel Reliability Modes
```haskell
import GBNet
-- Unreliable: fire-and-forget (position updates)
let unreliable = defaultChannelConfig { ccDeliveryMode = Unreliable }
-- Reliable ordered: guaranteed delivery, in-order (chat, RPC)
let reliable = defaultChannelConfig { ccDeliveryMode = ReliableOrdered }
-- Reliable sequenced: latest-only, drops stale (state sync)
let sequenced = defaultChannelConfig { ccDeliveryMode = ReliableSequenced }
```
### Configuration
```haskell
let config = defaultNetworkConfig
{ ncMaxClients = 32
, ncConnectionTimeoutMs = 10000.0
, ncKeepaliveIntervalMs = 1000.0
, ncMtu = 1200
, ncEnableConnectionMigration = True
, ncChannelConfigs = [unreliableChannel, reliableChannel]
, ncEncryptionKey = Just (EncryptionKey myKey) -- optional AEAD encryption
}
```
---
## Serialization
### Zero-Copy Storable Serialization
```haskell
{-# LANGUAGE TemplateHaskell #-}
import GBNet
data PlayerState = PlayerState
{ psX :: !Float
, psY :: !Float
, psHealth :: !Word8
} deriving (Eq, Show)
deriveStorable ''PlayerState
-- Serialize (14ns, zero-copy)
let bytes = serialize playerState
-- Deserialize
let Right player = deserialize bytes :: Either String PlayerState
```
### Nested Types Just Work
```haskell
data Vec3 = Vec3 !Float !Float !Float
deriveStorable ''Vec3
data Transform = Transform !Vec3 !Float -- position + rotation
deriveStorable ''Transform
-- Nested types compose via Storable
let bytes = serialize (Transform pos angle) -- still 14ns
```
### Why Storable?
- **C-level speed** — 14ns serialization via direct memory layout
- **Standard Haskell** — uses base `Storable` typeclass
- **Composable** — nested types work automatically
- **Pure API** — `serialize`/`deserialize` are pure functions
---
## Testing
### Pure Deterministic Testing with TestNet
The `MonadNetwork` typeclass allows swapping real sockets for a pure test implementation:
```haskell
import GBNet
import GBNet.TestNet
-- Run peer logic purely — no actual network IO
testHandshake :: ((), TestNetState)
testHandshake = runTestNet action (initialTestNetState myAddr)
where
action = do
-- Simulate sending
netSend remoteAddr someData
-- Advance simulated time (absolute MonoTime in nanoseconds)
advanceTime (100 * 1000000) -- 100ms
-- Check what would be received
result <- netRecv
pure ()
```
### Multi-Peer World Simulation
```haskell
import GBNet.TestNet
-- Create a world with multiple peers
let world0 = newTestWorld
-- Run actions for each peer
let (result1, world1) = runPeerInWorld addr1 action1 world0
let (result2, world2) = runPeerInWorld addr2 action2 world1
-- Advance to absolute time and deliver ready packets
let world3 = worldAdvanceTime (100 * 1000000) world2 -- 100ms
```
### Simulating Network Conditions
```haskell
-- Add 50ms latency
simulateLatency 50
-- 10% packet loss
simulateLoss 0.1
-- Packet duplication and out-of-order delivery
let testCfg = defaultTestNetConfig
{ tncDuplicateChance = 0.05 -- 5% chance of duplicating packets
, tncOutOfOrderChance = 0.1 -- 10% chance of reordering
}
```
---
## Encryption
Optional ChaCha20-Poly1305 AEAD encryption for post-handshake packets:
```haskell
import GBNet
-- Pre-shared key (32 bytes) — both sides must use the same key
let key = EncryptionKey mySharedKey
let config = defaultNetworkConfig { ncEncryptionKey = Just key }
-- Handshake packets remain plaintext; all data packets are encrypted
-- Anti-replay: monotonic nonce counter rejects duplicate/old packets
```
Wire overhead: 24 bytes per encrypted packet (8-byte nonce + 16-byte auth tag).
## IPv6
IPv6 works out of the box — address family is detected automatically:
```haskell
-- IPv4 (existing)
let addr4 = anyAddr 7777
-- IPv6
let addr6 = anyAddr6 7777 -- bind to [::]:7777
let local = localhost6 7777 -- bind to [::1]:7777
let custom = ipv6 (0,0,0,1) 7777 -- specific address
```
---
## Architecture
```
┌─────────────────────────────────────────┐
│ User Application │
├─────────────────────────────────────────┤
│ GBNet (top-level re-exports) │
│ import GBNet -- gets everything │
├─────────────────────────────────────────┤
│ GBNet.Peer │
│ peerTick, peerConnect, PeerEvent │
├─────────────────────────────────────────┤
│ GBNet.Net (NetT transformer) │
│ Carries socket state for IO │
├──────────────┬──────────────────────────┤
│ NetT IO │ TestNet │
│ TQueue + │ (pure, deterministic) │
│ recv thread │ │
├──────────────┴──────────────────────────┤
│ GBNet.Class │
│ MonadTime, MonadNetwork typeclasses │
└─────────────────────────────────────────┘
```
### Module Overview
| Module | Purpose |
|--------|---------|
| `GBNet` | Top-level facade — import this for convenience |
| `GBNet.Class` | `MonadTime`, `MonadNetwork` typeclasses |
| `GBNet.Net` | `NetT` monad transformer with receive thread + TQueue |
| `GBNet.Net.IO` | `initNetState` — create real UDP socket and start receive thread |
| `GBNet.Peer` | `NetPeer`, `peerTick`, connection management |
| `GBNet.Crypto` | ChaCha20-Poly1305 AEAD encryption and decryption |
| `GBNet.Security` | CRC32C integrity, rate limiting, connect tokens |
| `GBNet.Congestion` | Dual-layer congestion control and backpressure |
| `GBNet.TestNet` | Pure test network, `TestWorld` for multi-peer |
| `GBNet.Serialize.TH` | `deriveStorable` TH for zero-copy serialization |
| `GBNet.Serialize` | `serialize`/`deserialize` pure functions |
### Explicit Imports (for larger codebases)
```haskell
-- Instead of `import GBNet`, be explicit:
import GBNet.Class (MonadNetwork, MonadTime, MonoTime(..))
import GBNet.Types (ChannelId(..), SequenceNum(..), MessageId(..))
import GBNet.Net (NetT, runNetT, evalNetT)
import GBNet.Net.IO (initNetState)
import GBNet.Peer (NetPeer, peerTick, PeerEvent(..))
import GBNet.Config (NetworkConfig(..), defaultNetworkConfig)
import GBNet.Crypto (EncryptionKey(..), NonceCounter(..)) -- optional encryption
```
---
## Replication Helpers
### Delta Compression
Only send changed fields:
```haskell
import GBNet.Replication.Delta
instance NetworkDelta PlayerState where
type Delta PlayerState = PlayerDelta
diff new old = PlayerDelta { ... }
apply state delta = state { ... }
```
### Interest Management
Filter by area-of-interest:
```haskell
import GBNet.Replication.Interest
let interest = newRadiusInterest 100.0
if relevant interest entityPos observerPos
then sendEntity entity
else skip
```
### Priority Accumulator
Fair bandwidth allocation:
```haskell
import GBNet.Replication.Priority
let acc = register npcId 2.0
$ register playerId 10.0
newPriorityAccumulator
let (selected, acc') = drainTop 1200 entitySize acc
```
### Snapshot Interpolation
Smooth client-side rendering:
```haskell
import GBNet.Replication.Interpolation
let buffer' = pushSnapshot serverTime state buffer
case sampleSnapshot renderTime buffer' of
Nothing -> waitForMoreSnapshots
Just interpolated -> render interpolated
```
---
## Congestion Control
gbnet-hs uses a dual-layer congestion control strategy:
### Binary Mode
A send-rate controller that tracks Good/Bad network conditions:
- **Good mode** — additive increase (AIMD): ramps send rate up to 4x base rate
- **Bad mode** — multiplicative decrease: halves current send rate on loss/high RTT
- Adaptive recovery timer with quick re-entry detection (doubles on rapid Good→Bad transitions)
### Window-Based (TCP New Reno)
A cwnd-based controller layered alongside binary mode:
- **Slow Start** — exponential growth until ssthresh
- **Congestion Avoidance** — additive increase per RTT
- **Recovery** — halves cwnd on packet loss (triggered by fast retransmit)
- **Slow Start Restart** — resets stale cwnd after idle periods (RFC 2861)
### Backpressure API
Applications can query congestion pressure and adapt:
```haskell
case peerStats peerId peer of
Nothing -> pure () -- Peer not connected
Just stats -> case nsCongestionLevel stats of
CongestionNone -> sendFreely
CongestionElevated -> reduceNonEssential
CongestionHigh -> dropLowPriority
CongestionCritical -> onlySendEssential
```
---
## Build & Test
Requires [GHCup](https://www.haskell.org/ghcup/) with GHC >= 9.6.
```bash
cabal build # Build library
cabal test # Run all tests
cabal build --ghc-options="-Werror" # Warnings as errors
cabal haddock # Generate docs
```
---
## Performance
Optimized for game networking:
- **Zero-allocation serialization** — Storable-based `poke`/`peek`, 14ns for user types (~70M ops/sec)
- **Zero-allocation packet headers** — direct memory writes, 17ns serialize
- **Nested types same speed** — Storable composition has no overhead
- **Strict fields** with bang patterns throughout
- **GHC flags**: `-O2 -fspecialise-aggressively -fexpose-all-unfoldings`
- **INLINE pragmas** on hot paths
- **Hardware-accelerated CRC32C** via SSE4.2/ARMv8 CRC
- **Zero-poll receive** — dedicated thread blocks on epoll/kqueue, delivers via STM TQueue
### Benchmarks
```
storable/vec3/serialize 18.98 ns (52M ops/sec) -- user types
storable/transform/serialize 20.80 ns (48M ops/sec) -- nested types
packetheader/serialize 16.49 ns (60M ops/sec)
packetheader/deserialize 15.95 ns (62M ops/sec)
crypto/encrypt/64B 1.9 us (526K ops/sec) -- ChaCha20-Poly1305
crypto/decrypt/1KB 4.2 us (238K ops/sec)
```
Run with `cabal bench --enable-benchmarks`.
---
## Features
### Core Transport
- [x] Zero-copy Storable serialization (sub-20ns roundtrips)
- [x] Nested type composition via Storable typeclass
- [x] Template Haskell `deriveStorable` for automatic instances
- [x] Type-safe newtypes (`ChannelId`, `SequenceNum`, `MonoTime`, `MessageId`)
- [x] Reliable/unreliable/sequenced delivery modes
- [x] RTT estimation and adaptive retransmit
- [x] Large message fragmentation
- [x] Connection migration
- [x] Hardware-accelerated CRC32C validation (SSE4.2/ARMv8/software fallback)
- [x] Self-cleaning rate limiter
- [x] ChaCha20-Poly1305 AEAD encryption with anti-replay
- [x] IPv4 and IPv6 support
### Congestion Control
- [x] Binary mode (Good/Bad with AIMD recovery)
- [x] TCP New Reno window-based control (slow start, avoidance, recovery)
- [x] Slow Start Restart for idle connections (RFC 2861)
- [x] Application-level backpressure via `CongestionLevel`
- [x] CWND loss signal from fast retransmit
- [x] Adaptive recovery timer with quick re-entry detection
### Effect Abstraction
- [x] `MonadNetwork` typeclass
- [x] `NetT` monad transformer with dedicated receive thread + STM TQueue
- [x] `TestNet` pure deterministic network
- [x] `TestWorld` multi-peer simulation
- [x] TestNet packet duplication and out-of-order simulation
### Replication Helpers
- [x] Delta compression
- [x] Interest management
- [x] Priority accumulator
- [x] Snapshot interpolation
---
## Contributing
```bash
cabal test && cabal build --ghc-options="-Werror"
```
---
<p align="center">
<sub>MIT License · <a href="https://github.com/gondola-bros-entertainment">Gondola Bros Entertainment</a></sub>
</p>