English ✓ | 中文
Language ✓: this is the default (English) readme ✓ — the Chinese version is
readme.zh.md✓. Everything underdocs/is Chinese only ✗ — if you need another language, please translate it with an AI ✓ (hand the document to any LLM; ask it to keep macro names, commands, code blocks and the✓ / ✗markers verbatim ✓).
ffrdp is a fast, reliable transport protocol built on UDP + ARQ + FEC. Its goals are low latency and stable bandwidth on lossy networks.
Single-file implementation (ffrdp.c) plus one test program (test.c); no third-party dependencies other
than pthread, no kernel changes, and no change to UDP semantics.
| Feature | Notes |
|---|---|
| Full ARQ in userspace | Acknowledgement, timeouts, retransmission, windowing and flow control all live in userspace, so the strategy can follow the link's shape instead of being bound to a fixed kernel policy |
| Selective repeat + no delayed ACK | UNA + a MACK bitmap states precisely which frame is missing (rather than "everything past sequence N is lost"); data is ACKed as soon as it arrives, instead of wasting a round trip on delayed ACKs |
| Fast retransmit (with a waiting discipline) | Most holes the peer reports are actually reordering, not loss ⇒ how long to wait is decided by peer evidence (since v1.1.0), not by an unconditional immediate retransmit |
| Forward error correction (FEC) | Implemented with XOR: one FEC frame is attached every N frames, with N carried in the frame type (application-selectable, off by default) |
| Latency control for "bufferbloated" links | The core of v1.1.0: treat the number of frames in flight as the real brake, and determine what it should be with a single reproducible experiment |
| Peer-evidence driven | The RTO floor and the fast-retransmit wait are both driven by "the peer keeps reporting the same thing" — the only signal on the link that this end's own counters cannot pollute |
| Diagnostics friendly | ffrdp_dump() prints the three latency components, in-flight amount, retransmissions, wasted fraction, etc.; a version & build line self-identifies the binary so you never "run the wrong one" (since v1.1.0 it is merged with version into one line ✓; only v1.0.0 prints a standalone version line ✓) |
| Handshake & connection management (v2.0.0) | A three-way handshake to establish (SYN / SYN-ACK / ACK) and a four-way FIN teardown; the handshake and FIN use a dedicated frame type (35) ⇒ they do not consume data-ARQ sequence space ✓ |
| Multi-client server (v2.0.0) | The server keeps a connection table keyed by peer address (maxconn, default 16 ✓), one handle per connection; anything not yet handshaken is dropped ✓ |
| Encryption left to the application (v2.0.0) | The protocol hands the payload as-is to the ENCRYPT / DECRYPT callbacks (in place, same length ✓) ⇒ algorithms and keys live entirely on the application side ✓, and the library no longer needs openssl at all ✓ |
| Idle is not disconnected (v2.0.0) | If the peer has been silent for half the interval, a keep-alive QUERY is sent to confirm liveness ✓; only when the peer is really mute is it declared dead within idle_ms (default 5 s) and DISCONN reported ✓ |
Almost everything added over v1.0.0 is an optimisation aimed at bufferbloated links (concurrency control and
latency probing); the protocol itself did not get more complicated (the frame format gained exactly one
optional byte, see below). Size reference: v1.0.0's ffrdp.c was 670 lines ✓, v1.1.0 1316 lines ✓;
from v1.2.0 the transport layer is a separate file; from v2.0.0 the connection model and interface were
reworked — today ffrdp.c is 2240 lines ✓, lowlevel-udp.c 550 lines ✓, socket.c 85 lines ✓,
utils.c 156 lines ✓, test.c 667 lines ✓ (3698 lines in total ✓, all standard C + pthread ✓).
| Version | Branch | Frame format | Contents |
|---|---|---|---|
| v1.0.0 | v1.0.x |
4-byte frame header; 8-byte ACK | Basic ARQ (selective repeat / fast retransmit / UNA+MACK / rwnd) + FEC |
| v1.1.0 | v1.1.x |
unchanged ✓ | Logical protocol optimisations: wasted-retransmission ACK counter (the only new optional byte), peer-evidence driven RTO floor, fast-retransmit waiting discipline, in-flight gate + latency probing |
| v1.2.0 | v1.2.x |
unchanged ✓ | Transport-layer rework (lowlevel interface + worker-thread driven) and the protocol fixes that followed (wakeup criterion, ACK in the same round, window scaling); parameters and spec in docs/ffrdp-v1.md §3.1 |
| v2.0.0 (current) | master |
v1 frame format unchanged ✓ + new handshake frame type 35 (never enters data ARQ ✓) | Interface and connection-model rework: handle-based send/recv + event callbacks, three-way handshake and four-way teardown, multi-client accept, keepalive, encryption callbacks — see docs/ffrdp-v2.md |
How to tell which version you are running ✓: look at the version & build : … compiled <date> <time> line in
the dump (since v1.1.0 version and build are merged into one line ✓; only v1.0.0 still prints a
standalone version : … line ✓), then match it against the commit it was compiled from ✓.
(FFRDP_VERSION has existed since v1.0.0 ✓ — it is present in the ffrdp.h of all three v1 branches and of v2 ✓;
only binaries built earlier may lack it ✗, and then the build line plus the commit is all you have ✓.
The v2 dump also adds a per-connection layer ✓ — ffrdp_dump(ctxt, conn, …): conn < 0 prints the overview
(one block per connection ✓), conn >= 0 prints only that one ✓; each block's header states the handle,
the peer address and the handshake state ✓; a slot that never communicated occupies a single line ✓.)
Frame-format compatibility conclusion: v1.1.0 introduced no new frame type and changed neither the width
nor the meaning of any existing field ✓; the only change is one optional byte at the tail of the ACK frame
(dups, sent only when there is something to report ✓). v1.2.0 likewise changed neither the frame format nor
ACK semantics ✓, but three ffrdp.h API signatures changed ✗ (see §3.1.6 ✓): ffrdp_init() now takes an
address string, send / recv each gained a timeout_ms, and ffrdp_update() was removed.
v2.0.0's data plane is still isomorphic to v1 ✓ (frame header, ACK and FEC are byte-for-byte unchanged ✓);
all that is new is the handshake frame: a new frame type 35 ✓, while v1's parser only accepts ≤34 ⇒ an old
implementation simply drops it silently ✓ (details in docs/ffrdp-v2.md). ⇒ Interoperability
is therefore decided by the handshake switch ✓: hs:0 (the old path) works in both directions ✓; the default
hs:1 does not interoperate with v1 ✓ (deliberately: v2's connection model is not the same thing as v1's ✓).
⇒ There is an executable test for this ✓ (no more "reading the code and thinking they look the same" ✗):
V1_REF=<a v1.2.x binary> ./test.sh <a v2 binary> x (run locally, no ssh ✓) —
① v1 server ← v2 client (both sides hs:0) must work ✓, ② v2 server ← v1 client (both sides hs:0) must
work ✓, ③ v2 server with defaults (hs:1) ← v1 client must not work ✓. Step 5 of tools/check.sh is exactly
this ✓.
You need gcc (or a cross toolchain) + pthread. The build script is build.sh:
TOOLCHAIN_NAME=gcc ./build.sh # native build
TOOLCHAIN_NAME=arm-linux CROSS_COMPILE=arm-linux- ./build.sh # cross build
./build.sh clean # remove artefactsArtefacts (BUILD_PACKAGE_OUTDIR overrides the location, default ./out):
out/$TOOLCHAIN_NAME/lib/libffrdp.a static library
out/$TOOLCHAIN_NAME/include/ffrdp.h header
out/$TOOLCHAIN_NAME/lib/pkgconfig/libffrdp.pc pkg-config description
./test_ffrdp test program (left in the source directory)
build.sh compiles with -Wall -Werror -Wextra -Wunused-macros (warnings are errors: extra arguments,
implicit fall-through and unused macros are all failures ✓), and by default passes the two feature macros of the
dev / criteria profile: -DFFRDP_DIAG=1 (keep full diagnostics — ffrdp_dump and the counters only it uses ✓)
and -DFFRDP_FEC_ENABLE=1 (full FEC ✓); a release build passes neither macro ✗ ⇒ the dump subsystem, the
diagnostic counters and the FEC buffers are all compiled out ✓. Linking adds -lpthread (Windows/MinGW targets
additionally add -lws2_32). -DLINUX is added automatically by build.sh on non-mingw targets ✓ — without it
the Linux implementations of socket.c / utils.c / lowlevel-udp.c are hidden behind #ifdef LINUX ⇒ you get
a pile of empty functions ✓.
The minimal manual compile if you would rather not use the script:
gcc -Wall -Werror -DLINUX -c ffrdp.c -o ffrdp.o
gcc -Wall -Werror -DLINUX -c lowlevel-udp.c -o lowlevel-udp.o
gcc -Wall -Werror -DLINUX -c socket.c -o socket.o
gcc -Wall -Werror -DLINUX -c utils.c -o utils.o
ar rcs libffrdp.a ffrdp.o lowlevel-udp.o socket.o utils.o
gcc -Wall -Werror -DLINUX test.c libffrdp.a -lpthread -o test_ffrdp
⚠️ The command above is the minimal / release-shaped build ✓ — without-DFFRDP_DIAG=1/-DFFRDP_FEC_ENABLE=1both diagnostics and FEC are compiled out ✓ (which is another legitimate profile ✓). For a byte-for-byte match withbuild.sh's output ✓, just usebuild.sh, or add those two feature macros and the stricter warning switches yourself ✓.
test_ffrdp runs one ffrdp instance in each of two threads (server and client share the same parameterised
peer loop ✓) and the two sides continuously exchange random-length data, calling ffrdp_dump() every 10
seconds to observe latency, retransmissions, in-flight amount and other metrics. The server accepts multiple
clients through the FFRDP_ACCEPT event ✓ (one handle per connection ✓), and each client's byte stream is
verified independently ✓.
./test_ffrdp --server=0.0.0.0:8000 # one end acts as the server
./test_ffrdp --client=192.168.1.10:8000 # the other end acts as the client
./test_ffrdp --server --client # local self-test (both ends in one process)| Option | Meaning | Default |
|---|---|---|
--server[=ip:port] |
act as the server and bind to that address | 0.0.0.0:8000 |
--client[=ip:port] |
act as the client and connect to that address | 127.0.0.1:8000 |
--server_max_send_size=N |
maximum bytes per message on the server (including the 12 B framing header+trailer) | 16384 |
--client_max_send_size=N |
maximum bytes per message on the client (including the 12 B framing header+trailer) | 16384 |
--send_speed=N |
send rate limit: cap this process's send rate at N KB/s | 0 (unlimited) |
ffrdp_recv() is an ordered, reliable byte stream (there are no message boundaries), so the test program adds
its own framing to every message:
[4B total len][4B seq][payload...][4B crc32] crc32 covers len + seq + payload
The receiver reassembles the byte stream into messages and verifies each one: seq continuity (was anything
lost or reordered ✓), crc32 (was the content corrupted ✓) and length validity (did the stream lose sync ✓).
The sender only advances seq when ffrdp_send() returns the full length ✓ (ffrdp_send() is atomic: either
the whole message is accepted or it returns -1 having accepted nothing ✓).
Failures print VERIFY ... lines (at most 3 verbose lines per error class, the rest are only counted ✓), and a
summary is printed on exit:
[client] verify: sent 285 msgs / 1638311 B, recv 285 msgs / 1638311 B | seq_gap 0 (missing 0), crc_err 0, len_err 0, partial_err 0
[client] verify: OK -- received 285 messages, seq contiguous and crc all correct
Notes:
--serverand--clientmay be given together (in-process self-test);--send_speedthen applies to both ends;--send_speedcontrols the send rate the way iperf3 does: with--send_speed=100, roughly 100KB per second is handed toffrdp_send()(measured 100.37KB/s ✓; 1KB/s, 20KB/s and 500KB/s track just as closely ✓). It is a token bucket: the full one-second allowance is replenished every 1 s, and the size of the next message is chosen from the remaining allowance (messages are atomic and cannot be trimmed ✓); pass0or nothing to disable ✓;- Without a rate limit the speed is decided by the test program itself (framing + crc32 + pseudo-random fill cost CPU ✓) and is far above any real link ✓;
- With no arguments it prints usage and exits;
- Exit with
quit/exit✓ — only that path prints theverify:summary above before exiting ✓ (and withFFRDP_CLOSE=1it first runs throughffrdp_close()'s four-way teardown ✓); - Internally it uses the fixed strings
"ffrdps://<ip:port>,mss:1280,fec:0"(server) /"ffrdpc://<ip:port>,mss:1280,fec:0"(client) ⇒ MSS 1280 bytes, FEC redundancy 0 (off) ✓; changetest.cfor other combinations ✓; - Three environment switches (none set ⇒ exactly as before ✓):
FFRDP_HS=0disables the handshake and takes the old v1 path ✓;FFRDP_IDLE=<seconds>makes the application send not a single byte ⇒ specifically to verify "idle is not killed by mistake" ✓;FFRDP_CLOSE=1callsffrdp_close()before exiting ⇒ verifies the four-way teardown ✓.
Since v1.2.0 the protocol layer is decoupled from the socket: the foundation is the transport endpoint defined by
lowlevel.h (send + receive + timer, with its own worker thread). That layer has its own contract test, using only
127.0.0.1 and ports derived from the pid — no network and no configuration needed:
bash tools/test-lowlevel-udp.sh # plain build (-Wall -Wextra -Werror)
ASAN=1 bash tools/test-lowlevel-udp.sh # address sanitizer (with leak detection)It covers, section by section: argument parsing ✓, received messages and source addresses ✓, sending back
(including send from inside a callback and from the main thread) ✓, payload boundaries (including truncation at
1500) ✓, throttling and burst load ✓, two endpoints and concurrency ✓, lifecycle (70 init/exit cycles with zero
leaked fds/threads ✓, prompt return from exit when there is no deadline ✓) and timers (deadline precision /
already past fires immediately / overwrite semantics / reset from inside a callback / cross-thread wakeup /
RECV and TIMER never interleave / callbacks never nest) ✓.
The design trade-offs and measured numbers are in section 13 of docs/notes.md.
Compresses one real-machine comparison into a single command ✓: upload the binary → start the server remotely → start the client locally → fetch the logs → produce a summary ✓.
./test.sh <local-binary> <user@host> [password] [seconds] # arg 1: relative / absolute / bare name all work ✓
export SESSION=z5-run1 # share one identifier across a scan, for grouped comparison (same as STAMP)
./test.sh dist/test_ffrdp_w5a4 chenk@192.168.0.213 abc 120
column -s, -t logs/summary.csv | tail -10 # the summary table- When
sshpassis not installed it opens an SSH multiplexed connection ⇒ you type the password once per run ✓ (KEEP_MUX=1keeps it alive across runs ✓); - Two different versions compared against each other:
REMOTE_BIN=<peer binary> ./test.sh <local binary> <user@host> …✓
One command that runs every criterion (run it after touching the protocol ✓, especially for performance work ✓):
bash tools/check.sh # build + simulation (14 tests / 3 scenario groups) + ASAN + local two-process four paths + v1 interop
QUICK=1 bash tools/check.sh # skip ASAN and e2e (about 1 minute) ✓- Six steps: real-machine-profile
build.sh✓, simulation with the default model (HEAD and the working tree compared line by line ✓), simulation under ASAN ✓, local two-process runs (default /FFRDP_HS=0old path /FFRDP_IDLE=1 FFRDP_CLOSE=1idle+teardown / multi-client ✓) and v1 interop (hs:0must work in both directions, the defaulths:1must not ✓); - All green ⇒ exit 0, any red ⇒ exit 1 ⇒ it can be wired straight into
pre-pushor a pipeline ✓; logs land inlogs/check-<timestamp>/✓. (The local side still uses argument 1, the remote sideREMOTE_BIN✓ — so you can tell whether "one direction being cut" is a local send problem or a peer acknowledgement problem ✓); - Other switches:
DUR/PORT/LOGDIR✓,CLIENTS=N(start N clients locally against one server ⇒ exercises multi-client ✓; the ceiling is the server'smaxconn− 1 ✓),CPUWATCH=1(sample the client's CPU ✓),PINGWATCH=1(ping the peer once a second for the whole run ⇒ measure the link itself as min/avg/max + loss ✓, to tell "code or link" apart ✓),CLEAN=1(clean up the remote side afterwards ✓),DRY=1(print the plan only, touch no network ✓),FFRDP_IDLE=1/FFRDP_CLOSE=1(idle and teardown acceptance ✓),V1_REF=<v1 binary>(run the v2 ↔ v1.2.x interop matrix locally ✓, see §2 compatibility); - Each run appends to
$LOGDIR/summary.csv✓ (stamp,tag,role,resend_pct,waste_pct,rtts_ms,send_recv_MBps✓) ⇒ after scanning a set of binaries, just read the table ✓; - Read the criteria against the protocol document:
rttm/rtts,counter_resend_ratio,confirmed rate,inflight, cap,ack latency— seedocs/ffrdp-v1.md§3.2.4 ✓.⚠️ Dump field names have changed across versions ✗ (for example thelap period avgmaxof the v1.2.0 era was removed during the code-slimming ✗) ⇒ before trusting a field, compare it against the actual dump output of the build you have ✓.
.
├── ffrdp.c protocol implementation (single file: ARQ + FEC + flow/concurrency control + diagnostics)
├── ffrdp.h public API (v2.0.0: **8 functions**, see below — handle-based I/O + event callbacks)
├── lowlevel.h lowlevel interface (tx + rx + timer, with its own worker thread) — since v1.2.0
├── lowlevel-udp.c the UDP implementation of lowlevel (bind + send/recv + timer + wakeup socket)
├── socket.c / socket.h thin socket wrapper (init, options, timeouts, non-blocking)
├── utils.c / utils.h timestamps, thread setname/getname, module thread state machine
├── test.c test program (server/client share one parameterised peer loop + dump every 10 s)
├── test.sh **real-machine A/B comparison** (upload → remote server → local client → logs → summary; see §4)
├── build.sh build script (static library + test program + pkg-config)
├── readme.md this file, the **English version** (shown by default on GitHub ✓; kept in sync with the Chinese one ✓)
├── readme.zh.md the Chinese version: overview, versions, build, tests, layout
├── LICENSE
├── docs/
│ ├── ffrdp-v1.md protocol description: v1.0.0 / v1.1.0 / v1.2.0 in three parts (frame format, parameters, changes and rationale)
│ ├── ffrdp-v2.md the v2.0.0 description: interface (params / handles / events), handshake and teardown, multi-client, encryption callbacks, keepalive; §9 covers the v2.x improvements over v1.1.x
│ ├── sim-test.md how to use the differential simulation harness (tools/sim)
│ ├── ffrdp-latency.md audio/video latency budget and tuning: the formula, numbers for slow/fast APs, the cost of loss, MTU and fragmentation, how to measure
│ └── notes.md development and debugging record (chronological, including the raw logs of every run)
└── tools/ helper tools (not listed one by one ✓: **one command for every criterion** `tools/check.sh`, **compile-time memory account** `tools/size.sh`, differential simulation `tools/sim/`, hostile input, in-flight sweep, lowlevel contract test, log extraction — the directory contents are authoritative ✓)
void* ffrdp_init (void *params, FFRDP_CALLBACK callback, void *cbctx); // params below (role + configuration are in the string)
void ffrdp_exit (void *ctxt); // close every connection and free (joins the transport thread inside)
int ffrdp_send (void *ctxt, int conn, char *buf, int len, int timeout_ms); // atomic: len = whole message accepted; 0 = closed; -2 = queue full and no space within the timeout
int ffrdp_recv (void *ctxt, int conn, char *buf, int len, int timeout_ms); // ordered, reliable **byte stream** (no message boundaries), returns as soon as there is data
void ffrdp_flush(void *ctxt, int conn); // send the not-yet-full frame immediately (conn < 0 = all connections)
void ffrdp_dump (void *ctxt, int conn, int clearhistory); // conn < 0 = overall state; conn >= 0 = detailed state of that connection
int ffrdp_isdead(void *ctxt, int conn); // has **this connection** been declared lost
int ffrdp_close(void *ctxt, int conn); // close actively: send FIN, report DISCONN only after the peer's FIN (new in v2 ✓)params (string configuration; a parse failure returns NULL and prints the reason ✓):
"ffrdps://0.0.0.0:8000" server (bind to that address)
"ffrdpc://192.168.0.213:8000" client (connect to that address)
"ffrdps://0.0.0.0:8000,maxconn:10" maximum number of connections (default 16, ceiling FFRDP_MAXCONN)
"ffrdps://0.0.0.0:8000,handshake_ms:10000,idle_ms:5000" handshake timeout / idle (keepalive) timeout
",mss:1280,fec:0" this end's MSS and FEC redundancy (v1's smss / sfec)
",hs:0" disable the handshake ⇒ take the old v1 path (use this to interoperate with v1.x)
⚠️ Time keys **carry their unit** (`_ms` ✓) — the same style as `timeout_ms`, leaving no "config in seconds, internals in milliseconds" conversion point ✗.
- Handle-based ✓:
connis a connection handle (the server obtains it from theFFRDP_ACCEPTevent ✓) and is an opaque int ✓; slots are recycled, handles are not revived ✓ ⇒ a stale handle fails the table lookup (FFRDP_CONN_INVALID,isdead= 1 ✓) and can never address a newer connection ✓. - Event callbacks ✓:
FFRDP_ACCEPT/CONNECT/DISCONN/ENCRYPT/DECRYPT/RECVDATA— semantics, callback return values (FFRDP_CONSUMED/NOT_CONSUMED) and the encryption contract are indocs/ffrdp-v2.md✓. - Return codes ✓:
> 0= byte count ✓,FFRDP_CLOSED(0)= peer closed ✓,FFRDP_EAGAIN(-2)= nothing for now / no space within the timeout ✓,FFRDP_CONN_INVALID(-1)= invalid handle (including already closed ones ✓). timeout_ms✓:0= do not wait (old behaviour ✓),> 0= wait at most that many milliseconds ✓,< 0= wait until something can be received / sent ✓.- Handshake is on by default ✓ (
hs:0turns it off ⇒ v1 behaviour ✓); idle keep-alive is governed byidle_ms✓ (default 5000 ms ✓).
Legacy interface on record (the
v1.2.xbranch) ✓ — the following is the current interface only on the v1.2.x branch ✓; it is gone from v2'sffrdp.h✗. The three signature differences (ffrdp_inittakes an address string,send/recvgained atimeout_ms,ffrdp_updateremoved) are indocs/ffrdp-v1.md§3.1.6 ✓.
void* ffrdp_init (char *addr, char *txkey, char *rxkey, int server, int smss, int sfec); // addr = "ip:port" / "ip" (port defaults to 8000)
void ffrdp_free (void *ctxt);
int ffrdp_send (void *ctxt, char *buf, int len, int timeout_ms); // atomic: len = whole message accepted; -1 = not a single byte accepted
int ffrdp_recv (void *ctxt, char *buf, int len, int timeout_ms); // fetch application data (ordered, reliable **byte stream**, no message boundaries)
int ffrdp_isdead(void *ctxt); // has the peer been lost
void ffrdp_flush (void *ctxt); // send the not-yet-full frame immediately (do not hoard it)
void ffrdp_dump (void *ctxt, int clearhistory); // print diagnostics| To learn about | Read |
|---|---|
| Frame format, mack, FEC, RTO computation, communication flow | part 1 (v1.0.0) of docs/ffrdp-v1.md (Chinese) |
| The motivation for each change, the routes that were rejected, the raw logs | docs/notes.md (Chinese) |
| How to prove "no regression" without a real machine | docs/sim-test.md (Chinese) |
| One command that runs every criterion (build / simulation / ASAN / local two-process four paths / v1 interop) | tools/check.sh (usage in §4) |
| Contract test for the transport layer (packets / timers / wakeup) | tools/test-lowlevel-udp.sh (usage in §4) |
| The v2.0.0 interface (params / handles / events / encryption callbacks / handshake and teardown / keepalive) | docs/ffrdp-v2.md (Chinese) |
| v2.0.0's design trade-offs and boundary conditions (connection table / handle generation / half-open ceiling / encryption contract) | docs/ffrdp-v2.md (Chinese) |
| A/V (H.265 / VBR) latency budget and tuning: the formula, numbers for slow/fast APs, the cost of a loss, MTU and IP fragmentation, how to measure | docs/ffrdp-latency.md (Chinese) |
| v2.x improvements over v1.1.x (with acceptance criteria, known limits, rejected routes and still-open directions) | docs/ffrdp-v2.md §9 (Chinese) |
Everything under
docs/is written in Chinese ✗. If you need English (or any other language) versions, please translate them with an AI ✓ — see the note at the top of this file.