# myCloud **Repository Path**: liu-siyuan888/my-cloud ## Basic Information - **Project Name**: myCloud - **Description**: 私有协议文件管理服务器 - **Primary Language**: C - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-08-26 - **Last Updated**: 2024-03-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 私有协议文件服务器 ## 初代版本 **服务端:** + 与多个客户端建立 **TCP 长连接** + 使用**线程池**来完成客户端的请求,主线程只负责监听,子线程负责 I/O + 使用自定义的**小火车协议**传输文件(使用文件映射)、具有差错检验和断点续传功能 + 无注册功能,使用 linux **系统用户和密码**登录,验证用户名和密码 + 在 **系统日志** 中记录客户端的连接信息、操作记录、操作时间 **客户端:** + 目录命令:cd、ls、pwd、mkdir、rmdir + 文件命令:puts 将本地文件上传至服务器、 gets 文件名 下载服务器文件到本地、remove 删除服务器上的文件 + 非法命令会提示 ### 1 基础框架 #### 1.1 线程池 image-20230901203103910 ```c /* 所有共享资源 */ typedef struct threadPool_s { int workerNum; // 线程数量 pthread_t *workerArr; // 线程信息 taskQueue_t taskQueue; // 任务队列 int epollFd; // epollFd connetionBuffer_t fdBuf[128]; // 服务端缓存 pthread_mutex_t mutex; // 锁 pthread_cond_t cond; // 条件变量 } threadPool_t; ``` **注意!在主线程唤醒子线程前要先关闭 netfd 的监听,并在子线程工作结束后重新监听** ```c pthread_mutex_lock(&threadPool.mutex); enQueue(&threadPool.taskQueue, eventArr[i].data.fd); /* 注意! 必须先移除监听再唤醒线程, 然后在线程做完任务后添加监听 */ /* 否则多个线程会接到同一个任务 */ epollDelete(epollFd, eventArr[i].data.fd); pthread_cond_signal(&threadPool.cond); pthread_mutex_unlock(&threadPool.mutex); ``` #### 1.2 小火车协议 + 先发文件名,再发文件长度,最后接收文件 + 解决粘包问题 ```c int recvFileFromClient(int fd, threadPool_t *pThreadPool) { /* 文件名 */ train_t train; recvn(fd,&train.length,sizeof(train.length)); recvn(fd,train.data,train.length); char filename[4096] = {0}; changePath(&pThreadPool->fdBuf[0].netPath, pThreadPool->fdBuf[0].usr, train.data, filename); /* 文件长度 */ int filefd = open(filename,O_RDWR|O_CREAT,0666); off_t filesize; recvn(fd,&train.length,sizeof(train.length)); recvn(fd,train.data,train.length); memcpy(&filesize,train.data,sizeof(filesize)); ftruncate(filefd,filesize); /* 文件 */ char *p = (char *)mmap(NULL,filesize,PROT_READ|PROT_WRITE,MAP_SHARED,filefd,0); recvn(fd,p,filesize); munmap(p,filesize); close(filefd); return 0; } ``` ### 2 登录及密码验证 + 注意,从系统中获取用户密码信息需要更高的权限,客户端和服务端都要加 sudo ![image-20230901210449481](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309012104505.png) ![image-20230901210522533](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309012105566.png) + 盐值提取 ```c void get_salt(char *salt,char *passwd) { int i, j; for (i = 0, j = 0; passwd[i] && j != 3; i++) { if (passwd[i] == '$') { ++j; } } strncpy(salt, passwd, i - 1); } ``` + 从系统中获取用户信息并验证密码是否正确 ```c char userName[128] = {0}; ssize_t recvRet = recvMessageFromClient(netFd, userName, sizeof(userName)); if (!recvRet) { close(netFd); break; } printf("userName = %s\n", userName); struct spwd* sp = getspnam(userName); printf("systemPasswd = %s\n", sp->sp_pwdp); char salt[512] = {0}; if (!sp) { sendMessageToClient(netFd, "0", 1); printf("用户不存在\n"); continue; } else { get_salt(salt, sp->sp_pwdp); sendMessageToClient(netFd, salt, strlen(salt)); char saltPasswd[1024] = {0}; size_t recvRet = recvMessageFromClient(netFd, saltPasswd, sizeof(saltPasswd)); printf("clientPasswd = %s\n", saltPasswd); if (!recvRet) { close(netFd); break; } if (strcmp(sp->sp_pwdp, saltPasswd) == 0) { send(netFd, "1", 1, 0); printf("密码正确\n\n"); epollAdd(epollFd, netFd); break; } else { send(netFd, "0", 1, 0); printf("密码错误\n\n"); } } ``` ### 3 cd 命令 ![image-20230901220951991](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309012209001.png) + 使用栈结构保存当前路径 ![image-20230901220131783](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309012201812.png) + 思路:先取出当前路径,然后移动指针修改当前路径 ```c /* 当前路径栈 */ typedef struct pathStack_s{ char path[128][128]; int top; }pathStack_t; /* 一次连接的缓存 */ typedef struct connetionBuffer_s{ pathStack_t netPath; // 当前路径栈 char usr[128]; // 用户根目录路径 }connetionBuffer_t; ``` ### 4 ls 命令 ![image-20230901210845442](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309012108448.png) + 方法:遍历目录流 ```c int listSearch(const char* realPath,char* lsReturn) { DIR* pdir = opendir(realPath); ERROR_CHECK(pdir,NULL,"opendir"); struct dirent* pdirent; while((pdirent=readdir(pdir))){ if (strcmp(pdirent->d_name, ".") && strcmp(pdirent->d_name, "..")) { strcat(lsReturn,pdirent->d_name); if(pdirent->d_type == DT_DIR) { strcat(lsReturn,"/"); } strcat(lsReturn," "); } } strcat(lsReturn," "); return 0; } ``` ### 5 pwd 命令 + 每次 cd 会修改缓存中的当前路径,直接取出即可 ![image-20230901214632576](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309012146592.png) ```c if (strcmp(cmd, "pwd") == 0) { char buffer[128] = {0}; netdeskpwd(&pThreadPool->fdBuf[0].netPath, buffer); sendMessageToClient(netFd, buffer, strlen(buffer)); } ``` ### 6 mkdir 命令 和 rmdir 命令 ![image-20230901212439200](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309012124220.png) + mkdir:直接 mkdir 系统调用 + rmdir:直接 rmdir 系统调用 ```c if (strcmp(cmd, "mkdir") == 0) { memset(buf, 0, sizeof(buf)); changePath(&pThreadPool->fdBuf[0].netPath, pThreadPool->fdBuf[0].usr, path1, buf); int ret = mkdir(buf, 0777); if (ret == -1) { char buffer[1024] = {0}; sprintf(buffer, "mkdir: cannot create directory '%s'", path1); sendMessageToClient(netFd, buffer, strlen(buffer)); } else { sendMessageToClient(netFd, "mkdir finished", 14); } } else if (strcmp(cmd, "rmdir") == 0) { memset(buf, 0, sizeof(buf)); changePath(&pThreadPool->fdBuf[0].netPath, pThreadPool->fdBuf[0].usr, path1, buf); int ret = rmdir(buf); ERROR_CHECK(ret, -1, "remove"); if (ret == -1) { char buffer[1024] = {0}; sprintf(buffer, "rmdir: cannot remove directory '%s'", path1); sendMessageToClient(netFd, buffer, strlen(buffer)); } else { sendMessageToClient(netFd, "rmdir finished", 14); } } ``` ### 7 remove 命令 ![image-20230901214336283](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309012143317.png) + 系统调用 unlink ```c if (strcmp(cmd, "remove") == 0) { memset(buf, 0, sizeof(buf)); changePath(&pThreadPool->fdBuf[0].netPath, pThreadPool->fdBuf[0].usr, path1, buf); int ret = unlink(buf); if (ret == -1) { char buffer[1024] = {0}; sprintf(buffer, "remove: cannot remove '%s': No such file or directory", path1); sendMessageToClient(netFd, buffer, strlen(buffer)); } else { sendMessageToClient(netFd, "remove filished", 15); } } ``` ### 8 puts 命令 + 基于小火车协议 + 需要传递 md5 值 + 先发文件名,再发 md5 值,然后发文件长度,最后发文件 ```c int sendFileToServer(int fd, char *localPath, char *virtualPath) { /* 发文件名 */ train_t train; train.length = strlen(virtualPath); memcpy(train.data,virtualPath,train.length); send(fd,&train,sizeof(train.length)+train.length,MSG_NOSIGNAL); /* 发 md5 值 */ char md5[128]; changeToMd5(localPath,md5); train.length=strlen(md5); memcpy(train.data,md5,train.length); send(fd,&train,sizeof(train.length)+train.length,MSG_NOSIGNAL); /* 发文件长度 */ int filefd = open(localPath,O_RDWR); ERROR_CHECK(filefd,-1,"filefd"); struct stat statbuf; fstat(filefd,&statbuf); printf("%d\n",(int)statbuf.st_size); train.length = sizeof(statbuf.st_size); memcpy(train.data,&statbuf.st_size,train.length); send(fd,&train,sizeof(train.length)+train.length,MSG_NOSIGNAL); /* 发文件 */ sendfile(fd,filefd,NULL,statbuf.st_size); close(filefd); return 0; } ``` ### 9 gets 命令 与 断点重传功能 + 服务端先给客户端发文件名,客户端收到后返回一个偏移量 + 服务端收到偏移量后,就从偏移量开始传输 + 传输使用 mmap 提高效率(sendfile 系统调用) ```c int sendFileToClient(int fd, char *localPath, char *clientPath) { /* 发文件名 */ train_t train; train.length = strlen(clientPath); memcpy(train.data,clientPath,train.length); send(fd,&train,sizeof(train.length)+train.length,MSG_NOSIGNAL); /* 收偏移量 */ off_t offset; recv(fd,&offset,sizeof(offset),0); /* 发文件长度 */ int filefd = open(localPath,O_RDWR); ERROR_CHECK(filefd,-1,"filefd"); struct stat statbuf; fstat(filefd,&statbuf); train.length = sizeof(statbuf.st_size); memcpy(train.data,&statbuf.st_size,train.length); send(fd,&train,sizeof(train.length)+train.length,MSG_NOSIGNAL); /* 从偏移量开始发文件 */ sendfile(fd,filefd,&offset,statbuf.st_size-offset); close(filefd); return 0; } ``` ### 10 日志记录 + 本组直接将登录记录和命令记录放入系统日志文件中,系统调用 syslog + 注意:必须加 sudo 才能写入系统日志 + 路径 /var/log/syslog ```c #define LOG_WRITE(msg) { \ int fd = open("/var/log/syslog", O_WRONLY | O_APPEND | O_CREAT, 0666); \ time_t now = time(NULL); \ time(&now); \ struct tm* localTime = localtime(&now); \ char message[4096] = {0}; \ sprintf(message, "[%d-%d-%d][%d:%d:%d]: %s\n", localTime->tm_year + 1900, localTime->tm_mon + 1, localTime->tm_mday, localTime->tm_hour, localTime->tm_min, localTime->tm_sec, msg); \ write(fd, message, strlen(message)); \ close(fd); \ } ``` ## 最终版本 **服务端:** + 初代版本的 **线程池、私有协议、断点续传、秒传** 功能 + **长短命令分离**:使用线程池来完成客户端的请求,主线程做非 puts 和 gets 命令,子线程做 puts 和 gets 命令 + 注册和登录:用户信息存入 mysql 中,以密文形式存储 + 每次登陆后生成一个 **token** 发给客户端用于验证身份 + **虚拟目录系统**:虚拟的目录树全部存在 mysql 数据库中 + 文件存储:所有用户的所有文件放在一个真实的系统目录中,以 md5 值作为名字,同一份文件只存储一份 + **超时踢人**:客户端如果 30 秒没有操作,断开 TCP 连接,使用环形队列实现 **客户端:** + 检查并发送 cd、ls、pwd、mkdir、rmdir、puts、gets、remove 命令给服务端 + 长短命令分离:puts 和 gets 命令交给子线程,子线程与服务器建立新连接,验证 token 后才可以 I/O 文件 ### 1 数据库表结构 + 用户登录信息表 ```sql create table user (id int primary key auto_increment, username varchar(128) unique, salt varchar(128), passwd varchar(512), tomb int); ``` ![image-20230902092734876](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309020927896.png) ![image-20230902092638270](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309020926289.png) + 目录和文件系统表 ```c create table tree (id int primary key auto_increment, name varchar(128), username varchar(128), path varchar(256), pre_id int, md5 varchar(64), size bigint, createtime varchar(128),tomb int, unique key idx_username_path (username, path)); ``` ![image-20230902093027524](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309020930563.png) ![image-20230902092950495](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309020929536.png) + 存储位置:以 MD5 值作为 真实文件名 ![image-20230902093208769](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309020932814.png) ### 2 注册与登录 + 在 tcp 连接后就可以开始注册/登录,只实现了注册或者登录 + 使用 getpass 函数实现输入密码不显示的功能 + 用户在客户端输入账号和密码,用明文传给服务端 + 需要随机生成盐值加密后再存储到数据库中 + 如果是注册,服务端先查表,没有就插入一行数据 ```sql sprintf(sql,"select username from user where username = '%s';",tmpuser.userName); sprintf(sql,"insert into user (username,salt,passwd,tomb) values ('%s','%s','%s',0);",tmpuser.userName,saltnum,saltresult); ``` + 如果是登录,服务端直接找用户名,分为两种情况,用户名不存在和密码不正确 ```c sprintf(sql,"select salt, passwd, tomb from user where username = '%s';",tmpuser.userName); ``` + 盐值生成 ```c int salt(char *saltnum){ srand(time(NULL)); printf("salt begin\n"); saltnum[0] = '$'; saltnum[1] = '6'; saltnum[2] = '$'; //随机生成中间16位 const char char16[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; for (int i = 0; i < 16; ++i) { saltnum[3+i] = char16[rand()%(sizeof(char16)-1)]; } saltnum[19] = '$'; return 0; } ``` ### 3 cd 命令 + 服务端在缓存中存有当前路径,客户端发来一个相对路径/绝对路径(以 / 开头为区分) + 相对路径解决起来比较麻烦,要一个一个查表 ```sql sprintf(sql,"select path from tree where pre_id = %d and username = '%s' and tomb = 0;",row[0],username); ``` ![image-20230902093919600](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309020939639.png) ```c if (strcmp(cmd, "cd") == 0) { /* strcpy(pThreadPool->userBuffer[netFd], "liu"); */ int cdRet = changeDir(mysql, pThreadPool->userBuffer[netFd], pThreadPool->pathBuffer[netFd], path1); if (cdRet == -1) { sendMessageToClient(netFd, "cd: directory does not exist", 28); } else { sendMessageToClient(netFd, pThreadPool->pathBuffer[netFd], strlen(pThreadPool->pathBuffer[netFd])); } } ``` ### 4 ls 命令 ![image-20230902094714745](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309020947774.png) + 先取出当前目录的 id, 然后再 pre_id 里找当前 id ```sql sprintf(sql,"select id from tree where username = '%s' and path = '%s';\n",username,curr_path); sprintf(sql,"select name from tree where pre_id = %d and username = '%s' and tomb = 0;",row[0],username); ``` ```c int listSearch(MYSQL* mysql,const char* username,const char* curr_path,char* list){ //sq保存query语句 char sq[1024]={0}; sprintf(sq,"select id from tree where username = '%s' and path = '%s';\n",username,curr_path); int qret=mysql_query(mysql,sq); QUERY_CHECK(qret,mysql); MYSQL_RES *result=mysql_store_result(mysql); MYSQL_ROW row=mysql_fetch_row(result); //row[0]就是当前目录的id fprintf(stdout,"select name,md5,size,createtime from tree where pre_id = %s and tomb = 0;\n",row[0]); sprintf(sq,"select name,md5,size,createtime from tree where pre_id = %s and tomb = 0;",row[0]); qret=mysql_query(mysql,sq); QUERY_CHECK(qret,mysql); result=mysql_store_result(mysql); while((row=mysql_fetch_row(result))!=NULL){ strcat(list, row[3]); strcat(list, " "); strcat(list, row[2]); strcat(list, "\t"); strcat(list, row[0]); if (strcmp(row[1], "0") == 0) { strcat(list, "/"); } strcat(list,"\n"); } return 0; } ``` ### 5 gets 命令与断点重传 + 服务端先给客户端发文件名,客户端收到后返回一个偏移量 + 服务端收到偏移量后,就从偏移量开始传输 + 传输使用 mmap 提高效率(sendfile 系统调用) ```c int sendFileToClient(int fd, char *localPath, char *clientPath) { /* 发文件名 */ train_t train; train.length = strlen(clientPath); memcpy(train.data,clientPath,train.length); send(fd,&train,sizeof(train.length)+train.length,MSG_NOSIGNAL); /* 收偏移量 */ off_t offset; recv(fd,&offset,sizeof(offset),0); /* 发文件长度 */ int filefd = open(localPath,O_RDWR); ERROR_CHECK(filefd,-1,"filefd"); struct stat statbuf; fstat(filefd,&statbuf); train.length = sizeof(statbuf.st_size); memcpy(train.data,&statbuf.st_size,train.length); send(fd,&train,sizeof(train.length)+train.length,MSG_NOSIGNAL); /* 从偏移量开始发文件 */ sendfile(fd,filefd,&offset,statbuf.st_size-offset); close(filefd); return 0; } ``` ### 6 mkdir 命令 ![QQ图片20230902102438](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309021024435.jpg) + 小组实现的 mkdir 命令可以在相对路径下创建目录,但相对路径中不能有不存在的目录 + 相对路径创建文件的功能是基于 cd 实现的 + 先找是否有 tomb = 1 的目录 ```sql sprintf(sql,"select * from tree where path = '%s' and username = '%s' and tomb = 1;",path, username); ``` + 然后找目录是否存在:复制一份缓存中的当前路径,把路径参数传入修改的 cd 命令中(可以转换路径到任意目录或文件,不存在就报错) + 接着判断要创建的目录前的一串目录是否有不存在 ![image-20230902104315258](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309021043307.png) ```c int cutTail(char *path, char *left, char *right) { char tmp[1024] = {0}; memcpy(tmp, path, strlen(path)); memcpy(left, path, strlen(path)); char *p = strtok(tmp, "/"); while (1) { char *tmp = strtok(NULL, "/"); if (tmp == NULL) { memcpy(right, p, strlen(p) + 1); if (strlen(path) == strlen(right)) { strcpy(left, "."); break; } left[strlen(path) - strlen(right) - 1] = '\0'; break; } p = tmp; } return 0; } ``` + 插入表中 ```sql sprintf(sql,"insert into tree (name,username,path,pre_id,md5,tomb,size,createtime) values('%s' , '%s' , '%s' ,%d, '0' , 0, 0, '%s') ;",right,username,path,id,t); ``` ### 7 puts 命令与秒传功能 + 基于小火车协议 + 需要传递 md5 值,如果 md5 值相同即可妙传 + 先发文件名,再发 md5 值,然后发文件长度,最后发文件 + 同样支持相对路径 ```sql sprintf(sql,"insert into tree (name,username,path, pre_id,md5,size,createtime,tomb) values('%s','%s','%s',%d ,'%s', %d,'%s', %d);",filename,user,filepath,parent,filemd5,filesize,t, 0); ``` ![image-20230902105119498](https://cancanneedpicture.oss-cn-guangzhou.aliyuncs.com/TyporaPic/202309021051543.png) ```c int sendFileToServer(int fd, char *localPath, char *virtualPath) { /* 发文件名 */ train_t train; train.length = strlen(virtualPath); memcpy(train.data,virtualPath,train.length); send(fd,&train,sizeof(train.length)+train.length,MSG_NOSIGNAL); /* 发 md5 值 */ char md5[128]; changeToMd5(localPath,md5); train.length=strlen(md5); memcpy(train.data,md5,train.length); send(fd,&train,sizeof(train.length)+train.length,MSG_NOSIGNAL); /* 发文件长度 */ int filefd = open(localPath,O_RDWR); ERROR_CHECK(filefd,-1,"filefd"); struct stat statbuf; fstat(filefd,&statbuf); printf("%d\n",(int)statbuf.st_size); train.length = sizeof(statbuf.st_size); memcpy(train.data,&statbuf.st_size,train.length); send(fd,&train,sizeof(train.length)+train.length,MSG_NOSIGNAL); /* 发文件 */ sendfile(fd,filefd,NULL,statbuf.st_size); close(filefd); return 0; } ``` ### 8 rmdir 命令 + 同样支持相对路径,方法类似 mkdir ```sql sprintf(sql,"update tree set tomb=1 where path = '%s' and username = '%s';",path, username); ``` ### 9 remove 命令 + 同样支持相对路径,方法类似 mkdir ```c sprintf(sql,"update tree set tomb=1 where path = '%s' and username = '%s';",path, username); ``` ### 10 pwd 命令 + 一直存在缓存中,直接取出 ```c if (strcmp(cmd, "pwd") == 0) { printf("%s\n", pThreadPool->pathBuffer[netFd]); sendMessageToClient(netFd, pThreadPool->pathBuffer[netFd], strlen(pThreadPool->pathBuffer[netFd])); } ``` ### 11 长短命令分离 #### 11.1 服务端 ```c while (1) { int readyCount = epoll_wait(epollFd, eventArr, 10, -1); for (int i = 0; i < readyCount; ++i) { /* 事件一: 有新连接 */ if (eventArr[i].data.fd == socketFd) { struct sockaddr_in clientAddr; int netFd = acceptConnection(socketFd, &clientAddr); epollAdd(epollFd, netFd); // 添加监听 /* puts 和 gets 的新连接 */ if (子线程的连接) { pthread_mutex_lock(&threadPool.mutex); enQueue(&threadPool.taskQueue, netFd); // 加入任务队列 pthread_cond_signal(&threadPool.cond); // 唤醒子线程处理长命令 pthread_mutex_unlock(&threadPool.mutex); } /* 事件二: 处理短命令 */ } else { /* 处理短命令 */ } } } ``` #### 11.2 客户端 ```c while (1) { int readyCount = epoll_wait(epollFd, eventArr, 2, -1); ERROR_CHECK(readyCount, -1, "epoll_wait"); for (int i = 0; i < readyCount; ++i) { /* 事件一: 标准输入就绪 */ if (eventArr[i].data.fd == STDIN_FILENO) { memset(buf, 0, sizeof(buf)); ssize_t stdinLength = recvStdin(buf, sizeof(buf)); /* 解析命令 */ int ret = cutCmd(buf, cmd, path1, path2); if (!checkArguments(cmd, path1, path2, ret)) { write(STDOUT_FILENO, s, strlen(s)); continue; } /* puts 命令 和 gets 命令交给子线程处理 */ if (strcmp(cmd, "puts") == 0) { source_t source; initSource(&source, atoi(fd), buf, name, cmd, path1, path2); pthread_t thread; pthread_create(&thread, NULL, threadFunc, &source); } else if (strcmp(cmd, "gets") == 0) { source_t source; initSource(&source, atoi(fd), buf, name, cmd, path1, path2); pthread_t thread; pthread_create(&thread, NULL, threadFunc, &source); } else { /* 其他命令直接发给服务器主线程处理 */ sendMessageToServer(socketFd, buf, stdinLength); } /* 事件二: tcp 读端就绪 */ } else { ssize_t recvRet = recvMessageFromServer(socketFd, buf, sizeof(buf)); printf("%s\n", buf); write(STDOUT_FILENO, s, strlen(s)); } } } ``` + 由于小组设计了缓存系统(用来实现所有命令支持相对路径),所以子线程也要知道当前用户的缓存(知道短命令的 netfd) + 因此设计了新的命令 getfd,客户端可以获取短命令的 netfd,在创建子线程时要传入这个 netfd ### 12 token + 服务端生成 token:以客户端传来的用户名作为 key 值,生成 token,参考了 github 中的文档 ```c void createToken(char *username,char *token) { char* jwt; size_t jwt_length; struct l8w8jwt_encoding_params params; l8w8jwt_encoding_params_init(¶ms); params.alg = L8W8JWT_ALG_HS512; params.sub = "myCloud"; params.iss = "team07"; params.aud = "classmate"; params.iat = 0; params.exp = 0x7fffffff; /* Set to expire after 10 minutes (600 seconds). */ params.secret_key = (unsigned char*)username; params.secret_key_length = strlen(params.secret_key); params.out = &jwt; params.out_length = &jwt_length; int r = l8w8jwt_encode(¶ms); if(r==L8W8JWT_SUCCESS){ strcpy(token,jwt); } printf("\n l8w8jwt example HS512 token: %s \n", r == L8W8JWT_SUCCESS ? jwt : " (encoding failure) "); /* Always free the output jwt string! */ l8w8jwt_free(jwt); } ``` + token 的携带:服务端以小火车的形式发送给客户端,先发用户名,再发 token ```c int sendTokenToClient(int fd, char *username, char *token) { train_t train; memset(&train, 0, sizeof(train)); /* username */ train.length = strlen(username); memcpy(train.data,username, train.length); ssize_t sendRet = send(fd, &train, sizeof(train.length) +train.length, 0); ERROR_CHECK(sendRet, -1, "send username"); /* token */ train.length=strlen(token); memcpy(train.data,token,train.length); sendRet=send(fd, &train, sizeof(train.length) +train.length,0); ERROR_CHECK(sendRet, -1, "send token"); return 0; } ``` + token 的验证:客户端子线程执行 puts 和 gets 命令时,携带 token 相关信息,传给服务端认证 + 认证成功返回 1, 认证失败返回 0,参考了 github 文档 ```c int verify(char *username,char *token){ struct l8w8jwt_decoding_params params; l8w8jwt_decoding_params_init(¶ms); params.alg = L8W8JWT_ALG_HS512; params.jwt = (char*)token; params.jwt_length = strlen(token); params.verification_key = (unsigned char*)username; params.verification_key_length = strlen(username); params.validate_iss = "team07"; params.validate_sub = "myCloud"; params.validate_exp = 0; params.exp_tolerance_seconds = 60; params.validate_iat = 0; params.iat_tolerance_seconds = 60; enum l8w8jwt_validation_result validation_result; int decode_result = l8w8jwt_decode(¶ms, &validation_result, NULL, NULL); if (decode_result == L8W8JWT_SUCCESS && validation_result == L8W8JWT_VALID) { printf("\n Example HS512 token validation successful! \n"); return 1; } else { printf("\n Example HS512 token validation failed! \n"); return 0; } } ``` ### 13 定时器设计 + 使用 epoll 的超时就绪实现定时器,就绪时间设为 1000ms + 每次 epoll_wait 返回 ,就让 timer = time(NULL),就会跟随时间自增 ### 14 环形队列数据结构 时间轮演示 + 循环队列:定长数组实现,一共 30 个槽,每次 timer 前进就前进一个槽,有一个 currentSlot 指向当前的槽 ```c #define TIME_WHEEL_SIZE 30 //设置一个长度为30的循环队列 typedef struct time_wheel_s{ //30个槽位,每个槽位一个集合 HashSet* set[TIME_WHEEL_SIZE]; //当前的槽位 int curr_slot; //一个哈希表,用于记录fd在哪个槽位的集合里 HashTable* table; }time_wheel_t; ``` + map 设计:一个哈希表,键是文件描述符,值是槽位 ```c typedef struct{ Entry* entries[TABLE_SIZE]; }HashTable; ``` + set 设计:每个槽都有一个集合,用哈希表实现,存放槽内的文件描述符 ```c typedef struct Entry { int key; int value; struct Entry* next; } Entry; typedef struct { Entry* entries[TABLE_SIZE]; } HashSet; ```