/* * Copyright (c) 2006-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.socket; import net.jxta.credential.Credential; import net.jxta.document.AdvertisementFactory; import net.jxta.document.MimeMediaType; import net.jxta.document.StructuredDocument; import net.jxta.document.StructuredDocumentFactory; import net.jxta.document.XMLDocument; import net.jxta.endpoint.ByteArrayMessageElement; import net.jxta.endpoint.EndpointAddress; import net.jxta.endpoint.EndpointService; import net.jxta.endpoint.Message; import net.jxta.endpoint.MessageElement; import net.jxta.endpoint.Messenger; import net.jxta.endpoint.StringMessageElement; import net.jxta.endpoint.TextDocumentMessageElement; import net.jxta.id.ID; import net.jxta.id.IDFactory; import net.jxta.impl.util.pipe.reliable.FixedFlowControl; import net.jxta.impl.util.pipe.reliable.Outgoing; import net.jxta.impl.util.pipe.reliable.OutgoingMsgrAdaptor; import net.jxta.impl.util.pipe.reliable.ReliableInputStream; import net.jxta.impl.util.pipe.reliable.ReliableOutputStream; import net.jxta.logging.Logging; import net.jxta.membership.MembershipService; import net.jxta.peer.PeerID; import net.jxta.peergroup.PeerGroup; import net.jxta.peergroup.PeerGroupID; import net.jxta.pipe.InputPipe; import net.jxta.pipe.OutputPipe; import net.jxta.pipe.OutputPipeEvent; import net.jxta.pipe.OutputPipeListener; import net.jxta.pipe.PipeID; import net.jxta.pipe.PipeMsgEvent; import net.jxta.pipe.PipeMsgListener; import net.jxta.pipe.PipeService; import net.jxta.protocol.PeerAdvertisement; import net.jxta.protocol.PipeAdvertisement; import net.jxta.protocol.RouteAdvertisement; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; import java.net.SocketTimeoutException; import java.util.Collections; import java.util.Iterator; import java.util.logging.Level; import java.util.logging.Logger; /** * JxtaSocket is a sub-class of java.net.socket, and should be used like a java.net.Socket. * Key differences to keep in mind are the following : * </p> * - JxtaSocket does not implement Nagle's algorithm, therefore at end of a data frame a flush must invoked to enure all * buffered data is packaged and transmitted. * - JxtaSocket does not implement keep-alive, therefore it is possible the underlaying messengers to be closed due to * lack of inactivity, which manifests in a short latency, while the messenger are recreated. This limitation should cease * to exist as soon the inactivity logic is removed. * */ public class JxtaSocket extends Socket implements PipeMsgListener, OutputPipeListener { /** * Logger */ private final static Logger LOG = Logger.getLogger(JxtaSocket.class.getName()); private final static int MAXRETRYTIMEOUT = 120 * 1000; private final static int DEFAULT_TIMEOUT = 15 * 1000; /** * Default size for output buffers. Only used when we do not know the MTU * size for messengers sending to the remote peer and as an upper bounds * should the MTU size be really huge. */ private final static int DEFAULT_OUTPUT_BUFFER_SIZE = 256 * 1024; /** * If true then this peer initiated the connection. */ private boolean initiator = false; /** * The PeerGroup */ protected PeerGroup group; /** * Pipe Advertisement of the well known pipe. */ protected PipeAdvertisement pipeAdv; /** * Pipe Advertisement of local ephemeral pipe. */ protected PipeAdvertisement localEphemeralPipeAdv; /** * The input pipe for our ephemeral pipe. We will receive all messages on this pipe. */ protected InputPipe localEphemeralPipeIn; /** * Pipe Advertisement of it's ephemeral pipe. */ protected PipeAdvertisement remoteEphemeralPipeAdv; /** * The Messenger we use to */ protected Messenger remoteEphemeralPipeMsgr; protected PipeService pipeSvc; /** * The peer id of the peer we are connecting to or {@code null} if we are * willing to connect to any peer. */ protected PeerID remotePeerID; /** * Used to negotiate connection parameters */ protected OutputPipe connectOutpipe; /** * The timeout of the read() of this socket's input stream (in milliseconds) */ private int soTimeout = 100; /** * timeout for connect and close */ protected long timeout = 60 * 1000; /** * retry timeout in millisecods */ protected int retryTimeout = 60 * 1000; /** * maximum retry timeout allowed */ protected int maxRetryTimeout = MAXRETRYTIMEOUT; /** * retry window size */ protected int windowSize = 20; /** * Lock for output pipe resolution. */ protected final Object pipeResolveLock = new Object(); /** * Lock for ephemeral pipe connect states. */ protected final Object socketConnectLock = new Object(); /** * Lock for closing states. */ protected final Object closeLock = new Object(); /* *used to determine whether to wait for an ack */ private boolean closeAckReceived = false; /** * If {@code true} then this socket has been closed and can no longer be used. */ protected volatile boolean closed = false; /** * If {@code true} then we believer our end of the connection is open. */ protected boolean bound = false; /** * If {@code true} then we believe the remote peer currently has this socket open. */ protected boolean connected = false; /** * Credential of the remote peer. */ protected Credential remoteCredential = null; /** * Our credential that we provide to the remote peer. */ protected Credential localCredential = null; /** * The remote peer advertisement. */ private PeerAdvertisement remotePeerAdv = null; /** * If {@code true} then the socket is a stream socket otherwise it is a datagram socket. */ protected boolean isReliable = true; /** * If {@code true} then the output stream has been shutdown. All attempts * to write to the socket will fail. This socket can no longer be used to * send data though it may remain capable of receiving data. */ private boolean outputShutdown = false; /** * If {@code true} then the input stream has been shutdown. All attempts * to read from the socket will fail. This socket can no longer be used to * receive data though it may remain capable of sending data. */ private boolean inputShutdown = false; /** * Used for sending all messages by the reliable output and input streams. */ protected Outgoing outgoing = null; /** * The reliable input stream we use for receiving data if * {@link #isReliable} is {@code true}. */ protected ReliableInputStream ris = null; /** * The reliable output stream we use for sending data if * {@link #isReliable} is {@code true}. */ protected ReliableOutputStream ros = null; /** * The unreliable input stream we use for receiving data if * {@link #isReliable} is {@code false}. */ protected JxtaSocketInputStream nonReliableInputStream = null; /** * The unreliable output stream we use for sending data if * {@link #isReliable} is {@code false}. */ protected JxtaSocketOutputStream nonReliableOutputStream = null; /** * The size of the output buffers to use. If not set this defaults to the * MTU size of the messenger to the remote peer. */ private int outputBufferSize = -1; /** * This constructor does not establish a connection. Use this constructor * when altering the default parameters, and options of the socket. * <p/> * By default connections are reliable, and the default timeout is 60 * seconds. To alter a connection a call to create(false) changes the * connection to an unreliable one. */ public JxtaSocket() {} /** * This constructor is used by JxtaServer socket for creating JxtaSocket * instances in response to incoming connections. * * @param group group context * @param pipeAdv The original PipeAdvertisement * @param localCredential Our credential. * @param remoteEphemeralPipeAdv the phemeral pipe advertisement * @param remotePeerAdv remote peer advertisement * @param remoteCredential The remote peer's credential. * @param isReliable {@code true} for reliable stream connection or * {@code false} for unreliable stream connection. * @throws IOException if an io error occurs */ protected JxtaSocket(PeerGroup group, PipeAdvertisement pipeAdv, PipeAdvertisement remoteEphemeralPipeAdv, PeerAdvertisement remotePeerAdv, Credential localCredential, Credential remoteCredential, boolean isReliable) throws IOException { this.initiator = false; this.group = group; this.pipeAdv = pipeAdv; this.remoteEphemeralPipeAdv = remoteEphemeralPipeAdv; this.localEphemeralPipeAdv = newEphemeralPipeAdv(pipeAdv); this.remotePeerAdv = remotePeerAdv; this.remotePeerID = remotePeerAdv.getPeerID(); this.localCredential = localCredential; this.remoteCredential = remoteCredential; this.isReliable = isReliable; pipeSvc = group.getPipeService(); this.localEphemeralPipeIn = pipeSvc.createInputPipe(localEphemeralPipeAdv, this); connect(); Message connectResponse = createConnectMessage(group, localEphemeralPipeAdv, localCredential, isReliable, initiator); remoteEphemeralPipeMsgr.sendMessage(connectResponse); if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info("New socket : " + this); } } /** * Create a JxtaSocket connected to the give JxtaSocketAddress. * * @param address JxtaSocketAddress to connect to * @throws IOException if an io error occurs */ public JxtaSocket(SocketAddress address) throws IOException { connect(address, DEFAULT_TIMEOUT); } /** * Create a JxtaSocket to any node listening on pipeAdv * * @param group group context * @param pipeAdv PipeAdvertisement * @throws IOException if an io error occurs */ public JxtaSocket(PeerGroup group, PipeAdvertisement pipeAdv) throws IOException { connect(group, pipeAdv); } /** * Create a JxtaSocket to the given JxtaSocketAddress, within the timeout * specified in milliseconds. * * @param address JxtaSocket address to connect to * @param timeout The number of milliseconds within which the socket must * be successfully created. An exception will be thrown if the socket * cannot be created in the allotted time. A timeout value of {@code 0} * (zero) specifies an infinite timeout. * @throws IOException For failures in creating the socket. * @throws SocketTimeoutException If the socket cannot be created before * the timeout occurs. */ public JxtaSocket(SocketAddress address, int timeout) throws IOException { connect(address, timeout); } /** * Create a JxtaSocket to any peer listening on pipeAdv this attempts * establish a connection to specified pipe within the context of the * specified group within timeout specified in milliseconds. * * @param group group context * @param pipeAdv PipeAdvertisement * @param timeout The number of milliseconds within which the socket must * be successfully created. An exception will be thrown if the socket * cannot be created in the allotted time. A timeout value of {@code 0} * (zero) specifies an infinite timeout. * @throws IOException if an io error occurs * @throws SocketTimeoutException If the socket cannot be created before * the timeout occurs. */ public JxtaSocket(PeerGroup group, PipeAdvertisement pipeAdv, int timeout) throws IOException { connect(group, pipeAdv, timeout); } /** * Create a JxtaSocket to any peer listening on pipeAdv * this attempts establish a connection to specified * pipe within a context of <code>group</code> and within the timeout specified in milliseconds * * @param group group context * @param peerid node to connect to * @param pipeAdv PipeAdvertisement * @param timeout The number of milliseconds within which the socket must * be successfully created. An exception will be thrown if the socket * cannot be created in the allotted time. A timeout value of {@code 0} * (zero) specifies an infinite timeout. * @throws IOException For failures in creating the socket. * @throws SocketTimeoutException If the socket cannot be created before * the timeout occurs. */ public JxtaSocket(PeerGroup group, PeerID peerid, PipeAdvertisement pipeAdv, int timeout) throws IOException { connect(group, peerid, pipeAdv, timeout); } /** * Create a JxtaSocket to the given JxtaSocketAddress, within the timeout * specified in milliseconds. The JxtaSocket can be reliable (stream) or * not (datagram). If you want to use a SocketAddress in the constructor, * this is the preferred method. Either that, or use JxtaSocket(), followed * by create(boolean) to turn on reliability, followed by * connect(SocketAddress, int) or connect(SocketAddress) to make the * connection. * * @param address JxtaSocket address to connect to * @param timeout The number of milliseconds within which the socket must * be successfully created. An exception will be thrown if the socket * cannot be created in the allotted time. A timeout value of {@code 0} * (zero) specifies an infinite timeout. * @param reliable {@code true} for reliable stream connection or * {@code false} for unreliable stream connection. * @throws IOException For failures in creating the socket. * @throws SocketTimeoutException If the socket cannot be created before * the timeout occurs. */ public JxtaSocket(SocketAddress address, int timeout, boolean reliable) throws IOException { this.isReliable = reliable; connect(address, timeout); } /** * Create a JxtaSocket to any peer listening on pipeAdv * this attempts establish a connection to specified * pipe within a context of <code>group</code> and within the timeout specified in milliseconds * * @param group group context * @param peerid node to connect to * @param pipeAdv PipeAdvertisement * @param timeout The number of milliseconds within which the socket must * be successfully created. An exception will be thrown if the socket * cannot be created in the allotted time. A timeout value of {@code 0} * (zero) specifies an infinite timeout. * @param reliable {@code true} for reliable stream connection or * {@code false} for unreliable stream connection. * @throws IOException For failures in creating the socket. * @throws SocketTimeoutException If the socket cannot be created before * the timeout occurs. */ public JxtaSocket(PeerGroup group, PeerID peerid, PipeAdvertisement pipeAdv, int timeout, boolean reliable) throws IOException { this.isReliable = reliable; connect(group, peerid, pipeAdv, timeout); } /** * {@inheritDoc} */ @Override protected void finalize() throws Throwable { if (!closed) { if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.warning("JxtaSocket is being finalized without being previously closed. This is likely a users bug."); } } close(); super.finalize(); } /** * {@inheritDoc} * <p/> * Unsupported operation, an IOException will be thrown. * * @throws IOException Thrown in all cases as this operation is not supported. */ @Override public void bind(SocketAddress address) throws IOException { throw new IOException("Unsupported operation, use java.net.Socket instead"); } /** * {@inheritDoc} * <p/> * The default connect timeout of 60 seconds is used If SocketAddress is not an instance of JxtaSocketAddress, an * IOException will be thrown. */ @Override public void connect(SocketAddress address) throws IOException { connect(address, DEFAULT_TIMEOUT); } /** * {@inheritDoc} * <p/> * If SocketAddress is not an instance of JxtaSocketAddress, an IOException will be thrown. */ @Override public void connect(SocketAddress address, int timeout) throws IOException { if (!(address instanceof JxtaSocketAddress)) { throw new IOException("Subclass of SocketAddress not supported. Use JxtaSocketAddress instead."); } JxtaSocketAddress socketAddress = (JxtaSocketAddress) address; PeerGroup pg = PeerGroup.globalRegistry.lookupInstance(socketAddress.getPeerGroupId()); if (pg == null) { throw new IOException("Can't connect socket in PeerGroup with id " + socketAddress.getPeerGroupId() + ". No running instance of the group is registered."); } connect(pg.getWeakInterface(), socketAddress.getPeerId(), socketAddress.getPipeAdv(), timeout); pg.unref(); } /** * Connects to a JxtaServerSocket on any peer within the default timeout of 60 seconds * * @param group group context * @param pipeAdv PipeAdvertisement * @throws IOException if an io error occurs */ public void connect(PeerGroup group, PipeAdvertisement pipeAdv) throws IOException { connect(group, pipeAdv, DEFAULT_TIMEOUT); } /** * Connects to a JxtaServerSocket on any peer within a timeout specified in milliseconds * * @param group group context * @param pipeAdv PipeAdvertisement * @param timeout in milliseconds * @throws IOException if an io error occurs */ public void connect(PeerGroup group, PipeAdvertisement pipeAdv, int timeout) throws IOException { connect(group, null, pipeAdv, timeout); } /** * Connects to a JxtaServerSocket on a specific peer within a timeout specified in milliseconds * * @param group group context * @param peerid peer to connect to * @param pipeAdv PipeAdvertisement * @param timeout timeout in milliseconds * @throws IOException if an io error occurs */ public void connect(PeerGroup group, PeerID peerid, PipeAdvertisement pipeAdv, int timeout) throws IOException { if (PipeService.PropagateType.equals(pipeAdv.getType())) { throw new IOException("Propagate pipe advertisements are not supported"); } if (timeout < 0) { throw new IllegalArgumentException("timeout may not be negative"); } this.initiator = true; this.group = group; this.remotePeerID = peerid; this.pipeAdv = pipeAdv; if (this.localEphemeralPipeAdv == null) { this.localEphemeralPipeAdv = newEphemeralPipeAdv(pipeAdv); pipeSvc = group.getPipeService(); this.localEphemeralPipeIn = pipeSvc.createInputPipe(localEphemeralPipeAdv, this); } this.timeout = (timeout == 0) ? Long.MAX_VALUE : timeout; Message openMsg = createConnectMessage(group, localEphemeralPipeAdv, localCredential, isReliable, initiator); long connectTimeoutAt = System.currentTimeMillis() + timeout; if (connectTimeoutAt < timeout) { // ensure no overflow connectTimeoutAt = Long.MAX_VALUE; } // Create the output pipe and send this message. Need to retry the call // to createOutputPipe. If there is no rendezvous yet and the // destination is not reachable by mcast, then createOutputPipe has no // effect. We repeat it with exponential delays. if (peerid == null) { pipeSvc.createOutputPipe(pipeAdv, this); } else { pipeSvc.createOutputPipe(pipeAdv, Collections.singleton(peerid), this); } if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.log(Level.FINE, "Beginning Output Pipe Resolution. " + this); } // Wait for the pipe resolution. synchronized (pipeResolveLock) { while (connectOutpipe == null) { try { long waitFor = connectTimeoutAt - System.currentTimeMillis(); if (waitFor <= 0) { // too late break; } if (connectOutpipe == null) { // in case the pipe is resolved pipeResolveLock.wait(waitFor); } } catch (InterruptedException ie) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.log(Level.FINE, "Interrupted", ie); } Thread.interrupted(); SocketException exp = new SocketException("Connect Interrupted"); exp.initCause(ie); throw exp; } } } if (connectOutpipe == null) { throw new SocketTimeoutException("Connection (resolution) timeout"); } try { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.log(Level.FINE, "Sending connect message. " + this); } // send connect message connectOutpipe.send(openMsg); // wait for the connect response. synchronized (socketConnectLock) { while (!isConnected()) { try { long waitFor = connectTimeoutAt - System.currentTimeMillis(); if (waitFor <= 0) { // too late break; } socketConnectLock.wait(waitFor); } catch (InterruptedException ie) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.log(Level.FINE, "Interrupted", ie); } Thread.interrupted(); SocketException exp = new SocketException("Connect Interrupted"); exp.initCause(ie); throw exp; } } } } finally { connectOutpipe.close(); connectOutpipe = null; } if (!isConnected()) { throw new SocketTimeoutException("Connection timeout (connect)"); } if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info("New socket connection : " + this); } // The socket is bound now. setBound(true); } /** * obtain the cred doc from the group object * * @param group the group context * @return The <code>Credential</code> value */ protected static Credential getDefaultCredential(PeerGroup group) { try { MembershipService membership = group.getMembershipService(); return membership.getDefaultCredential(); } catch (Exception e) { if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.log(Level.WARNING, "failed to get credential", e); } } return null; } /** * get the remote credential doc * * @return Credential StructuredDocument */ public Credential getCredentialDoc() { try { return remoteCredential; } catch (Exception failure) { if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.log(Level.WARNING, "failed to generate credential document ", failure); } return null; } } /** * Sets our credential to be used by this socket connection. If no * credentials are set, the default group credential will be used. * * @param localCredential The credential to be used for connection responses * or <tt>null</tt> if the default credential is to be used. */ public void setCredential(Credential localCredential) { if (localCredential == null) { this.localCredential = localCredential; } else { try { MembershipService membership = group.getMembershipService(); this.localCredential = membership.getDefaultCredential(); } catch (Exception failed) { this.localCredential = null; } } } /** * Create a connection request/response message * * @param group The group in which the socket is being used. * @param pipeAdv Advertisement for our ephemeral pipe. * @param credential Our credential or null if default credential is to * be used. * @param isReliable The socket is to be reliable (stream). * @param initiator indicates initiator * @return The message. * @throws IOException if an I/O error occurs */ protected Message createConnectMessage(PeerGroup group, PipeAdvertisement pipeAdv, Credential credential, boolean isReliable, boolean initiator) throws IOException { Message msg = new Message(); if (credential == null) { credential = getDefaultCredential(group); } if ((credential == null) && PipeService.UnicastSecureType.equals(pipeAdv.getType())) { throw new IOException("Credentials must be established to initiate a secure connection."); } if (credential != null) { try { XMLDocument credDoc = (XMLDocument) credential.getDocument(MimeMediaType.XMLUTF8); msg.addMessageElement(JxtaServerSocket.MSG_ELEMENT_NAMESPACE, new TextDocumentMessageElement(JxtaServerSocket.credTag, credDoc, null)); } catch (Exception failed) { IOException failure = new IOException("Could not generate credential element."); failure.initCause(failed); throw failure; } } msg.addMessageElement(JxtaServerSocket.MSG_ELEMENT_NAMESPACE, new TextDocumentMessageElement(initiator ? JxtaServerSocket.reqPipeTag : JxtaServerSocket.remPipeTag, (XMLDocument) pipeAdv.getDocument(MimeMediaType.XMLUTF8), null)); msg.addMessageElement(JxtaServerSocket.MSG_ELEMENT_NAMESPACE, new TextDocumentMessageElement(JxtaServerSocket.remPeerTag, (XMLDocument) group.getPeerAdvertisement().getDocument(MimeMediaType.XMLUTF8), null)); msg.addMessageElement(JxtaServerSocket.MSG_ELEMENT_NAMESPACE, new StringMessageElement(JxtaServerSocket.streamTag, Boolean.toString(isReliable), null)); return msg; } /** * Create a pipe advertisement for an ephemeral pipe (w/random pipe ID) from an existing pipe advertisement. * The specified pipe adveritsement is only used for the name and type * * @param pipeAdv to get the basename and type from * @return A new pipe advertisement for an ephemeral pipe. */ protected static PipeAdvertisement newEphemeralPipeAdv(PipeAdvertisement pipeAdv) { PipeAdvertisement adv = (PipeAdvertisement) AdvertisementFactory.newAdvertisement(PipeAdvertisement.getAdvertisementType()); PeerGroupID gid = (PeerGroupID) ((PipeID) pipeAdv.getPipeID()).getPeerGroupID(); adv.setPipeID(IDFactory.newPipeID(gid)); adv.setName(pipeAdv.getName() + ".remote"); adv.setType(pipeAdv.getType()); return adv; } /** * {@inheritDoc} */ @Override public boolean isBound() { return bound; } /** * Sets whether this socket is currently bound or not. A socket is considered bound if the local resources required * in order to interact with a remote peer are allocated and open. * * @param boundState The new bound state. */ private void setBound(boolean boundState) { bound = boundState; } /** * {@inheritDoc} */ @Override public boolean isConnected() { return connected; } /** * Sets whether this socket is currently connected or not. A socket is * considered connected if is believed that the Socket's remote peer has * the resources required in order to interact with a local peer (us) * allocated and open. * * @param connectedState The new connected state. */ private void setConnected(boolean connectedState) { connected = connectedState; } /** * Create an appropriate Outgoing Adaptor. This method exists primarily * so that sub-classes can substitute a different Outgoing sub-class. * * @param msgr The messenger to be wrapped. * @param timeout The timeout value; * @return Outgoing The messenger wrapped in an appropriate adaptor. */ protected Outgoing makeOutgoing(Messenger msgr, long timeout) { return new OutgoingMsgrAdaptor(msgr, (int) timeout); } /** * Opens the ephemeral output pipe for the remote peer. Also opens the * input and output streams. (delaying adds complexity). * * @throws IOException Thrown for errors in opening resources. */ private void connect() throws IOException { remoteEphemeralPipeMsgr = lightweightOutputPipe(group, remoteEphemeralPipeAdv, remotePeerAdv); if (remoteEphemeralPipeMsgr == null) { throw new IOException("Could not create messenger back to connecting peer"); } // Force the buffer size smaller if user set it too high. if (remoteEphemeralPipeMsgr.getMTU() < outputBufferSize) { outputBufferSize = Math.min((int) remoteEphemeralPipeMsgr.getMTU(), DEFAULT_OUTPUT_BUFFER_SIZE); } if (outputBufferSize == -1) { outputBufferSize = Math.min((int) remoteEphemeralPipeMsgr.getMTU(), DEFAULT_OUTPUT_BUFFER_SIZE); } // Force the creation of the inputStream now. Waiting until someone // calls getInputStream() would likely cause us to drop messages. if (isReliable) { outgoing = makeOutgoing(remoteEphemeralPipeMsgr, retryTimeout); ris = new ReliableInputStream(outgoing, soTimeout); ros = new ReliableOutputStream(outgoing, new FixedFlowControl(windowSize)); try { ros.setSendBufferSize(outputBufferSize); } catch (IOException ignored) {// it's only a preference... } } else { nonReliableInputStream = new JxtaSocketInputStream(this, windowSize); nonReliableOutputStream = new JxtaSocketOutputStream(this, outputBufferSize); } // the socket is now connected! setConnected(true); // The socket is bound now. setBound(true); } /** * {@inheritDoc} */ @Override public InputStream getInputStream() throws IOException { checkState(); if (isInputShutdown()) { throw new SocketException("Input already shutdown."); } if (isReliable) { return ris; } else { return nonReliableInputStream; } } /** * {@inheritDoc} */ @Override public OutputStream getOutputStream() throws IOException { checkState(); if (isOutputShutdown()) { throw new SocketException("Output already shutdown."); } return isReliable ? ros : nonReliableOutputStream; } /** * {@inheritDoc} * <p/> * We hard-close both the input and output streams. Nothing can be * written or read to the socket after this method. Any queued incoming * data is discarded. Any additional incoming messages will be ACKed but * their content will be discarded. We will attempt to send any data which * has already been written to the OutputStream. * <p/> * Once the output queue is empty we will send a close message to tell * the remote side that no more data is coming. * <p/> * This is the only method in this class which is {@code synchronized}. * All others use internal synchronization. */ @Override public synchronized void close() throws IOException { try { synchronized (closeLock) { // ServerSocket has a timeout of zero which is no use... int to = (int)Math.max(DEFAULT_TIMEOUT, timeout); long closeEndsAt = System.currentTimeMillis() + to; if (closeEndsAt < to) { closeEndsAt = Long.MAX_VALUE; } if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info("Closing " + this + " timeout=" + to + "ms."); } if (closed) { return; } closed = true; while (isConnected()) { long closingFor = closeEndsAt - System.currentTimeMillis(); if (closingFor <= 0) { break; } if (isReliable) { try { if (ros.isQueueEmpty()) { // Only send a close if the queue is empty. sendClose(); } else { // Reliable Output Stream not empty. Don't send close yet. ros.waitQueueEmpty(1000); continue; } } catch (InterruptedException woken) { Thread.interrupted(); break; } } else { sendClose(); } if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Sent close, awaiting ACK for " + this); } // Don't send our close too many times. try { long nextTry = Math.min(20000, closingFor); if (nextTry > 0 && isConnected()) { closeLock.wait(nextTry); } } catch (InterruptedException woken) { Thread.interrupted(); break; } } if (isConnected()) { // Last ditch close attempt if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Still connected at end of timeout. Forcing closed." + this); } sendClose(); throw new SocketTimeoutException("Failed to receive close ack from remote connection."); } } } finally { // We must keep the streams open until we've exchanged close request/response // ...now we can close them. shutdownOutput(); shutdownInput(); // No matter what else happens at the end of close() we are no // longer connected and no longer bound. setConnected(false); unbind(); if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info("Socket closed : " + this); } } } /** * This is called when closure is initiated on the remote side. By * convention we receive the close message only after we have ACKed the last * data segment. * <p/> * We soft-close the InputStream which allows us to read data already * received. * <p/> * We hard-close our output stream and discard all queued, unACKed data * as the remote side doesn't want to receive it (the remote side has * already unbound themselves, they just want our close ACK in order to clean * up.) * * @throws IOException if an I/O error occurs */ protected void closeFromRemote() throws IOException { synchronized (closeLock) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.INFO)) { LOG.info("Received a remote close request." + this); } // If we are still bound then send them a close ACK. if (isBound() && (ros != null && ros.isQueueEmpty())) { // do not ack until the queue is empty sendCloseACK(); } if (isConnected()) { setConnected(false); if (isReliable) { ris.softClose(); ros.hardClose(); } else { nonReliableInputStream.softClose(); nonReliableOutputStream.hardClose(); } } if (closeAckReceived) { closeLock.notifyAll(); } } } /** * Closes the input pipe which we use to receive messages and the messenger * used for sending messages. */ protected synchronized void unbind() { if (!isBound()) { return; } if (isReliable) { try { ris.close(); } catch (IOException ignored) {// ignored } ros.hardClose(); } else { nonReliableInputStream.close(); nonReliableOutputStream.hardClose(); } // We are no longer bound setBound(false); // close pipe and messenger if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Closing ephemeral input pipe"); } localEphemeralPipeIn.close(); if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Closing remote ephemeral pipe messenger"); } if(null != outgoing) { outgoing.close(); } remoteEphemeralPipeMsgr.close(); } /** * {@inheritDoc} */ public void pipeMsgEvent(PipeMsgEvent event) { if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) { LOG.log(Level.FINER, "Pipe Message Event for " + this + "\n\t" + event.getMessage() + " for " + event.getPipeID()); } Message message = event.getMessage(); if (message == null) { return; } // look for close request/ack MessageElement element = message.getMessageElement(JxtaServerSocket.MSG_ELEMENT_NAMESPACE, JxtaServerSocket.closeTag); if (element != null) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Handling a close message " + this + " : " + element.toString()); } if (JxtaServerSocket.closeReqValue.equals(element.toString())) { try { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Received a close request"); } closeFromRemote(); } catch (IOException ie) { if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.log(Level.SEVERE, "failed during closeFromRemote", ie); } } } else if (JxtaServerSocket.closeAckValue.equals(element.toString())) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Received a close acknowledgement"); } synchronized (closeLock) { closeAckReceived = true; setConnected(false); closeLock.notifyAll(); } } return; } if (!isConnected()) { // connect response if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.log(Level.FINE, "Processing connect response : " + message); } // look for a remote pipe answer element = message.getMessageElement(JxtaServerSocket.MSG_ELEMENT_NAMESPACE, JxtaServerSocket.remPipeTag); PipeAdvertisement incomingPipeAdv = null; if (element != null) { try { XMLDocument pipeAdvDoc = (XMLDocument) StructuredDocumentFactory.newStructuredDocument(element); incomingPipeAdv = (PipeAdvertisement) AdvertisementFactory.newAdvertisement(pipeAdvDoc); } catch (IOException badPipeAdv) {// ignored } } element = message.getMessageElement(JxtaServerSocket.MSG_ELEMENT_NAMESPACE, JxtaServerSocket.remPeerTag); PeerAdvertisement incomingRemotePeerAdv = null; if (element != null) { try { XMLDocument peerAdvDoc = (XMLDocument) StructuredDocumentFactory.newStructuredDocument(element); incomingRemotePeerAdv = (PeerAdvertisement) AdvertisementFactory.newAdvertisement(peerAdvDoc); } catch (IOException badPeerAdv) {// ignored } } element = message.getMessageElement(JxtaServerSocket.MSG_ELEMENT_NAMESPACE, JxtaServerSocket.credTag); Credential incomingCredential = null; if (element != null) { try { StructuredDocument incomingCredentialDoc = StructuredDocumentFactory.newStructuredDocument(element); incomingCredential = group.getMembershipService().makeCredential(incomingCredentialDoc); } catch (Exception failed) { if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.log(Level.WARNING, "Unable to generate credential for " + this, failed); } } } element = message.getMessageElement(JxtaServerSocket.MSG_ELEMENT_NAMESPACE, JxtaServerSocket.streamTag); boolean incomingIsReliable = isReliable; if (element != null) { incomingIsReliable = Boolean.valueOf(element.toString()); } if ((null != incomingPipeAdv) && (null != incomingRemotePeerAdv)) { if ((null != remotePeerID) && (remotePeerID != incomingRemotePeerAdv.getPeerID())) { // let the connection attempt timeout if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.warning( "Connection response from wrong peer! " + remotePeerID + " != " + incomingRemotePeerAdv.getPeerID()); } return; } synchronized (socketConnectLock) { if (!isConnected()) { remoteCredential = incomingCredential; remotePeerAdv = incomingRemotePeerAdv; remotePeerID = incomingRemotePeerAdv.getPeerID(); remoteEphemeralPipeAdv = incomingPipeAdv; isReliable = incomingIsReliable; // Force the creation of the inputStream now. Waiting until someone // calls getInputStream() would likely cause us to drop messages. // FIXME: it would be even better if we could create the // input stream BEFORE having the output pipe resolved, but // that would force us to have the MsrgAdaptor block // until we can give it the real pipe or msgr... later. try { connect(); } catch (IOException failed) { if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.log(Level.WARNING, "Connection failed : " + this, failed); } return; } socketConnectLock.notify(); if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.log(Level.INFO, "New Socket Connection : " + this); } } } return; } } // Often we are called to handle data before the socket is connected. synchronized (socketConnectLock) { long timeoutAt = System.currentTimeMillis() + timeout; if (timeoutAt < timeout) { timeoutAt = Long.MAX_VALUE; } while (!isClosed() && !isConnected()) { long waitFor = timeoutAt - System.currentTimeMillis(); if (waitFor <= 0) { break; } if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.log(Level.FINE, "Holding " + message + " for " + timeout); } try { socketConnectLock.wait(timeout); } catch (InterruptedException woken) { return; } } } if (!isReliable) { // is there data ? Iterator<MessageElement> dataElements = message.getMessageElements(JxtaServerSocket.MSG_ELEMENT_NAMESPACE, JxtaServerSocket.dataTag); while (dataElements.hasNext()) { MessageElement anElement = dataElements.next(); nonReliableInputStream.enqueue(anElement); } } else { // Give ACKs to the Reliable Output Stream if (ros != null) { ros.recv(message); } // Give data blocks to the Reliable Input Stream if (ris != null) { ris.recv(message); } } } /** * {@inheritDoc} */ public void outputPipeEvent(OutputPipeEvent event) { OutputPipe op = event.getOutputPipe(); if (op.getAdvertisement() == null) { if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.warning("The output pipe has no internal pipe advertisement. discarding event"); } return; } // name can be different, therefore check the id + type if (pipeAdv.getID().equals(op.getAdvertisement().getID()) && pipeAdv.getType().equals(op.getAdvertisement().getType())) { synchronized (pipeResolveLock) { // modify op within lock to prevent a race with the if. if (connectOutpipe == null) { connectOutpipe = op; // if not null, will be closed. op = null; } pipeResolveLock.notify(); } // Ooops one too many, we were too fast re-trying. if (op != null) { op.close(); } } else { if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.warning("Unexpected OutputPipe :" + op); } } } /** * A lightweight output pipe constructor, note the return type * Since all the info needed is available, there's no need for to use the pipe service * to resolve the pipe we have all we need to construct a messenger. * * @param group group context * @param pipeAdv Remote Pipe Advertisement * @param peerAdv Remote Peer Advertisement * @return Messenger */ protected static Messenger lightweightOutputPipe(PeerGroup group, PipeAdvertisement pipeAdv, PeerAdvertisement peerAdv) { EndpointService endpoint = group.getEndpointService(); ID opId = pipeAdv.getPipeID(); String destPeer = peerAdv.getPeerID().getUniqueValue().toString(); // Get an endpoint messenger to that address EndpointAddress addr; RouteAdvertisement routeHint = net.jxta.impl.endpoint.EndpointUtils.extractRouteAdv(peerAdv); if (pipeAdv.getType().equals(PipeService.UnicastType)) { addr = new EndpointAddress("jxta", destPeer, "PipeService", opId.toString()); } else if (pipeAdv.getType().equals(PipeService.UnicastSecureType)) { addr = new EndpointAddress("jxtatls", destPeer, "PipeService", opId.toString()); } else { // not a supported type throw new IllegalArgumentException(pipeAdv.getType() + " is not a supported pipe type"); } if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("New pipe lightweight messenger for " + addr); } return endpoint.getMessenger(addr, routeHint); } /** * Sends a close message * @throws IOException if an io error occurs */ private void sendClose() throws IOException { Message msg = new Message(); msg.addMessageElement(JxtaServerSocket.MSG_ELEMENT_NAMESPACE , new StringMessageElement(JxtaServerSocket.closeTag, JxtaServerSocket.closeReqValue, null)); if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Sending a close request " + this + " : " + msg); } if( ! remoteEphemeralPipeMsgr.sendMessageN(msg, null, null) ){ if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.severe("Failed to send a close request " + this + " : " + msg); } } } /** * Sends a close ack message * @throws IOException if an io error occurs */ private void sendCloseACK() throws IOException { Message msg = new Message(); msg.addMessageElement(JxtaServerSocket.MSG_ELEMENT_NAMESPACE, new StringMessageElement(JxtaServerSocket.closeTag, JxtaServerSocket.closeAckValue, null)); if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Sending a close ACK " + this + " : " + msg); } if( ! remoteEphemeralPipeMsgr.sendMessageN(msg, null, null) ){ if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.severe("Failed to send a close ACK " + this + " : " + msg); } } } /** * {@inheritDoc} */ @Override public int getSoTimeout() throws SocketException { if (isClosed()) { throw new SocketException("Socket is closed"); } if (timeout > Integer.MAX_VALUE) { return 0; } else { return (int) timeout; } } /** * {@inheritDoc} */ @Override public void setSoTimeout(int soTimeout) throws SocketException { if (soTimeout < 0) { throw new IllegalArgumentException("Invalid Socket timeout :" + soTimeout); } this.timeout = soTimeout; if (!isBound()) { return; } // If we are bound then set the timeout on the streams. // FIXME, ros does not define a timeout as it only relies on window saturation, it should take into account // the socket timeout if (isReliable) { if (ris != null) { ris.setTimeout(soTimeout); } } else { nonReliableInputStream.setTimeout((long) soTimeout); } } /** * Gets the Retry Timeout of the reliability layer * * @return The retry Timeout value */ public int getRetryTimeout() { return retryTimeout; } /** * Sets the Retry Timeout of the underlying reliability layer. * In reliable mode it is possible for this call to block * trying to obtain a lock on reliable input stream * * @param retryTimeout The new retry timeout value * @throws SocketException if an I/O error occurs */ public void setRetryTimeout(int retryTimeout) throws SocketException { if (retryTimeout <= 0 || retryTimeout > maxRetryTimeout) { throw new IllegalArgumentException("Invalid Retry Socket timeout :" + retryTimeout); } this.retryTimeout = retryTimeout; if (outgoing != null) { outgoing.setTimeout(retryTimeout); } } /** * When in reliable mode, gets the Reliable library window size * * @return The windowSize value */ public int getWindowSize() { return windowSize; } /** * When in reliable mode, sets the Reliable library window size * * @param windowSize The new window size value * @throws SocketException if an I/O error occurs */ public void setWindowSize(int windowSize) throws SocketException { if (isBound()) { throw new SocketException("Socket bound. Can not change the window size"); } this.windowSize = windowSize; } /** * Returns the closed state of the JxtaSocket. * * @return true if the socket has been closed */ @Override public boolean isClosed() { return closed; } /** * Performs on behalf of JxtaSocketOutputStream. * * @param buf the data. * @param offset the start offset in the data. * @param length the number of bytes to write. * @throws IOException if an I/O error occurs * @see java.io.OutputStream#write */ protected void write(byte[] buf, int offset, int length) throws IOException { checkState(); if (isReliable) { ros.write(buf, offset, length); } else { byte[] bufCopy = new byte[length]; System.arraycopy(buf, offset, bufCopy, 0, length); Message msg = new Message(); msg.addMessageElement(JxtaServerSocket.MSG_ELEMENT_NAMESPACE, new ByteArrayMessageElement(JxtaServerSocket.dataTag, MimeMediaType.AOS, bufCopy, 0, length, null)); remoteEphemeralPipeMsgr.sendMessageB(msg, null, null); } } /** * @throws SocketException if closed, not bound or not connected. */ private void checkState() throws SocketException { if (isClosed()) { throw new SocketException("Socket is closed."); } else if (!isBound()) { throw new SocketException("Socket not bound."); } else if (!isConnected()) { throw new SocketException("Socket not connected."); } } /** * {@inheritDoc} */ @Override public int getSendBufferSize() throws SocketException { if (isOutputShutdown()) { throw new SocketException("Socket is closed"); } return (outputBufferSize == -1) ? DEFAULT_OUTPUT_BUFFER_SIZE : outputBufferSize; } /** * {@inheritDoc} */ @Override public void setSendBufferSize(int size) throws SocketException { if (isOutputShutdown()) { throw new SocketException("Socket is closed"); } if (size < 1) { throw new IllegalArgumentException("negative/zero buffer size"); } if ((null != remoteEphemeralPipeMsgr) && (size > remoteEphemeralPipeMsgr.getMTU())) { throw new IllegalArgumentException("Buffer size larger than limit : " + remoteEphemeralPipeMsgr.getMTU()); } outputBufferSize = size; if (null != ros) { try { ros.setSendBufferSize(size); } catch (SocketException failure) { throw failure; } catch (IOException failed) { SocketException failure = new SocketException("Failed"); failure.initCause(failed); throw failure; } } } /** * {@inheritDoc} */ @Override public int getReceiveBufferSize() throws SocketException { if (isInputShutdown()) { throw new SocketException("Socket is closed"); } // this is just rough size return outputBufferSize * windowSize; } /** * {@inheritDoc} */ @Override public boolean getKeepAlive() throws SocketException { if (inputShutdown) { throw new SocketException("Socket is closed"); } return false; } /** * {@inheritDoc} */ @Override public int getTrafficClass() throws SocketException { throw new SocketException("TrafficClass not yet defined"); } /** * {@inheritDoc} */ @Override public void setTrafficClass(int tc) throws SocketException { // a place holder when and if we decide to add hints regarding // flow info hints such as (IPTOS_LOWCOST (0x02), IPTOS_RELIABILITY (0x04), etc throw new SocketException("TrafficClass not yet defined"); } /** * {@inheritDoc} */ @Override public boolean isInputShutdown() { return inputShutdown; } /** * {@inheritDoc} */ @Override public boolean isOutputShutdown() { return outputShutdown; } /** * {@inheritDoc} */ @Override public void sendUrgentData(int data) throws IOException { throw new SocketException("Urgent data not supported"); } /** * {@inheritDoc} */ @Override public void setOOBInline(boolean state) throws SocketException { throw new SocketException("Enable/disable OOBINLINE supported"); } /** * {@inheritDoc} */ @Override public void setKeepAlive(boolean state) throws SocketException { if (isClosed()) { throw new SocketException("Socket is closed"); } throw new SocketException("Operation not supported"); } /** * {@inheritDoc} */ @Override public void shutdownInput() throws IOException { inputShutdown = true; if (isReliable) { // hard close (EOF on next read) ris.close(); } else { // hard close (EOF on next read) nonReliableInputStream.close(); } } /** * {@inheritDoc} */ @Override public void shutdownOutput() throws IOException { outputShutdown = true; if (isReliable) { ros.setLingerDelay(timeout); // soft close (finish sending if you can) ros.close(); } else { // soft close (finish sending if you can) nonReliableOutputStream.close(); } } /** * {@inheritDoc} */ @Override public SocketAddress getLocalSocketAddress() { if (!isBound()) { return null; } return new JxtaSocketAddress(group, localEphemeralPipeAdv, group.getPeerAdvertisement()); } /** * {@inheritDoc} */ @Override public SocketAddress getRemoteSocketAddress() { if (!isConnected()) { return null; } return new JxtaSocketAddress(group, remoteEphemeralPipeAdv, remotePeerAdv); } /** * {@inheritDoc} * <p/> * This output is suitable for debugging but should not be parsed. All * of the information is available through other means. */ @Override public String toString() { StringBuilder result = new StringBuilder(); result.append(getClass().getName()); result.append('@'); result.append(System.identityHashCode(this)); result.append('['); if (null != pipeAdv) { result.append(pipeAdv.getPipeID().getUniqueValue()); } result.append('/'); if (null != localEphemeralPipeAdv) { result.append(localEphemeralPipeAdv.getPipeID().getUniqueValue()); } result.append(']'); result.append(isClosed() ? " CLOSED :" : " OPEN :"); result.append(initiator ? " I " : " i "); result.append(isReliable ? " R " : " r "); result.append(isBound() ? " B " : " b "); result.append(isConnected() ? " C " : " c "); return result.toString(); } }