资源下载地址:https://download.csdn.net/download/sheziqiong/85836435
资源下载地址:https://download.csdn.net/download/sheziqiong/85836435
联网的 PC 机、Wireshark 软件
Visual C++、gcc 等 C++ 集成开发环境。
设计请求、指示(服务器主动发给客户端的)、响应数据包的格式,至少要考虑如下问题:
定义两个数据包的边界如何识别。
定义数据包的请求、指示、响应类型字段。
定义数据包的长度字段或者结尾标记。
定义数据包内数据字段的格式(特别是考虑客户端列表数据如何表达)。
小组分工:1 人负责编写服务端,1 人负责编写客户端。
客户端编写步骤(需要采用多线程模式)
运行初始化,调用 socket(),向操作系统申请 socket 句柄。
编写一个菜单功能,列出 7 个选项等待用户选择。
根据用户选择,做出相应的动作(未连接时,只能选连接功能和退出功能)
选择连接功能:请用户输入服务器 IP 和端口,然后调用 connect(),等待返回结果并打印。连接成功后设置连接状态为已连接。然后创建一个接收数据的子线程,循环调用 receive(),如果收到了一个完整的响应数据包,就通过线程间通信(如消息队列)发送给主线程,然后继续调用 receive(),直至收到主线程通知退出。
选择断开功能:调用 close(),并设置连接状态为未连接。通知并等待子线程关闭。
选择获取时间功能:组装请求数据包,类型设置为时间请求,然后调用 send()将数据发送给服务器,接着等待接收数据的子线程返回结果,并根据响应数据包的内容,打印时间信息。
选择获取名字功能:组装请求数据包,类型设置为名字请求,然后调用 send()将数据发送给服务器,接着等待接收数据的子线程返回结果,并根据响应数据包的内容,打印名字信息。
选择获取客户端列表功能:组装请求数据包,类型设置为列表请求,然后调用 send() 将数据发送给服务器,接着等待接收数据的子线程返回结果,并根据响应数据包的内容,打印客户端列表信息(编号、IP 地址、端口等)。
选择发送消息功能(选择前需要先获得客户端列表):请用户输入客户端的列表编号和要发送的内容,然后组装请求数据包,类型设置为消息请求,然后调用 send()将数据发送给服务器,接着等待接收数据的子线程返回结果,并根据响应数据包的内容,打印消息发送结果(是否成功送达另一个客户端)。
选择退出功能:判断连接状态是否为已连接,是则先调用断开功能,然后再退出程序。否则,直接退出程序。
主线程除了在等待用户的输入外,还在处理子线程的消息队列,如果有消息到达,则进行处理,如果是响应消息,则打印响应消息的数据内容(比如时间、名字、客户端列表等);如果是指示消息,则打印指示消息的内容(比如服务器转发的别的客户端的消息内容、发送者编号、IP 地址、端口等)。
服务端编写步骤(需要采用多线程模式)
运行初始化,调用 socket(),向操作系统申请 socket 句柄
调用 bind(),绑定监听端口(请使用学号的后 4 位作为服务器的监听端口),接着调用 listen(),设置连接等待队列长度主线程循环调用 accept(),直到返回一个有效的 socket 句柄,在客户端列表中增加一个新客户端的项目,并记录下该客户端句柄和连接状态、端口。然后创建一个子线程后继续调用 accept()。该子线程的主要步骤是(刚获得的句柄要传递给子线程,子线程内部要使用该句柄发送和接收数据):
调用 send(),发送一个 hello 消息给客户端(可选)
循环调用 receive(),如果收到了一个完整的请求数据包,根据请求类型做相应的动作:
请求类型为获取时间:调用 time()获取本地时间,然后将时间数据组装进响应数据包,调用 send()发给客户端
请求类型为获取名字:将服务器的名字组装进响应数据包,调用 send()发给客户端
请求类型为获取客户端列表:读取客户端列表数据,将编号、IP 地址、端口等数据组装进响应数据包,调用 send()发给客户端
请求类型为发送消息:根据编号读取客户端列表数据,如果编号不存在,将错误代码和出错描述信息组装进响应数据包,调用 send()发回源客户端;如果编号存在并且状态是已连接,则将要转发的消息组装进指示数据包。调用 send()发给接收客户端(使用接收客户端的 socket 句柄),发送成功后组装转发成功的响应数据包,调用 send()发回源客户端。
主线程还负责检测退出指令(如用户按退出键或者收到退出信号),检测到后即通知并等待各子线程退出。最后关闭 Socket,主程序退出。
编程结束后,双方程序运行,检查是否实现功能要求,如果有问题,查找原因,并修改,直至满足功能要求使用多个客户端同时连接服务端,检查并发性使用 Wireshark 抓取每个功能的交互数据包
请将以下内容和本实验报告一起打包成一个压缩文件上传:
源代码:客户端和服务端的代码分别在一个目录
可执行文件:可运行的.exe 文件或 Linux 可执行文件,客户端和服务端各一个
以下实验记录均需结合屏幕截图(截取源代码或运行结果),进行文字标注(看完请删除本句)。
描述请求数据包的格式(画图说明),请求类型的定义
Int DES (目的地的 socket ID) |
---|
REQ_TYPE OPT (请求的类型) |
Char[1024] MES (消息内容,固定长度) |
const int MAXMES = 1024;
enum REQ_TYPE {DISCON, TIME, NAME, LINK, SEND};
string REQ_STR[] = { "DISCON", "TIME", "NAME", "LINK", "SEND" };
struct MESSAGE {
int DES;
REQ_TYPE OPT;
char MES[MAXMES];
};
描述响应数据包的格式(画图说明),响应类型的定义
Int DES (目的地的 socket ID) |
---|
REQ_TYPE OPT (请求的类型) |
Char[1024] MES (消息内容,固定长度) |
描述指示数据包的格式(画图说明),指示类型的定义
Int DES (目的地的 socket ID) |
---|
REQ_TYPE OPT (请求的类型) |
Char[1024] MES (消息内容,固定长度) |
客户端初始运行后显示的菜单选项
客户端的主线程循环关键代码截图(描述总体,省略细节部分)
根据输入的指令,选择像服务器发放什么类型的包
while(1) {
// initialize the MESSAGE being sent
MESSAGE sen ;
sen = MESSAGE{
-1, NAME, ""
} ;
int opt ;
string Text ;
int des_num ;
cin >> opt ;
switch (opt) {
case 1:
sen.OPT = DISCON ;
break ;
case 2:
sen.OPT = TIME ;
break ;
case 3:
sen.OPT = NAME ;
break ;
case 4:
sen.OPT = LINK ;
break ;
case 5:
// only the type of sending message to others need to be treated differently
sen.OPT = SEND ;
// 输入要发送的目的地和信息内容,略
case 6:
cout << "Quiting..." << endl ;
return 0 ;
default:
cout << "Wrong option!" << endl ;
break ;
}
if ( opt < 1 || opt > 6 )
continue ;
// sending the packet char buf[sizeof(MESSAGE)] ; memcpy(buf, &sen, sizeof(MESSAGE)) ;
int seRet = send(client, buf, sizeof(buf), 0);
if (seRet == -1) {
cout << "Sending failed:" << errno << endl;
}
}
客户端的接收数据子线程循环关键代码截图(描述总体,省略细节部分)
void receiveT(SOCKET client) {
char buf[sizeof(MESSAGE)];
MESSAGE sen ;
while ( 1 ) {
// Message Receiving
int reRet = recv(client, buf, sizeof(buf), 0);
if (reRet == -1) {
cout << "Receiving failed:"<< errno << endl;
exit(0);
}
memcpy(&sen, buf, sizeof(buf)) ;
if(sen.OPT == DISCON) {
cout << "Disconnecting..." << endl ;
exit(0);
}
}
}
服务器初始运行后显示的界面
服务器的主线程循环关键代码截图(描述总体,省略细节部分)
while (true) {
int client = accept(slisten, (sockaddr*)&sin, &addr_size);
if (client == -1)
// 省略
// lambda expression used when using thread thread t([&](sockaddr_in addr, int client) {
// 放在下一题里
}, sin, client);
t.detach();
}
服务器的客户端处理子线程循环关键代码截图(描述总体,省略细节部分) char TMP_CLI[255];
// Get ip from hex
inet_ntop(AF_INET, (void*)&sin.sin_addr, TMP_CLI, 16);
Clients[client] = sin;
Clients_info[client].ip = TMP_CLI;
Clients_info[client].port = sin.sin_port;
// Initialize Flas as TRUE Flags[client] = 1;
while (Flags[client]) {
// Receive the packet char buf[sizeof(MESSAGE)];
int reRet = recv(client, buf, sizeof(buf), 0);
if (reRet == -1) {
cout << "receive failed:" << errno << endl;
break;
}
// interpret the packet MESSAGE rec, sen;
memcpy(&rec, buf, sizeof(MESSAGE));
memset(sen.MES, 0, sizeof(sen.MES));
sen.DES = client;
sen.OPT = rec.OPT;
// Process it according to its type switch (rec.OPT) { case DISCON:
Flags[client] = 0;
Clients_info.erase(client);
cout << "Disconnected link with " << client << endl;
break;
case TIME:
cout << "Send time to " << client << endl;
cc = time(0);
tmp_time = ctime(&cc);
strcpy(rec.MES, tmp_time.c_str());
break;
case NAME:
cout << "Send name to " << client << endl;
gethostname(name, size);
strcpy(rec.MES, name);
break;
case LINK:
cout << "Send current active links to " << client << endl;
// 通过 map 获取现有的活跃链接,并发送
break;
case SEND:
cout << "Send message from " << client << " to " << rec.DES << endl;
// 向目标地址发送消息
break;
default:
cout << "Wrong requirement type!" << endl;
}
// Always reply the sender after processing the packet if (rec.OPT >= DISCON && rec.OPT <= SEND) {
// 回复发出指令的客户端
}
}
客户端选择连接功能时,客户端和服务端显示内容截图。
Wireshark 抓取的数据包截图:
客户端选择获取时间功能时,客户端和服务端显示内容截图。
客户端:
服务端:
Wireshark 抓取的数据包截图(展开应用层数据包,标记请求、响应类型、返回的时间数据对应的位置):
客户端选择获取名字功能时,客户端和服务端显示内容截图。
客户端:
服务端:
Wireshark 抓取的数据包截图(展开应用层数据包,标记请求、响应类型、返回的名字数据对应的位置):
相关的服务器的处理代码片段:
gethostname(name, size);strcpy(rec.MES, name);客户端选择获取客户端列表功能时,客户端和服务端显示内容截图。
客户端:
服务端:
Wireshark 抓取的数据包截图(展开应用层数据包,标记请求、响应类型、返回的客户端列表
数据对应的位置):
相关的服务器的处理代码片段:
tmp_link = "\tID\tIP\tPort\n";
for (auto i : Clients_info)
tmp_link = tmp_link + "\t" + to_string(i.first) + "\t" + i.second.ip + "\t"
+ to_string(i.second.port) + "\n";
strcpy(rec.MES, tmp_link.c_str());
客户端选择发送消息功能时,客户端和服务端显示内容截图。
发送消息的客户端:
服务器:
接收消息的客户端:
Wireshark 抓取的数据包截图(发送和接收分别标记):
发送:
接受:
相关的服务器的处理代码片段:
cout << "Send message from " << client << " to " << rec.DES << endl;
tmp_text = rec.MES;
tmp_text = "message from [" + to_string(client) + "]: \n" + tmp_text;
memset(sen.MES, 0, sizeof(sen.MES));
strcpy(sen.MES, tmp_text.c_str());
sen.DES = rec.DES;
cout << "Start sending..." << endl;
ret = send(sen.DES, (char*)&sen, sizeof(sen), 0);
if (ret < 0) {
cout << "send failed" << endl;
strcpy(rec.MES, "Sending Failed");
}
相关的客户端(发送和接收消息)处理代码片段:发送:
sen.OPT = SEND ;
cout << "To. " ;
cin >> des_num ;
sen.DES = des_num ;
cout << "Input the text need to send:\n" ;
cin >> Text ;
strcpy(sen.MES, Text.c_str()) ;
接受就是前面接受服务器消息的方法。
拔掉客户端的网线,然后退出客户端程序。观察客户端的 TCP 连接状态,并使用 Wireshark观察客户端是否发出了 TCP 连接释放的消息。同时观察服务端的 TCP 连接状态在较长时间内分钟以上)是否发生变化。直接用关闭窗口的方式关掉了,有释放连接。
再次连上客户端的网线,重新运行客户端程序。选择连接功能,连上后选择获取客户端列表功能,查看之前异常退出的连接是否还在。选择给这个之前异常退出的客户端连接发送消息,出现了什么情况?
没有了该线程对应的 while 循环中会在链接断开的时候立即得到链接失败的信息。
断开前:
断开后:
修改获取时间功能,改为用户选择 1 次,程序内自动发送 100 次请求。服务器是否正常处理了次请求,截取客户端收到的响应(通过程序计数一下是否有 100 个响应回来),并使用Wireshark 抓取数据包,观察实际发出的数据包个数。
开始:
结束:
开始:
结束:
多个客户端同时连接服务器,同时发送时间请求(程序内自动连续调用 100 次 send),服务器和客户端的运行截图
资源下载地址:https://download.csdn.net/download/sheziqiong/85836435
资源下载地址:https://download.csdn.net/download/sheziqiong/85836435