package org.threadly.litesockets;
import java.io.Closeable;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import org.threadly.concurrent.event.ListenerHelper;
import org.threadly.concurrent.future.ListenableFuture;
import org.threadly.litesockets.buffers.ReuseableMergedByteBuffers;
import org.threadly.litesockets.utils.SimpleByteStats;
import org.threadly.util.ArgumentVerifier;
import org.threadly.util.Clock;
/**
* <p>This is the base Client object for client communication. Anything that reads or writes data
* will use this object. The clients work by having buffered reads and writes for the socket. They tell the
* SocketExecuter when a read can be added or a write is ready for the socket.</p>
*
* <p>All reads are issued on a callback on a Reader in a single threaded manor.
* All writes will be sent in the order they are received. In general its better to write full protocol parsable
* packets with each write where possible. If not possible your own locking/ordering will have to be ensured.
* Close events will happen on there own Closer callback but it uses the same ThreadKey as the Reader thread.
* All Reads should be received before a close event happens.</p>
*
* The client object can not function with out being in a SocketExecuter. Writes can be added before its put
* in the executer, but it will not write to the socket until added to the executer.
*
* @author lwahlmeier
*
*/
public abstract class Client implements Closeable {
/**
* SocketOptions that can be set set on Clients.
*
* @author lwahlmeier
* @deprecated this is deprecated in favor of {@link ClientOptions}
*/
@Deprecated
public static enum SocketOption {
TCP_NODELAY, SEND_BUFFER_SIZE, RECV_BUFFER_SIZE, UDP_FRAME_SIZE, USE_NATIVE_BUFFERS
}
/**
* Default max buffer size (64k). Read and write buffers are independent of each other.
*/
protected static final int DEFAULT_MAX_BUFFER_SIZE = 65536;
/**
* When we hit the minimum read buffer size we will create a new one of this size (64k).
*/
protected static final int NEW_READ_BUFFER_SIZE = 65536;
/**
* Minimum allowed readBuffer (4k). If the readBuffer is lower then this we will create a new one.
*/
protected static final int MIN_READ_BUFFER_SIZE = 4096;
protected static final ByteBuffer EMPTY_BYTEBUFFER = ByteBuffer.allocate(0);
protected final ReuseableMergedByteBuffers readBuffers = new ReuseableMergedByteBuffers(false);
protected final SocketExecuter se;
protected final long startTime = Clock.lastKnownForwardProgressingMillis();
protected final Object readerLock = new Object();
protected final Object writerLock = new Object();
protected final ClientByteStats stats = new ClientByteStats();
protected final AtomicBoolean closed = new AtomicBoolean(false);
protected final ListenerHelper<Reader> readerListener = new ListenerHelper<Reader>(Reader.class);
protected final ListenerHelper<CloseListener> closerListener =
new ListenerHelper<CloseListener>(CloseListener.class);
protected volatile boolean useNativeBuffers = false;
protected volatile boolean keepReadBuffer = true;
protected volatile int maxBufferSize = DEFAULT_MAX_BUFFER_SIZE;
protected volatile int newReadBufferSize = NEW_READ_BUFFER_SIZE;
private ByteBuffer readByteBuffer = EMPTY_BYTEBUFFER;
public Client(final SocketExecuter se) {
this.se = se;
}
/**
* <p>Used by SocketExecuter to set if there was a success or error when connecting, completing the
* {@link ListenableFuture}.</p>
*
* @param t if there was an error connecting this is provided otherwise a successful connect will pass {@code null}.
*/
protected abstract void setConnectionStatus(Throwable t);
/**
* <p>This provides the next available Write buffer. This is typically only called by the {@link SocketExecuter}.
* This is not threadsafe as the same {@link ByteBuffer} will be provided to any thread that calls this, until
* something consumes all byte available in it at which point another {@link ByteBuffer} will be provided.</p>
*
* <p>Assuming something actually removes data from the returned {@link ByteBuffer} an additional call to
* {@link #reduceWrite(int)} must be made</p>
*
*
* @return a {@link ByteBuffer} that can be used to Read new data off the socket for this client.
*/
protected abstract ByteBuffer getWriteBuffer();
/**
* <p>This is called after a write is written to the clients socket. This tells the client how much of that
* {@link ByteBuffer} was written and then reduces the writeBuffersSize accordingly.</p>
*
* @param size the size in bytes of data written on the socket.
*/
protected abstract void reduceWrite(int size);
/**
* <p>Gets the raw Socket object for this Client. If the client does not have a Socket
* it will return null (ie {@link UDPClient}). This is basically getChannel().socket()</p>
*
* @return the Socket for this client.
*/
protected abstract Socket getSocket();
/**
* <p>Returns the {@link SocketChannel} for this client. If the client does not have a {@link SocketChannel}
* it will return null (ie {@link UDPClient}).</p>
*
* @return the {@link SocketChannel} for this client.
*/
protected abstract SocketChannel getChannel();
/**
*
* @return the remote {@link SocketAddress} this client is connected to.
*/
public abstract SocketAddress getRemoteSocketAddress();
/**
*
* @return the local {@link SocketAddress} this client is using.
*/
public abstract SocketAddress getLocalSocketAddress();
/**
* Returns true if this client has data pending in its write buffers. False if there is no data pending write.
*
* @return true if data is pending write, false if there is no data to write.
*/
public abstract boolean canWrite();
/**
* <p>This tells us if the client has timed out before it has been connected to the socket.
* If the client has connected fully this will return false from that point on (even on a closed connection).</p>
*
* @return false if the client has been connected, true if it has not connected and the timeout limit has been reached.
*/
public abstract boolean hasConnectionTimedOut();
/**
* <p>This lets you set lower level socket options for this client. Mainly Buffer sizes and no delay options.</p>
*
* @param so The {@link SocketOption} to set for the client.
* @param value The value for the socket option (1 for on, 0 for off).
* @return True if the option was set, false if not.
* @deprecated use the {@link #clientOptions()} call.
*/
@Deprecated
public abstract boolean setSocketOption(SocketOption so, int value);
public abstract ClientOptions clientOptions();
/**
*
* <p>Called to connect this client to a host. This is done non-blocking.</p>
*
* <p>If there is an error connecting {@link #close()} will also be called on the client.</p>
*
* @return A {@link ListenableFuture} that will complete when the socket is connected, or fail if we cant connect.
*/
public abstract ListenableFuture<Boolean> connect();
/**
* Sets the connection timeout value for this client. This must be called before {@link #connect()} has called on this client.
*
* @param timeout the time in milliseconds to wait for the client to connect.
*/
public abstract void setConnectionTimeout(int timeout);
/**
* <p>Used to get this clients connection timeout information.</p>
*
* @return the max amount of time to wait for a connection to connect on this socket.
*/
public abstract int getTimeout();
/**
* <p>This is used to get the current size of the unWriten writeBuffer.</p>
*
* @return the current size of the writeBuffer.
*/
public abstract int getWriteBufferSize();
/**
* <p>This is used by the {@link SocketExecuter} to help understand how to manage this client.
* Currently only UDP and TCP exist.</p>
*
* @return The IP protocol type of this client.
*/
public abstract WireProtocol getProtocol();
/**
* <p>This is called to write data to the clients socket. Its important to note that there is no back
* pressure when adding writes so care should be taken to now allow the clients {@link #getWriteBufferSize()} to get
* to big.</p>
*
* @param bb The {@link ByteBuffer} to write onto the clients socket.
* @return A {@link ListenableFuture} that will be completed once the data has been fully written to the socket.
*/
public abstract ListenableFuture<?> write(ByteBuffer bb);
/**
* <p>Closes this client. Reads can still occur after this it called. {@link CloseListener#onClose(Client)} will still be
* called (if set) once all reads are done.</p>
*/
public abstract void close();
protected void addReadStats(final int size) {
stats.addRead(size);
}
protected void addWriteStats(final int size) {
stats.addWrite(size);
}
/**
* <p>When this clients socket has a read pending and {@link #canRead()} is true, this is where the ByteBuffer for the read comes from.
* In general this should only be used by the ReadThread in the {@link SocketExecuter} and it should be noted
* that it is not threadsafe.</p>
*
* @return A {@link ByteBuffer} to use during this clients read operations.
*/
protected ByteBuffer provideReadByteBuffer() {
if(keepReadBuffer) {
if(readByteBuffer.remaining() < MIN_READ_BUFFER_SIZE) {
if(useNativeBuffers) {
readByteBuffer = ByteBuffer.allocateDirect(newReadBufferSize);
} else {
readByteBuffer = ByteBuffer.allocate(newReadBufferSize);
}
}
return readByteBuffer;
} else {
if(useNativeBuffers) {
return ByteBuffer.allocateDirect(newReadBufferSize);
} else {
return ByteBuffer.allocate(newReadBufferSize);
}
}
}
/**
* <p>This is used to get the currently set {@link Closer} for this client.</p>
*
* @return the current {@link Closer} interface for this client.
*/
protected void callClosers() {
this.closerListener.call().onClose(this);
}
protected void callReader() {
this.readerListener.call().onRead(this);
}
/**
*
* <p>Adds a {@link ByteBuffer} to the Clients readBuffer. This is normally only used by the {@link SocketExecuter},
* though it could be used to artificially inject data through the client. Calling this will schedule a
* calling the currently set Reader on the client.</p>
*
*
* @param bb the {@link ByteBuffer} to add to the clients readBuffer.
*/
protected void addReadBuffer(final ByteBuffer bb) {
addReadStats(bb.remaining());
int start;
int end;
synchronized(readerLock) {
start = readBuffers.remaining();
readBuffers.add(bb);
end = readBuffers.remaining();
}
if(end > 0 && start == 0){
callReader();
}
}
/**
* Returns true if this client can have reads added to it or false if its read buffers are full.
*
* @return true if more reads can be added, false if not.
*/
public boolean canRead() {
return readBuffers.remaining() < maxBufferSize;
}
/**
* <p>This is used to get the current size of the readBuffers pending reads.</p>
*
* @return the current size of the ReadBuffer.
*/
public int getReadBufferSize() {
return readBuffers.remaining();
}
/**
* This is used to get the currently set max buffer size.
*
* @return the current MaxBuffer size allowed. The read and write buffer use this independently.
*/
public int getMaxBufferSize() {
return this.maxBufferSize;
}
/**
* <p> This returns this clients {@link Executor}.</p>
*
* <p> Its worth noting that operations done on this {@link Executor} can/will block Read callbacks on the
* client, but it does provide you the ability to execute things on the clients read thread.</p>
*
* @return The {@link Executor} for the client.
*/
public Executor getClientsThreadExecutor() {
return se.getExecutorFor(this);
}
/**
* <p>This is used to get the clients {@link SocketExecuter}.</p>
*
* @return the {@link SocketExecuter} set for this client. if none, null is returned.
*/
public SocketExecuter getClientsSocketExecuter() {
return se;
}
/**
* <p>This adds a {@link CloseListener} for this client. Once set the client will call .onClose
* on it once it a socket close is detected.</p>
*
* @param closer sets this clients {@link CloseListener} callback.
*/
public void addCloseListener(final CloseListener closer) {
if(closed.get()) {
getClientsThreadExecutor().execute(new Runnable() {
@Override
public void run() {
closer.onClose(Client.this);
}});
} else {
closerListener.addListener(closer, this.getClientsThreadExecutor());
}
}
/**
* <p>This sets the Reader for the client. Once set data received on the socket will be callback
* on this Reader to be processed. If no reader is set before connecting the client read data will just
* queue up till we hit the {@link #getMaxBufferSize()} size, then will be flushed to the first Reader added.</p>
*
* @param reader the {@link Reader} callback to set for this client.
*/
public void setReader(final Reader reader) {
if(! closed.get()) {
readerListener.clearListeners();
if(reader != null) {
readerListener.addListener(reader, this.getClientsThreadExecutor());
synchronized(readerLock) {
if(this.getReadBufferSize() > 0) {
readerListener.call().onRead(this);
}
}
}
}
}
/**
* <p>This allows you to set/change the max buffer size for this client object.
* This is the in java memory buffer not the additional socket buffer the OS might setup.</p>
*
* <p>In general this should be set to the max size you can deal with. The lower this is the more often we
* will end up adding/removing the client from the selectors. Only the read buffer really follows this,
* writes will buffer up as much as you let it. You need room in your heap for buffers for all clients.
* This buffer is not kept at full size, so clients will rarely use that much memory assuming the the protocol parsing
* and network are keeping up with the data going in/out.</p>
*
* @param size max buffer size in bytes.
* @deprecated use the {@link #clientOptions()} and set the {@link ClientOptions#setMaxClientReadBuffer(int size)} call.
*/
@Deprecated
public void setMaxBufferSize(final int size) {
clientOptions().setMaxClientReadBuffer(size);
}
/**
* <p>Whenever a the {@link Reader} Interfaces {@link Reader#onRead(Client)} is called the
* {@link #getRead()} should be called from the client.</p>
*
* @return a {@link ReuseableMergedByteBuffers} of the current read data for this client.
*/
public ReuseableMergedByteBuffers getRead() {
ReuseableMergedByteBuffers mbb = null;
synchronized(readerLock) {
mbb = readBuffers.duplicateAndClean();
}
if(mbb.remaining() >= maxBufferSize) {
se.setClientOperations(this);
}
return mbb;
}
/**
* <p>Returns if this client is closed or not. Once a client is marked closed there is no way to reOpen it.
* You must just make a new client. Just because this returns false does not mean the client is connected.
* Before a client connects, but has not yet closed this will be false.</p>
*
* @return true if the client is closed, false if the client has not yet been closed.
*/
public boolean isClosed() {
return closed.get();
}
protected boolean setClose() {
return closed.compareAndSet(false, true);
}
/**
* Returns the {@link SimpleByteStats} for this client.
*
* @return the byte stats for this client.
*/
public SimpleByteStats getStats() {
return stats;
}
/**
* Implementation of the SimpleByteStats.
*/
private static class ClientByteStats extends SimpleByteStats {
public ClientByteStats() {
super();
}
@Override
protected void addWrite(final int size) {
ArgumentVerifier.assertNotNegative(size, "size");
super.addWrite(size);
}
@Override
protected void addRead(final int size) {
ArgumentVerifier.assertNotNegative(size, "size");
super.addRead(size);
}
}
/**
* Used to notify when a Client there is data to Read for a Client.
*
* <p>Any client with this set will call .onRead(client) when a read is read from the socket.
* These will happen in a single threaded manor for that client. The thread used is the clients
* thread and should not be blocked for long. If it is the client will back up and stop reading until
* it is unblocked.</p>
*
* <p>The implementor of Reader should call {@link Client#getRead()}.</p>
*
* <p>If the same Reader is used by multiple clients each client will call the Reader on its own thread so be careful
* what objects your modifying if you do that.</p>
*
*
*
*/
public interface Reader {
/**
* <p>When this callback is called it will pass you the Client object
* that did the read. .getRead() should be called on once and only once
* before returning. If it is failed to be called or called more then once
* you could end up with uncalled data on the wire or getting a null.</p>
*
* @param client This is the client the read is being called for.
*/
public void onRead(Client client);
}
/**
* Used to notify when a Client is closed.
*
* <p>It is also single threaded on the same thread key as the reads.
* No action must be taken when this is called but it is usually advised to do some kind of clean up or something
* as this client object will no longer be used for anything.</p>
*
*/
public interface CloseListener {
/**
* This notifies the callback about the client being closed.
*
* @param client This is the client the close is being called for.
*/
public void onClose(Client client);
}
/**
* ClientOptions that can be changed depending on what kind of client you want.
*
* In general these should not be set unless you have a very specific use case.
*
* @author lwahlmeier
*
*/
public interface ClientOptions {
/**
* This is only available for connection backed by a TCP socket.
* It will turn on TcpNoDelay on the the connection at the System level.
* This means that data queued to go out the socket will not delay before being sent.
*
* @param enabled true means NoDelay is on, false means NoDelay is off.
* @return true if this was able to be set.
*/
public boolean setTcpNoDelay(boolean enabled);
/**
* Returns the current state of TcpNoDelay.
*
* @return true means NoDelay is on, false means NoDelay is off.
*/
public boolean getTcpNoDelay();
/**
* Sets this client to use Native or Direct ByteBuffers.
* This can save allocations to the Heap, but is generally only useful
* for things like pass through proxies.
*
* @param enabled true means use Native buffers false means use Heap buffers.
* @return true if this was able to be set.
*/
public boolean setNativeBuffers(boolean enabled);
/**
* Returns the current state of native buffers.
*
* @return true means native buffers are generated false means they are not.
*/
public boolean getNativeBuffers();
/**
* Sets reduced Read buffer allocations. This is accomplished by over allocating
* the read buffer and returning subsets of it. This can make reads much faster but
* does also use more memory.
*
* @param enabled true for enabled false for disabled.
* @return true if this was able to be set.
*/
public boolean setReducedReadAllocations(boolean enabled);
/**
* Returns the current state of ReducedReadAllocations.
*
* @return true for enabled false for disabled.
*/
public boolean getReducedReadAllocations();
/**
* Sets the max size of read buffer the client is allowed to have.
* Once this is reached the client will stop doing read operations until
* the Read buffers gets under this size. This can really effect performance
* especially if its set to small.
*
* @param size in bytes.
* @return true if this was able to be set.
*/
public boolean setMaxClientReadBuffer(int size);
/**
* Returns the currently set max Read buffer size in Bytes.
*
* @return size of max read buffer size.
*/
public int getMaxClientReadBuffer();
/**
* Sets the size of the ByteBuffer used for Reads. The larger this
* buffer is the more data we can read from the socket at once. If
* {@link #getReducedReadAllocations()} is true we will reuse the unused
* space in this buffer until it gets below the minimum read threshold.
*
* @param size in bytes.
* @return true if this was able to be set.
*/
public boolean setReadAllocationSize(int size);
/**
* Returns the current Read buffer allocation size in bytes.
*
* @return bytes allocated for reads.
*/
public int getReadAllocationSize();
/**
* This sets the System level socket send buffer size. Every OS
* has its own min and max values for this, if you go over or under that
* it will not be set.
*
* @param size buffer size in bytes.
* @return true if this was able to be set.
*/
public boolean setSocketSendBuffer(int size);
/**
* Returns the currently set send buffer size in bytes.
*
* @return send buffer size in bytes.
*/
public int getSocketSendBuffer();
/**
* This sets the System level socket receive buffer size. Every OS
* has its own min and max values for this, if you go over or under that
* it will not be set.
*
* @param size buffer size in bytes.
* @return true if this was able to be set.
*/
public boolean setSocketRecvBuffer(int size);
/**
* Returns the currently set receive buffer size in bytes.
*
* @return send buffer size in bytes.
*/
public int getSocketRecvBuffer();
/**
* Sets the UDP frame size. This only possible on UDP backed clients.
*
* @param size max frame size in bytes
* @return true if this was able to be set.
*/
public boolean setUdpFrameSize(int size);
/**
* Returns the currently set UDP frame size in bytes.
*
* @return frame size in bytes.
*/
public int getUdpFrameSize();
}
/**
*
* @author lwahlmeier
*
*/
protected class BaseClientOptions implements ClientOptions {
@Override
public boolean setNativeBuffers(boolean enabled) {
useNativeBuffers = enabled;
return true;
}
@Override
public boolean getNativeBuffers() {
return useNativeBuffers;
}
@Override
public boolean setReducedReadAllocations(boolean enabled) {
keepReadBuffer = enabled;
if(!keepReadBuffer) {
readByteBuffer = EMPTY_BYTEBUFFER;
}
return true;
}
@Override
public boolean getReducedReadAllocations() {
return keepReadBuffer;
}
@Override
public boolean setReadAllocationSize(int size) {
newReadBufferSize = size;
return true;
}
@Override
public int getReadAllocationSize() {
return newReadBufferSize;
}
@Override
public boolean setMaxClientReadBuffer(int size) {
maxBufferSize = size;
return true;
}
@Override
public int getMaxClientReadBuffer() {
return maxBufferSize;
}
@Override
public boolean setTcpNoDelay(boolean enabled) {
return false;
}
@Override
public boolean getTcpNoDelay() {
return false;
}
@Override
public boolean setSocketSendBuffer(int size) {
return false;
}
@Override
public int getSocketSendBuffer() {
return -1;
}
@Override
public boolean setSocketRecvBuffer(int size) {
return false;
}
@Override
public int getSocketRecvBuffer() {
return -1;
}
@Override
public boolean setUdpFrameSize(int size) {
return false;
}
@Override
public int getUdpFrameSize() {
return -1;
}
}
}