Changelog:
- 5 Sep 2024: be more explicit about how to dynamically set timeouts.
- 5 Sep 2024 9:10pm: fix bug which sometimes caused simulator to crash (with error about
self.error()
call); also update simulator to output more statistics in--json
mode (used by graidng scripts) - 6 Sep 2024: add note about common error of not resetting timestamp each time a packet is resent
- 9 Sep 2024: correct text about transmitting a message in
from_network
to text about transmitting a message infrom_application
; make frame count in reliable1 example match command line - 10 Sep 2024: describe
trace()
function in hints; adjust part 2 due date; mention that adjusting buffer size for part 1/2 is probably not a good idea - 12 Sep 2024: add description of common error of failing to handle when a packet times out multiple times to hints
- 15 Sep 2024: change
sliding
tosliding-window
to match what I did in autograder;note that both are accepted. - 28 Sep 2024: for part 3, allow performing one decrease for multiple consecutive timeouts
- 28 Sep 2024: add hint with one strategy for handling sequence number wraparound
- 7 Oct 2024: discuss to_network() not copying
packet
argument; update skeleton code to do this - 8 Oct 2024: mention and encourage the window size increase/decrease of TCP (1 packet per round trip increase, halving to decrease)
- 8 Oct 2024: explictly mention window size should not become 0; note about choice to keep window size as int v float
- 8 Oct 2024: add note about how Python lambdas can inadvertantly capture the
wrong
thing
1 Your Task
Download the supplied simulator and skeleton code at here (last updated 7 Oct 2024).
In the skeleton code, you should only modify
ends.py
, which initially has code that implements transmission without any sort of reliablity.In
ends.py
,ReliableSender
receives messages from its simulated application layer then transmits them using the simulated network layer:- We will call the
from_application
method with each message to be sent. This method should returnTrue
if an attempt was made to transmit the message. If an attempt cannot immediately be made to transmit the message, then it should return False. (If you return False, do not attempt to transmit the message in the future; our code will callfrom_application
again later.) - Given a Packet object
p
, you can callself.to_network(p)
to send it to the receiver. (You will probably do this from thefrom_application
method. I would recommend making a new Packet object for each call (see below).) - Whenever you are ready to receive more data from the application (after having returned False from
from_application
), you must callself.ready_for_more_from_application()
. It is okay if you call this method more times than necessary.
Then
ReliableReceiver
receives those messages from its simulated network layer (in thefrom_network
method), and forwards them to its simulated application layer:- We will call the
from_network
method with each packet received. (This method’s return value does not matter.) - Given a Message object
m
, you can callself.to_application(m)
to send that message to the application.
In
config.py
, we set global variables that will control your implementation, a variableFOO
in this file is references asconfig.FOO
below.Packet objects which have the following fields:
data
: up to 20 bytes, or the special value None to indicate no data is presentis_end
: a boolean fieldseq_num
: an integer sequence number which must be between 0 and config.MAX_SEQUENCE inclusiveack_num
: an integer ack number which must be between 0 and config.MAX_SEQUENCE inclusive or the special value None to specify no ack number
Message objects which have the following fields:
data
: up to 20 bytes (never None)is_end
: a boolean field, which will be True if and only if this is the last message to be sent
We supply you with code that constructs Packet objects from Message objects and vice-versa. However, this code does not implement any form of reliablity; you will need to modify or replace it.
Note that your submissions will only include
ends.py
; we will test your submission with unmodified versions of the other files.- We will call the
1.1 Part 1 (first submission)
1.1.1 one/zero acknowledgments
Modify
ends.py
, so that whenconfig.MODE
isone-zero
, it uses acknowledgments and a 1-bit sequence number (like in section 2.5.1 of Computer Networks: A Systems Approach), and retransmits a packet (from the sender) indefinitely if an acknowledgment is not received config.INITIAL_TIMEOUT time units after a packet is sent.If MySender’s
from_application
cannot immediately transmit a message, then it should return False and not queue the message. (Our simulator/testing code will try resending it later.)Test your implementation:
With no losses (loss rate 0), two packets per packet of data + 1 or 2 extra packets should be sent in total. For example:
python3 main.py --generate-input 1000 --delay 1 --initial-timeout 10
should result in everything being received and not much more than 1000 frames sent.
Running the simulator with high loss rates (such as by adding
--drop 0.4
) should still result in everything being transmitted consistently, though using more frames.With no or almost no losses (such as
--drop 0.01
), one segment should be transmitted per two delays, so running the simulator with--delay=1
should result in a transmission rate of about 20 bytes/(1*2 time units) = 10 bytes per time unit.
1.1.2 dynamic timeouts
Instead of just using
config.INITIAL_TIMEOUT
, compute the timeout dynamically based on the observed round-trip time.My recommendation to do this would be:
sample the round-trip time using the
timestamp
field in packets:Use the
now()
function to get the simulation timestamp and set this in the sender. Be sure to record a new time each time a packet is sent, even if it is resent from a timeout.In the receiver, copy the timestamp from you receive into the acknowledgment packet.
In the sender, when you receive an ack subtract the timestamp you receive from
now()
to get a sample of the round-tript timetake an exponentially weighted moving average1 of the sampled round-trip times:
initialize the average to the initial timeout or the first recorded RTT
choose an
alpha
to determine how much the average is weighted toward recent RTT measurements. I recommend something like 0.2; higher values mean recent changes in RTT affect the average more.each time you record a new RTT, update the average:
new avg = RTT * alpha + old avg * (1 - alpha)
set the timeout to around twice that moving average. (A more sophisticated approach would estimate the variance of the RTT and use that information to set the timeout.)
Test your implementation:
With no losses, setting the initial timeout low compared to the actual timeout should result in relatively few extra messages sent, for example:
python3 main.py --generate-input 100 --initial-timeout 1 --delay 10
should result in not much more than 120 frames being sent in each direction, not something like 1000 frames.
With a relatively low loss rate, setting the initial timeout very high should result in transmission taking not much longer than setting it more normally. For example,
python3 main.py --generate-input 10000 --drop 0.01 -- delay 1 --initial-timeout 100
should take somewhat more than 20000 time units, not anything larger than 30000 time units.
1.1.3 submission
- Submit your
ends.py
on the submission site.
1.2 Part 2 (second submission)
1.2.1 Sliding windows
Modify
ends.py
, so that whenconfig.MODE
issliding-window
(because I originally wrotesliding
here, we’ll also accept that, but the autograder doesn’t use this, so manual intervention in grading would be needed), it uses a sliding window (like in section 2.5.2 of Computer Networks: A Systems Approach) with:- a window size equal to
config.INITIAL_WINDOW
(which will be at least 1)
In addition:
- the first packet by the sender sent should have a sequence number of 0, the second a sequence number of 1, and so on (regardless of the size of the packet)
- your implementation must call
self.to_application
on messages in the order they were sent (even if they are not received in order)
Optionally, you may chose to have your implementation resend packets immediately after some number of duplicate ACKs (or similar signal indicating missing packets).
- a window size equal to
Test your implementation:
- With no losses (loss rate 0) and a timeout substantially longer than twice the transmission delay, no more than two packets per packet of data + 1 or 2 extra packets should be sent in total.
- With no or almost no losses, about config.INITIAL_WINDOW / 2 segments should be transmitted per round-trip. With a
--delay=1
and INITIAL_WINDOW of 10, this should resultin a transmission rate of around 20 bytes * 5 segments/(1 * 2 time unit) = 50 bytes per time unit
1.2.2 Sequence number wrarpound
Make your implementation limit its sequence numbers to between 0 and
config.MAXIMUM_SEQUENCE
and have this work when sequence nubmers need to wraparound.Test your implementation:
- Set MAXIMUM_SEQUENCE to a value much smaller the number of packets sent but more than twice the INTIIAL_WINDOW.
1.2.3 submission
- Submit your
ends.py
on the submission site.
1.3 Part 3 (third submission)
Modify
ends.py
, so that whenconfig.MODE
isvariable-sliding
, it uses a variable sliding window, tracked by the sender with:a initial window size equal to
config.INITIAL_WINDOW
(which will be at least 1)a maximum window size equal to
config.MAXIMUM_WINDOW
(which will be at leastconfig.INITIAL_WINDOW
)window size increasing by approximately fixed number of packets for each round trip time (provided this does not exceed the maximum) when packets are not lost.
If you track the window size as a floating point value, you can approximate this by setting
window_size = min(window_size + INCREASE_PER_ROUND_TRIP / window_size, config.MAXIMUM_WINDOW)
.I’d suggest around 1 packet per round-trip-time, like standard TCP.
when a packet transmission timeouts, the window sizes is divided by a fixed number (but with a minimum size of 1). Optionally, you may detect when multiple consecutive or near-consecutive all timeout, and only do one window size adjustment for all of these packets.
I’d suggest halving the window size, like standard TCP.
when the window size is decreased, some packets with pending retransmission timeouts will now be outside the window. The pending retransmission timeouts for these packets should be temporarily ignored. When the sender’s window later includes these packets again (by the window size increasing or by the last ack received increasing), then, they should be retransmitted.
Optionally, you may also:
on receiving some number of duplicate ACKs for a packet, immediately decrease the window size and retransmit the following packet instead of waiting for the following packet to timeout
implement some form of
slow start
, where, while no congestion is experienced, window sizes increase exponentially, and then window sizes increase according to the additive increase/multiplicative decrease rule
To keep your implementation simpler, you may assume that the
config.MAXIMUM_SEQUENCE
is larger than the number of messages sent.Modify your
MySender
implementation to output to alast-window-sizes.csv
comma-separated values containing a time in the first column and the window size in the second column. (You may have additional columns if you choose.) Output at least one row for every time the window size is changed.Test your implementation:
- Plotting the data in
last-window-sizes.csv
should show a sawtooth-type pattern. Note that the sawtooth may span a quite large number of packets, especially if you do not implement slow-start (which would make therising
part of the sawtooth initially faster), retransmission on duplicate ACKs (which would likely make the falling parts of the sawtooth less extreme by usually preventing multiple timeouts from being experienced in a row), or limit window size reductions from consecutive timeouts. - The peak window size should be relatively close to the bandwidth-delay product, taking into account queuing delay. (For example with
--delay=3 --bandwith=5 --buffer=12
, packets take 3 time units traverse the link and 12 / 5 = 2.4 time units to be removed form a full buffer, so the total effective delay is 3 + 2.4 = 5.4 time units. Thus, the bandwidth-delay product in this case would be 5.4 times 5 = 27 packets.)
- Plotting the data in
1.3.1 submission
- Submit your
ends.py
on the submission site.
2 Running the Code
You can run your implementation in a simulator, which allows you to use how your code deals with packet loss or corruption and uses a simulated clock for timeouts.
For example, a command like:
python3 main.py --generate-input=100 --delay=0.1
will run the simulator:
- transmitting 100 20-byte messages, which will be sent as fast as possible,
- a simulated delay of 0.1 time units for transmitting (in either direction),
The simulator will check that the received data matches the sent data and is in order. The simulator will terminate without an error message when all data is received (via the
to_application
method).You can override
config.py
settings in the simulator using a corresponding command-line option; for example:python3 simulator.py … –mode one-zero
will run the simuluation with the settings specified by
...
but withconfig.MODE
set to'one-zero'
.The simulator supports simulated random losses with the
--drop
option.For example, a command like:
python3 simulator.py –generate-input=100 –delay=0.1 –drop=0.01 –mode one-zero –timeout 1
will run the simulation:
- transmitting 100 20-byte message, which will be sent as fast as possible,
- a simulated delay of 0.1 time units for transmitting (in either direction),
- with 1% of packets dropped (chosen at random),
- setting
config.MODE
to'one-zero'
andconfig.TIMEOUT
to1
The simulator also supports simulated bandwidth limits using the
--bandwidth=BANDWIDTH
and
--buffer=BUFFER-SIZE
options.
In this simulation each link has a buffer that can hold BUFFER-SIZE packets. To transmit a packet, it is first added to the buffer. Then, packets are removed the buffer at least 1/BANDWIDTH time units after the last packet was transmitted.
The simulated bandwidth is implemented by using the following logic on segment transmission:
- if the last segment was transmitted more than 1/BANDWIDTH time units ago, transmit the new segment immediately (so it is received with the delay time unit delay)
- if the last segment was transmitted less than 1/BANDWIDTH time units ago, and no other segments are pending transmission, transmit the the new segment 1/bandwidth time units after the previous one;
- otherwise, if less than BUFFER segments are pending transmission
I do not recommend changing the buffer size from the default until part 3.
3 API
3.1 to_network
- Before 7 Oct 2024, the supplied simulator does not make copies of the Packet object passed to
to_network
, so (unless you updated your copy of the simulator) making changes to the object after callingto_network
may mess up the simulation. I would recommend passing a new Packet object each time toto_network
.
3.2 timers
You will need to setup a timer to handle retransmissions, the
util
library provides acreate_timer
andcancel_timer
method to do this. For example, code like:self.resend_timer = create_timer( 10, lambda: self._do_resend() )
will cause
self._do_resend()
to run 10 time units in the future.If, before
self._do_resend()
runs you decide you do not want to run it, then you can usecancel_timer(self.resend_timer)
Most likely your
from_network
implementation that receives acknowledgments will do something like this.
3.3 trace()
There is a
trace()
function you can call liketrace("some-name", "some message here")
if
some-name
in the set in config.py’sTRACE
variable, then this will causesome message here
to be output. Ifsome-name
is not in config.py’sTRACE
variable, nothing will be output.You can also pass an option like
--trace=some-name,some-other-name,...
to override the value of
TRACE
from the comman dline.
4 Hints
4.1 timers doing the wrong thing
If you do something like:
def bar(): if True: x = 1 some_timer = create_timer(10, lambda: foo(x)) ... if True: ... x = 2
then the timer will call foo with
2
, not1
. One way to avoid this is to call function with its own local variables to create timers.
4.2 Part 1
If your timeouts are becoming very large, make sure you are setting a new timestamp each time you resend a packet. Otherwise, you may end up measuring the timeout to set your timeout and causing it to get very large.
If your implementation for part 1 hangs, one possible reason can be failing to account for timeouts occuring multiple times. If you resend after a timeout, and then fail to receive an ACK again, you need to make sure you resend again after a second timeout.
An inefficient but non-very-error-prone way to deal with sequence number wraparound is to construct a function that computes the list of sequence numbers that is in the window. Then you can use an item in list check for your conditions about sequence numbers. If you’re having difficulty reasoning about how to sequence number wraparound to work, I’d recommend starting with this approach, then possibly figuring out how to make it more efficient.
This is the type of average typically used in TCP; other types of moving averages would also be fine.↩︎