2024-04-22
大学课程
00

目录

1 实验给定要求
2 实验要求分析
3 v1代码

主要内容:

Linux下的TCP文件共享系统实验记录、设计过程、具体代码。

1 实验给定要求

相关信息

设计性实验2

  • 日期:2024-4-29
  • 课时:4-6
  • 题目:Linux下的TCP文件共享系统

内容: 设计并实现一个基于tcp的文件共享系统,均采用命令行界面。

要求如下:

Linux服务端:

  1. 所有参数通过命令行设置,若运行时提供的参数不全,则显示使用方法
  2. 可通过命令行设置共享目录,该参数的位置自定义。客户端只能看到共享目录下的文件。
  3. 多个用户可以同时读取(下载)同一个文件
  4. 不提供上传功能
  5. 可以向客户端传送文件目录信息,至少要包括文件名及大小两个属性,也可以包含修改日期等
  6. 具有并发性,并发的实现机制限于多线程、非阻塞模式、select模型、epoll模型,可以混用
  7. 至少要在两个不同的共享目录下完成测试

客户端:

  1. 所有参数通过命令行设置,若运行时提供的参数不全,则显示使用方法
  2. 可向服务端发送文本或二进制格式的请求,自定义
  3. 可下载服务端的文件目录
  4. 可下载指定的文件。若本地已存在同名文件,应自动重命名处理,具体规则自行规定
  5. 做好可移植性的准备。先在Linux平台上实现,通过测试后移植到Windows平台上。

应用层协议:

  1. 自行规定应用层协议,并形成相应文档
  2. 在实验报告中体现要点
  3. 设计应用层协议时,在满足功能要求的基础上,尽可能减少数据传输量

2 实验要求分析

服务端

对外提供两个接口,一个是请求共享文件夹的文件夹内的所有文件的路径信息,每个文件包括文件名、大小和修改日期三个属性;另一个是请求下载共享文件夹内的任意文件或文件目录。

  1. 请求所有文件信息:服务端需要读取给定的路径下的所有文件,并将这些文件的文件名、详细信息和目录一起发送给客户端
  2. 请求下载文件/文件目录:服务端接受客户端的请求文件,首先确定是否存在该文件。如果是,则将其文件详细内容发回客户端;如果否,则发送错误信息

客户端

接收用户输入,发送对应服务端两个接口的两种请求。

  1. 请求所有文件信息:客户端需要接收服务端的信息,还原文件夹的树形结构并且进行展示
  2. 请求下载文件/文件目录:客户端接收用户输入并且将其封装在报文中发送给服务端,等待服务端的回传,将其写入文件中。

3 v1代码

  • 服务端:
cpp
#include <stdio.h> #include <dirent.h> #include <libgen.h> #include <stdlib.h> #include <vector> #include <string> #include <iostream> #include <cstring> #include <filesystem> #include <stack> #include <fstream> #include <queue> #include <sys/stat.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <thread> #include <mutex> # define CMD_HELLO 1 # define CMD_QUERY 2 # define CMD_FILEINVAILD 4 # define CMD_FILEFOUND 5 using namespace std; mutex mtx; // 创建指定长度的- string spaces(int num) { string result; for (int i = 0; i < num; i++) { result += "----"; } return result; } // RootDictStructure类 它的实例抽象意义上代表一个文件夹和它里面的所有内容 class RootDictStructure { private: vector<RootDictStructure> childFolders; // 存储子文件夹的实例 vector<string> files; // 存储文件夹下所有文件的文件名 string path; // 指向这个文件夹的绝对路径 public: string folderName; // 该文件夹的文件夹名 RootDictStructure(string rootPath); string isExist(string targetName); string toTreeString(); string toString(); RootDictStructure() { // 无参的默认构造函数 folderName = ""; }; }; // RootDictStructure类的构造方法 传入一个文件夹的绝对路径 返回该文件夹所对应的RootDictStructure类的实例 // 该方法会对子文件夹递归地创建RootDictStructure类的实例 RootDictStructure::RootDictStructure(string rootPath) { path = rootPath; folderName = filesystem::path(path).stem().string(); try { for (const auto& entry : std::filesystem::directory_iterator(rootPath)) { if (entry.is_directory()) { childFolders.push_back(RootDictStructure(entry.path())); } else if (entry.is_regular_file()) { files.push_back(entry.path().filename()); } } } catch (const std::filesystem::filesystem_error& e) { std::cout << "Error accessing folder: " << e.what() << std::endl; } } // 对于给定的一个文件/文件夹的名 判断它是否存在于这个文件夹中 // 如果存在 则返回该文件/文件夹的绝对路径 // 如果不存在 则返回空字符串 string RootDictStructure::isExist(string targetName) { if (folderName == targetName) { return path; } for (int i = 0; i < files.size(); i++) { if (files[i] == targetName) { return path + "/" + files[i]; } } // 队列 做层序用的 std::queue<RootDictStructure> queue; for (int s = 0; s < childFolders.size(); s++) { queue.push(childFolders[s]); } while (queue.size() != 0) { RootDictStructure element = queue.front(); queue.pop(); if (element.folderName == targetName) { return element.path; } for (int i = 0; i < element.files.size(); i++) { if (element.files[i] == targetName) { return element.path + "/" + element.files[i]; } } for (int i = 0; i < element.childFolders.size(); i++) { queue.push(element.childFolders[i]); } } return ""; } // 将文件夹内的所有内容抽象为文件夹树并且输出 该方法不在网络传输中使用 仅作为检查信息 string RootDictStructure::toTreeString() { string result = folderName + " (Folder)" + "\n"; for (int i = 0; i < files.size(); i++) { result = result + "----" + files[i] + " (File)" + "\n"; } // 栈 做前序用的 栈里的每个元素都对应一个文件夹结构和它在树中的深度 std::stack<std::pair<RootDictStructure, int>> stack; for (int s = childFolders.size() - 1; s >= 0; s--) { stack.push({childFolders[s], 1}); } while (stack.size() != 0) { std::pair<RootDictStructure, int> element = stack.top(); stack.pop(); result = result + spaces(element.second) + element.first.folderName + " (Folder)" + "\n"; for (int i = 0; i < element.first.files.size(); i++) { result = result + spaces(element.second + 1); result += element.first.files[i]; result += " (File)"; result += "\n"; } for (int i = element.first.childFolders.size() - 1; i >= 0; i--) { stack.push({element.first.childFolders[i], element.second + 1}); // 深度 + 1 } } return result; } // 将文件夹内的所有文件结构转换为可发送的字符串 string RootDictStructure::toString() { /* 格式: 最开始的是主文件夹名 之后每一个{}内部表示一个文件夹和它内部的所有子文件 最开始是文件夹名 []内表示该文件夹在文件树上的层数 之后是文件名 ()内表示该文件的占用子节数 */ string result = ""; // 栈 做前序用的 栈里的每个元素都对应一个文件夹结构和它在树中的深度 std::stack<std::pair<RootDictStructure, int>> stack; stack.push({*this, 0}); struct stat statbuf; // 读取文件信息所需要的结构体 while (stack.size() != 0) { std::pair<RootDictStructure, int> element = stack.top(); stack.pop(); result = result + "{" + element.first.folderName + "[" + std::to_string(element.second) + "]"; for (int i = 0; i < element.first.files.size(); i++) { result += element.first.files[i]; stat((element.first.path + "/" + element.first.files[i]).c_str(), &statbuf); // 这个statbuf.st_size是off_t类型 是偏移位类型 linux里可以等效是long类型 result += "(" + std::to_string(statbuf.st_size) + ")"; // 这个操作会将其转换为字符串类型 使用unicode编码 还有上面的element.second也是 整型转字符串 使用unicode编码 } result += "}"; for (int i = element.first.childFolders.size() - 1; i >= 0; i--) { stack.push({element.first.childFolders[i], element.second + 1}); // 深度 + 1 } } return result; } // 按十六进制打印给定的字符串 仅作检查信息使用 void printHex(const std::string& str) { std::stringstream ss; for (int i = 0; i < str.length(); i++) { unsigned char c = static_cast<unsigned char>(str[i]); ss << std::setw(2) << std::setfill('0') << std::hex << static_cast<int>(c); if ((i + 1) % 2 == 0) { ss << " "; } if ((i + 1) % 24 == 0) { ss << std::endl; } } std::cout << ss.str() << std::endl; } RootDictStructure root; // 线程的启动函数 处理每一个连接 void clientHandler(int socketfd, string clientInfo) { mtx.lock(); cout << "Received Hello from " << clientInfo << ", Thread ID is: " << this_thread::get_id() << endl; mtx.unlock(); char buffer[2048]; memset(buffer, 0, sizeof(buffer)); int n = recv(socketfd, buffer, sizeof(buffer), 0); if (n < 0) { perror("recv error!"); return; } if (buffer[0] != CMD_HELLO) { mtx.lock(); cout << "Hello from " << clientInfo << " is invaild: " << buffer << endl; mtx.unlock(); close(socketfd); return; } string reply = root.toString(); n = send(socketfd, reply.c_str(), strlen(reply.c_str()), 0); if (n < 0) { perror("send error"); return; } while (true) { sleep(1); memset(buffer, 0, sizeof(buffer)); n = recv(socketfd, buffer, sizeof(buffer), 0); if (n < 0) { perror("recv error!"); return; } if (n == 0) { mtx.lock(); cout << "Exit from " << clientInfo << endl; mtx.unlock(); close(socketfd); return; } if (buffer[0] == CMD_QUERY) { string queryFileName = string(buffer + 1); mtx.lock(); cout << "Query File: " + queryFileName + " from " << clientInfo << endl; mtx.unlock(); string queryFilePath = root.isExist(queryFileName); memset(buffer, 0, sizeof(buffer)); if (queryFilePath == "") { mtx.lock(); cout << "Query File: " + queryFileName + " from " << clientInfo << " is invaild: " << buffer << endl; mtx.unlock(); buffer[0] = CMD_FILEINVAILD; n = send(socketfd, buffer, 2048, 0); if (n < 0) { perror("send error"); return; } } else { mtx.lock(); cout << "Query File: " + queryFileName + " from " << clientInfo << " is found!" << endl; mtx.unlock(); ifstream file(queryFilePath); if (!file.is_open()) { mtx.lock(); cout << "Query File: " + queryFileName + " from " << clientInfo << " can not open!" << endl; mtx.unlock(); } file.seekg(0, std::ios::end); std::streampos fileSize = file.tellg() + 1; file.seekg(0, std::ios::beg); char* fileBuffer = new char[fileSize]; fileBuffer[0] = CMD_FILEFOUND; file.read(fileBuffer + 1, fileSize - 1); file.close(); n = send(socketfd, fileBuffer, fileSize, 0); if (n < 0) { perror("send error"); return; } delete[] fileBuffer; } } else { mtx.lock(); cout << "Query from " << clientInfo << " is invaild: " << buffer << ", try to receive again!" << endl; mtx.unlock(); } } close(socketfd); } int main(int argc, char const *argv[]) { if (argc != 4) { cout << "Usage: ./server ip port dictionary" << endl; return 0; } root = RootDictStructure(argv[3]); cout << "File Structure is Loaded:" << endl; cout << root.toTreeString() << endl; int port = atoi(argv[2]); string serverIP = argv[1]; int server_socket = socket(AF_INET, SOCK_STREAM, 0); if (server_socket == -1) { perror("Create Socket Failed: \n"); return -1; } struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = inet_addr(serverIP.c_str()); int ret = bind(server_socket, (struct sockaddr*)&addr, sizeof(addr)); if (0 > ret) { perror("Bind Failed: \n"); close(server_socket); return -1; } cout << "Start Serving" << endl; listen(server_socket, 10); int clientfd; struct sockaddr_in client_addr; socklen_t client_addr_len = sizeof(client_addr); char client_ip[INET_ADDRSTRLEN]; int client_port = 0; while (true) { clientfd = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len); if (clientfd < 0) { perror("accept error: "); continue; } inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip, INET_ADDRSTRLEN); client_port = ntohs(client_addr.sin_port); thread t(clientHandler, clientfd, string(client_ip) + ":" + to_string(client_port)); t.detach(); } return 0; }
  • 客户端:
cpp
#include <iostream> #include <string> #include <vector> #include <bits/stdc++.h> #include <unordered_map> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <fstream> # define CMD_HELLO 1 # define CMD_QUERY 2 # define CMD_FILEINVAILD 4 # define CMD_FILEFOUND 5 using namespace std; string spaces(int num) { string result; for (int i = 0; i < num; i++) { result += "----"; } return result; } // parseStringToFolder 传入一段表示文件夹结构的字符串 将其进行解析并且输出 void parseString(string inputString) { int rightPointer = 0, leftPointer = 0; while (true) { if (inputString[rightPointer] != '[') { rightPointer += 1; } else { break; } } string folderName = inputString.substr(0, rightPointer); // 找传入的文件夹的名字 leftPointer = rightPointer + 1; while (inputString[rightPointer] != ']') { rightPointer += 1; } int depth = stoi(inputString.substr(leftPointer, rightPointer - leftPointer)); // 找传入的文件夹的对应节点的深度 cout << spaces(depth) + folderName + " (Folder)" << endl; // 开始解析该文件夹下的文件信息 leftPointer = rightPointer + 1; while (rightPointer < inputString.size()) { if (inputString[rightPointer] != '(') { rightPointer += 1; continue; } else { string fileName = inputString.substr(leftPointer, rightPointer - leftPointer); leftPointer = rightPointer + 1; while (inputString[rightPointer] != ')') { rightPointer += 1; } cout << spaces(depth + 1) + fileName + " (File)" + " Size: " + inputString.substr(leftPointer, rightPointer - leftPointer) + " bytes" << endl; leftPointer = rightPointer + 1; } } } // startParseString 开始将传入的表示全部文件夹结构字符串进行解析 并按照文件夹结构的方式进行输出 void startParseString(string inputString) { cout << "Start Parse String To Folder Tree" << endl; int rightPointer = 0, leftPointer = 0; // 先找根节点 while (true) { if (inputString[rightPointer] != '}') { rightPointer += 1; } else { break; } } while (true) { if (inputString[leftPointer] != '{') { rightPointer += 1; } else { break; } } parseString(inputString.substr(leftPointer + 1, rightPointer - leftPointer - 1)); // 再找剩下的子节点 rightPointer += 1; while (rightPointer < inputString.size()) { leftPointer = rightPointer; while (inputString[rightPointer] != '}') { rightPointer += 1; } parseString(inputString.substr(leftPointer + 1, rightPointer - leftPointer - 1)); rightPointer += 1; } cout << "Parse Folder Tree Finished!" << endl; } int main(int argc, char const *argv[]) { if (argc != 3) { cout << "Usage: ./client ip port" << endl; return 0; } int socketfd = socket(AF_INET, SOCK_STREAM, 0); if (socketfd == -1) { perror("create socket failed: \n"); return -1; } struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(atoi(argv[2])); server_addr.sin_addr.s_addr = inet_addr(argv[1]); if (connect(socketfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("connect server failed: \n"); return -1; } char buffer[4096]; memset(buffer, 0, sizeof(buffer)); buffer[0] = CMD_HELLO; ssize_t bytes_sent = send(socketfd, buffer, sizeof(buffer), 0); if (bytes_sent == -1) { perror("sending hello failed: \n"); close(socketfd); return -1; } buffer[0] = 0; ssize_t bytes_recv = recv(socketfd, buffer, sizeof(buffer), 0); if (bytes_recv == -1) { perror("receiving file structure failed: \n"); close(socketfd); return -1; } cout << "Connect to Server Successful! Server File Structure: " << endl; startParseString(buffer); memset(buffer, 0, sizeof(buffer)); string fileName = ""; while (true) { cout << "Please Enter File Name that will Sync to There, You can Exit with Enter 'exit': " << endl; cin >> fileName; if (fileName == "exit") { break; } buffer[0] = CMD_QUERY; strcpy(buffer + 1, fileName.c_str()); bytes_sent = send(socketfd, buffer, sizeof(buffer), 0); if (bytes_sent == -1) { perror("sending query failed: \n"); close(socketfd); return -1; } memset(buffer, 0, sizeof(buffer)); ssize_t bytes_recv = recv(socketfd, buffer, sizeof(buffer), 0); if (bytes_recv == -1) { perror("receiving data failed: \n"); close(socketfd); return -1; } if (buffer[0] == CMD_FILEINVAILD) { cout << "File is Invaild!, Please Enter File Name Again!" << endl; continue; } else if (buffer[0] == CMD_FILEFOUND) { cout << "File is Found!" << endl; ofstream file(fileName); file.write(buffer + 1, bytes_recv - 1); if (bytes_recv == 4096) { while (true) { memset(buffer, 0, sizeof(buffer)); ssize_t bytes_recv = recv(socketfd, buffer, sizeof(buffer), 0); if (bytes_recv == -1) { perror("receiving data failed: \n"); close(socketfd); return -1; } else { file.write(buffer, bytes_recv); if (bytes_recv != 4096) { break; } } } } file.flush(); file.close(); cout << "Receive File Successfully!" << endl; } } close(socketfd); return 1; }

本文作者:御坂19327号

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!