15.3 Scattering, Gathering, and Composed Buffers

Master advanced buffer patterns for efficient structured data handling.

Scattering Reads

Basic Scatter Reading:

import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.net.InetSocketAddress;

/**
 * Read data directly into multiple buffers
 */
public static void scatterRead() throws IOException {
    // Create multiple buffers for different message parts
    ByteBuffer header = ByteBuffer.allocate(128);
    ByteBuffer body = ByteBuffer.allocate(1024);
    ByteBuffer trailer = ByteBuffer.allocate(256);

    ByteBuffer[] buffers = {header, body, trailer};

    try (SocketChannel channel = SocketChannel.open()) {
        channel.connect(new InetSocketAddress("localhost", 8080));

        // Read entire message into appropriate buffers
        long bytesRead = channel.read(buffers);

        System.out.println("Total bytes read: " + bytesRead);
        System.out.println("Header position: " + header.position());
        System.out.println("Body position: " + body.position());
        System.out.println("Trailer position: " + trailer.position());
    }
}

/**
 * Partial scatter read
 */
public static void partialScatterRead() throws IOException {
    ByteBuffer[] buffers = {
        ByteBuffer.allocate(256),
        ByteBuffer.allocate(512),
        ByteBuffer.allocate(256)
    };

    try (SocketChannel channel = SocketChannel.open()) {
        channel.connect(new InetSocketAddress("localhost", 8080));

        // Read into buffers[1] and buffers[2] only
        long bytesRead = channel.read(buffers, 1, 2);

        System.out.println("Bytes read into buffers[1] and [2]: " + bytesRead);
        System.out.println("buffers[0] position (unchanged): " + buffers[0].position());
        System.out.println("buffers[1] position: " + buffers[1].position());
        System.out.println("buffers[2] position: " + buffers[2].position());
    }
}

Protocol Parsing with Scatter:

/**
 * Parse message protocol: [length(4)][type(1)][data(var)][checksum(4)]
 */
public class ProtocolParser {
    private static final int HEADER_SIZE = 5;
    private static final int CHECKSUM_SIZE = 4;

    /**
     * Read protocol message with scatter
     */
    public static ProtocolMessage readMessage(SocketChannel channel) 
            throws IOException {

        // Prepare buffers for protocol fields
        ByteBuffer header = ByteBuffer.allocate(HEADER_SIZE);
        ByteBuffer dataBuffer = null;
        ByteBuffer checksum = ByteBuffer.allocate(CHECKSUM_SIZE);

        // First scatter: read header
        while (header.hasRemaining()) {
            channel.read(header);
        }

        // Parse header
        header.flip();
        int dataLength = header.getInt();
        byte type = header.get();

        // Allocate buffer for data
        dataBuffer = ByteBuffer.allocate(dataLength);

        // Second scatter: read data and checksum
        ByteBuffer[] buffers = {dataBuffer, checksum};
        long totalRead = 0;
        while (totalRead < (dataLength + CHECKSUM_SIZE)) {
            totalRead += channel.read(buffers);
        }

        // Parse message
        dataBuffer.flip();
        byte[] data = new byte[dataBuffer.remaining()];
        dataBuffer.get(data);

        checksum.flip();
        int checksumValue = checksum.getInt();

        return new ProtocolMessage(type, data, checksumValue);
    }

    public static class ProtocolMessage {
        public final byte type;
        public final byte[] data;
        public final int checksum;

        public ProtocolMessage(byte type, byte[] data, int checksum) {
            this.type = type;
            this.data = data;
            this.checksum = checksum;
        }
    }
}

Gathering Writes

Basic Gather Writing:

/**
 * Write multiple buffers in single operation
 */
public static void gatherWrite() throws IOException {
    ByteBuffer header = ByteBuffer.wrap(new byte[]{0x01, 0x02, 0x03});
    ByteBuffer body = ByteBuffer.wrap("Hello, World!".getBytes());
    ByteBuffer trailer = ByteBuffer.wrap(new byte[]{0xFF, 0xFE});

    ByteBuffer[] buffers = {header, body, trailer};

    try (FileChannel out = FileChannel.open(Path.of("output.bin"),
            StandardOpenOption.CREATE,
            StandardOpenOption.WRITE,
            StandardOpenOption.TRUNCATE_EXISTING)) {

        // Write all buffers in order
        long bytesWritten = out.write(buffers);
        System.out.println("Bytes written: " + bytesWritten);
    }
}

/**
 * Partial gather write
 */
public static void partialGatherWrite() throws IOException {
    ByteBuffer[] buffers = {
        ByteBuffer.wrap(new byte[]{0x01}),
        ByteBuffer.wrap(new byte[]{0x02}),
        ByteBuffer.wrap(new byte[]{0x03}),
        ByteBuffer.wrap(new byte[]{0x04})
    };

    try (FileChannel out = FileChannel.open(Path.of("output.bin"),
            StandardOpenOption.CREATE,
            StandardOpenOption.WRITE)) {

        // Write only buffers[1] and buffers[2]
        long bytesWritten = out.write(buffers, 1, 2);
        System.out.println("Bytes written: " + bytesWritten); // 2
    }
}

Building Messages with Gather:

/**
 * Construct and send protocol message
 */
public class MessageBuilder {

    /**
     * Build message with header, body, footer
     */
    public static void sendStructuredMessage(SocketChannel channel,
                                           String body) throws IOException {

        // Header: message length and type
        ByteBuffer header = ByteBuffer.allocate(5);
        header.putInt(body.length());
        header.put((byte) 0x01); // Type
        header.flip();

        // Body
        ByteBuffer bodyBuf = ByteBuffer.wrap(body.getBytes());

        // Trailer: checksum
        ByteBuffer trailer = ByteBuffer.allocate(4);
        int checksum = calculateChecksum(body.getBytes());
        trailer.putInt(checksum);
        trailer.flip();

        // Send all at once
        ByteBuffer[] buffers = {header, bodyBuf, trailer};

        long bytesWritten = 0;
        while (bytesWritten < header.capacity() + bodyBuf.capacity() + trailer.capacity()) {
            bytesWritten += channel.write(buffers);
        }

        System.out.println("Message sent: " + bytesWritten + " bytes");
    }

    private static int calculateChecksum(byte[] data) {
        int sum = 0;
        for (byte b : data) {
            sum += b & 0xFF;
        }
        return sum;
    }

    public static void main(String[] args) throws IOException {
        // Example: send message
        try (SocketChannel channel = SocketChannel.open()) {
            channel.connect(new InetSocketAddress("localhost", 8080));
            sendStructuredMessage(channel, "Hello, Protocol!");
        }
    }
}

Composed Buffers

Combining Multiple Buffers:

import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;

/**
 * Logically combine multiple buffers
 */
public class ComposedBufferExample {

    /**
     * Create composed buffer from multiple sources
     */
    public static ByteBuffer compose(ByteBuffer... buffers) throws IOException {
        // Calculate total size
        int totalSize = 0;
        for (ByteBuffer buf : buffers) {
            totalSize += buf.remaining();
        }

        // Create result buffer
        ByteBuffer result = ByteBuffer.allocate(totalSize);

        // Copy all buffers
        for (ByteBuffer buf : buffers) {
            result.put(buf);
        }

        result.flip();
        return result;
    }

    /**
     * Copy buffer and append
     */
    public static ByteBuffer append(ByteBuffer original, ByteBuffer additional) {
        ByteBuffer combined = ByteBuffer.allocate(
            original.remaining() + additional.remaining()
        );

        combined.put(original);
        combined.put(additional);
        combined.flip();

        return combined;
    }

    /**
     * Prepend to buffer
     */
    public static ByteBuffer prepend(ByteBuffer prefix, ByteBuffer original) {
        ByteBuffer combined = ByteBuffer.allocate(
            prefix.remaining() + original.remaining()
        );

        combined.put(prefix);
        combined.put(original);
        combined.flip();

        return combined;
    }

    /**
     * Insert into buffer
     */
    public static ByteBuffer insert(ByteBuffer buffer, int position, 
                                   ByteBuffer insertion) {
        ByteBuffer combined = ByteBuffer.allocate(
            buffer.remaining() + insertion.remaining()
        );

        // Copy before insertion point
        int originalPos = buffer.position();
        buffer.position(0);
        buffer.limit(position);
        combined.put(buffer);

        // Insert new data
        combined.put(insertion);

        // Copy after insertion point
        buffer.position(position);
        buffer.limit(buffer.capacity());
        combined.put(buffer);

        buffer.position(originalPos);
        combined.flip();

        return combined;
    }
}

Logical Buffer View:

/**
 * Create logical view of multiple buffers as single buffer
 */
public class CompositeByteBuffer {
    private final ByteBuffer[] buffers;
    private int position = 0;
    private int limit;

    public CompositeByteBuffer(ByteBuffer... buffers) {
        this.buffers = buffers;
        this.limit = calculateTotal();
    }

    private int calculateTotal() {
        int total = 0;
        for (ByteBuffer buf : buffers) {
            total += buf.remaining();
        }
        return total;
    }

    /**
     * Get next byte without advancing position
     */
    public byte peek() {
        if (position >= limit) {
            throw new BufferUnderflowException();
        }

        int bufferIndex = findBufferIndex(position);
        int offsetInBuffer = position - getBufferStartPosition(bufferIndex);

        return buffers[bufferIndex].get(offsetInBuffer);
    }

    /**
     * Get next byte and advance position
     */
    public byte get() {
        byte value = peek();
        position++;
        return value;
    }

    /**
     * Get multiple bytes
     */
    public void get(byte[] dst) {
        for (int i = 0; i < dst.length; i++) {
            dst[i] = get();
        }
    }

    /**
     * Advance position
     */
    public void skip(int bytes) {
        position = Math.min(position + bytes, limit);
    }

    /**
     * Get current position
     */
    public int position() {
        return position;
    }

    /**
     * Set position
     */
    public void position(int newPosition) {
        if (newPosition < 0 || newPosition > limit) {
            throw new IllegalArgumentException("Invalid position: " + newPosition);
        }
        this.position = newPosition;
    }

    /**
     * Get total size
     */
    public int remaining() {
        return limit - position;
    }

    private int findBufferIndex(int position) {
        int accumulator = 0;
        for (int i = 0; i < buffers.length; i++) {
            int bufferSize = buffers[i].remaining();
            if (position < accumulator + bufferSize) {
                return i;
            }
            accumulator += bufferSize;
        }
        throw new IndexOutOfBoundsException("Position: " + position);
    }

    private int getBufferStartPosition(int bufferIndex) {
        int accumulator = 0;
        for (int i = 0; i < bufferIndex; i++) {
            accumulator += buffers[i].remaining();
        }
        return accumulator;
    }

    /**
     * Usage example
     */
    public static void main(String[] args) {
        ByteBuffer buf1 = ByteBuffer.wrap("Hello, ".getBytes());
        ByteBuffer buf2 = ByteBuffer.wrap("World".getBytes());
        ByteBuffer buf3 = ByteBuffer.wrap("!".getBytes());

        CompositeByteBuffer composite = new CompositeByteBuffer(buf1, buf2, buf3);

        System.out.println("Remaining: " + composite.remaining()); // 13

        // Read bytes one by one
        while (composite.remaining() > 0) {
            System.out.print((char) composite.get());
        }
        System.out.println();

        // Reset and peek
        composite.position(0);
        System.out.println("First char: " + (char) composite.peek());
        System.out.println("Position after peek: " + composite.position()); // Still 0

        // Read entire message
        byte[] message = new byte[composite.remaining()];
        composite.get(message);
        System.out.println("Message: " + new String(message));
    }
}

Typed Buffers Deep Dive

Working with Typed Buffers:

/**
 * Efficient structured data with typed buffers
 */
public class StructuredData {

    /**
     * Record structure: 4 fields
     * [id(int), value(long), score(float), active(byte)]
     */
    public static class Record {
        public int id;
        public long value;
        public float score;
        public byte active;

        public void writeTo(ByteBuffer buffer) {
            buffer.putInt(id);
            buffer.putLong(value);
            buffer.putFloat(score);
            buffer.put(active);
        }

        public static Record readFrom(ByteBuffer buffer) {
            Record record = new Record();
            record.id = buffer.getInt();
            record.value = buffer.getLong();
            record.score = buffer.getFloat();
            record.active = buffer.get();
            return record;
        }
    }

    /**
     * Write records using IntBuffer view
     */
    public static void writeRecordsTyped() {
        // Use IntBuffer for integer fields
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        IntBuffer intView = byteBuffer.asIntBuffer();
        LongBuffer longView = byteBuffer.asLongBuffer();

        Record record1 = new Record();
        record1.id = 1;
        record1.value = 1000L;
        record1.score = 95.5f;
        record1.active = 1;

        // Write using byte buffer
        byteBuffer.clear();
        record1.writeTo(byteBuffer);
        byteBuffer.flip();

        // Read back using views
        intView.position(0);
        System.out.println("ID from IntView: " + intView.get());
    }

    /**
     * Batch operations with typed buffers
     */
    public static void batchProcessing() {
        ByteBuffer buffer = ByteBuffer.allocate(256);
        IntBuffer intBuffer = buffer.asIntBuffer();

        // Write batch of integers
        int[] values = {10, 20, 30, 40, 50};
        for (int value : values) {
            intBuffer.put(value);
        }

        // Read back
        intBuffer.flip();
        int[] readValues = new int[intBuffer.remaining()];
        intBuffer.get(readValues);

        System.out.println("Read values: " + Arrays.toString(readValues));
    }
}

CharBuffer for Text:

/**
 * Text processing with CharBuffer
 */
public class TextBuffering {

    /**
     * Wrap string as CharBuffer
     */
    public static CharBuffer wrapString(String text) {
        return CharBuffer.wrap(text);
    }

    /**
     * Build string in buffer
     */
    public static String buildString(CharBuffer buffer) {
        if (buffer.hasArray()) {
            return new String(buffer.array(), buffer.arrayOffset(), buffer.remaining());
        }

        char[] chars = new char[buffer.remaining()];
        buffer.get(chars);
        return new String(chars);
    }

    /**
     * Line-by-line text processing
     */
    public static void processText(String text) {
        CharBuffer buffer = CharBuffer.wrap(text);

        StringBuilder line = new StringBuilder();

        while (buffer.hasRemaining()) {
            char ch = buffer.get();

            if (ch == '\n') {
                System.out.println("Line: " + line);
                line = new StringBuilder();
            } else {
                line.append(ch);
            }
        }

        if (line.length() > 0) {
            System.out.println("Line: " + line);
        }
    }
}

Best Practices

1. Use Scatter/Gather for Protocol Parsing:

// Separate concerns: header, body, trailer
ByteBuffer[] buffers = {headerBuf, bodyBuf, trailerBuf};
channel.read(buffers);

2. Compose Buffers Logically:

// Keep buffers separate and compose on demand
ByteBuffer composed = compose(buf1, buf2, buf3);

3. Choose Correct Buffer Type:

// Use typed buffers for efficiency
IntBuffer ints = intBuffer.asIntBuffer();
LongBuffer longs = byteBuffer.asLongBuffer();

4. Handle Partial Operations:

// May not read/write entire array
long totalRead = 0;
while (totalRead < expectedSize) {
    totalRead += channel.read(buffers);
}

5. Maintain Buffer Position Tracking:

// Know where data is in composed buffers
int bufferIndex = findBufferContaining(position);

These scattering, gathering, and composition techniques enable efficient structured data handling in modern NIO applications.