jawnae.pyronet.PyroClient Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of telegramapi Show documentation
Show all versions of telegramapi Show documentation
Java library to create Telegram Clients
/*
* Created on Sep 24, 2008
*/
package jawnae.pyronet;
import org.telegram.mtproto.transport.ByteBufferDesc;
import java.io.EOFException;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.NotYetConnectedException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class PyroClient
{
private final PyroSelector selector;
private final SelectionKey key;
private final ByteStream outbound;
// called by PyroSelector.connect()
PyroClient(PyroSelector selector, InetSocketAddress bind, InetSocketAddress host) throws IOException
{
this(selector, PyroClient.bindAndConfigure(selector, SocketChannel.open(), bind));
((SocketChannel) this.key.channel()).connect(host);
}
// called by PyroClient and PyroServer
PyroClient(PyroSelector selector, SelectionKey key)
{
this.selector = selector;
this.selector.checkThread();
this.key = key;
this.key.attach(this);
this.outbound = new ByteStream();
this.listeners = new CopyOnWriteArrayList();
this.lastEventTime = System.currentTimeMillis();
}
//
private final List listeners;
public void addListener(PyroClientListener listener)
{
this.selector.checkThread();
this.listeners.add(listener);
}
public void removeListener(PyroClientListener listener)
{
this.selector.checkThread();
this.listeners.remove(listener);
}
public void removeListeners()
{
this.selector.checkThread();
this.listeners.clear();
}
/**
* Returns the PyroSelector that created this client
*/
public PyroSelector selector()
{
return this.selector;
}
//
private Object attachment;
/**
* Attach any object to a client, for example to store session information
*/
public void attach(Object attachment)
{
this.attachment = attachment;
}
/**
* Returns the previously attached object, or null
if none is set
*/
public T attachment()
{
return (T) this.attachment;
}
//
/**
* Returns the local socket address (host+port)
*/
public InetSocketAddress getLocalAddress()
{
Socket s = ((SocketChannel) key.channel()).socket();
return (InetSocketAddress) s.getLocalSocketAddress();
}
/**
* Returns the remove socket address (host+port)
*/
public InetSocketAddress getRemoteAddress()
{
Socket s = ((SocketChannel) key.channel()).socket();
return (InetSocketAddress) s.getRemoteSocketAddress();
}
//
public void setTimeout(int ms) throws IOException
{
this.selector.checkThread();
((SocketChannel) key.channel()).socket().setSoTimeout(ms);
// prevent a call to setTimeout from immediately causing a timeout
this.lastEventTime = System.currentTimeMillis();
this.timeout = ms;
}
public void setLinger(boolean enabled, int seconds) throws IOException
{
this.selector.checkThread();
((SocketChannel) key.channel()).socket().setSoLinger(enabled, seconds);
}
public void setKeepAlive(boolean enabled) throws IOException
{
this.selector.checkThread();
((SocketChannel) key.channel()).socket().setKeepAlive(enabled);
}
//
//
//
private boolean doEagerWrite = false;
/**
* If enabled, causes calls to write() to make an attempt to write the bytes,
* without waiting for the selector to signal writable state.
*/
public void setEagerWrite(boolean enabled)
{
this.doEagerWrite = enabled;
}
//
/**
* Will enqueue the bytes to send them
* 1. when the selector is ready to write, if eagerWrite is disabled (default)
* 2. immediately, if eagerWrite is enabled
* The ByteBuffer instance is kept, not copied, and thus should not be modified
* @throws PyroException when shutdown() has been called.
*/
public void write(ByteBufferDesc data) throws PyroException {
this.selector.checkThread();
if (!this.key.isValid()) {
// graceful, as this is meant to be async
return;
}
if (this.doShutdown)
{
throw new PyroException("shutting down");
}
this.outbound.append(data);
if (this.doEagerWrite)
{
try
{
this.onReadyToWrite(System.currentTimeMillis());
}
catch (NotYetConnectedException exc)
{
this.adjustWriteOp();
}
catch (IOException exc)
{
this.onConnectionError(exc);
key.cancel();
}
}
else
{
this.adjustWriteOp();
}
}
/**
* Writes as many as possible bytes to the socket buffer
*/
public int flush()
{
int total = 0;
while (this.outbound.hasData())
{
int written;
try
{
written = this.onReadyToWrite(System.currentTimeMillis());
}
catch (IOException exc)
{
written = 0;
}
if (written == 0)
{
break;
}
total += written;
}
return total;
}
/**
* Makes an attempt to write all outbound
* bytes, fails on failure.
*
* @throws PyroException on failure
*/
public int flushOrDie() throws PyroException
{
int total = 0;
while (this.outbound.hasData())
{
int written;
try
{
written = this.onReadyToWrite(System.currentTimeMillis());
}
catch (IOException exc)
{
written = 0;
}
if (written == 0)
{
throw new PyroException("failed to flush, wrote " + total + " bytes");
}
total += written;
}
return total;
}
/**
* Returns whether there are bytes left in the
* outbound queue.
*/
public boolean hasDataEnqueued()
{
this.selector.checkThread();
return this.outbound.hasData();
}
private boolean doShutdown = false;
/**
* Gracefully shuts down the connection. The connection
* is closed after the last outbound bytes are sent.
* Enqueuing new bytes after shutdown, is not allowed
* and will throw an exception
*/
public void shutdown()
{
this.selector.checkThread();
this.doShutdown = true;
if (!this.hasDataEnqueued())
{
this.dropConnection();
}
}
/**
* Immediately drop the connection, regardless of any
* pending outbound bytes. Actual behaviour depends on
* the socket linger settings.
*/
public void dropConnection()
{
this.selector.checkThread();
if (this.isDisconnected())
{
return;
}
Runnable drop = new Runnable()
{
@Override
@SuppressWarnings("synthetic-access")
public void run()
{
try
{
if (key.channel().isOpen())
{
key.channel().close();
}
}
catch (IOException exc)
{
selector().scheduleTask(this);
}
}
};
drop.run();
this.onConnectionError("local");
}
/**
* Returns whether the connection is connected to a remote client.
*/
public boolean isDisconnected()
{
this.selector.checkThread();
return !this.key.channel().isOpen();
}
//
void onInterestOp(long now)
{
if (!key.isValid())
{
this.onConnectionError("remote");
}
else
{
try
{
if (key.isConnectable())
this.onReadyToConnect(now);
if (key.isReadable())
this.onReadyToRead(now);
if (key.isWritable())
this.onReadyToWrite(now);
}
catch (IOException exc)
{
this.onConnectionError(exc);
key.cancel();
}
}
}
private long timeout = 0L;
private long lastEventTime;
boolean didTimeout(long now) {
return this.timeout != 0 && (now - this.lastEventTime) > this.timeout;
}
private void onReadyToConnect(long now) throws IOException
{
this.selector.checkThread();
this.lastEventTime = now;
this.selector.adjustInterestOp(key, SelectionKey.OP_CONNECT, false);
((SocketChannel) key.channel()).finishConnect();
for (PyroClientListener listener : this.listeners)
listener.connectedClient(this);
}
private void onReadyToRead(long now) throws IOException
{
this.selector.checkThread();
this.lastEventTime = now;
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = this.selector.networkBuffer;
// read from channel
buffer.clear();
int bytes = channel.read(buffer);
if (bytes == -1)
throw new EOFException();
buffer.flip();
for (PyroClientListener listener : this.listeners)
listener.receivedData(this, buffer);
}
private int onReadyToWrite(long now) throws IOException
{
this.selector.checkThread();
//this.lastEventTime = now;
int sent = 0;
// copy outbound bytes into network buffer
ByteBuffer buffer = this.selector.networkBuffer;
buffer.clear();
this.outbound.get(buffer);
buffer.flip();
// write to channel
if (buffer.hasRemaining())
{
SocketChannel channel = (SocketChannel) key.channel();
sent = channel.write(buffer);
}
if (sent > 0)
{
this.outbound.discard(sent);
}
for (PyroClientListener listener : this.listeners)
listener.sentData(this, sent);
this.adjustWriteOp();
if (this.doShutdown && !this.outbound.hasData())
{
this.dropConnection();
}
return sent;
}
void onConnectionError(final Object cause)
{
this.selector.checkThread();
try
{
// if the key is invalid, the channel may remain open!!
this.key.channel().close();
}
catch (IOException exc)
{
// type: java.io.IOException
// message: "A non-blocking socket operation could not be completed immediately"
// try again later
this.selector.scheduleTask(new Runnable()
{
@Override
public void run()
{
PyroClient.this.onConnectionError(cause);
}
});
return;
}
if (cause instanceof ConnectException)
{
for (PyroClientListener listener : this.listeners)
listener.unconnectableClient(this, (Exception)cause);
}
else if (cause instanceof EOFException) // after read=-1
{
for (PyroClientListener listener : this.listeners)
listener.disconnectedClient(this);
}
else if (cause instanceof IOException)
{
for (PyroClientListener listener : this.listeners)
listener.droppedClient(this, (IOException) cause);
}
else if (!(cause instanceof String))
{
for (PyroClientListener listener: this.listeners)
listener.unconnectableClient(this, null);
}
else if (cause.equals("local"))
{
for (PyroClientListener listener : this.listeners)
listener.disconnectedClient(this);
}
else if (cause.equals("remote"))
{
for (PyroClientListener listener : this.listeners)
listener.droppedClient(this, null);
}
else
{
throw new IllegalStateException("illegal cause: " + cause);
}
}
public String toString()
{
return this.getClass().getSimpleName() + "[" + this.getAddressText() + "]";
}
private final String getAddressText()
{
if (!this.key.channel().isOpen())
return "closed";
InetSocketAddress sockaddr = this.getRemoteAddress();
if (sockaddr == null)
return "connecting";
InetAddress inetaddr = sockaddr.getAddress();
return inetaddr.getHostAddress() + "@" + sockaddr.getPort();
}
//
void adjustWriteOp()
{
this.selector.checkThread();
boolean interested = this.outbound.hasData();
this.selector.adjustInterestOp(this.key, SelectionKey.OP_WRITE, interested);
}
static final SelectionKey bindAndConfigure(PyroSelector selector, SocketChannel channel, InetSocketAddress bind) throws IOException
{
selector.checkThread();
channel.socket().bind(bind);
return configure(selector, channel, true);
}
static final SelectionKey configure(PyroSelector selector, SocketChannel channel, boolean connect) throws IOException
{
selector.checkThread();
channel.configureBlocking(false);
//channel.socket().setSoLinger(false, 0); // this will b0rk your connections
channel.socket().setSoLinger(true, 4);
channel.socket().setReuseAddress(false);
channel.socket().setKeepAlive(false);
channel.socket().setTcpNoDelay(true);
channel.socket().setReceiveBufferSize(PyroSelector.BUFFER_SIZE);
channel.socket().setSendBufferSize(PyroSelector.BUFFER_SIZE);
int ops = SelectionKey.OP_READ;
if (connect)
ops |= SelectionKey.OP_CONNECT;
return selector.register(channel, ops);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy