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

net.openhft.chronicle.wire.channel.impl.TCPChronicleChannel Maven / Gradle / Ivy

There is a newer version: 2.27ea1
Show newest version
/*
 * Copyright 2016-2022 chronicle.software
 *
 *       https://chronicle.software
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.openhft.chronicle.wire.channel.impl;

import net.openhft.chronicle.bytes.Bytes;
import net.openhft.chronicle.core.Jvm;
import net.openhft.chronicle.core.io.*;
import net.openhft.chronicle.core.util.Mocker;
import net.openhft.chronicle.threads.PauserMode;
import net.openhft.chronicle.wire.*;
import net.openhft.chronicle.wire.channel.*;
import net.openhft.chronicle.wire.converter.NanoTime;

import java.io.IOException;
import java.net.Socket;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import java.util.function.Function;

import static java.util.Objects.requireNonNull;
import static net.openhft.chronicle.core.io.Closeable.closeQuietly;
import static net.openhft.chronicle.core.io.ClosedIORuntimeException.newIORuntimeException;

/**
 * This is the TCPChronicleChannel class which provides a channel that communicates
 * over TCP and encapsulates the Chronicle logic for networking, with a focus on
 * initialization, input-output buffer management, and header parsing.
 * The class is designed to work both as an initiator and as an acceptor.
 */
@SuppressWarnings("deprecation")
public class TCPChronicleChannel extends AbstractCloseable implements InternalChronicleChannel {

    // Default capacity for the channel buffers
    static final int CAPACITY = Integer.getInteger("tcp.capacity", 2 << 20); // 2 MB
    private static final String HEADER = "header";
    private static final ChannelHeader NO_HEADER = Mocker.ignored(ChannelHeader.class);
    private static final boolean DUMP_YAML = Jvm.getBoolean("dumpYaml");
    private final ReentrantLock lock = new ReentrantLock();
    private final ChronicleChannelCfg channelCfg;
    private final Wire in = createBuffer();
    private final Wire out = createBuffer();
    private final DocumentContextHolder dch = new ConnectionDocumentContextHolder();

    private final Function replaceInHeader;
    private final Function replaceOutHeader;
    private ChronicleContext chronicleContext;
    private SystemContext systemContext;
    private SocketChannel sc;
    private ChannelHeader headerIn;
    private ChannelHeader headerInToUse;
    private ChannelHeader headerOut;
    private long lastTestMessage;
    private SocketRegistry socketRegistry;
    private boolean privateSocketRegistry;
    private boolean endOfData = false;
    private boolean unsentTestMessage = false;
    private int bufferSize = CAPACITY * 2;
    private Consumer closeCallback;

    /**
     * Initiator Constructor for TCPChronicleChannel.
     * Initializes a TCPChronicleChannel with given configurations for acting as an initiator.
     *
     * @param channelCfg       Configuration settings for this channel.
     * @param headerOut        Header to be used for outgoing messages.
     * @param socketRegistry   Registry to manage and provide socket channels.
     * @throws InvalidMarshallableException If the given parameters result in invalid marshalling.
     */
    @SuppressWarnings("this-escape")
    public TCPChronicleChannel(ChronicleChannelCfg channelCfg,
                               ChannelHeader headerOut,
                               SocketRegistry socketRegistry) throws InvalidMarshallableException {
        try {
            this.channelCfg = requireNonNull(channelCfg);
            this.headerOut = requireNonNull(headerOut);
            this.socketRegistry = socketRegistry;
            this.replaceInHeader = null;
            this.replaceOutHeader = null;
            this.sc = null;
            assert channelCfg.initiator();
            checkConnected();
        } catch (Throwable t) {
            close();
            throw t;
        }
    }

    /**
     * Acceptor Constructor for TCPChronicleChannel.
     * Initializes a TCPChronicleChannel with given configurations for acting as an acceptor.
     *
     * @param systemContext    Context for the system in which this channel operates.
     * @param channelCfg       Configuration settings for this channel.
     * @param sc               SocketChannel to which this TCPChronicleChannel corresponds.
     * @param replaceInHeader  Function to replace incoming header.
     * @param replaceOutHeader Function to replace outgoing header.
     */
    @SuppressWarnings("this-escape")
    public TCPChronicleChannel(SystemContext systemContext,
                               ChronicleChannelCfg channelCfg,
                               SocketChannel sc,
                               Function replaceInHeader,
                               Function replaceOutHeader) {
        try {
            this.systemContext = systemContext;
            this.channelCfg = requireNonNull(channelCfg);
            this.sc = requireNonNull(sc);
            this.replaceInHeader = requireNonNull(replaceInHeader);
            this.replaceOutHeader = requireNonNull(replaceOutHeader);

            this.headerOut = null;
            assert !channelCfg.initiator();
        } catch (Throwable t) {
            close();
            throw t;
        }
    }

    /**
     * Validates the header to ensure it adheres to certain conditions.
     * 

* This method performs the following checks: *

    *
  • The header should be non-negative.
  • *
  • The header value should not indicate oversized data.
  • *
  • The header value should not indicate oversized meta-data.
  • *
* * @param header The header value to be validated. * @return True if the header is valid, otherwise exceptions are thrown for invalid conditions. * @throws IllegalStateException if the header is not ready or if it's indicating oversized data or meta-data. */ @SuppressWarnings("SameReturnValue") static boolean validateHeader(int header) { if (header < 0) throw new IllegalStateException("Not ready header " + Integer.toUnsignedString(header, 16)); if (header < 0x4000_0000 && header > 0x20_0000) throw new IllegalStateException("Oversized data header " + Integer.toUnsignedString(header, 16)); if (header > 0x4000_1000) throw new IllegalStateException("Oversized meta-data header " + Integer.toUnsignedString(header, 16)); return true; } @Override public ChronicleChannelCfg channelCfg() { return channelCfg; } /** * Initiates the process to flush the data stored in the 'out' buffer. */ void flush() { flushOut(out); } /** * Flushes out the data stored in the given wire's buffer. * This method writes the data to the associated socket channel until all data is sent. * * @param out The wire containing the data to be flushed out. * @throws IORuntimeException if an error occurs while writing to the socket channel. */ void flushOut(Wire out) { @SuppressWarnings("unchecked") final Bytes bytes = (Bytes) out.bytes(); if (out.bytes().writeRemaining() <= 0) return; ByteBuffer bb = bytes.underlyingObject(); Buffer b = bb; assert bb != null; b.position(Math.toIntExact(bytes.readPosition())); b.limit(Math.toIntExact(bytes.readLimit())); while (bb.remaining() > 0) { int len; try { len = sc.write(bb); } catch (IOException e) { Thread.yield(); if (isClosing()) return; throw newIORuntimeException(e); } if (len < 0) throw new ClosedIORuntimeException("Closed"); } out.clear(); } /** * Creates a buffer to store data with elastic capacity. * * @return A new wire instance with the created buffer. */ private Wire createBuffer() { final Bytes bytes = Bytes.elasticByteBuffer(CAPACITY); IOTools.unmonitor(bytes); bytes.singleThreadedCheckDisabled(true); return WireType.BINARY_LIGHT.apply(bytes); } @Override public DocumentContext readingDocument() throws ClosedIORuntimeException { if (unsentTestMessage && out.writingIsComplete()) testMessage(lastTestMessage); final DocumentContext dc = readingDocument0(); // System.out.println("in - " + Wires.fromSizePrefixedBlobs(dc)); if (dc.isMetaData()) { final Wire wire = dc.wire(); long pos = wire.bytes().readPosition(); final String event = wire.readEvent(String.class); if ("testMessage".equals(event)) { final long testMessage = wire.getValueIn().readLong(NanoTime.INSTANCE); unsentTestMessage = testMessage > lastTestMessage; lastTestMessage = testMessage; } wire.bytes().readPosition(pos); } return dc; } /** * Retrieves a reading document from the wire 'in'. *

* This method checks if the channel is connected and then attempts to retrieve a reading document. * If no document is available, the method will perform various checks and modifications on the buffer * to ensure efficient reading. It will also handle specific protocol conditions and handle exceptions * like detecting an HTTP request or an invalid protocol. * * @return A document context representing the reading document. * @throws ClosedIORuntimeException if the socket channel is closed while attempting to read. * @throws HTTPDetectedException if an HTTP GET request is detected. * @throws InvalidProtocolException if an invalid protocol signature is detected. * @throws IORuntimeException if any other IO error occurs during the reading process. */ private DocumentContext readingDocument0() { checkConnected(); @SuppressWarnings("unchecked") final Bytes bytes = (Bytes) in.bytes(); if (bytes.readRemaining() == 0) bytes.clear(); // Try to retrieve a reading document from 'in' final DocumentContext dc = in.readingDocument(); if (dc.isPresent()) return dc; // return an isPresent = false on an empty buffer once. if (in.bytes().isEmpty() && endOfData) { endOfData = false; return dc; } // Compact the bytes if the read position exceeds certain thresholds if (bytes.readPosition() * 2 > Math.max(CAPACITY / 2, bytes.readLimit())) bytes.compact(); // Set up the byte buffer for reading from the socket channel final ByteBuffer bb = bytes.underlyingObject(); bb.position(Math.toIntExact(bytes.writePosition())); bb.limit(Math.min(bb.capacity(), Math.toIntExact(bytes.writeLimit()))); // Attempt to read from the socket channel int read; try { read = sc.read(bb); } catch (IOException e) { close(); throw newIORuntimeException(e); } // Handle conditions where the channel is closed if (read < 0) { close(); throw new ClosedIORuntimeException("Closed"); } endOfData = true; bytes.writeSkip(read); // Check the header for specific protocol conditions final int header = bytes.readInt(bytes.readPosition()); if (headerOut == NO_HEADER) { // Detect HTTP GET request if (header == 0x20544547) { throw new HTTPDetectedException("Start of request\n" + bytes); } // Detect invalid protocol if (header >> 16 != 0x4000) { throw new InvalidProtocolException("Dump\n" + bytes.toHexString()); } } // Validate the header if enough bytes are remaining assert bytes.readRemaining() < 4 || validateHeader(header); // Dump the content if required if (DUMP_YAML) System.out.println("in - " + Integer.toUnsignedString(header, 16) + "\n" + Wires.fromSizePrefixedBlobs(in)); return in.readingDocument(); } /** * Ensures that the current socket channel is connected. *

* If the socket channel (sc) is not open, the method attempts to establish a connection based * on the host ports provided by the channel configuration (channelCfg). For initiators, it tries to * connect to each host-port combination until a successful connection is established or all attempts fail. * @throws InvalidMarshallableException if any marshalling error occurs. * @throws IllegalStateException if the current state indicates closure. * @throws IllegalArgumentException if an invalid port is detected. * @throws IORuntimeException if all connection attempts fail or for IO issues. */ synchronized void checkConnected() throws InvalidMarshallableException { // Check if socket channel is open and if headerOut is set if (sc != null && sc.isOpen()) { if (headerOut == null) { acceptorRespondToHeader(); } return; } closeQuietly(sc); // Check if in a closing state if (isClosing()) throw new IllegalStateException("Closed"); final Set hostPorts = channelCfg.hostPorts(); // Connection initiation logic for initiators if (channelCfg.initiator()) { boolean success = false; Outer: for (HostPortCfg hp : hostPorts) { // Invalid port check if (hp.port() < -1) throw new IllegalArgumentException("Invalid port " + hp.port() + " connecting to " + hp.hostname()); try { long end = System.nanoTime() + (long) (channelCfg.connectionTimeoutSecs() * 1e9); if (socketRegistry == null) { socketRegistry = new SocketRegistry(); privateSocketRegistry = true; } for (int delay = 1; ; delay++) { try { sc = socketRegistry.createSocketChannel(hp.hostname(), hp.port()); configureSocket(); writeHeader(); readHeader(); success = true; break Outer; } catch (IOException e) { if (System.nanoTime() > end) throw new IORuntimeException("hostport=" + hp, e); Jvm.pause(delay); } } } catch (Exception e) { Jvm.warn().on(getClass(), "failed to connect to host-port=" + hp); } } if (!success) throw new IORuntimeException("failed to connect to any of the following " + hostPorts); } in.clear(); out.clear(); } /** * Configures the current socket channel based on the pauser mode and sets buffer sizes. *

* This method adjusts the blocking mode of the socket channel and also sets the send and receive * buffer sizes for the underlying socket. It calculates the total buffer size based on these values. * * @throws IOException if any IO error occurs during configuration. */ private void configureSocket() throws IOException { // Adjust blocking mode based on pauser mode if (channelCfg.pauserMode() == PauserMode.busy) sc.configureBlocking(false); // Adjust socket buffer sizes final Socket socket = sc.socket(); socket.setReceiveBufferSize(CAPACITY); socket.setSendBufferSize(CAPACITY); bufferSize = socket.getReceiveBufferSize() + socket.getSendBufferSize(); } public void closeCallback(Consumer closeCallback) { this.closeCallback = closeCallback; } @Override protected void performClose() { try { Consumer c = closeCallback; if (c != null) c.accept(this); } catch (Exception e) { Jvm.warn().on(getClass(), e); } Closeable.closeQuietly(sc); if (privateSocketRegistry) Closeable.closeQuietly(socketRegistry); } /** * Synchronized method to send an appropriate response header. *

* The method reads an incoming header and decides the appropriate response header to send. * If a predefined header exists, it uses that, or else it creates a new response header or * a redirection header based on the input. * * @throws InvalidMarshallableException if any marshalling error occurs during header handling. */ synchronized void acceptorRespondToHeader() throws InvalidMarshallableException { headerOut = NO_HEADER; readHeader(); headerInToUse = replaceInHeader.apply(headerIn); final ChannelHeader replyHeader = replaceOutHeader.apply(headerInToUse); // Decide on the appropriate response header to use if (replyHeader == null) { if (headerIn instanceof ChannelHandler) // it's a ChannelHeader headerOut = ((ChannelHandler) headerIn).responseHeader(chronicleContext); else // reject the connection headerOut = new RedirectHeader(Collections.emptyList()); } else { // return the header headerOut = replyHeader; } // Set system context for the response header and write it if (systemContext != null) headerOut.systemContext(systemContext); writeHeader(); } /** * Writes the current response header to the wire. *

* This method writes the {@code headerOut} object to the wire within a document context. * * @throws InvalidMarshallableException if any marshalling error occurs during the write operation. */ private void writeHeader() throws InvalidMarshallableException { try (DocumentContext dc = writingDocument(true)) { dc.wire().write(HEADER).object(headerOut); } out.bytes().singleThreadedCheckReset(); } @Override public ChannelHeader headerOut() { assert headerOut != null; return headerOut; } @Override public ChannelHeader headerIn() { if (headerIn == null) { acceptorRespondToHeader(); } return headerIn; } @Override public ChannelHeader headerInToUse() { if (headerInToUse == null) { acceptorRespondToHeader(); } return headerInToUse; } /** * Reads the incoming header from the wire. *

* This method continuously reads the wire until a valid header is retrieved or the thread is interrupted. * If an unexpected message type is encountered, a warning is issued. * * @throws InvalidMarshallableException if any marshalling error occurs during the read operation. */ private void readHeader() throws InvalidMarshallableException { while (!Thread.currentThread().isInterrupted()) { try (DocumentContext dc = readingDocument()) { if (!dc.isPresent()) { Thread.yield(); continue; } final String s = dc.wire().readEvent(String.class); if (!HEADER.equals(s)) { Jvm.warn().on(getClass(), "Unexpected first message type " + s); } headerIn = dc.wire().getValueIn().object(ChannelHeader.class); break; } } in.bytes().singleThreadedCheckReset(); } @Override public DocumentContext writingDocument(boolean metaData) throws UnrecoverableTimeoutException { checkConnected(); lock.lock(); final DocumentContext dc = out.writingDocument(metaData); dch.documentContext(dc); return dch; } /** * Getter method for the current connection configuration. *

* Returns the {@code channelCfg} object representing the current connection configuration. * * @return The {@code ChronicleChannelCfg} object for the current connection. */ public ChronicleChannelCfg connectionCfg() { return channelCfg; } @Override public void testMessage(long now) { try { try (DocumentContext dc = writingDocument(true)) { dc.wire().write("testMessage").writeLong(NanoTime.INSTANCE, now); } } catch (Exception e) { if (isClosing()) { Jvm.debug().on(getClass(), "Ignoring testMessage exception as it is closing " + e); return; } throw e; } } @Override public long lastTestMessage() { return lastTestMessage; } @Override public DocumentContext acquireWritingDocument(boolean metaData) throws UnrecoverableTimeoutException { checkConnected(); lock.lock(); final DocumentContext dc = out.acquireWritingDocument(metaData); dch.documentContext(dc); return dch; } @Override public boolean supportsEventPoller() { return false; } @Override public EventPoller eventPoller() { return null; } @Override public ChronicleChannel eventPoller(EventPoller eventPoller) { throw new UnsupportedOperationException(); } @Override public Wire acquireProducer() { lock.lock(); return out; } @Override public void releaseProducer() { flush(); lock.unlock(); } /** * Returns the combined size of the send and receive buffers for the socket connection. * * @return The combined buffer size in bytes. */ public int bufferSize() { return bufferSize; } @Override public boolean recordHistory() { if (headerOut instanceof ChannelHandler && ((ChannelHandler) headerOut).recordHistory()) return true; return headerInToUse instanceof ChannelHandler && ((ChannelHandler) headerInToUse).recordHistory(); } /** * Represents a specialized DocumentContextHolder for managing connection-based document context. *

* This holder ensures proper flushing of data upon closure and handles chained elements within a document context. */ private class ConnectionDocumentContextHolder extends DocumentContextHolder implements WriteDocumentContext { private boolean chainedElement; @Override public void close() { super.close(); if (!chainedElement) try { flush(); } catch (ClosedIORuntimeException ignored) { // ignored } lock.unlock(); } @Override public void start(boolean metaData) { } @Override public boolean chainedElement() { return chainedElement; } @Override public void chainedElement(boolean chainedElement) { this.chainedElement = chainedElement; final DocumentContext dc = documentContext(); if (dc instanceof WriteDocumentContext) ((WriteDocumentContext) dc).chainedElement(chainedElement); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy