lab0:热身
获取网页
1. 直接访问http://cs144.keithw.org/hello,可以看到返回的是一个HTML页面
2. 现在用telnet命令连接到http://cs144.keithw.org/hello,手动构建一个HTTP请求报文,看是否能得到相同结果,HTTP请求报文的格式如下:
![](https://i-blog.csdnimg.cn/blog_migrate/83bc8e42d285c3eb7296f1a118dacb51.png)
可以看到telnet构建http报文发送后成功返回HTML页面的代码
这里官方还留了个小作业,就是试着访问 http://cs144.keithw.org/lab0/sunetid,根据sunetid返回相应的secret code
webget
开始编程部分了,课程要求使用modern C++,给出以下建议:
- 使用cppreference.com作为语言参考文档
- 不要使用malloc()或者free()
- 不要使用new或delete
- 杜绝使用裸指针,只在必要时使用智能指针
- 避免使用模板、多线程、锁和虚函数
- 避免使用C风格字符串和相关的字符串函数,因为很容易出错。可以用std::string代替
- 不要使用C风格的强制类型转换,有必要的话可以使用static_cast
- 尽量使用const引用传递参数
- 使用const声明变量,除非需要更改
- 使用const函数,除非需要在函数内修改对象
- 避免使用全局变量,并且每个变量应该维持最小的作用域
- 交作业前,用cmake --build build --target tidy来检查代码风格,cmake --build build --target format格式化代码
项目里其实已经封装了linux的网络编程接口,先读一下util/socket.hh和util/file descriptor.hh了解以下如何使用(不了解网络编程api可以看下Linux 教程 | 爱编程的大丙 (subingwen.cn)的套接字通信部分)
其实webget就是让我们用封装好的接口模拟上面的telnet,连接到对应网站然后发送一个http请求报文,并且读取对应的响应报文并输出,大概步骤如下:
- 创建socket
- 绑定地址
- 构造HTTP请求报文
- 发送报文
- 读取相应报文
- 输出
测试结果如下
内存中可靠的字节流实现
这个lab要求实现一个能在内存中可靠传输的字节流(不要求线程安全),项目里已经给出了相应的接口设计,我们只需要在src/byte_stream.hh和src/byte_stream.cc里面实现即可
注意,只能在ByteStream类中添加成员变量,在Writer和Reader中实现对应接口。说实话直接看接口描述的话没太看懂怎么用的,结合测试用例才看懂这些接口是什么意思,可以参考byte_stream_***.cc看看是怎么用的
这里简单说下我的理解:
- Writer
-
push:往缓冲区中写入一段字符串,但是写入字符串长度不能大于可写入的容量,如果大于的话只写入一部分。
-
close:关闭写端,应用层手动设置
-
is_close:是否已关闭,应用层没调用close的话就没关闭
-
available_capacity:剩余可写入的容量
-
bytes_pushed:总共写入了多少字节
-
-
Reader
-
peek:返回读缓冲的一段字符串,可以是全部,也可以是一部分,看自己实现
-
pop:一般跟peek配合使用,如果peek返回长度为2的字符串,那么一般peek之后要pop(2)
-
is_finished:写端已经关闭并且读缓存为0则返回true
-
bytes_buffered:读缓冲字符串的大小
-
bytes_popped:总共读了多少字节
-
按照以上思路,我用的是一个环形缓冲区来维护,第一版吞吐量是2.55 Gbit/s
但是看lab文档说实现是有可能大于10Gbit/s(跟电脑配置有关,我配置是i5 9th),于是分析了下实现,发现可能瓶颈在Writer::push的实现,优化了一下重新测试,优化后的吞吐量为12.79Gbit/s
lab1: 将字符串完整拼接存储到字节流中
这次实验是要实现一个重组器,来将网络中发送过来的有可能乱序、重复的包拼接完成后再存入到lab0实现的字节流当中。
课程笔记里给出的Reassembler在协议栈中的作用,负责将包收集后写入到字节流的写端,写完后关闭。
实验文档建议我们最少使用一种数据结构,我的实现逻辑是将传入的字节流用str_cache_来维护,pair里面是字节流的起始和中止索引。
std::map<std::pair<std::uint64_t, std::uint64_t>, std::string> str_cache_
然后再判断ByteStream是否有容量可以写入,有的话就立即写入,如果不能全部写入就把剩下的存回去str_cache_
维护逻辑的实现参考下面这张图,建议也看看实验文档的FAQ。
最后实现是用了将近80行代码,超过了实验要求的上限,没再去优化压行了。
实现的重组器吞吐量在7.75Gbit/s左右
lab2 实现TcpReceiver
本次实验要利用lab0实现的字节流和lab1实现的重组器来实现一个简易的TcpReceiver,ack号意味着TcpReceiver下一个要接收的字节流,流量控制用一个窗口大小来控制,TcpReceiver只接收当前窗口内的字节流。
Tcp中有三种索引,分别是序列号、绝对索引和流索引,它们的关系如下。
绝对索引和流索引之间很容易转换,就是加一和减一的关系。但是序列号和绝对索引之间就没那么容易转换了,序列号是32位的,绝对索引是64位的,起始序列号是随机的,而且序列号在到之后又会变为0,所以在传输过程中一个序列号有可能会对应多个绝对索引,这里我们要先实现一个序列号和绝对索引之间的转换器。
这里实现上需要一个checkpoint,来定位到正确绝对索引,绝对索引要取到离checkpoint最近的那一个。
接下来就是TcpReceiver的实现了,这个类已经有一个Reassembler了,按照讲义的流程来讲应该还需要一个Wrap32来存储isn,还有一个uint64的值来存储绝对索引。
实现细节:
- 要收到SYN后才开始接收消息,携带SYN的报文也可以携带消息,也可以携带FIN。
- 利用上一节实现的Wrap32来实现序列号和绝对索引的转换。
- SYN和FIN不是payload的一部分,只是TcpMessage里面的两个flag。
- 上一个lab的那张图可以参考