/* * Copyright 2016 higherfrequencytrading.com * * 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.network; import net.openhft.chronicle.bytes.Bytes; import net.openhft.chronicle.core.Jvm; import net.openhft.chronicle.core.Maths; import net.openhft.chronicle.core.OS; import net.openhft.chronicle.core.io.Closeable; import net.openhft.chronicle.core.io.IORuntimeException; import net.openhft.chronicle.core.io.IOTools; import net.openhft.chronicle.core.threads.EventHandler; import net.openhft.chronicle.core.threads.HandlerPriority; import net.openhft.chronicle.core.threads.InvalidEventHandlerException; import net.openhft.chronicle.core.util.Time; import net.openhft.chronicle.network.api.TcpHandler; import net.openhft.chronicle.network.api.session.SessionDetailsProvider; import net.openhft.chronicle.network.connection.TcpChannelHub; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ClosedByInterruptException; import java.nio.channels.ClosedChannelException; import java.nio.channels.SocketChannel; /** * Created by peter.lawrey on 22/01/15. */ public class TcpEventHandler implements EventHandler, Closeable, TcpEventHandlerManager { public static final int TCP_BUFFER = TcpChannelHub.TCP_BUFFER; private static final Logger LOG = LoggerFactory.getLogger(TcpEventHandler.class); @NotNull private final SocketChannel sc; @NotNull private final NetworkContext nc; @NotNull private final SessionDetailsProvider sessionDetails; @NotNull private final WriteEventHandler writeEventHandler; @NotNull private final NetworkLog readLog, writeLog; @NotNull private final Bytes<ByteBuffer> inBBB; @NotNull private final Bytes<ByteBuffer> outBBB; private int oneInTen; private volatile boolean isCleaned; @Nullable private volatile TcpHandler tcpHandler; private long lastTickReadTime = Time.tickTime(); private volatile boolean closed; public TcpEventHandler(@NotNull NetworkContext nc) { this.writeEventHandler = new WriteEventHandler(); this.sc = nc.socketChannel(); this.nc = nc; try { sc.configureBlocking(false); sc.socket().setTcpNoDelay(true); if (TCP_BUFFER >= 64 << 10) { sc.socket().setReceiveBufferSize(TCP_BUFFER); sc.socket().setSendBufferSize(TCP_BUFFER); } } catch (IOException e) { Jvm.warn().on(getClass(), e); } // there is nothing which needs to be written by default. this.sessionDetails = new VanillaSessionDetails(); try { sessionDetails.clientAddress((InetSocketAddress) sc.getRemoteAddress()); } catch (IOException e) { throw new IORuntimeException(e); } // allow these to be used by another thread. // todo check that this can be commented out inBBB = Bytes.elasticByteBuffer(TCP_BUFFER + OS.pageSize()); outBBB = Bytes.elasticByteBuffer(TCP_BUFFER); // must be set after we take a slice(); outBBB.underlyingObject().limit(0); readLog = new NetworkLog(this.sc, "read"); writeLog = new NetworkLog(this.sc, "write"); } @Override public boolean isClosed() { return closed; } @NotNull @Override public HandlerPriority priority() { ServerThreadingStrategy sts = nc.serverThreadingStrategy(); switch (sts) { case SINGLE_THREADED: return HandlerPriority.MEDIUM; case CONCURRENT: return HandlerPriority.CONCURRENT; case MULTI_THREADED_BUSY_WAITING: return HandlerPriority.BLOCKING; default: throw new UnsupportedOperationException("todo"); } } @Override public void tcpHandler(TcpHandler tcpHandler) { nc.onHandlerChanged(tcpHandler); this.tcpHandler = tcpHandler; } @Override public synchronized boolean action() throws InvalidEventHandlerException { final HeartbeatListener heartbeatListener = nc.heartbeatListener(); if (tcpHandler == null) return false; if (!sc.isOpen()) { tcpHandler.onEndOfConnection(false); Closeable.closeQuietly(nc); // clear these to free up memory. throw new InvalidEventHandlerException("socket is closed"); } else if (closed) { Closeable.closeQuietly(nc); throw new InvalidEventHandlerException(); } boolean busy = false; if (oneInTen++ >= 8) { oneInTen = 0; try { busy |= writeEventHandler.action(); } catch (Exception e) { Jvm.warn().on(getClass(), e); } } try { @Nullable ByteBuffer inBB = inBBB.underlyingObject(); int start = inBB.position(); int read = inBB.remaining() > 0 ? sc.read(inBB) : Integer.MAX_VALUE; // if (read < Integer.MAX_VALUE && read != 0) // System.out.println("read " + read); if (read > 0) { WanSimulator.dataRead(read); tcpHandler.onReadTime(System.nanoTime()); lastTickReadTime = Time.tickTime(); // if (Jvm.isDebug()) // System.out.println("Read: " + read + " start: " + start + " pos: " + inBB // .position()); readLog.log(inBB, start, inBB.position()); // inBB.position() where the data has been read() up to. if (invokeHandler()) oneInTen++; return true; } else if (read == 0) { if (outBBB.readRemaining() > 0) { // System.out.println("w " + outBBB.readRemaining()); if (invokeHandler()) return true; } return false; } if (read < 0) { close(); throw new InvalidEventHandlerException("socket closed " + sc); } readLog.idle(); } catch (ClosedChannelException e) { closeSC(); throw new InvalidEventHandlerException(e); } catch (IOException e) { closeSC(); handleIOE(e, tcpHandler.hasClientClosed(), nc.heartbeatListener()); throw new InvalidEventHandlerException(); } catch (InvalidEventHandlerException e) { closeSC(); throw e; } catch (Exception e) { closeSC(); Jvm.warn().on(getClass(), "", e); throw new InvalidEventHandlerException(e); } finally { if (nc.heartbeatTimeoutMs() > 0) { long tickTime = Time.tickTime(); if (tickTime > lastTickReadTime + nc.heartbeatTimeoutMs()) { if (heartbeatListener != null) nc.heartbeatListener().onMissedHeartbeat(); closeSC(); throw new InvalidEventHandlerException("heartbeat timeout"); } } } return busy; } private synchronized void clean() { if (isCleaned) return; isCleaned = true; final long usedDirectMemory = Jvm.usedDirectMemory(); IOTools.clean(inBBB.underlyingObject()); IOTools.clean(outBBB.underlyingObject()); if (usedDirectMemory == Jvm.usedDirectMemory()) Jvm.warn().on(getClass(), "nothing cleaned"); } boolean invokeHandler() throws IOException { boolean busy = false; final int position = inBBB.underlyingObject().position(); inBBB.readLimit(position); outBBB.writePosition(outBBB.underlyingObject().limit()); long lastInBBBReadPosition; do { lastInBBBReadPosition = inBBB.readPosition(); tcpHandler.process(inBBB, outBBB, nc); // did it write something? if (outBBB.writePosition() > outBBB.underlyingObject().limit() || outBBB.writePosition() >= 4) { outBBB.underlyingObject().limit(Maths.toInt32(outBBB.writePosition())); busy |= tryWrite(); break; } } while (lastInBBBReadPosition != inBBB.readPosition()); if (inBBB.readRemaining() == 0) { inBBB.clear(); @Nullable ByteBuffer inBB = inBBB.underlyingObject(); inBB.clear(); } else if (inBBB.readPosition() > 0) { // if it read some data compact(); @Nullable ByteBuffer inBB = inBBB.underlyingObject(); inBB.position((int) inBBB.readPosition()); inBB.limit((int) inBBB.readLimit()); inBB.compact(); inBBB.readPosition(0); inBBB.readLimit(inBB.remaining()); // System.out.println("Compact " + inBBB.readPosition() + " rem " + inBBB.readRemaining() + " " + Wires.lengthOf(inBBB.readInt(inBBB.readPosition()))); busy = true; } else if (inBBB.readPosition() > 0) { System.out.println("pos " + inBBB.readPosition()); } return busy; } private void handleIOE(@NotNull IOException e, final boolean clientIntentionallyClosed, @Nullable HeartbeatListener heartbeatListener) { try { if (clientIntentionallyClosed) return; if (e.getMessage() != null && e.getMessage().startsWith("Connection reset by peer")) LOG.trace("", e.getMessage()); else if (e.getMessage() != null && e.getMessage().startsWith("An existing connection " + "was forcibly closed")) Jvm.debug().on(getClass(), e.getMessage()); else if (!(e instanceof ClosedByInterruptException)) Jvm.warn().on(getClass(), "", e); // The remote server has sent you a RST packet, which indicates an immediate dropping of the connection, // rather than the usual handshake. This bypasses the normal half-closed state transition. // I like this description: "Connection reset by peer" is the TCP/IP equivalent // of slamming the phone back on the hook. if (heartbeatListener != null) heartbeatListener.onMissedHeartbeat(); } finally { closeSC(); } } @Override public void close() { closed = true; closeSC(); clean(); } private void closeSC() { Closeable.closeQuietly(this.nc.networkStatsListener()); Closeable.closeQuietly(tcpHandler); Closeable.closeQuietly(sc); Closeable.closeQuietly(nc); } private boolean tryWrite() throws IOException { if (outBBB.underlyingObject().remaining() <= 0) return false; int start = outBBB.underlyingObject().position(); long writeTickTime = Time.tickTime(); long writeTime = System.nanoTime(); assert !sc.isBlocking(); int wrote = sc.write(outBBB.underlyingObject()); // if (wrote > 200) // System.out.println("w "+wrote); tcpHandler.onWriteTime(writeTime); writeLog.log(outBBB.underlyingObject(), start, outBBB.underlyingObject().position()); if (wrote < 0) { closeSC(); } else if (wrote > 0) { // lastTickReadTime = writeTickTime; outBBB.underlyingObject().compact().flip(); outBBB.writePosition(outBBB.underlyingObject().limit()); return true; } return false; } public static class Factory implements MarshallableFunction<NetworkContext, TcpEventHandler> { public Factory() { } @NotNull @Override public TcpEventHandler apply(@NotNull NetworkContext nc) { return new TcpEventHandler(nc); } } private class WriteEventHandler implements EventHandler { @Override public boolean action() throws InvalidEventHandlerException { if (!sc.isOpen()) throw new InvalidEventHandlerException("socket is closed"); boolean busy = false; try { // get more data to write if the buffer was empty // or we can write some of what is there int remaining = outBBB.underlyingObject().remaining(); busy = remaining > 0; if (busy) tryWrite(); if (outBBB.underlyingObject().remaining() == remaining) { busy |= invokeHandler(); if (!busy) busy = tryWrite(); } } catch (ClosedChannelException cce) { closeSC(); } catch (IOException e) { if (!closed) handleIOE(e, tcpHandler.hasClientClosed(), nc.heartbeatListener()); } return busy; } // public HandlerPriority priority() { // return HandlerPriority.CONCURRENT; //} } }