package net.i2p.client.streaming.impl;
import net.i2p.I2PAppContext;
import net.i2p.data.ByteArray;
import net.i2p.util.Log;
/**
* Receive data from the MessageOutputStream, build a packet,
* and send it through a connection. The write calls on this
* do NOT block, but they also do not necessary imply immediate
* delivery, or even the generation of a new packet. This class
* is the only one that builds useful outbound Packet objects.
*<p>
* MessageOutputStream -> ConnectionDataReceiver -> Connection -> PacketQueue -> I2PSession
*<p>
* There's one of these per MessageOutputStream.
* It stores no state. It sends everything to the Connection unless
* the Connection is closed,
*/
class ConnectionDataReceiver implements MessageOutputStream.DataReceiver {
private final I2PAppContext _context;
private final Log _log;
private final Connection _connection;
private static final MessageOutputStream.WriteStatus _dummyStatus = new DummyStatus();
/**
* @param con non-null
*/
public ConnectionDataReceiver(I2PAppContext ctx, Connection con) {
_context = ctx;
_log = ctx.logManager().getLog(ConnectionDataReceiver.class);
_connection = con;
}
/**
* This tells the flusher in MessageOutputStream whether to flush.
* It won't flush if this returns true.
*
* It was: return con.getUnackedPacketsSent() > 0 (i.e. Nagle)
* But then, for data that fills more than one packet, the last part of
* the data isn't sent until all the previous packets are acked. Which is very slow.
* The poor interaction of Nagle and Delayed Acknowledgements is well-documented.
*
* So let's send data along unless the outbound window is full.
* (i.e. no-Nagle or TCP_NODELAY)
*
* Probably should have a configuration option for this.
*
* @return !flush
*/
public boolean writeInProcess() {
return _connection.getUnackedPacketsSent() >= _connection.getOptions().getWindowSize();
}
/**
* Send some data through the connection, or if there is no new data, this
* may generate a packet with a plain ACK/NACK or CLOSE, or nothing whatsoever
* if there's nothing new to send.
*
* This is called from MessageOutputStream, i.e. data from the client.
*
* @param buf data to be sent - may be null
* @param off offset into the buffer to start writing from
* @param size how many bytes of the buffer to write (may be 0)
* @return an object to allow optional blocking for data acceptance or
* delivery.
*/
public MessageOutputStream.WriteStatus writeData(byte[] buf, int off, int size) {
Connection con = _connection;
//if (con == null) return _dummyStatus;
boolean doSend = true;
if ( (size <= 0) && (con.getLastSendId() >= 0) ) {
if (con.getOutputStream().getClosed()) {
if (con.getCloseSentOn() <= 0) {
doSend = true;
} else {
// closed, no new data, and we've already sent a close packet
doSend = false;
}
} else {
// no new data, not closed, already synchronized
doSend = false;
}
}
if (con.getUnackedPacketsReceived() > 0)
doSend = true;
if (_log.shouldLog(Log.INFO) && !doSend)
_log.info("writeData called: size="+size + " doSend=" + doSend
+ " unackedReceived: " + con.getUnackedPacketsReceived()
+ " con: " + con /* , new Exception("write called by") */ );
if (doSend) {
PacketLocal packet = send(buf, off, size);
// this shouldn't be null
//if (packet == null) return _dummyStatus;
//dont wait for non-acks
if ( (packet.getSequenceNum() > 0) || (packet.isFlagSet(Packet.FLAG_SYNCHRONIZE)) )
return packet;
else
return _dummyStatus;
} else {
return _dummyStatus;
}
}
/**
* Send some data through the connection, attaching any appropriate flags
* onto the packet.
*
* Called externally from Connection with args (null, 0, 0) to send an ack
*
* @param buf data to be sent - may be null
* @param off offset into the buffer to start writing from
* @param size how many bytes of the buffer to write (may be 0)
* @return the packet sent
*/
public PacketLocal send(byte buf[], int off, int size) {
return send(buf, off, size, false);
}
/**
* Called externally from Connection with args (null, 0, 0, true) to send an empty data packet
*
* @param buf data to be sent - may be null
* @param off offset into the buffer to start writing from
* @param size how many bytes of the buffer to write (may be 0)
* @param forceIncrement even if the buffer is empty, increment the packetId
* so we get an ACK back
* @return the packet sent
*/
public PacketLocal send(byte buf[], int off, int size, boolean forceIncrement) {
//long before = System.currentTimeMillis();
PacketLocal packet = buildPacket(buf, off, size, forceIncrement);
//long built = System.currentTimeMillis();
_connection.sendPacket(packet);
//long sent = System.currentTimeMillis();
//if ( (built-before > 5*1000) && (_log.shouldLog(Log.WARN)) )
// _log.warn(took " + (built-before) + "ms to build a packet: " + packet);
//if ( (sent-built> 5*1000) && (_log.shouldLog(Log.WARN)) )
// _log.warn(took " + (sent-built) + "ms to send a packet: " + packet);
return packet;
}
private static boolean isAckOnly(Connection con, int size) {
boolean ackOnly = ( (size <= 0) && // no data
(con.getLastSendId() >= 0) && // not a SYN
( (!con.getOutputStream().getClosed()) || // not a CLOSE
(con.getOutputStream().getClosed() &&
con.getCloseSentOn() > 0) )); // or it is a dup CLOSE
return ackOnly;
}
/**
* Compose a packet.
* Most flags are set here; however, some are set in Connection.sendPacket()
* and Connection.ResendPacketEvent.retransmit().
* Take care not to set the same options both here and in Connection.
*
* @param buf data to be sent - may be null
* @param off offset into the buffer to start writing from
* @param size how many bytes of the buffer to write (may be 0)
* @param forceIncrement even if the buffer is empty, increment the packetId
* so we get an ACK back
* @return the packet to be sent
*/
private PacketLocal buildPacket(byte buf[], int off, int size, boolean forceIncrement) {
if (size > Packet.MAX_PAYLOAD_SIZE) throw new IllegalArgumentException("size is too large (" + size + ")");
boolean ackOnly = isAckOnly(_connection, size);
boolean isFirst = (_connection.getAckedPackets() <= 0) && (_connection.getUnackedPacketsSent() <= 0);
PacketLocal packet = new PacketLocal(_context, _connection.getRemotePeer(), _connection);
//ByteArray data = packet.acquirePayload();
ByteArray data = new ByteArray(new byte[size]);
if (size > 0)
System.arraycopy(buf, off, data.getData(), 0, size);
data.setValid(size);
data.setOffset(0);
packet.setPayload(data);
if ( (ackOnly && !forceIncrement) && (!isFirst) )
packet.setSequenceNum(0);
else
packet.setSequenceNum(_connection.getNextOutboundPacketNum());
packet.setSendStreamId(_connection.getSendStreamId());
packet.setReceiveStreamId(_connection.getReceiveStreamId());
// not needed here, handled in PacketQueue.enqueue()
//con.getInputStream().updateAcks(packet);
// Do not set optional delay here, set in Connection.sendPacket()
// bugfix release 0.7.8, we weren't dividing by 1000
packet.setResendDelay(_connection.getOptions().getResendDelay() / 1000);
if (_connection.getOptions().getProfile() == ConnectionOptions.PROFILE_INTERACTIVE)
packet.setFlag(Packet.FLAG_PROFILE_INTERACTIVE, true);
else
packet.setFlag(Packet.FLAG_PROFILE_INTERACTIVE, false);
//if ( (!ackOnly) && (packet.getSequenceNum() <= 0) ) {
if (isFirst) {
packet.setFlag(Packet.FLAG_SYNCHRONIZE);
packet.setOptionalFrom();
packet.setOptionalMaxSize(_connection.getOptions().getMaxMessageSize());
}
packet.setLocalPort(_connection.getLocalPort());
packet.setRemotePort(_connection.getPort());
if (_connection.getSendStreamId() == Packet.STREAM_ID_UNKNOWN) {
packet.setFlag(Packet.FLAG_NO_ACK);
}
// don't set the closed flag if this is a plain ACK and there are outstanding
// packets sent, otherwise the other side could receive the CLOSE prematurely,
// since this ACK could arrive before the unacked payload message.
// TODO if the only unacked packet is the CLOSE packet and it didn't have any data...
//
// FIXME Implement better half-close by sending CLOSE whenever. Needs 0.9.9 bug fixes
// throughout network?
//
if (_connection.getOutputStream().getClosed() &&
( (size > 0) || (_connection.getUnackedPacketsSent() <= 0) || (packet.getSequenceNum() > 0) ) ) {
packet.setFlag(Packet.FLAG_CLOSE);
_connection.notifyCloseSent();
}
if (_log.shouldLog(Log.DEBUG))
_log.debug("New OB pkt (acks not yet filled in): " + packet + " on " + _connection);
return packet;
}
/**
* Used if no new packet was sent.
*/
private static final class DummyStatus implements MessageOutputStream.WriteStatus {
public final void waitForAccept(int maxWaitMs) { return; }
public final void waitForCompletion(int maxWaitMs) { return; }
public final boolean writeAccepted() { return true; }
public final boolean writeFailed() { return false; }
public final boolean writeSuccessful() { return true; }
}
void destroy() {
//_connection = null;
}
}