/////////////////////////////////////////////////////////////////////////////// // // Microsoft Research Singularity // // Copyright (c) Microsoft Corporation. All rights reserved. // /////////////////////////////////////////////////////////////////////////////// /** * Microsoft Research, Cambridge * author: Yaron Weinsberg, Richard Black */ // #define DEBUG_TCP using NetStack.Common; using System; using System.Threading; using System.Collections; using System.Diagnostics; #if !SINGULARITY using System.Net; #endif using System.Net.IP; using Drivers.Net; using NetStack.Protocols; using NetStack.Contracts; using Microsoft.Contracts; using Microsoft.SingSharp; using Microsoft.SingSharp.Runtime; using Microsoft.Singularity; using Microsoft.Singularity.Channels; namespace NetStack.Runtime { /** * This the TCP session object */ public class TcpSession : Session { // the session's transmit q size (num of packets) public const int TxQSize=100; // the session's receive q size (num of packets) public const int RcvQSize=100; // the maximum number of retries to send before giving up private const int MaxRetries = 5; // the number of seconds to wait for an active connect to succeed private const int ConnectTimeout = 15; // the number of seconds to wait for a passive connect to succeed private const int PassiveConnectTimeout = 5; // the number of seconds between probes of a remote host that has // closed its receive window private const int PersistTimeout = 2; // the number of seconds to wait for a graceful shutdown to complete private const int PoliteShutdownTimeout = 10; // TCP retransmission state private RJBlack.Timer retransTimer; private const uint InitialRetransInterval = 3000; // 3s in ms, Per RFC 2988 // 200ms, more aggressive than RFC "SHOULD" of 1s private const uint MinimumRetransInterval = 200; // Per-session retransmission state private uint retransInterval = InitialRetransInterval, srtt = InitialRetransInterval, rttvar = 0; // All in ms internal TcpState stateContext; // our FSM state internal TcpState oldState; // our previous state internal TcpError connectError; // the result of the Connect request // accepted session list (TcpSessions) private ArrayList acceptedSessions; private int maxAcceptedSessions; // a monitor for the accepted session list private object acceptSessionMonitor; // the passive session (owner) of this session // (applied to passive sessions) internal TcpSession passiveSession; // the retransmit queue private const int RetransmitQSize = 100; private ArrayList retransmitQ; // Setup / teardown timers RJBlack.Timer setupTimer; RJBlack.Timer shutdownTimer; // Information on the "persist" state (for when the remote host // has closed its receive window) RJBlack.Timer persistTimer; // An event that gets set when initial establishment either // succeeds or times out internal System.Threading.ManualResetEvent setupCompleteEvent; // An event that gets set when we're done shutting down this session internal System.Threading.ManualResetEvent closedEvent; // is this session is valid for USERS to read/write data protected bool isValidForRead, isValidForWrite; // Whether or not BindLocalEndPoint() has been called bool haveBound = false; public bool ValidForRead { get { return isValidForRead;} set {isValidForRead=value;}} public bool ValidForWrite { get { return isValidForWrite;} set {isValidForWrite=value;}} // TCB public struct TCB { public SNDValues SND; public RCVValues RCV; // send parameters public struct SNDValues { public uint UNA; // send unacknowledges public uint NXT; // send seqnum that will be transmitted public uint WND; // send window public uint UP; // send urgent pointer public uint WL1; // segment seq number used for last window update public uint WL2; // segment ack number used for last window update public uint ISS; // initial send sequence number public uint NextSeq; // next sequence number to use when packetizing // (not the same as NXT) } // receive parameters public struct RCVValues { public uint NXT; // receive next public uint WND; // receive window public uint UP; // receive urgent pointer public uint IRS; // initial receive sequence number } } // the session's TCB internal TCB sessionTCB; [NotDelayed] public TcpSession(IProtocol! p) : base(p,TxQSize,RcvQSize) { sessionTCB = new TCB(); sessionTCB.SND.WND = TcpFormat.TCP_MSS; sessionTCB.RCV.WND = TcpFormat.TCP_MSS; retransmitQ = new ArrayList(RetransmitQSize); acceptedSessions = new ArrayList(); maxAcceptedSessions = 0; // Changed by Listen() acceptSessionMonitor = new object(); setupCompleteEvent = new System.Threading.ManualResetEvent(false); closedEvent = new System.Threading.ManualResetEvent(false); passiveSession = null; // at first the session is valid (user can interact with it) isValidForRead = true; isValidForWrite = true; // create and initialize the init state this.oldState = null; ChangeState(TCPFSM.CLOSED); } public new void ReInitialize(IProtocol! p) { base.ReInitialize(p); sessionTCB = new TCB(); sessionTCB.SND.WND = TcpFormat.TCP_MSS; sessionTCB.RCV.WND = TcpFormat.TCP_MSS; maxAcceptedSessions = 0; passiveSession = null; isValidForRead = true; isValidForWrite = true; DrainQueue(outQueue); DrainQueue(inQueue); retransmitQ.Clear(); acceptedSessions.Clear(); setupCompleteEvent.Reset(); closedEvent.Reset(); // create and initialize the init state this.oldState = null; if (!IsClosed) { ChangeState(TCPFSM.CLOSED); } DestroyTimer(ref setupTimer); DestroyTimer(ref shutdownTimer); DestroyTimer(ref persistTimer); retransInterval = InitialRetransInterval; } public bool IsClosed { get { return this.stateContext == TCPFSM.CLOSED; } } private void DestroyTimer(ref RJBlack.Timer t) { if (t != null) { Core.Instance().TheDispatcher.RemoveTimeoutCallback(t); } t = null; } [ Conditional("DEBUG_TCP") ] private static void DebugPrint(string format, params object [] args) { Core.Log("TCP: "); Core.Log(format, args); } internal void StartConnectTimer() { ulong timeout; if (passiveSession != null) { timeout = PassiveConnectTimeout; } else { timeout = ConnectTimeout; } Dispatcher.Callback fun = new Dispatcher.Callback(OnConnectTimeout); ulong expiryTime = (ulong)DateTime.UtcNow.Ticks + (timeout * DateTime.TicksPerSecond); setupTimer = Core.Instance().TheDispatcher.AddCallback(fun, null, expiryTime); } internal NetStatus OnConnectTimeout(Dispatcher.CallbackArgs args) { // We failed to become established in time. Bail out. Terminate(null, TcpError.Timeout); return NetStatus.Code.PROTOCOL_OK; } // change the state of this session internal void ChangeState(TcpState! newState) { if (stateContext != null) { oldState = stateContext; stateContext.OnStateLeave(this, newState); } // If we've become Established, stop the connect timer if there // is one ticking if (newState == TCPFSM.ESTABLISHED) { DestroyTimer(ref setupTimer); } if (newState == TCPFSM.CLOSED) { // Stop retransmitting DestroyTimer(ref retransTimer); } stateContext = newState; newState.OnStateEnter(this); } // the message is dispatched to the sessions. the sender // is the protocol and the context is session specific // (i.e., TCP can pass the TCP header to avoid // processing it again at the session instance) public delegate NetStatus OnPacketReceive(object sender, NetPacket! packet, object context); // this is the state's delegate for handling the // protocol triggered event // the object parameter will be set to IProtocol interface internal override NetStatus OnReceive(object sender, NetPacket! pkt, object ctx) { DebugPrint("Packet received."); if (stateContext != null) { // process it in the current state's context DebugPrint("Packet received: State->{0}", stateContext.GetStateName()); return (stateContext.OnPacketReceive(this, pkt, ctx)); } return NetStatus.Code.PROTOCOL_DROP_ERROR; } private void StartPersistTimer() { Dispatcher.Callback fun = new Dispatcher.Callback(OnPersistTimeout); ulong expiryTime = (ulong)DateTime.UtcNow.Ticks + (PersistTimeout * DateTime.TicksPerSecond); persistTimer = Core.Instance().TheDispatcher.AddCallback(fun, null, expiryTime); } internal void StopPersistTimer() { DestroyTimer(ref persistTimer); } internal bool InPersistState() { return persistTimer != null; } internal uint FreeRemoteWindowBytes() { // The portion of the remote window that is available is the most recently // advertised window size minus any outstanding unacknowledged data return sessionTCB.SND.WND - (sessionTCB.SND.NXT - sessionTCB.SND.UNA); } // Called when SND.WND is updated internal void HandleWindowUpdate() { uint newWindow = sessionTCB.SND.WND; if (InPersistState() && (newWindow > 0)) { // The remote receive window just reopened StopPersistTimer(); } // The window update may have made it newly possible to transmit // queued data. if ((FreeRemoteWindowBytes() > 0) && (outQueue.Count > 0)) { Core.Instance().SignalOutboundPackets(); } } private NetStatus OnPersistTimeout(Dispatcher.CallbackArgs timeoutArg) { // // NOTE This is a hack. A proper TCP stack is supposed to // transmit a packet consisting of just one byte when probing the // remote host to see if it has reopened its receive window. // However, we prepacketize data, so we don't have that option. // Instead, we probe using full packets. // TcpSegment seg = null; if (retransmitQ.Count > 0) { // Use the oldest unacknowledged packet to probe seg = (TcpSegment)retransmitQ[0]; } else { // Nothing in the retransmit queue; probe using the next // normal packet. This will transition the packet to the // retransmission queue. seg = GetNextPacket(true /*ignore window*/ ); } if (seg != null) { seg.Mux = BoundMux; NetStatus err = Protocol.OnProtocolSend(seg); assert err == NetStatus.Code.PROTOCOL_OK; } if (stateContext != TCPFSM.CLOSED) { // rearm StartPersistTimer(); } return NetStatus.Code.PROTOCOL_OK; } private TcpSegment GetNextPacket() { return GetNextPacket(false); } private TcpSegment GetNextPacket(bool ignoreReceiverWindow) { if (outQueue.Count == 0) { return null; } lock (outQueue.SyncRoot) { // recheck after lock if (outQueue.Count == 0) { return null; } if (((TcpSegment!)outQueue[0]).retries > 0) { // Special case: the head packet is a retransmission. No special work. return (TcpSegment)base.GetPacket(outQueue, false, 0); // non blocking } else { // The head packet is *not* a retransmission. Make sure we // have room to to move it to the retransmission queue. if (retransmitQ.Count < retransmitQ.Capacity) { TcpSegment nextSegment = (TcpSegment)outQueue[0]; assert nextSegment != null; uint segSize = nextSegment.GetSegmentLength(); if ((!ignoreReceiverWindow) && (segSize > FreeRemoteWindowBytes())) { return null; // Don't overrun the receiver } // Call the base class to dequeue the packet in an orderly way TcpSegment! seg = (TcpSegment!)base.GetPacket(outQueue, false, 0); // non blocking assert seg == nextSegment; if (!seg.isAck) { // save it for RTT adjustments seg.sendTime = (ulong)DateTime.UtcNow.Ticks; retransmitQ.Add(seg); // Make sure the retransmitQ stays sorted if (retransmitQ.Count > 1) { TcpSegment! previousTail = (TcpSegment!)retransmitQ[retransmitQ.Count - 2]; assert TCPFSM.TCPSeqGreater(seg.seq, previousTail.seq); } // Kick off the retransmit timer if we are first if (retransTimer == null) { RestartRetransTimer(); } } else if (segSize == 0) { segSize = 1; // ACKs take up one segment number } // Advance the NXT counter since we're about to put this // segment on the wire sessionTCB.SND.NXT = seg.seq + segSize; return seg; } } } return null; } // NB: call *after* removing or adding items to the retransmitQ internal void RestartRetransTimer() { DestroyTimer(ref retransTimer); if (retransmitQ.Count > 0) { ulong nowTime = (ulong)DateTime.UtcNow.Ticks; // TODO: We should use a dynamically-calculated timeout interval ulong t = nowTime + (ulong)TimeSpan.FromMilliseconds(retransInterval).Ticks; retransTimer = Core.Instance().TheDispatcher.AddCallback( new Dispatcher.Callback(OnRetransmitTimeout), null, t); } // else all data has been acknowledged } internal void FlushRetransmissions() { DestroyTimer(ref retransTimer); retransmitQ.Clear(); } private void UpdateRTT(uint measurement) { const uint MaxCredibleMeasurement = 10000; // 10s in ms uint newInterval; if (measurement > MaxCredibleMeasurement) { // Garbage return; } if (retransInterval == InitialRetransInterval) { // We have never set the session RTT. srtt = measurement; rttvar = srtt / 2; newInterval = measurement * 2; } else { // Second or subsequent measurement. Per RFC 2988 uint abs_srtt_meas = srtt > measurement ? srtt - measurement : measurement - srtt; rttvar = ((rttvar * 3) / 4) + (abs_srtt_meas / 4); srtt = ((7 * srtt) / 8) + (measurement / 8); newInterval = srtt + (4 * rttvar); } this.retransInterval = newInterval < MinimumRetransInterval ? MinimumRetransInterval : newInterval; } // Process a remote acknowledgement of data up to the given sequence number internal void ACKThrough(uint seqNum) { ulong nowTicks = (ulong)DateTime.UtcNow.Ticks; int removed = 0; // Pop packets off the retransmitQ through the acked seqnum while (retransmitQ.Count > 0) { TcpSegment! headSeg = (TcpSegment!)retransmitQ[0]; if (retransmitQ.Count > 1) { TcpSegment! nextSeg = (TcpSegment!)retransmitQ[1]; // Make sure the queue is in order assert TCPFSM.TCPSeqLess(headSeg.seq, nextSeg.seq); } // If the head segment is fully acknowledged, pop it if (TCPFSM.TCPSeqLEQ(headSeg.seq + headSeg.GetSegmentLength(), seqNum)) { retransmitQ.RemoveAt(0); removed++; // Use this ACK for RTT calculations. // Ignore ACKs for retransmitted data. if (headSeg.retries == 0) { UpdateRTT(headSeg.GetRTT(nowTicks)); } } else { // Out of packets to pop break; } } if (removed > 0) { RestartRetransTimer(); } // else this ACK didn't acknowledge any new data // INVARIANT: the head of the retransmit queue must contain // the first unacked seqnum. if (retransmitQ.Count > 0) { TcpSegment! headSeg = (TcpSegment!)retransmitQ[0]; bool hasFirstUnacked = TCPFSM.TCPSeqLEQ(headSeg.seq, sessionTCB.SND.UNA) && TCPFSM.TCPSeqGEQ(headSeg.seq + headSeg.GetSegmentLength(), sessionTCB.SND.UNA); assert hasFirstUnacked; } // We may have paused transmission so as to not overrun the receiver. // Poke the netstack core to be sure we get serviced if we have data // to send. if ((FreeRemoteWindowBytes() > 0) && (outQueue.Count > 0)) { Core.Instance().SignalOutboundPackets(); } } // we need to override GetPacket. We transmit the packet // and put it in the retransmit queue until we get an ack. // (we only do it for data segments including SYN which counts for one) // if a timer expired before ack, we retransmit it until we give up. override internal NetPacket GetPacket(ArrayList! q, bool toBlock, int timeout) { // We only concern ourselves with the remote host's receive window in states // where we are transmitting bool shouldRespectRemoteWindow = (stateContext != TCPFSM.CLOSED) && (stateContext != TCPFSM.LISTEN) && (stateContext != TCPFSM.SYN_SENT) && (stateContext != TCPFSM.SYN_RECVD); // There needs to be at least one packet-worth of space in the send-window for us // to be sure we won't overrun the remote host. if (shouldRespectRemoteWindow && (sessionTCB.SND.WND == 0)) { // Make sure the persist timer is ticking if (!InPersistState()) { StartPersistTimer(); } // else already in the persist state } else { StopPersistTimer(); return GetNextPacket(); } return null; } private void PriorityEnqueuePacket(ArrayList! queue, NetPacket! packet) { lock (queue.SyncRoot) { // This may increase the capacity of the queue. We probably want // watermark limit for user additions to the queue and not worry about // internal additions to the queue. queue.Insert(0, packet); } // Poke the core to service our queue Core.Instance().SignalOutboundPackets(); } // Handler for TCP timeouts internal NetStatus OnRetransmitTimeout(Dispatcher.CallbackArgs timeoutArg) { if (!InPersistState()) { // Retransmit the oldest unacknowledged packet assert retransmitQ.Count > 0; TcpSegment! oldest = (TcpSegment!)retransmitQ[0]; ++oldest.retries; if (oldest.retries >= MaxRetries) { // Give up Abort(TcpError.Timeout); return NetStatus.Code.PROTOCOL_OK; } // INVARIANT: the head of the retransmit queue must contain // the first unacked seqnum if (retransmitQ.Count > 0) { // TODO make this an assert TcpSegment! headSeg = (TcpSegment!)retransmitQ[0]; bool hasFirstUnacked = TCPFSM.TCPSeqLEQ(headSeg.seq, sessionTCB.SND.UNA) && TCPFSM.TCPSeqGreater(headSeg.seq + headSeg.GetSegmentLength(), sessionTCB.SND.UNA); assert hasFirstUnacked; } PriorityEnqueuePacket(outQueue, oldest); } else { // we're in the persist state and retransmissions are suspended. } // Back off! retransInterval = retransInterval * 2; RestartRetransTimer(); return NetStatus.Code.PROTOCOL_OK; } internal override bool IsSessionValidForUserRead() { return isValidForRead; } internal override bool IsSessionValidForUserWrite() { return isValidForWrite; } // data can be still available on a non-valid session public bool IsDataAvailable() { return (inQueue.Count>0); } // Callback type for packetizing data private delegate void CopyDataDelegate(byte[]! intoArray, int sourceOffset, int destOffset, int length); // Helper delegate for dealing with GC data private class GCDataCopier { private byte[] gcData; public GCDataCopier(byte[] gcData) { this.gcData = gcData; } public void CopyData(byte[]! intoArray, int sourceOffset, int destOffset, int length) { if (sourceOffset + length > gcData.Length) { throw new Exception("Overrun of GC data helper"); } Array.Copy(gcData, sourceOffset, intoArray, destOffset, length); } } // Helper class for dealing with ExHeap data private class ExHeapDataCopier { VContainer exHeapData; public ExHeapDataCopier([Claims] byte[]! in ExHeap exHeapData) { this.exHeapData = new VContainer(exHeapData); } public void CopyData(byte[]! intoArray, int sourceOffset, int destOffset, int length) { if (this.exHeapData == null) { throw new Exception("ExHeapDataCopier used after Destroy()"); } byte[]! in ExHeap exHeapData = this.exHeapData.Acquire(); try { if (sourceOffset + length > exHeapData.Length) { throw new Exception("Overrun of ExHeap data helper"); } Bitter.ToByteArray(exHeapData, sourceOffset, length, intoArray, destOffset); } finally { this.exHeapData.Release(exHeapData); } } public void Destroy() { // Explicitly discard our ExHeap object byte[]! in ExHeap data = this.exHeapData.Acquire(); delete data; this.exHeapData = null; } } public int WriteData([Claims] byte[]! in ExHeap data) { int dataLength = data.Length; ExHeapDataCopier helper = new ExHeapDataCopier(data); int retval = InternalWrite(new CopyDataDelegate(helper.CopyData), dataLength); // Make sure the ExHeap block gets thrown away immediately to // reduce pressure on the finalizer thread helper.Destroy(); return retval; } override public int WriteData(byte[]! data) { GCDataCopier helper = new GCDataCopier(data); return InternalWrite(new CopyDataDelegate(helper.CopyData), data.Length); } // here we create the segments from the data // The user is blocked until we have more room. // we return -1 if we can't write (session is not established) // TBC: according to the spec, when TCP is about to send it, if there is not // enough space at the peer receive buffer it can split it // to several smaller segments. private int InternalWrite(CopyDataDelegate! dataCopier, int dataSize) { if (!ValidForWrite) { return -1; } // This is the number of full packets to send uint mssCount = (uint)(dataSize / TcpFormat.TCP_MSS); // This is the size of the last (non-full) packet. uint mssResidue = (uint)(dataSize % TcpFormat.TCP_MSS); int readIndex = 0; uint segSequence = sessionTCB.SND.NextSeq; const int baseFrameSize = EthernetFormat.Size + IPFormat.Size + TcpFormat.Size; while (mssCount!=0) { // create a TCP segment without options // handle the data first byte[] pktData = new byte[baseFrameSize + TcpFormat.TCP_MSS]; dataCopier(pktData, readIndex, baseFrameSize, TcpFormat.TCP_MSS); TcpFormat.WriteTcpSegment(pktData,this.LocalPort, this.RemotePort, sessionTCB.RCV.NXT, segSequence, TcpFormat.TCP_MSS, this.LocalAddress, this.RemoteAddress, TcpFormat.TCP_MSS, true,false,false,false,false); TcpSegment seg = new TcpSegment(pktData, this, segSequence, false); // the next segment sequence segSequence += TcpFormat.TCP_MSS; readIndex += TcpFormat.TCP_MSS; base.PutPacket(outQueue, seg, true); mssCount--; } if (mssResidue!=0) { byte[] pktData = new byte[baseFrameSize + mssResidue]; dataCopier(pktData, readIndex, baseFrameSize, (int)mssResidue); TcpFormat.WriteTcpSegment(pktData, this.LocalPort, this.RemotePort, sessionTCB.RCV.NXT, segSequence, TcpFormat.TCP_MSS, this.LocalAddress, this.RemoteAddress, (ushort)mssResidue, true,false,false,false,false); TcpSegment seg = new TcpSegment(pktData, this, segSequence, false); segSequence += mssResidue; base.PutPacket(outQueue,seg,true); } sessionTCB.SND.NextSeq = segSequence; // since we always send it all... return dataSize; } public bool BindLocalEndPoint(IPv4 address, ushort port) { haveBound = true; SetRemoteEndPoint(IPv4.Broadcast, 0); return SetLocalEndPoint(address, port); } // the method is used to make a session active (i.e., active open) // TBC: manage local ports automatically // we currently more restrictive regarding user // interaction (can't change passive to active etc) public bool Connect(IPv4 dstIP, ushort dstPort, out TcpError error) { DebugPrint("Connect: {0:x8}/{1}", dstIP, dstPort); if (stateContext!=TCPFSM.CLOSED) { DebugPrint("Connect: Failed FSM Closed", dstIP, dstPort); error = TcpError.AlreadyConnected; return false; } // init the session's parameters SetRemoteEndPoint(dstIP, dstPort); // Set the local endpoint to "don't care" if the user // hasn't called BindLocalEndPoint() previously if (!haveBound) { SetLocalEndPoint(IPv4.Any, 0); } sessionTCB.RCV = new TcpSession.TCB.RCVValues(); sessionTCB.SND = new TcpSession.TCB.SNDValues(); DrainQueue(outQueue); DrainQueue(inQueue); retransmitQ.Clear(); // change this session state to SYNSENT. // a SYN message will be sent to the destination ChangeState(TCPFSM.SYN_SENT); // provide a default error this.connectError = TcpError.Unknown; // block the user until the session is ready setupCompleteEvent.WaitOne(); DebugPrint("Connect: SetupCompleteEvent signalled"); if (stateContext != TCPFSM.ESTABLISHED) { // The connect failed. error = this.connectError; DebugPrint("Connect: failed {0}", error); return false; } else { // The connection is up and running properly. error = TcpError.Unknown; DebugPrint("Connect: success"); return true; } } private NetStatus OnShutdownTimedout(Dispatcher.CallbackArgs args) { // No more Mr. Nice Guy Abort(TcpError.Timeout); return NetStatus.Code.PROTOCOL_OK; } // close the session override public bool Close() { // Signal that we're done sending; this will start the polite // shutdown process. DoneSending(); // Start a timer to make sure we don't wait for the shutdown // forever. TODO: we should use a value passed in by the // caller for the timeout rather than hard-coding it. Dispatcher.Callback fun = new Dispatcher.Callback(OnShutdownTimedout); ulong expiryTime = (ulong)DateTime.UtcNow.Ticks + (PoliteShutdownTimeout * DateTime.TicksPerSecond); shutdownTimer = Core.Instance().TheDispatcher.AddCallback(fun, null, expiryTime); // Wait while we complete the shutdown, drain the outbound // queue, etc. closedEvent.WaitOne(); // Quash the timer in case it's still ticking if (Core.Instance().TheDispatcher.RemoveTimeoutCallback(shutdownTimer)) { Tracing.Log(Tracing.Debug, "Tcp quash timer successful."); } else { Tracing.Log(Tracing.Debug, "Tcp quash timer already expired."); } shutdownTimer = null; // After a Close() completes, pending data isn't available anymore! DrainQueue(inQueue); return true; } // hard-shutdown override public bool Abort() { return Abort(TcpError.Unknown); } public bool Abort(TcpError error) { // Abort our connection with a RST segment Terminate(TCPFSM.CreateResetSegment(this, true), error); return true; } private void Terminate(TcpSegment finalPacket, TcpError error) { StopPersistTimer(); if (stateContext == TCPFSM.CLOSED) { // Signal anyone waiting, for good measure. setupCompleteEvent.Set(); } else { // This will set the setup-complete event as a side effect TCPFSM.HandleTerminateSession(this, finalPacket, error); } } // passively open a session public bool Listen(int backlog) { if (stateContext != TCPFSM.CLOSED) return false; // User must have previously bound if (!haveBound) { return false; } maxAcceptedSessions = backlog; ChangeState(TCPFSM.LISTEN); return true; } // start accepting new clients // will block until a new client connection is established public TcpSession Accept() { if (stateContext != TCPFSM.LISTEN) return null; TcpSession tcpSession=null; // block the user until a session is available lock (acceptSessionMonitor) { while (acceptedSessions.Count==0) Monitor.Wait(acceptSessionMonitor); tcpSession = (TcpSession)acceptedSessions[0]; acceptedSessions.RemoveAt(0); } return tcpSession; } // Returns false if our queue is full internal bool AddAcceptedSession(TcpSession newSession) { lock (acceptSessionMonitor) { if (AcceptQueueIsFull()) { return false; } else { acceptedSessions.Add(newSession); Monitor.PulseAll(acceptSessionMonitor); return true; } } } // Indicate whether there are queued sessions waiting to be Accept()ed public int GetNumWaitingListenSessions() { lock (acceptSessionMonitor) { return acceptedSessions.Count; } } // Determine whether the accept queue is full or not internal bool AcceptQueueIsFull() { return GetNumWaitingListenSessions() >= maxAcceptedSessions; } // Indicate that we're done sending public void DoneSending() { if (!isValidForWrite) { // Nothing to do return; } isValidForWrite = false; if (stateContext == TCPFSM.ESTABLISHED) { // Transition to FIN_WAIT1 since we are the first // side to close TCPFSM.SendFin(this, true); // can block ChangeState(TCPFSM.FIN_WAIT1); } else if (stateContext == TCPFSM.CLOSE_WAIT) { // The other side closed first; transition to // LAST_ACK instead TCPFSM.SendFin(this, true); // blocks ChangeState(TCPFSM.LAST_ACK); } else { // We're in some transitory setup or teardown // state; just abort the connection. Abort(TcpError.Closed); } } // Indicate that we're done receiving public void DoneReceiving() { ValidForRead = false; } } // a TCP segment public class TcpSegment : NetPacket { // segment identifier internal uint seq; // the segment sequence number internal uint retries; // number of retransmit retries internal bool isAck; // is it an ack segment (no retrans for ack!) internal ulong sendTime; // used to dynamically adjust the RTT // create a TcpSegment, add room for lower level protocols public TcpSegment(byte[]! buffer) : base(buffer) { seq=0; retries=0; isAck=false; sendTime=0; } // the isAck indicate that this is an ack segment // (we never ack an ack segments without data) [NotDelayed] public TcpSegment(byte[]! buffer, TcpSession! owner, uint seqNum, bool isAck) : base(buffer) { seq=seqNum; retries=0; this.isAck=isAck; sendTime=0; this.SessionContext = owner; } public TcpSession owner { // owner was a field in TcpSegment, but it mirrors field // in NetPacket and one we now use for unblocking after // the ARP response comes back. get { return this.SessionContext as TcpSession; } set { this.SessionContext = value; } } // return the TCP data segment size public uint GetSegmentLength() { return ((uint)(base.Length - EthernetFormat.Size - IPFormat.Size - TcpFormat.Size)); } public uint GetRTT(ulong receiveTime) { ulong deltaTime = (receiveTime>sendTime ? receiveTime-sendTime : 0 ); return (uint)(TimeSpan.FromTicks((long)deltaTime)).Milliseconds; } } }