package com.limegroup.gnutella.connection; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.util.Properties; import java.util.zip.Deflater; import java.util.zip.Inflater; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.limewire.io.CompressingOutputStream; import org.limewire.io.IOUtils; import org.limewire.io.NetworkInstanceUtils; import org.limewire.io.UncompressingInputStream; import org.limewire.net.SocketsManager; import org.limewire.net.SocketsManager.ConnectType; import com.limegroup.gnutella.Acceptor; import com.limegroup.gnutella.NetworkManager; import com.limegroup.gnutella.handshaking.BadHandshakeException; import com.limegroup.gnutella.handshaking.BlockingIncomingHandshaker; import com.limegroup.gnutella.handshaking.BlockingOutgoingHandshaker; import com.limegroup.gnutella.handshaking.HandshakeResponder; import com.limegroup.gnutella.handshaking.Handshaker; import com.limegroup.gnutella.handshaking.NoGnutellaOkException; import com.limegroup.gnutella.messages.BadPacketException; import com.limegroup.gnutella.messages.Message; import com.limegroup.gnutella.messages.MessageFactory; import com.limegroup.gnutella.messages.Message.Network; import com.limegroup.gnutella.messages.vendor.CapabilitiesVMFactory; import com.limegroup.gnutella.messages.vendor.MessagesSupportedVendorMessage; /** * A connection for use with tests that blocks for reading/writing. */ public class BlockingConnection extends AbstractConnection { private static final Log LOG = LogFactory.getLog(BlockingConnection.class); private volatile InputStream _in; private volatile OutputStream _out; /** * The Inflater to use for inflating read streams, initialized in * initialize() if the connection told us it's sending with a * Content-Encoding of deflate. Definitions: Inflater.getTotalOut -- The * number of UNCOMPRESSED bytes Inflater.getTotalIn -- The number of * COMPRESSED bytes */ private volatile Inflater _inflater; /** * The Deflater to use for deflating written streams, initialized in * initialize() if we told the connection we're sending with a * Content-Encoding of deflate. Note that this is the same as '_out', but is * assigned here as the appropriate type so we don't have to cast when we * want to measure the compression savings. Definitions: * Deflater.getTotalOut -- The number of COMPRESSED bytes * Deflater.getTotalIn -- The number of UNCOMPRESSED bytes */ private volatile Deflater _deflater; private final SocketsManager socketsManager; private final MessageFactory messageFactory; /** * Creates an uninitialized outgoing Gnutella 0.6 connection with the * desired outgoing properties that may use TLS. * * @param host the name of the host to connect to * @param port the port of the remote host * @param connectType the type of connection that should be made * @throws <tt>NullPointerException</tt> if any of the arguments are * <tt>null</tt> * @throws <tt>IllegalArgumentException</tt> if the port is invalid */ BlockingConnection(String host, int port, ConnectType connectType, CapabilitiesVMFactory capabilitiesVMFactory, SocketsManager socketsManager, Acceptor acceptor, MessagesSupportedVendorMessage supportedVendorMessage, MessageFactory messageFactory, NetworkManager networkManager, NetworkInstanceUtils networkInstanceUtils) { super(host, port, connectType, capabilitiesVMFactory, supportedVendorMessage, networkManager, acceptor, networkInstanceUtils); this.messageFactory = messageFactory; this.socketsManager = socketsManager; } /** * Creates an uninitialized incoming 0.6 Gnutella connection. If the client * is attempting to connect using an 0.4 handshake, it is rejected. * * @param socket the socket accepted by a ServerSocket. The word "GNUTELLA " * and nothing else must have been read from the socket. * @param responder the headers to be sent in response to the client's * "GNUTELLA CONNECT". * @throws <tt>NullPointerException</tt> if any of the arguments are * <tt>null</tt> */ BlockingConnection(Socket socket, CapabilitiesVMFactory capabilitiesVMFactory, Acceptor acceptor, MessagesSupportedVendorMessage supportedVendorMessage, MessageFactory messageFactory, NetworkManager networkManager, NetworkInstanceUtils networkInstanceUtils) { super(socket, capabilitiesVMFactory, supportedVendorMessage, networkManager, acceptor, networkInstanceUtils); this.socketsManager = null; this.messageFactory = messageFactory; } /** * Initialize the connection by doing the handshake. Throws IOException if * we were unable to establish a normal messaging connection for any reason. * Do not call send or receive if this happens. * * @param timeout for outgoing connections, the timeout in milliseconds to * use in establishing the socket, or 0 for no timeout. If the * platform does not support native timeouts, it will be emulated * with threads. * @exception IOException we were unable to connect to the host * @exception NoGnutellaOkException one of the participants responded with * an error code other than 200 OK (possibly after several rounds * of 401's) * @exception BadHandshakeException some other problem establishing the * connection, e.g., the server responded with HTTP, closed the * the connection during handshaking, etc. */ public void initialize(Properties requestHeaders, HandshakeResponder responder, int timeout) throws IOException, NoGnutellaOkException, BadHandshakeException { if (isOutgoing()) { setSocket(socketsManager.connect(new InetSocketAddress(getAddress(), getPort()), timeout, getConnectType())); } initializeHandshake(); Handshaker shaker = createHandshaker(requestHeaders, responder); try { shaker.shake(); } catch (NoGnutellaOkException e) { setHeaders(shaker.getReadHeaders(), shaker.getWrittenHeaders()); close(); throw e; } catch (IOException e) { setHeaders(shaker.getReadHeaders(), shaker.getWrittenHeaders()); close(); throw new BadHandshakeException(e); } handshakeInitialized(shaker); // wrap the streams with inflater/deflater // These calls must be delayed until absolutely necessary (here) // because the native construction for Deflater & Inflater // allocate buffers outside of Java's memory heap, preventing // Java from fully knowing when/how to GC. The call to end() // (done explicitly in the close() method of this class, and // implicitly in the finalization of the Deflater & Inflater) // releases these buffers. if (isWriteDeflated()) { _deflater = new Deflater(); _out = new CompressingOutputStream(_out, _deflater); } if (isReadDeflated()) { _inflater = new Inflater(); _in = new UncompressingInputStream(_in, _inflater); } getConnectionBandwidthStatistics().setCompressionOption(isWriteDeflated(), isReadDeflated(), new CompressionBandwidthTrackerImpl(_inflater, _deflater)); } /** Constructs the Handshaker object. */ protected Handshaker createHandshaker(Properties requestHeaders, HandshakeResponder responder) throws IOException { try { _in = new BufferedInputStream(getSocket().getInputStream()); _out = new BufferedOutputStream(getSocket().getOutputStream()); if (_in == null) throw new IOException("null input stream"); else if (_out == null) throw new IOException("null output stream"); } catch (Exception e) { // Apparently Socket.getInput/OutputStream throws // NullPointerException if the socket is closed. (See Sun bug // 4091706.) Unfortunately the socket may have been closed after the // the check above, e.g., if the user pressed disconnect. So we // catch NullPointerException here--and any other weird possible // exceptions. An alternative is to obtain a lock before doing these // calls, but we are afraid that getInput/OutputStream may be a // blocking operation. Just to be safe, we also check that in/out // are not null. close(); throw new IOException("could not establish connection"); } if (isOutgoing()) return new BlockingOutgoingHandshaker(requestHeaders, responder, getSocket(), _in, _out); else return new BlockingIncomingHandshaker(responder, getSocket(), _in, _out); } // /////////////////////////////////////////////////////////////////////// /** A tiny allocation optimization; see Message.read(InputStream,byte[]). */ private final byte[] HEADER_BUF = new byte[23]; /** * Receives a message. This method is NOT thread-safe. Behavior is undefined * if two threads are in a receive call at the same time for a given * connection. * * @requires this is fully initialized * @effects exactly like Message.read(), but blocks until a message is * available. A half-completed message results in * InterruptedIOException. */ public Message receive() throws IOException, BadPacketException { // On the Macintosh, sockets *appear* to return the same ping reply // repeatedly if the connection has been closed remotely. This prevents // connections from dying. The following works around the problem. Note // that Message.read may still throw IOException below. // See note on _closed for more information. if (!isOpen()) throw CONNECTION_CLOSED; Message m = null; while (m == null) { m = readAndUpdateStatistics(); } return m; } /* * (non-Javadoc) * * @see com.limegroup.gnutella.Connection#receive(int) */ public Message receive(int timeout) throws IOException, BadPacketException, InterruptedIOException { // See note in receive(). if (!isOpen()) throw CONNECTION_CLOSED; // temporarily change socket timeout. int oldTimeout = getSocket().getSoTimeout(); getSocket().setSoTimeout(timeout); try { Message m = readAndUpdateStatistics(); if (m == null) { throw new InterruptedIOException("null message read"); } return m; } finally { getSocket().setSoTimeout(oldTimeout); } } /** * Reads a message from the network and updates the appropriate statistics. */ private Message readAndUpdateStatistics() throws IOException, BadPacketException { Message msg = messageFactory.read(_in, Network.TCP, HEADER_BUF, getSoftMax(), null); if (msg != null) processReadMessage(msg); return msg; } /** * Optimization -- reuse the header buffer since sending will only be done * on one thread. */ private final byte[] OUT_HEADER_BUF = new byte[23]; /* * (non-Javadoc) * * @see com.limegroup.gnutella.Connection#send(com.limegroup.gnutella.messages.Message) */ public void send(Message m) throws IOException { if (LOG.isTraceEnabled()) LOG.trace("Connection (" + toString() + ") is sending message: " + m); // The try/catch block is necessary for two reasons... // See the notes in Connection.close above the calls // to end() on the Inflater/Deflater and close() // on the Input/OutputStreams for the details. try { m.write(_out, OUT_HEADER_BUF); processWrittenMessage(m); } catch (NullPointerException e) { throw CONNECTION_CLOSED; } } /* * (non-Javadoc) * * @see com.limegroup.gnutella.Connection#flush() */ public void flush() throws IOException { // The try/catch block is necessary for two reasons... // See the notes in Connection.close above the calls // to end() on the Inflater/Deflater and close() // on the Input/OutputStreams for the details. try { _out.flush(); } catch (NullPointerException npe) { throw CONNECTION_CLOSED; } } /* * (non-Javadoc) * * @see com.limegroup.gnutella.Connection#close() */ @Override protected void closeImpl() { IOUtils.close(_deflater); IOUtils.close(_inflater); IOUtils.close(_in); IOUtils.close(_out); } }