/*********************************************************************
* DataChannel.java
* created on 10.02.2006 by netseeker
* $Source$
* $Date$
* $Revision$
*
* ====================================================================
*
* Copyright 2006 netseeker aka Michael Manske
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ====================================================================
*
* This file is part of the ejoe framework.
* For more information on the author, please see
* <http://www.manskes.de/>.
*
*********************************************************************/
package de.netseeker.ejoe.io;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.text.ParseException;
import java.util.logging.Level;
import java.util.logging.Logger;
import de.netseeker.ejoe.ConnectionHeader;
import de.netseeker.ejoe.EJConstants;
import de.netseeker.ejoe.cache.ByteBufferAllocator;
/**
* Utility class handling all socket oriented data IO on nio channels. DataChannels must be implemented as singletons to
* avoid creation of a new object for each socket IO operation. Otherwise heavy load could result in fast-growing memory
* consumption.
*
* @author netseeker
* @since 0.3.9.1
*/
public class DataChannel
{
private static final Logger logger = Logger.getLogger( DataChannel.class.getName() );
private static DataChannel dataChannel = new DataChannel();
/**
* Singleton with hidden constructor, only child classes are allowed to construct new instances
*/
protected DataChannel()
{
super();
}
/**
* Invoking this method has the same effect as invoking {@link DataChannel#getInstance(null)}
*
* @return a default instance of DataChannel
*/
public static DataChannel getInstance()
{
return getInstance( null );
}
/**
* Returns appropiate instance of DataChannel for the given connection header. If the header is null an instance of
* this class will be returned.
*
* @param header a valid connection header or null
* @return an instance of DataChannel
*/
public static DataChannel getInstance( ConnectionHeader header )
{
if ( header == null )
{
return dataChannel;
}
else if ( header.isHttp() )
{
return HttpChannel.getInstance();
}
else
{
return DefaultChannel.getInstance();
}
}
/**
* Handshake for a socket channel. It's used as workaround for a know issue with java sockets: Sometimes only the
* first byte will get transferred through a socket connection when reading from it first time. The other bytes will
* follow not until the next read. This method sends/receives one Byte through the socket to "initialize" the socket
* channel. So all following read/write operations don't have to handle that "1-Byte issue". The send/received Byte
* is used also as connection header, it contains information about compression, nio usage, if the connection is a
* persistent or non-persistent one...
*
* @param sendBeforeReceive if true we will try to send one byte then read one byte otherwise we will use the
* opposite way around.
* @throws IOException
*/
public ConnectionHeader handshake( final ConnectionHeader header, SocketChannel channel, long timeout )
throws IOException, ParseException
{
boolean isHttp = header.isHttp();
ConnectionHeader receiverHeader = null;
ByteBuffer magicBuf = null;
byte[] preReadData = null;
if ( !header.isClient() )
{
// expect to read the first four bytes for determining the used protocol
magicBuf = ByteBufferAllocator.allocate( 4, false );
semiBlockingRead( channel, magicBuf, timeout );
// at least the handshake must be read in one operation
// if not prevent us from dealing with bad networks or crappy clients
if ( magicBuf.hasRemaining() ) return null;
magicBuf.flip();
// copy the read four byte into a buffer - we will need them again maybe
preReadData = new byte[4];
magicBuf.get( preReadData );
magicBuf.rewind();
// read the first four bytes as int
int magicNo = magicBuf.getInt();
isHttp = (magicNo != EJConstants.EJOE_MAGIC_NUMBER);
}
try
{
// seems like the usual EJOE protocol
if ( !isHttp )
{
// complete the handshake using our DefaultChannel, we can skip the preread four bytes
receiverHeader = DefaultChannel.getInstance().handshake( header, channel, timeout );
}
// seems like HTTP protocol
else
{
// complete the handshake using a HttpChannel and hand over the preread four bytes
receiverHeader = ((HttpChannel) HttpChannel.getInstance()).handshake( header, channel, preReadData,
timeout );
}
// copy the host into the received client header
if ( receiverHeader.isClient() && receiverHeader.getHost() == null )
{
receiverHeader.setHost( header.getHost() );
}
// copy the adapter into the returned server header
if ( header.isClient() && header.hasAdapter() && receiverHeader.getAdapterName() == null )
{
receiverHeader.setAdapterName( header.getAdapterName() );
}
}
catch ( IncompleteIOException ioe )
{
// nothing to do
// at least the handshake must be read in one operation
// if not prevent us from dealing with bad networks or crappy clients
}
return receiverHeader;
}
/**
* Tries to send the given ByteBuffer completely through the given SocketChannel three times
*
* @param channel
* @param buffer
* @throws IncompleteIOException if the given ByteBuffer could not be send completely
* @throws IOException
*/
public void nonBlockingWrite( WritableByteChannel channel, ByteBuffer buffer ) throws IOException
{
int runs = 0;
do
{
channel.write( buffer );
runs++;
}
while ( buffer.hasRemaining() && runs < EJConstants.NIO_MAX_ITERATIONS );
if ( buffer.hasRemaining() )
{
logger.log( Level.FINEST, "Incomplete write detected, registering for write again." );
buffer.compact();
throw new IncompleteIOException( buffer, SelectionKey.OP_WRITE );
}
}
/**
* Tries to send the given ByteBuffer completely through the given SocketChannel within a given timeout
*
* @param channel
* @param buffer
* @param timeout
* @throws IncompleteIOException if the given ByteBuffer could not be send completely
* @throws IOException
*/
public void semiBlockingWrite( WritableByteChannel channel, ByteBuffer buffer, long timeout ) throws IOException
{
long timestamp = System.currentTimeMillis();
long timePeriod = -1;
do
{
channel.write( buffer );
timePeriod = System.currentTimeMillis() - timestamp;
}
while ( buffer.hasRemaining() && (timePeriod < timeout) );
if ( timePeriod >= timeout )
{
throw new SocketTimeoutException();
}
if ( buffer.hasRemaining() )
{
logger.log( Level.FINEST, "Incomplete write detected, registering for write again." );
buffer.compact();
throw new IncompleteIOException( buffer, SelectionKey.OP_WRITE );
}
}
/**
* Tries to send the given ByteBuffer completely through the given SocketChannel within a given timeout
*
* @param channel
* @param buffer
* @return
* @throws IOException
*/
public static void nonBlockingRead( ReadableByteChannel channel, ByteBuffer buffer ) throws IOException
{
int read = 0, runs = 0;
try
{
do
{
read = channel.read( buffer );
runs++;
}
// read until end of data is reached (-1) or the buffer is full or we tried to read for
// EJConstants.NIO_MAX_ITERATIONS
while ( read != -1 && buffer.hasRemaining() && runs < EJConstants.NIO_MAX_ITERATIONS );
}
catch ( IOException e )
{
// most likely the sender did close the connection or something other (firewall?) does interfere the
// communication
ClosedChannelException che = new ClosedChannelException();
che.setStackTrace( e.getStackTrace() );
che.initCause( e.getCause() );
throw che;
}
if ( buffer.hasRemaining() )
{
if ( read == -1 )
{
throw new ClosedChannelException();
}
else
{
logger.log( Level.FINEST, "Incomplete read detected, registering for read again." );
throw new IncompleteIOException( buffer, SelectionKey.OP_READ );
}
}
}
/**
* Tries to read ByteBuffer.remaining() bytes the into given ByteBuffer from the given SocketChannel within a given
* timeout.
*
* @param channel
* @param buffer
* @param timeout
* @return
* @throws IOException
*/
public static void semiBlockingRead( ReadableByteChannel channel, ByteBuffer buffer, long timeout )
throws IOException
{
long timestamp = System.currentTimeMillis();
long timePeriod = -1;
int read = 0;
try
{
do
{
read = channel.read( buffer );
timePeriod = System.currentTimeMillis() - timestamp;
}
while ( read != -1 && buffer.hasRemaining() && (timePeriod < timeout) );
}
catch ( IOException e )
{
// most likely the sender did close the connection
ClosedChannelException che = new ClosedChannelException();
che.setStackTrace( e.getStackTrace() );
che.initCause( e.getCause() );
throw che;
}
if ( timePeriod >= timeout )
{
throw new SocketTimeoutException();
}
if ( buffer.hasRemaining() )
{
if ( read == -1 )
{
throw new ClosedChannelException();
}
else
{
logger.log( Level.FINEST, "Incomplete read detected, registering for read again." );
throw new IncompleteIOException( buffer, SelectionKey.OP_READ );
}
}
}
/**
* Receives a EJOE specific header containing the size of the next ByteBuffer.
*
* @param timeout read timeout
* @return the length of the following data package
* @throws IOException
*/
public int readHeader( ConnectionHeader header, long timeout ) throws IOException
{
throw new UnsupportedOperationException();
}
/**
* Sends a EJOE specific header containing the lengh of the given ByteBuffer
*
* @param timeout write timeout
* @throws IOException
*/
public void writeHeader( ConnectionHeader header, ByteBuffer buffer, long timeout ) throws IOException
{
throw new UnsupportedOperationException();
}
/**
* Decodes and reformats request data if the underlying protocol layer makes it neccessary
*
* @param buffer
*/
public ByteBuffer decode( ByteBuffer buffer ) throws UnsupportedEncodingException
{
// default implementation does nothing
return buffer;
}
}