小目标6:下载文件功能的实现
指定文件的下载功能:客户端用户输入服务器目录中的文件名,服务器打开这个文件,读取文件的内容,发送给客户端
实现:服务器端打开某个文件并读取文件,然后把内容传给客户端
服务器端定义一个函数server_file_download
用于打开文件读取内容+传送给客户端,我们先定死一个文件
我们把MSG稍微修改了一下,增加了一个bytes属性,buffer扩容到1024,便于后续使用(客户端和服务器都要修改)
typedef struct msg
{
int type;//协议类型 0 表示登陆包 1.文件名传输包 2.文件下载包
int flag;
char buffer[1024];//存放除文件名之外的内容。
char fname[50];//如果type是1 就是文件名传输包,那fname里面就存放着文件名
int bytes;//这个字段用来记录传输文件时每个数据包实际的文件字节数
}MSG; //这个结构体会根据业务需求的不断变化,这个结构体会添加新的字段。
注意补上头文件和宏定义
#include<fcntl.h>
服务器里面定义一个下载文件的函数
实现内容:打开文件,读取内容,发送给客户端
//通过打开服务器中的某个文件,并使用socket网络发送给客户端
//我的文件路径是这样的:Ubantu的主文件夹/hello/hello2
//hello2是一个文本文件,不需要后缀
void server_file_download(int accept_socket)
{
MSG file_msg = { 0 };//表示文件内容的信息
int res = 0;
int fd; //文件描述符 linux下系统下重要概念,linux任务所有设备都是文件。对文件的打开,对设备的读写都可以使用文件描述符概念
fd = open("/home/liujiajun/hello/hello2", O_RDONLY);//这里的文件路径要结合自己的来修改
if (fd < 0)//表示文件打开失败,则返回失败的原因
{
perror("file open error:");
return;
}
file_msg.type = MSG_TYPE_DOWNLOAD;//设置消息类型为下载文件
strcpy(file_msg.fname, "hello2");//初始化文件名
//在读取文件并把文件传到客户端 这个时候,MSG结构中的buffer就是存放文件的内容,但是一般来说文件都超过1024字节,所以要发送多个包。而且这个MSG结构中
while ((res = read(fd, file_msg.buffer, sizeof(file_msg.buffer))) > 0) //当read用于读取文件的时候,当文件读到末尾之后,read将返回小于0的信息
{ //res就是实际读取文件的字节数
//file_msg.bytes也表示此次读的消息的字节数
file_msg.bytes = res;
res = write(accept_socket, &file_msg, sizeof(MSG));//把文件消息发送给服务器
if (res <= 0)
{
perror("server send file error:");
}
memset(file_msg.buffer, 0, sizeof(file_msg.buffer));//清缓存便于下一次读取
}
}
服务器这边在线程函数里面调用server_file_download
更新后的线程函数代码如下,注意看while循环里面的变化
前面要补上宏定义(客户端和服务器)
#define MSG_TYPE_DOWNLOAD 2
调用部分:
void* thread_fun(void* arg) {
int acpt_socket = *(int*)arg;
int res;
MSG recv_msg = { 0 };
char buffer[50] = { 0 };
printf("目录信息发送客户端完成!\n");
while (1) {
res = read(acpt_socket, &recv_msg, sizeof(MSG));
if (res == 0) {
printf("客户端已经断开\n");
break;
}
if (recv_msg.type == MSG_TYPE_FILENAME) {//说明客户端发过来的信息要的是目录
search_server_dir(acpt_socket);
memset(&recv_msg, 0, sizeof(MSG));
}
else if (recv_msg.type == MSG_TYPE_DOWNLOAD)
{
//printf("download函数未调用\n");
server_file_download(acpt_socket);
//printf("download函数已调用\n");
memset(&recv_msg, 0, sizeof(MSG));
}
memset(&recv_msg, 0, sizeof(MSG));
}
}
服务器这边写好之后,客户端的switch_case里面也要补上相应的代码,按2下载
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;
和1的差别仅仅在于send_msg.type = MSG_TYPE_DOWNLOAD;部分
客户端线程函数也要补上相应的内容,判断接收从服务器来的文件
注意要补上相关的头文件
#include<sys/stat.h>
#include<sys/types.h>
#include<errno.h>
#include<fcntl.h>
头文件的解释(ChatGPT):
-
<sys/stat.h>
:-
这个头文件包含了与文件状态相关的常量和结构体的定义,例如文件的大小、权限、时间戳等信息。
-
常用的结构体包括
struct stat
,它用于存储文件的各种属性信息。
-
-
<sys/types.h>
:-
这个头文件包含了一些基本的系统数据类型的定义,例如整数类型、字符类型和一些系统调用相关的特殊类型。
-
一些常见的类型包括
size_t
、pid_t
、off_t
等。
-
-
<errno.h>
:-
这个头文件包含了一组宏和变量,用于处理错误条件。当系统调用或库函数发生错误时,它们通常会设置一个特定的错误码,程序可以检查这个错误码以确定错误的原因。
-
常见的宏包括
errno
(当前错误码的变量)、perror
(将错误信息打印到标准错误输出)以及一系列的错误码宏,如EIO
、EINVAL
等。
-
-
<fcntl.h>
:-
这个头文件包含了文件控制操作的常量定义,用于在打开、创建、关闭文件时设置文件属性和行为。
-
常见的常量包括
O_RDONLY
(只读模式)、O_WRONLY
(只写模式)、O_RDWR
(读写模式)以及各种文件标志(如O_CREAT
、O_TRUNC
等)。
-
并且补上相关的代码,下面是客户端的线程函数修改后的代码(具体看注释)
int fd = -1; //这个是用来打开文件进行读写的文件描述符,默认情况下为0表示没有打开文件
//客户端的线程函数
void* thread_func(void* arg) {
int client_socket = *((int*)arg);
MSG recv_msg = { 0 };
int res;
char pwd[100] = { 0 };
//接受服务器端发来的数据
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函数 download目录
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表示写权限
fd = open("./download1/hello2", O_CREAT | O_WRONLY, 0666);//打开成功之后肯定会有个文件描述符返回
if (fd < 0)//表示创建/写失败
{
perror("file open error:");
}
}
//通过上面的创建目录,以及文件描述符的判断通过后,就可以从MSG结构体里面的buffer取数据了
//recv_msg.buffer存放的就是文件的部分内容,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;
}
}
}
}
完整代码
服务器:
#include<cstdio>//C++标准库的头文件
#include<unistd.h>//Unix标准头文件
#include<sys/types.h>//这个头文件定义了各种系统相关的数据类型
#include<sys/socket.h>//这个头文件用于网络编程,包含了与套接字(socket)相关的函数和数据结构的声明
#include<arpa/inet.h>//通常用于处理IP地址和套接字地址的转换
#include<string.h>//字符串头文件,因为后面有用到memset
#include<pthread.h>//线程相关
#include<stdlib.h>
#include<dirent.h>
#include<fcntl.h>
#define MSG_TYPE_LOGIN 0
#define MSG_TYPE_FILENAME 1
#define MSG_TYPE_DOWNLOAD 2
typedef struct msg
{
int type;//协议类型 0 表示登陆包 1.文件名传输包 2.文件下载包
int flag;
char buffer[1024];//存放除文件名之外的内容。
char fname[50];//如果type是1 就是文件名传输包,那fname里面就存放着文件名
int bytes;//这个字段用来记录传输文件时每个数据包实际的文件字节数
}MSG; //这个结构体会根据业务需求的不断变化,这个结构体会添加新的字段。
//根据网盘客户端的业务需求,客户端想要查看下服务器这边目录下的文件信息
//因此服务器必须设置一个功能,把某个目录下的文件名信息全部获取出来发给客户端
//默认情况下服务器的目录我们默认设置为/home
void search_server_dir(int accept_socket)//因为函数里面调用了write函数,需要用到套接字
{
//opendir是打开Linux目录的api函数
struct dirent* dir = NULL;
int res = 0;//存储实际发送信息的字节数
MSG info_msg = { 0 };//定义信息的结构体并且初始化
info_msg.type = MSG_TYPE_FILENAME;//设置文件类型
DIR* dp = opendir("/home/liujiajun");
if (NULL == dp)
{
perror("open dir error:");
return;
}
while (1)
{
dir = readdir(dp);
if (NULL == dir) //如果readdir函数返回是空值,全部目录读取完成
{
break;
}
if (dir->d_name[0] != '.')//把.隐藏文件过滤掉
{
// 清空 info_msg.fname,并将当前文件名拷贝到 info_msg.fname 中
memset(info_msg.fname, 0, sizeof(info_msg.fname));//每一次输出之后都要把存放文件名的空间重新刷新
strcpy(info_msg.fname, dir->d_name);//把每一个文件名拷贝到info_msg结构体里面,通过socket发送出去
// 使用 write 函数将 info_msg 结构体发送给客户端
res = write(accept_socket, &info_msg, sizeof(MSG));//把信息发送给客户端
if (res < 0) {
perror("send client error:");
return;
}
}
}
}
//通过打开服务器中的某个文件,并使用socket网络发送给客户端,至于说明文件我们先定一个zhuizhui.txt,那么可以根据实际情况
void server_file_download(int accept_socket)
{
MSG file_msg = { 0 };//表示文件内容的信息
int res = 0;
int fd; //文件描述符 linux下系统下重要概念,linux任务所有设备都是文件。对文件的打开,对设备的读写都可以使用文件描述符概念
fd = open("/home/liujiajun/hello/hello2", O_RDONLY);//这里的文件路径要结合自己的来修改
if (fd < 0)//表示文件打开失败,则返回失败的原因
{
perror("file open error:");
return;
}
file_msg.type = MSG_TYPE_DOWNLOAD;//设置消息类型为下载文件
strcpy(file_msg.fname, "hello2");//初始化文件名
//在读取文件并把文件传到客户端 这个时候,MSG结构中的buffer就是存放文件的内容,但是一般来说文件都超过1024字节,所以要发送多个包。而且这个MSG结构中
while ((res = read(fd, file_msg.buffer, sizeof(file_msg.buffer))) > 0) //当read用于读取文件的时候,当文件读到末尾之后,read将返回小于0的信息
{ //res就是实际读取文件的字节数
//file_msg.bytes也表示此次读的消息的字节数
file_msg.bytes = res;
res = write(accept_socket, &file_msg, sizeof(MSG));//把文件消息发送给服务器
if (res <= 0)
{
perror("server send file error:");
}
memset(file_msg.buffer, 0, sizeof(file_msg.buffer));//清缓存便于下一次读取
}
}
void* thread_fun(void* arg) {
int acpt_socket = *(int*)arg;
int res;
MSG recv_msg = { 0 };
char buffer[50] = { 0 };
printf("目录信息发送客户端完成!\n");
while (1) {
res = read(acpt_socket, &recv_msg, sizeof(MSG));
if (res == 0) {
printf("客户端已经断开\n");
break;
}
if (recv_msg.type == MSG_TYPE_FILENAME) {//说明客户端发过来的信息要的是目录
search_server_dir(acpt_socket);
memset(&recv_msg, 0, sizeof(MSG));
}
else if (recv_msg.type == MSG_TYPE_DOWNLOAD)
{
//printf("download函数未调用\n");
server_file_download(acpt_socket);
//printf("download函数已调用\n");
memset(&recv_msg, 0, sizeof(MSG));
}
memset(&recv_msg, 0, sizeof(MSG));
}
}
int main() {
int server_socket;//这是一个唯一标识套接字的整数
int accept_socket;//创建一个存储接受到的客户端连接的套接字文件描述符。
int res = 0;//后续用到
MSG recv_msg = { 0 };
pthread_t thread_id;//线程编号
char buffer[50] = { 0 };//定义缓冲区,用于暂时存储接收和发送的数据
//第一步创建套接字描述符
printf("开始创建TCP服务器\n");
server_socket = socket(AF_INET, SOCK_STREAM, 0);
/*
创建了一个套接字,并将其文件描述符存储在 server_socket 变量中
AF_INET 表示IPv4地址族
SOCK_STREAM: 这是套接字类型,表示创建的套接字将使用面向连接的TCP协议
0: 这是套接字的协议参数,通常设置为0
*/
if (server_socket < 0) {
perror("socket create failed:");
return 0;
}
struct sockaddr_in server_addr;//存储套接字信息的变量
server_addr.sin_family = AF_INET;//指定了地址族为 AF_INET
server_addr.sin_addr.s_addr = INADDR_ANY;//表示服务器将接受来自任何可用网络接口的连接请求
server_addr.sin_port = htons(6666);//端口号不可以直接用数字赋值,htons将主机字节序(通常是小端字节序)的端口号转换为网络字节序(大端字节序)
//如果运行时出现了报错Address already in use如何解决
int optvalue = 1;
setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &optvalue, sizeof(optvalue));
if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("server bind error:");
return 0;
}
if (listen(server_socket, 10) < 0) {
perror("server listen error:");
return 0;
}
printf("TCP服务器准备完成,等待客户端的连接\n");
while (1) //服务器将持续接收和发送数据,直到手动停止程序。
{
accept_socket = accept(server_socket, NULL, NULL);
printf("有客户端连接到服务器!\n");
//创建一个线程
pthread_create(&thread_id, NULL, thread_fun, &accept_socket);
}
return 0;
}
客户端
#include<cstdio>
#include<unistd.h>
#include<arpa/inet.h>
#include<string.h>
#include<stdlib.h>
#include<pthread.h>//线程相关
#include<sys/stat.h>
#include<sys/types.h>
#include<errno.h>
#include<fcntl.h>
#define MSG_TYPE_LOGIN 0
#define MSG_TYPE_FILENAME 1
#define MSG_TYPE_DOWNLOAD 2
typedef struct msg
{
int type;//协议类型 0 表示登陆包 1.文件名传输包 2.文件下载包
int flag;
char buffer[1024];//存放除文件名之外的内容。
char fname[50];//如果type是1 就是文件名传输包,那fname里面就存放着文件名
int bytes;//这个字段用来记录传输文件时每个数据包实际的文件字节数
}MSG; //这个结构体会根据业务需求的不断变化,这个结构体会添加新的字段。
int fd = -1; //这个是用来打开文件进行读写的文件描述符,默认情况下为0表示没有打开文件
//客户端的线程函数
void* thread_func(void* arg) {
int client_socket = *((int*)arg);
MSG recv_msg = { 0 };
int res;
char pwd[100] = { 0 };
//接受服务器端发来的数据
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函数 download目录
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表示写权限
fd = open("./download1/hello2", O_CREAT | O_WRONLY, 0666);//打开成功之后肯定会有个文件描述符返回
if (fd < 0)//表示创建/写失败
{
perror("file open error:");
}
}
//通过上面的创建目录,以及文件描述符的判断通过后,就可以从MSG结构体里面的buffer取数据了
//recv_msg.buffer存放的就是文件的部分内容,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\t0、退出系统\n");
printf("=====================================================================\n");
printf("请选择你要执行的操作: ");
}
int main() {
int client_socket;
pthread_t thread_id;
int res;
char c;
char buffer[50] = { 0 };//创建缓冲区,后面会用到
struct sockaddr_in server_addr;// server_addr,用于存储套接字的地址信息。
MSG send_msg = { 0 };//定义发送给服务器消息的结构体,表示要执行哪一个命令的消息
//MSG recv_msg = { 0 };/*定义接受的结构体*/
client_socket = socket(AF_INET, SOCK_STREAM, 0);
if (client_socket < 0) {
perror("client socket failed:");
return 0;
}
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();
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':
break;
case '0':
return 0;
}
sleep(1);//延迟一会儿再输出UI界面,这样可以让UI界面在输出目录之后执行
net_disk_ui();
}
return 0;
}
运行结果
服务器:
客户端:
最关键的是在Ubantu的client运行的目录下确实多了一个download1文件夹和hello2的文件