The protocol uses a fixed-header format with variable-length payload. Each packet consists of an 11-byte header followed by up to 1013 bytes of payload data.
Value: 2Description: Synchronization packet to initiate connection (first step of three-way handshake).Usage: Sent by client to server to request connection establishment.Fields:
Sequence Number: Always 1
Address: Server address
Port: Server listening port (typically 8000)
Payload: Arbitrary data (e.g., “1”)
Example:
Packet syn = new Packet.Builder() .setType(2) // SYN .setSequenceNumber(1L) .setPeerAddress(InetAddress.getByName("192.168.1.10")) .setPortNumber(8000) // Server's main port .setPayload("1".getBytes()) .create();
Type 3: SYN-ACK
Value: 3Description: Synchronization-acknowledgment packet (second step of three-way handshake).Usage: Sent by server to client to acknowledge SYN and provide handler port.Fields:
Sequence Number: Same as received SYN (typically 1)
Address: Client address
Port: Client port
Payload: Handler port number as ASCII string
Example:
// Server creates handler socket on random port 54321DatagramSocket handlerSocket = new DatagramSocket();int handlerPort = handlerSocket.getLocalPort(); // 54321Packet synAck = new Packet( 3, // SYN-ACK 1, // Same seq as SYN clientAddress, clientPort, Integer.toString(handlerPort).getBytes() // "54321");
Critical: The client MUST parse the payload to extract the handler port for all subsequent communication.
Type 4: NACK
Value: 4Description: Negative acknowledgment requesting retransmission of a specific packet.Semantics: Selective NACK
A NACK with sequence number N requests immediate retransmission of packet N only
Does not affect window or other packets
When Sent:
Receiver gets packet M but is expecting packet N (where N < M)
Receiver sends NACK(N) to request the missing packet
Example:
// Receiver expects packet 10 but receives packet 15// Sends NACKs for packets 10, 11, 12, 13, 14for (int i = 10; i < 15; i++) { if (!receivedPackets.containsKey(i)) { Packet nack = new Packet( 4, // Type: NACK i, // Missing packet number senderAddress, senderPort, new byte[0] // Empty payload ); socket.send(nack); }}
Payload: Empty (0 bytes)
Type 6: CLOSE
Value: 6Description: Connection termination signal.Usage: Indicates end of data transmission and connection closure.Fields:
Sequence Number: Not significant
Address: Peer address
Port: Peer port
Payload: Empty
Example:
// Server sends CLOSE after sending responsePacket close = new Packet( 6, // Type: CLOSE 0, // Sequence not used clientAddress, clientPort, new byte[0]);socket.send(close);// Client receiving loopPacket p = Packet.fromBytes(data);if (p.getType() == 6) { // Close connection and exit socket.close(); return;}
The CLOSE packet is mentioned in the code but not fully implemented in the handshake or teardown process.
Offset: 1-4 Size: 4 bytes (unsigned 32-bit integer) Byte Order: Big Endian Range: 0 to 4,294,967,295Purpose: Identifies the packet’s position in the sequence of transmitted packets.Sequence Numbering:
Packet 1: Size information (number of total packets)
Packet 2-N: Actual data packets
Implementation:
// In Packet.java, line 72private void write(ByteBuffer buf) { buf.put((byte) type); buf.putInt((int) sequenceNumber); // 4 bytes, Big Endian buf.put(peerAddress.getAddress()); buf.putShort((short) peerPort); buf.put(payload);}
Sequence Number Wraparound: With a 32-bit sequence number, wraparound occurs after 4,294,967,295 packets (~4.3 billion packets). For typical use cases, this is not a concern.
Offset: 5-8 Size: 4 bytes (IPv4 address) Byte Order: N/A (4 octets) Format: IPv4 dotted-decimal notationPurpose: Specifies the destination IP address of the packet.Routing Behavior:
Sender → Router: Contains final destination address
Router → Receiver: Router swaps this with sender’s address
Example:
Client (192.168.1.5) sends to Server (192.168.1.10):┌─────────────────────────────────────────────────────┐│ Client constructs packet with address = 192.168.1.10│└─────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────┐│ Router receives from 192.168.1.5 ││ Swaps address to 192.168.1.5 (sender's address) ││ Forwards to 192.168.1.10 │└─────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────┐│ Server receives packet with address = 192.168.1.5 ││ Knows to send responses to 192.168.1.5 │└─────────────────────────────────────────────────────┘
Implementation:
// In Packet.java, line 73buf.put(peerAddress.getAddress()); // 4-byte IPv4 address
IPv6 Not Supported: The protocol only supports IPv4 addresses (4 bytes). IPv6 support would require 16 bytes and protocol modifications.
Offset: 11 to end Size: Variable (0 to 1013 bytes) Encoding: Raw bytes (application-dependent)Purpose: Contains the actual data being transmitted.Payload Types by Packet Type:
Packet Type
Payload Content
Typical Size
DATA (seq=1)
Total packet count (ASCII)
1-10 bytes
DATA (seq>1)
Application data chunk
1013 bytes
ACK
Empty
0 bytes
NACK
Empty
0 bytes
SYN
Arbitrary data
1-10 bytes
SYN-ACK
Handler port (ASCII)
4-6 bytes
CLOSE
Empty
0 bytes
Data Fragmentation:
// Split large data into 1013-byte chunksfinal int PAYLOAD_SIZE = 1013;List<byte[]> chunks = IntStream.iterate(0, i -> i + PAYLOAD_SIZE) .limit((data.length + PAYLOAD_SIZE - 1) / PAYLOAD_SIZE) .mapToObj(i -> Arrays.copyOfRange(data, i, Math.min(i + PAYLOAD_SIZE, data.length))) .collect(Collectors.toList());// First packet contains countint totalPackets = chunks.size() + 1;
Maximum Payload Size: The payload must not exceed 1013 bytes. With the 11-byte header, this ensures the total packet size stays within the 1024-byte limit.
public static final int MIN_LEN = 11; // Header onlypublic static final int MAX_LEN = 11 + 1024 = 1035; // Wait, this is wrong!
Code Inconsistency: The Java code defines MAX_LEN = 11 + 1024, but the actual maximum should be 1024 bytes total (11-byte header + 1013-byte payload). This appears to be a bug in the constant definition.The correct values should be:
public static final int MIN_LEN = 11;public static final int MAX_LEN = 1024; // Total packet sizepublic static final int MAX_PAYLOAD = 1013; // MAX_LEN - MIN_LEN
Big Endian Required: All multi-byte fields (sequence number and port) MUST be in Big Endian (network byte order). Mixing endianness will cause parsing failures.
Why Big Endian?
Standard network byte order (RFC 1700)
Ensures interoperability between different architectures
// Packet Typespublic static final int TYPE_DATA = 0;public static final int TYPE_ACK = 1;public static final int TYPE_SYN = 2;public static final int TYPE_SYN_ACK = 3;public static final int TYPE_NACK = 4;public static final int TYPE_CLOSE = 6;// Size Constantspublic static final int HEADER_SIZE = 11;public static final int MAX_PAYLOAD_SIZE = 1013;public static final int MAX_PACKET_SIZE = 1024;public static final int MIN_PACKET_SIZE = 11;// Field Offsetspublic static final int OFFSET_TYPE = 0;public static final int OFFSET_SEQUENCE = 1;public static final int OFFSET_ADDRESS = 5;public static final int OFFSET_PORT = 9;public static final int OFFSET_PAYLOAD = 11;// Field Sizespublic static final int SIZE_TYPE = 1;public static final int SIZE_SEQUENCE = 4;public static final int SIZE_ADDRESS = 4;public static final int SIZE_PORT = 2;
// Efficient: Pre-serialize packetsfor (int i = 0; i < chunks.size(); i++) { Packet p = createPacket(i, chunks.get(i)); packets.add(new DatagramPacket(p.toBytes(), ...)); // Serialize once}// Send multiple times without re-serializationsocket.send(packets.get(5));socket.send(packets.get(5)); // Retransmit using same bytes