cs144实验
lab0 :warm up
第0张就是温习一下网络的相关知识。
1 Set up GNU/Linux
配置环境就直接跳过了,按照官方教程就行
2 Networking by hand
1 | $ telnet cs144.keithw.org http |
终端输入上述代码
新建一个会话进行观察
1 | $ netcat -v -l -n -p 9090 |
3 Writing webget
思路
实现 apps/webget.cc 中的 get_URL().
实现思路基本按照实验指导的提示和代码注释, 建立连接后发送 HTTP 请求报文. 然后打印回复报文的内容. 由于回复报文可能不止一个, 因此需要通过检查 EOF 标志位来判断是否接收完毕.
void get_URL(const string &host, const string &path) {
// Your code here.// You will need to connect to the “http” service on
// the computer whose name is in the “host” string,
// then request the URL path given in the “path” string.
// Then you'll need to print out everything the server sends back,
// (not just one call to read() -- everything) until you reach
// the "eof" (end of file).
// 直接参照tcp socks的api来哦发请求,使用address,还有
TCPSocket socks{};
socks.connect(Address(host,"http"));
// 发送api
socks.write("GET "+path+" HTTP/1.1\r\nHost: "+host+"\r\n\r\n");
socks.shutdown(SHUT_WR);
while(!socks.eof()){
cout<<socks.read();
}
// 关闭管道
socks.close();
cerr << "Function called: get_URL(" << host << ", " << path << ").\n";
cerr << "Warning: get_URL() has not been implemented yet.\n";
}
4.in-memory reliable byte stream
任务二要求我们实现一个内存内的有序可靠字节流:
字节流可以从写入端写入,并以相同的顺序,从读取端读取
字节流是有限的,写者可以终止写入。而读者可以在读取到字节流末尾时,不再读取。
字节流支持流量控制,以控制内存的使用。当所使用的缓冲区爆满时,将禁止写入操作。
写入的字节流可能会很长,必须考虑到字节流大于缓冲区大小的情况。即便缓冲区只有1字节大小,所实现的程序也必须支持正常的写入读取操作。
在单线程环境下执行,无需考虑多线程生产者-消费者模型下各类条件竞争问题。
参照这张图,我们现在要实现的是bytestream,他的主要功能就是从队头取文件,队尾放文件。符合这个的数据结构是双端队列,dequeue。因此我们需要加入这个数据结构
下面,我们来看他需要实现的方法,还需要那些额外变量
1 | public: |
我们发现,bool还有size_t是需要进行返回的函数,因此加入 bool is_eof; size_t _capacity; size_t _written_size; size_t _read_size; 最后我们还要加上上面推理得到的数据结构双端队列deque deque
1 | class ByteStream { |
首先是实现构造函数,把上面新加入的变量全部初始化
ByteStream::ByteStream(const size_t capacity): _capacity(capacity),is_eof(false),_written_size(0),_read_size(0),_output() {}
接下来,我们把需要返回的,先进行返回
1 | void ByteStream::end_input() {is_eof=true;} |
- 结束输入就是设置标识符eof为结束
- eof代表牌既不能输入,而且也不能读取,就是没有队列长度
- 剩余空间就是最先开始的长度-当前buffer占用的长度
接下来我们就是实现,双端队列的write
size_t ByteStream::write(const string &data) {
- 我们首先需要判断,还能不能写,不能写就返回
- 之后进行判断,当前能写的长度和data的长度,哪一个少,我们王少的写,同时写入长度增加
- 最后返回写入了多少长度
1 | size_t ByteStream::write(const string &data) { |
下一个函数peek_out和队列的输出后汉书一样,输出队头元素
- 判断最长能输出的长度
- 然后调用队列的输出pop,
1 | string ByteStream::peek_output(const size_t len) const { |
下一个pop。和队列的pop一样,参照上面的,但是需要加入到已经读取了read_size
1 | void ByteStream::pop_output(const size_t len) { |
现在我们要实现read代码,他的思路就是调用peek,还有pop就行
1 | //! Read (i.e., copy and then pop) the next "len" bytes of the stream |
5 总结
总体来说第一个还是比较简单的,我们按照这个图发现,byte_stream是最底层的模块,他的作用就是退工读写队列,明白他是双端队列就好办了
明白了,这个,我们需要实现的就是write还有read功能,write写入到队尾,看剩余长度够不够,选择最小的进行写入。read也是选择最小的进行读取。思路和使用普通的队列差不多
lab1 :StreamReassembler
在我们所实现的流重组器中,有以下几种特性:
接收子字符串。这些子字符串中包含了一串字节,以及该字符串在总的数据流中的第一个字节的索引。
流的每个字节都有自己唯一的索引,从零开始向上计数。
StreamReassembler 中存在一个 ByteStream 用于输出,当重组器知道了流的下一个字节,它就会将其写入至 ByteStream中。
需要注意的是,传入的子串中:
子串之间可能相互重复,存在重叠部分
但假设重叠部分数据完全重复。
不存在某些 index 下的数据在某个子串中是一种数据,在另一个子串里又是另一种数据。
重叠部分的处理最为麻烦。
可能会传一些已经被装配了的数据
如果 ByteStream 已满,则必须暂停装配,将未装配数据暂时保存起来
除了上面的要求以外,容量 Capacity 需要严格限制:
为了便于说明,将图中的绿色区域称为 ByteStream,将图中存放红色区域的内存范围(即 first unassembled - first unacceptable)称为 Unassembled_strs。
CS144 要求将 ByteStream + Unassembled_strs 的内存占用总和限制在 Reassember 中构造函数传入的 capacity 大小。因此我们在构造 Reassembler 时,需要既将传入的 capacity 参数设置为 ByteStream
的缓冲区大小上限,也将其设置为first unassembled - first unacceptable的范围大小,以避免极端情况下的内存使用。
思路:
这一个的任务是要求我们实现重组机器reassemble
主要功能包括对收到的字符串进行排序,之后传入到之前的byte_stream写入到缓存里面
根据上面的图,我们可以确定,绿色的是已经排序好了的,是在byte_tream里面的
绿色的+红色的是capacity,我们目前能放入到assemble的只有红色的
我们确定还没有排好序的为next_index,最大读取的就是next_index+(capacity-byte_stream。size)
同时在红色地区可能有元素,就是没有排序,我们使用unordered_map<int,char>来进行记录,下表的值,例如rec[100]=”c”这也是为了让byte_stream容易write
因此我们需要
根据上面的思路,我们来看api, void push_substring(const std::string &data, const uint64_t index, const bool eof);
- 需要我们把data放入到index的位置,并且设置eof
- 首先根据上面的分析,最长能够到达的是next_index+(capacity-byte_stream。size),如果大于等于,就说明超过了,直接return
- 接下来就是如果index+data.size()<=next_index+_output.remaining_capacity()&&eof表示写入完成,那么我们设置,stream_byte的is_eof为true
- 之后就是开始加载到reassemble的流程,和之前一样,data的前半部分可能已经被加载到重组器里面了,我们选择最大的开始,max(next_index,index),然后选择最小的长度,作为能放入到重组器的min(iondex+data.size,next_index+(capacity-byte_stream。size),作为读取的开始,然后我们放入到rec作为缓冲 注意(rec可能当前索引i已经有值了,这时候直接跳过
上半部分代码:
1 | // DUMMY_CODE(data, index, eof); |
在已经写完了的情况下,我们就是对rec进行重拍程字符串,丢到stream_byte来进行写入,遍历next_index,看当前是不是有索引值,悠久进行加入,然后同事,构造一个新的字符串,直到next_index没有值,我们就不在进行while。结束之后,如果有eof标志,也要进行设置byte_stream为写入完成
1 | //写入结束,放入到output里面 |
整体代码如下
1 | void StreamReassembler::push_substring(const string &data, const size_t index, const bool eof) { |
剩下的我们,直接返回所需要的元素就行。设置那个重组器构造函数
1 |
|
总结
这一个的作业,主要是处理充排气,我们都知道tcp接受的时候,可能会先收到后发来的tcp分段,乱序到达,需要进行排列之后才能写入,所以这一届的任务就是实现重拍器实现tcp发送的一个任务。主要的难点就是完全理解这张图
知道这个容量是byte_stream+还没有重组的,然后使用数据结构map,记录每一个位置的索引值
Lab 2: the TCP receiver
经典看不懂到底说的是什么
结合 Lab1 中实现的字节流重组器,可以发现,在数据的收发过程中存在几种序列号:
- 序列号
seqno
:32bit 无符号整数,从初始序列号 ISN 开始递增,SYN 和 FIN 各占一个编号,溢出之后从 0 开始接着数 - 绝对序列号
absolute seqno
:64bit 无符号整数,从 0 开始递增,0 对应 ISN,不会溢出 - 字节流索引
stream index
:64bit 无符号整数,从 0 开始递增,不考虑 SYN 报文段,所以 0 对应 ISN + 1,不会溢出
假设 ISN 为 232−2232−2,待传输的数据为 cat
,那么三种编号的关系如下表所示:
由于 uint32_t
的数值范围为 0∼232−10∼232−1,所以 a
对应的报文段序列号溢出,又从 0 开始计数了。
处于安全性考虑,以及避免与之前的 TCP 报文混淆,TCP 需要让每个 seqno 都不可被猜测到,并且降低重复的可能性。因此 TCP seqno 不会从 0 开始,而是从一个 32 位随机数起步(称为初始序列号 ISN)
思路:
- 首先实现序列号转绝对序列号,因为我们发送的是32位的seqno,但是接受之后,他会变成64位的,但是因为64位的位置更大,所以我们需要checkpoint,做为基准点,看他到底是哪一个
- 从64位转到32位,就容易点,根据上面的公式,进行移项,我们只需要abs+isn,强转到32位就行
1 | //! Transform an "absolute" 64-bit sequence number (zero-indexed) into a WrappingInt32 |
1 | uint64_t unwrap(WrappingInt32 n, WrappingInt32 isn, uint64_t checkpoint) |
对于这个api,我们根据上面的分析是checkpoint是附近的值,来基于确认的我们知道(seqno之间的差距)一定等于abs之间的差距,所以我们可以先把chekpoint转到32来,计算他与n之间的差别,然后checkpoint与差值进行计算,因为是uint,所以一定大于0.如果小于0,需要加上2的32
1 | uint64_t unwrap(WrappingInt32 n, WrappingInt32 isn, uint64_t checkpoint) { |
TCPReceiver 实现
要求
需要实现一些类成员函数
segment_received()
: 该函数将会在每次获取到 TCP 报文时被调用。该函数需要完成:如果接收到了 SYN 包,则设置 ISN 编号。
注意:SYN 和 FIN 包仍然可以携带用户数据并一同传输。同时,同一个数据包下既可以设置 SYN 标志也可以设置 FIN 标志。
将获取到的数据传入流重组器,并在接收到 FIN 包时终止数据传输。
ackno()
:返回接收方尚未获取到的第一个字节的字节索引。如果 ISN 暂未被设置,则返回空。window_size()
:返回接收窗口的大小,即第一个未组装的字节索引和第一个不可接受的字节索引之间的长度。
这是 CS144 对 TCP receiver 的期望执行流程:
第三个我们可以直接返回,因为我们在重组器就知道,这个串口就是容量-byte_stream的长度
1 | size_t TCPReceiver::window_size() const { return _capacity-_reassembler.stream_out().buffer_size(); } |
然后我们看这个api,**void TCPReceiver::segment_received(const TCPSegment &seg) **, 作用就是把收到的tcp片段,进行设置,如果我们自己没有syn,这个有,就设置他为isn,并且在把这个包传入到重组器里面。同时我们进行写入的是index,需要转换成64位(上文的unwarp),然后索引还需要-1,data是在seg的payload字段
1 | void TCPReceiver::segment_received(const TCPSegment &seg) { |
我接下来,我们看 optional
1 | optional<WrappingInt32> TCPReceiver::ackno() const { |
总结
这一届的任务就是让我们知道接收器的作用。
- 接受分段,设置isn,然后交给重拍器进行处理
- 返回ackno,下一个需要对方发来的序列号
- 计算串口,能容纳多少,为了拥塞控制
lab 3 :TCPSender 实现
我们已经实现了receiver部分,现在就是sende部分。
在该实验中,我们需要完成 TCPSender 的以下四个接口:
fill_window:TCPSender 从 ByteStream 中读取数据,并以 TCPSegement 的形式发送,尽可能地填充接收者的窗口。但每个TCP段的大小不得超过
TCPConfig::MAX PAYLOAD SIZE
。若接收方的 Windows size 为 0,则发送方将按照接收方 window size 为 1 的情况进行处理,持续发包。
因为虽然此时发送方发送的数据包可能会被接收方拒绝,但接收方可以在反向发送 ack 包时,将自己最新的 window size 返回给发送者。否则若双方停止了通信,那么当接收方的 window size 变大后,发送方仍然无法得知接收方可接受的字节数量。
若远程没有 ack 这个在 window size 为 0 的情况下发送的一字节数据包,那么发送者重传时不要将 RTO 乘2。这是因为将 RTO 双倍的目的是为了避免网络拥堵,但此时的数据包丢弃并不是因为网络拥堵的问题,而是远程放不下了。
ack_received:对接收方返回的 ackno 和 window size 进行处理。丢弃那些已经完全确认但仍然处于追踪队列的数据包。同时如果 window size 仍然存在空闲,则继续发包。
tick:该函数将会被调用以指示经过的时间长度。发送方可能需要重新发送一些超时且没有被确认的数据包。
send_empty_segment:生成并发送一个在 seq 空间中长度为 0 并正确设置 seqno 的 TCPSegment,这可让用户发送一个空的 ACK 段。
思路分析
我们首先拿最简单的trick进行分析,**void TCPSender::tick(const size_t ms_since_last_tick)**,这个函数的作用就是传入时间来进行模拟时钟,如果景观这些时间超过了定时器规定的时间,就需要进行重新发送文件,同事重启定时器。
为了使用方便,我们自定义一个定时器
1 | class Timer { |
剩余时间,规定的超时时间,还有是否开启的定时器,我们只需要一下几个函数,开启,关闭,是不是超时,设置rto时间,还有更新剩余时间。
对于trick这个函数,思路就是
- 首先更新定时器剩余时间,如果没有超过,就直接return
- 如果超过,再看待确认队列是不是空的,是空的,就初始化为最开始的rto,然后返回
- 不是,就需要进行重传,重新放入到sender的发送队列,并且更新rto,加倍红船时间(timer的设置时间函数)
- 最后打开定时器
1 | void TCPSender::tick(const size_t ms_since_last_tick) { |
接下来就是返回能直接返回的,我们设置重传变量,到时候直接返回
1 | unsigned int TCPSender::consecutive_retransmissions() const { return _outstand_bytes; } |
返回没有被确认的字节数
1 | uint64_t TCPSender::bytes_in_flight() const { return _outstand_bytes; } |
‘
现在我们研究fill_windows这个函数,void TCPSender::fill_window(),函数的意识,就是让我们发送byte_stream里面的byte,这个就是发送代码。由于tcp是可靠传输,他有选择重传还有超时重传的机制,保证tcp发送seg能够被确认。因此这个ill_window的作用,就是进行发送seg。
整个作用就是
- 发送最大的长度seg,加入到待确认队列里面
- 如果还没有设置syn表示,就自己先发一个syn包
- 最后,如果是byte——stream已经发送完了,而且窗口还有剩余的,那就发送fin停止包
(注意)L:haishi需要按照上面这幅图来理解,ack代表已经进行确认的,next代表下一个序列号,那么我们最大能发送的max(_window_size, static_cast
- 如果当前还没有建立通信syn,而且等待队列也是空的,那么首先我发syn
- 如果不是,就首先计算,当前最大的剩余大小max(_window_size, static_cast
(1)) + _ack_seq - _next_seqno - 然后计算byte_stream能够发送的最大值,之后,剩余减去最大值,一直while
- 去除data,构建seg(我们自己实现了一个新的发送seg方法
- 之后如果把byte发送完了,但是窗口还有剩余,我们就发送fin结束包
发送代码
- 首先构造seg,之后进行设置他head,包括,syn,fin,还有序列号(这个直接用next——seqno)生成
- 放入到待确认的队列,更新待确认的byte的长度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 void TCPSender::send_segment(string &&data, bool syn, bool fin) {
// 创建报文段
TCPSegment segment;
segment.header().syn = syn;
segment.header().fin = fin;
segment.header().seqno = next_seqno();
segment.payload() = std::move(data);
// 将报文段放到发送队列中
_segments_out.push(segment);
_outstand_segments.push({segment, _next_seqno});
// 更新序号
auto len = segment.length_in_sequence_space();
_outstand_bytes += len;
_next_seqno += len;
}
整体代码如下:
1 | void TCPSender::fill_window() { |
这个代码就相当于是tcp里面的发送代码,建立绘画,并且把没有进行确认的seg放入到待确认的队列里面
下面我们来看 void TCPSender::ack_received(const WrappingInt32 ackno, const uint16_t window_size) 这个api
他传入的是返回号ackno,还有更新发送窗口的大小。这个就相当于得到ack号来进行确认之前传的文件是不是已经收到
- 如果当前ack号<=我自己的ackno,相当于返回的是没有,直接return
- 如果当前的ack,大于我要法的next_seqno,那也是没用
- 我们只要接受到ack号,就把拥塞控制回复到最开始,重传次数也变成0
- 之后就是累计确认
- 确认完成之后,我们再次进行发送使用fill_window
- 最后如果还有没有进行确认的,就需要我们进行使用打开计时器了
1 | void TCPSender::ack_received(const WrappingInt32 ackno, const uint16_t window_size) { |
这一部分主要是为了完成,ack发送器的确认,确认成功之后,我就接着再次发送,相当于tcp的三次握手,首先,是建立syn,没有收到syn,那就是我来进行发送,之后获取到ack,那么我们就代表建立成功,更新syn为true,同事更新本地ack为接收到的ack,然后累计确认已近收到了的seg,最后收到确认之后,sender还是需要进行发送,调用fill—windows,最后来进行设置计时器打开与关闭
总结
这一届需要我们实现的是tcp的发送端,我们需要完成发送byte里面的字段,然后放入到待确认的队列里面,根据会传来的ackno来进行累计确认还有更新,相当于实现tcp的三次握手阶段,我们需要进行发送的文件方法,然后使用累计确认,来确保tcp完整的连接。
lab4 tcp connection
投降了,这一个,直接抄的CS144计算机网络 Lab4 | Kiprey’s Blog,完全看不懂到底在说什么,不知所云,边界测试条件也是一堆,放弃了
TCPConnection 需要将 TCPSender 和 TCPReceiver 结合,实现成一个 TCP 终端,同时收发数据。
TCPConnection 有几个规则需要遵守:
对于接收数据段而言:
如果接收到的数据包设置了 RST 标志,则将输入输出字节流全部设置为 错误 状态,并永久关闭 TCP 连接。
如果没有收到 RST 标志,则将该数据包传达给 TCPReceiver 来处理,它将对数据包中的 seqno、SYN、payload、FIN 进行处理。
如果接收到的数据包中设置了 ACK 标志,则向当前 TCPConnection 中它自己的 TCPSender 告知远程终端的 ackno 和 window_size。
这一步相当重要,因为数据包在网络中以乱序形式发送,因此远程发送给本地的 ackno 存在滞后性。
将远程的 ackno 和 window size 附加至发送数据中可以降低这种滞后性,提高 TCP 效率。
如果接收到的 TCP 数据包包含了一个有效 seqno,则 TCPConnection 必须至少返回一个 TCP 包作为回复,以告知远程终端 此时的 ackno 和 window size。
如果接收到的 TCP 数据包包含的 seqno 是无效的,则 TCPConnection 也需要回复一个类似的无效数据包。这是因为远程终端可能会发送无效数据包以确认当前连接是否有效,同时查看此时接收方的 ackno 和 window size。这被称为 TCP 的
keep-alive
。1
2
3COPYif (_receiver.ackno().has_value() && seg.length_in_sequence_space() == 0 && seg.header().seqno == _receiver.ackno().value() - 1) {
_sender.send_empty_segment();
}
对于发送数据段来说:
- 当 TCPSender 将一个 TCPSegment 数据包添加到待发送队列中时,TCPConnection 需要从中取出并将其发送。
- 在发送当前数据包之前,TCPConnection 会获取当前它自己的 TCPReceiver 的 ackno 和 window size,将其放置进待发送 TCPSegment 中,并设置其 ACK 标志。
TCPConnection 需要检测时间的流逝。它存在一个 tick 函数,该函数将会被操作系统持续调用。当 TCPConnection 的 tick 函数被调用后,它需要
- 告知 TCPSender 时间的流逝,这可能会让 TCPSender 重新发送被丢弃的数据包
- 如果连续重传次数超过
TCPConfig::MAX RETX ATTEMPTS
,则发送一个 RST 包。 - 在条件适合的情况下关闭 TCP 连接(当处于 TCP 的 TIME_WAIT 状态时)。
TCP 连接的关闭稍微麻烦一些,主要有以下几种情况需要考虑:
接收方收到 RST 标志或者发送方发送 RST 标志后,设置当前 TCPConnection 的输入输出字节流的状态为错误状态,并立即停止退出。这种属于暴力退出(unclear shutdown),可能会导致尚未传输完成的数据丢失(例如仍然在网络中运输的数据包在接收方收到RST标志后被丢弃)。
若想让双方都在数据流收发完整后退出(clear shutdonw),则情况略微麻烦一点。先上张四次挥手的图:
简单讲下挥手的流程:
当客户端的数据全部发送完成,则将会发送 FIN 包以告知服务器 客户端数据全部发送完成(发送完成,不等于被接收完成)。但请注意,此时的服务器仍然可以发送数据至客户端。
当服务器对 客户端的 FIN 进行 ack 后,则说明服务器确认接收客户端的全部数据。
服务器继续发送数据,直到服务器的数据已经全部发送完成,则向客户端发送 FIN 包以告知服务端数据全部发送完成。
当客户端对服务端的 FIN 发送 ack 后,则说明客户端确认接收服务端的全部数据。注意,此时客户端可以确认:
- 服务端成功接收客户端全部数据
- 客户端成功接收服务端的全部数据
此时客户端可以百分百相信,此时断开连接对客户端是没有任何危害的。
但是!当服务器没接收到 客户端的 ACK 时,
- 服务器可以确认它成功接收客户端全部数据
- 服务器不知道客户端是否成功接收服务端的全部数据
也就是说,服务器一定要获得到客户端的 ACK 才能关闭。
若服务器在超时时间内没获得到客户端的 FIN ACK,则会重发 FIN 包。但假如此时客户端已经断连,那么服务器将永远无法获取到客户端的 FIN ACK。因此即便客户端已经完成了它的所有任务,它仍然需要等待服务器端一小段时间,以便于处理服务端的 FIN 包。
当服务器获取到了客户端的 FIN_ACK 后,它就直接关闭连接。而客户端也会在超时后静默关闭。此时双方均成功获取对方的全部数据,没有造成任何危害。
这里有个很重要的点是,TCP 不会对 ACK 包来进行 ACK。例如服务端不会对客户端发来的 FIN_ACK
分析
常见的返回值,直接调用receiver或者sender得到,或者自己加入这个值
[CS144] Lab 4: The TCP connection_cs144 lab4 tcpconnection实现笔记_PeakCrosser的博客-CSDN博客
1 |
|
首先还是先用trick函数来实现,void TCPConnection::tick(const size_t ms_since_last_tick) ,这个传入的是时间,我们需要用这个歌时间,进行更新sender的trick,还有更新,上次接受到的时间片段。如果经过这个sender的trick,定时器重传次数超时了,我们就需要关闭这个连接,使用rst的方式关闭连接。如果是正常关闭,那就设置active为false就行,然后发送segment
对于发送数据段来说:
- 当 TCPSender 将一个 TCPSegment 数据包添加到待发送队列中时,TCPConnection 需要从中取出并将其发送。
- 在发送当前数据包之前,TCPConnection 会获取当前它自己的 TCPReceiver 的 ackno 和 window size,将其放置进待发送 TCPSegment 中,并设置其 ACK 标志。
参照上面的思路,我们需要做的就是,从sender里面的发送队列选择seg,然后,加入ackno,来作为发送的标识(这个是receiver)里面进行得到,最后就是设置窗口大小,然后放入到connection的发送队列里面
主要功能就是从sender取值,然后装配ack,之后放入到connection的
- sender获取seg
- receiver获取ackno进行装配
- 放入到connection得到发送队列里面
1 | void TCPConnection::send_segments() { |
下面实现connect这个,这个代码只会在刚开始建立连接的时候才是用,我们的作用就是发送数据到sender,然后connection从sender进行取值
1 | void TCPConnection::connect() { |
write()
方法
该方法即上层应用向 TCP 的字节流中写入数据进行发送.写数据
调用sender的byte进行写入数据
1 | size_t TCPConnection::write(const string &data) { |
对于接收数据段而言:
如果接收到的数据包设置了 RST 标志,则将输入输出字节流全部设置为 错误 状态,并永久关闭 TCP 连接。
如果没有收到 RST 标志,则将该数据包传达给 TCPReceiver 来处理,它将对数据包中的 seqno、SYN、payload、FIN 进行处理。
如果接收到的数据包中设置了 ACK 标志,则向当前 TCPConnection 中它自己的 TCPSender 告知远程终端的 ackno 和 window_size。
这一步相当重要,因为数据包在网络中以乱序形式发送,因此远程发送给本地的 ackno 存在滞后性。
将远程的 ackno 和 window size 附加至发送数据中可以降低这种滞后性,提高 TCP 效率。
如果接收到的 TCP 数据包包含了一个有效 seqno,则 TCPConnection 必须至少返回一个 TCP 包作为回复,以告知远程终端 此时的 ackno 和 window size。
如果接收到的 TCP 数据包包含的 seqno 是无效的,则 TCPConnection 也需要回复一个类似的无效数据包。这是因为远程终端可能会发送无效数据包以确认当前连接是否有效,同时查看此时接收方的 ackno 和 window size。这被称为 TCP 的
keep-alive
。
那句上面的介绍,我们知道connection的接送流程是什么。
- 判断是不是rst,是就设置rxt,然后直接非正常关闭
- 不是就交给receiver处理,使用receiver的segment-receiver(得到index,还有数据交给重组器进行处理)
- 同事这个接受端如果设置ack,就要交给sender进行处理,这样构成tcp可靠传输,更新窗口
- 同时接收到欧晓的seqno,就需要我们进行返回我们的窗口大小
- 如果无效,发送一个空的seg
end_input_stream()
方法
该方法即结束需要发送的数据流, 即出站流. 因此需要调用 _sender.steam_in().end_input()
方法. 而结束流的隐含信息是要发送一个 FIN 报文段,
sender的作用就是进行发送byte—stream的数据,所以我们首先进行设置byte-stream为结束状态,然后调用这个fill,进行发送fin挥手包,最后还是需要发送到connection的
(只要使用fill-windows,就一定要进行发送到connection
1 | void TCPConnection::end_input_stream() { |
1 | void TCPConnection::segment_received(const TCPSegment &seg) { |
总体流程如下
- 检测是不是还不是alive
- 检查是不是rst
- 接下来看,是不是刚开始建立绘画的ack状态,(没有ackno还有next发送的,刚开始进行建立)
- 之后就是receiver接受这个seg,同事如果有ack,那么sender也需要进行校验
- 如果是刚建立的,那么还是需要进行发送fill_windows作为返回放入到sender的发送器
- 如果自己的发送器还有接收器都为空,那么就说明彻底结束,不需要继续keep alive
- 如果接收到的 TCP 数据包包含的 seqno 是无效的,则 TCPConnection 也需要回复一个类似的无效数据包,返回处理特殊情况
1 | void TCPConnection::segment_received(const TCPSegment &seg) { |
最后一个就是关闭,分为正常操作,还有不正常。正常直接就是不在activate活跃,不正常全部进入error状态
下面是正常关闭
TCP 的正常关闭即 TCP 的四次挥手, 是本实验中最为复杂的地方, 其中也有笔者不是很确定的地方.
TCP 正常关闭的四个前提已经在 #要点 中提到, 如下即前提与代码的对应:Prereq#1: _receiver.unassembled_bytes()==0 && _receiver.stream_out().input_ended(), 实际上可以直接转化为 _receiver.stream_out().input_ended()(此处使用 _receiver.stream_out().eof() 方法同样能通过测试, 任务指导中描述使用的 ended, 因此这里笔者选择了前者).
Prereq#2: _sender.stream_in().eof(). 这里需要是 eof() 方法而非 input_ended(), 因为要确保发送字节流的所有数据已经全部发送出去.
Prereq#3: _sender.bytes_in_flight()==0
主要就是发送器为eof,接收器为eof,然后还没有没被确认的byte
1 | // TCP clean shutdown |
不正常的关闭就是设置为error,然后也是关闭
1 | inline void TCPConnection::unclean_shutdown() { |
最后一个就是rst,这个作用就是封装的时候,我们从sender里面提取的seg加入一个rst标志位,然后再次调用connection的发送代码来进行发送。
1 | void TCPConnection::send_RST() { |
整体代码
1 |
|
总结
这一届主要是实现如何建立连接,在两个对等节点之间。我们调用之前实现的sender还有receiver来进行处理。这一节的边界条件太多,直接参考的别人的实现。
主要实现的功能就是发送还有接受
connection的发送功能就是从sender里面获取seg,然后进行封装ack来发送,放入到自己的connection的队列
connection的接受功能就比较多,建立会话联系,1.刚开始建立syn的时候收到seg,这个歌时候要看他有没有syn标识,有就直接交给receiver处理,因为这个时候不需要进行校验ack,保证tcp的完整性。2.如果是rst错误,直接不安全的关闭连接。3.普通的seg,来确保收到的正确,这个时候就需要receiver还有sender,receiver手机seg,sender使用ackno更新窗口还有确认之前发送的已经到达,之后还需要发送自己文件,sender使用fill-windows来发送。最后是特殊条件,还有放入到sender队列里面,这个时候,就需要connection进行发送。
trick的功能
- 更新sender里面的定时器
- 然后超时设置rst
- 之后就是接着发送send(因为重传,可能会放入新的seg到sender的队列里面)
剩下的就是普通的代码,可以直接返回的。
总体来说,这个代码还是非常难得。不像xv6那种有提示,这个代码的提示基本没有,操作说明说了和没有说一样,只能按照别人已经实现了的代码,来倒退这个函数到底是要干什么。这几个la做完,的确对tcp的可靠传输,还有拥塞控制有了更一步的了解。主要的是还是要参考,下面2张图。
知道sender,还有receiver的底层
然后重组器的实现机制,和发送器能发送的窗口。
- 重组器的next就是第一个没有assemble的
- 发送器的窗口大小是ack+windows—next,ack是第一个还没有读入到的,绿色的就是没有确认的