Buffer Management
Overview
Rcon uses double buffering with ByteBuffer for efficient network I/O. Buffers are reused rather than reallocated, reducing memory churn and GC pressure.
Buffer Architecture
Application Buffer Socket
│ │ │
│ write │ │
├────────────>│ │
│ │ flush │
│ ├────────────>│
│ │ │
│ read │ │
│<────────────┤ │
│ │ fill │
│ │<─────────────┤
│ │ │
│ compact │ │
│ │<─────────────┤ (internal)Buffer Sizes
Double Buffering Pattern
The Problem
Simple buffer allocation on each read:
// WRONG - Allocates new buffer on every read
while (true) {
ByteBuffer buffer = ByteBuffer.allocate(4096); // New allocation!
channel.read(buffer);
// ... process packet ...
}This causes: * High memory allocation rate * Increased GC pressure * Poor performance
The Solution: Double Buffering
// CORRECT - Reuse buffer with compact()
ByteBuffer buffer = ByteBuffer.allocate(4096);
while (true) {
// 1. Fill buffer from channel
channel.read(buffer);
// 2. Flip for reading
buffer.flip();
// 3. Process packet data
processPacket(buffer);
// 4. Compact for reuse
buffer.compact(); // Move remaining data to beginning
}ByteBuffer Operations
flip()
Switch from write mode to read mode:
Before flip():
+------------------+------------------+
| Read Data | Unused |
+------------------+------------------+
^ ^ ^
0 position limit
After flip():
+------------------+------------------+
| Read Data | Unused |
+------------------+------------------+
^ ^ ^
0 limit capacity
position = 0 (reset to beginning)
limit = previous position (data length)compact()
Move remaining data to beginning, prepare for more writes:
Before compact():
+------------------+------------------+
| Read Data | Unused |
+------------------+------------------+
^ ^ ^ ^
0 position limit capacity
After compact():
+------------------+------------------+
| Remaining Data | Free Space |
+------------------+------------------+
^ ^ ^
0 position limit
- Move data from [position, limit) to [0, remaining)
- Set position = remaining
- Set limit = capacity
- Buffer ready for more writesImplementation: PacketReader
public class PacketReader implements AutoCloseable {
private static final int BUFFER_SIZE = 4096;
private final SocketChannel channel;
private final ByteBuffer buffer;
public PacketReader(SocketChannel channel) {
this.channel = channel;
this.buffer = ByteBuffer.allocate(BUFFER_SIZE);
}
public Packet readPacket(Duration timeout) throws IOException {
// Read until we have a complete packet
while (true) {
// Check if we have enough data for size field
buffer.flip();
if (buffer.remaining() < 4) {
// Need more data
buffer.compact();
channel.read(buffer);
continue;
}
// Read packet size
int size = buffer.getInt();
// Check if we have complete packet
if (buffer.remaining() < size) {
// Need more data
buffer.compact();
channel.read(buffer);
continue;
}
// We have complete packet!
return decodePacket(size);
}
}
private Packet decodePacket(int size) {
// Read ID, type, payload
int id = buffer.getInt();
int type = buffer.getInt();
byte[] payload = new byte[size - 8];
buffer.get(payload);
// Compact remaining data for next packet
buffer.compact();
return new Packet(id, PacketType.fromValue(type), payload);
}
@Override
public void close() {
// No resources to release (buffer is stack-allocated)
}
}Implementation: PacketWriter
public class PacketWriter implements AutoCloseable {
private static final int BUFFER_SIZE = 1460;
private final SocketChannel channel;
private final ByteBuffer buffer;
public PacketWriter(SocketChannel channel) {
this.channel = channel;
this.buffer = ByteBuffer.allocate(BUFFER_SIZE);
}
public void writePacket(Packet packet) throws IOException {
// Encode packet to buffer
buffer.clear();
encodePacket(packet, buffer);
buffer.flip();
// Write buffer to channel
while (buffer.hasRemaining()) {
channel.write(buffer);
}
}
private void encodePacket(Packet packet, ByteBuffer buffer) {
byte[] payload = packet.getPayload();
int size = 4 + 4 + payload.length;
buffer.putInt(size); // Size field
buffer.putInt(packet.getId()); // ID field
buffer.putInt(packet.getType().getValue()); // Type field
buffer.put(payload); // Payload
}
@Override
public void close() {
// No resources to release (buffer is stack-allocated)
}
}Performance Benefits
Tuning Buffer Sizes
Large Responses
If you frequently receive responses >4096 bytes, increase receive buffer:
// Custom PacketReader with larger buffer
public class LargePacketReader extends PacketReader {
private static final int BUFFER_SIZE = 65536; // 64 KB
// ... implementation ...
}This reduces the number of read syscalls for large responses.
Many Small Commands
If you send many small commands, smaller send buffer may reduce latency:
// Custom PacketWriter with smaller buffer
public class LowLatencyPacketWriter extends PacketWriter {
private static final int BUFFER_SIZE = 512; // 512 bytes
// ... implementation ...
}This reduces the time to fill and flush the buffer.
Monitoring Buffer Usage
Add instrumentation to monitor buffer utilization:
public class InstrumentedPacketReader extends PacketReader {
private long totalReads = 0;
private long totalBytes = 0;
private long maxRemaining = 0;
@Override
public Packet readPacket(Duration timeout) throws IOException {
Packet packet = super.readPacket(timeout);
// Track metrics
totalReads++;
totalBytes += packet.getPayload().length + 12;
maxRemaining = Math.max(maxRemaining, buffer.remaining());
return packet;
}
public double getAverageUtilization() {
return (double) totalBytes / (totalReads * BUFFER_SIZE);
}
public int getMaxRemaining() {
return maxRemaining;
}
}Common Pitfalls
Forgetting to flip()
// WRONG - Buffer still in write mode
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
int data = buffer.getInt(); // Reads from wrong position!// CORRECT - Flip before reading
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
buffer.flip(); // Switch to read mode
int data = buffer.getInt(); // Correct!Forgetting to compact()
// WRONG - Loses remaining data
while (true) {
channel.read(buffer);
buffer.flip();
processSomeData(buffer);
buffer.clear(); // Clears ALL data, including unread!
}// CORRECT - Compact preserves remaining data
while (true) {
channel.read(buffer);
buffer.flip();
processSomeData(buffer);
buffer.compact(); // Preserves unread data
}Direct Buffers
For high-performance scenarios, consider direct buffers:
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);Pros: * May avoid one copy between kernel and userspace * Better for I/O-heavy workloads
Cons: * Slightly slower allocation * Memory not in JVM heap (harder to debug)
See Also
-
Packet Protocol - Protocol specification
-
Performance Tuning - Optimization guide