package speedytools.common.network.multipart; import net.minecraftforge.fml.common.network.simpleimpl.MessageContext; import net.minecraftforge.fml.relauncher.Side; import speedytools.common.network.Packet250Types; import speedytools.common.network.PacketHandlerRegistry; import speedytools.common.network.PacketSender; import speedytools.common.utilities.ErrorLog; import java.util.Map; import java.util.TreeMap; import java.util.TreeSet; /** * User: The Grey Ghost * Date: 31/03/14 * Multipart Packets are used to send large amounts of data, bigger than will fit into a single Packet250CustomPayload. * They are designed to be tolerant of network faults such as dropped packets, out-of-order packets, duplicate copies * of the same packet, loss of connection, etc * See MultipartProtocols.txt and MultipartPacket.png * The MultipartOneAtATimeReceiver and MultipartOneAtATimeSender are designed to only handle one of each * type of multipartpacket at a time (eg Selection, MapData, etc). If another packet is sent before the first is * finished, the first packet is aborted. * Usage: * (1) Instantiate MultipartOneAtATimeSender * (2) setPacketSender with the appropriate PacketSender (wrapper for either a NetClientHandler or NetServerHandler) * (3) sendMultipartPacket to send your MultipartPacket, with an appropriate PacketLinkage used to keep your class * informed of transmission progress. * (4) When incoming packets arrive, process them with processIncomingPacket * (5) Periodically (eg each tick) call onTick() to send next segment, handle timeouts, etc * (6) To abort a packet, call abortPacket */ public class MultipartOneAtATimeSender { public MultipartOneAtATimeSender(PacketHandlerRegistry packetHandlerRegistry, Packet250Types acknowledgementPacketType, Side side) { packetBeingSent = null; previousPacketID = MultipartPacket.NULL_PACKET_ID; abortedPackets = new TreeSet<Integer>(); unacknowledgedAbortPackets = new TreeMap<Integer, Packet250MultipartSegment>(); incomingPacketHandler = this.new IncomingPacketHandler(); Packet250MultipartSegmentAcknowledge.registerHandler(packetHandlerRegistry, incomingPacketHandler, side, acknowledgementPacketType); } // create a sender with a different handler; null -> don't register any handler (caller should do it) public MultipartOneAtATimeSender(PacketHandlerRegistry packetHandlerRegistry, Packet250MultipartSegmentAcknowledge.PacketHandlerMethod i_incomingPacketHandler, Packet250Types acknowledgementPacketType, Side side) { packetBeingSent = null; previousPacketID = MultipartPacket.NULL_PACKET_ID; abortedPackets = new TreeSet<Integer>(); unacknowledgedAbortPackets = new TreeMap<Integer, Packet250MultipartSegment>(); incomingPacketHandler = i_incomingPacketHandler; if (incomingPacketHandler != null) { Packet250MultipartSegmentAcknowledge.registerHandler(packetHandlerRegistry, incomingPacketHandler, side, acknowledgementPacketType); } } /** * Changes to a new PacketSender * @param newPacketSender */ public void setPacketSender(PacketSender newPacketSender) { packetSender = newPacketSender; } /** * start sending the given packet. * Only one packet of any given type can be sent at any one time. If a second packet of the same type is added, the first is aborted. * the packet uniqueID must greater than all previously sent packets * @param linkage the linkage that should be used to inform the sender of progress * @param packet the packet to be sent. The uniqueID of the packet must match the unique ID of the linkage! * @return true if the packet was successfully added (and hadn't previously been added) */ public boolean sendMultipartPacket(PacketLinkage linkage, MultipartPacket packet) { // - start transmission, provide a callback if (packet.getUniqueID() <= previousPacketID) { throw new IllegalArgumentException("packetID " + packet.getUniqueID() + " was older than a previous saved packetID "+ previousPacketID); } if (packetBeingSent != null) { if (packet.getUniqueID() <= packetBeingSent.packet.getUniqueID()) { throw new IllegalArgumentException("packetID " + packet.getUniqueID() + " was older than existing packetID "+ packetBeingSent.packet.getUniqueID()); } doAbortPacket(); } if (linkage.getPacketID() != packet.getUniqueID()) { throw new IllegalArgumentException("linkage packetID " + linkage.getPacketID() + " did not match packet packetID "+ packet.getUniqueID()); } if (packet.hasBeenAborted()) return false; PacketTransmissionInfo packetTransmissionInfo = new PacketTransmissionInfo(); packetTransmissionInfo.packet = packet; packetTransmissionInfo.linkage = linkage; packetTransmissionInfo.transmissionState = PacketTransmissionInfo.TransmissionState.SENDING_INITIAL_SEGMENTS; packetTransmissionInfo.timeOfLastAction = 0; assert packetBeingSent == null; packetBeingSent = packetTransmissionInfo; doTransmission(packetTransmissionInfo); linkage.progressUpdate(packet.getPercentComplete()); return true; } private void doAbortPacket() { PacketTransmissionInfo pti = packetBeingSent; if (pti == null) return; pti.linkage.packetAborted(); Packet250MultipartSegment abortPacket = pti.packet.getSenderAbortPacket(); packetSender.sendPacket(abortPacket); int packetUniqueID = pti.packet.getUniqueID(); assert previousPacketID <= packetUniqueID; previousPacketID = packetUniqueID; packetBeingSent = null; abortedPackets.add(packetUniqueID); unacknowledgedAbortPackets.put(packetUniqueID, abortPacket); } private static final int ACKNOWLEDGEMENT_WAIT_MS = 100; // minimum ms elapsed between sending a packet and expecting an acknowledgement private static final int MAX_UNACKNOWLEDGED_SEGMENTS = 20; // if we have this many unacknowledged segments, don't send any more until some are acknowledged //todo implement private static final int MS_TO_NS = 1000000; /** * Transmit the next part of this packet as necessary * @param packetTransmissionInfo * @return true if something was transmitted, false if no action was performed */ private boolean doTransmission(PacketTransmissionInfo packetTransmissionInfo) { // see multipartprotocols.txt for more information on the transmission behaviour assert !packetTransmissionInfo.packet.hasBeenAborted(); // packet should have been removed from transmission list // System.out.println("MultipartOneAtATimeSender doTransmission"); if (!packetSender.readyForAnotherPacket()) return false; boolean sentSomethingFlag = false; switch (packetTransmissionInfo.transmissionState) { case RECEIVING: { assert false: "doTransmission called for a packet in RECEIVING state:"; break; } case SENDING_INITIAL_SEGMENTS: { if (!packetTransmissionInfo.packet.allSegmentsSent()) { Packet250MultipartSegment nextSegment = packetTransmissionInfo.packet.getNextUnsentSegment(); if (nextSegment != null) { sentSomethingFlag = packetSender.sendPacket(nextSegment); packetTransmissionInfo.timeOfLastAction = System.nanoTime(); } } if (packetTransmissionInfo.packet.allSegmentsSent()) { packetTransmissionInfo.transmissionState = PacketTransmissionInfo.TransmissionState.SENDER_WAITING_FOR_ACK; } break; } case SENDER_WAITING_FOR_ACK: { assert !packetTransmissionInfo.packet.allSegmentsAcknowledged(); // packet should have been removed from transmission list if (System.nanoTime() - packetTransmissionInfo.timeOfLastAction >= ACKNOWLEDGEMENT_WAIT_MS * MS_TO_NS) { // timeout waiting for ack: send the first unacked segment, then wait Packet250MultipartSegment nextSegment = packetTransmissionInfo.packet.getNextUnacknowledgedSegment(); if (nextSegment != null) { sentSomethingFlag = packetSender.sendPacket(nextSegment); packetTransmissionInfo.timeOfLastAction = System.nanoTime(); packetTransmissionInfo.transmissionState = PacketTransmissionInfo.TransmissionState.WAITING_FOR_FIRST_RESEND; packetTransmissionInfo.packet.resetAcknowledgementsReceivedFlag(); } } break; } case WAITING_FOR_FIRST_RESEND: { assert !packetTransmissionInfo.packet.allSegmentsAcknowledged(); // packet should have been removed from transmission list if (packetTransmissionInfo.packet.getAcknowledgementsReceivedFlag()) { packetTransmissionInfo.transmissionState = PacketTransmissionInfo.TransmissionState.RESENDING; } else if (System.nanoTime() - packetTransmissionInfo.timeOfLastAction >= ACKNOWLEDGEMENT_WAIT_MS * MS_TO_NS) { // timeout waiting for ack: resend the first unacked segment, then wait again packetTransmissionInfo.packet.resetToOldestUnacknowledgedSegment(); Packet250MultipartSegment nextSegment = packetTransmissionInfo.packet.getNextUnacknowledgedSegment(); if (nextSegment != null) { sentSomethingFlag = packetSender.sendPacket(nextSegment); packetTransmissionInfo.timeOfLastAction = System.nanoTime(); } } break; } case RESENDING: { assert !packetTransmissionInfo.packet.allSegmentsAcknowledged(); // packet should have been removed from transmission list Packet250MultipartSegment nextSegment = packetTransmissionInfo.packet.getNextUnacknowledgedSegment(); if (nextSegment == null) { packetTransmissionInfo.transmissionState = PacketTransmissionInfo.TransmissionState.SENDER_WAITING_FOR_ACK; } else { sentSomethingFlag = packetSender.sendPacket(nextSegment); packetTransmissionInfo.timeOfLastAction = System.nanoTime(); } break; } default: { assert false: "invalid transmission state: " + packetTransmissionInfo.transmissionState; } } return sentSomethingFlag; } public boolean handleIncomingPacket(Packet250MultipartSegmentAcknowledge packet) { int packetUniqueID = packet.getUniqueID(); // If this is an old packet: // (1) if we have no record of this packet being aborted by us, ignore it. Otherwise // (2) if the receiver has previously replied with an abort, ignore it. Otherwise, // (3) reply with abort - unless the incoming packet is also an abort packet, i.e. is an acknowledgement of our abort. if (packetUniqueID <= previousPacketID) { if (!abortedPackets.contains(packetUniqueID)) return false; if (!unacknowledgedAbortPackets.containsKey(packetUniqueID)) return false; // if not in map, already received abort ACK Packet250MultipartSegment abortPacket = MultipartPacket.getAbortPacketForLostPacket(packet); if (abortPacket == null) { unacknowledgedAbortPackets.remove(packetUniqueID); } else { packetSender.sendPacket(abortPacket); } return false; } PacketTransmissionInfo pti = packetBeingSent; if (pti == null) { ErrorLog.defaultLog().info("Incoming packetUniqueID " + packetUniqueID + " was newer than the most recent packet sent " + previousPacketID); return false; } if (packetUniqueID != pti.packet.getUniqueID()) { ErrorLog.defaultLog().info("Incoming packetUniqueID " + packetUniqueID + " was newer than the packet currently being sent " + pti.packet.getUniqueID()); return false; } boolean success = doProcessIncoming(pti, packet); if (success) pti.linkage.progressUpdate(pti.packet.getPercentComplete()); return success; } public class IncomingPacketHandler implements Packet250MultipartSegmentAcknowledge.PacketHandlerMethod { /** * processes an incoming packet; * informs the appropriate linkage of progress * sends an abort packet back if this packet has already been completed * @return true for success, false if packet is invalid or is ignored */ public boolean handlePacket(Packet250MultipartSegmentAcknowledge packet, MessageContext ctx) { return handleIncomingPacket(packet); } } private boolean doProcessIncoming(PacketTransmissionInfo packetTransmissionInfo, Packet250MultipartSegmentAcknowledge packet) { boolean success = packetTransmissionInfo.packet.processIncomingAcknowledgement(packet); if ( packetTransmissionInfo.packet.hasBeenAborted() || packetTransmissionInfo.packet.allSegmentsAcknowledged()) { assert previousPacketID < packetTransmissionInfo.packet.getUniqueID(); previousPacketID = packetTransmissionInfo.packet.getUniqueID(); if (packetTransmissionInfo.packet.hasBeenAborted()) { packetBeingSent.linkage.packetAborted(); abortedPackets.add(packetTransmissionInfo.packet.getUniqueID()); } else { packetBeingSent.linkage.packetCompleted(); } packetBeingSent = null; } else { packetTransmissionInfo.linkage.progressUpdate(packetTransmissionInfo.packet.getPercentComplete()); } return success; } private final int MAX_ABORTED_PACKET_COUNT = 100; // retain this many aborted packet IDs private final long ABORT_RESEND_DELAY_NS = 1000 * 1000 * 1000L; // how often to resend abort packets if no response recvd. /** * should be called frequently to handle sending of segments within packet, etc */ public void onTick() { if (packetBeingSent != null) { boolean retval = doTransmission(packetBeingSent); if (retval) packetBeingSent.linkage.progressUpdate(packetBeingSent.packet.getPercentComplete()); } // any aborts which haven't been acknowledged - send again long timeNow = System.nanoTime(); if (timeNow - timeLastAbortPacketSent > ABORT_RESEND_DELAY_NS) { timeLastAbortPacketSent = timeNow; for (Map.Entry<Integer, Packet250MultipartSegment> entry : unacknowledgedAbortPackets.entrySet()) { packetSender.sendPacket(entry.getValue()); } } while (abortedPackets.size() > MAX_ABORTED_PACKET_COUNT) abortedPackets.pollFirst(); } // abort the packet associated with this linkage public void abortPacket(PacketLinkage linkage) { if (packetBeingSent == null) return; if (packetBeingSent.linkage.getPacketID() != linkage.getPacketID()) return; doAbortPacket(); } /** * This class is used by the MultipartPacketHandler to communicate the packet transmission progress to the sender */ public interface PacketLinkage { public void progressUpdate(int percentComplete); public void packetCompleted(); public void packetAborted(); public int getPacketID(); } private static class PacketTransmissionInfo { public MultipartPacket packet; public PacketLinkage linkage; public long timeOfLastAction; public TransmissionState transmissionState; public enum TransmissionState {RECEIVING, SENDING_INITIAL_SEGMENTS, SENDER_WAITING_FOR_ACK, WAITING_FOR_FIRST_RESEND, RESENDING}; } private PacketTransmissionInfo packetBeingSent; private int previousPacketID; private TreeSet<Integer> abortedPackets; private TreeMap<Integer, Packet250MultipartSegment> unacknowledgedAbortPackets; private long timeLastAbortPacketSent = -1; private PacketSender packetSender; Packet250MultipartSegmentAcknowledge.PacketHandlerMethod incomingPacketHandler; }