序言

满怀热忱开始 cs144 的旅途,第一个碰到的困难便是环境搭建。在这三天时间里不断配置、删库、配置,看了不知道多少博客(许多都是跟着文档一笔带过)和评论,最后终于完成了实验环境的搭建!

最初是打算做 SU CS144 最新的 2024 Spring minnow版本,但是它要求需要 ubuntu23、gcc 和 g++13 以上,如果环境没有达到实验要求,后面的 cmake 会出错。因此最后还是 2021 的Sponge版(听说这个版本的 lab4 TCP Connection 特别难,而在 minnow 版中直接换成了另一个简单的 lab)。

环境配置

我选择的是WSL2+VScode的方式进行实验,WSL2 安装起来非常简便而且体量轻,不仅可以在基于 Linux 的环境中进行开发,使用特定于 Linux 的工具链和实用程序,还可以在 Windows 上舒适地运行和调试基于 Linux 的应用程序。

工具链:gcc &g++ 13.1.0、gdb 12.1、make(由于我对于 gcc、g编译器,还有cmakemakegdb都不了解,甚至 C也是现学现用的,因此配置过程中遇到了非常多的问题,耗费了大量时间和精力)
工具

环境搭建

1. 从官网下载解压 gcc 和 g++

https://gcc.gnu.org/

2. 拉取 start code

–获取项目框架

由于 Stanford 官方已经把Sponge的代码库换成了最新的Minnow的库,所以为了拉到开始代码得去拉别人已经做好的实验,再用 git 回退到初始状态。非常感谢老哥 LRL52 提供的Lab start code

–git 关联远端仓库

  1. 由于 start code 是从别人的代码仓库上 clone 到本地的,如果此时直接关联自己的远程仓库git remote add origin <URL>,则会报错error: remote origin already exists
  2. 解决方法: 1. git remote rm origin删除关联的远程库 2. git remote add origin <URL>关联自己的远程库 3. git push origin main把本地仓库推送到远程仓库(Github 配置 SSH)
    git

3. 根据官方文档初始化项目

要求
在 make 时候出现了问题:
make失败
查了一下发现是/libsponge/util/address.hh没有包含<array>
make成功

4. 反思

经历了三天的挫折,环境最终还是搭建好了,但我感觉自己在这一段痛苦的实践中收获甚微:感觉自己的信息检索能力增强了些,但具体工具的知识我却没有花时间去了解。当然我的重心还是要放在具体实验上的,如果花时间在这些“无足轻重”的事情上,或许会顾此失彼因小失大

Part1-完成 webget

程序要求

webget
这个webget函数的功能就是将我们一开始手打的HTTP请求报文写进程序里,并且发送到目的服务器,获得服务器返回的响应报文,显示在终端。

首先我们要明确两个进程连接的过程。这个过程涉及客服端/服务端的socket创建,以及接下来的三次握手建立全双工(bi-directional)的(持续/非持续)连接。webget 只要求我们建立客户端的 socket,并和给定的目的主机host进行连接。我们知道建立 TCP 连接需要三次握手(three-way handshake),但我们在 socket 面向应用层的这端不需要显示地对三次握手进行编程,因为这个过程交由我们的操作系统隐式完成了。不仅如此,很多轮子官方也给我们搓好了,需要我们读一下/libsponge/util目录下的类接口(完成 webget 需要了解address.hhsocket.hh),也可以看官方的 library 网站

实验过程与源码

这是我写的 webget,因为第一次做实验一开始无处下手,所以先自己凭感觉写了一次,然后借鉴其他朋友的代码进行了修改(1. 发送报文后关闭连接 2.while 判断读到文件末尾的接口)。运行./apps/webget cs144.keithw.org /hello(根据makefile编译的 webget 可执行程序应该在build目录下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*webget.cc*/
void get_URL(const string &host, const string &path) {
    //通过默认构造函数(default constructor)创建客户端socket对象
    TCPSocket client_socket = TCPSocket();
   
    //创建server的地址
    Address server_address = Address(host, "http");
   
    //TCP三次握手(操作系统内核隐式完成)后创建TCP连接
    client_socket.connect(server_address);  
   
    //发送Http GET请求报文
    client_socket.write("GET " + path + " HTTP/1.1\r\n");
    client_socket.write("Host: " + host + "\r\n");
    client_socket.write("Connection: close\r\n");
    client_socket.write("\r\n");
   
    //关闭TCP单向连接(Write)
    client_socket.shutdown(SHUT_WR);

    //打印出接收到的字节,以EOF(End of File)为结束符号
    while(!client_socket.eof() ){
        cout << client_socket.read();
    }
    client_socket.close();
   
    cerr << "Function called: get_URL(" << host << ", " << path << ").\n";
    cerr << "Warning: get_URL() has not been implemented yet.\n";
}

webget结果

Part2-实现内存上的可靠字节流

实验要求

为了在内存上实现一个字节流,我们首先需要补充对ByteStream类的定义,再实现相应的类方法。我们需要一种数据结构来模拟接收端的缓冲区(buffer),我选择的是deque双端队列来抽象。其次还要两个数据成员total_writtentotal_read表示这个在字节流上的总读取/写入的数据量。后面我在写end_input()input_ended()方法时卡住了,还是关于如何判断字节流已经读到了末尾。参考了一下其他朋友的代码后才发现,字节流是否到结尾也是需要自己模拟的,所以又添加了end_stream成员表示字节流是否关闭。

The byte stream is finite: the writer can end the input, and then no more bytes can be written. When the reader has read to the end of the stream, it will reach “EOF” (end of file) and no more bytes can be read.

1
2
3
4
5
6
7
8
9
10
11
12
13
/*byte_stream.hh*/
class ByteStream {
  private:
//添加ByteStream类的私有成员
    std::deque<char> stream;
    bool end_stream;
    size_t stream_capacity;
    size_t total_written;
    size_t total_read;
    bool _error{};
  public:
  ...
}

调试过程

我使用 VScode 的 CMake 插件进行测试用例的调试。最开始的实现六个测试一个都没通过,修改了一下类定义后只过了byte_stream_construction测试,也就是说我的方法实现有很大的问题。下面是byte_stream_capacity测试用例的调试,可以看见maximum居然是一个很大的数字,后面一看是 maximum 写错了。我想用maximum 表示剩余的缓冲区空间,却错写成了stream.size()-data.size()(已写入的数据减去待写入的数据,我也不知道什么意思…),因此造成了数值溢出!
调试
接下来继续用测试用例 debug 修改了几个错误后,终于实现了字节流 😭!!
成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/*byte_stream.cc*/
#include "byte_stream.hh"
#include <iostream>

using namespace std;

//构造函数
ByteStream::ByteStream(const size_t capacity): stream(), end_stream(false),
 stream_capacity(capacity), total_written(0), total_read(0){}

//write方法:Write a string of bytes into the stream. Write as many as will fit, and return how many were written.
size_t ByteStream::write(const string &data) {
    if(input_ended()){
        cerr << "ByteStream is shut down, can't write data!\n";
        return 0;
    }
   
    if(stream.size() >= stream_capacity){
        cerr << "The buffer is not enough, can't write data now!\n";
        return 0;
    }
   
    const char* head = data.data();
    size_t maximum = stream_capacity - stream.size();
    size_t total_bytes;
    for(total_bytes = 0; total_bytes < min(data.size(), maximum); total_bytes++){
        stream.push_back(static_cast<char>(head[total_bytes]));
    }
    total_written += total_bytes;

    return total_bytes;
}

//peek_output方法:`len` – bytes will be copied from the output side of the buffer
string ByteStream::peek_output(const size_t len) const {
    string peek;
    if(len > stream.size()){
        cerr << "Can't peek 'len' bytes data, access exceed!\n";
        return peek;
    }

    for(int i = 0; i < int(len); i++){
        peek += stream[i];
    }

    return peek;
}

//pop_output方法:len bytes will be removed from the output side of the buffer
void ByteStream::pop_output(const size_t len) {

    for(int i = 1; i <= int(len); i++){
        stream.pop_front();
    }

    total_read += len;
}

//read方法: (i.e., copy and then pop) the next "len" bytes of the stream,len bytes will be popped and returns a string
std::string ByteStream::read(const size_t len) {
    string output;
    if(len > buffer_size()){
        cerr << "Can't read 'len' bytes data, access exceed!\n";
        return output;
    }
   
    for(int i = 1; i <= int(len); i++){
        output = output + stream.front();
        stream.pop_front();
    }
    total_read += len;
   
    return output;
}

void ByteStream::end_input() {end_stream = true;}

bool ByteStream::input_ended() const { return end_stream; }

size_t ByteStream::buffer_size() const { return stream.size(); }

bool ByteStream::buffer_empty() const {return stream.empty(); }

bool ByteStream::eof() const {
    if(stream.size() == 0 && input_ended()){
        return true;
    }
    else{
        return false;
    }
}

size_t ByteStream::bytes_written() const { return total_written; }

size_t ByteStream::bytes_read() const { return total_read; }

size_t ByteStream::remaining_capacity() const { return stream_capacity - stream.size(); }

总结

虽然实验起步的阶段踩了非常多的坑,但自己却从没想过放弃,碰到问题就一定要去解决问题,因为这些都是必须要面对的,尽管解决问题的过程非常消磨人的精力,不过在痛苦之后能确切地感觉到自己真的变强了。九层之台起于垒土,或许内功的增长取决于平常解决问题收获的点滴。

为了做计网实验,我实在是花了绝大部分时间在学习其他“知识”,而非学习网络知识本身。
在从别人仓库拉 start code 时,之前在搭建博客学习的 git 都忘了很多,才发现自己其实很不了解这个工具,因此看missing semester重新学了一边 git。在项目构建阶段,由于不明白 cmake 和 make 编译失败的原因,在解决完问题后了解了一下 CMake 工具,同时看官方的CMakeLists,也就大概明白配置文档所写内容的含义是什么了。编写代码阶段,由于我根本不会 C++面向对象的特性,所以还花了很多时间学语言,同时也是对着类库看接口,也更清楚地明白构造函数、继承、虚函数等语言特性。

历时两周多终于完成了第一个 lab,不过我觉得这些时间花的都是值得的,让我了解到了理论知识以外的实践知识。希望接下来能更加熟悉整个编写调试的过程,善始善终完成整个大实验。