/* * Copyright 2016 The Netty Project * * The Netty Project licenses this file to you 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 io.netty.channel.kqueue; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import io.netty.channel.Channel; import io.netty.channel.ChannelConfig; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelMetadata; import io.netty.channel.ChannelOutboundBuffer; import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPromise; import io.netty.channel.ConnectTimeoutException; import io.netty.channel.DefaultFileRegion; import io.netty.channel.EventLoop; import io.netty.channel.FileRegion; import io.netty.channel.socket.DuplexChannel; import io.netty.channel.unix.IovArray; import io.netty.channel.unix.SocketWritableByteChannel; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; import io.netty.util.internal.ThrowableUtil; import io.netty.util.internal.UnstableApi; import java.io.IOException; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.ConnectionPendingException; import java.nio.channels.WritableByteChannel; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import static io.netty.channel.unix.Limits.IOV_MAX; @UnstableApi public abstract class AbstractKQueueStreamChannel extends AbstractKQueueChannel implements DuplexChannel { private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16); private static final ClosedChannelException DO_CLOSE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace( new ClosedChannelException(), AbstractKQueueStreamChannel.class, "doClose()"); private static final String EXPECTED_TYPES = " (expected: " + StringUtil.simpleClassName(ByteBuf.class) + ", " + StringUtil.simpleClassName(DefaultFileRegion.class) + ')'; private ChannelPromise connectPromise; private ScheduledFuture<?> connectTimeoutFuture; private SocketAddress requestedRemoteAddress; private WritableByteChannel byteChannel; AbstractKQueueStreamChannel(Channel parent, BsdSocket fd, boolean active) { super(parent, fd, active, true); } @Override protected AbstractKQueueUnsafe newUnsafe() { return new KQueueStreamUnsafe(); } @Override public ChannelMetadata metadata() { return METADATA; } /** * Write bytes form the given {@link ByteBuf} to the underlying {@link java.nio.channels.Channel}. * @param buf the {@link ByteBuf} from which the bytes should be written */ private boolean writeBytes(ChannelOutboundBuffer in, ByteBuf buf, int writeSpinCount) throws Exception { int readableBytes = buf.readableBytes(); if (readableBytes == 0) { in.remove(); return true; } if (buf.hasMemoryAddress() || buf.nioBufferCount() == 1) { int writtenBytes = doWriteBytes(buf, writeSpinCount); in.removeBytes(writtenBytes); return writtenBytes == readableBytes; } else { ByteBuffer[] nioBuffers = buf.nioBuffers(); return writeBytesMultiple(in, nioBuffers, nioBuffers.length, readableBytes, writeSpinCount); } } private boolean writeBytesMultiple( ChannelOutboundBuffer in, IovArray array, int writeSpinCount) throws IOException { long expectedWrittenBytes = array.size(); final long initialExpectedWrittenBytes = expectedWrittenBytes; int cnt = array.count(); assert expectedWrittenBytes != 0; assert cnt != 0; boolean done = false; int offset = 0; int end = offset + cnt; for (int i = writeSpinCount; i > 0; --i) { long localWrittenBytes = socket.writevAddresses(array.memoryAddress(offset), cnt); if (localWrittenBytes == 0) { break; } expectedWrittenBytes -= localWrittenBytes; if (expectedWrittenBytes == 0) { // Written everything, just break out here (fast-path) done = true; break; } do { long bytes = array.processWritten(offset, localWrittenBytes); if (bytes == -1) { // incomplete write break; } else { offset++; cnt--; localWrittenBytes -= bytes; } } while (offset < end && localWrittenBytes > 0); } in.removeBytes(initialExpectedWrittenBytes - expectedWrittenBytes); return done; } private boolean writeBytesMultiple( ChannelOutboundBuffer in, ByteBuffer[] nioBuffers, int nioBufferCnt, long expectedWrittenBytes, int writeSpinCount) throws IOException { assert expectedWrittenBytes != 0; final long initialExpectedWrittenBytes = expectedWrittenBytes; boolean done = false; int offset = 0; int end = offset + nioBufferCnt; for (int i = writeSpinCount; i > 0; --i) { long localWrittenBytes = socket.writev(nioBuffers, offset, nioBufferCnt); if (localWrittenBytes == 0) { break; } expectedWrittenBytes -= localWrittenBytes; if (expectedWrittenBytes == 0) { // Written everything, just break out here (fast-path) done = true; break; } do { ByteBuffer buffer = nioBuffers[offset]; int pos = buffer.position(); int bytes = buffer.limit() - pos; if (bytes > localWrittenBytes) { buffer.position(pos + (int) localWrittenBytes); // incomplete write break; } else { offset++; nioBufferCnt--; localWrittenBytes -= bytes; } } while (offset < end && localWrittenBytes > 0); } in.removeBytes(initialExpectedWrittenBytes - expectedWrittenBytes); return done; } /** * Write a {@link DefaultFileRegion} * * @param region the {@link DefaultFileRegion} from which the bytes should be written * @return amount the amount of written bytes */ private boolean writeDefaultFileRegion( ChannelOutboundBuffer in, DefaultFileRegion region, int writeSpinCount) throws Exception { final long regionCount = region.count(); if (region.transferred() >= regionCount) { in.remove(); return true; } final long baseOffset = region.position(); boolean done = false; long flushedAmount = 0; for (int i = writeSpinCount; i > 0; --i) { final long offset = region.transferred(); final long localFlushedAmount = socket.sendFile(region, baseOffset, offset, regionCount - offset); if (localFlushedAmount == 0) { break; } flushedAmount += localFlushedAmount; if (region.transferred() >= regionCount) { done = true; break; } } if (flushedAmount > 0) { in.progress(flushedAmount); } if (done) { in.remove(); } return done; } private boolean writeFileRegion( ChannelOutboundBuffer in, FileRegion region, final int writeSpinCount) throws Exception { if (region.transferred() >= region.count()) { in.remove(); return true; } boolean done = false; long flushedAmount = 0; if (byteChannel == null) { byteChannel = new KQueueSocketWritableByteChannel(); } for (int i = writeSpinCount; i > 0; --i) { final long localFlushedAmount = region.transferTo(byteChannel, region.transferred()); if (localFlushedAmount == 0) { break; } flushedAmount += localFlushedAmount; if (region.transferred() >= region.count()) { done = true; break; } } if (flushedAmount > 0) { in.progress(flushedAmount); } if (done) { in.remove(); } return done; } @Override protected void doWrite(ChannelOutboundBuffer in) throws Exception { int writeSpinCount = config().getWriteSpinCount(); for (;;) { final int msgCount = in.size(); if (msgCount == 0) { // Wrote all messages. writeFilter(false); // Return here so we not set the EPOLLOUT flag. return; } // Do gathering write if the outbounf buffer entries start with more than one ByteBuf. if (msgCount > 1 && in.current() instanceof ByteBuf) { if (!doWriteMultiple(in, writeSpinCount)) { // Break the loop and so set EPOLLOUT flag. break; } // We do not break the loop here even if the outbound buffer was flushed completely, // because a user might have triggered another write and flush when we notify his or her // listeners. } else { // msgCount == 1 if (!doWriteSingle(in, writeSpinCount)) { // Break the loop and so set EPOLLOUT flag. break; } } } // Underlying descriptor can not accept all data currently, so set the EPOLLOUT flag to be woken up // when it can accept more data. writeFilter(true); } protected boolean doWriteSingle(ChannelOutboundBuffer in, int writeSpinCount) throws Exception { // The outbound buffer contains only one message or it contains a file region. Object msg = in.current(); if (msg instanceof ByteBuf) { if (!writeBytes(in, (ByteBuf) msg, writeSpinCount)) { // was not able to write everything so break here we will get notified later again once // the network stack can handle more writes. return false; } } else if (msg instanceof DefaultFileRegion) { if (!writeDefaultFileRegion(in, (DefaultFileRegion) msg, writeSpinCount)) { // was not able to write everything so break here we will get notified later again once // the network stack can handle more writes. return false; } } else if (msg instanceof FileRegion) { if (!writeFileRegion(in, (FileRegion) msg, writeSpinCount)) { // was not able to write everything so break here we will get notified later again once // the network stack can handle more writes. return false; } } else { // Should never reach here. throw new Error(); } return true; } private boolean doWriteMultiple(ChannelOutboundBuffer in, int writeSpinCount) throws Exception { if (PlatformDependent.hasUnsafe()) { // this means we can cast to IovArray and write the IovArray directly. IovArray array = ((KQueueEventLoop) eventLoop()).cleanArray(); in.forEachFlushedMessage(array); int cnt = array.count(); if (cnt >= 1) { // TODO: Handle the case where cnt == 1 specially. if (!writeBytesMultiple(in, array, writeSpinCount)) { // was not able to write everything so break here we will get notified later again once // the network stack can handle more writes. return false; } } else { // cnt == 0, which means the outbound buffer contained empty buffers only. in.removeBytes(0); } } else { ByteBuffer[] buffers = in.nioBuffers(); int cnt = in.nioBufferCount(); if (cnt >= 1) { // TODO: Handle the case where cnt == 1 specially. if (!writeBytesMultiple(in, buffers, cnt, in.nioBufferSize(), writeSpinCount)) { // was not able to write everything so break here we will get notified later again once // the network stack can handle more writes. return false; } } else { // cnt == 0, which means the outbound buffer contained empty buffers only. in.removeBytes(0); } } return true; } @Override protected Object filterOutboundMessage(Object msg) { if (msg instanceof ByteBuf) { ByteBuf buf = (ByteBuf) msg; if (!buf.hasMemoryAddress() && (PlatformDependent.hasUnsafe() || !buf.isDirect())) { if (buf instanceof CompositeByteBuf) { // Special handling of CompositeByteBuf to reduce memory copies if some of the Components // in the CompositeByteBuf are backed by a memoryAddress. CompositeByteBuf comp = (CompositeByteBuf) buf; if (!comp.isDirect() || comp.nioBufferCount() > IOV_MAX) { // more then 1024 buffers for gathering writes so just do a memory copy. buf = newDirectBuffer(buf); assert buf.hasMemoryAddress(); } } else { // We can only handle buffers with memory address so we need to copy if a non direct is // passed to write. buf = newDirectBuffer(buf); assert buf.hasMemoryAddress(); } } return buf; } if (msg instanceof FileRegion) { return msg; } throw new UnsupportedOperationException( "unsupported message type: " + StringUtil.simpleClassName(msg) + EXPECTED_TYPES); } private void shutdownOutput0(final ChannelPromise promise) { try { socket.shutdown(false, true); promise.setSuccess(); } catch (Throwable cause) { promise.setFailure(cause); } } private void shutdownInput0(final ChannelPromise promise) { try { socket.shutdown(true, false); promise.setSuccess(); } catch (Throwable cause) { promise.setFailure(cause); } } private void shutdown0(final ChannelPromise promise) { try { socket.shutdown(true, true); promise.setSuccess(); } catch (Throwable cause) { promise.setFailure(cause); } } @Override public boolean isOutputShutdown() { return socket.isOutputShutdown(); } @Override public boolean isInputShutdown() { return socket.isInputShutdown(); } @Override public boolean isShutdown() { return socket.isShutdown(); } @Override public ChannelFuture shutdownOutput() { return shutdownOutput(newPromise()); } @Override public ChannelFuture shutdownOutput(final ChannelPromise promise) { Executor closeExecutor = ((KQueueStreamUnsafe) unsafe()).prepareToClose(); if (closeExecutor != null) { closeExecutor.execute(new Runnable() { @Override public void run() { shutdownOutput0(promise); } }); } else { EventLoop loop = eventLoop(); if (loop.inEventLoop()) { shutdownOutput0(promise); } else { loop.execute(new Runnable() { @Override public void run() { shutdownOutput0(promise); } }); } } return promise; } @Override public ChannelFuture shutdownInput() { return shutdownInput(newPromise()); } @Override public ChannelFuture shutdownInput(final ChannelPromise promise) { Executor closeExecutor = ((KQueueStreamUnsafe) unsafe()).prepareToClose(); if (closeExecutor != null) { closeExecutor.execute(new Runnable() { @Override public void run() { shutdownInput0(promise); } }); } else { EventLoop loop = eventLoop(); if (loop.inEventLoop()) { shutdownInput0(promise); } else { loop.execute(new Runnable() { @Override public void run() { shutdownInput0(promise); } }); } } return promise; } @Override public ChannelFuture shutdown() { return shutdown(newPromise()); } @Override public ChannelFuture shutdown(final ChannelPromise promise) { Executor closeExecutor = ((KQueueStreamUnsafe) unsafe()).prepareToClose(); if (closeExecutor != null) { closeExecutor.execute(new Runnable() { @Override public void run() { shutdown0(promise); } }); } else { EventLoop loop = eventLoop(); if (loop.inEventLoop()) { shutdown0(promise); } else { loop.execute(new Runnable() { @Override public void run() { shutdown0(promise); } }); } } return promise; } @Override protected void doClose() throws Exception { ChannelPromise promise = connectPromise; if (promise != null) { // Use tryFailure() instead of setFailure() to avoid the race against cancel(). promise.tryFailure(DO_CLOSE_CLOSED_CHANNEL_EXCEPTION); connectPromise = null; } ScheduledFuture<?> future = connectTimeoutFuture; if (future != null) { future.cancel(false); connectTimeoutFuture = null; } // Calling super.doClose() first so splceTo(...) will fail on next call. super.doClose(); } /** * Connect to the remote peer */ protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception { if (localAddress != null) { socket.bind(localAddress); } boolean success = false; try { boolean connected = socket.connect(remoteAddress); if (!connected) { writeFilter(true); } success = true; return connected; } finally { if (!success) { doClose(); } } } class KQueueStreamUnsafe extends AbstractKQueueUnsafe { // Overridden here just to be able to access this method from AbstractKQueueStreamChannel @Override protected Executor prepareToClose() { return super.prepareToClose(); } @Override void readReady(final KQueueRecvByteAllocatorHandle allocHandle) { final ChannelConfig config = config(); if (shouldBreakReadReady(config)) { clearReadFilter0(); return; } final ChannelPipeline pipeline = pipeline(); final ByteBufAllocator allocator = config.getAllocator(); allocHandle.reset(config); readReadyBefore(); ByteBuf byteBuf = null; boolean close = false; try { do { // we use a direct buffer here as the native implementations only be able // to handle direct buffers. byteBuf = allocHandle.allocate(allocator); allocHandle.lastBytesRead(doReadBytes(byteBuf)); if (allocHandle.lastBytesRead() <= 0) { // nothing was read, release the buffer. byteBuf.release(); byteBuf = null; close = allocHandle.lastBytesRead() < 0; break; } allocHandle.incMessagesRead(1); readPending = false; pipeline.fireChannelRead(byteBuf); byteBuf = null; if (shouldBreakReadReady(config)) { // We need to do this for two reasons: // // - If the input was shutdown in between (which may be the case when the user did it in the // fireChannelRead(...) method we should not try to read again to not produce any // miss-leading exceptions. // // - If the user closes the channel we need to ensure we not try to read from it again as // the filedescriptor may be re-used already by the OS if the system is handling a lot of // concurrent connections and so needs a lot of filedescriptors. If not do this we risk // reading data from a filedescriptor that belongs to another socket then the socket that // was "wrapped" by this Channel implementation. break; } } while (allocHandle.continueReading()); allocHandle.readComplete(); pipeline.fireChannelReadComplete(); if (close) { shutdownInput(false); } } catch (Throwable t) { handleReadException(pipeline, byteBuf, t, close, allocHandle); } finally { readReadyFinally(config); } } @Override void writeReady() { if (connectPromise != null) { // pending connect which is now complete so handle it. finishConnect(); } else { super.writeReady(); } } @Override public void connect( final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) { if (!promise.setUncancellable() || !ensureOpen(promise)) { return; } try { if (connectPromise != null) { throw new ConnectionPendingException(); } boolean wasActive = isActive(); if (doConnect(remoteAddress, localAddress)) { fulfillConnectPromise(promise, wasActive); } else { connectPromise = promise; requestedRemoteAddress = remoteAddress; // Schedule connect timeout. int connectTimeoutMillis = config().getConnectTimeoutMillis(); if (connectTimeoutMillis > 0) { connectTimeoutFuture = eventLoop().schedule(new Runnable() { @Override public void run() { ChannelPromise connectPromise = AbstractKQueueStreamChannel.this.connectPromise; ConnectTimeoutException cause = new ConnectTimeoutException("connection timed out: " + remoteAddress); if (connectPromise != null && connectPromise.tryFailure(cause)) { close(voidPromise()); } } }, connectTimeoutMillis, TimeUnit.MILLISECONDS); } promise.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (future.isCancelled()) { if (connectTimeoutFuture != null) { connectTimeoutFuture.cancel(false); } connectPromise = null; close(voidPromise()); } } }); } } catch (Throwable t) { closeIfClosed(); promise.tryFailure(annotateConnectException(t, remoteAddress)); } } private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) { if (promise == null) { // Closed via cancellation and the promise has been notified already. return; } active = true; // Get the state as trySuccess() may trigger an ChannelFutureListener that will close the Channel. // We still need to ensure we call fireChannelActive() in this case. boolean active = isActive(); // trySuccess() will return false if a user cancelled the connection attempt. boolean promiseSet = promise.trySuccess(); // Regardless if the connection attempt was cancelled, channelActive() event should be triggered, // because what happened is what happened. if (!wasActive && active) { pipeline().fireChannelActive(); } // If a user cancelled the connection attempt, close the channel, which is followed by channelInactive(). if (!promiseSet) { close(voidPromise()); } } private void fulfillConnectPromise(ChannelPromise promise, Throwable cause) { if (promise == null) { // Closed via cancellation and the promise has been notified already. return; } // Use tryFailure() instead of setFailure() to avoid the race against cancel(). promise.tryFailure(cause); closeIfClosed(); } private void finishConnect() { // Note this method is invoked by the event loop only if the connection attempt was // neither cancelled nor timed out. assert eventLoop().inEventLoop(); boolean connectStillInProgress = false; try { boolean wasActive = isActive(); if (!doFinishConnect()) { connectStillInProgress = true; return; } fulfillConnectPromise(connectPromise, wasActive); } catch (Throwable t) { fulfillConnectPromise(connectPromise, annotateConnectException(t, requestedRemoteAddress)); } finally { if (!connectStillInProgress) { // Check for null as the connectTimeoutFuture is only created if a connectTimeoutMillis > 0 is used // See https://github.com/netty/netty/issues/1770 if (connectTimeoutFuture != null) { connectTimeoutFuture.cancel(false); } connectPromise = null; } } } boolean doFinishConnect() throws Exception { if (socket.finishConnect()) { writeFilter(false); return true; } else { writeFilter(true); return false; } } private void handleReadException(ChannelPipeline pipeline, ByteBuf byteBuf, Throwable cause, boolean close, KQueueRecvByteAllocatorHandle allocHandle) { if (byteBuf != null) { if (byteBuf.isReadable()) { readPending = false; pipeline.fireChannelRead(byteBuf); } else { byteBuf.release(); } } allocHandle.readComplete(); pipeline.fireChannelReadComplete(); pipeline.fireExceptionCaught(cause); if (close || cause instanceof IOException) { shutdownInput(false); } } } private final class KQueueSocketWritableByteChannel extends SocketWritableByteChannel { KQueueSocketWritableByteChannel() { super(socket); } @Override protected ByteBufAllocator alloc() { return AbstractKQueueStreamChannel.this.alloc(); } } }