/*
* Copyright (c) 2001-2007 Sun Microsystems, Inc. All rights reserved.
*
* The Sun Project JXTA(TM) Software License
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. The end-user documentation included with the redistribution, if any, must
* include the following acknowledgment: "This product includes software
* developed by Sun Microsystems, Inc. for JXTA(TM) technology."
* Alternately, this acknowledgment may appear in the software itself, if
* and wherever such third-party acknowledgments normally appear.
*
* 4. The names "Sun", "Sun Microsystems, Inc.", "JXTA" and "Project JXTA" must
* not be used to endorse or promote products derived from this software
* without prior written permission. For written permission, please contact
* Project JXTA at http://www.jxta.org.
*
* 5. Products derived from this software may not be called "JXTA", nor may
* "JXTA" appear in their name, without prior written permission of Sun.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SUN
* MICROSYSTEMS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* JXTA is a registered trademark of Sun Microsystems, Inc. in the United
* States and other countries.
*
* Please see the license information page at :
* <http://www.jxta.org/project/www/license.html> for instructions on use of
* the license in source files.
*
* ====================================================================
*
* This software consists of voluntary contributions made by many individuals
* on behalf of Project JXTA. For more information on Project JXTA, please see
* http://www.jxta.org.
*
* This license is based on the BSD license adopted by the Apache Foundation.
*/
package net.jxta.impl.endpoint.tcp;
import net.jxta.document.MimeMediaType;
import net.jxta.endpoint.EndpointAddress;
import net.jxta.endpoint.Message;
import net.jxta.endpoint.MessageElement;
import net.jxta.endpoint.StringMessageElement;
import net.jxta.endpoint.WireFormatMessage;
import net.jxta.endpoint.WireFormatMessageFactory;
import net.jxta.id.ID;
import net.jxta.impl.endpoint.BlockingMessenger;
import net.jxta.impl.endpoint.EndpointServiceImpl;
import net.jxta.impl.endpoint.msgframing.MessagePackageHeader;
import net.jxta.impl.endpoint.msgframing.WelcomeMessage;
import net.jxta.impl.endpoint.transportMeter.TransportBindingMeter;
import net.jxta.impl.endpoint.transportMeter.TransportMeterBuildSettings;
import net.jxta.impl.util.TimeUtils;
import net.jxta.logging.Logging;
import net.jxta.peer.PeerID;
import java.io.EOFException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Implements a messenger which sends messages via raw TCP sockets.
*/
public class TcpMessenger extends BlockingMessenger implements Runnable {
/**
* Logger
*/
private static final Logger LOG = Logger.getLogger(TcpMessenger.class.getName());
/**
* The number of times we allow our write selector to be selected with no
* progress before we give up.
*/
private static final int MAX_WRITE_ATTEMPTS = 3;
/**
* Description of our current location within the stream.
*/
private enum readState {
/**
* Reading the initial welcome
*/
WELCOME,
/**
* Reading a message header
*/
HEADER,
/**
* Reading a message
*/
BODY
}
/**
* The source address of messages sent on this messenger.
*/
private final EndpointAddress srcAddress;
private final MessageElement srcAddressElement;
/**
* Cache of the logical destination of this messenger. (It helps if it works even after close)
*/
private EndpointAddress logicalDestAddress;
/**
* The message tcpTransport we are working for.
*/
private final TcpTransport tcpTransport;
private EndpointAddress dstAddress = null;
private EndpointAddress origAddress = null;
private EndpointAddress fullDstAddress = null;
private InetAddress inetAddress = null;
private int port = 0;
/**
* If {@code true} Then this messenger is closed or in the process of
* closing and can no longer be used.
*/
private AtomicBoolean closed = new AtomicBoolean(false);
private boolean closingDueToFailure = false;
private WelcomeMessage itsWelcome = null;
private final long createdAt = TimeUtils.timeNow();
private long lastUsed = TimeUtils.timeNow();
private SocketChannel socketChannel = null;
private boolean selfDestruct = true;
private TransportBindingMeter transportBindingMeter;
/**
* If this is an incoming connection we must not close it when the messenger
* disappears. It has many reasons to disappear while the connection must
* keep receiving messages. This is causing some problems for incoming
* messengers that are managed by some entity, such as the router or the
* relay. These two do call close explicitly when they discard a messenger,
* and their intent is truly to close the underlying connection. So
* basically we need to distinguish between incoming messengers that are
* abandoned without closing (for these we must protect the input side
* because that's the only reason for the connection being there) and
* incoming messengers that are explicitly closed (in which case we must let
* the entire connection be closed).
*/
private boolean initiator;
private AtomicReference<readState> state = new AtomicReference<readState>(readState.WELCOME);
private final static int MAX_LEN = 4096;
private ByteBuffer buffer = ByteBuffer.allocate(MAX_LEN);
/**
* Header from the current incoming message (if any).
*/
private MessagePackageHeader header = null;
/**
* Time at which we began receiving the current incoming message.
*/
long receiveBeginTime = 0;
/**
* Enforces single writer on message write in case the messenger is being
* used on multiple threads.
*/
private final ReentrantLock writeLock = new ReentrantLock();
/**
* Create a new TcpMessenger for the specified address.
*
* @param socketChannel the SocketChannel for the messenger
* @param transport the tcp MessageSender we are working for.
* @throws java.io.IOException if an io error occurs
*/
TcpMessenger(SocketChannel socketChannel, TcpTransport transport) throws IOException {
super(transport.group.getPeerGroupID(),
new EndpointAddress(transport.getProtocolName(),
socketChannel.socket().getInetAddress().getHostAddress() + ":" + socketChannel.socket().getPort(), null, null), true);
initiator = false;
this.socketChannel = socketChannel;
this.tcpTransport = transport;
this.srcAddress = transport.getPublicAddress();
this.srcAddressElement = new StringMessageElement(EndpointServiceImpl.MESSAGE_SOURCE_NAME, srcAddress.toString(), null);
try {
if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
LOG.info("Connection from " + socketChannel.socket().getInetAddress().getHostAddress() + ":" + socketChannel.socket().getPort());
}
// Set the socket options.
Socket socket = socketChannel.socket();
int useBufferSize = Math.max(TcpTransport.SendBufferSize, socket.getSendBufferSize());
socket.setSendBufferSize(useBufferSize);
inetAddress = socketChannel.socket().getInetAddress();
port = socketChannel.socket().getPort();
socket.setKeepAlive(true);
socket.setSoTimeout(TcpTransport.connectionTimeOut);
// Disable Nagle's algorithm (We do this to reduce latency)
socket.setTcpNoDelay(true);
// Temporarily, our address for inclusion in the welcome message response.
dstAddress = new EndpointAddress(this.tcpTransport.getProtocolName(), inetAddress.getHostAddress() + ":" + port, null, null);
fullDstAddress = dstAddress;
startMessenger();
} catch (IOException io) {
if (TransportMeterBuildSettings.TRANSPORT_METERING) {
transportBindingMeter = this.tcpTransport.getUnicastTransportBindingMeter(null, dstAddress);
if (transportBindingMeter != null) {
transportBindingMeter.connectionFailed(initiator, TimeUtils.timeNow() - createdAt);
}
}
// If we failed for any reason, make sure the socket is closed.
// We're the only one to know about it.
if (socketChannel != null) {
socketChannel.close();
}
throw io;
}
if (TransportMeterBuildSettings.TRANSPORT_METERING) {
transportBindingMeter = this.tcpTransport.getUnicastTransportBindingMeter((PeerID) getDestinationPeerID(), dstAddress);
if (transportBindingMeter != null) {
transportBindingMeter.connectionEstablished(initiator, TimeUtils.timeNow() - createdAt);
}
}
if (!isConnected()) {
throw new IOException("Failed to establish connection to " + dstAddress);
}
}
/**
* Create a new TcpMessenger for the specified address.
*
* @param destaddr the destination of the messenger
* @param tcpTransport the tcp MessageSender we are working for.
* @throws java.io.IOException if an io error occurs
*/
TcpMessenger(EndpointAddress destaddr, TcpTransport tcpTransport) throws IOException {
this(destaddr, tcpTransport, true);
}
/**
* Create a new TcpMessenger for the specified address.
*
* @param destaddr the destination of the messenger
* @param tcpTransport the tcp MessageSender we are working for.
* @param selfDestruct indicates whether the messenger created will self destruct when idle
* @throws java.io.IOException if an io error occurs
*/
TcpMessenger(EndpointAddress destaddr, TcpTransport tcpTransport, boolean selfDestruct) throws IOException {
// We need self destruction: tcp messengers are expensive to make and they refer to
// a connection that must eventually be closed.
super(tcpTransport.group.getPeerGroupID(), destaddr, selfDestruct);
this.selfDestruct = selfDestruct;
this.origAddress = destaddr;
initiator = true;
this.tcpTransport = tcpTransport;
this.fullDstAddress = destaddr;
this.dstAddress = new EndpointAddress(destaddr, null, null);
this.srcAddress = tcpTransport.getPublicAddress();
srcAddressElement = new StringMessageElement(EndpointServiceImpl.MESSAGE_SOURCE_NAME, srcAddress.toString(), null);
String protoAddr = destaddr.getProtocolAddress();
int portIndex = protoAddr.lastIndexOf(":");
if (portIndex == -1) {
throw new IllegalArgumentException("Invalid Protocol Address (port # missing) ");
}
String portString = protoAddr.substring(portIndex + 1);
try {
port = Integer.valueOf(portString);
} catch (NumberFormatException caught) {
throw new IllegalArgumentException("Invalid Protocol Address (port # invalid): " + portString);
}
// Check for bad port number.
if ((port <= 0) || (port > 65535)) {
throw new IllegalArgumentException("Invalid port number in Protocol Address : " + port);
}
String hostString = protoAddr.substring(0, portIndex);
inetAddress = InetAddress.getByName(hostString);
if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
LOG.info("Creating new TCP Connection to : " + dstAddress + " / " + inetAddress.getHostAddress() + ":" + port);
}
// See if we're attempting to use the loopback address.
// And if so, is the peer configured for the loopback network only?
// (otherwise the connection is not permitted). Btw, the otherway around
// is just as wrong, so we check both at once and pretend it cannot work,
// even if it might have.
// FIXME 20041130 This is not an appropriate check if the other peer is
// running on the same machine and the InetAddress.getByName returns the
// loopback address.
// if (inetAddress.isLoopbackAddress() != tcpTransport.usingInterface.isLoopbackAddress()) {
// throw new IOException("Network unreachable--connect to loopback attempted.");
// }
try {
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine("Connecting to " + inetAddress.getHostAddress() + ":" + port + " via "
+ this.tcpTransport.usingInterface.getHostAddress() + ":0");
}
socketChannel = SocketChannel.open();
Socket socket = socketChannel.socket();
// Bind it to our outbound interface.
SocketAddress bindAddress = new InetSocketAddress(this.tcpTransport.usingInterface, 0);
socket.bind(bindAddress);
// Set Socket options.
int useBufferSize = Math.max(TcpTransport.SendBufferSize, socket.getSendBufferSize());
socket.setSendBufferSize(useBufferSize);
useBufferSize = Math.max(TcpTransport.RecvBufferSize, socket.getReceiveBufferSize());
socket.setReceiveBufferSize(useBufferSize);
socket.setKeepAlive(true);
socket.setSoTimeout(TcpTransport.connectionTimeOut);
// Disable Nagle's algorithm (We do this to reduce latency)
socket.setTcpNoDelay(true);
SocketAddress connectAddress = new InetSocketAddress(inetAddress, port);
socketChannel.connect(connectAddress);
startMessenger();
} catch (IOException io) {
if (TransportMeterBuildSettings.TRANSPORT_METERING) {
transportBindingMeter = this.tcpTransport.getUnicastTransportBindingMeter(null, dstAddress);
if (transportBindingMeter != null) {
transportBindingMeter.connectionFailed(initiator, TimeUtils.timeNow() - createdAt);
}
}
// If we failed for any reason, make sure the socket is closed. This is the only place it is known.
if (socketChannel != null) {
socketChannel.close();
}
throw io;
}
if (TransportMeterBuildSettings.TRANSPORT_METERING) {
transportBindingMeter = this.tcpTransport.getUnicastTransportBindingMeter((PeerID) getDestinationPeerID(), dstAddress);
if (transportBindingMeter != null) {
transportBindingMeter.connectionEstablished(initiator, TimeUtils.timeNow() - createdAt);
}
// TODO: We need to add the bytes from the Welcome Messages to the transportBindingMeter, iam@jxta.org
}
if (!isConnected()) {
throw new IOException("Failed to establish connection to " + dstAddress);
}
}
/**
* The cost of just having a finalize routine is high. The finalizer is
* a bottleneck and can delay garbage collection all the way to heap
* exhaustion. Leave this comment as a reminder to future maintainers.
* <p/>
* These messengers are given to application layers. Endpoint code
* always calls {@code close()} when needed.
* <p/>
* There used to be an incoming special case in order to *prevent*
* closure because the inherited finalize used to call close. This is
* no-longer the case. For the outgoing case, we do not need to call close
* for the reason explained above.
* @throws Throwable for errors during finalization.
*/
@Override
protected void finalize() throws Throwable {
try {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Messenger being finalized. closing messenger");
}
closeImpl();
} finally {
super.finalize();
}
}
/**
* {@inheritDoc}
* <p/>
* Now everyone knows its closed and the connection can no-longer be
* obtained. So, we can go about our business of closing it.
*/
public void closeImpl() {
if(!closed.compareAndSet(false, true)) {
return;
}
super.close();
// we are idle now. Way idle.
setLastUsed(0);
if (socketChannel != null) {
// unregister from selector.
tcpTransport.unregister(socketChannel);
try {
socketChannel.close();
} catch (IOException e) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "Failed to close messenger " + toString(), e);
}
}
if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
LOG.info((closingDueToFailure ? "Failure" : "Normal") + " close (open "
+ TimeUtils.toRelativeTimeMillis(TimeUtils.timeNow(), createdAt) + "ms) of socket to : " + dstAddress + " / "
+ inetAddress.getHostAddress() + ":" + port);
if (closingDueToFailure && Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "stack trace", new Throwable("stack trace"));
}
}
}
if (TransportMeterBuildSettings.TRANSPORT_METERING && (transportBindingMeter != null)) {
if (closingDueToFailure) {
transportBindingMeter.connectionDropped(initiator, TimeUtils.timeNow() - createdAt);
} else {
transportBindingMeter.connectionClosed(initiator, TimeUtils.timeNow() - createdAt);
}
}
}
/**
* {@inheritDoc}
*/
public boolean isClosed() {
// FIXME - jice 20040413: Warning. this is overloading the standard
// isClosed(). Things were arranged so that it
// should still work, but it's a stretch. Transports should get a deeper
// retrofit eventually.
if (isConnected()) {
return false;
}
// Ah, this connection is broken. So, we weren't closed, but now
// we are. That could happen redundantly since two threads could
// find that holdIt.isConnected() is false before one of them
// first zeroes conn. But it does not matter. super.close() is
// idempotent (and does pretty much nothing in our case, anyway).
super.close();
return true;
}
/**
* {@inheritDoc}
* <p/>
* Since we probe the connection status, we'll keep a messenger as long
* as the connection is active, even if only on the incoming side.
* So we're being a bit nice to the other side. Anyway, incoming
* connections do not go away when the messenger does. There's a receive
* timeout for that.
*/
public boolean isIdleImpl() {
// With NIO, other than the FD, there's no overhead in keeping the channel open,
return false;
}
/**
* {@inheritDoc}
*/
public EndpointAddress getLogicalDestinationImpl() {
// FIXME 20070127 bondolo THIS IS BEING CALLED BEFORE IT IS INITED.
return logicalDestAddress;
}
/**
* {@inheritDoc}
*/
public void sendMessageBImpl(Message message, String service, String serviceParam) throws IOException {
sendMessageDirect(message, service, serviceParam, false);
}
public void sendMessageDirect(Message message, String service, String serviceParam, boolean direct) throws IOException {
if (isClosed()) {
IOException failure = new IOException("Messenger was closed, it cannot be used to send messages.");
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, failure.getMessage(), failure);
}
throw failure;
}
// Set the message with the appropriate src and dest address
message.replaceMessageElement(EndpointServiceImpl.MESSAGE_SOURCE_NS, srcAddressElement);
EndpointAddress destAddressToUse;
if (direct) {
destAddressToUse = origAddress;
} else {
destAddressToUse = getDestAddressToUse(service, serviceParam);
}
MessageElement dstAddressElement = new StringMessageElement(EndpointServiceImpl.MESSAGE_DESTINATION_NAME, destAddressToUse.toString(), null);
message.replaceMessageElement(EndpointServiceImpl.MESSAGE_DESTINATION_NS, dstAddressElement);
// send it
try {
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine("Sending " + message + " to " + destAddressToUse + " on connection " + getDestinationAddress());
}
xmitMessage(message);
} catch (IOException caught) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "Message send failed for " + message, caught);
}
closeImpl();
throw caught;
}
}
private void startMessenger() throws IOException {
socketChannel.configureBlocking(true);
// Send the welcome message
WelcomeMessage myWelcome = new WelcomeMessage(fullDstAddress,
tcpTransport.getPublicAddress(),
tcpTransport.group.getPeerID(), false);
long written = write(new ByteBuffer[]{myWelcome.getByteBuffer()});
tcpTransport.incrementBytesSent(written);
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine("welcome message sent");
}
while (state.get() == readState.WELCOME) {
if (TimeUtils.toRelativeTimeMillis(TimeUtils.timeNow(), this.createdAt) > (TcpTransport.connectionTimeOut)) {
throw new SocketTimeoutException("Failed to receive remote welcome message before timeout.");
}
read();
processBuffer();
}
if (!closed.get()) {
socketChannel.configureBlocking(false);
tcpTransport.register(socketChannel, this);
}
}
/**
* Send message to the remote peer.
*
* @param msg the message to send.
* @throws java.io.IOException For errors sending the message.
*/
private void xmitMessage(Message msg) throws IOException {
if (closed.get()) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Connection was closed to : " + dstAddress);
}
throw new IOException("Connection was closed to : " + dstAddress);
}
long sendBeginTime = TimeUtils.timeNow();
long size = 0;
try {
// todo 20020730 bondolo@jxta.org Do something with content-coding here
// serialize the message.
WireFormatMessage serialed = WireFormatMessageFactory.toWire(msg, WireFormatMessageFactory.DEFAULT_WIRE_MIME, null);
// Build the package header
MessagePackageHeader header = new MessagePackageHeader();
header.setContentTypeHeader(serialed.getMimeType());
size = serialed.getByteLength();
header.setContentLengthHeader(size);
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine("Sending " + msg + " (" + size + ") to " + dstAddress + " via " + inetAddress.getHostAddress() + ":"+ port);
}
List<ByteBuffer> partBuffers = new ArrayList<ByteBuffer>();
partBuffers.add(header.getByteBuffer());
partBuffers.addAll(Arrays.asList(serialed.getByteBuffers()));
long written;
writeLock.lock();
try {
written = write(partBuffers.toArray(new ByteBuffer[partBuffers.size()]));
} finally {
writeLock.unlock();
}
if (TransportMeterBuildSettings.TRANSPORT_METERING && (transportBindingMeter != null)) {
transportBindingMeter.messageSent(initiator, msg, TimeUtils.timeNow() - sendBeginTime, written);
}
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine(MessageFormat.format("Sent {0} bytes {1} successfully via {2}:{3}", written, msg,
inetAddress.getHostAddress(), port));
}
tcpTransport.incrementBytesSent(written);
tcpTransport.incrementMessagesSent();
setLastUsed(TimeUtils.timeNow());
} catch (SocketTimeoutException failed) {
SocketTimeoutException failure = new SocketTimeoutException("Failed sending " + msg + " to : " + inetAddress.getHostAddress() + ":" + port);
failure.initCause(failed);
throw failure;
} catch (IOException failed) {
if (TransportMeterBuildSettings.TRANSPORT_METERING && (transportBindingMeter != null)) {
transportBindingMeter.sendFailure(initiator, msg, TimeUtils.timeNow() - sendBeginTime, size);
}
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "Message send failed for " + inetAddress.getHostAddress() + ":" + port, failed);
}
closingDueToFailure = true;
close();
IOException failure = new IOException("Failed sending " + msg + " to : " + inetAddress.getHostAddress() + ":" + port);
failure.initCause(failed);
throw failure;
}
}
/**
* Blocking write of byte buffers to the socket channel.
*
* @param byteBuffers The bytes to write.
* @return The number of bytes written.
* @throws IOException Thrown for errors while writing message.
*/
private long write(final ByteBuffer[] byteBuffers) throws IOException {
long nBytes = 0;
for (ByteBuffer byteBuffer : byteBuffers) nBytes += write(byteBuffer);
return nBytes;
}
private long write(final ByteBuffer byteBuffer) throws IOException {
// Determine how many bytes there are to be written in the buffers.
long bytesToWrite = byteBuffer.remaining();
if (bytesToWrite == 0L) {
return 0L;
}
long bytesWritten = 0;
Selector writeSelector = null;
SelectionKey wKey = null;
int attempts = 1;
try {
do {
long wroteBytes;
// Write from the buffers until we write nothing.
do {
wroteBytes = socketChannel.write(byteBuffer);
bytesWritten += wroteBytes;
if (wroteBytes < 0) {
throw new EOFException();
}
if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
LOG.finer(MessageFormat.format("Wrote {0} bytes", wroteBytes));
}
} while (wroteBytes != 0);
// Are we done?
if (bytesWritten == bytesToWrite) {
break;
}
// This attempt failed, we may try again.
attempts++;
if (attempts > MAX_WRITE_ATTEMPTS) {
throw new IOException(MessageFormat.format("Max write attempts ({0}) exceeded ({1})", attempts, MAX_WRITE_ATTEMPTS));
}
// Get a write selector, we're going to do some waiting.
if (writeSelector == null) {
try {
writeSelector = tcpTransport.getSelector();
} catch (InterruptedException woken) {
InterruptedIOException incompleteIO = new InterruptedIOException("Interrupted while acquiring write selector.");
incompleteIO.initCause(woken);
incompleteIO.bytesTransferred = (int) Math.min(bytesWritten, Integer.MAX_VALUE);
throw incompleteIO;
}
if (writeSelector == null) {
continue;
}
wKey = socketChannel.register(writeSelector, SelectionKey.OP_WRITE);
}
long time = TimeUtils.timeNow();
// Wait until we are told we can write again.
int ready = writeSelector.select(TcpTransport.connectionTimeOut);
if (ready == 0 && (TimeUtils.toRelativeTimeMillis(TimeUtils.timeNow(), time) > TcpTransport.connectionTimeOut)) {
throw new SocketTimeoutException("Timeout during socket write");
} else {
attempts--;
}
} while (attempts <= MAX_WRITE_ATTEMPTS);
} finally {
// cancel the key before returning selector to the pool.
if (wKey != null) {
wKey.cancel();
wKey = null;
}
// put the selector back in the pool
if (writeSelector != null) {
// clean up the selector
writeSelector.selectNow();
tcpTransport.returnSelector(writeSelector);
}
}
return bytesWritten;
}
/**
* parses a welcome from a buffer
*
* @param buffer the buffer to parse, if successful the state is set to HEADER
* @return true if successfully parsed
*/
private boolean processWelcome(ByteBuffer buffer) {
try {
if (itsWelcome == null) {
itsWelcome = new WelcomeMessage();
}
if (!itsWelcome.read(buffer)) {
return false;
}
// The correct value for dstAddr: that of the other party.
dstAddress = itsWelcome.getPublicAddress();
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine("Creating a logical address from : " + itsWelcome.getWelcomeString());
}
fullDstAddress = dstAddress;
logicalDestAddress = new EndpointAddress("jxta", itsWelcome.getPeerID().getUniqueValue().toString(), null, null);
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine("Hello from " + itsWelcome.getPublicAddress() + " [" + itsWelcome.getPeerID() + "] ");
}
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine("Registering Messenger from " + socketChannel.socket().getInetAddress().getHostAddress() + ":"+ socketChannel.socket().getPort());
}
try {
// announce this messenger by connection address
tcpTransport.messengerReadyEvent(this, getConnectionAddress());
} catch (Throwable all) {
if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, "Uncaught Throwable in thread :" + Thread.currentThread().getName(), all);
}
IOException failure = new IOException("Failure announcing messenger.");
failure.initCause(all);
throw failure;
}
} catch (IOException e) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "Error while parsing the welcome message", e);
}
closeImpl();
return false;
}
return true;
}
/**
* parses a header from a buffer
*
* @param buffer the buffer to parse, if successful the state is set to BODY
* @return true if successfully parsed
*/
private boolean processHeader(ByteBuffer buffer) {
if (null == header) {
header = new MessagePackageHeader();
}
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine(MessageFormat.format("{0} Processing message package header, buffer stats:{1}", Thread.currentThread(), buffer.toString()));
}
try {
if (!header.readHeader(buffer)) {
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine(MessageFormat.format("{0} maintaining current state at header, buffer stats :{1}", Thread.currentThread(), buffer.toString()));
}
return false;
}
} catch (IOException e) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.FINE, "Error while parsing the message header", e);
}
if (!socketChannel.isConnected()) {
// defunct connection close the messenger
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.warning("SocketChannel closed. Closing the messenger");
}
closeImpl();
}
}
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine(MessageFormat.format("{0} setting current state to body, Buffer stats :{1}, remaining elements {2}:",
Thread.currentThread(), buffer.toString(), buffer.remaining()));
}
return true;
}
private Message processMessage(ByteBuffer buffer, MessagePackageHeader header) throws IOException {
// TODO 20020730 bondolo@jxta.org Do something with content-coding here.
MimeMediaType msgMime = header.getContentTypeHeader();
return WireFormatMessageFactory.fromBuffer(buffer, msgMime, null);
}
/**
* {@inheritDoc}
* <p/>
* This is what gets run by the Executor. It reads whatever is available,
* processes it and then goes back to the selector waiting for more IO
*/
public void run() {
try {
while (read()) {
List<Message> msgs = processBuffer();
for (Message msg : msgs) {
// Use the group's threadpool to process the message
tcpTransport.executor.execute(new MessageProcessor(msg));
}
}
// resets the interestOPS and wakeup the selector
if (socketChannel != null) {
tcpTransport.register(socketChannel, this);
}
} catch (Throwable all) {
if (Logging.SHOW_SEVERE) {
LOG.log(Level.SEVERE, "Uncaught Throwable", all);
}
}
}
/**
* @return true to indicate read maybe required
*/
private boolean read() {
if (closed.get() || socketChannel == null) {
return false;
}
if (!socketChannel.isConnected()) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.warning("SocketChannel is not connected. closing connection");
}
closeImpl();
return false;
}
try {
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine(MessageFormat.format("{0} State before read(): {1}, buffer stats : {2}, remaining :{3}",
Thread.currentThread(), state.get(), buffer.toString(), buffer.remaining()));
}
int read = socketChannel.read(buffer);
if (read < 0) {
if (!socketChannel.isConnected() || read < 0) {
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine(MessageFormat.format("{0} Closing due to EOF", Thread.currentThread()));
}
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.warning("SocketChannel is not connected. closing connection");
}
closeImpl();
}
return false;
} else if (read == 0) {
return false;
}
tcpTransport.incrementBytesReceived(read);
// prepare the buffer for reading
buffer.flip();
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine(MessageFormat.format("{0} SocketChannel.read() == {1} bytes. Buffer stats:{2}, remaining {3}",
Thread.currentThread(), read, buffer.toString(), buffer.remaining()));
}
return true;
} catch (ClosedChannelException e) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "Channel closed while reading data", e);
}
closeImpl();
return false;
} catch (InterruptedIOException woken) {
// Framing is maintained within this object, therefore a read maybe interrupted then resumed
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.warning(MessageFormat.format("tcp receive - interrupted : read() {0} {1}:{2}", woken.bytesTransferred,
inetAddress.getHostAddress(), port));
}
} catch (IOException ioe) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "IOException occured while reading data", ioe);
}
closeImpl();
return false;
} catch (Throwable e) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, MessageFormat.format("tcp receive - Error on connection {0}:{1}", inetAddress.getHostAddress(), port), e);
}
closingDueToFailure = true;
closeImpl();
return false;
}
// if the channel has a valid read ops return true, otherwise false
return (socketChannel.validOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ;
}
/**
* processes the input byte buffer
* @return the list of messages present in the buffer
*/
@SuppressWarnings("fallthrough")
public List<Message> processBuffer() {
List<Message> msgs = new ArrayList<Message>();
boolean done = false;
while (!done) {
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine(MessageFormat.format("{0} processBuffer({1}). Buffer stats:{2}, elements remaining {3}",
Thread.currentThread(), state.getClass(), buffer.toString(), buffer.remaining()));
}
switch (state.get()) {
case WELCOME:
// Parse Welcome message
boolean wseen = processWelcome(buffer);
if (wseen) {
state.set(readState.HEADER);
}
done = true;
break;
case HEADER:
// process the message header
boolean hseen = processHeader(buffer);
if (!hseen) {
done = true;
break;
}
receiveBeginTime = TimeUtils.timeNow();
// todo add a check for MTU size
if (header.getContentLengthHeader() > buffer.capacity()) {
ByteBuffer src = buffer;
// create an array backed buffer
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine(MessageFormat.format("{0} Reallocating a new buffer of size {1} to replace :{2}",
Thread.currentThread(), header.getContentLengthHeader(), buffer.toString()));
}
// This implementation limits the message size to the MTU which is always < 2GB
buffer = ByteBuffer.allocate((int) header.getContentLengthHeader());
buffer.put(src);
buffer.flip();
}
state.set(readState.BODY);
/* FALLSTHROUGH */
case BODY:
// process the message body
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine(MessageFormat.format(" {0} Proccessing Message Body. expecting {1}, {2} elements remaining {3}",
Thread.currentThread(), header.getContentLengthHeader(), buffer.toString(), buffer.remaining()));
}
if (buffer.remaining() >= (int) header.getContentLengthHeader()) {
Message msg;
try {
msg = processMessage(buffer, header);
} catch (IOException io) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "Failed to parse a message from buffer. closing connection", io);
}
closeImpl();
done = true;
break;
}
if (TransportMeterBuildSettings.TRANSPORT_METERING && (transportBindingMeter != null)) {
transportBindingMeter.messageReceived(initiator, msg, TimeUtils.timeNow() - receiveBeginTime,
header.getContentLengthHeader());
}
tcpTransport.incrementMessagesReceived();
setLastUsed(TimeUtils.timeNow());
state.set(readState.HEADER);
header = null;
msgs.add(msg);
} else {
done = true;
break;
}
}
} // while loop
// prepare the buffer for more data
buffer.compact();
return msgs;
}
/**
* A small class for processing individual messages.
*/
private class MessageProcessor implements Runnable {
private Message msg;
MessageProcessor(Message msg) {
this.msg = msg;
}
public void run() {
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine(MessageFormat.format("{0} calling EndpointService.demux({1})",
Thread.currentThread(),
msg, inetAddress.getHostAddress(), port));
}
tcpTransport.endpoint.processIncomingMessage(msg);
}
}
/**
* return the current connection status.
*
* @return true if there is an active connection to the remote peer, otherwise false.
*/
private boolean isConnected() {
return !closed.get();
}
/**
* Return the absolute time in milliseconds at which this Connection was last used.
*
* @return absolute time in milliseconds.
*/
private long getLastUsed() {
return !selfDestruct ? System.currentTimeMillis() : lastUsed;
}
/**
* Set the last used time for this connection in absolute milliseconds.
*
* @param time absolute time in milliseconds.
*/
private void setLastUsed(long time) {
lastUsed = time;
}
/**
* Returns the metering object for this tcpTransport
*
* @return the metering object for this tcpTransport
*/
TransportBindingMeter getTransportBindingMeter() {
return transportBindingMeter;
}
/**
* Returns the remote address
*
* @return the remote address
*/
private EndpointAddress getConnectionAddress() {
// Somewhat confusing but destinationAddress is the name of that thing
// for the welcome message.
return itsWelcome.getDestinationAddress();
}
/**
* Returns Remote PeerID
*
* @return Remote PeerID
*/
private ID getDestinationPeerID() {
return itsWelcome.getPeerID();
}
}