feat(core): Gnutella joining after Java, measured against Java

GnutellaRun lets Java's PeerToPeerAnwendung join peers on terminal-net.
The core's gnutella.ts does the same: a ping in Java's text format to
port 6346, a pong back, the ping sent on to the other peers and their
pongs back the way it came, at most the program's number of peers.

Departure from Java: a ping that comes a second time gets no pong and
its connection stays (Java's worker throws on the missing pong).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 12:29:54 +00:00
co-authored by Claude Opus 5.5
parent 9d42b90157
commit b4a95e6cac
18 changed files with 37937 additions and 3 deletions
+35
View File
@@ -870,6 +870,41 @@ Gemessen (02.10.2026):
ebenso eine unter Last (andere Sitzungen auf dem Rechner: die erste
ARP-Runde dauert dann knapp 300 statt 225 ms).
## Gnutella, Beitreten (F3.6a)
`GnutellaRun` installiert Javas Gnutella-Programm (`PeerToPeerAnwendung`)
auf vier Rechnern von `terminal-net` (192.168.0.20, .30, .31 und 10.0.0.2
hinter dem Router; .10 ohne) und ruft `beitretenNetzwerk` und
`resetNetwork` zu festen Zeiten, wie das Fenster es tut. Geschrieben werden
je Karte die Frames samt Tabellen, die bekannten Teilnehmer jedes Programms
bei jeder Meldung an sein Fenster und am Ende.
Gemessen (02.10.2026):
* Ein Paket ist Text, seine Felder durch `//` getrennt (`PeerToPeerPaket
.toString`): Ping `guid//0x00//hops//ttl//0//IP`, Pong
`guid//0x01//0//8//Bits//IP//6346//Dateien//Bytes`; die Bitlänge zählt
die Zeichen der Adresse mal acht und für jede Zahl die kleinste
Zweierpotenz, die sie fasst. Die GUID ist zufällig (`new Random()`).
* Beitreten: eine Verbindung zum Port 6346 des Teilnehmers, der Ping; der
Teilnehmer nimmt den Absender des Pings auf, antwortet mit einem Pong und
schickt den Ping (hops+1, ttl−1) über je eine neue Verbindung an seine
anderen Teilnehmer, außer an den Absender und den Rechner des Pings;
deren Pongs gehen (hops+1, ttl−1) über die Verbindung zurück, auf der der
Ping kam (`sendePaket` nach GUID). Wer einen Pong auf seinen eigenen Ping
bekommt, nimmt dessen Rechner auf; jeder höchstens so viele, wie er
behält. Keine Verbindung wird je geschlossen.
* Fehler Javas: kommt derselbe Ping ein zweites Mal an, gibt `erstellePong`
`null` zurück, und `toString()` darauf wirft; der Arbeiter schließt die
Verbindung (gemeint war: kein Pong). Ein Text ohne Paketkopf wirft
ebenso und schließt die Verbindung.
* Zu sich selbst kein Ping; zu einem Rechner ohne Programm ein SYN (Javas
eines statt drei); ins Leere nur ARP.
* Wo viele Verbindungen zugleich laufen, kommen die Frames zweier
Verbindungen (oder beider Richtungen einer) an einer Karte mal so, mal so
herum, einige zehn Millisekunden auseinander: Javas Thread-Takt über
mehrere Sprünge.
## Datei-Explorer, Texteditor, Bildbetrachter (F3.5b)
`FileCases` tut, was die Fenster dieser Programme mit den Dateien eines
+12 -1
View File
@@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then
fi
classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
mkdir -p "$here/.build"
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java" "$here/src/TerminalRun.java" "$here/src/FileCases.java"
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java" "$here/src/TerminalRun.java" "$here/src/FileCases.java" "$here/src/GnutellaRun.java"
# FILIUS writes its settings below $HOME; keep them out of the real one.
mkdir -p "$here/.build/home"
# Removed first: a crashed run must not leave the previous result standing.
@@ -286,6 +286,17 @@ for language in de en; do
test -s "$output"
done
# The Gnutella program joining peers on terminal-net (GnutellaRun;
# packages/core/test/gnutellaJava.test.ts).
for run in pair:6000 chain:9000 twoPaths:15000 full:11000 reset:7000 errors:40000; do
story="${run%%:*}"
output="$root/packages/core/test/scenarios/gnutella-java-$story.java.json"
rm -f "$output"
HOME="$here/.build/home" xvfb-run -a java $german -cp "$classpath:$here/.build" GnutellaRun \
"$root/packages/core/test/scenarios/terminal-net.scenario.json" "$story" "${run##*:}" "$output" 2>&1 | grep -v 'logback\|^$\|^\s*at \|scrollpane' || true
test -s "$output"
done
# What the file explorer and the text editor do to a host's files, without a
# running network, in both languages (FileCases; packages/core/test/filePrograms.test.ts).
for language in de en; do
+140
View File
@@ -0,0 +1,140 @@
// Runs a story of FILIUS' Gnutella program (PeerToPeerAnwendung with its
// PeerToPeerServer on port 6346 and PeerToPeerClient) on terminal-net in Java
// FILIUS and records what really happens: every frame each card saw, Java's
// tables of them, each program's known peers whenever it tells its window,
// and at the end each program's known peers and search results.
//
// GnutellaRun <scenario.json> <story> <milliseconds> <out.json>
//
// terminal-net: the program on 192.168.0.20, .30, .31 and 10.0.0.2 (behind
// the router), each keeping at most three peers unless the story says
// otherwise; .10 has none. A call comes as the program's window makes it
// (beitretenNetzwerk, resetNetwork); the acts are in the output.
// pair .30 joins .31
// chain .30 joins .31, then .20 joins .30 (the ping goes on to .31)
// twoPaths .31 joins 10.0.0.2, .30 joins .31 and 10.0.0.2, then .20 joins
// .30: the ping reaches .31 and 10.0.0.2 each on two paths
// full .31 keeps two peers, .30 and .20 one; .30, .20 and 10.0.0.2 join .31
// reset .30 joins .31, resets, joins again
// errors .20 joins itself, .10 (no program), .99 (no one)
import filius.software.dateiaustausch.PeerToPeerAnwendung;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class GnutellaRun {
static final String CLIENT = "00000000-0000-0000-0000-000000000401";
static final String WEB = "00000000-0000-0000-0000-000000000403";
static final String ECHO = "00000000-0000-0000-0000-000000000404";
static final String FAR = "00000000-0000-0000-0000-000000000406";
static final String[] PEERS = { CLIENT, WEB, ECHO, FAR };
static final String CLASS = "filius.software.dateiaustausch.PeerToPeerAnwendung";
static ScenarioNetwork network;
static final List<Object[]> events = new ArrayList<>();
static synchronized void event(String host, String kind, String text) {
events.add(new Object[] { System.currentTimeMillis() - network.start, host, kind, text });
}
static void sleepUntil(long milliseconds) throws InterruptedException {
long rest = network.start + milliseconds - System.currentTimeMillis();
if (rest > 0) Thread.sleep(rest);
}
/** The story's calls: time, host, call ("join <address>" or "reset"). */
static List<Object[]> acts(String story) {
List<Object[]> acts = new ArrayList<>();
switch (story) {
case "pair":
acts.add(new Object[] { 500L, WEB, "join 192.168.0.31" });
break;
case "chain":
acts.add(new Object[] { 500L, WEB, "join 192.168.0.31" });
acts.add(new Object[] { 3000L, CLIENT, "join 192.168.0.30" });
break;
case "twoPaths":
acts.add(new Object[] { 500L, ECHO, "join 10.0.0.2" });
acts.add(new Object[] { 3000L, WEB, "join 192.168.0.31" });
acts.add(new Object[] { 5500L, WEB, "join 10.0.0.2" });
acts.add(new Object[] { 8000L, CLIENT, "join 192.168.0.30" });
break;
case "full":
acts.add(new Object[] { 500L, WEB, "join 192.168.0.31" });
acts.add(new Object[] { 3000L, CLIENT, "join 192.168.0.31" });
acts.add(new Object[] { 5500L, FAR, "join 192.168.0.31" });
break;
case "reset":
acts.add(new Object[] { 500L, WEB, "join 192.168.0.31" });
acts.add(new Object[] { 3000L, WEB, "reset" });
acts.add(new Object[] { 3500L, WEB, "join 192.168.0.31" });
break;
case "errors":
acts.add(new Object[] { 500L, CLIENT, "join 192.168.0.20" });
acts.add(new Object[] { 1000L, CLIENT, "join 192.168.0.10" });
acts.add(new Object[] { 20000L, CLIENT, "join 192.168.0.99" });
break;
default:
throw new IllegalArgumentException("unknown story " + story);
}
return acts;
}
public static void main(String[] args) throws Exception {
network = ScenarioNetwork.read(args[0]);
String story = args[1];
long end = Long.parseLong(args[2]);
List<Object[]> acts = acts(story);
Map<String, PeerToPeerAnwendung> programs = new LinkedHashMap<>();
for (String host : PEERS) {
network.system(host).installApp(CLASS, false);
PeerToPeerAnwendung program = (PeerToPeerAnwendung) network.system(host).holeSoftware(CLASS);
program.setMaxTeilnehmerZahl(!story.equals("full") ? 3 : host.equals(ECHO) ? 2 : host.equals(FAR) ? 3 : 1);
program.hinzuBeobachter((observable, data) -> event(host, "peers", String.join(",", program.holeBekanntePeerToPeerTeilnehmer())));
programs.put(host, program);
}
network.start();
for (Object[] act : acts) {
sleepUntil((Long) act[0]);
String host = (String) act[1];
String call = (String) act[2];
event(host, "call", call);
if (call.startsWith("join ")) programs.get(host).beitretenNetzwerk(call.substring("join ".length()));
else programs.get(host).resetNetwork();
}
sleepUntil(end);
ScenarioNetwork.Json out = new ScenarioNetwork.Json();
out.open('{');
network.writeFrames(out);
out.key("acts").open('[');
for (Object[] act : acts) out.open('[').number((Long) act[0]).string((String) act[1]).string((String) act[2]).close(']');
out.close(']');
synchronized (GnutellaRun.class) {
out.key("events").open('[');
for (Object[] each : events) out.open('[').number((Long) each[0]).string((String) each[1]).string((String) each[2]).string((String) each[3]).close(']');
out.close(']');
}
out.key("programs").open('{');
for (Map.Entry<String, PeerToPeerAnwendung> entry : programs.entrySet()) {
PeerToPeerAnwendung program = entry.getValue();
out.key(entry.getKey()).open('{');
out.key("peers").open('[');
for (String peer : program.holeBekanntePeerToPeerTeilnehmer()) out.string(peer);
out.close(']');
out.key("results").open('[');
for (String result : program.holeErgebnisse()) out.string(result);
out.close(']');
out.close('}');
}
out.close('}');
out.close('}');
network.stop();
ScenarioNetwork.write(args[3], out);
System.exit(0);
}
}