要求
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 () { ifstream is ("1.jpg" , ifstream::in | ios::binary) ; is.seekg (0 , is.end); int length = is.tellg (); is.seekg (0 , is.beg); char * buffer = new char [length]; is.read (buffer, length); ofstream out; out.open ("mashikei.jpg" , ios::out | ios::binary); out.write ((const char *)buffer, length * sizeof (char )); 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 ; 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 (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 ; 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仓库