Linux网盘程序——客户端(完整注释版)
#include<cstdio>//C++标准库的头文件
#include<unistd.h>//Unix标准头文件
#include<arpa/inet.h>//通常用于处理IP地址和套接字地址的转换
#include<string.h>//字符串头文件
#include<stdlib.h>//包含了一些标准库函数,用于内存分配、释放以及其他一些通用的实用功能
#include<pthread.h>//线程相关,用于支持多线程编程
#include<sys/stat.h>//用于获取文件状态信息的函数和数据结构,例如文件的大小、权限
#include<sys/types.h>//包含了一些系统相关的数据类型和常量,例如文件描述符
#include<errno.h>//包含了错误码(errno)的定义,用于在程序中处理错误情况
#include<fcntl.h>//包含了文件控制操作相关的常量和函数,例如打开文件、关闭文件、读取文件等
#define MSG_TYPE_LOGIN 0//表示登录类型
#define MSG_TYPE_FILENAME 1//表示查询文件目录操作类型
#define MSG_TYPE_DOWNLOAD 2//表示下载文件操作类型
#define MSG_TYPE_UPLOAD 3//表示上传文件操作类型
#define MSG_TYPE_UPLOAD_DATA 4//表示上传文件数据类型
typedef struct msg
{
int type;//协议类型 0 表示登陆包 1.文件名传输包 2.文件下载包 ……
char fname[50];//存放文件名
char buffer[1024];//存放文件数据
int bytes;//这个字段用来记录传输文件时每个数据包实际的文件字节数
}MSG; //这个结构体会根据业务需求的不断变化,可能会增减新的字段。
char up_file_name[20] = { 0 };
int fd = -1; //这个是用来打开文件进行读写的文件描述符,默认情况下为0表示没有打开文件
//客户端的线程函数
//上传文件数据的线程函数
void* upload_file_thread(void* args)
{
// 客户端实现上传文件到服务器的逻辑思路
//1 首先要打开文件
MSG up_file_msg = { 0 }; //上传文件消息的结构体变量
char buffer[1024] = { 0 }; // 用来保存读取文件的数据缓冲区
int client_socket = *((int*)args); //定义套接字描述符接收形参
int res = 0; //实际读写字节数
int fd = -1; //文件描述符
fd = open("./download/css.txt", O_RDONLY); //打开文件, O_RDONLY表示只读
if (fd < 0)
{
perror("open up file error : ");
return NULL;
}
up_file_msg.type = MSG_TYPE_UPLOAD_DATA;//类型设置为上传数据文件,便于服务器识别
while ((res = read(fd, buffer, sizeof(buffer))) > 0)//从文件中读取数据,并将读取的数据存储到 buffer 中。读取的字节数保存在 res 变量中
{
// 要把文件数据内容拷贝到 MSG 结构体中的 buffer 中
memcpy(up_file_msg.buffer, buffer, res);//将从文件读取的数据复制到 up_file_msg.buffer 中
up_file_msg.bytes = res;
//下面的res是复用,和上面的res无关
res = write(client_socket, &up_file_msg, sizeof(MSG));// write 函数将 up_file_msg 结构体中的数据通过套接字发送给服务器
memset(buffer, 0, sizeof(buffer));//清缓存
memset(up_file_msg.buffer, 0, sizeof(up_file_msg.buffer));
}
close(fd);//关目录
}
//客户端的多线程的线程函数
void* thread_func(void* arg) {
int client_socket = *((int*)arg);//定义套接字描述符接收形参
MSG recv_msg = { 0 };//定义接收消息的结构体变量
int res;//实际读写字节数
//循环read不断接收从服务器端发来的数据
while (1) {
res = read(client_socket, &recv_msg, sizeof(MSG));
if (recv_msg.type == MSG_TYPE_FILENAME) {//服务器发过来的是包含文件名的数据报,就输出文件名
printf("server path filename=%s\n", recv_msg.fname);
memset(&recv_msg, 0, sizeof(MSG));
}
else if (recv_msg.type == MSG_TYPE_DOWNLOAD) ///服务器发过来的是包含文件的数据,就做好接收准备
{
//1.要确定下这个文件放在哪个目录下,我们可以调mkdir函数创建一个目录:默认东西都放在download1目录下
if (mkdir("download1", S_IRWXU) < 0)
{
if (errno == EEXIST)//如果目录已经存在就无视,否则输出报错信息
{
//printf("dir exist continue!\n");
}
else
{
perror("mkdir error");
}
}
//2.目录创建没问题之后,就要开始创建文件
if (fd == -1)//如果文件还没有打开过
{
//O_CREAT 表示如果文件不存在则创建
//O_WRONLY 表示文件将以写入方式打开,允许你写入文件的内容。
//0666 表示文件所有者、文件组和其他用户都有读取和写入权限。
fd = open("./download1/hello2", O_CREAT | O_WRONLY, 0666);//打开成功之后肯定会有个文件描述符返回
if (fd < 0)//表示创建/写入失败
{
perror("file open error:");
}
}
//通过上面的创建目录,以及文件描述符的判断通过后,就可以从MSG结构体里面的buffer取数据了
//将 recv_msg.buffer 中的数据写入由文件描述符 fd 标识的文件
//写入的字节数由 recv_msg.bytes 指定
res = write(fd, recv_msg.buffer, recv_msg.bytes);
if (res < 0)//表示写入失败
{
perror("file write error:");
}
//那么我们怎么判断文件内容都全部发完了呢?可以通过recv_msg.bytes是否小于recv_buffer的最大字节数1024
if (recv_msg.bytes < sizeof(recv_msg.buffer))
{
printf("file download finish!\n");
close(fd);
fd = -1;
}
}
}
}
void net_disk_ui()
{
printf("=========================TCP网盘程序=================================\n");
printf("=========================功能菜单=================================\n");
printf("\t\t\t1、查询文件\n");
printf("\t\t\t2、下载文件\n");
printf("\t\t\t3、上传文件\n");
printf("\t\t\t4、UI界面\n");
printf("\t\t\t0、退出系统\n");
printf("=====================================================================\n");
printf("请选择你要执行的操作: ");
}
int main() {
int client_socket;//创建客户端套接字描述符
pthread_t thread_id;
pthread_t thread_send_id;
int res;
char c;
MSG send_msg = { 0 };//定义发送给服务器消息的结构体变量
client_socket = socket(AF_INET, SOCK_STREAM, 0);//创建客户端套接字
if (client_socket < 0) {
perror("client socket failed:");
return 0;
}
struct sockaddr_in server_addr;// server_addr,用于存储套接字的地址信息
server_addr.sin_family = AF_INET;//AF_INET 表示IPv4地址族
server_addr.sin_addr.s_addr = inet_addr("192.168.43.128");//这里填Ubantu虚拟机的网卡IP地址,如果服务器和客户端在同一台机子上,则IP地址可以写成127.0.0.1
server_addr.sin_port = htons(6666);//端口号赋值
//创建好套接字之后,通过connect连接到服务器
if (connect(client_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect error!");
return 0;
}
printf("客户端连接服务器成功!\n");
//创建线程,支持多线程
pthread_create(&thread_id, NULL, thread_func, &client_socket);
net_disk_ui();//输出UI界面
while (1)
{
c = getchar();
switch (c) {//根据键盘输入来执行不同的操作
case '1':
//要让服务器给我们发送目录信息
//这个while循环本身也是死循环,那么我们要让客户端也创建线程,让接受服务器的数据的代码放到线程里面
send_msg.type = MSG_TYPE_FILENAME;//指定命令类型为查询文件目录类型
res=write(client_socket, &send_msg, sizeof(MSG));//把send_msg数据发送出去
if (res < 0) {//表示发送失败
perror("send msg error:");
}
memset(&send_msg, 0, sizeof(MSG));//清缓存
break;
case '2':
send_msg.type = MSG_TYPE_DOWNLOAD;//指定命令类型为下载文件操作类型
res = write(client_socket, &send_msg, sizeof(MSG));
if (res < 0)
{
perror("send msg error:");
}
memset(&send_msg, 0, sizeof(MSG));
break;
case '3':
send_msg.type = MSG_TYPE_UPLOAD;//指定命令类型为上传文件操作类型
strcpy(up_file_name, "css.txt");//上传文件的文件名
printf("input upload filename:");
puts(up_file_name);
//在上传文件给服务器之前,你要先发送一个数据包给服务器,告诉服务器我这边准备上传文件了
strcpy(send_msg.fname, up_file_name);
res = write(client_socket, &send_msg, sizeof(MSG));
if (res < 0)
{
perror("send upload package error:");
continue;
}
memset(&send_msg, 0, sizeof(MSG));
pthread_create(&thread_send_id, NULL, upload_file_thread, &client_socket);
break;
//由于考虑到上传文件是需要比较长的时间,考虑到如果文件很大,那么就需要非常长的时间,这个时候如果写
// //在这里那么久卡诺导致其他功能卡住排队等待,因此需要把发送文件内容的代码放进线程里面,因此需要创建线程
// //还需要创建一个新的线程,来专门处理文件上传任务
case '\n':
net_disk_ui();
break;
case '0':
return 0;
}
printf("按4即可显示UI界面\n");
}
return 0;
}