本博客旨在帮助新手尽量无痛启动CS144课程。

每次开启一个lab,最困扰我这种新手的就是环境配置,这次的cs144踩坑踩了好多天才彻底装成功,要是再算上完成lab0就更久了。所以我决定把自己踩的坑和经验都分享出来,不仅仅是记录,更希望能帮助到后来的同学。如有错误或者问题请联系我的邮箱:yoo2i@qq.com

环境配置

虚拟机安装工具链

在介绍不同的虚拟机管理软件之前,先给出参考链接:

我尝试过两种虚拟机管理软件,分别是VMware和virtualbox,分开讲讲吧:

  • VirtualBox

​ 这个据说是斯坦福官方推荐的方式,并且也提供了装配好的虚拟机供大家使用。但是我最后并没有用这个,有三个原因:一是这个软件总会出一些奇奇怪怪的小bug;二是课程提供的虚拟硬盘大小似乎不够,导致我给它装图形化页面的时候老是失败;三是这个软件不如VMware好用。如果有想尝试的同学可以去试一试,这个我踩得坑就不多了。(如果有玩csgo的同学想装这个,一定要卸掉完美世界竞技平台,这两个软件会冲突。足足花了一下午才在互联网的某个角落发现这个

  • VMware

​ 我装的虚拟机版本是Ubuntu22.04。在安装完Ubuntu后,就可以开始安装工具链了,具体工具列表见链接BYO Linux installation (stanford.edu)。这些工具还是比较好装的。

​ 在安装完工具后就可以拷代码了,上文提到的LRL52老哥提供了一个GitHub仓库,将主分支版本回退到他第一次修改之前的初始状态,然后将lab0-startercode合并到主分支就可以开始lab0了(此后每进行下一个lab就合并一次开始代码进主分支)。然后我分了一个work分支用来编写代码。

配置vscode帮助开发

​ 我选择通过vscode远程连接虚拟机进行开发,使用clangd进行代码补全,gdb进行代码调试。

​ 首先安装Remote-SSH插件用来远程连接,插件使用方法网上很多了,不用以我给的为准。VSCode使用 - Remote-SSH 配置说明_vscode remote ssh配置_jackailson的博客-CSDN博客

​ 然后是通过vscode往虚拟机上安装插件来帮助我们更好的阅读、开发、调试,我往上安装的插件有clangd(为了高亮和自动补全)、C/C++、GDB Debug(这两个是为了调试)。

  • clangd

​ 在vscode上安装完后需要在虚拟机上也有一个clangd,并且作为服务器开启服务,推荐看官方文档Getting started (llvm.org),可读性非常好。时间太久我自己踩过的坑已经忘记了不少,只记得中文搜出来的没一个解决问题的

  • C/C++、GDB Debug

​ 调试方面我的经验也不多,都是这个老哥的博客教的:【计算机网络】Stanford CS144 Lab Assignments 学习笔记 - 康宇PL - 博客园 (cnblogs.com)。C/C++的作用是为了支持GDB Debug,没有的话我会报错。

Lab0:networking warmup

Networking by hand

Fetch a Web page

坑点有只有一个,就是两行请求体的输入一定要快,一个字一个字输入可能不够导致超时,所以推荐复制粘贴。

1
telnet cs144.keithw.org http
1
2
GET /hello HTTP/1.1
Host: cs144.keithw.org

在第二行输入结束后按下回车还要再按一下,这也将成为后面最大的坑点。

Listening and connecting

将本机作为服务器进行内容传输的。一个终端输入的字符会按顺序输出到另一个终端,很有意思。

1
nc -lvp 9090
1
telnet localhost 9090

启动项目

执行下面这些命令

1
2
3
4
cd cs144
mkdir build&&cd build
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=True ..
make

第三行的参数是为了生成json文件来让clangd明白我们的项目结构从而能够代码高亮自动补全的。

webget

需要认真读一读代码文档,熟悉一下c++。

坑点在于要多写一个/r/n。

编码完成后要进行的测试的话需要这么操作:

1
2
3
4
tr -d '\r' < ../tests/webget_t.sh > ../tests/webget_t.sh
tr -d '\r' < ../tests/webget_t.sh > ../tests/webget_t2.sh

make check_webget

删除多余的\r测试程序才能运行。doraemonzzz老哥翻译的文档可能是从pdf复制的,最后一条命令的下划线没有复制上,所以我在上文说英文中文对比着看,不要只看翻译版。

我的代码如下:

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
#include "socket.hh"
#include "util.hh"

#include <cstdlib>
#include <iostream>
#include <sys/socket.h>

using namespace std;
//开始编码
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).

TCPSocket mysocket;
mysocket.connect(Address(host,"http"));
mysocket.write("GET "+path+" HTTP/1.1"+"\r\n"); //系统输入的path自带第一个分割符/,不用自己加
mysocket.write("Host: "+host+"\r\n");
mysocket.write("Connection: close\r\n");
mysocket.write("\r\n");
mysocket.shutdown(SHUT_WR);

while (!mysocket.eof()) {
cout<<mysocket.read();
}

mysocket.close();//为什么不可以只关闭SHUT_RD来充当close?SHUT_WR已经关闭了
cerr << "Function called: get_URL(" << host << ", " << path << ").\n";
cerr << "Warning: get_URL() has not been implemented yet.\n";
}

int main(int argc, char *argv[]) {
try {
if (argc <= 0) {
abort(); // For sticklers: don't try to access argv[0] if argc <= 0.
}

// The program takes two command-line arguments: the hostname and "path" part of the URL.
// Print the usage message unless there are these two arguments (plus the program name
// itself, so arg count = 3 in total).
if (argc != 3) {
cerr << "Usage: " << argv[0] << " HOST PATH\n";
cerr << "\tExample: " << argv[0] << " stanford.edu /class/cs144\n";
return EXIT_FAILURE;
}

// Get the command-line arguments.
const string host = argv[1];
const string path = argv[2];

// Call the student-written function.
get_URL(host, path);
} catch (const exception &e) {
cerr << e.what() << "\n";
return EXIT_FAILURE;
}

return EXIT_SUCCESS;
}

ByteStream

​ 读完要求感觉可以用std::queue,但是无法实现peek函数。查阅得知queue的设计理念只是简单的队列,无法查看内部元素,所以选择std::deque,deque的设计理念不仅是可以在两头方便的插入删除,还可以访问里面的元素从而实现peek函数。

​ 第二个点是eof函数的设计,文档只是含糊一说,实际上eof需要仔细思考一下,具体的解释我放在注释里了。

​ 代码如下:

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
98
#ifndef SPONGE_LIBSPONGE_BYTE_STREAM_HH
#define SPONGE_LIBSPONGE_BYTE_STREAM_HH

#include <cstddef>
#include <string>
#include <deque>

//! \brief An in-order byte stream.

//! Bytes are written on the "input" side and read from the "output"
//! side. The byte stream is finite: the writer can end the input,
//! and then no more bytes can be written.
class ByteStream {
private:
// Your code here -- add private members as necessary.

// Hint: This doesn't need to be a sophisticated data structure at
// all, but if any of your tests are taking longer than a second,
// that's a sign that you probably want to keep exploring
// different approaches.

//
std::deque<char> _buffer = {};
size_t _read_count{};
size_t _write_count{};
size_t _capacity{};
bool _write_end{};
bool _error{}; //c++11引入的“列表初始化”,bool初始化为false

public:
//! Construct a stream with room for `capacity` bytes.
ByteStream(const size_t capacity);


//! \name "Input" interface for the writer
//!@{

//! Write a string of bytes into the stream. Write as many
//! as will fit, and return how many were written.
//! \returns the number of bytes accepted into the stream
size_t write(const std::string &data);

//! \returns the number of additional bytes that the stream has space for
size_t remaining_capacity() const;

//! Signal that the byte stream has reached its ending
void end_input();

//! Indicate that the stream suffered an error.
void set_error() { _error = true; }
//!@}


//! \name "Output" interface for the reader
//!@{

//! Peek at next "len" bytes of the stream
//! \returns a string
std::string peek_output(const size_t len) const;

//! Remove bytes from the buffer
void pop_output(const size_t len);

//! Read (i.e., copy and then pop) the next "len" bytes of the stream
//! \returns a string
std::string read(const size_t len);

//! \returns `true` if the stream input has ended
bool input_ended() const;

//! \returns `true` if the stream has suffered an error
bool error() const { return _error; }

//! \returns the maximum amount that can currently be read from the stream
size_t buffer_size() const;

//! \returns `true` if the buffer is empty
bool buffer_empty() const;

//! \returns `true` if the output has reached the ending
bool eof() const;
//!@}


//! \name General accounting
//是总计数器,记录了这个类一共读写多少,所以要定义私有成员变量来记录
//!@{

//! Total number of bytes written
size_t bytes_written() const;//

//! Total number of bytes popped
size_t bytes_read() const;
//!@}
};

#endif // SPONGE_LIBSPONGE_BYTE_STREAM_HH

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
#include "byte_stream.hh"
#include <cstddef>
#include <deque>
#include <string>

// Dummy implementation of a flow-controlled in-memory byte stream.

// For Lab 0, please replace with a real implementation that passes the
// automated checks run by `make check_lab0`.

// You will need to add private members to the class declaration in `byte_stream.hh`

template <typename... Targs>
void DUMMY_CODE(Targs &&... /* unused */) {}

using namespace std;

ByteStream::ByteStream(const size_t capacity) : _capacity(capacity){}

size_t ByteStream::write(const string &data) {
size_t length = data.length();

if(length>_capacity-_buffer.size()) length=_capacity-_buffer.size();
_write_count+=length;

for(size_t i=0;i<length;i++) _buffer.push_back(data[i]);

return length;
}

//! \param[in] len bytes will be copied from the output side of the buffer
string ByteStream::peek_output(const size_t len) const {
size_t length=len;

if(length>_buffer.size()) length=buffer_size();

return string().assign(_buffer.begin(), _buffer.begin() + length);

}

//! \param[in] len bytes will be removed from the output side of the buffer
void ByteStream::pop_output(const size_t len) {
size_t length=len;

if(length>_buffer.size()) length=_buffer.size();
_read_count+=length;

for(size_t i=0;i<length;i++) _buffer.pop_front();
}

//! Read (i.e., copy and then pop) the next "len" bytes of the stream
//! \param[in] len bytes will be popped and returned
//! \returns a string
std::string ByteStream::read(const size_t len) {
string result = ByteStream::peek_output(len);
ByteStream::pop_output(len);

return result;
}

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

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

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

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

bool ByteStream::eof() const { return input_ended()&&_buffer.empty();//不仅仅buffer要空,写者还要结束输入 }

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

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

size_t ByteStream::remaining_capacity() const { return _capacity-_buffer.size(); }

写在最后

​ 我大概在九月中旬就开始尝试配环境了,但是尝试了好多天才成功。原计划国庆完成lab0,结果国庆有其他的事lab0又搁置了,最后的代码都是昨天(15号)写的。希望这个学期可以把cs144完成吧!看了其他博客发现0-4难度逐级攀升,是有点畏难情绪的,但是最开始的自己连看复杂一点的c++代码都恐惧,看完了不也是不过如此嘛,只管去做!