feat(core): TCP timing and closing after Java, measured with Java FILIUS
TCP follows Java's TCPSocket where it differs from Swift: each attempt and TIME_WAIT wait three round trips that follow the speed (8250 ms at 90 %), counted from when the segment is handed down, before ARP; the next segment ends TIME_WAIT; the other end closes by itself on FIN; data no one acknowledges closes with FIN; the accepted socket has the listener's initial sequence number, and the listener takes the next. Not taken from Java: three SYNs, not one (Java's retry loop ends after the first timeout), named in tcpJava.test.ts as JAVA_BUGS. tcpJava.test.ts compares TcpRun's three stories frame by frame and state by state; the Swift comparison only checks that the TCP scenarios differ. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,10 @@
|
||||
// without remote link, wireless and gateway NAT; with the firewall)
|
||||
// "UDP sockets and Router RIP" (lines ~4472–5187, the
|
||||
// UDP sockets; RIP not yet)
|
||||
// "Java-compatible TCP sockets" (lines ~5187–5913)
|
||||
// "Java-compatible TCP sockets" (lines ~5187–5913; the
|
||||
// timing and closing after Java's TCPSocket, measured with Java
|
||||
// FILIUS: 3 round trips per attempt and for TIME_WAIT, the other end
|
||||
// closes by itself on FIN, failed data closes with FIN)
|
||||
// "DHCP server and automatic client" (lines ~5913–6694; the
|
||||
// client rebuilt after Java's DHCPClient, measured with Java FILIUS:
|
||||
// one message per attempt, an ARP check that waits a round trip,
|
||||
@@ -366,8 +369,6 @@ function sameIgnoringCase(lhs: string | undefined, rhs: string | undefined): boo
|
||||
|
||||
export const TCP_MAXIMUM_SEGMENT_SIZE = 1_460;
|
||||
export const TCP_MAXIMUM_SEND_ATTEMPTS = 3;
|
||||
export const TCP_ROUND_TRIP_TIME_MILLISECONDS = 2_500;
|
||||
export const TCP_DEFAULT_TIMEOUT_MILLISECONDS = TCP_MAXIMUM_SEND_ATTEMPTS * TCP_ROUND_TRIP_TIME_MILLISECONDS;
|
||||
export const TCP_EPHEMERAL_PORT_LOWER_BOUND = 49_152;
|
||||
export const TCP_EPHEMERAL_PORT_UPPER_BOUND_EXCLUSIVE = 65_535;
|
||||
|
||||
@@ -1330,7 +1331,8 @@ export class NetworkRuntimeEngine {
|
||||
tcpState: 'SYN_RCVD',
|
||||
parentListenerSocketId: listener.id,
|
||||
});
|
||||
const session = this.makeTcpSession(acceptedId, 'SYN_RCVD');
|
||||
// Java accepts on the socket that listened: it has the listener's initial sequence number.
|
||||
const session = this.makeTcpSession(acceptedId, 'SYN_RCVD', state.tcpSessionsById.get(listener.id)?.nextSendSequenceNumber);
|
||||
session.remoteSequenceNumber = nextTcpSequenceNumber(segment);
|
||||
state.tcpSessionsById.set(acceptedId, session);
|
||||
getOrCreate(state.tcpAcceptedSocketIdsByListenerId, listener.id, () => []).push(acceptedId);
|
||||
@@ -1370,14 +1372,28 @@ export class NetworkRuntimeEngine {
|
||||
this.sendTcpAcknowledgement(socketId, segment);
|
||||
return;
|
||||
}
|
||||
case 'SYN_RCVD':
|
||||
case 'SYN_RCVD': {
|
||||
if (!has(TCP_FLAG.acknowledgement) || !this.acknowledgeTcpIfExpected(socketId, segment.acknowledgementNumber)) return;
|
||||
this.updateTcpState(socketId, 'ESTABLISHED');
|
||||
// Java: the socket that listened is the connection now; the server
|
||||
// listens on with a new socket, and a new initial sequence number.
|
||||
const listenerId = socket.parentListenerSocketId;
|
||||
if (listenerId !== undefined && this.stateValue.tcpSessionsById.has(listenerId)) {
|
||||
this.stateValue.tcpSessionsById.set(listenerId, this.makeTcpSession(listenerId, 'LISTEN'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'TIME_WAIT':
|
||||
this.clearTcpPendingTransmission(socketId);
|
||||
this.updateTcpState(socketId, 'CLOSED');
|
||||
return;
|
||||
case 'ESTABLISHED':
|
||||
if (has(TCP_FLAG.finish)) {
|
||||
this.sendTcpAcknowledgement(socketId, segment);
|
||||
this.updateTcpState(socketId, 'CLOSE_WAIT');
|
||||
// Java's socket thread leaves listen once the state is no longer
|
||||
// ESTABLISHED and closes by itself, without waiting for the application.
|
||||
this.closeTcpSocket(socketId);
|
||||
} else if (segment.payload.length > 0 || has(TCP_FLAG.push)) {
|
||||
this.receiveTcpPayload(socketId, segment);
|
||||
} else if (has(TCP_FLAG.acknowledgement)) {
|
||||
@@ -1453,6 +1469,9 @@ export class NetworkRuntimeEngine {
|
||||
if (socket === undefined || socket.remoteIPAddress === undefined || socket.remotePort === undefined || session === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
// Java's socket starts waiting when it hands the segment down, before
|
||||
// ARP has found the next hop; the timeout counts from here.
|
||||
const handedDownAt = state.currentTimeMilliseconds;
|
||||
let segment: TcpSegment;
|
||||
if (retransmission && session.lastSentSegment !== undefined) {
|
||||
segment = session.lastSentSegment;
|
||||
@@ -1500,14 +1519,19 @@ export class NetworkRuntimeEngine {
|
||||
detail: retransmission ? `${detail} retransmission attempt ${session.sendAttempts}` : detail,
|
||||
});
|
||||
const delivery = this.sendIPv4Packet(socket.nodeId, packet, networkInterface.portId);
|
||||
if (state.tcpSessionsById.get(socketId)?.lastSentSegment !== undefined) this.scheduleTcpTimeoutIfNeeded(socketId);
|
||||
if (state.tcpSessionsById.get(socketId)?.lastSentSegment !== undefined) this.scheduleTcpTimeoutIfNeeded(socketId, handedDownAt);
|
||||
return delivery;
|
||||
}
|
||||
|
||||
private scheduleTcpTimeoutIfNeeded(socketId: Id): void {
|
||||
private scheduleTcpTimeoutIfNeeded(socketId: Id, fromMilliseconds: number): void {
|
||||
const pending = this.stateValue.pendingEvents.some((event) => event.kind.kind === 'tcpTimeout' && event.kind.sessionId === socketId);
|
||||
if (pending) return;
|
||||
this.schedule(this.stateValue.currentTimeMilliseconds + TCP_DEFAULT_TIMEOUT_MILLISECONDS, { kind: 'tcpTimeout', sessionId: socketId });
|
||||
this.schedule(fromMilliseconds + this.tcpTimeoutMilliseconds(), { kind: 'tcpTimeout', sessionId: socketId });
|
||||
}
|
||||
|
||||
/** Java's defaultTimeout: MAX_SENDEVERSUCHE round trips, for each attempt and for TIME_WAIT. */
|
||||
private tcpTimeoutMilliseconds(): number {
|
||||
return TCP_MAXIMUM_SEND_ATTEMPTS * this.runtimeSpeed.roundTripTimeMilliseconds;
|
||||
}
|
||||
|
||||
private handleTcpTimeout(socketId: Id): void {
|
||||
@@ -1523,7 +1547,14 @@ export class NetworkRuntimeEngine {
|
||||
if (session === undefined || last === undefined) return;
|
||||
if (session.sendAttempts >= TCP_MAXIMUM_SEND_ATTEMPTS) {
|
||||
this.clearTcpPendingTransmission(socketId);
|
||||
this.updateTcpState(socketId, 'CLOSED');
|
||||
// Java: data that no one acknowledges (sent only while ESTABLISHED)
|
||||
// ends senden with schliessen, an orderly close with FIN. A handshake
|
||||
// or a FIN that fails closes at once.
|
||||
if (socket.tcpState === 'ESTABLISHED') {
|
||||
this.closeTcpSocket(socketId);
|
||||
} else {
|
||||
this.updateTcpState(socketId, 'CLOSED');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const synchronize = (last.flags & TCP_FLAG.synchronize) !== 0;
|
||||
@@ -1560,10 +1591,11 @@ export class NetworkRuntimeEngine {
|
||||
);
|
||||
}
|
||||
|
||||
/** Java's close loop: TIME_WAIT ends with the next segment, else after defaultTimeout. */
|
||||
private enterTcpTimeWait(socketId: Id): void {
|
||||
this.clearTcpPendingTransmission(socketId);
|
||||
this.updateTcpState(socketId, 'TIME_WAIT');
|
||||
this.schedule(this.stateValue.currentTimeMilliseconds + TCP_ROUND_TRIP_TIME_MILLISECONDS, { kind: 'tcpTimeout', sessionId: socketId });
|
||||
this.schedule(this.stateValue.currentTimeMilliseconds + this.tcpTimeoutMilliseconds(), { kind: 'tcpTimeout', sessionId: socketId });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1579,8 +1611,12 @@ export class NetworkRuntimeEngine {
|
||||
const socket = this.stateValue.socketsById.get(socketId);
|
||||
if (socket === undefined || socket.tcpState === undefined) return advancedToTimeout;
|
||||
const waitingOnState = options.waitingStates?.includes(socket.tcpState) === true;
|
||||
// Data waits for its acknowledgement only while connected: once the
|
||||
// socket closes (Java: senden gives up and calls schliessen), it returns.
|
||||
const waitingOnAck =
|
||||
options.waitingForAcknowledgement === true && this.stateValue.tcpSessionsById.get(socketId)?.lastSentSegment !== undefined;
|
||||
options.waitingForAcknowledgement === true &&
|
||||
socket.tcpState === 'ESTABLISHED' &&
|
||||
this.stateValue.tcpSessionsById.get(socketId)?.lastSentSegment !== undefined;
|
||||
if (!waitingOnState && !waitingOnAck) return advancedToTimeout;
|
||||
const deadlines = this.stateValue.pendingEvents
|
||||
.filter((event) => event.kind.kind === 'tcpTimeout' && event.kind.sessionId === socketId)
|
||||
@@ -1637,8 +1673,11 @@ export class NetworkRuntimeEngine {
|
||||
return Number(BigInt.asUintN(32, (value % 4_294_967_296n) * 1_000_000n));
|
||||
}
|
||||
|
||||
private makeTcpSession(socketId: Id, socketState: TcpSocketState): TcpSession {
|
||||
const initialSequence = this.allocateTcpInitialSequenceNumber();
|
||||
private makeTcpSession(
|
||||
socketId: Id,
|
||||
socketState: TcpSocketState,
|
||||
initialSequence: number = this.allocateTcpInitialSequenceNumber(),
|
||||
): TcpSession {
|
||||
return {
|
||||
id: socketId,
|
||||
socketId,
|
||||
|
||||
Reference in New Issue
Block a user