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

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

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

package org.dellroad.muxable.simple;

import io.permazen.util.LongEncoder;

import java.io.IOException;
import java.nio.ByteBuffer;

import org.dellroad.muxable.Directions;
import org.slf4j.Logger;

/**
 * Output state machine for the {@link SimpleMuxableChannel} framing protocol.
 *
 * 

* Instances are not thread safe. */ public class ProtocolWriter extends LoggingSupport { private final ChannelIds channelIds; // channel ID tracker private final OutputHandler outputHandler; // how we send bytes to the other side private State state = State.INITIAL; // current state private boolean reentrantHandler; // we are invoking the event handler /** * Constructor. * * @param channelIds channel ID tracker (should be shared with the {@link ProtocolReader}) * @param outputHandler invoked when there are bytes to send * @throws IllegalArgumentException if {@code outputHandler} is null */ public ProtocolWriter(ChannelIds channelIds, OutputHandler outputHandler) { if (channelIds == null) throw new IllegalArgumentException("null channelIds"); if (outputHandler == null) throw new IllegalArgumentException("null outputHandler"); this.channelIds = channelIds; this.outputHandler = outputHandler; } /** * Constructor. * * @param log {@link Logger} to use * @param logPrefix prefix for all log messages, or null for empty string * @param channelIds channel ID tracker (should be shared with the {@link ProtocolReader}) * @param outputHandler invoked when there are bytes to send * @throws IllegalArgumentException if {@code log} or {@code outputHandler} is null */ public ProtocolWriter(Logger log, String logPrefix, ChannelIds channelIds, OutputHandler outputHandler) { super(log, logPrefix); if (channelIds == null) throw new IllegalArgumentException("null channelIds"); if (outputHandler == null) throw new IllegalArgumentException("null outputHandler"); this.channelIds = channelIds; this.outputHandler = outputHandler; } /** * Open a new local nested channel. * *

* Because the channel was created by the local side, the returned channel ID will always be positive. * *

* Any generated output will delivered to the configured {@link OutputHandler} synchronously (in the current thread). * However, this method must not be invoked re-entrantly. * * @param requestData the request data to send to the remote side * @param directions which I/O direction(s) are being established (from the local point of view) * @return the ID for the newly opened channel (always positive) * @throws IOException if thrown by the {@link OutputHandler} * @throws IllegalArgumentException if either parameter is null * @throws IllegalStateException if this method is invoked re-entrantly by the {@link OutputHandler} * @throws IllegalStateException if all 263 channel ID's have already been allocated */ public long openNestedChannel(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.reentrantHandler) throw new IllegalStateException("illegal re-entrant invocation"); // Allocate new local channel ID final long channelId = this.channelIds.allocateLocalChannelId(); // Send channel request to peer this.trace("open new local channel %d", channelId); this.sendData(channelId, directions, requestData); // Done return channelId; } /** * Send data on the nested channel with the specified channel ID. * *

* The specified nested channel may have already been {@linkplain #closeNestedChannel closed} (this can happen * due to race conditions between the local side writing and the remote side closing); if so, no data is actually * sent and this method returns false. * *

* Generated output, if any, will delivered to the configured {@link OutputHandler} synchronously (in the current thread). * However, this method must not be invoked re-entrantly. * * @param channelId encoded channel ID (positive for local channels, negative for remote channels) * @param data the data to send * @return true if data was framed and written to the {@link OutputHandler}, false if the channel has already been closed * @throws IOException if thrown by the {@link OutputHandler} * @throws IllegalArgumentException if {@code channelId} is not a valid channel ID * @throws IllegalArgumentException if {@code data} is null * @throws IllegalStateException if {@link #closeConnection closeConnection()} has been invoked on this instance * @throws IllegalStateException if this method is invoked re-entrantly by the {@link OutputHandler} */ public boolean writeNestedChannel(long channelId, ByteBuffer data) throws IOException { // Sanity check if (data == null) throw new IllegalArgumentException("null data"); if (this.reentrantHandler) throw new IllegalStateException("illegal re-entrant invocation"); if (this.state.equals(State.CLOSED)) throw new IllegalStateException("connection is closed"); // Check whether channel is still open if (!this.channelIds.isChannelOpen(channelId)) return false; // Send data this.trace("write to channel %s%d: %s", channelId < 0 ? "R" : "L", Math.abs(channelId), LoggingSupport.toString(data, 64)); this.sendData(channelId, null, data); return true; } /** * Close an open nested channel. * *

* This sends a "close connection" frame to the peer, unless we know the peer already knows the channel is closed. * *

* Generated output, if any, will delivered to the configured {@link OutputHandler} synchronously (in the current thread). * However, this method must not be invoked re-entrantly. * * @param channelId encoded channel ID (positive for local channels, negative for remote channels) * @return true if a "close connection" frame was written to the {@link OutputHandler}, * false the peer already knows that the channel is closed * @throws IOException if thrown by the {@link OutputHandler} * @throws IllegalArgumentException if {@code channelId} is not the ID of an open nested channel * @throws IllegalStateException if {@link #closeConnection closeConnection()} has been invoked on this instance */ public boolean closeNestedChannel(long channelId) throws IOException { // Sanity check if (this.reentrantHandler) throw new IllegalStateException("illegal re-entrant invocation"); // Deallocate channel; if already deallocated nothing need be done final boolean closed = channelId < 0 ? this.channelIds.freeChannelId(-channelId, false) : this.channelIds.freeChannelId(channelId, true); if (closed) return false; // Send data this.trace("write close channel %s%d", channelId < 0 ? "R" : "L", Math.abs(channelId)); this.sendData(channelId, null, null); return true; } /** * Close the entire connection. * *

* This sends a "close connection" frame to the peer, which closes the overall connection and all nested channels. * *

* Generated output will delivered to the configured {@link OutputHandler} synchronously (in the current thread). * However, this method must not be invoked re-entrantly. * * @throws IOException if thrown by the {@link OutputHandler} * @throws IllegalStateException if this method is invoked re-entrantly by the {@link OutputHandler} * @throws IllegalStateException if this method has already been invoked on this instance */ public void closeConnection() throws IOException { // Sanity check if (this.reentrantHandler) throw new IllegalStateException("illegal re-entrant invocation"); // Write a "close" frame this.trace("write close connection"); this.sendData(0, null, null); this.state = State.CLOSED; } // Internal methods @SuppressWarnings("fallthrough") private void sendData(long channelId, Directions directions, ByteBuffer payload) throws IOException { this.reentrantHandler = true; try { // Build initial header final ByteBuffer header = ByteBuffer.allocate(8 + 3 * LongEncoder.MAX_ENCODED_LENGTH + 1); switch (this.state) { case INITIAL: // send initial greeting header.putLong(ProtocolConstants.PROTOCOL_COOKIE); // big endian value LongEncoder.write(header, ProtocolConstants.CURRENT_PROTOCOL_VERSION); this.state = State.RUNNING; // FALLTHROUGH case RUNNING: // send channel ID and payload length LongEncoder.write(header, channelId); if (directions != null) { final int flags; switch (directions) { case INPUT_ONLY: flags = ProtocolConstants.FLAG_DIRECTION_INPUT; break; case OUTPUT_ONLY: flags = ProtocolConstants.FLAG_DIRECTION_OUTPUT; break; case BIDIRECTIONAL: flags = ProtocolConstants.FLAG_DIRECTION_INPUT | ProtocolConstants.FLAG_DIRECTION_OUTPUT; break; default: throw new RuntimeException("internal error"); } header.put((byte)flags); } LongEncoder.write(header, payload != null ? payload.remaining() : 0); break; case CLOSED: throw new IllegalStateException("connection is closed"); default: throw new RuntimeException("internal error"); } header.flip(); // Send header this.trace("send header %s", LoggingSupport.toString(header, Integer.MAX_VALUE)); this.outputHandler.sendOutput(header); // Send payload if (payload != null && payload.hasRemaining()) { this.trace("send payload %s", LoggingSupport.toString(payload, 64)); this.outputHandler.sendOutput(payload); } } finally { this.reentrantHandler = false; } } // OutputHandler @FunctionalInterface public interface OutputHandler { /** * Send raw data to the remote peer. * * @param data the data to send; this method may take ownership * @throws IOException if an I/O error occurs * @throws IllegalArgumentException if {@code data} is null */ void sendOutput(ByteBuffer data) throws IOException; } // State private enum State { INITIAL, RUNNING, CLOSED; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy