iOS进阶_多线程(三.多线程属性和安全隐患)

本文介绍了iOS开发中线程的基本属性如name、isMainThread及threadPriority,并探讨了多线程开发中的安全隐患及其解决方法,包括使用互斥锁和原子属性确保数据安全。

线程属性

  • name属性,在大型的商业项目中,通常希望程序在崩溃的时候,能够获取到程序准确的所在的线程!
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self threadDemo];
}

-(void)threadDemo{
    NSThread * t =[[NSThread alloc]initWithTarget:self selector:@selector(demo) object:nil];
    t.name =@"Thread A";
    [t start];
}
-(void)demo{
    for (int i=0; i<20; i++) {
        NSLog(@"%@ %d",[NSThread currentThread],i);
    }   
}
  • isMainThread属性 判断当前线程是不是主线程

  • threadPriority属性 线程优先级
    优先级 从 0.0 到 1.0 默认值0.5
    优先级只是保证CPU调度的可能性更高

多线程目的:将耗时操作放在后台,不阻塞UI线程
建议:在开发的时候,不要修改优先级

在多线程开发中,不要相信一次的运行结果!!

-(void)threadDemo{
    NSThread * t =[[NSThread alloc]initWithTarget:self selector:@selector(demo) object:nil];
    t.name =@"Thread A";
    t.threadPriority = 0.1;
    [t start];

    NSThread * t1 =[[NSThread alloc]initWithTarget:self selector:@selector(demo) object:nil];
    t1.name =@"Thread B";
    t1.threadPriority = 1.0;
    [t1 start];
}

多线程的安全隐患

  • 资源共享
    1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
    比如多个线程访问同一对象,同一个变量,同一个文件

  • 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题

这里写图片描述

安全隐患解决-互斥锁

这里写图片描述

互斥锁的缺点是仅支持串行操作

#import "ViewController.h"

@interface ViewController ()
//票数
@property(assign,nonatomic)int tickets;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.tickets = 20;
    NSThread * t1 =[[NSThread alloc]initWithTarget:self selector:@selector(saleTickets) object:nil];
    t1.name =@"售票员A";
    [t1 start];

    NSThread * t2 =[[NSThread alloc]initWithTarget:self selector:@selector(saleTickets) object:nil];
    t2.name =@"售票员B";
    [t2 start];
}

-(void)saleTickets{
    while (YES) {
        [NSThread sleepForTimeInterval:1.0];//卖票需要消耗1秒的时间
        //    1.判断是否有票
        if(self.tickets >0){
            //2.如果有就卖一张
            self.tickets--;
            NSLog(@"剩下%d张票 %@",self.tickets,[NSThread currentThread]);
        }else{
            //3.如果没有了,提示用户
            NSLog(@"卖完了!%@",[NSThread currentThread]);
            break;
        }
    }
}
@end

我们运行代码,可以明显看到数据错误。

解决方案:添加互斥锁 synchronized

-(void)saleTickets{
    while (YES) {
        [NSThread sleepForTimeInterval:1.0];
        //互斥锁 -- 保证锁内的代码,同一时间只有一条线程执行
        //互斥锁的范围应该尽量小,范围打了,效率就差
        //参数:任意OC对象都OK!一般用self      
        //注意:局部变量,每一个线程单独拥有的,因此没法加锁!!
        //NSObject * lock =[NSObject new];
        //@synchronized(lock){
        @synchronized(self){
            //    1.判断是否有票
            if(self.tickets >0){
                //2.如果有就卖一张
                self.tickets--;
                NSLog(@"剩下%d张票 %@",self.tickets,[NSThread currentThread]);
            }else{
                //3.如果没有了,提示用户
                NSLog(@"卖完了!%@",[NSThread currentThread]);
                break;
            }
        }
    }
}

原子属性

  • nonatomic 非原子属性
  • atomic 原子属性,保证这个属性的安全性(线程安全),就是针对多线程设计的

原子属性的目的:多个线程写入这个对象的时候,保证同一时间只有一个线程能够执行
单写多读的一种多线程技术,同样可能出现“脏数据”,重新读一下。

#import "ViewController.h"
@interface ViewController ()

@property(nonatomic,strong)NSObject * lockObjc;
/*原子属性*/
@property(atomic,strong)NSObject * myAtomic;
@end

@implementation ViewController

@synthesize myAtomic = _myAtomic;
/**
 OC中定义属性,通常会生成 _成员变量  如果同时重写了getter和setter方法 _成员变量就不自动生成
@synthesize  合成指令,在Xcode4.5 之前非常常见

 @param myAtomic 自定义成员变量
 */
-(void)setMyAtomic:(NSObject *)myAtomic{
    @synchronized(self){//互斥锁 加锁
        _myAtomic = myAtomic;
    }
}

-(NSObject *)myAtomic{
    return _myAtomic;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    NSData * data;
    //原子属性 ==YES 先把文件保存在一个临时的文件中,等全部写入后,再改名
    [data writeToFile:@"wt.mp4" atomically:YES];
}

实际上,原子属性内部有一个锁,自旋锁
自旋锁 & 互斥锁

  • -共同点:

    都能够保证线程安全

  • -不同点:

    互斥锁:如果线程被锁在外面,线程就会进入休眠状态,等待锁打开,然后被唤醒
    自旋锁:如果线程被锁在外面,线程就会用死循环的方式一直等待锁被打开

    总结:无论什么锁,都很消耗性能,效率不高

    • 线程安全
      在多个线程进行读写操作时,仍然保证数据正确
    • UI线程,共同约定:所有更新UI的操作都放在主线程上执行
      原因:UIKit框架都是线程不安全的!!(因为线程安全效率下降!)

iOS开发的建议:

  • 所有属性都声明为nonatonic
  • 尽量避免多线程抢夺同一块资源
  • 尽量将加锁,资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力
// server.cpp - 文件中转服务器,支持上传时设置密码 #include <winsock2.h> #include <windows.h> #include <iostream> #include <fstream> #include <string> #include <vector> #include <map> #include <thread> #include <mutex> #include <sys/stat.h> #pragma comment(lib, "ws2_32.lib") using namespace std; const int BUFFER_SIZE = 4096; const string STORAGE_DIR = "./storage/"; mutex fileMutex; map<string, size_t> fileList; // 文件名 -> 大小 map<string, string> filePasswords; // 文件名 -> 密码(明文) void initWinsock() { WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { cout << "初始化网络失败,请重试。" << endl; exit(1); } } void cleanupWinsock() { WSACleanup(); } bool fileExists(const string& filename) { ifstream f(filename, ios::binary); return f.good(); } size_t getFileSize(const string& filename) { struct stat buf; if (stat(filename.c_str(), &buf) == 0) { return static_cast<size_t>(buf.st_size); } return 0; } bool createStorageDir() { WIN32_FIND_DATA ffd; HANDLE hFind = FindFirstFile(STORAGE_DIR.c_str(), &ffd); if (hFind == INVALID_HANDLE_VALUE) { if (!CreateDirectory(STORAGE_DIR.c_str(), NULL)) { cout << "无法创建存储目录 " << STORAGE_DIR << endl; return false; } } else { FindClose(hFind); } return true; } vector<string> getStoredFiles() { vector<string> files; string searchPath = STORAGE_DIR + "*.*"; WIN32_FIND_DATA ffd; HANDLE hFind = FindFirstFile(searchPath.c_str(), &ffd); if (hFind != INVALID_HANDLE_VALUE) { do { if (!(ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { files.push_back(ffd.cFileName); } } while (FindNextFile(hFind, &ffd)); FindClose(hFind); } lock_guard<mutex> lock(fileMutex); fileList.clear(); for (const auto& f : files) { ifstream fin(STORAGE_DIR + f, ios::binary | ios::ate); if (fin.is_open()) { fileList[f] = fin.tellg(); fin.close(); } } return files; } void sendFileList(SOCKET sock) { auto files = getStoredFiles(); int count = files.size(); send(sock, (const char*)&count, sizeof(count), 0); for (const auto& name : files) { int len = name.length(); send(sock, (const char*)&len, sizeof(len), 0); send(sock, name.c_str(), len, 0); size_t size = fileList[name]; send(sock, (const char*)&size, sizeof(size), 0); } cout << "已发送文件列表给客户端(共 " << count << " 个文件)" << endl; } void saveReceivedFile(SOCKET sock) { int nameLen; recv(sock, (char*)&nameLen, sizeof(nameLen), 0); string filename(nameLen, '\0'); recv(sock, &filename[0], nameLen, 0); size_t fileSize; recv(sock, (char*)&fileSize, sizeof(fileSize), 0); bool requiresPassword; recv(sock, (char*)&requiresPassword, sizeof(requiresPassword), 0); string password = ""; if (requiresPassword) { int pwdLen; recv(sock, (char*)&pwdLen, sizeof(pwdLen), 0); password.resize(pwdLen); recv(sock, &password[0], pwdLen, 0); } string fullPath = STORAGE_DIR + filename; int copyIndex = 1; string baseName = filename.substr(0, filename.find_last_of('.')); string ext = filename.substr(filename.find_last_of('.')); string finalName = filename; while (fileExists(fullPath)) { finalName = baseName + "(" + to_string(copyIndex) + ")" + ext; fullPath = STORAGE_DIR + finalName; copyIndex++; } if (finalName != filename) { int newLen = finalName.length(); send(sock, (const char*)&newLen, sizeof(newLen), 0); send(sock, finalName.c_str(), newLen, 0); cout << "文件重命名为 \"" << finalName << "\" 避免覆盖" << endl; } else { send(sock, (const char*)&nameLen, sizeof(nameLen), 0); send(sock, filename.c_str(), nameLen, 0); } cout << "正在接收文件 \"" << filename << "\" -> \"" << finalName << "\" (" << fileSize << " 字节)" << endl; ofstream outFile(fullPath, ios::binary); if (!outFile.is_open()) { cout << "无法创建文件进行写入: " << finalName << endl; return; } char buffer[BUFFER_SIZE]; size_t totalReceived = 0; int lastPercent = -1; while (totalReceived < fileSize) { int toRead = min(BUFFER_SIZE, static_cast<int>(fileSize - totalReceived)); int bytesRead = recv(sock, buffer, toRead, 0); if (bytesRead <= 0) break; outFile.write(buffer, bytesRead); totalReceived += bytesRead; int percent = (int)((double)totalReceived / fileSize * 100); if (percent != lastPercent && percent % 5 == 0) { cout << " 进度: " << percent << "% (" << totalReceived << "/" << fileSize << ")" << endl; lastPercent = percent; } } outFile.close(); if (totalReceived == fileSize) { cout << "文件 \"" << finalName << "\" 接收完成!" << endl; lock_guard<mutex> lock(fileMutex); fileList[finalName] = fileSize; if (requiresPassword && !password.empty()) { filePasswords[finalName] = password; cout << "已为文件 \"" << finalName << "\" 设置密码保护。" << endl; } else { filePasswords.erase(finalName); } } else { cout << "文件接收中断,删除不完整文件。" << endl; remove(fullPath.c_str()); } } void sendFileToClient(SOCKET sock) { int nameLen; int ret = recv(sock, (char*)&nameLen, sizeof(nameLen), 0); if (ret <= 0) { cout << "接收文件名长度失败,连接中断。" << endl; return; } string filename(nameLen, '\0'); ret = recv(sock, &filename[0], nameLen, 0); if (ret <= 0) { cout << "接收文件名失败。" << endl; return; } string fullPath = STORAGE_DIR + filename; ifstream inFile(fullPath, ios::binary | ios::ate); if (!inFile.is_open()) { size_t notFound = (size_t)-1; send(sock, (const char*)&notFound, sizeof(notFound), 0); cout << "请求的文件 \"" << filename << "\" 不存在!" << endl; return; } auto it = filePasswords.find(filename); bool isProtected = (it != filePasswords.end()); send(sock, (const char*)&isProtected, sizeof(isProtected), 0); bool accessGranted = true; if (isProtected) { char pwdBuffer[256] = {0}; // 必须完整接收 256 字节 int total = 0; while (total < 256) { ret = recv(sock, pwdBuffer + total, 256 - total, 0); if (ret <= 0) { cout << "接收密码时连接中断。" << endl; closesocket(sock); return; } total += ret; } string receivedPwd = pwdBuffer; if (receivedPwd != it->second) { accessGranted = false; cout << "客户端尝试下载 \"" << filename << "\" 但密码错误!" << endl; } else { cout << "密码正确,允许下载 \"" << filename << "\"" << endl; } } // 无论是否加密,都必须返回授权状态 send(sock, (const char*)&accessGranted, sizeof(accessGranted), 0); if (!accessGranted) { return; // 不继续发送文件 } size_t fileSize = inFile.tellg(); inFile.seekg(0, ios::beg); send(sock, (const char*)&fileSize, sizeof(fileSize), 0); cout << "正在发送文件 \"" << filename << "\" (" << fileSize << " 字节) ..." << endl; char buffer[BUFFER_SIZE]; size_t totalSent = 0; int lastPercent = -1; while (inFile.read(buffer, BUFFER_SIZE), inFile.gcount() > 0) { streamsize chunkSize = inFile.gcount(); if (send(sock, buffer, chunkSize, 0) == SOCKET_ERROR) { cout << "发送失败,连接中断。" << endl; break; } totalSent += chunkSize; int percent = (int)((double)totalSent / fileSize * 100); if (percent != lastPercent && percent % 5 == 0) { cout << " 进度: " << percent << "% (" << totalSent << "/" << fileSize << ")" << endl; lastPercent = percent; } } inFile.close(); if (totalSent == fileSize) cout << "文件 \"" << filename << "\" 发送完成!" << endl; } void handleClient(SOCKET clientSocket) { cout << "新客户端接入,开始处理请求..." << endl; while (true) { int cmd; int ret = recv(clientSocket, (char*)&cmd, sizeof(cmd), 0); if (ret <= 0) { cout << "客户端断开连接。" << endl; break; } switch (cmd) { case 1: saveReceivedFile(clientSocket); break; case 2: sendFileToClient(clientSocket); break; case 3: sendFileList(clientSocket); break; case 0: cout << "客户端主动退出。" << endl; closesocket(clientSocket); return; default: cout << "未知指令: " << cmd << endl; break; } } closesocket(clientSocket); } int main() { setlocale(LC_ALL, ".UTF8"); initWinsock(); if (!createStorageDir()) return 1; int port; cout << "========== 文件中转服务器 ==========" << endl; cout << "请输入监听端口(例如 8888): "; cin >> port; SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, 0); if (serverSocket == INVALID_SOCKET) { cout << "创建套接字失败。" << endl; return 1; } sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(port); serverAddr.sin_addr.s_addr = INADDR_ANY; if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) { cout << "绑定端口失败,请检查是否被占用。" << endl; closesocket(serverSocket); return 1; } if (listen(serverSocket, 5) == SOCKET_ERROR) { cout << "启动监听失败。" << endl; closesocket(serverSocket); return 1; } cout << "中转服务器已启动,正在监听端口 " << port << " ..." << endl; cout << "所有文件将保存在 .\\storage\\ 目录下" << endl; while (true) { sockaddr_in clientAddr; int addrLen = sizeof(clientAddr); SOCKET clientSock = accept(serverSocket, (struct sockaddr*)&clientAddr, &addrLen); if (clientSock != INVALID_SOCKET) { string ip = inet_ntoa(clientAddr.sin_addr); cout << "\n来自 [" << ip << "] 的客户端已连接,开启新线程处理..." << endl; thread t(handleClient, clientSock); t.detach(); // 分离线程 } } closesocket(serverSocket); cleanupWinsock(); return 0; } // client.cpp - 文件中转客户端 #include <winsock2.h> #include <windows.h> #include <iostream> #include <fstream> #include <string> #include <vector> #include <sys/stat.h> #pragma comment(lib, "ws2_32.lib") using namespace std; const int BUFFER_SIZE = 4096; void initWinsock() { WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { cout << "初始化网络失败,请重试。" << endl; exit(1); } } void cleanupWinsock() { WSACleanup(); } bool fileExists(const string& filename) { ifstream f(filename, ios::binary); return f.good(); } size_t getFileSize(const string& filename) { struct stat buf; if (stat(filename.c_str(), &buf) == 0) { return static_cast<size_t>(buf.st_size); } return 0; } struct FileInfo { string name; size_t size; }; vector<FileInfo> requestFileList(SOCKET sock) { int cmd = 3; send(sock, (const char*)&cmd, sizeof(cmd), 0); int count; recv(sock, (char*)&count, sizeof(count), 0); vector<FileInfo> files; for (int i = 0; i < count; ++i) { int len; recv(sock, (char*)&len, sizeof(len), 0); string name(len, '\0'); recv(sock, &name[0], len, 0); size_t size; recv(sock, (char*)&size, sizeof(size), 0); files.push_back({name, size}); } return files; } void displayFileList(const vector<FileInfo>& files) { if (files.empty()) { cout << "当前服务器上没有可下载的文件。\n" << endl; return; } cout << "\n服务器文件列表:" << endl; cout << "--------------------------------------------------" << endl; for (size_t i = 0; i < files.size(); ++i) { const auto& f = files[i]; double mb = f.size / (1024.0 * 1024.0); cout << "[" << i+1 << "] " << f.name << " | " << f.size << " 字节" << " (" << (mb < 0.01 ? "<0.01" : to_string((int)(mb*100)/100.0)) << " MB)" << endl; } cout << "--------------------------------------------------" << endl; } void uploadFile(SOCKET sock) { string filepath; cout << "请输入要上传的文件路径: "; cin >> filepath; if (!fileExists(filepath)) { cout << "找不到该文件,请确认路径正确。\n" << endl; return; } size_t fileSize = getFileSize(filepath); ifstream inFile(filepath, ios::binary); if (!inFile.is_open()) { cout << "无法打开文件读取。\n" << endl; return; } size_t pos = filepath.find_last_of("\\/"); string filename = (pos == string::npos) ? filepath : filepath.substr(pos + 1); int cmd = 1; send(sock, (const char*)&cmd, sizeof(cmd), 0); int nameLen = filename.length(); send(sock, (const char*)&nameLen, sizeof(nameLen), 0); send(sock, filename.c_str(), nameLen, 0); send(sock, (const char*)&fileSize, sizeof(fileSize), 0); char protect; string password = ""; cout << "是否为此文件设置下载密码?(y/n): "; cin >> protect; bool requiresPassword = (protect == 'y' || protect == 'Y'); send(sock, (const char*)&requiresPassword, sizeof(requiresPassword), 0); if (requiresPassword) { cout << "请输入下载口令: "; cin >> password; int pwdLen = password.length(); send(sock, (const char*)&pwdLen, sizeof(pwdLen), 0); send(sock, password.c_str(), pwdLen, 0); cout << "已设置密码保护,下载时需输入口令。\n" << endl; } else { cout << "该文件无需密码即可下载。\n" << endl; } cout << "正在上传 \"" << filename << "\" (" << fileSize << " 字节) ...\n" << endl; char buffer[BUFFER_SIZE]; size_t totalSent = 0; int lastPercent = -1; while (inFile.read(buffer, BUFFER_SIZE), inFile.gcount() > 0) { streamsize chunk = inFile.gcount(); if (send(sock, buffer, chunk, 0) == SOCKET_ERROR) { cout << "上传失败,连接中断。\n" << endl; inFile.close(); return; } totalSent += chunk; int percent = (int)((double)totalSent / fileSize * 100); if (percent != lastPercent && percent % 5 == 0) { cout << " 进度: " << percent << "% (" << totalSent << "/" << fileSize << ")" << endl; lastPercent = percent; } } inFile.close(); recv(sock, (char*)&nameLen, sizeof(nameLen), 0); string finalName(nameLen, '\0'); recv(sock, &finalName[0], nameLen, 0); cout << "文件上传成功!服务器保存为 \"" << finalName << "\"\n" << endl; } void downloadFile(SOCKET sock) { vector<FileInfo> files = requestFileList(sock); displayFileList(files); if (files.empty()) return; int choice; cout << "请输入要下载的文件编号 (1-" << files.size() << "),输入 0 取消: "; cin >> choice; if (choice < 1 || choice > (int)files.size()) { cout << "操作取消。\n" << endl; return; } const string& filename = files[choice - 1].name; size_t fileSize = files[choice - 1].size; int cmd = 2; send(sock, (const char*)&cmd, sizeof(cmd), 0); int nameLen = filename.length(); send(sock, (const char*)&nameLen, sizeof(nameLen), 0); send(sock, filename.c_str(), nameLen, 0); size_t actualSize; recv(sock, (char*)&actualSize, sizeof(actualSize), 0); if (actualSize == (size_t)-1) { cout << "文件已被删除或无法访问!\n" << endl; return; } bool isProtected; recv(sock, (char*)&isProtected, sizeof(isProtected), 0); if (isProtected) { string password; cout << "该文件受密码保护,请输入访问口令: "; cin >> password; char pwdBuffer[256] = {0}; strncpy(pwdBuffer, password.c_str(), 255); send(sock, pwdBuffer, 256, 0); // 固定发 256 字节 bool accessGranted; int ret = recv(sock, (char*)&accessGranted, sizeof(accessGranted), 0); if (ret <= 0) { cout << "与服务器断开连接!\n" << endl; return; } if (!accessGranted) { cout << "密码错误,禁止下载!\n" << endl; return; } else { cout << "密码正确,开始下载...\n" << endl; } } else { cout << "该文件无需密码,直接下载。\n" << endl; } ofstream outFile(filename, ios::binary); if (!outFile.is_open()) { cout << "无法创建本地文件 \"" << filename << "\" 进行写入。\n" << endl; return; } cout << "正在下载 \"" << filename << "\" (" << actualSize << " 字节) ...\n" << endl; char buffer[BUFFER_SIZE]; size_t totalReceived = 0; int lastPercent = -1; while (totalReceived < actualSize) { int toRead = min(BUFFER_SIZE, static_cast<int>(actualSize - totalReceived)); int bytesRead = recv(sock, buffer, toRead, 0); if (bytesRead <= 0) break; outFile.write(buffer, bytesRead); totalReceived += bytesRead; int percent = (int)((double)totalReceived / actualSize * 100); if (percent != lastPercent && percent % 5 == 0) { cout << " 进度: " << percent << "% (" << totalReceived << "/" << actualSize << ")" << endl; lastPercent = percent; } } outFile.close(); if (totalReceived == actualSize) { cout << "文件 \"" << filename << "\" 下载完成!\n" << endl; } else { cout << "下载中断,删除不完整文件。\n" << endl; remove(filename.c_str()); } } int main() { setlocale(LC_ALL, ".UTF8"); initWinsock(); string ip; int port; cout << "========== 文件中转客户端 ==========" << endl; cout << "请输入服务器IP地址(例如 127.0.0.1): "; cin >> ip; cout << "请输入服务器端口号(例如 8888): "; cin >> port; SOCKET sock = socket(AF_INET, SOCK_STREAM, 0); if (sock == INVALID_SOCKET) { cout << "创建套接字失败。\n" << endl; return 1; } sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(port); serverAddr.sin_addr.s_addr = inet_addr(ip.c_str()); cout << "正在连接服务器 " << ip << ":" << port << " ...\n" << endl; if (connect(sock, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) { cout << "连接服务器失败,请检查服务器是否运行。\n" << endl; closesocket(sock); return 1; } cout << "成功连接到中转服务器!\n" << endl; while (true) { cout << "\n主菜单:" << endl; cout << "1. 上传文件到服务器" << endl; cout << "2. 从服务器下载文件" << endl; cout << "3. 刷新文件列表" << endl; cout << "4. 退出程序" << endl; cout << "请选择操作: "; int choice; cin >> choice; switch (choice) { case 1: uploadFile(sock); break; case 2: downloadFile(sock); break; case 3: { auto files = requestFileList(sock); displayFileList(files); break; } case 4: { int cmd = 0; send(sock, (const char*)&cmd, sizeof(cmd), 0); cout << "已退出客户端程序。\n" << endl; closesocket(sock); cleanupWinsock(); system("pause"); return 0; } default: cout << "无效选择,请重新输入。\n" << endl; break; } } closesocket(sock); cleanupWinsock(); system("pause"); return 0; } 看看代码的Bug,并修改
最新发布
11-19
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值