Stanford CS144 Lab
于2023年2月6日2023年2月6日由Sukuna发布
Lab0.Warm Up
1 Networking by hand
这一个部分主要是体验一些基本的应用层协议,主要是HTTP协议和SMTP协议.
Both of these tasks rely on a networking abstraction called a reliable bidirectional in-order byte stream: you’ll type a sequence of bytes into the terminal, and the same sequence of bytes will eventually be delivered, in the same order, to a program running on another computer (a server). The server responds with its own sequence of bytes, delivered back to your terminal.
在实验资料中给出的是这么一段话,这句话的意思就是所有的应用层协议都是由底层支撑的,这个底层可以理解成可靠的二进制比特流的传输,一方应用程序会产生比特流投入到传输通道中,另一方的应用程序会从传输通道中获取到比特流信息.这个传输通道就是Socket,套接字.
1.1 获取一个网页.
我们跟随着实验的步骤来进行执行.
1)首先用浏览器打开网页http://cs144.keithw.org/hello.
发现内容就是简短的一行字 Hello,CS144.
2) 用telnet进行连接,输入telnet cs144.keithw.org http
这一步的做法就是在你的电脑和另一台电脑(域名为cs144.keithw.org)进行一个套接字连接,(原文:a reliable byte stream between your computer and anothercomputer),这个时候你的电脑和cs144.keithw.org(获取这个域名的IP需要通过DNS服务器进行)就连接上了.
3)输入GET /hello HTTP/1.1
这个是HTTP协议的应用,HTTP协议的命令有GET和POST两种,其中POST一般就是投放一个表单到服务器上,GET就是从服务器获取资源.其中资源的名称是可以指定的,uri就是指定资源, 一般uri的组成就是 服务器的域名 资源在服务器的根目录的对应的文件位置.比如说hust.edu.cn/1.jpg,就是在hust.edu.cn这个服务器根目录下的1.jpg这个文件.
这个命令就是用HTTP/1.1的方式获取服务器的资源
4) 输入 Host: cs144.keithw.org
这个指定host的地址,因为一个uri的构成就是域名 文件位置.
上面几个做法就是利用telnet软件制作一个HTTP请求报文.请求报文分成头部和请求本身
这个时候就会返回一个HTTP响应报文:
代码语言:javascript复制HTTP/1.1 200 OK
Date: Thu, 03 Mar 2022 07:53:25 GMT
Server: Apache
Last-Modified: Thu, 13 Dec 2018 15:45:29 GMT
ETag: "e-57ce93446cb64"
Accept-Ranges: bytes
Content-Length: 14
Content-Type: text/plain
Hello, CS144!
其中分成第一行,首部和报文内容,第一行表示使用的HTTP版本,状态码和短语,我们可以利用状态码和短语来判断服务器是否正常响应了我们的请求:
首部就蕴含了一些控制信息,比如说报文长度和报文类型等.最后的就是报文内容,就是主体部分.
5) 获得sunsetid
如法炮制:构造请求报文和响应报文即可.
1.2 给自己发送邮件
1) 首先连接到服务器,这里要使用SMTP协议进行连接.
2) 给服务器打招呼
3) 登陆
注意这里是BASE64编码的,第一行语句表示Username:,就输入你的邮箱地址,第二行语句表示授权码,把授权码转化成BASE64编码的即可.
4) 写邮件
MAIL 命令,发件人
RCPT 命令,收件人
DATA表示开始书写新建内容
.表示书写结束
1.3 监听服务器
1) 使用netcat -v -l -p 9090创建一个监听端口.
2) 连接本机创建的监听端口:telnet localhost 9090
这个时候客户端和服务器连接上了.
HTTP的连接本质上是C/S模型:客户-服务器模型,在上文我们提到了管道的观念,其实服务器一直在一个管道上监听信息,一旦监听到了信息就对应地往管道里面投放信息.
总的来说,就是客户端向服务器提出申请请求,服务器一直在监听客户端的申请请求,一有申请请求就立刻建立TCP连接.
2 自制Socket
This feature is known as a stream socket. To your program and to the Web server, the socket looks like an ordinary file descriptor (similar to a file on disk, or to the stdin or stdout I/O streams). When two stream sockets are connected, any bytes written to one socket will eventually come out in the same order from the other socket on the other computer.
Socket在Linux操作系统中本质上就是一个文件,一旦两个Socket相互连接,应用程序会往一个Socket递交数据,另外一个Socket就会原封不动地把数据传递过来.连接的方式在运输层有讲,客户端的一个网络端口创建一个Socket,往服务器的一个网络端口发送请求,这是第一次握手,接着服务器的网络端口传输ACK给客户端,这是第二次握手,接着客户端会传输一个最后的请求,这个叫三次握手.三次握手后,连接就完成了,这个时候两个Socket(可以理解成网络端口?)相互链接了.
需要注意的是,在应用层我们一般是注重逻辑通信,Socket是一个逻辑概念,应用程序把数据投给一个叫做Socket的东西,你可以理解成逻辑通信的一端,但是具体Socket往下是怎么做的不是应用程序需要关注的.
这个实验就需要我们模拟一个Socket应用,与一个服务器的端口建立连接.然后获取网页.
代码语言:javascript复制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 rearequestd() -- everything) until you reach
// the "eof" (end of file).
// create a TCPSocket
TCPSocket client_socket;
// connect with host. host is a parameter.
client_socket.connect(Address(host, "http"));
// send a request Message. the request is made of 2 sentences.
string request = "GET " path " HTTP/1.1rn" "Host: " host "rnConnection: closernrn";
client_socket.write(request);
// get the Message
while(!client_socket.eof()){
string reply = client_socket.read();
cout<<reply;
}
cerr << "Function called: get_URL(" << host << ", " << path << ").n";
cerr << "Warning: get_URL() has not been implemented yet.n";
}
这个时候先创建一个TCPSocket,首先先进行连接,然后像之前一样创建request,接着这个Socket就可以把request写进去.然后服务器会返回数据,这个数据是读取到Socket的,读数据一直读到EOF即可.
由于这个实验是面向初学者的,具体Socket怎么读怎么写我们没有考虑,我们只用调用教授已经写好的写,读操作.
3 缓冲区队列
要求实现一个有序字节流类(in-order byte stream),使之支持读写、容量控制。这个字节流类似于一个带容量的队列,从一头读,从另一头写。当流中的数据达到容量上限时,便无法再写入新的数据。特别的,写操作被分为了peek和pop两步。peek为从头部开始读取指定数量的字节,pop为弹出指定数量的字节。
总的来说就是做一个桶,可以从下方获得内容,也可以从上方添加内容,当桶满的时候就不可以添加东西了
ByteStream具有一定的容量,最大允许存储该容量大小的数据;在读取端读出一部分数据后,它会释放掉已经被读出的内容,以腾出空间继续让写端写入数据。
这个实验为我们后期实现TCP协议有着帮助.
上面的是缓冲区队列的一些声明,对于读写两方,操作是不同的.
有个小提示,如果C 的构造函数可以使用像这样的方法进行初始化的
代码语言:javascript复制class baba (const int abab) _abab(abab){
}
这个本质上就是数据结构题,完成缓冲区队列罢了.
Lab1.stitching substrings into a byte stream
该lab要求我们实现一个流重组类,可以将Sender发来的带索引号的字节碎片重组成有序的字节写入到byte_stream。接收端从发送端读取数据,调用流重组器,流重组器对数据进行排序,排序好后写入byte_stream。
流重组器也是有capasity的,也就是说流重组器也有一定的容量,一次性处理太多的信息会导致流重组器不能够正常地工作.同样的我们把流处理器当成一个双端队列即可.
private类中还有一个ByteStream类型的变量,所有的内容都输出给ByteStream,还有一个容量变量.其中ByteStream中的bytes_read返回ByteStream处理了多少元素.
因为重组类的函数中,支持的index是first unread=_output.bytes_read()(已经读取的元素)到first unacceptable的这一块区域,我们要保证输入的字节编号是在这个区域里面的.
在重组器类的函数中,push_substring函数完成重组工作。其参数有data(string),index(size_t),eof(bool),data是待排序的数据,index是数据首元素的序号,eof用于判断是否结束。结束后将不再有数据输入。
在重组的过程中,我们会遇到重复、丢包、重叠、乱序的现象。为了使乱序的数据找到有序的位置,我使用’ ’维护重组器中数据的相对序号,例如,第一次data为make,index为0,第二次data为great,index为13,而处于两组数据中间的数据未知,我们就用’ ’代替,即make great。这样就维护了已经进入重组器的数据的有序。当然,写入的data中也有可能含有 ,这是,我们就需要一个bool双端队列,来记录相应位置的数据是否有序,在上述例子中,队列的bool值为111100000000011111。
所以说我们在数据结构中添加几项,一个是_unassembled_byte,是一个std::deque<char>,暂时存储还乱序的字符串,_check_byte是std::deque<bool>,这个元素与_unassembled_byte一一对应,当un[i]存储着还没有发送的字符的时候,ch[I]=true,否则为false,还有一个_lens_un,这个记录乱序的字符的长度.
程序的总体结构:
发送端的数据->流重组器(重组成有序的数据)->Bytestream(在Lab0就做好的队列)->TCP接收端. 流重组器需要做的是,把所有有序的数据写入到接收端. 其中字符的编号是从1一直往后延伸的,因为队列的首和尾都可以记录.TCP的发送端发送的数据也是(字符号、字符串)字符的编号一直往后延伸.
这个时候我们回忆一下对应数据的表示:
output.bytes_read()
:接收端从ByteStream获得的字符数量.
output.bytes_write()
:流重组器写入ByteStream的字符数量-1.而且是流重组器的有效数据中index最小的序号
_lens_un
指的还在流重组器里面的数据的长度.
其中:output.bytes_read() _capacity
是ByteStream可以接受的范围,output.bytes_write() _lens_un
是流重组器的有效数据中index最大的序号.
1.我们判断输入序号是否大于内存与已读取数据之和,也就是说,该数据是否属于unacceptable中的数据,如果是这样的数据,我们没有足够的内存写入,因为写入这样的数据需要添加 ,从而超过capasity的大小。代码如下:
代码语言:javascript复制if(index>_output.bytes_read() _capacity){
return;
}
2.字符串部分在区域内,但是部分在区域外,那就把区域外的内容舍弃,只读取区域内的内容.
我们需要判断data中最后一个数据的序号是否大于内存与已读取数据之和,如果大于,我们就要将能写入的部分写入,也就是按data的顺序尽可能地写入数据而不超过capasity,在写入的过程中,我们也会遇到两种情况,一种是序号index大于此时已经在流重组器的最后一个数据的序号,在这种情况下我们要在流重组器最后一个序号与index之间填入’ ′,同时将相应的bool双端队列(_check_byte)设置为false,做完这些工作后,才开始写入新的数据。另一种情况是index的小于或者等于流重组器最后一个数据的序号,我们需要弹出冲突的数据,举个例子就是,index序号为5,此时流重组器中的数据为stanford,我们就要从序号5的数据也就是o开始弹出,变成stanf,再写入data中的数据。代码如下:
代码语言:javascript复制if(index data.length()>_capacity _output.bytes_read()){
for(size_t i=_lens_un _output.bytes_written();i<_capacity _output.bytes_read();i ){
if(i<index){
_unassembled_byte.push_back('