All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dellroad.muxable.simple.SimpleMuxableChannel Maven / Gradle / Ivy

/*
 * Copyright (C) 2021 Archie L. Cobbs. All rights reserved.
 */

package org.dellroad.muxable.simple;

import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousCloseException;
import java.nio.channels.ByteChannel;
import java.nio.channels.Pipe;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.WritableByteChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.Optional;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.stream.LongStream;

import org.dellroad.muxable.Directions;
import org.dellroad.muxable.MuxableChannel;
import org.dellroad.muxable.NestedChannel;
import org.dellroad.stuff.net.SelectorSupport;
import org.dellroad.stuff.util.LongMap;

/**
 * An implementation of the {@link MuxableChannel} interface that multiplexes nested channels over a single underlying
 * {@link ByteChannel} (or {@link ReadableByteChannel}, {@link WritableByteChannel} pair) using a simple framing protocol.
 *
 * 

* The nested channel data and {@link NestedChannel}s share the same underlying "real" channel, * so any one left unread for too long can block all the others. This implementation only guarantees that * "senseless" deadlock won't happen (see {@link MuxableChannel}). * *

* Protocol Description * *

* After the initial connection, each side transmits a {@linkplain ProtocolConstants#PROTOCOL_COOKIE protocol cookie} * followed by its {@linkplain ProtocolConstants#CURRENT_PROTOCOL_VERSION current protocol version}. Once so established, * the protocol simply consists of frames being sent back and forth. A frame consists of a channel ID, * an optional flags byte, a payload length, and finally the payload content. The channel ID and * length values are encoded via {@link io.permazen.util.LongEncoder}. * *

* The channel ID is negated by the sender for channels created by the receiver; this way, the receiver can differentiate * a local channel ID (negative) from a remote channel ID (positive). Reception of a frame with a channel ID of zero means * to immediately close the entire parent connection. * *

* Each side keeps track of which channel ID's have been allocated (by either side). Reception of a remote channel ID * that is equal to the next available remote channel ID means the remote side is requesting to open a new nested channel; * the subsequent payload becomes the data associated with the request, and a flag byte is sent after the channel ID * specifying the {@link Directions} for the new nested channel. * *

* Reception of zero length payload implies closing the associated nested channel (however, this does not apply to the * initial frame that opens a new channel, so it's possible to request opening a new channel with zero bytes of * request data). Closing a nested channel always means closing both directions. * *

* Note that it's possible for one side to receive data on a nested channel that it has already closed, because that data * may have been sent prior to the remote side receiving the local side's close notification. Such data is discarded. * *

* Java NIO * *

* Because an internal service thread is created, instances must be explicitly {@link #start}'d before use * and {@link #stop}'d when no longer needed. When this instance is {@link #stop}'d, the underlying * channel(s) with which it was constructed are closed. * *

* Invoking {@link #close} on this instance has the same effect as invoking {@link #stop}. * *

* Instances are thread safe. */ public class SimpleMuxableChannel extends SelectorSupport implements MuxableChannel { private static final int MAIN_CHANNEL_INPUT_BUFFER_SIZE = (1 << 20) - 64; // 1MB minus 64 bytes of overhead private static final long MAIN_CHANNEL_OUTPUT_QUEUE_FULL = (1L << 26); // 64MB private static final int NESTED_CHANNEL_INPUT_BUFFER_SIZE = (1 << 17) - 64; // 128K minus 64 bytes of overhead private static final long NESTED_CHANNEL_OUTPUT_QUEUE_FULL = (1L << 23); // 8MB private static final int REQUEST_QUEUE_CAPACITY = 1024; private static final int DEFAULT_HOUSEKEEPING_INTERVAL = 200; // 200ms // Logging @SuppressWarnings("this-escape") private final LoggingSupport log = this.buildLoggingSupport(); // Given channel(s) private final SelectableChannel input; private final SelectableChannel output; /* +-------------------------------------+ / \ / ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ / ┏━━━━━━━━━━━━━━━┓ ┃ SimpleMuxableChannel ┃ / /┃ NestedChannel ┃ ┃ ┃ / / ┃ ┃ ┃ ┃ / / ┃ input ━╋━━━━━━━━━━━━╋━ NestedInfo.inputFlow ┃ ┏━━━━━━━━━━━━━┓ ┃ output ━╋━━+ +━━╋━ NestedInfo.inputFlow ┃ ┃ ┃ ┗━━━━━━━━━━━━━━━┛ \ / ┃ ... ┃ ┃ Application ┃ \ / ┃ [peerInput] ━╋━━━━━ from network ┃ ┃ ┏━━━━━━━━━━━━━━━┓ \/ ┃ ┃ ┗━━━━━━━━━━━━━┛ ┃ NestedChannel ┃ /\ ┃ [peerOutput] ━╋━━━━━ to network \ ┃ ┃ / \ ┃ ┃ \ ┃ input ━╋━━━+ +━━━╋━ NestedInfo.outputFlow ┃ \┃ output ━╋━━━━━━━━━━━━╋━ NestedInfo.outputFlow ┃ ┗━━━━━━━━━━━━━━━┛ ┃ ... ┃ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ */ // Peer flows private PeerInputFlow peerInput; private PeerOutputFlow peerOutput; // Nested channels private final LongMap nestedInfoMap = new LongMap<>(); // Incoming new channel requests private final ArrayBlockingQueue requests = new ArrayBlockingQueue<>(REQUEST_QUEUE_CAPACITY); // Framing protocol input & output state private final ChannelIds channelIds = new ChannelIds(this.log.log, this.log.logPrefix + "ChannelIds: "); private final ProtocolReader reader = new ProtocolReader(this.log.log, this.log.logPrefix + "ProtocolReader: ", this.channelIds, new ProtocolReader.InputHandler() { @Override public void nestedChannelRequest(long channelId, ByteBuffer requestData, Directions directions) throws IOException { SimpleMuxableChannel.this.nestedChannelRequest(channelId, requestData, directions); } @Override public void nestedChannelData(long channelId, ByteBuffer data) throws IOException { SimpleMuxableChannel.this.nestedChannelData(channelId, data); } @Override public void nestedChannelClosed(long channelId) throws IOException { SimpleMuxableChannel.this.nestedChannelClosed(channelId); } }); private final ProtocolWriter writer = new ProtocolWriter(this.log.log, this.log.logPrefix + "ProtocolWriter: ", this.channelIds, this::enqueuePeerOutput); // This counts how many of the nested output queues are full. We only really care if this number is zero or not, // as that's what tells us whether to select for read on the peer's input. But we keep track of the exact count // to avoid having to survey all of the other nested output queues every time any one of them changes. private long numNestedOutputsFull; // Misc other state private State state = State.NOT_STARTED; private volatile Throwable shutdownCause; // Constructors /** * Constructor taking a single, bi-directional {@link ByteChannel}. * *

* Equivalent to: {@code SimpleMuxableChannel(channel, channel)}. * * @param channel underlying channel for both input and output * @param bidirectional channel type * @throws IllegalArgumentException if {@code channel} is null */ public SimpleMuxableChannel(C channel) { this(channel, channel); } /** * Constructor inferring the {@link SelectorProvider} to use. * *

* The {@link SelectorProvider} is inferred from {@code input}. * * @param input channel receiving input from the remote side * @param output channel taking output from the local side; may be the same as {@code input} * @param input channel type * @param output channel type * @throws IllegalArgumentException if either channel is null */ public SimpleMuxableChannel(I input, O output) { this(input != null ? input.provider() : SelectorProvider.provider(), input, output); } /** * Primary constructor. * *

* The given channels will be closed when this instance is closed. * * @param provider the {@link SelectorProvider} that this instance will use * @param input channel receiving input from the remote side * @param output channel taking output from the local side; may be the same as {@code input} * @param input channel type * @param output channel type * @throws IllegalArgumentException if any parameter is null */ public SimpleMuxableChannel(SelectorProvider provider, I input, O output) { super(provider); if (input == null) throw new IllegalArgumentException("null input"); if (output == null) throw new IllegalArgumentException("null output"); this.input = input; this.output = output; this.setHousekeepingInterval(SimpleMuxableChannel.DEFAULT_HOUSEKEEPING_INTERVAL); } protected LoggingSupport buildLoggingSupport() { return new LoggingSupport(this); } // MuxableChannel @Override public SimpleNestedChannel newNestedChannel(ByteBuffer requestData) throws IOException { return (SimpleNestedChannel)MuxableChannel.super.newNestedChannel(requestData); } @Override public synchronized SimpleNestedChannel newNestedChannel(ByteBuffer requestData, Directions directions) throws IOException { // Sanity check if (requestData == null) throw new IllegalArgumentException("null requestData"); if (directions == null) throw new IllegalArgumentException("null directions"); if (!this.state.equals(State.RUNNING)) throw new IOException("channel is in state " + this.state, this.shutdownCause); // Allocate a new local channel ID and send request to peer this.log.debug("creating new local channel %d", this.channelIds.getNextLocalChannelId()); final long channelId = this.writer.openNestedChannel(requestData, directions); // Setup the nested channel locally return this.newNestedChannel(channelId, requestData, directions); } @Override public BlockingQueue getNestedChannelRequests() { return this.requests; } // I/O Event Handling // Read data from the peer on the peer input channel and run it through the input protocol state machine private void handlePeerChannelReadable() throws IOException { assert Thread.holdsLock(this); this.log.trace("%s %s", "peer channel", "readable"); final ByteBuffer data = this.peerInput.read(); this.log.trace("%s input %s", "peer channel", LoggingSupport.toString(data, 64)); if (!this.reader.input(data)) throw new EOFException("connection closed by the remote peer"); } // Read data from the application on a nested channel and run it through the output protocol state machine private void handleNestedChannelReadable(NestedInputFlow nestedInput) throws IOException { assert Thread.holdsLock(this); this.log.trace("%s %s", nestedInput, "readable"); final ByteBuffer data = nestedInput.read(); this.log.trace("%s input %s", nestedInput, LoggingSupport.toString(data, 64)); this.writer.writeNestedChannel(nestedInput.getChannelId(), data); } // Write data to the peer on the peer output channel and update selectors if there was a meaningful output queue change private void handlePeerChannelWritable() throws IOException { assert Thread.holdsLock(this); this.log.trace("%s %s", "peer channel", "writable"); final int flags = this.peerOutput.write(); this.log.trace("%s %s", "peer channel", OutputQueue.describeFlags(flags)); if (this.wentNonFull(flags)) this.nestedInfoMap.values().forEach(NestedInfo::startReading); if (this.wentEmpty(flags)) this.peerOutput.stopWriting(); } // Write data to the application on a nested channel and update selectors if there was a meaningful output queue change private void handleNestedChannelWritable(NestedOutputFlow nestedOutput) throws IOException { assert Thread.holdsLock(this); this.log.trace("%s %s", nestedOutput, "writable"); final int flags = nestedOutput.write(); this.log.trace("%s %s", nestedOutput, OutputQueue.describeFlags(flags)); if (this.wentNonFull(flags) && --this.numNestedOutputsFull == 0) this.peerInput.startReading(); if (this.wentEmpty(flags)) nestedOutput.stopWriting(); } // An exception has occurred on one of the channels that connect to the peer over the network. // This causes the whole thing to be shutdown. private void handlePeerChannelClosed(Flow flow, Throwable cause) { assert Thread.holdsLock(this); if (this.shutdownCause == null) { this.log.debug("exception on %s: %s", flow, cause); this.shutdownCause = cause; this.close(); } } // An exception has occurred on one of the nested channels that connect to the application private void handleNestedChannelException(NestedFlow nestedInfo, Throwable cause) { assert Thread.holdsLock(this); // Log the exception final long channelId = nestedInfo.getChannelId(); final boolean alreadyClosed = !this.nestedInfoMap.containsKey(channelId); if (alreadyClosed) this.log.trace("exception on %s: %s", nestedInfo, cause); else this.log.debug("exception on %s: %s", nestedInfo, cause); // Close the nested channel if (!this.closeNestedChannel(channelId, true)) return; } // Somebody invoked SimpleNestedChannel.close() synchronized void handleNestedChannelClosed(long channelId) { this.log.debug("close() invoked on channel %s%d", channelId < 0 ? "R" : "L", Math.abs(channelId)); this.closeNestedChannel(channelId, true); } // ProtocolReader.InputHandler // The peer has asked to create a new nested channel private void nestedChannelRequest(long channelId, ByteBuffer requestData, Directions directions) throws IOException { assert Thread.holdsLock(this); this.log.debug("rec'd new channel request: remote channel %d, requestData %s", -channelId, LoggingSupport.toString(requestData, 64)); this.requests.add(this.newNestedChannel(channelId, requestData, directions)); } // The peer has sent us some data on a nested channel private void nestedChannelData(long channelId, ByteBuffer data) { assert Thread.holdsLock(this); this.log.trace("recv data on channel %s%d: %s", channelId < 0 ? "R" : "L", Math.abs(channelId), LoggingSupport.toString(data, 64)); final NestedInfo nestedInfo = this.nestedInfoMap.get(channelId); if (nestedInfo == null) { // remote must have sent the data before it knew we closed the channel this.log.debug("ignoring data on closed channel %s%d: %s", channelId < 0 ? "R" : "L", Math.abs(channelId), LoggingSupport.toString(data, 64)); return; } if (nestedInfo.outputFlow() == null) { throw new ProtocolViolationException(this.reader.getOffset() - data.remaining(), String.format("rec'd data on write-only %s", nestedInfo)); } if (this.enqueueOutputCheckFull(nestedInfo.outputFlow(), data) && this.numNestedOutputsFull++ == 0) this.peerInput.stopReading(); } // The peer has told us that it is closing a nested channel private void nestedChannelClosed(long channelId) { assert Thread.holdsLock(this); this.log.debug("rec'd close for channel %s%d", channelId < 0 ? "R" : "L", Math.abs(channelId)); this.closeNestedChannel(channelId, false); } // ProtocolWriter.OutputHandler // The ProtocolWriter has generated some output to send to the peer private void enqueuePeerOutput(ByteBuffer data) { assert Thread.holdsLock(this); if (this.enqueueOutputCheckFull(this.peerOutput, data)) this.nestedInfoMap.values().forEach(NestedInfo::stopReading); } // Helper // Enqueue some data on the given output and enable writing if the queue went from empty to non-empty. // Returns true if the queue transitioned from less-than-full to full; in that case, the caller must enact flow control. private boolean enqueueOutputCheckFull(OutputFlow outputFlow, ByteBuffer data) { assert Thread.holdsLock(this); if (outputFlow == null) throw new IllegalArgumentException("null outputFlow"); if (data == null) throw new IllegalArgumentException("null data"); this.log.trace("writing to %s: %s", outputFlow, LoggingSupport.toString(data, 64)); final int flags = outputFlow.enqueue(data); this.log.trace("%s %s", outputFlow, OutputQueue.describeFlags(flags)); if (this.wentNonEmpty(flags)) outputFlow.startWriting(); return this.wentFull(flags); } // SelectorSupport @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public synchronized void start() throws IOException { // Sanity check if (!this.state.equals(State.NOT_STARTED)) throw new IllegalStateException("can't start in state " + this.state); this.log.debug("starting"); super.start(); // Create peer input & output; if they are the same channel, use a single common SelectionKey if (this.input == this.output) { // Create combined IOHandler final IOHandler combinedHandler = new IOHandler() { @Override public void serviceIO(SelectionKey key) throws IOException { final boolean invalid = !key.isValid(); if (invalid || key.isReadable()) SimpleMuxableChannel.this.peerInput.serviceIO(key); if (invalid || key.isWritable()) SimpleMuxableChannel.this.peerOutput.serviceIO(key); } @Override public void close(Throwable cause) { SimpleMuxableChannel.this.closeAndCatch(SimpleMuxableChannel.this.peerInput); SimpleMuxableChannel.this.closeAndCatch(SimpleMuxableChannel.this.peerOutput); } @Override public String toString() { return "peer input/output channel"; } }; // Use a single selection key for both flows final SelectionKey key = this.createSelectionKey(this.input, combinedHandler, true); this.peerInput = new PeerInputFlow(this.input, key); this.peerOutput = new PeerOutputFlow(this.output, key); } else { this.peerInput = new PeerInputFlow(this.input); this.peerOutput = new PeerOutputFlow(this.output); } // Start reading peer input this.peerInput.startReading(); this.state = State.RUNNING; this.log.debug("started"); } @Override public synchronized void stop() { if (this.state.equals(State.RUNNING)) { this.log.debug("stopping"); super.stop(); this.shutdown(); this.state = State.STOPPED; this.log.debug("stopped"); } } // Channel @Override public synchronized boolean isOpen() { return this.state.equals(State.RUNNING); } @Override public void close() { this.stop(); } // Internal methods private SimpleNestedChannel newNestedChannel(long channelId, ByteBuffer requestData, Directions directions) throws IOException { // Sanity check assert Thread.holdsLock(this); if (this.nestedInfoMap.containsKey(channelId)) throw new RuntimeException("internal error"); // Debug this.log.debug("opening new %s channel %s%d", directions, channelId < 0 ? "R" : "L", Math.abs(channelId)); // Create channel for local <- peer data flow final NestedOutputFlow outputFlow; final Pipe.SourceChannel nestedInput; if (directions.hasInput()) { final Pipe pipe = this.provider.openPipe(); outputFlow = new NestedOutputFlow(pipe, channelId); nestedInput = pipe.source(); } else { outputFlow = null; nestedInput = null; } // Create channel for local -> peer data flow final NestedInputFlow inputFlow; final Pipe.SinkChannel nestedOutput; if (directions.hasOutput()) { final Pipe pipe = this.provider.openPipe(); inputFlow = new NestedInputFlow(pipe, channelId); inputFlow.startReading(); nestedOutput = pipe.sink(); } else { inputFlow = null; nestedOutput = null; } // Add nested channel this.nestedInfoMap.put(channelId, new NestedInfo(channelId, inputFlow, outputFlow)); // Build request object and add to queue return new SimpleNestedChannel(this, channelId, nestedInput, nestedOutput, requestData); } private boolean closeNestedChannel(long channelId, boolean notifyPeer) { assert Thread.holdsLock(this); // Have we already closed this nested channel? If so, ignore this duplicate request. final NestedInfo nestedInfo = this.nestedInfoMap.get(channelId); if (nestedInfo == null) return false; // Debug this.log.debug("closing %s", nestedInfo); // Close local -> peer direction if (nestedInfo.inputFlow() != null) nestedInfo.inputFlow().close(); // Close local <- peer direction if (nestedInfo.outputFlow() != null) { nestedInfo.outputFlow().close(); if (nestedInfo.outputFlow().getOutputQueue().isFull() && --this.numNestedOutputsFull == 0) this.peerInput.startReading(); } // Notify the peer if (notifyPeer) { try { this.writer.closeNestedChannel(channelId); } catch (IOException e) { // ignore } } // Done return true; } private boolean wentEmpty(int flags) { return (flags & (OutputQueue.WAS_EMPTY | OutputQueue.NOW_EMPTY)) == OutputQueue.NOW_EMPTY; } private boolean wentFull(int flags) { return (flags & (OutputQueue.WAS_FULL | OutputQueue.NOW_FULL)) == OutputQueue.NOW_FULL; } private boolean wentNonEmpty(int flags) { return (flags & (OutputQueue.WAS_EMPTY | OutputQueue.NOW_EMPTY)) == OutputQueue.WAS_EMPTY; } private boolean wentNonFull(int flags) { return (flags & (OutputQueue.WAS_FULL | OutputQueue.NOW_FULL)) == OutputQueue.WAS_FULL; } private void shutdown() { assert Thread.holdsLock(this); LongStream.of(this.nestedInfoMap.keySet().toLongArray()) .forEach(channelId -> this.closeNestedChannel(channelId, false)); this.numNestedOutputsFull = 0; this.log.debug("closing %s", this.peerInput); this.peerInput.close(); this.log.debug("closing %s", this.peerOutput); this.peerOutput.close(); } protected void closeAndCatch(Closeable closeable) { try { closeable.close(); } catch (IOException e) { // ignore } } // Flow private abstract class Flow implements IOHandler, Closeable { private final T channel; private final SelectionKey key; Flow(T channel, SelectionKey key) throws IOException { if (channel == null) throw new IllegalArgumentException("null channel"); this.channel = channel; this.key = key != null ? key : SimpleMuxableChannel.this.createSelectionKey(this.channel, this, true); } public SelectionKey getKey() { return this.key; } public T getChannel() { return this.channel; } /** * Describe this channel for debug purposes. */ @Override public abstract String toString(); @Override public final void serviceIO(SelectionKey key) throws IOException { if (!key.isValid()) { this.close(new AsynchronousCloseException()); return; } this.service(); } protected abstract void service() throws IOException; @Override public void close() { SimpleMuxableChannel.this.closeAndCatch(this.channel); } } // InputFlow private abstract class InputFlow extends Flow { private final int bufferSize; InputFlow(T channel, SelectionKey key, int bufferSize) throws IOException { super(channel, key); this.bufferSize = bufferSize; } public void startReading() { SimpleMuxableChannel.this.log.trace("%s %s %s", this, "enable", "select for read"); SimpleMuxableChannel.this.selectFor(this.getKey(), SelectionKey.OP_READ, true); } public void stopReading() { SimpleMuxableChannel.this.log.trace("%s %s %s", this, "disable", "select for read"); SimpleMuxableChannel.this.selectFor(this.getKey(), SelectionKey.OP_READ, false); } public ByteBuffer read() throws IOException { final ByteBuffer data = ByteBuffer.allocate(this.bufferSize); final int numRead = this.getChannel().read(data); if (numRead == -1) throw new EOFException("got EOF reading channel"); data.flip(); return data; } } // NestedFlow private interface NestedFlow { /** * Determine whether channel was originated remotely or locally. */ default boolean isRemote() { return this.getChannelId() < 0; } /** * Get the (encoded) channel ID for this channel. */ long getChannelId(); } // PeerInputFlow // Represents the "real" underlying input stream from the remote peer private class PeerInputFlow extends InputFlow { PeerInputFlow(T channel, SelectionKey key) throws IOException { super(channel, key, MAIN_CHANNEL_INPUT_BUFFER_SIZE); } PeerInputFlow(T channel) throws IOException { this(channel, null); } @Override public void service() throws IOException { SimpleMuxableChannel.this.handlePeerChannelReadable(); } @Override public void close(Throwable cause) { SimpleMuxableChannel.this.handlePeerChannelClosed(this, cause); } @Override public String toString() { return "peer input channel"; } } // NestedInputFlow // Represents the input from the application for outgoing data to be written on a nested channel private class NestedInputFlow extends InputFlow implements NestedFlow { private final long channelId; private final Pipe.SinkChannel pipeSink; NestedInputFlow(Pipe pipe, long channelId) throws IOException { super(pipe.source(), null, NESTED_CHANNEL_INPUT_BUFFER_SIZE); this.channelId = channelId; this.pipeSink = pipe.sink(); } @Override public long getChannelId() { return this.channelId; } @Override public void service() throws IOException { SimpleMuxableChannel.this.handleNestedChannelReadable(this); } @Override public void close(Throwable cause) { SimpleMuxableChannel.this.handleNestedChannelException(this, cause); } @Override public void close() { super.close(); SimpleMuxableChannel.this.closeAndCatch(this.pipeSink); } @Override public String toString() { return String.format("nested input channel %s%d", this.isRemote() ? "R" : "L", Math.abs(this.channelId)); } } // OutputFlow private abstract class OutputFlow extends Flow { private final OutputQueue queue; OutputFlow(T channel, SelectionKey key, long outputQueueFullMark) throws IOException { super(channel, key); this.queue = new OutputQueue(outputQueueFullMark); } public void startWriting() { SimpleMuxableChannel.this.log.trace("%s %s %s", this, "enable", "select for write"); SimpleMuxableChannel.this.selectFor(this.getKey(), SelectionKey.OP_WRITE, true); } public void stopWriting() { SimpleMuxableChannel.this.log.trace("%s %s %s", this, "disable", "select for write"); SimpleMuxableChannel.this.selectFor(this.getKey(), SelectionKey.OP_WRITE, false); } public OutputQueue getOutputQueue() { return this.queue; } public int enqueue(ByteBuffer data) { return this.queue.enqueue(data); } public int write() throws IOException { return this.queue.writeTo(this.getChannel()); } } // PeerOutputFlow // Represents the "real" underlying output stream to the remote peer private class PeerOutputFlow extends OutputFlow { PeerOutputFlow(T channel, SelectionKey key) throws IOException { super(channel, key, MAIN_CHANNEL_OUTPUT_QUEUE_FULL); } PeerOutputFlow(T channel) throws IOException { this(channel, null); } @Override public void service() throws IOException { SimpleMuxableChannel.this.handlePeerChannelWritable(); } @Override public void close(Throwable cause) { SimpleMuxableChannel.this.handlePeerChannelClosed(this, cause); } @Override public String toString() { return "peer output channel"; } } // NestedOutputFlow // Represents the output to the application for incoming data that was received on a nested channel private class NestedOutputFlow extends OutputFlow implements NestedFlow { private final long channelId; private final Pipe.SourceChannel pipeSource; NestedOutputFlow(Pipe pipe, long channelId) throws IOException { super(pipe.sink(), null, NESTED_CHANNEL_OUTPUT_QUEUE_FULL); this.channelId = channelId; this.pipeSource = pipe.source(); } @Override public long getChannelId() { return this.channelId; } @Override public void service() throws IOException { SimpleMuxableChannel.this.handleNestedChannelWritable(this); } @Override public void close(Throwable cause) { SimpleMuxableChannel.this.handleNestedChannelException(this, cause); } @Override public void close() { super.close(); SimpleMuxableChannel.this.closeAndCatch(this.pipeSource); } @Override public String toString() { return String.format("nested output channel %s%d", this.isRemote() ? "R" : "L", Math.abs(this.channelId)); } } // NestedInfo private record NestedInfo(long channelId, NestedInputFlow inputFlow, NestedOutputFlow outputFlow) { public void startReading() { Optional.ofNullable(this.inputFlow()) .ifPresent(InputFlow::startReading); } public void stopReading() { Optional.ofNullable(this.inputFlow()) .ifPresent(InputFlow::stopReading); } @Override public String toString() { final long channelId = this.channelId(); return String.format("nested channel %s%d", channelId < 0 ? "R" : "L", Math.abs(channelId)); } } // State private enum State { NOT_STARTED, RUNNING, STOPPED; } }