目录
一、项目要求
模拟FTP核心原理:
客户端连接服务器后,向服务器发送一个文件。文件名可以通过参数指定,服务器端接受客户端传来的文件(文件名随意),如果文件不存在自动创建文件,如果文件存在,那么清空文件然后写入。
项目功能介绍:
有服务器端和客户端代码,基于TCP写。
在不同路径下运行服务器端和客户端,模拟另一台电脑在访问服务器。
客户端和服务器端连接成功后出现以下提示:功能菜单列表
********************list******************** // 列出服务器所在目录下的文件名(目录不显示)
******************upload***************** // 上传一个文件到服务器端
****************download**************** // 下载一个文件到客户端
********************quit******************* // 退出客户端(服务器端不退出,等待下一个客户端连接)
PS:1. 功能实现需要连接网络,测试前确保虚拟机联网。
2. 运行时先运行服务器,再运行客户端连接服务器,退出时客户端退出,服务器等待下一个客户端连接
二、编写思路
客户端:
1. 创建一个客户端信息的结构体,需要存储客户端的用户名、命令、要上传/下载的文件名。每次进行命令传输只需要向客户端发送结构体即可。由于文件内容可能较大,来回传递次数较多,如果将文件内容也放在结构体中传输,会浪费许多资源,所以需要单独设置一个数组进行文件内容的传输。
2. 因为要使用TCP通信,连接步骤必不可少:创建套接字、绑定服务器信息、发送连接请求。连接成功后打印命令菜单,循环输入命令实现文件的上传和下载。命令菜单可以用switch...case选择结构实现:输入对应命令,跳转到相应的函数,实现功能。
3. 将每个功能单独写成一个函数进行实现,实现一个函数的功能后再进行下一个函数功能的编写。
服务器端:
1. 同样需要创建一个与客户端相同的结构体用来接收和存储客户端的信息,每次接收命令则接收整个结构体。同样文件内容需要单独定义数组进行传输。
2. TCP通信服务器端流程同样必不可少:创建套接字、初始化服务器信息结构体、绑定服务器信息、监听套接字、等待客户端连接。连接成功后创建子线程,子进程与客户端进行通信,主线程等待下一位客户端连接,实现多线程并发式服务器。
3. 子线程接收客户端发来的命令,执行对应的函数功能。
三、整体框架
客户端:
// Client.c
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <dirent.h>
#define PORT 8085 // 端口号
#define BUFFER_SIZE 128 // 最大输入信息
#define NAME_SIZE 64 // 最大用户名长度
struct client
{
char name[NAME_SIZE]; // 客户端的名称
char command[NAME_SIZE]; // 客户端输入的命令
char filename[NAME_SIZE]; // 客户端要上传/下载的文件名
//mode_t mode; // 文件的类型与权限
};
int sockfd; // 文件描述符
// 帮助菜单
void ftp_commond_helpmenu(void);
// 命令菜单
void ftp_commond_menu(struct client *cli);
// 上传文件函数
int ftp_uploadfile(struct client *cli);
// 下载文件函数
int ftp_downloadfile(struct client *cli);
// 列出服务器目录下的文件名
int ftp_listfile(struct client *cli);
// 退出函数
void ftp_quit(struct client *cli);
int main(int argc, char const *argv[])
{
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 填充服务器地址结构体
struct sockaddr_in serv_addr;
// 请求连接
connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
// 为客户端结构体分配内存
struct client *cli = (struct client *)malloc(sizeof(struct client));
// 发送用户名到服务器
scanf("%s", cli->name);
send(sockfd, cli->name, sizeof(cli->name), 0);
ftp_commond_helpmenu();
// 命令菜单
ftp_commond_menu(cli);
// 关闭套接字
close(sockfd);
return 0;
}
// 帮助命令菜单
void ftp_commond_helpmenu(void)
{
printf("**************************************************\n");
printf("* upload\t\t\t\t*\n");
printf("* download\t\t\t\t*\n");
printf("* list\t\t\t\t*\n");
printf("* quit\t\t\t\t*\n");
printf("* help\t\t\t\t*\n");
printf("**************************************************\n");
}
// 命令处理函数
void ftp_commond_menu(struct client *cli)
{
// 循环发送命令到服务器
return;
}
// 下载文件函数
int ftp_downloadfile(struct client *cli)
{
// 1. 打开文件
// 2. 接收文件数据
// 3. 写到文件中
}
// 上传文件函数
int ftp_uploadfile(struct client *cli)
{
// 1. 打开文件
// 2. 读取文件
// 3. 发送文件
return 0;
}
// 客户端:列出服务器目录下的文件名
int ftp_listfile(struct client *cli)
{
// 循环接收文件名
return 0;
}
// 退出函数
void ftp_quit(struct client *cli)
{
return;
}
服务器端:
// Sever.c
#define PORT 8085
#define MAX_CLIENTS 10
#define NAME_SIZE 64
#define BUF_SIZE 128
// 结构体表示连接到服务器的客户端
struct client
{
char name[NAME_SIZE]; // 客户端的名称
char command[NAME_SIZE]; // 客户端输入的命令
char filename[NAME_SIZE]; // 客户端要上传/下载的文件名
//mode_t mode; // 文件的类型与权限
int sockfd; // 与客户端通信的套接字文件描述符
};
struct client *clients[MAX_CLIENTS]; // 存储指向客户端结构体的指针的数组
int client_count = 0; // 连接的客户端数量
pthread_mutex_t client_mutex; // 互斥锁
// 处理客户端命令请求
void *ftp_commond_menu(void *arg);
// 上传文件函数
int ftp_uploadfile(struct client *cli);
// 下载文件函数
int ftp_downloadfile(struct client *cli);
// 列出服务器目录下的文件名
int ftp_listfile(struct client *cli);
// 退出函数
void ftp_quit(struct client *cli);
int main(int argc, char const *argv[])
{
// 创建连接套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器地址信息
struct sockaddr_in addr;
// 绑定套接字
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
// 监听套接字
listen(sockfd, 5);
// 循环接收客户端连接
while (1)
{
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
// 等待客户端连接
int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);
// 为客户端结构体分配内存
struct client *cli = (struct client *)malloc(sizeof(struct client));
// 接受客户端名称
recv(connfd, cli->name, sizeof(cli->name), 0);
// 创建线程处理客户端通信
pthread_t tid;
int pthread_id = pthread_create(&tid, NULL, (void *)ftp_commond_menu, cli);
// 分离线程以允许其独立运行
pthread_detach(tid);
}
close(sockfd);
return 0;
}
// 处理客户端命令请求
void *ftp_commond_menu(void *arg)
{
struct client *cli = (struct client *)arg;
// 接受客户端命令
while (recv(cli->sockfd, cli->command, sizeof(cli->command), 0) > 0)
{
}
// 客户端断开连接时打印提示消息
printf("client %s is disconnected.\n", cli->name);
// 关闭客户端套接字并将其从数组中移除
close(cli->sockfd);
free(cli);
pthread_exit(NULL);
}
// 上传文件函数
int ftp_uploadfile(struct client *cli)
{
recv(cli->sockfd, cli->filename, sizeof(cli->filename), 0);
// 1. 打开文件
int fd = open(cli->filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
// 2. 接收文件内容
char buf[BUF_SIZE];
while (1)
{
int ret = recv(cli->sockfd, buf, sizeof(buf), 0);
// 3. 写到文件中
write(fd, buf, strlen(buf));
}
close(fd);
return 0;
}
// 下载文件函数
int ftp_downloadfile(struct client *cli)
{
// 1. 接受文件名
recv(cli->sockfd, cli->filename, sizeof(cli->filename), 0);
// 2. 打开文件
int fd = open(cli->filename, O_RDONLY);
char buf[BUF_SIZE];
while (1)
{
memset(buf, 0, sizeof(buf));
// 3. 读取文件
int len = read(fd, buf, BUF_SIZE);
// 4. 发送文件
int ret = send(cli->sockfd, buf, sizeof(buf), 0);
}
close(fd);
printf("downloadfile success.\n");
return 0;
}
// 列出服务器目录下的文件名
int ftp_listfile(struct client *cli)
{
// 1. 打开目录
struct dirent *dir;
DIR *dirp = opendir(".");
// 2. 读取目录下的文件名
while (1)
{
// 3. 发送文件名
send(cli->sockfd, cli->filename, sizeof(cli->filename), 0);
}
closedir(dirp);
return 0;
}
// 退出函数
void ftp_quit(struct client *cli)
{
return;
}
四、代码详解
list功能函数
服务器端:
// 列出服务器目录下的文件名
int ftp_listfile(struct client *cli)
{
// 1. 打开目录
struct dirent *dir;
DIR *dirp = opendir(".");
if (dirp == NULL)
{
perror("opendir");
return -1;
}
// 2. 读取目录下的文件名
while (1)
{
dir = readdir(dirp);
if (dir == NULL)
{
break;
}
// 3. 发送文件名
if (dir->d_type == 8)
{
memset(cli->filename, 0, sizeof(cli->filename));
strcpy(cli->filename, dir->d_name);
send(cli->sockfd, cli->filename, sizeof(cli->filename), 0);
}
}
memset(cli->filename, 0, sizeof(cli->filename));
strcpy(cli->filename, "over");
send(cli->sockfd, cli->filename, sizeof(cli->filename), 0);
memset(cli->filename, 0, sizeof(cli->filename));
printf("list file success\n");
closedir(dirp);
return 0;
}
1. 使用opdir函数打开当前目录的文件目录。
2. 使用while(1)循环读取文件夹中的文件名,并将文件类型为普通文件的文件名发送到客户端。
3. 当文件全部读取完毕后,发送“over”指令标志文件名读取结束。
4. closedir关闭目录文件
客户端:
// 客户端:列出服务器目录下的文件名
int ftp_listfile(struct client *cli)
{
// char buf[BUFFER_SIZE];
while (1)
{
memset(cli->filename, 0, sizeof(cli->filename));
int ret = recv(sockfd, cli->filename, sizeof(cli->filename), 0);
if (ret < 0)
{
perror("recv");
return -1;
}
if (strcmp(cli->filename, "over") == 0)
{
break;
}
else
{
printf("%s\t", cli->filename);
}
}
printf("\nlist over\n");
return 0;
}
1. 循环接收服务器端发来的文件名,并将其打印在终端。
2. 当读取到“over”标识时,结束循环。
upload功能函数
服务器端:
// 上传文件函数
int ftp_uploadfile(struct client *cli)
{
memset(cli->filename, 0, sizeof(cli->filename));
recv(cli->sockfd, cli->filename, sizeof(cli->filename), 0);
// 1. 打开文件
int fd = open(cli->filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return -1;
}
char buf[BUF_SIZE];
while (1)
{
memset(buf, 0, sizeof(buf));
// 2. 接收文件内容
int ret = recv(cli->sockfd, buf, sizeof(buf), 0);
if(ret < 0)
{
perror("recv");
return -1;
}
if(strcmp(buf,"exit") == 0)
{
break;
}
// 3. 写到文件中
write(fd, buf, strlen(buf));
}
close(fd);
printf("uploadfile success.\n");
return 0;
}
1. 接收到客户端发来的文件名时,使用open函数打开对应文件,打开权限为O_WRONLY | O_CREAT | O_TRUNC(可写、若无该文件则创建、若有该文件则清空),文件权限默认0666(实际权限0664)。
2. while(1)循环接收文件内容,并将其写入文件中。当读到“exit”标识时,标志文件内容已全部写完,退出循环。
3. close关闭文件描述符。
客户端:
// 上传文件函数
int ftp_uploadfile(struct client *cli)
{
printf("Please input the file name:\n");
memset(cli->filename, 0, sizeof(cli->filename));
scanf("%s", cli->filename);
while (getchar() != '\n')
;
send(sockfd, cli->filename, sizeof(cli->filename), 0);
struct stat st;
stat(cli->filename, &st);
if (S_ISDIR(st.st_mode))
{
printf("该文件为目录\n");
return -1;
}
cli->mode = st.st_mode;
// 1. 打开文件
int fd = open(cli->filename, O_RDONLY);
if (fd < 0)
{
perror("open1");
return -1;
}
char buf[BUFFER_SIZE];
while (1)
{
memset(buf, 0, sizeof(buf));
// 2. 读取文件
int len = read(fd, buf, BUFFER_SIZE-1);
buf[BUFFER_SIZE - 1] = '\0';
if (len < 0)
{
perror("read");
return -1;
}
if (len == 0)
{
send(sockfd, "exit", 4, 0);
break;
}
else
{
// 3. 发送文件
int ret = send(sockfd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("send");
return -1;
}
}
}
close(fd);
printf("upload success.\n");
return 0;
}
1. 输入并发送要上传的文件名,并判断是否是普通文件。
2. 打开要上传的文件,权限为只读。
3. while(1)循环从文件中读取内容,并将其发送到服务器端。文件读取完毕后,发送“exit”标识标志文件内容传输完成。
4. close关闭文件描述符
download功能函数
服务器端:
// 下载文件函数
int ftp_downloadfile(struct client *cli)
{
memset(cli->filename, 0, sizeof(cli->filename));
// 1. 接受文件名
recv(cli->sockfd, cli->filename, sizeof(cli->filename), 0);
struct stat st;
stat(cli->filename, &st);
if (S_ISDIR(st.st_mode))
{
printf("该文件为目录\n");
return -1;
}
cli->mode = st.st_mode;
// 2. 打开文件
int fd = open(cli->filename, O_RDONLY);
if (fd < 0)
{
perror("open");
return -1;
}
char buf[BUF_SIZE];
while (1)
{
memset(buf, 0, sizeof(buf));
// 3. 读取文件
int len = read(fd, buf, BUF_SIZE);
if (len < 0)
{
perror("read");
return -1;
}
else if (len == 0)
{
send(cli->sockfd, "exit", 4, 0);
break;
}
// 4. 发送文件
int ret = send(cli->sockfd, buf, sizeof(buf), 0);
if(ret < 0)
{
perror("send");
return -1;
}
}
close(fd);
printf("downloadfile success.\n");
return 0;
}
1. 接收客户端发来的要下载的文件名。
2. open函数打开文件,权限为只读。
3. while(1)循环从文件中读取内容,并发送到客户端。当文件读取完毕时,发送“exit”标志文件内容传输完毕,退出while循环。
4. close函数关闭文件描述符
客户端:
// 下载文件函数
int ftp_downloadfile(struct client *cli)
{
memset(cli->filename, 0, sizeof(cli->filename));
printf("Please input the filename: \n");
scanf("%s", cli->filename);
while (getchar() != '\n')
;
send(sockfd, cli->filename, sizeof(cli->filename), 0);
// 1. 打开文件
int fd = open(cli->filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return -1;
}
char buf[BUFFER_SIZE];
while (1)
{
memset(buf, 0, sizeof(buf));
// 2. 接收文件数据
int ret = recv(sockfd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("recv");
return -1;
}
if (strcmp(buf, "exit") == 0)
{
break;
}
// 3. 写到文件中
write(fd, buf, strlen(buf));
}
close(fd);
printf("download success.\n");
return 0;
}
1. 输入并发送要下载的文件名给服务器端。
2. open函数打开该文件,权限为O_WRONLY | O_CREAT | O_TRUNC(可写、若无该文件则创建、若有该文件则清空),默认创建权限为0666(实际为0664)。
3. while(1)循环接收文件内容并写入文件中。当接收到“exit”时标志文件内容传输结束,退出循环。
4. close关闭文件描述符。
五、完整代码
客户端:
// Client.c
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <dirent.h>
#define PORT 8085 // 端口号
#define BUFFER_SIZE 128 // 最大输入信息
#define NAME_SIZE 64 // 最大用户名长度
struct client
{
char name[NAME_SIZE]; // 客户端的名称
char command[NAME_SIZE]; // 客户端输入的命令
char filename[NAME_SIZE]; // 客户端要上传/下载的文件名
//mode_t mode; // 文件的类型与权限
};
int sockfd; // 文件描述符
// 帮助菜单
void ftp_commond_helpmenu(void);
// 命令菜单
void ftp_commond_menu(struct client *cli);
// 上传文件函数
int ftp_uploadfile(struct client *cli);
// 下载文件函数
int ftp_downloadfile(struct client *cli);
// 列出服务器目录下的文件名
int ftp_listfile(struct client *cli);
// 退出函数
void ftp_quit(struct client *cli);
int main(int argc, char const *argv[])
{
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket");
return -1;
}
printf("socket OK.\n");
// 填充服务器地址结构体
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT); // 端口号
serv_addr.sin_addr.s_addr = inet_addr(argv[1]); // 服务器IP
// 请求连接
if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)))
{
perror("connect");
return -1;
}
printf("connect success\n");
// 为客户端结构体分配内存
struct client *cli = (struct client *)malloc(sizeof(struct client));
// 发送用户名到服务器
printf("please input your username: ");
scanf("%s", cli->name);
while (getchar() != '\n')
;
send(sockfd, cli->name, sizeof(cli->name), 0);
ftp_commond_helpmenu();
ftp_commond_menu(cli);
// 关闭套接字
close(sockfd);
return 0;
}
// 帮助命令菜单
void ftp_commond_helpmenu(void)
{
printf("**************************************************\n");
printf("* upload\t\t\t\t*\n");
printf("* download\t\t\t\t*\n");
printf("* list\t\t\t\t*\n");
printf("* quit\t\t\t\t*\n");
printf("* help\t\t\t\t*\n");
printf("**************************************************\n");
}
// 命令处理函数
void ftp_commond_menu(struct client *cli)
{
// 发送命令到服务器
while (1)
{
memset(cli->command, 0, sizeof(cli->command));
printf("Please input your command: \n");
scanf("%s", cli->command);
while (getchar() != '\n')
;
send(sockfd, cli->command, sizeof(cli->command), 0);
if (strcmp(cli->command, "upload") == 0)
{
ftp_uploadfile(cli);
}
else if (strcmp(cli->command, "download") == 0)
{
ftp_downloadfile(cli);
}
else if (strcmp(cli->command, "list") == 0)
{
ftp_listfile(cli);
}
else if (strcmp(cli->command, "quit") == 0)
{
ftp_quit(cli);
break;
}
else if (strcmp(cli->command, "help") == 0)
{
ftp_commond_helpmenu();
}
else
{
printf("Invalid command\n");
}
}
free(cli);
printf("exit\n");
return;
}
// 下载文件函数
int ftp_downloadfile(struct client *cli)
{
memset(cli->filename, 0, sizeof(cli->filename));
printf("Please input the filename: \n");
scanf("%s", cli->filename);
while (getchar() != '\n')
;
send(sockfd, cli->filename, sizeof(cli->filename), 0);
// 1. 打开文件
int fd = open(cli->filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return -1;
}
char buf[BUFFER_SIZE];
while (1)
{
memset(buf, 0, sizeof(buf));
// 2. 接收文件数据
int ret = recv(sockfd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("recv");
return -1;
}
if (strcmp(buf, "exit") == 0)
{
break;
}
// 3. 写到文件中
write(fd, buf, strlen(buf));
}
printf("download success.\n");
return 0;
}
// 上传文件函数
int ftp_uploadfile(struct client *cli)
{
printf("Please input the file name:\n");
memset(cli->filename, 0, sizeof(cli->filename));
scanf("%s", cli->filename);
while (getchar() != '\n')
;
send(sockfd, cli->filename, sizeof(cli->filename), 0);
struct stat st;
stat(cli->filename, &st);
if (S_ISDIR(st.st_mode))
{
printf("该文件为目录\n");
return -1;
}
cli->mode = st.st_mode;
// 1. 打开文件
int fd = open(cli->filename, O_RDONLY);
if (fd < 0)
{
perror("open1");
return -1;
}
char buf[BUFFER_SIZE];
while (1)
{
memset(buf, 0, sizeof(buf));
// 2. 读取文件
int len = read(fd, buf, BUFFER_SIZE-1);
buf[BUFFER_SIZE - 1] = '\0';
if (len < 0)
{
perror("read");
return -1;
}
if (len == 0)
{
send(sockfd, "exit", 4, 0);
break;
}
else
{
// 3. 发送文件
int ret = send(sockfd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("send");
return -1;
}
}
}
close(fd);
printf("upload success.\n");
return 0;
}
// 客户端:列出服务器目录下的文件名
int ftp_listfile(struct client *cli)
{
// char buf[BUFFER_SIZE];
while (1)
{
memset(cli->filename, 0, sizeof(cli->filename));
int ret = recv(sockfd, cli->filename, sizeof(cli->filename), 0);
if (ret < 0)
{
perror("recv");
return -1;
}
if (strcmp(cli->filename, "over") == 0)
{
break;
}
else
{
printf("%s\t", cli->filename);
}
}
printf("\nlist over\n");
return 0;
}
// 退出函数
void ftp_quit(struct client *cli)
{
return;
}
服务器端:
// Sever.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <pthread.h>
#define PORT 8085
#define MAX_CLIENTS 10
#define NAME_SIZE 64
#define BUF_SIZE 128
// 结构体表示连接到服务器的客户端
struct client
{
char name[NAME_SIZE]; // 客户端的名称
char command[NAME_SIZE]; // 客户端输入的命令
char filename[NAME_SIZE]; // 客户端要上传/下载的文件名
//mode_t mode; // 文件的类型与权限
int sockfd; // 与客户端通信的套接字文件描述符
};
struct client *clients[MAX_CLIENTS]; // 存储指向客户端结构体的指针的数组
int client_count = 0; // 连接的客户端数量
pthread_mutex_t client_mutex; // 互斥锁
// 处理客户端命令请求
void *ftp_commond_menu(void *arg);
// 上传文件函数
int ftp_uploadfile(struct client *cli);
// 下载文件函数
int ftp_downloadfile(struct client *cli);
// 列出服务器目录下的文件名
int ftp_listfile(struct client *cli);
// 退出函数
void ftp_quit(struct client *cli);
int main(int argc, char const *argv[])
{
// 创建连接套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket");
return -1;
}
printf("socket ok\n");
// 初始化服务器地址信息
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = INADDR_ANY;
// 绑定套接字
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)))
{
perror("bind");
return -1;
}
printf("bind ok\n");
// 监听套接字
if (listen(sockfd, 5))
{
perror("listen");
return -1;
}
printf("listen ok\n");
// 循环接收客户端连接
while (1)
{
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
// 等待客户端连接
printf("Waiting for client...\n");
int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);
if (connfd < 0)
{
perror("accept");
return -1;
}
printf("Accept success\n");
pthread_mutex_lock(&client_mutex); // 互斥锁以安全地访问客户端数据
if (client_count < MAX_CLIENTS)
{
// 为客户端结构体分配内存
struct client *cli = (struct client *)malloc(sizeof(struct client));
// 接受客户端名称
recv(connfd, cli->name, sizeof(cli->name), 0);
cli->sockfd = connfd;
clients[client_count++] = cli;
printf("Client %s is connected.\n", cli->name);
// 创建线程处理客户端通信
pthread_t tid;
int pthread_id = pthread_create(&tid, NULL, (void *)ftp_commond_menu, cli);
if (pthread_id < 0)
{
perror("pthread_create");
close(connfd);
return -1;
}
// 分离线程以允许其独立运行
pthread_detach(tid);
}
else
{
printf("Maximum clients reached.Connection rejected.\n");
close(connfd);
}
pthread_mutex_unlock(&client_mutex);
}
close(sockfd);
return 0;
}
// 处理客户端命令请求
void *ftp_commond_menu(void *arg)
{
struct client *cli = (struct client *)arg;
// 接受客户端命令
while (recv(cli->sockfd, cli->command, sizeof(cli->command), 0) > 0)
{
printf("Receive command: %s\n", cli->command);
if (strcmp(cli->command, "upload") == 0)
{
ftp_uploadfile(cli);
}
else if (strcmp(cli->command, "download") == 0)
{
ftp_downloadfile(cli);
}
else if (strcmp(cli->command, "list") == 0)
{
ftp_listfile(cli);
}
else if (strcmp(cli->command, "quit") == 0)
{
ftp_quit(cli);
break;
}
else
{
printf("Invalid command\n");
send(cli->sockfd, "Invalid command", 14, 0);
}
}
// 客户端断开连接时打印提示消息
printf("client %s is disconnected.\n", cli->name);
// 关闭客户端套接字并将其从数组中移除
pthread_mutex_lock(&client_mutex);
close(cli->sockfd);
for (int i = 0; i < client_count; i++)
{
if (clients[i]->sockfd == cli->sockfd)
{
for (int j = i; j < client_count - 1; j++)
{
clients[j] = clients[j + 1];
}
client_count--;
break;
}
}
free(cli);
pthread_mutex_unlock(&client_mutex);
pthread_exit(NULL);
}
// 上传文件函数
int ftp_uploadfile(struct client *cli)
{
memset(cli->filename, 0, sizeof(cli->filename));
recv(cli->sockfd, cli->filename, sizeof(cli->filename), 0);
// 1. 打开文件
int fd = open(cli->filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return -1;
}
char buf[BUF_SIZE];
while (1)
{
memset(buf, 0, sizeof(buf));
// 2. 接收文件内容
int ret = recv(cli->sockfd, buf, sizeof(buf), 0);
if(ret < 0)
{
perror("recv");
return -1;
}
if(strcmp(buf,"exit") == 0)
{
break;
}
// 3. 写到文件中
write(fd, buf, strlen(buf));
}
close(fd);
printf("uploadfile success.\n");
return 0;
}
// 下载文件函数
int ftp_downloadfile(struct client *cli)
{
memset(cli->filename, 0, sizeof(cli->filename));
// 1. 接受文件名
recv(cli->sockfd, cli->filename, sizeof(cli->filename), 0);
struct stat st;
stat(cli->filename, &st);
if (S_ISDIR(st.st_mode))
{
printf("该文件为目录\n");
return -1;
}
cli->mode = st.st_mode;
// 2. 打开文件
int fd = open(cli->filename, O_RDONLY);
if (fd < 0)
{
perror("open");
return -1;
}
char buf[BUF_SIZE];
while (1)
{
memset(buf, 0, sizeof(buf));
// 3. 读取文件
int len = read(fd, buf, BUF_SIZE);
if (len < 0)
{
perror("read");
return -1;
}
else if (len == 0)
{
send(cli->sockfd, "exit", 4, 0);
break;
}
// 4. 发送文件
int ret = send(cli->sockfd, buf, sizeof(buf), 0);
if(ret < 0)
{
perror("send");
return -1;
}
}
close(fd);
printf("downloadfile success.\n");
return 0;
}
// 列出服务器目录下的文件名
int ftp_listfile(struct client *cli)
{
// 1. 打开目录
struct dirent *dir;
DIR *dirp = opendir(".");
if (dirp == NULL)
{
perror("opendir");
return -1;
}
// 2. 读取目录下的文件名
while (1)
{
dir = readdir(dirp);
if (dir == NULL)
{
break;
}
// 3. 发送文件名
if (dir->d_type == 8)
{
memset(cli->filename, 0, sizeof(cli->filename));
strcpy(cli->filename, dir->d_name);
send(cli->sockfd, cli->filename, sizeof(cli->filename), 0);
}
}
memset(cli->filename, 0, sizeof(cli->filename));
strcpy(cli->filename, "over");
send(cli->sockfd, cli->filename, sizeof(cli->filename), 0);
memset(cli->filename, 0, sizeof(cli->filename));
printf("list file success\n");
closedir(dirp);
return 0;
}
// 退出函数
void ftp_quit(struct client *cli)
{
return;
}
六、总结与扩展
注意事项:
send函数发送和recv函数接收时,传输的内容大小一定要对应,不然很容易出现发送多次只接收一次的情况(这与接收缓存区的刷新机制有关)。
另外,在读取和发送的文件内容为中文时,容易在读取数组的开始和结束处出现乱码,原因是文件编码格式不兼容导致的。目前我还没找到合适的解决办法,有知道解决方法的大佬可以发在评论区。
总结:
该项目不仅用到了网络编程的知识,还用到了目录IO的部分知识,综合性较强。此外,该项目需要的综合思维比较强,容易出现细小的bug,而且不易察觉。可以在项目开始编写前写出思路流程,能够大大减少细小错误的产生,同时,良好的编程习惯也是非常重要的。
可扩展部分:
客户端可以使用QT编写的图形化窗口实现功能,或者网页实现,使界面更加美观、操作更加便捷,用户体验更佳。
上传文件或下载文件时,默认创建的文件权限为0666,可以读取相应文件权限,使创建的文件权限与要上传或下载的文件权限相同。
可以扩展实现上传或下载文件夹功能。
可以扩展实现上传或下载其他目录下的文件。