实验3:基于UDP服务设计可靠传输协议并编程实现 实验内容: 任务3-1:利用数据报套接字在用户空间实现面向连接的可靠数据传输,功能包括:建立连接、差错检测、确认重传。流量控制采用停等机制,完成给定测试文件的传输。
报文格式
#pragma pack(1) struct message //报文格式{ int flag; DWORD SendIP, RecvIP; u_short SendPort, RecvPort; int msgseq; int ackseq; int index; int filelength; int fill ; u_short checksum; char msg[BUF_SIZE]; }; #pragma pack()
状态转换图与交互流程
由于本次实验采用停等机制,即每次client端发送一条消息 后,需要等待对方回复==对应==的ACK ,才能发送下一条消息,故client端和server端(发送端和接收端)状态转换图类似,这里用一张图说明。这里黑色部分为正常情况,蓝色部分为出现异常时的处理。
下面简要介绍交互大致流程,具体细节将在后面对应部分详述
client端 :
初始状态0
建立连接 :发送一条带有标识SYN 的消息,表示希望建立连接,对方收到后回复对应的ACK ,连接建立成功。当收到ACK后进入状态1
发送文件 :
消息1,文件开始标识位SF=1 ,index=发送文件需要的消息条数-1 ,filelength=最后一条消息msg成员的有效位数 。当收到对应的ACK后,进入状态2 。每条消息都是收到上一条对应的ACK后再发送。
发送文件,每条msg的有效长度固定为1024,最后一条EF=1 ,收到ACK后退回状态1
断开连接 :发送一条标识为FIN 的消息,对方收到后回复对应的ACK,断开连接成功,退回状态0
client端由于是主动发送的一端,一般情况下不会设计蓝色线条的状态跳转,但是可能会有超过重传次数 情况下做出的断网处理
server端 :
- 初始:状态0
建立连接 :收到一条SYN ,回复对应的ACK ,连接建立成功,进入状态1
发送文件 :收到包含SF 、index 、filelength 的消息,开始接收文件,进入状态2。对于每条消息都返回一条ACK 。当收到包含标志位EF 的消息,检查index与收到文件消息的条数 ,若一致,则返回ACK ,退回状态1
断开连接 :收到包含标识为FIN 的消息,返回ACK ,进入状态0
注: 当server端和client端在一定时间内没有接收或不能接收到对方的消息 ,需要做断网处理 ,即自动退回到状态0
无异常情况下的发送流程
上图描绘了在正常情况下的发送流程,由于本次采用停等 机制,client端每次发送消息后,都要接收到对方回复的对应的ACK (即收到ACK消息的ackseq=发出消息的msgseq)才能发能发送下一条指令
建立连接 正常情况 下的连接建立过程:
客户端发送带有标识SYN 的消息x,接收到对方对消息x的确认消息ACK 。由于只是单项传输 ,所以两次握手 即可
出现异常情况 下的处理过程
==注意==,为了处理server返回ACK丢包 导致的两端状态不同步和其它异常 情况,server端的三个状态 均可以接收SYN 请求,也均可以接收FIN 断开请求
消息①:ACK丢包 ,client端超过一段时间没收到ACK后重新发送SYN
消息②:部分信息丢失或被篡改,即校验和出错 ,server端不返回ACK ,client端检测到没有在规定时间接收到ACK后重新发送 消息
消息③:server端虽然返回了ACK,但由于某些原因client端没有在规定时间内接收到ACK ,client端仍需要重传
消息④:client端发出消息④后,在超时时限内收到了两条ACK ,且ackseq相同,由于这两条ACK是对仅仅是由重传造成的相同消息的确认,故具有相同效力 ,取较早收到的ACK 即可
int buildconnectionCli () { message a, b; a.set_syn(); a.msgseq = sendseq++; if (stopwaitsend(a, b))return 1 ; else return 0 ; }
int buildconnectionSer (message t) { message a,b; a.set_ack(t); a.msgseq = sendseq++; simplesend(a); cout << "连接成功" <<endl ; return 1 ; }
连接断开 这里与连接建立类似,由于是单向传输,只需要两次挥手即可,程序逻辑与连接建立类似,client端发送含有FIN 的消息,server端收到后回复对应的ACK ,连接断开
差错重传 使用校验和 方式检测是否出现差错,校验和计算方式与rdt3.0类似,只是根据本次实验设计的报文格式进行了简单修改。校验和计算包括0-255bit。
计算方法如下
void message::setchecksum () { int sum = 0 ; u_char* temp = (u_char*)this ; for (int i = 0 ; i < 16 ; i++) { sum += temp[2 * i] << 8 + temp[2 * i + 1 ]; while (sum >= 0x10000 ) { int t = sum >> 16 ; sum += t; } } this ->checksum = ~(u_short)sum; }
bool message::checkchecksum () { int sum = 0 ; u_char* temp = (u_char*)this ; for (int i = 0 ; i < 16 ; i++) { sum += temp[2 * i] << 8 + temp[2 * i + 1 ]; while (sum >= 0x10000 ) { int t = sum >> 16 ; sum += t; } } if (checksum + (u_short)sum == 65535 ) return true ; return false ; }
根据rdt3.0,当server端检测到校验和错误时,不发送NAK ,也不返回ACK ,只是等待client端超时重新发送 。
确认重传 前面在连接建立部分已经简要进行介绍,当client端无法在规定时间内接收到相应消息的ACK 时,重传消息
主要涉及以下几种情况
server端检测校验和出错 ,不发送ACK
server端发送的ACK丢包
server端发送的ACK未在超时时限内收到
注:这里的recv函数非阻塞
bool stopwaitsend (message& a, message b) { simplesend(a); clockstart = clock(); int flag = 0 ; while (1 ) { simplerecv(b); if (b.get_ack()&&b.ackseq==a.msgseq) { return 1 ; } clockend = clock(); if (flag == SENT_TIMES) return 0 ; if ((clockend - clockstart) / CLOCKS_PER_SEC >= WAIT_TIME) { flag++; clockstart = clock(); cout << "重传" << flag << endl ; simplesend(a); } } return 0 ; }
停等机制 该机制下client端每发送一条消息,都需要等待接收到对方返回的ACK才能发送下一条,client端代码即解释见确认重传 部分,这里对server端代码进行解释,即收到client端的消息且检测校验和正确后,向对方发送相应消息的ACK
bool stopwaitrecv (message& a, message b) { int flag = 0 ; while (1 ) { simplerecv(a); if (a.get_exist()) { int check = a.checkchecksum(); if (a.checkchecksum()) { b.set_ack(a); b.msgseq = sendseq++; simplesend(b); memset ((char *)&b, 0 , sizeof (message)); return 1 ; } } } cout << "接收失败" << endl ; return 0 ; }
给定测试文件的传输 client端:
读入文件至内存中
文件起始消息SF置1 ,将发送文件所需要的消息条数index 和最后一条消息msg段有效位数 填入报头中index 和filelength 相应位置,msg成员填入文件名
将内存中的消息复制到message类对象的msg段,逐条发送
最后一条消息EF置1 ,告知对方文件发送结束
void readfile (char * name,char content[10000 ][1024 ],int &length, int & index) { index = 0 ; length = 0 ; ifstream in (name, ifstream::binary) ; if (!in) { cout << "文件无效" << endl ; return ; } char t = in.get (); while (in) { content[index][length % 1024 ] = t; length++; if (length % 1024 == 0 ) { index++; length = 0 ; } t = in.get (); } in.close (); }
int sendfile (char * name) { cout << "开始发送文件" << endl ; int length = 0 ; int index = 0 ; readfile( name,content,length,index); clock_t timestart = clock(); message a; a.index = index; a.filelength = length; int t = strlen (name); for (int i = 0 ; i < t; i++) a.msg[i]=name[i]; a.set_startfile(); message b; a.msgseq = sendseq++; if (!stopwaitsend(a, b)) { cout << "文件传输失败" << endl ; return 0 ; } for (int i = 0 ; i <= index; i++) { message temp; if (i == index) { temp.set_endfile(); for (int j = 0 ; j < length; j++) temp.msg[j] = content[i][j]; } else { for (int j = 0 ; j < 1024 ; j++) temp.msg[j] = content[i][j]; } temp.msgseq = sendseq++; if (stopwaitsend(temp,b)== 0 ) { cout << "文件发送失败" << endl ; return 0 ; } } clock_t timeend = clock(); double endtime = (double )(timeend - timestart) / CLOCKS_PER_SEC; cout << "Total time:" << endtime << endl ; cout << "吞吐率:" << (double )(index + 1 ) * sizeof (message) * 8 /endtime /1024 /1024 << "Mbps" << endl ; cout << "文件发送成功" << endl ; return 1 ; }
server端:
接收到包含SF=1 的消息,读出index、filelength和文件名 ,返回ACK
循环接收对方发来的消息,将msg段复制到内存中 ,逐条返回ACK
当收到的消息包含标识为EF 时,表示该条消息msg段有效位数为filelength ,将其复制到内存中。校验index与收到文件消息的条数 ,如果正确返回ACK
将内存中的数据写入文件
int recvfile (message a) { message t; t.set_ack(a); a.msgseq = sendseq++; simplesend(t); int index = a.index; int length = a.filelength; char name[30 ]; memset (name, 0 , 30 ); for (int i = 0 ; a.msg[i]; i++) name[i] = a.msg[i]; for (int i = 0 ; i <= index; i++) { message b, c; memset (content[i], 0 , 1024 ); if (stopwaitrecv(b, c)) { if (i == index) { for (int j = 0 ; j < length; j++) content[i][j] = b.msg[j]; } else { for (int j = 0 ; j < 1024 ; j++) content[i][j] = b.msg[j]; } } else { cout << "出错0" << endl ; return 0 ; } if (i == index) { if (!b.get_endfile()) { cout << "出错1" << endl ; return 0 ; } } } outfile(name, content, length, index); cout <<name<< " 文件接收成功" << endl ; return 1 ; }
void outfile (char * name, char content[50000 ][1024 ], int length, int & index) { ofstream fout (name, ofstream::binary) ; for (int i = 0 ; i < index; i++) { for (int j = 0 ; j < FILE_PACKET_LENGTH; j++) fout << content[i][j]; } for (int j = 0 ; j < length; j++) fout << content[index][j]; fout.close (); }
附录 程序执行界面展示
socket的创建 头文件
#include <WinSock2.h>//windows socket 编程头文件 #pragma comment(lib,"ws2_32.lib" )
WSADATA wsaData; if (WSAStartup(MAKEWORD(2 , 2 ), &wsaData) != 0 ){ cout << "socket初始化失败" << endl ; return 0 ; } sock = socket(AF_INET, SOCK_DGRAM, 0 ); if (sock == INVALID_SOCKET){ cout << "socket创建失败" ; return -1 ; } struct timeval timeout ;timeout.tv_sec = 1 ; timeout.tv_usec = 0 ; if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof (timeout)) == -1 ) { cout << "setsockopt failed:" ; } cout << "server3-1" << endl ;addrop.sin_addr.s_addr = inet_addr("192.168.89.1" ); addrop.sin_family = AF_INET; addrop.sin_port = htons(SPORT); addr.sin_addr.s_addr = inet_addr("192.168.89.1" ); addr.sin_family = AF_INET; addr.sin_port = htons(CPORT); if (bind(sock, (SOCKADDR*)&addrop, sizeof (SOCKADDR)) == -1 ){ cout << "bind error" << endl ; return -1 ; } ...... closesocket(sock); WSACleanup(); return 0 ;
消息的发送与接收
为了简化后续代码,将send和recv简化封装成函数
注意这里由于使用的是UDP ,每次发送和接收都需要指明目的或者来源IP地址
void simplesend (message& a) { a.set_exist(); a.setchecksum(); if (sendto(sock, (char *)&a, sizeof (message), 0 , (struct sockaddr*)&addr, sizeof (sockaddr)) == SOCKET_ERROR); { } } void simplerecv (message& a) { memset (a.msg, 0 , sizeof (a.msg)); recvfrom(sock, (char *)&a, sizeof (message), 0 , (struct sockaddr*)&addr, &addr_len); }
支持用户输入IP
cout << "是否使用默认IP?是1,否2 " ;int i;cin >> i;if (i == 2 ){ char s[20 ] = {}; char c[20 ] = {}; cout << "请输入服务器IP: " ; cin >> s; cout << "请输入客户端IP: " ; cin >> c; addrop.sin_addr.s_addr = inet_addr(s); addr.sin_addr.s_addr = inet_addr(c); }
message类 初始化
message::message() { memset (this , 0 , sizeof (message)); SendPort = SPORT; RecvPort = CPORT; SendIP = addr.sin_addr.s_addr; RecvIP = addrop.sin_addr.s_addr; }
标志位的获取与设置 int message::get_syn () { if (this ->flag & 0x02 ) return 1 ; else return 0 ; }
void message::set_ack (message b) { if (get_ack() == 0 ) flag += 0x01 ; this ->ackseq = b.msgseq; }
void message::set_syn () { if (get_syn() == 0 ) flag += 0x02 ; }
消息处理流程 server: while (1 ){ message b; simplerecv(b); if (b.get_exist()) { if (!tackle(b))break ; } Sleep(20 ); }
int tackle (message b) { if (b.get_syn()) { if (buildconnectionSer(b)) { status = 1 ; cout << "建立连接" << endl ; } else cout << "连接建立失败" << endl ; } else if (b.get_startfile()) { if (status) { cout << "开始接收文件" << endl ; recvfile(b); } else cout << "请先建立连接" << endl ; } if (b.get_fin()) { if (byeser(b)) { status = 0 ; sendseq = 0 ; } else cout << "未连接" << endl ; cout << "对方已经断开连接,是否结束程序?0退出,1不退出 " ; int i; cin >> i; if (i == 0 ) return 0 ; } return 1 ; }
client while (1 ){ int op; cout << "请选择操作:传输文件1,退出0 " << endl ; cin >> op; if (op == 0 ) break ; else if (op != 1 ) cout << "无效操作" << endl ; else if (op == 1 ) { cout << "请输入文件名 " << endl ; char name[30 ] = {}; cin >> name; if (buildconnectionCli()) { if (sendfile(name)) { cout << "文件发送成功" << endl ; } else cout << "文件发送失败" << endl ; } else cout << "连接建立失败" << endl ; if (byecli()) { cout << "连接已断开" << endl ; sendseq = 0 ; } else cout << "连接断开失败" << endl ; } sendseq = 0 ; }