Changelog:
- 6 Sep 2025: supply updated config.py; more fully remove references to MAXIMUM_SEQUENCE from the writeup since it’s just set to a huge number
- 8 Sep 2025: correct ReliableSender/ReliableReceiver to MySender/MyReceiver; note explicitly that to_network exists in MyReceiver like in MySender
- 9 Sep 2025: update config.py in reliable skeleton to default to
one-zeroand not listno-ackmode that new error-checking code does not support - 15 Sep 2025: note about using config settings and not using
from config import …
- 16 Sep 2025: correct
sliding
tosliding-window
to match connection.py - 16 Sep 2025: update config.py in skeleton code not to refer to wrong
sliding
setting - 18 Sep 2025: update main.py to be more permissive about timeout enforcement in sliding-window setting
- 17 Dec 2025 (for future semesters): don’t say to set moving average to initial timeout
If you downloaded the skeleton code before 6 Septemer 2025 around 5pm, you may need an updated version of config.py. Also after the morning of 18 September 2025, there is an updated main.py was updated to be more permissive about timeout enforcement for the sliding window case, which would likely be relevant for solutions attempting to selective acknowledgments or similar.
1 Your Task
Download the supplied simulator and skeleton code at here (last updated 18 September 2025).
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,MySenderreceives messages from its simulated application layer then transmits them using the simulated network layer:- We will call the
from_applicationmethod with each message to be sent. This method should returnTrueif 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_applicationagain 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_applicationmethod. 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
MyReceiverreceives those messages from its simulated network layer (in thefrom_networkmethod), and forwards them to its simulated application layer:- We will call the
from_networkmethod 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. - Like with
MyReceiver, you can callself.to_networkto send a Packet to the network, like in the MySender class.
In
config.py, we set global variables that will control your implementation, a variableFOOin this file is references asconfig.FOObelow.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 numberack_num: an integer ack number 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.MODEisone-zero:it does not send a new message until the previous is acknowledged (like in section 2.5.1 of Computer Networks: A Systems Approach), and retransmits a packet (from the sender)
if a message is not acknowledged, it is resent (as many times as needed) config.INITIAL_TIMEOUT time units after a packet is sent.
the acknowledgment number of an acknowledgment matches the sequence number of the message being acknowledged
sequence numbers either:
alternate between 0 and 1, or
start at some value and increase indefinitely
If MySender’s
from_applicationcannot immediately transmit a message, then it should return False and not queue the message. (Our simulator/testing code will try resending it later.)Your implementation must send acknowledgment with an
ack_numequal to the number for the most recently received message.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 10should 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=1should 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
timestampfield 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 based on your 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 10should 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 100should take somewhat more than 20000 time units, not anything larger than 30000 time units.
1.1.3 submission
- Submit your
ends.pyon the submission site.
1.2 Part 2 (second submission)
1.2.1 Sliding windows
Modify
ends.py, so that whenconfig.MODEissliding-window, 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:
messages are assigned consecutive sequence numbers; so if data packets for the first message are assigned sequence number 0, then the next should be sequence number 1, then 2, and so on (regardless of the size of the messages)
your implementation must call
self.to_applicationon messages in the order they were sent (even if they are not received in order)when sending acknowledgments, your implementation must use cumulative acknolwedgments with
ack_numset to the highest sequence number such that that data packet and all previous data packets were also successfully received.(For example, if sequence numbers 0, 1, 2, 3, 4, 5, and 6 were sent, but only 0, 1, 2, 3, 5, and 6 were received, you should send an
ack_numof 3; after sequence number 4 is received, you should send anack_numof 6.)
Optionally: you may chose to:
have your implementation resend packets immediately after some number of duplicate ACKs (or similar signal indicating missing packets).
use the
sackfield of packets to provideselective acknowledgment
information about additional frames that have been received afterack_numto avoid some retransmissionschoose to wraparound sequence numbers at some point (as long as there are more than 2 times the window size plus 1 sequence numbers)
- 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=1and 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 submission
- Submit your
ends.pyon the submission site.
2 API
2.1 config settings
After doing
import config, you can access settings in config.py with syntax likeconfig.MODE.I do not recommend trying to use
from config import FOOinstead ofconfig.FOO, since you may not see changes ourmain.pymakes to these config settings.
2.2 sending/making packets
- From both
MySender/MyReceiver, you can send packets with theto_networkmethod (that we will inject in those classes).
2.3 timers
You will need to setup a timer to handle retransmissions, the
utillibrary provides acreate_timerandcancel_timermethod 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_networkimplementation that receives acknowledgments will do something like this.
2.4 trace()
There is a
trace()function you can call liketrace("some-name", "some message here")if
some-namein the set in config.py’sTRACEvariable, then this will causesome message hereto be output. Ifsome-nameis not in config.py’sTRACEvariable, nothing will be output.You can also pass an option like
--trace=some-name,some-other-name,...to override the value of
TRACEfrom the comman dline.
3 Hints
3.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 = 2then 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.
3.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.
This is the type of average typically used in TCP; other types of moving averages would also be fine.↩︎