type
status
slug
date
summary
tags
category
password
icon
Overview
TCP is a protocol that reliably conveys a pair of flow-controlled byte streams (one in each direction) over unreliable datagrams. Two party participate in the TCP connection, and each party is a peer of the other. Each peer acts as both “sender” (of its own outgoing
byte-stream) and “receiver” (of an incoming byte-stream) at the same time.
It will be your TCPSender’s responsibility to:
- Keep track of the receiver’s window (receiving incoming TCPReceiverMessages with their acknos and window sizes)
- Fill the window when possible, by reading from the ByteStream, creating new TCP segments (including SYN and FIN flags if needed), and sending them. The sender should keep sending segments until either the window is full or the outbound ByteStream has nothing more to send.
- Keep track of which segments have been sent but not yet acknowledged by the receiver—we call these “outstanding” segments
- Re-send outstanding segments if enough time passes since they were sent, and they haven’t been acknowledged yet
implementation detail:
- Every few milliseconds, your TCPSender’s tick method will be called with an argument that tells it how many milliseconds have elapsed since the last time the method was called. Use this to maintain a notion of the total number of milliseconds the TCPSender has been alive. Please don’t try to call any “time” or “clock” functions from the operating system or CPU—the tick method is your only access to the passage of time. That keeps things deterministic(确定的) and testable(可测试的).
- When the TCPSender is constructed, it’s given an argument that tells it the “initial value” of the retransmission timeout (RTO). The RTO is the number of milliseconds to wait before resending an outstanding TCP segment. The value of the RTO will change over time, but the “initial value” stays the same. The starter code saves the “initial value” of the RTO in a member variable called initial_RTO_ms_.
- You’ll implement the retransmission timer: an alarm that can be started at a certain time, and the alarm goes off (or “expires”) once the RTO has elapsed. We emphasize that this notion of time passing comes from the tick method being called—not by getting the actual time of day.
- Every time a segment containing data (nonzero length in sequence space) is sent (whether it’s the first time or a retransmission), if the timer is not running, start it running so that it will expire after RTO milliseconds (for the current value of RTO). By “expire,” we mean that the time will run out a certain number of milliseconds in the future.我觉得关于在这个计时何时开始更详细的是看第3章的FSM以及图3.33那个程序示意,这里是TCP的流水线
- When all outstanding data has been acknowledged, stop the retransmission timer.
- If tick is called and the retransmission timer has expired: (a) Retransmit the earliest (lowest sequence number) segment that hasn’t been fully acknowledged by the TCP receiver. You’ll need to be storing the outstanding segments in some internal data structure that makes it possible to do this. (b) If the window size is nonzero: i. Keep track of the number of consecutive retransmissions, and increment it because you just retransmitted something. Your TCPConnection will use this information to decide if the connection is hopeless (too many consecutive retransmissions in a row) and needs to be aborted(记得吗,这个是用于快速重传,冗余ACK). ii. Double the value of RTO. This is called “exponential backoff”—it slows down retransmissions on lousy networks to avoid further gumming up the works. (c) Reset the retransmission timer and start it such that it expires after RTO milliseconds (taking into account that you may have just doubled the value of RTO!).
- When the receiver gives the sender an ackno that acknowledges the successful receipt of new data (the ackno reflects an absolute sequence number bigger than any previous ackno):
- Set the RTO back to its “initial value.”
- If the sender has any outstanding data, restart the retransmission timer so that it will expire after RTO milliseconds (for the current value of RTO).
- Reset the count of “consecutive retransmissions” back to zero.
Implementing the TCP sender
the basic idea of what the TCP sender does : given an outgoing ByteStream, split it up into segments, send them to the receiver, and if they don’t get acknowledged soon enough, keep resending them.
Now it’s time for the concrete interface that your TCPSender will provide. There are four important events that it needs to handle:
- void push( const TransmitFunction& transmit );
The TCPSender is asked to fill the window from the outbound byte stream: it reads from the stream and sends as many TCPSenderMessages as possible, as long as there are new bytes to be read and space available in the window. It sends them by calling the provided transmit() function on them.
What should I do if the window size is zero?
If the receiver has announced a window size of zero, the push method should pretend like the window size is one. The sender might end up sending a single byte that gets rejected (and not acknowledged) by the receiver, but this can also provoke the receiver into sending a new acknowledgment segment where it reveals that more space has opened up in its window. Without this, the sender would never learn that it was allowed to start sending again.
This is the only special-case behavior your implementation should have for the case of a zero-size window. The TCPSender shouldn’t actually remember a false window size of 1. The special case is only within the push method. Also, N.B.(注意(Nota Bene)) that even if the window size is one (or 20, or 200), the window might still be full. A “full” window is not the same as a “zero-size” window.
这个情况之第三章讲过的喔,这里给一个超链接过去吧:
the TCP specification requires Host A to continue to send segments with one data byte when B’s receive window is zero. These segments will be acknowledged by the receiver. Eventually the buffer will begin to empty and the acknowledgments will contain a nonzero rwdn value.(wow,真的很细)
- void receive( const TCPReceiverMessage& msg );
A message is received from the receiver, conveying the new left (= ackno) and right (= ackno + window size) edges of the window. The TCPSender should look through its collection of outstanding segments and remove any that have now been fully acknowledged (the ackno is greater than all of the sequence numbers in the segment).
- void tick( uint64 t ms_since_last_tick, const TransmitFunction& transmit );
Time has passed — a certain number of milliseconds since the last time this method was called. The sender may need to retransmit an outstanding(未解决的) segment; it can call the transmit() function to do this. (Reminder: please don’t try to use real-world “clock” or “gettimeofday” functions in your code; the only reference to time passing comes from the ms_since_last_tick argument.),这里有点不理解这个tick怎么实现,好像是传入的参数ms_since_last_tick是指的上次调用到现在经过的时间,所以这个时间是做参数传递
- TCPSenderMessage make_empty_message() const;
The TCPSender should generate and send a zero-length message with the sequence number set correctly. This is useful if the peer wants to send a TCPReceiverMessage (e.g. because it needs to acknowledge something from the peer’s sender) and needs to generate a TCPSenderMessage to go with it.
Note: a segment like this one, which occupies no sequence numbers, doesn’t need to be kept track of as “outstanding” and won’t ever be retransmitted.
To complete Checkpoint 3, please review the full interface in src/tcp_sender.hh implement the complete TCPSender public interface in the tcp_sender.hh and tcp_sender.cc files. We expect you’ll want to add private methods and member variables, and possibly a helper class.
F&Q摘录
- What do I do if an acknowledgment only partially acknowledges some outstanding segment? Should I try to clip off the bytes that got acknowledged?
A TCP sender could do this, but for purposes of this class, there’s no need to get fancy. Treat each segment as fully outstanding until it’s been fully acknowledged—all of the sequence numbers it occupies are less than the ackno.
实操开写
这里从疑问入手:
- transmit是我们发送内容出去的函数,只需要往这个transmit函数(可以说类型是TRansmitFunction)里面传入TCPSendMessage类型的数据就可以了。我们最开始发送的第一条数据是需要标记syn作为开始的,如何在push函数中确定此次调用是没有/有建立连接呢?
当前我就拿了一个private变量has_init来标记是否已经初始化
- 第二个问题是在考虑怎么获取发送内容,肯定会通过peek()来得到,但是peek本身不会移动,如果没有去pop掉相应的内容,那么就没有办法移动,但是pop掉过后,怎么重传超时/冗余ack检测到的outstanding重传问题?因为之前的数据已经pop掉了啊,那发送过的还没有ack的segment怎么办,需要重传的话,从哪里拿数据?
这个地方就需要仔细的思考,我们sender方发送信息,数据从上层应用调用socket接口(我们这里是byte_stream接口来提供的可靠字节流传输,应用层调用writer方写入),而下面tcp传输层,我们需要实现,也就是从这个接口,byte_stream里面读出数据,使用reader读取,这里要回忆上一个lab里面对于序号的理解:
再看看这个接口,第一个测试的时候就能看到计算sequence number是根据sequence number(从随机起来的isn_开始算,不过大小长度和absolute sequence number是一样的),这里我的想法是,在tcp发送的时候,我们是要做切片的,再来回味一下前面对于push接口的要求:
The TCPSender is asked to fill the window from the outbound byte stream: it reads from the stream and sends as many TCPSenderMessages as possible, as long as there are new bytes to be read and space available in the window. It sends them by calling the provided transmit() function on them. You’ll want to make sure that every TCPSenderMessage you send fits fully inside the receiver’s window(你发送过的所有都在window内,这个公式我在下面给,这是在第三章里面提出过的). Make each individual message as big as possible, but no bigger than the value given by TCPConfig::MAX PAYLOAD SIZE (1452 bytes).(根据这个切割) You can use the TCPSenderMessage::sequence_length() method to count the total number of sequence numbers occupied by a segment. Remember that the SYN and FIN flags also occupy a sequence number each, which means that they occupy space in the window.
给出公式来满足在window内传输
于是乎,我们只需要在发送端这边保证已交付给Ip的数据但是还没有被确认的数据的大小小于等于接收端接收窗口大小即可: 传送端满足(注意TCP是累计确认的方式喔):
程序里面可以略微改一下,因为LastBytesSent的位置不太好,我们往后移动一位,变成NextByte2Sent,并且LastByteAcked转换为发送方的视角,则这个ack的内容是指的nextbytes,公式改为:
+1来自于NextBytes2Sent相对于LastBytesSent的一个偏移,注意我们写程序的时候≤可能要变成<喔,比如写的while循环
对于取出应用层调用bytestream的数据,我的想法还是那部分数据取出来后要保存,等对方确认后才丢弃,但是取出来后就要从发送方的bytestream里面去掉(pop出来),因为按照题目所说,重传也只是按照当时分的片,整片整片传输,那么我就需要把当时传出去的那个片给保留一段时间,因为可能需要重传嘛。并且记着ACK一次过后需要重开timer喔。
- check3里面强调了一个:You’ll implement the retransmission timer: an alarm that can be started at a certain time, and the alarm goes off (or “expires”) once the RTO has elapsed. We emphasize that this notion of time passing comes from the tick method being called—not by getting the actual time of day.这是什么意思呢?我为什么需要实现一个重传的计时器?不是重传每次由tick调用吗?为了理解这一点,我再去读check3中的提示:
In addition to sending those segments, the TCPSender also has to keep track of its outstanding segments until the sequence numbers they occupy have been fully acknowledged. Periodically(定期的), the owner of the TCPSender will call the TCPSender’s tick method, indicating the passage of time. The TCPSender is responsible for looking through its collection of outstanding TCPSenderMessages and deciding if the oldest-sent segment has been outstanding for too long without acknowledgment (that is, without all of its sequence numbers being acknowledged). If so, it needs to be retransmitted (sent again).
我懂了,它的意思是我们的时间概念由tick来产生,tick每次给出此次tick与上一次tick经过的时间,我们需要做一个时间积累,这样就能检测是否超时,并且当有超时的时候进行归零。
- 有个问题是接收receiver的msg的时候,receiver的ackno是对于之前发送方发送的stream的index的ack,所以这里的zeropoint就是指的发送方最开始的isn_,所以我们对于接收到的ack,就调用unwrap函数,zeropoint是isn_,而checkpoint是LastByteAcked。之前忽略了一个地方,就是说返回的ackno指的是nextbyte接收方还没收到的
- 天坑,这里是需要去完整理清三次握手过程的,我是傻逼,没有理解到,重新改了一些程序结构
- 发生一次超时过后,我们的等待时间会倍增,而且前面说错了,需要记录的不是冗余ack进行快速重传,而是去记录重传次数来看看是否这条tcp连接应该放弃
- 注意接收方会在接收到FIN后还会发送一次收到FIN的ack,这个时候就不需要再次发送FIN
- You’ll want to make sure that every TCPSenderMessage you send fits fully inside the receiver’s window. Make each individual message as big as possible, but no bigger than the value given by TCPConfig::MAX_PAYLOAD_SIZE (1452 bytes).这个MAX_PAYLOAD_SIZE 是谁的大小?sendMsg的还是它的payload的大小?我认为是它的payload大小
写完了,里面遇到了不少细节,我强行用if else然后拿了许多变量进行打补丁,实在是不优雅,接下来我打算理一下思路,进行优化代码。
代码优化
从commit msg为:lab3 finish with shit like code, full of idea and patch
可以知道我那写的多垃圾
想想几个关键的地方:
一、最最最恶心的莫过于FIN这个信息,这个信息到底什么时候会发送?
我觉得应该有两个地方:
- 被捎带发送,如果发送方发送数据,然后发现诶,好像还有空余的,那么就可以把FIN发送走吧,但是这里有个问题:怎么检测需要发送FIN,难道是writer方is_close就去吗?好像不行,因为有一种情况:FIN通过第二种方式,单独发送过后,writer仍然是close,但是你想,这个时候岂不是FIN被单独发送走,没有经过writer,那岂不是也不会再出现发送payload数据的情况了吗?因为发送方已经不发送数据了,而FIN也是被单独发送走了的,所以这种情况我们pass掉,那再想想,捎带了数据发送了FIN,但是后面单独发FIN的情况怎么处理?让我们到第二个情况来看
- 单独发送,这里有个地方要考虑,要是FIN之前被捎带了,那么单独发送这里怎么知道FIN被捎带了?我shit版本的处理是增加一个bool,只要FIN在两种情况中的任意一种被捎带,就把这个bool值置为false,实在不优雅。我一直想把这两个情况进行合并,甚至把握手过程也合并,因为我们并不是严格的三次握手,这就要解决一个问题:
我们的发送方push的时候,需要有内容才会push,内容包括FIN SYN 和 pay_load,只有其中一个不为空即可,SYN可以单独鉴别,并且发起syn不会带payload,所以只需要很简单,把它和正常发送内容一样包含进来,第一次push的时候多一个SYN即可。检验时只需要检验是否为第一次,我们给一个SYN变量作为bool值信号即可,这个是可行的,我实验成功了,下一步是把FIN这个部分也合并进来,不然实在是太不优雅了,但是有个很细节的问题:我想通过判定writer().is_closed()方法来判定是否需要发送FIN,但是第一次发送完FIN后,会收到一个回应,也即是ack你的FIN,但是这个时候本不应该发送内容,但writer().is_closed()判定仍然生效,那么就会再次多发送一个信息,这是在send_close test文件里面的“FIN acked test”,里面出现的问题。
- 最大的问题就是FIN的捎带,怎么带?它要占一个byte在rwnd里面,最开始就加入它吗?那怎么鉴别捎带不了?就是发现有数据并且加不进去了,那么就发现捎带不了咯,就吐出来一个FIN(如果有的话,再截取)
对比了一下改进之前,删除的行数不多,但是思路上更清晰了。
- 作者:liamY
- 链接:https://liamy.clovy.top/article/cs144/lab3
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。