要求

NKU COMPUTER NETWORK LAB3-1

实验要求

利用数据报套接字在用户空间实现面向连接的可靠数据传输,功能包括:建立连接、差错检测、确认重传等。流量控制采用停等机制,完成给定测试文件的传输。

写在前面

实验报告的.tex文件,我也附在了GITHUB仓库之中,有需要的自取,记得STAR

一些说明

也是不同评分对应不同要求吧,比如说rdt2.0 2.1 2.2 3.0,肯定会有评分上的不同的,不过一般写到这个作业的时候已经比较紧了,编译和OS的压力很大,那么最推荐的情况就是完成基础要求,顺利拿到90分。

当然我是20级,深陷内卷地狱之中,实在是不敢不卷,因此我一直在实现一些加分的要求。~~学弟学妹们可以直接使用rdt2.0,只完成两次握手,不完成挥手,这样在我看来是最简单的。当然,看着我的代码,可以自己修改,或者直接减少某些功能,这样也会降低实验难度。~~好像不太星,因为rdt2.0没有重传的机制,这时候建议魔改rdt2.2,是比较好的实现方式。

实验步骤

协议本身

最最重要的一点,协议要自己设计一下。
有以下几种选择以及具体实现:

  • 定义一个很大的字符数组,不完全实现rdt的内容,通过下标的方式设置伪首部的标志位。字符串在程序中操作会很容易,会大大简化程序。这种模式的典型为我丁学长和孙一丁。墙裂推荐!

  • 定义一个class或者struct,对应你自己将要实现的报文,这时你的报文可以设计得很复杂~~(搞一大堆乱七八糟的东西)~~。然后传递这个class或者struct即可。不过这里最好考虑对齐的问题,因为你很可能无法正好地使用1Byte。这种模式的典型为我斌哥和朱哥。

  • 定义一个很大的数组,但是设计非常水的报文格式。这种模式嘛,感觉像是装x失败,随意吧。。。

以下是我的报文格式此处应该有个图片,但是目前没有。过些日子看看能不能弄出个mermaid的形式来画一下,图床太慢了。

下面进入具体程序的内容

首先是文件读写

先来看一下图片格式的读写。注意,下面这段小的程序并不会每一行都被用在实际的lab3-1中,仅仅是以此说明。

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
#include<iostream>
#include <fstream>//必须有这个头文件,这是文件读写的关键
using namespace std;
int main(){
// 1. 打开图片文件
ifstream is("1.jpg", ifstream::in | ios::binary);
// 2. 计算图片长度
is.seekg(0, is.end); //将文件流指针定位到流的末尾
int length = is.tellg();
is.seekg(0, is.beg); //将文件流指针重新定位到流的开始
// 3. 创建内存缓存区
char * buffer = new char[length];
// 4. 读取图片
is.read(buffer, length);
// 到此,图片已经成功的被读取到内存(buffer)中

//接下来是写入图片
ofstream out; // 读取图像
out.open("mashikei.jpg", ios::out | ios::binary);
//从buffer中写数据到out指向的文件中
out.write((const char*)buffer, length * sizeof(char));
//关闭文件指针,释放buffer内存
out.close();
return 0;
}

这是一个简单的小程序,重点在于通过二进制的方式读入图片。采用C++风格的ftream。至于txt文件,可以再加一个对文件名的判定,然后写一种不使用二进制的读写方式,当然更推荐与图片相同的读写方式。更多关于文件读写的内容,这里给出参考链接,非常详细。
注意,很多人写入的图片没有内容或者损坏(是个裂开的标志)甚至不完整,这里都是你的buffer的内容不正确导致的。

然后这里有一个UDP发送文件的连接,没啥大用,经供参考吧。本次实验,核心中的核心就是把图片和txt发送过去,在你的接收端,能顺利接收图片然后打开,就已经成功了一大半!而不是其他的东西

非阻塞

首先说说非阻塞是干啥的,经历了lab1我们知道如果阻塞了,程序就会卡住,确切的说就会卡在发送or接收函数上,那么这时候你的重传机制就不起作用了。这个东西很闪人,如果你的程序写的不好(比如我的),就会需要一直在阻塞与非阻塞之间进行切换,而且稍微没弄好就写错了。如果是学过多线程,也可以考虑使用多线程代替非阻塞。关于阻塞与非阻塞,提供一些csdn的参考

然后进入各个功能实现

注意,这里的代码都不全,仅讲解重点功能。

报文设计

我搞了个大结构体,被这种写法坑惨了,大概如下所示:

1
2
3
4
5
6
7
8
9
10
struct message
{
u_long flag;//首部标志位信息
u_short seq;//序列号
u_short ack;//确认号
u_long len;//数据部分长度
u_long num; //发送的消息包含几个包
u_short checksum;//校验和
char data[1024];//数据长度
}

握手

设置为三次握手,是很无聊的东西,直接上程序。这里注意,第三次握手并不能被server段真的收到。因为只需要单方传输。

客户端
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
int beginconnect()
{
cout << "开始连接!发送第一次握手!" << endl;
message recvMsg, sendMsg;
sendMsg.setSYN();
sendMsg.seq = 88;
sendmessage(sendMsg);
int start = clock();
int end;
while (true)
{
recvMsg = recvmessage();
if (recvMsg.isACK() && recvMsg.isSYN()&& recvMsg.ack == sendMsg.seq + 1) {
SetColor(14,0);
cout << "收到第二次握手!" << endl;
break;
}
}
sendMsg.init_message();
sendMsg.setACK();
sendMsg.seq = 89;
sendMsg.ack = recvMsg.seq + 1;
cout << "发送第三次握手的数据包" << endl;
sendmessage(sendMsg);
return 0;
}
服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int WaitConnect()
{
cout << "服务器等待连接" << endl;
message recvMsg, sendMsg;
while (true)
{
recvMsg = recvmessage();
if (recvMsg.isSYN())
{
cout << "收到第一次握手成功!" << endl;
break;
}
}
sendMsg.setSYN();
sendMsg.setACK();
sendMsg.ack = recvMsg.seq + 1; // 将要发送确认包的ack设为收到包的seq+1
sendMsg.setSYN();
cout << "发送第二次握手信息!" << endl;
sendmessage(sendMsg);
SetColor(14,0);
cout << "接收到确认连接,连接成功" << endl;
return 0;
}

挥手

贼TM离谱的东西,其实挥手这一步是没必要的,因为文件是否传输完是按照最后一个包的检测进行判断的,在得到消息后直接结束程序即可。只是考虑到可能我们想在一次运行的时候传输多个文件,那你把协议改了不就行了,非要实现挥手我也拦不住,建议仅仅实现两次挥手。我仅仅实现了两次挥手,代码如下所示:

客户端
1
2
3
4
5
6
7
8
9
10
11
int closeconnect() {  // 断开连接
message recvMsg, sendMsg;
sendMsg.setFIN();
sendMsg.seq = 8888;
sendmessage(sendMsg);
while (true) {
recvMsg = recvmessage();
}
cout << "接收到确认连接,断开连接成功" << endl << endl;
return 0;
}
服务器
1
2
3
4
5
6
7
if (msg.isFIN()) {
sendMsg.setACK();
sendMsg.ack = msg.seq + 1;
sendmessage(sendMsg);
cout<<"已经收到客户端发过来的挥手请求,并且发送了第二次挥手,服务器将结束运行!再见!"<<endl;
return 0;
}

校验和

推荐直接用老师给的函数。也可以自己设计校验和的内容然后更改函数,强者们实现的都不一样。代码如下所示:

两端基本相同
1
2
3
4
5
6
7
8
9
10
11
12
u_short cksum(u_short* buf, int count) {
register u_long sum = 0;
while (count--) {
sum += *(buf++);
// if cksum overflow 16 bits, it will keep its carry-bit
if (sum & 0xffff0000) {
sum &= 0xffff;
sum++;
}
}
return ~(sum & 0xffff);
}

确认重传的实现

这块看起来比较复杂,但是很直观,应该都能看懂。不过我在此遇到的问题是,我的server端不能超时重传,想实现的话会非常复杂,感觉有点浪费时间,因此没做。此处大量代码如下所示:

客户端
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
int waitSend(message sendMsg, int seq)
{
message recvMsg;
sendMsg.seq = seq;
sendmessage(sendMsg);
int iMode = 1; //1:非阻塞,0:阻塞
ioctlsocket(Client, FIONBIO, (u_long FAR*) & iMode);//非阻塞设置
int count = 0;
clock_t start = clock();
clock_t end;
while (1) {
end = clock();
if (end - start > 50) {
SetColor(0,12);
cout << "应答超时,重新发送数据包" << endl;
sendmessage(sendMsg);
count++;
cout<<"尝试重新发送第"<<count<<"次,最多10次"<<endl;
if(count>=10){
SetColor(0,12);
cout << "重发失败,请确认网络通畅以及服务端启动后,重新启动客户端并重新发送文件!再见!" << endl;
break;
}
start = clock();
}
recvMsg = recvmessage();
if (recvMsg.isACK() && recvMsg.ack == seq) {
cout << "收到服务器发来的ack正确的确认数据包!" << endl;
cout << endl;
return 1;
}
}
return 0;
}
服务器
1
2
3
4
5
6
7
8
9
10
11
12
if (recvMsg.seq == seq) {
sendMsg.setACK();
sendMsg.ack = recvMsg.seq;
SetColor(14,0);
cout << "收到seq为" << recvMsg.seq << "的数据包" << endl;
SetColor(14,0);
cout << "发送确认收到的数据包(对应的ack)" << endl;
sendmessage(sendMsg);
cout << endl;
out.write(recvMsg.data, recvMsg.len);
break;
}

主函数

依次调用上面的各个功能即可。

客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
SetColor();
// 初始化套接字
Start();
SetColor(0,12);
beginconnect();
sendFirstName();
closesocket(Client);
WSACleanup();
system("pause");
return 0;
}
服务器
1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
SetColor(14,0);
Start();
WaitConnect();
getFileName();
//关闭套接字
closesocket(Server);
WSACleanup();
system("pause");
return 0;
}

文件传输的其他讲解

关于文件发送,这里没有写的很详细,因为我实现的不好。大致解释,我们需要先把文件名发送过去,然后再发送文件内容。这一步我个人认为是必要的,因为如果你不发送文件名,对方怎么知道你传输的文件是什么格式呢?至于说服务器写文件时,是不是必须使用这个文件名,就随意了。第一个包,应该是类似于“心跳包”一样的东西。而最后一个包,也要处理以下,需要告诉服务器,传输完毕了。同时如果是定长的数据包的话,对最后一个包要进行填充。我个人认为,最好上手的程序是我丁哥的,逻辑清楚,协议合理,代码可读性强。将会在本学期结束附上他的代码链接

小小总结

关于代码

写的并不好,而且可能有错,**反正记住只要改不了程序就改协议!**不建议直接复制,仅供参考。

关于整个lab3

这个玩意和后面的实验有着比较直观的联系,我还是建议学弟学妹们谨慎设计协议以及程序,不要让程序在3-1就陷入不可更改、不可维护的地步,那样会导致3-2和3-3的进行异常困难。因此,尽量简化程序也是后续实验得以顺利进行的一种可能性。

一些吐槽

又没有实验指导书,让哐哐写出1k行程序来。。。这个实验的质量远不如cs144,而且开专的实验由于都是自己设计,很容易让你养成把所有东西写在main里的坏习惯,比如孙一丁的丑代码,居然还是18级的优秀作业呢。。。有余力的可以看一下CS144,国外的实验会让你学会怎么构建project,怎么写出框架,而不是自己写一堆离谱的开放式要求的程序。

github仓库