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
- ptClient.cpp
g++ ptClient.cpp -o ptClient
4.2 Server端IP地址
4.3 多端接收和发送数据包
- 多端连接服务器
多个客户端同时连接服务器,将不同客户端连接的描述符s加入连接集合client_sockets中。
- 多端请求服务
当不同的用户分别请求服务器的服务,客户端会将有服务请求的客户端连接描述符s加入任务队列work_queue中,由一开始的MAX_THREADS个线程取出为其提供服务。
当用户请求上传文件的服务时,由于用户是将一个文件分成多个数据包dataPack分别上传服务器,由于可能无法实现多个线程同时对一个连接描述符进行接收数据包,此处采用的处理是线程接收数据包的过程中,若客户端又发送了另一个数据包,则该线程继续为该客户端提供服务,直至客户端没有连续的数据包发送后,则将该客户端连接描述符从任务队列work_queue中清除。由于每个数据包均记录了数据包的内容所对应的文件,已经对应的内容从文件起始位置的偏移,所以即使由于时延导致数据包不能连续发送,仍不会破坏文件的上传。
当用户请求下载文件的服务时,则处理相对简单。根据数据包中相应的信息,打开相应的文件,读完文件内容构造数据包发送到客户端,客户端读取数据包后,将文件写入相应的文件即可。
- 客户端并行请求服务
当两个客户端同时上传文件时,第一个客户端正在上传文件的过程中,第二客户端请求上传文件,则主线程会将其加入任务队列,并由另一个线程为第二客户端的文件上传进行服务,实现并发多线程服务。
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提供了一种更加灵活的加锁方式,可以在构造函数中指定是否立即加锁,也可以在之后的代码中动态地加锁和解锁,这种灵活性可以更好地控制锁的粒度,提高代码的执行效率。
- unique_lock是可移动的,可以通过移动语义将unique_lock对象从一个线程传递到另一个线程,这种可移动性方便了线程间的数据传递和同步。
- unique_lock允许在同一个线程中多次加锁和解锁同一个互斥量,这种可重入性比pthread_mutex_lock更加安全,避免了死锁问题。
- unique_lock在执行加锁和解锁操作时,使用了RAII技术,将加锁和解锁操作封装在构造函数和析构函数中,可以更好地保证锁的正确性,同时也避免了忘记解锁的问题,在性能上也比较优秀。
如果应用场景比较复杂,或者需要更好的灵活性、可移动性和可重入性,那么unique_lock可能更加适合。