Linux网络编程之多线程并发TCP连接

1、要求

1.在编程实践2基础上,采用多线程技术和IO复用实现一个线程服务多个客户端,服务进程里面的多个线程服务大量客户端。要求:

(1)源代码格式化良好并适当注释;
(2)除上述明确要求的功能外,还要注意其它问题,比如线程互斥、服务程序支持线程数和每个线程支持客户端算的可配置性等;
(3)提交报告,报告中包括程序源代码和测试效果截图。

2、基本设计思路

2.1 基本框架

本次的实验的基本结构是由上次的实验的修改而来,也就是TCP连接传输文件。

2.2 IO复用

通过使用select函数监听描述符来使用IO复用技术,首先通过监听服务器的socket描述符来监听listen是否存在用户请求连接,若存在用户请求连接,则将连接对应的描述符s加入到连接集合client_sockets中并将其加入监听请求服务中监听集合中;

当监听请求服务的监听集合监听到存在描述符请求服务,则服务器将相应的描述符加入到任务队列中,并设置其状态为就绪状态,等待线程为其提供服务。

2.3 线程池

由于用户连接大部分时间处于阻塞状态,故如果使用一个线程为一个用户连接提供服务的话,同样也会浪费许多资源。故本次采用类似线程池的方式来为用户提供服务(类似一个简单的线程池模型)。

在主线程main开始时,直接创建MAX_THREADS个线程thread_worker来监听任务队列work_queue中任务,若任务队列work_queue中存在准备就绪状态的任务,则线程将其取出进行提供服务(将任务队列中相应描述的状态修改为处理中的状态),服务提供完毕则将相应的任务从任务队列中清除。

2.4 注意事项

由于TCP连接接收和发送缓冲区默认大小的限制,当我们传输大文件的时候,若服务端不能够及时处理客户端发送过来的数据包,那么就会出现数据包被丢弃的现象,导致服务端无法得到完整的文件。

采用的解决方法是:修改TCP连接的接收和发送的缓冲区大小(此处需要对应的修改linux系统的tcp连接的接收和发送缓冲区大小)

linux命令行运行以下指令

sudo su
echo 52428800 > /proc/sys/net/core/rmem_max

3、源代码

3.1 ptServer.cpp
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<unistd.h>
#include<fcntl.h>
#include<dirent.h>
#include<string>
#include<vector>
#include<mutex>
#include<thread>
#include<algorithm>

using namespace std;

#define port 8888
#define backlog 5
#define MAXBUFF 4096
#define MAXNAMELEN 100
#define MAX_CLIENTS 1000    //最大客户端连接数
#define MAX_THREADS 10      //最大开辟线程数

void process_con_server(int s);                 //服务器处理与客户端的连接服务
void analyzeCommand(char *command, int s);      //解析客户端指令
void sendMessage(int s);                        //向客户端发送可下载文件信息
char** splitString(char* str, int& num);        //以一个或多个空格分割字符串
void sendFile(int s, char *fname);              //服务器向客户端发送文件,即客户端下载文件
void thread_worker(int thread_id);              //线程工作函数
void closeSock(int s);                          //关闭某个客户端的连接

/*文件信息*/
typedef struct FileMess{
    unsigned long fileLen;
    char fileName[100];
} FileMess;

/*数据包*/
typedef struct DataPack{
    int cmtype;     //什么指令的数据包, '1'表示ls指令, '2'表示recvfile下载文件指令, '3'表示sendfile上传文件指令
    char type;     //'D'表示数据,'M'表示文件信息, 'E'表示错误数据包
    int packSize;   //整个数据包的大小
    char content[MAXBUFF];  //数据包携带数据缓冲区
    int contentLen;     //数据包中数据的长度
    unsigned long position;    //数据在文件中的字节位置
    FileMess fileMess;      //文件信息
} DataPack;

typedef struct sock_statu{
    int sock;   //tcp连接描述符
    int flag;   //'1'表示准备就绪;'0'表示处理中.
} sock_statu;


void recvFile(int s, DataPack* dataPack);       //服务器接收文件,即客户端上传文件

/*目录信息数据包*/
typedef struct DirPack{
    int flag;      //标志位,1表示包含目录文件名,0表示结束目录发送的空数据包
    char content[100];
} DirPack;


int client_sockets[MAX_CLIENTS] = {0};  // 用户连接描述符存储数组,连接数组
vector<sock_statu> work_queue;          // 用户请求服务描述符数组,任务队列 
mutex mtx;

int main(int argc, char *argv[]){
    int ss, sc;
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;
    int err;
    pid_t pid;
    
    /*创建套接字socket*/
    ss = socket(AF_INET, SOCK_STREAM, 0);
    if(ss < 0){
        printf("Create Socket error!!!\n");
        return -1;
    }
    
    //设置tcp连接缓冲区的大小, 注意:这里还需要设置linux系统中支持的缓冲区大小
    int buffer_size = 50 * 1024 * 1024; // 缓冲区大小为50MB
    setsockopt(ss, SOL_SOCKET, SO_RCVBUF, &buffer_size, sizeof(buffer_size)); // 设置接收缓冲区大小
    setsockopt(ss, SOL_SOCKET, SO_SNDBUF, &buffer_size, sizeof(buffer_size)); // 设置发送缓冲区大小

    /*设置服务器基本信息*/
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(port);

    /*绑定服务器地址到套接字socket*/
    err = bind(ss, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if(err < 0){
        printf("Bind server address error!!!\n");
        return -1;
    }
     
    /*启动监听*/
    err = listen(ss, backlog);  //设置最大排队数量为backlog
    if(err < 0){
        printf("Start listenning failure!!!\n");
        return -1;
    }
    
    fd_set set, wd_set, rd_set;
    FD_ZERO(&set);
    FD_ZERO(&wd_set);
    FD_ZERO(&rd_set);
    FD_SET(ss, &set);

    fd_set fd_cli;
    struct timeval tv;
    FD_ZERO(&fd_cli);
    tv.tv_sec = 1;
    tv.tv_usec = 0;

    int maxFd = 0;

    //开了MAX_THREADS个线程同时监听客户端连接是否有请求服务数据包
    vector<thread> threads;
    for (int i = 0; i < MAX_THREADS; i++) {
        threads.emplace_back(thread_worker, i);
    }

    /*监听服务器socket描述符是否有客户端请求连接*/
    while(1){
        rd_set = set;
        wd_set = set;
        err = select(ss+1, &rd_set, &wd_set, NULL, &tv);
        if(err < 0){
            printf("Select function execute failing...");
            return -1;
        }

        //若存在客户端请求连接
        if(FD_ISSET(ss, &rd_set)){
            socklen_t addrlen = sizeof(struct sockaddr);
            sc = accept(ss, (struct sockaddr*)&client_addr, &addrlen);
            FD_SET(sc, &fd_cli);
            maxFd = (sc > maxFd) ? sc : maxFd;
            {   //线程中需要对客户端关闭连接后进行客户端连接清除操作,所以要对共享资源client_sockets加上互斥锁
                unique_lock<mutex> lock(mtx);
                for (int i = 0; i < MAX_CLIENTS; i++) {
                    if (client_sockets[i] == 0) {
                        client_sockets[i] = sc;
                        printf("客户端socket:%d加入连接队列\n", sc);
                        break;
                    }
                }
                lock.unlock();
            }
        }   

        rd_set = fd_cli;
        wd_set = fd_cli;
        //检查是否有客户端发送服务请求
        err = select(maxFd+1, &rd_set, &wd_set, NULL, &tv);
        for(int i=0; i<MAX_CLIENTS; ++i){
            int jump = 0;
            //判断任务队列中是否已经包含对应的套接字,有则不在将套接字加入任务队列中
            for(int j=0; j<work_queue.size(); ++j){
                    if(work_queue[j].sock == client_sockets[i]){
                        jump = 1;
                        break;
                    }
            }
            if(client_sockets[i] == 0 || !FD_ISSET(client_sockets[i], &rd_set) || jump){
                continue;
            }

            // 将有服务请求的用户连接的描述符加入任务队列中
            sock_statu st;
            st.sock = client_sockets[i];
            st.flag = 1;
            {   //加上互斥锁
                unique_lock<mutex> lock(mtx);
                printf("客户端socket:%d加入任务队列\n", client_sockets[i]);
                work_queue.push_back(st);   //将该描述符加入任务队列的末尾
                lock.unlock();
            }
        }

    }
    //等待线程关闭
    for (auto& t : threads) { 
        t.join(); 
    }
    return 0;
}

//工作线程
void thread_worker(int thread_id) {
    //考虑一种情况:当该线程正在处理某一个客户端连接描述符的时候,客户端又发来数据,恰好被FD_ISSET监听到
    //又加入到vector<sock_stute>容器中了。该考虑此时其他队列拿到这个描述符后,应该如何处理?
    //此处解决方案是,对每个描述符加入一个flag标志,'1'表示准备就绪;'0'表示处理中。

    while (true) {
        int client_socket = 0;
        sock_statu st;
        st.sock = 0;
        st.flag = 0;
        {   
            unique_lock<mutex> lock(mtx);
            //若任务队列为空,则循环等待,直接任务队列不为空
            while (work_queue.empty()) {
                lock.unlock();
                sleep(1);   //每隔1秒查看一次任务队列是否为空
                lock.lock();
            }
            //知道取出一个准备就绪状态的有服务请求的描述符
            while(1){
                for (int i=0; i<work_queue.size(); ++i) {
                    if(work_queue[i].flag==1 && work_queue[i].sock!=0){
                        client_socket = work_queue[i].sock;
                        work_queue[i].flag = 0;
                        break;
                    }
                }
                //若无准备就绪状态的描述符,则继续等待
                if(client_socket == 0){
                    lock.unlock();
                    sleep(1);
                    lock.lock();
                }
                else {
                    break;
                }
            }
            lock.unlock();
        }
        printf("线程%d:客户端socket:%d正在处理连接\n", thread_id, client_socket);
        process_con_server(client_socket);  //处理任务

        printf("线程%d:客户端socket:%d连接处理完成\n", thread_id, client_socket);
        //删除任务队列中中已经处理完成的描述符的元素
        {
            unique_lock<mutex> lock(mtx);
            for (int i = 0; i < work_queue.size(); ++i ) {
                if(work_queue[i].sock == client_socket && work_queue[i].flag == 0){
                    work_queue.erase(work_queue.begin() + i);
                    break;
                }
            }
            lock.unlock();
        }
    }
}

/*服务器处理与客户端的连接服务*/
void process_con_server(int s){
    ssize_t size;
    DataPack *dataPack = (DataPack *)malloc(sizeof(DataPack));
    memset(dataPack, 0, sizeof(DataPack));
    //将recv函数设置为非阻塞式接收缓冲区数据
    int flags = fcntl(s, F_GETFL, 0);
    fcntl(s, F_SETFL, flags | O_NONBLOCK);

    size = recv(s, dataPack, sizeof(DataPack), 0);

    if(size == 0){  //表示客户端已关闭连接, 服务端需要关闭socket并从socket数组中清空socket
        printf("客户端socket:%d 已断开连接......\n", s);
        closeSock(s);
        return;
    }
    //持续处理来自该描述符的任务,直至该客户端的任务处理完毕
    while(size > 0){
        printf("dataPack->cmtype:%d\n", dataPack->cmtype);
        printf("dataPack->type:%c\n", dataPack->type);
        printf("dataPack->contentlen:%d\n", dataPack->contentLen);
        //根据标识符cmtype的类型判断该数据包的类型,并作出进一步处理
        if(dataPack->cmtype == 1){
            sendMessage(s);
        }
        else if(dataPack->cmtype == 2){
            printf("download命令正在处理%s\n", dataPack->fileMess.fileName);
            sendFile(s, dataPack->fileMess.fileName);
        }
        else if(dataPack->cmtype == 3){
            recvFile(s, dataPack);
        }
        //取出下一个任务,若返回值为-1则代表任务处理完毕
        size = recv(s, dataPack, sizeof(DataPack), 0);
    }
}

/*关闭某个客户端连接*/
void closeSock(int s){
    close(s);
    {   // 加锁清空客户端连接队列中相应的socket
        unique_lock<mutex> lock(mtx);
        for (int i = 0; i < MAX_CLIENTS - 1; ++i) {
            if(s == client_sockets[i]){
                client_sockets[i] = 0;
                break;
            }
        }
        lock.unlock();
    }
}

/*服务器向客户端发送文件,即客户端下载文件*/
void sendFile(int s, char *fname){  //fname含有'\n'回车
    DataPack dataPack;
    char path[100] = "./resources/";
    //拼接文件名与资源路径得到文件路径
    strncat(path, fname, strlen(fname));
    printf("%s\n", path);
    //判断用户获取的资源文件是否存在
    int st = access(path, F_OK);
    if(-1 == st){
        /*设置错误信息,并以错误信息类型发送数据包*/
        dataPack.type = 'E';
        dataPack.packSize = sizeof(DataPack);
        char *buffer = "The file isn't exist!!!\n";
        strncpy(dataPack.content, buffer, strlen(buffer));
        dataPack.contentLen = strlen(buffer);
        dataPack.position = 0;
        send(s, &dataPack, dataPack.packSize, 0);
        return;
    }
    printf("打开并获取文件信息!\n");

    /*获取并发送文件信息*/
    struct stat statbuf;
    stat(path, &statbuf);
    dataPack.type = 'M';
    dataPack.packSize = sizeof(DataPack);
    dataPack.fileMess.fileLen = statbuf.st_size;
    strncpy(dataPack.fileMess.fileName, fname, strlen(fname));
    unsigned long sRe = send(s, &dataPack, dataPack.packSize, 0);
    printf("成功发送文件信息数据包!\n");
    

    /*发送文件内容*/
    unsigned long sendedCount = 0;      //记录已发送的数据大小
    int fd = open(path, O_RDONLY);     //打开文件
    struct flock lock;
    lock.l_type = F_RDLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start = 0;
    lock.l_len = 0;
    fcntl(fd, F_SETLKW, &lock);
    while(sendedCount < statbuf.st_size){
        //构造文件内容数据包
        DataPack filedata;
        memset(&filedata, 0, sizeof(DataPack));
        unsigned long readBytes = read(fd, filedata.content, MAXBUFF);
        filedata.contentLen = readBytes;
        filedata.type = 'D';
        filedata.packSize = sizeof(DataPack);
        filedata.position = sendedCount;

        unsigned long sDa = send(s, &filedata, filedata.packSize, 0);
        if(sDa > 0){
            sendedCount += filedata.contentLen;
        }
        printf("成功发送数据:%ld bytes\n", sendedCount);
    }

    //构造结束标志数据包,标志文件传输完毕
    memset(&dataPack, 0, sizeof(DataPack));
    dataPack.type = 'E';
    send(s, &dataPack, sizeof(DataPack), 0);
    lock.l_type = F_UNLCK;
    fcntl(fd, F_SETLKW, &lock);
    close(fd);
    return;        
}

/*服务器接收文件,即客户端上传文件*/
void recvFile(int s, DataPack* dataPack){
    printf("type:%c\n", dataPack->type);
    long fileSize = 0;     //记录接收文件的大小
    struct flock lck;
    lck.l_type = F_WRLCK;
    lck.l_whence = SEEK_SET;
    lck.l_start = 0;
    lck.l_len = 0;
    //若该数据包为指令,则创建空文件等待接收数据
    if(dataPack->type == 'C'){
        char path[200] = "./upload/";
        strcat(path, dataPack->fileMess.fileName);
        //无论什么时候都创新创建文件
        int fd = open(path, O_CREAT | O_TRUNC | O_RDWR, S_IRWXU);
        fileSize = dataPack->fileMess.fileLen;
        //对文件加写锁
        int ret = fcntl(fd, F_SETLK, &lck);
        if (ret == -1) {
            printf("Lock failed.\n");
            return ;
        }

        void *nullChar = malloc(4096);
        memset(nullChar, 0, 4096);

        //向文件中写入文件大小的空数据
        while(fileSize > 0){
            if(fileSize - 4096 >= 0){
                ssize_t t = write(fd, nullChar, 4096);
                fileSize = fileSize - 4096;
            }
            else{
                ssize_t t = write(fd, nullChar, fileSize);
                fileSize = 0;
            }
        }
        lck.l_type = F_UNLCK;
        fcntl(fd, F_SETLK, &lck);
        close(fd);
    }
    //若该数据包为数据,则打开相应的文件写入数据
    else if(dataPack->type == 'D'){
        char path[200] = "./upload/";
        strcat(path, dataPack->fileMess.fileName);
        printf("开始写入数据!\n");
        if(!strcmp(path, "./upload/")){
            return;
        }
        int fd = open(path, O_WRONLY);
        while (fd == -1){
            printf("文件打开失败, 尝试重新打开文件%s\n", path);
            fd = open(path, O_WRONLY);
        }
        //对文件加写锁
        fcntl(fd, F_SETLK, &lck);
        lseek(fd, dataPack->position, SEEK_SET);
        write(fd, dataPack->content, dataPack->contentLen);
        printf("向%s文件写入%d字节内容\n", dataPack->fileMess.fileName, dataPack->contentLen);
        lck.l_type = F_UNLCK;
        fcntl(fd, F_SETLK, &lck);
        close(fd);
        return ;
    }
}

/*向客户端发送可下载文件信息*/
void sendMessage(int s){
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    DIR *dirp;
    struct dirent *direntp;
    //目录项目打开锁,防止读取目录时其他线程对目录的修改
    pthread_mutex_lock(&mutex);
    int count = 0;
    DirPack *dirpack = (DirPack *)malloc(sizeof(DirPack));
    memset(dirpack, 0, sizeof(DirPack));
    dirpack->flag = 1;
    // 打开当前目录
    dirp = opendir("./resources/");
    // 读取目录内容
    while ((direntp = readdir(dirp)) != NULL) {
        // 忽略当前目录和父目录
        if (strcmp(direntp->d_name, ".") == 0 || strcmp(direntp->d_name, "..") == 0) {
            continue;
        }

        // 将文件名发送到客户端中
        char *dirfName = strdup(direntp->d_name);   //获取文件名
        strncpy(dirpack->content, dirfName, strlen(dirfName));
        send(s, dirpack, sizeof(DirPack), 0);
        // 清空数据包的内容
        memset(dirpack->content, 0, sizeof(dirpack->content));
    }

    //构造结束数据包
    dirpack->flag = 0;
    send(s, dirpack, sizeof(DirPack), 0);

    // 关闭目录
    closedir(dirp);
    pthread_mutex_unlock(&mutex);
}
3.2 ptClient.cpp
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<unistd.h>
#include<arpa/inet.h>

#define port 8888
#define MAXBUFF 4096

void process_con_client(int s);             //处理与服务器的连接服务
char** splitString(char* str, int& num);    //以一个或多个空格分割字符串
void analyzeCommand(char *command, int s);  //解析用户输入的命令    
void recvFile(int s, char *fname);          //接收来自服务器的文件数据包
void sendFile(int s, char *fname);          //向服务器上传文件
void recvMessage(int s);                    //接收可下载文件目录信息

/*文件信息*/
typedef struct FileMess{
    unsigned long fileLen;
    char fileName[100];
} FileMess;

/*文件数据包*/
typedef struct DataPack{
    int cmtype;     //什么指令的数据包, '1'表示ls指令, '2'表示recvfile下载文件指令, '3'表示sendfile上传文件指令
    char type;     //'C'表示指令, 'D'表示数据,'M'表示文件信息, 'E'表示错误数据包
    int packSize;   //整个数据包的大小
    char content[MAXBUFF];  //数据包携带数据缓冲区
    int contentLen;     //数据包中数据的长度
    unsigned long position;    //数据在文件中的字节位置
    FileMess fileMess;      //文件信息
} DataPack;

/*目录信息数据包*/
typedef struct DirPack{
    int flag;      //标志位,1表示包含目录文件名,0表示结束目录发送的空数据包
    char content[100];
} DirPack;

int main(int argc, char *argv[]){
    int s;
    struct sockaddr_in server_addr;
    int err;

    /*创建套接字socket*/
    s = socket(AF_INET, SOCK_STREAM, 0);
    if(s < 0){
        printf("Create socket error!!!\n");
        return -1;
    }

    /*设置连接服务器IP地址和端口号*/
    memset(&server_addr, 0, sizeof(struct sockaddr_in));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    inet_pton(AF_INET, argv[1], &server_addr.sin_addr);

    /*连接服务器*/
    err = connect(s, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
    if(err < 0){
        printf("Connect error!!!\n");
        return -1;
    }
    process_con_client(s);  //请求服务
    close(s);       //关闭连接
    return 0;
}

/*处理客户端与服务器的连接*/
void process_con_client(int s){
    ssize_t size = 0;
    char buffer[1024];

    //反复处理用户输入的命令
    while(1){
        memset(buffer, 0, sizeof(buffer));
        size = read(STDIN_FILENO, buffer, 1024);
        if(size > 0){
            // write(s, buffer, size);      //此处不再直接发送指令本身
            analyzeCommand(buffer, s);
        }
    }
    close(s);
}


/*以一个或多个空格分割字符串*/
/*返回值:指针数组*/
/*num:传出参数,字符串分割成的子串个数*/
char** splitString(char* str, int& num) {
    int len = strlen(str);
    char** result = new char*[len];

    int count = 0;
    int start = 0;
    /*遍历分割字符串*/
    for (int i = 0; i < len; ++i) {
        if (str[i] == ' ') {
            if (i - start > 0) {
                int size = i - start;
                result[count] = new char[size + 1];     //size+1是为了尾部添加'\0'作为截至
                strncpy(result[count], &str[start], size);
                result[count][size] = '\0';
                ++count;
            }
            start = i + 1;
        }
    }

    /*若字符串不以空格结尾,则需要将字符串尾部添加*/
    if (len - start > 0) {
        int size = len - start;
        result[count] = new char[size + 1];
        strncpy(result[count], &str[start], size);
        result[count][size] = '\0';
        ++count;
    }

    num = count;
    return result;
}

/*解析客户端用户输入的指令, 并向客户端发送相应的指令数据包*/
void analyzeCommand(char *command, int s){
    int substrNum;
    char** substr = splitString(command, substrNum);

    /*判断用户输入的指令是否符合格式*/
    /*此处仅提供三种指令,ls、download [filename]、send [filename]*/
    if(substrNum > 2){
        char *buffer = "The format of command is error!!!\n";
        write(STDOUT_FILENO, buffer, strlen(buffer));
    }

    DataPack dataPack;
    memset(&dataPack, 0, sizeof(DataPack));

    /*显示可下载的文件列表, 接收来自服务器传输过来的信息*/
    if(!strcmp(substr[0], "ls\n")){
        dataPack.cmtype = 1;
        dataPack.type = 'C';
        send(s, &dataPack, sizeof(dataPack), 0);
        recvMessage(s);
    }
    /*下载指定文件*/
    if(!strcmp(substr[0], "download")){
        //去除文件名末尾的回车
        char *fName = (char*)malloc(strlen(substr[1])-1);   
        strncpy(fName, substr[1], strlen(substr[1])-1);

        dataPack.cmtype = 2;
        dataPack.type = 'C';
        strncpy(dataPack.fileMess.fileName, fName, strlen(fName));
        send(s, &dataPack, sizeof(dataPack), 0);
        recvFile(s, fName);
    }
    /*上传指定的文件到服务器*/
    if(!strcmp(substr[0], "send")){
        //去除文件名末尾的回车
        char *fName = (char*)malloc(strlen(substr[1])-1);   
        strncpy(fName, substr[1], strlen(substr[1])-1);
        //判断是否存在相应名称的文件
        int st = access(fName, F_OK);
        if(-1 == st){
            char *buffer = "The file isn't exist!!!\n";
            write(STDOUT_FILENO, buffer, strlen(buffer));
            return ;
        }
        //首先发送一个空内容文件信息数据包
        struct stat statbuf;
        stat(fName, &statbuf);
        dataPack.cmtype = 3;
        dataPack.type = 'C';    //'C'表示指令数据包
        dataPack.fileMess.fileLen = statbuf.st_size;
        strncpy(dataPack.fileMess.fileName, fName, strlen(fName));
        send(s, &dataPack, sizeof(dataPack), 0);
        //调用文件内容发送函数
        sendFile(s, fName);
    }
}

/*执行文件下载命令,接收服务器发来的数据包*/
void recvFile(int s, char *fname){
    
    unsigned long fileSize = 0;     //记录接收文件的大小
    unsigned long recvedCount = 0;  //记录已接收的数据量大小
    int fd = 0;
    DataPack *dataPack = (DataPack *)malloc(sizeof(DataPack));  //动态分配内存
    while(1){
        memset(dataPack, 0, sizeof(DataPack));
        unsigned long recvBytes = recv(s, dataPack, sizeof(DataPack), 0);

        //'E'类型的数据包为错误数据包
        //在文件传输完成后,会发送一个空内容(content)的错误数据包,表示文件传输完成
        //也可以选择重新定义一个新的类型的数据包作为结束数据包类型
        if(dataPack->type == 'E'){
            write(STDOUT_FILENO, dataPack->content, dataPack->contentLen);
            break;
        }
        
        //'M'类型的数据包为文件信息数据包,根据相应的信息创建文件
        else if(dataPack->type == 'M'){
            char path[200] = "./download/";   
            strncat(path, fname, strlen(fname));
            //无论什么时候都创新创建文件
            fd = open(path, O_CREAT | O_TRUNC | O_RDWR, S_IRWXU);
            fileSize = dataPack->fileMess.fileLen;
            
        }
        
        //'D'类型数据包为文件内容数据包,解析数据包内容,并写入相应文件的相应位置
        else if(dataPack->type = 'D'){
            lseek(fd, recvedCount, SEEK_SET);
            write(fd, dataPack->content, dataPack->contentLen);
            recvedCount += dataPack->contentLen;
       }
    }
    if (fd != 0){
        close(fd);      //关闭文件描述符
    }
}

/*接收服务器发来的可下载文件目录数据包*/
void recvMessage(int s){
    DirPack *dirpack = (DirPack *)malloc(sizeof(DirPack));      //动态分配内存
    memset(dirpack, 0, sizeof(DirPack));
    int count = 0;
    while(1){
        recv(s, dirpack, sizeof(DirPack), 0);
        //接收完毕目录后会发现一个flag标志为0的数据包,表示目录数据包发送完毕
        if(dirpack->flag == 0){
            memset(dirpack, 0, sizeof(DirPack));    
            break;
        }
        write(STDOUT_FILENO, dirpack->content, sizeof(dirpack->content));

        ++count;
        //排布显示
        if(count%4 == 0){
            write(STDOUT_FILENO, "\n", 1);
        }
        else {
            write(STDOUT_FILENO, "\t", 1);
        }
        memset(dirpack, 0, sizeof(DirPack));
    }
    if(count%4 != 0){
        write(STDOUT_FILENO, "\n", 1);
    }
    free(dirpack);
}

/*执行上传文件命令,向服务器发送文件信息和内容数据包*/
void sendFile(int s, char *fname){  //fname含有'\n'回车
    DataPack dataPack;
    memset(&dataPack, 0, sizeof(DataPack));
        
    /*获取并发送文件信息*/
    struct stat statbuf;
    stat(fname, &statbuf);
    dataPack.cmtype = 3;
    dataPack.type = 'D';
    dataPack.packSize = sizeof(DataPack);
    dataPack.fileMess.fileLen = statbuf.st_size;
    strncpy(dataPack.fileMess.fileName, fname, strlen(fname));

    /*发送文件内容*/
    unsigned long sendedCount = 0;      //记录已发送的数据大小
    int fd = open(fname, O_RDONLY);     //打开文件
    //当发送的数据量小于文件大小时则继续发送数据
    while(sendedCount < statbuf.st_size){
        memset(&dataPack.content, 0, sizeof(dataPack.content));
        unsigned long readBytes = read(fd, dataPack.content, MAXBUFF);
        while(readBytes == 0){
            printf("读取文件数据失败,尝试重新读取数据");
            readBytes = read(fd, dataPack.content, MAXBUFF);
        }
        dataPack.contentLen = readBytes;
        dataPack.position = sendedCount;    

        unsigned long sDa = send(s, &dataPack, dataPack.packSize, 0);
        while(sDa < 0){
            sDa = send(s, &dataPack, dataPack.packSize, 0);
            printf("文件%s偏移量%ld后的%ld字节数据发送失败,尝试重新发送", fname, sendedCount, readBytes);
        }
        // usleep(10000);
        sendedCount += dataPack.contentLen;
        printf("成功发送数据:%ld bytes\n", sendedCount);
        
    }
    close(fd);
    return;        
}

4、测试效果

4.1 g++编译源代码
  • ptServer.cpp
g++ ptServer.cpp -o ptServer -lpthread

image-20230329204343997

  • ptClient.cpp
g++ ptClient.cpp -o ptClient

image-20230329204254089

4.2 Server端IP地址

image-20230329204539621

4.3 多端接收和发送数据包
  • 多端连接服务器

在这里插入图片描述

多个客户端同时连接服务器,将不同客户端连接的描述符s加入连接集合client_sockets中。

  • 多端请求服务

image-20230331165308259

当不同的用户分别请求服务器的服务,客户端会将有服务请求的客户端连接描述符s加入任务队列work_queue中,由一开始的MAX_THREADS个线程取出为其提供服务。

当用户请求上传文件的服务时,由于用户是将一个文件分成多个数据包dataPack分别上传服务器,由于可能无法实现多个线程同时对一个连接描述符进行接收数据包,此处采用的处理是线程接收数据包的过程中,若客户端又发送了另一个数据包,则该线程继续为该客户端提供服务,直至客户端没有连续的数据包发送后,则将该客户端连接描述符从任务队列work_queue中清除。由于每个数据包均记录了数据包的内容所对应的文件,已经对应的内容从文件起始位置的偏移,所以即使由于时延导致数据包不能连续发送,仍不会破坏文件的上传。

当用户请求下载文件的服务时,则处理相对简单。根据数据包中相应的信息,打开相应的文件,读完文件内容构造数据包发送到客户端,客户端读取数据包后,将文件写入相应的文件即可。

  • 客户端并行请求服务

image-20230331173211833

image-20230331173513120

当两个客户端同时上传文件时,第一个客户端正在上传文件的过程中,第二客户端请求上传文件,则主线程会将其加入任务队列,并由另一个线程为第二客户端的文件上传进行服务,实现并发多线程服务。

pthread_mutex_lock和unique_lock的比较

pthread_mutex_lock和unique_lock都是用于多线程编程中的加锁方式。

pthread_mutex_lock

对于简单的多线程编程场景,使用pthread_mutex_lock是比较简单和直接的选择。

pthread_mutex_lock也是一种简单、直接、易于理解和使用的加锁方式,可以满足基本的线程同步需求,并且在性能上也比较优秀。

如果应用场景比较简单,或者需要和其他语言或库进行兼容,那么pthread_mutex_lock可能更加适合。

pthread_mutex_lock ALOCK = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock ALOCK = PTHREAD_MUTEX_INITIALIZER;是一种在定义互斥量时直接初始化的方式。这种方式可以在定义变量的同时,初始化变量的值,避免了在使用前需要显式地调用pthread_mutex_init函数进行初始化的麻烦,简化了代码的编写和阅读。

unique_lock

unique_lock是C++11标准库中提供的一种更加灵活的锁类型。

unique_lock提供了一种更加灵活的加锁方式,可以在构造函数中指定是否立即加锁,也可以在之后的代码中动态地加锁和解锁,这种灵活性可以更好地控制锁的粒度,提高代码的执行效率。

  1. unique_lock是可移动的,可以通过移动语义将unique_lock对象从一个线程传递到另一个线程,这种可移动性方便了线程间的数据传递和同步。
  2. unique_lock允许在同一个线程中多次加锁和解锁同一个互斥量,这种可重入性比pthread_mutex_lock更加安全,避免了死锁问题。
  3. unique_lock在执行加锁和解锁操作时,使用了RAII技术,将加锁和解锁操作封装在构造函数和析构函数中,可以更好地保证锁的正确性,同时也避免了忘记解锁的问题,在性能上也比较优秀。

如果应用场景比较复杂,或者需要更好的灵活性可移动性可重入性,那么unique_lock可能更加适合。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值