feat(core): SYN+ACK and FIN go once, as in Java, measured with Java FILIUS
TcpRun loses exactly one frame (loseFirst: the card's output buffer drops it; the sender's packet log still shows it, as with setDrop) in three new stories: lostSynAck, lostFin, lostLastFin. Measured: Java never repeats a SYN+ACK or a FIN. The server gives a handshake up after one timeout (3 RTT) and closes with a FIN from SYN_RCVD (its boundary check sometimes lets it wait twice; it means once). After its FIN a socket waits at most five times, each wait ended by a segment or the timeout; then it is gone. The core does the same (TCP_CLOSING_WAITS, with TIME_WAIT in the loop) and shows CLOSED where Java leaves the state standing (a named Java bug). One departure: a repeated SYN in SYN_RCVD gets the SYN+ACK once more and the wait starts anew; Java closes there, but its client never repeats the SYN. lostSynAck is compared up to the second SYN (JAVA_UNTIL); the rest is an engine test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -203,6 +203,17 @@ und Mitschnitt teilen `DhcpRun` und `TcpRun` in `ScenarioNetwork`.
|
||||
* `lostData`: verbunden, dann gehen alle Frames verloren
|
||||
(`Verbindung.setDrop`, wie der Paketverlust des Kerns), während der Client
|
||||
sendet; der Verlust endet, wenn `senden` scheitert.
|
||||
* `lostSynAck`: das erste SYN+ACK des Servers geht verloren.
|
||||
* `lostFin`: verbunden, der Client schließt; sein FIN geht verloren.
|
||||
* `lostLastFin`: verbunden, der Client schließt; das FIN des Servers geht
|
||||
verloren.
|
||||
|
||||
Einen einzelnen Frame verliert `loseFirst`: Per Reflection ersetzt es den
|
||||
Ausgangspuffer der Karte (`Port.ausgangsPuffer`) durch einen, der den ersten
|
||||
passenden Frame verwirft, statt ihn einzureihen. Das Kabel wartet am alten
|
||||
Puffer und wird dort einmal geweckt. Der Lauscher des Absenders sieht den
|
||||
Frame wie bei `setDrop` (er schreibt nach dem Einreihen); anders als dort
|
||||
belegt der Frame das Kabel nicht.
|
||||
|
||||
Gemessen (30.09.2026, 90 %, RTT 2750 ms), was Swift anders macht:
|
||||
|
||||
@@ -220,6 +231,14 @@ Gemessen (30.09.2026, 90 %, RTT 2750 ms), was Swift anders macht:
|
||||
`connectClientMode` `closeSocket`, und die Schleife über drei Versuche
|
||||
endet (`verbinden` scheitert nach 3 RTT). Der Server-Socket wiederholt sein
|
||||
SYN+ACK nie.
|
||||
* Nach einem verlorenen SYN+ACK gibt der Server nach 3 RTT auf und schickt aus
|
||||
`SYN_RCVD` ein FIN (`close`). In zwei von drei Läufen wartete er 6 RTT: Der
|
||||
Vergleich `vergangen > defaultTimeout()` liegt genau auf der Grenze.
|
||||
* Ein FIN geht einmal hinaus, nie wieder. Danach wartet `close` höchstens
|
||||
fünfmal 3 RTT; jedes ankommende Segment beendet eine Wartezeit. Dann ist
|
||||
der Socket weg, sein Zustand bleibt aber stehen (`FIN_WAIT_1`,
|
||||
`FIN_WAIT_2`, `LAST_ACK`). Die Gegenseite eines verlorenen FIN bleibt
|
||||
`ESTABLISHED`.
|
||||
* Startnummern aus einem Zähler über alle TCP-Sockets (1000000, 2000000, …),
|
||||
wie im Kern; der freie Port ist zufällig.
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ done
|
||||
|
||||
# TCP as Java FILIUS runs it: stories of socket calls on tcp-lan's network
|
||||
# (TcpRun); frames, calls and socket states, for the core's TCP comparison.
|
||||
for run in exchange:30000 closedPort:30000 lostData:45000; do
|
||||
for run in exchange:30000 closedPort:30000 lostData:45000 lostSynAck:70000 lostFin:60000 lostLastFin:60000; do
|
||||
story="${run%%:*}"
|
||||
output="$root/packages/core/test/scenarios/tcp-java-$story.java.json"
|
||||
rm -f "$output"
|
||||
|
||||
@@ -14,17 +14,31 @@
|
||||
// closedPort a client connects to port 81, where nothing listens
|
||||
// lostData connected, all frames are lost (Verbindung.setDrop) while
|
||||
// the client sends; loss ends when senden fails
|
||||
// lostSynAck the server's first SYN+ACK is lost
|
||||
// lostFin connected, the client closes; its first FIN is lost
|
||||
// lostLastFin connected, the client closes; the server's first FIN is lost
|
||||
//
|
||||
// A single frame is lost on its sender's card (loseFirst): the card's output
|
||||
// buffer drops it instead of queueing it. The packet log sees it on the
|
||||
// sender, as with setDrop; unlike setDrop, the cable does not carry it.
|
||||
|
||||
import filius.hardware.Port;
|
||||
import filius.hardware.Verbindung;
|
||||
import filius.hardware.knoten.InternetKnoten;
|
||||
import filius.software.netzzugangsschicht.EthernetFrame;
|
||||
import filius.software.transportschicht.ServerSocket;
|
||||
import filius.software.transportschicht.Socket;
|
||||
import filius.software.transportschicht.TCPSocket;
|
||||
import filius.software.transportschicht.TcpSegment;
|
||||
import filius.software.vermittlungsschicht.IpPaket;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class TcpRun {
|
||||
static final String SERVER = "00000000-0000-0000-0000-000000000101";
|
||||
@@ -85,6 +99,75 @@ public class TcpRun {
|
||||
return thread;
|
||||
}
|
||||
|
||||
/**
|
||||
* The node's card drops the first frame whose TCP segment matches, instead
|
||||
* of putting it into its output buffer. The cable waits on the buffer it
|
||||
* found: woken there once, it takes the new one.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static void loseFirst(String nodeId, String label, Predicate<TcpSegment> which) throws Exception {
|
||||
Port port = ((InternetKnoten) network.system(nodeId).getKnoten()).getNetzwerkInterfaces().get(0).getPort();
|
||||
Field field = Port.class.getDeclaredField("ausgangsPuffer");
|
||||
field.setAccessible(true);
|
||||
LinkedList<EthernetFrame> old = (LinkedList<EthernetFrame>) field.get(port);
|
||||
field.set(port, new LinkedList<EthernetFrame>() {
|
||||
boolean lost;
|
||||
|
||||
@Override
|
||||
public boolean add(EthernetFrame frame) {
|
||||
if (!lost && frame.getDaten() instanceof IpPaket
|
||||
&& ((IpPaket) frame.getDaten()).getSegment() instanceof TcpSegment
|
||||
&& which.test((TcpSegment) ((IpPaket) frame.getDaten()).getSegment())) {
|
||||
lost = true;
|
||||
event("lost " + label);
|
||||
return true;
|
||||
}
|
||||
return super.add(frame);
|
||||
}
|
||||
});
|
||||
synchronized (old) {
|
||||
old.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
/** The listener's socket that waits for a connection, as soon as there is one. */
|
||||
static void watchListening(String label, ServerSocket listener) {
|
||||
Thread thread = new Thread(() -> {
|
||||
try {
|
||||
Field field = ServerSocket.class.getDeclaredField("aktuellerSocket");
|
||||
field.setAccessible(true);
|
||||
while (field.get(listener) == null) Thread.sleep(1);
|
||||
watch(label, (Socket) field.get(listener));
|
||||
} catch (Exception e) {
|
||||
event("watch failed " + e.getClass().getSimpleName());
|
||||
}
|
||||
});
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
/** Connected, then the client closes; the server's socket closes by itself. */
|
||||
static void connectAndClose() throws Exception {
|
||||
server(() -> {
|
||||
ServerSocket listener = new ServerSocket(network.system(SERVER), 80, IpPaket.TCP, new ArrayList<>());
|
||||
Socket accepted = (Socket) call("server oeffnen", listener::oeffnen);
|
||||
watch("server", accepted);
|
||||
return null;
|
||||
});
|
||||
Thread.sleep(500);
|
||||
TCPSocket client = new TCPSocket(network.system(CLIENT), "192.168.0.10", 80);
|
||||
watch("client", client);
|
||||
call("client verbinden", () -> {
|
||||
client.verbinden();
|
||||
return null;
|
||||
});
|
||||
Thread.sleep(500);
|
||||
call("client schliessen", () -> {
|
||||
client.schliessen();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
static String repeat(String text, int count) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (int i = 0; i < count; i++) result.append(text);
|
||||
@@ -95,6 +178,19 @@ public class TcpRun {
|
||||
network = ScenarioNetwork.read(args[0]);
|
||||
String story = args[1];
|
||||
long duration = Long.parseLong(args[2]);
|
||||
switch (story) {
|
||||
case "lostSynAck":
|
||||
loseFirst(SERVER, "SYN+ACK", segment -> segment.isSyn() && segment.isAck());
|
||||
break;
|
||||
case "lostFin":
|
||||
loseFirst(CLIENT, "FIN", TcpSegment::isFin);
|
||||
break;
|
||||
case "lostLastFin":
|
||||
loseFirst(SERVER, "FIN", TcpSegment::isFin);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
network.start();
|
||||
Thread poller = new Thread(() -> {
|
||||
while (true) {
|
||||
@@ -182,6 +278,26 @@ public class TcpRun {
|
||||
event("loss off");
|
||||
break;
|
||||
}
|
||||
case "lostSynAck": {
|
||||
server(() -> {
|
||||
ServerSocket listener = new ServerSocket(network.system(SERVER), 80, IpPaket.TCP, new ArrayList<>());
|
||||
watchListening("server", listener);
|
||||
call("server oeffnen", listener::oeffnen);
|
||||
return null;
|
||||
});
|
||||
Thread.sleep(500);
|
||||
TCPSocket client = new TCPSocket(network.system(CLIENT), "192.168.0.10", 80);
|
||||
watch("client", client);
|
||||
call("client verbinden", () -> {
|
||||
client.verbinden();
|
||||
return null;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "lostFin":
|
||||
case "lostLastFin":
|
||||
connectAndClose();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("unknown story " + story);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user