基于C++实现(控制台)Socket 接口实现自定义协议通信
♻️ 资源大小1.49MB➡️资源下载https://download.csdn.net/download/s1t16/87430277基于 Socket 接口实现自定义协议通信一、实验目的学习如何设计网络应用协议掌握 Socket 编程接口编写基本的网络应用软件二、 实验内容根据自定义的协议规范使用 Socket 编程接口编写基本的网络应用软件。掌握 C 语言形式的 Socket 编程接口用法能够正确发送和接收网络数据包。开发一个客户端实现人机交互界面和与服务器的通信。开发一个服务端实现并发处理多个客户端的请求。程序界面不做要求使用命令行或最简单的窗体即可。功能要求如下运输层协议采用 TCP客户端采用交互菜单形式用户可以选择以下功能a) 连接请求连接到指定地址和端口的服务端。b) 断开连接断开与服务端的连接。c)获取时间: 请求服务端给出当前时间。d)获取名字请求服务端给出其机器的名称。e)活动连接列表请求服务端给出当前连接的所有客户端信息编号、IP 地址、端口等f)发消息请求服务端把消息转发给对应编号的客户端该客户端收到后显示在屏幕上g) 退出断开连接并退出客户端程序3.服务端接收到客户端请求后根据客户端传过来的指令完成特定任务a)向客户端传送服务端所在机器的当前时间。b)向客户端传送服务端所在机器的名称。c)向客户端传送当前连接的所有客户端信息。d)将某客户端发送过来的内容转发给指定编号的其他客户端。e)采用异步多线程编程模式正确处理多个客户端同时连接同时发送消息的情况。根据上述功能要求设计一个客户端和服务端之间的应用通信协议。本实验涉及到网络数据包发送部分不能使用任何的 Socket 封装类只能使用最底层的 C 语言形式的 Socket API。本实验可组成小组服务端和客户端可由不同人来完成。主要仪器设备联网的 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 IDREQ_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 IDREQ_TYPE OPT 请求的类型Char[1024] MES 消息内容固定长度描述指示数据包的格式画图说明指示类型的定义Int DES 目的地的 socket IDREQ_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服务器和客户端的运行截图五、实验结果与分析根据你编写的程序运行效果分别解答以下问题看完请删除本句客户端是否需要调用 bind 操作它的源端口是如何产生的每一次调用 connect时客户端的端口是否都保持不变不需要不是client 的端口是自动分配的。六、 假设在服务端调用 listen 和调用 accept 之间设了一个调试断点暂停在此断点时此时客户端调用 connect 后是否马上能连接成功可以。Accept 是取出链接的不影响 connect连续快速 send 多次数据后通过 Wireshark 抓包看到的发送的 Tcp Segment 次数是否和 send 的次数完全一致是服务器在同一个端口接收多个客户端的数据如何能区分数据包是属于哪个客户端的客户端的端口是不同的可以以此区分客户端主动断开连接后当时的 TCP 连接状态是什么这个状态保持了多久可以使用 netstat -an 查看立即断开几乎没有保持多久。客户端断网后异常退出服务器的 TCP 连接状态有什么变化吗服务器该如何检测连接是否继续有效TCP 会断开服务器在当前线程一直在询问链接的状态如果断开也会及时反馈。七、讨论、心得这感觉上课和之前的实验……和这个完全脱节了啊…做得一脸茫然 QAQ……然后对着参考瞎写一通我人没了.jpg做完准备截抓包图的时候发现 wireshark 没东西……只好重新整了一遍重装 wireshark 啥的……Client 的编译命令要加一个 “-lwsock32 -stdc11”本来发现 client 可以直接命令行编译还想抛弃 vs2019 来着结果 server 读 ip 的部分照着 c20 写了没时间搞一个支持 c20 的 g只好一个用命令行编译一个用 vs 了也还行反正直接用 exe 就好。在做讨论题的时候意识到好像可以通过在每一个线程中记录当前 socket 的 id在断开链接时也许可以删除数据库里 active linkage 的信息于是及时进行了修改但是前面部分来不及全部改完添加了这一部分