文章目录
- 前言
- 一、实现思路
- 二、实现FTP服务器
- 三、实现FTP客户端
- 四、实现体验
- 总结
前言
本篇文章带大家来完成一下C语言FTP文件传输助手最基础的功能,也就是客户端和服务器之间进行最基础的文件传输的功能。
一、实现思路
实现一个基本的 FTP 客户端和服务器,可以按照以下思路进行:
1.客户端首先请求下载文件,并将文件名发送到服务器。
2.服务器收到文件名后,找到对应的文件,并将文件大小发送回客户端。
3.客户端接收到文件大小后,准备接收数据(如分配内存),并通知服务器可以开始发送数据。
4.服务器收到开始接收数据的指令后,开始发送文件数据。
5.客户端接收数据并保存,完成后通知服务器数据接收完毕。
6.最后,双方关闭连接,结束文件传输。
二、实现FTP服务器
创建FTPServer.c和FTPServer.h来管理服务器代码:
FTPServer.c:
#include "FTPServer.h"
#include <stdio.h>// 定义全局变量和库
char g_recvbuf[1024] = { 0 }; // 用于接收来自客户端的数据缓冲区
int g_filesize = 0; // 存储文件的大小
#pragma comment(lib, "Ws2_32.lib") // 链接 Winsock 库SOCKET sockfd; // 套接字描述符char* g_filebuf; // 用于存储文件内容的内存空间// 初始化 Winsock 库
bool initSocket(void)
{int iResult;WSADATA wsaData;// 调用 WSAStartup 函数以初始化 Winsock 库iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);if (iResult != 0) {printf("WSAStartup failed: %d\n", iResult);return false;}return true;
}// 关闭套接字和清理 Winsock
bool closeSocket(void)
{closesocket(sockfd); // 关闭套接字WSACleanup(); // 清理 Winsock
}// 监听客户端的请求
void listenToClient(void)
{// 1. 创建套接字sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (INVALID_SOCKET == sockfd){printf("socket failed: %ld\n", WSAGetLastError());WSACleanup();return;}// 2. 绑定 IP 地址和端口号struct sockaddr_in seraddr;seraddr.sin_family = AF_INET; // 使用 IPv4seraddr.sin_addr.S_un.S_addr = ADDR_ANY; // 监听所有网络接口seraddr.sin_port = htons(SERPORT); // 端口号,SERPORT 需要在代码中定义if (0 != bind(sockfd, (struct sockaddr*)&seraddr, sizeof(seraddr))){printf("bind failed: %ld\n", WSAGetLastError());WSACleanup();return;}// 3. 监听端口if (0 != listen(sockfd, LISTEN_NUM)) // LISTEN_NUM 是最大挂起连接数{printf("listen failed: %ld\n", WSAGetLastError());WSACleanup();return;}// 4. 等待连接struct sockaddr_in clientaddr;int len = sizeof(clientaddr);SOCKET clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);if (INVALID_SOCKET == clientfd){printf("accept failed: %ld\n", WSAGetLastError());WSACleanup();return;}printf("connect is ok\n");// 处理客户端消息while (processMsg(clientfd)){Sleep(100); // 休眠 100 毫秒}
}// 读取文件内容
bool readFile(SOCKET clientfd, MSGHEADER* pmsg)
{// 打开指定的文件FILE* fp = fopen(pmsg->fileinfo.fileName, "rb");if (NULL == fp){printf("open %s is err\n", pmsg->fileinfo.fileName);// 文件打开失败,将错误消息发送给客户端MSGHEADER msg;msg.msgID = MSG_OPENFILE_FALID; // 错误消息标识符if (SOCKET_ERROR == send(clientfd, (const char*)&msg, sizeof(msg), 0)){printf("send is err\n");}return false;}// 计算文件大小fseek(fp, 0, SEEK_END);g_filesize = ftell(fp);fseek(fp, 0, SEEK_SET);// 发送文件大小和文件名MSGHEADER msg;msg.msgID = MSG_FILESIZE; // 文件大小消息标识符msg.fileinfo.filesize = g_filesize;// 处理文件名,确保没有路径,只保留文件名char tfname[200] = { 0 }, text[100];_splitpath(pmsg->fileinfo.fileName, NULL, NULL, tfname, text);strcat(tfname, text);strcpy(msg.fileinfo.fileName, tfname);if (SOCKET_ERROR == send(clientfd, (const char*)&msg, sizeof(msg), 0)){printf("send is err\n");}// 分配内存并读取文件内容g_filebuf = calloc(g_filesize + 1, sizeof(char)); // +1 为了存储结束符if (NULL == g_filebuf){printf("申请内存失败\n");return false;}fread(g_filebuf, sizeof(char), g_filesize, fp);fclose(fp);return true;
}// 发送文件内容
void SendFile(SOCKET clientfd, MSGHEADER* pmsg)
{MSGHEADER msg;msg.msgID = MSG_READY_READ; // 文件准备好消息标识符msg.packet.filesize = g_filesize;memcpy(msg.packet.payload, g_filebuf, g_filesize); // 复制文件内容到消息中printf("server start send file\n");send(clientfd, (const char*)&msg, sizeof(msg), 0);printf("server send file end\n");return true;
}// 处理客户端消息
bool processMsg(SOCKET clientfd)
{int len = 0;// 接收来自客户端的消息memset(g_recvbuf, 0, sizeof(g_recvbuf)); // 清空接收缓冲区len = recv(clientfd, g_recvbuf, 1024, 0);if (len <= 0){printf("客户端下线: %ld\n", WSAGetLastError());return false;}// 将接收到的消息转换为 MSGHEADER 类型MSGHEADER* recvMSG = (MSGHEADER*)g_recvbuf;switch (recvMSG->msgID){case MSG_FILENAME:{readFile(clientfd, recvMSG); // 读取文件}break;case MSG_SENDFILE:{printf("MSG_SENDFILE\n");SendFile(clientfd, recvMSG); // 发送文件}break;case MSG_SUCCESSED:{printf("MSG_SUCCESSED\n"); // 文件接收成功}break;default:break;}return true;
}
FTPServer.h:
#pragma once#include <stdbool.h>
#include <winsock2.h>
#include <ws2tcpip.h>// 定义服务端口号
#define SERPORT 8080
// 定义最大监听队列长度
#define LISTEN_NUM 10// 初始化套接字
bool initSocket(void);// 关闭套接字
bool closeSocket(void);// 监听客户端连接
void listenToClient(void);// 处理客户端消息
bool processMsg(SOCKET clientfd);// 消息标记枚举
enum MSGTAG
{MSG_FILENAME = 1, // 文件名MSG_FILESIZE, // 文件大小MSG_READY_READ, // 准备接收MSG_SENDFILE, // 发送MSG_SUCCESSED, // 传输完成MSG_OPENFILE_FALID // 告诉客户端文件找不到
};#pragma pack(1) // 取消结构体的内存对齐// 消息头结构体
typedef struct MsgHeader
{enum MSGTAG msgID; // 当前消息标记union MyUnion{// 文件信息结构struct{int filesize; // 文件大小char fileName[256]; // 文件名}fileinfo;// 文件数据包结构struct{int filesize; // 文件大小char payload[1024 - sizeof(int) * 2]; // 文件内容}packet;};}MSGHEADER;#pragma pack() // 恢复默认对齐方式
main.c:
#include <stdio.h>
#include "FTPServer.h"int main(void)
{initSocket();listenToClient();closeSocket();return 0;
}
代码思路:
这段代码的实现思路可以分为几个主要部分:
-
初始化和关闭套接字:
initSocket(void)
:设置服务器套接字,通常包括创建套接字、绑定到指定端口、设置监听等步骤。closeSocket(void)
:关闭套接字,释放资源,结束网络通信。
-
监听客户端连接:
listenToClient(void)
:使服务器开始监听传入的客户端连接请求,并将请求排入队列。
-
处理客户端消息:
processMsg(SOCKET clientfd)
:处理客户端发送的消息,包括接收数据、解析消息头和内容,并根据msgID
执行相应的操作(例如接收文件名、文件大小,发送文件数据等)。
-
消息定义和数据结构:
- 使用
enum MSGTAG
定义消息标识符,区分不同的消息类型。 MsgHeader
结构体封装了消息头和消息体,其中包括文件信息和数据包内容,利用union
来处理不同消息类型的具体数据。
- 使用
-
网络协议设计:
- 通过
MsgHeader
结构体定义消息格式,确保客户端和服务器之间的数据传输具有一致的结构,避免因数据布局不同而产生的问题。
- 通过
-
内存对齐:
- 使用
#pragma pack(1)
确保结构体在内存中的布局与网络传输中的布局一致,防止因内存对齐产生的额外填充字节影响数据的解析。
- 使用
三、实现FTP客户端
FTPClient.c:
#include "FTPClient.h" // 包含自定义的头文件
#include <stdio.h>
#include <string.h>
#include <malloc.h>#pragma comment(lib, "Ws2_32.lib") // 链接 Winsock 库SOCKET sockfd; // 套接字描述符char g_recvbuf[1024]; // 用于接收从服务器发来的消息的缓冲区int g_sizefile = 0; // 文件总大小
char* g_filebuf; // 用于存储文件内容的内存空间char g_filename[256]; // 文件名// 发送文件名给服务端
void downloadFileName(SOCKET serverfd)
{char filename[1024] = { 0 };scanf("%s", filename); // 获取用户输入的文件名MSGHEADER file;file.msgID = MSG_FILENAME;strcpy(file.fileinfo.fileName, filename); // 将文件名拷贝到结构体中// 将文件名发送给服务器send(serverfd, (const char*)&file, sizeof(file), 0);
}// 准备接收来自服务器的文件
void readyread(SOCKET serverfd, MSGHEADER* pmsg)
{// 分配内存空间以存储文件g_sizefile = pmsg->fileinfo.filesize;g_filebuf = calloc(g_sizefile + 1, sizeof(char));if (g_filebuf == NULL){printf("申请内存空间失败\n");}else{MSGHEADER msg;msg.msgID = MSG_SENDFILE;send(serverfd, (const char*)&msg, sizeof(msg), 0); // 通知服务器可以发送文件}strcpy(g_filename, pmsg->fileinfo.fileName); // 保存文件名printf("pmsg->name :%s pmsg->size : %d\n", pmsg->fileinfo.fileName, pmsg->fileinfo.filesize);
}// 将文件内容写入新文件中
void writefile(SOCKET serverfd, MSGHEADER* pmsg)
{int filesize = pmsg->packet.filesize; // 获取文件大小printf("filesize : %d g_filename : %s\n", filesize, g_filename);// 打开文件以进行写入FILE* pf = fopen(g_filename, "wb");if (NULL == pf){printf("打开文件失败\n");return;}// 写入文件内容fwrite(pmsg->packet.payload, sizeof(char), filesize, pf);fclose(pf); // 关闭文件// 提示服务器文件接收成功MSGHEADER msg;msg.msgID = MSG_SUCCESSED;send(serverfd, (const char*)&msg, sizeof(msg), 0);
}bool initSocket(void)
{// 初始化 Winsockint iResult;WSADATA wsaData;iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);if (iResult != 0){printf("WSAStartup failed: %d\n", iResult);return false;}return true;
}bool closeSocket(void)
{closesocket(sockfd); // 关闭套接字WSACleanup(); // 清理 Winsock
}void ConnectToHost(void)
{// 创建套接字sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (INVALID_SOCKET == sockfd){printf("socket failed: %ld\n", WSAGetLastError());WSACleanup();return;}// 绑定 IP 和端口号并连接服务器struct sockaddr_in seraddr;seraddr.sin_family = AF_INET;inet_pton(AF_INET, "192.168.199.1", &seraddr.sin_addr); // 服务器 IP 地址seraddr.sin_port = htons(SERPORT); // 服务器端口号if (0 != connect(sockfd, (struct sockaddr*)&seraddr, sizeof(seraddr))){printf("connect failed: %ld\n", WSAGetLastError());WSACleanup();return;}// 发送文件名给服务端downloadFileName(sockfd);// 处理从服务器接收到的消息while (processMsg(sockfd)){Sleep(100); // 每 100 毫秒检查一次}
}bool processMsg(SOCKET serverfd)
{// 接收来自服务端的消息recv(serverfd, g_recvbuf, 1024, 0);MSGHEADER* pmsg = (MSGHEADER*)&g_recvbuf;// 处理接收到的消息switch (pmsg->msgID){case MSG_OPENFILE_FALID:{// 文件未找到,重新发送文件名downloadFileName(serverfd);}break;case MSG_FILESIZE:{readyread(serverfd, pmsg); // 准备接收文件}break;case MSG_READY_READ:{printf("MSG_READY_READ\n");writefile(serverfd, pmsg); // 将文件写入本地}break;default:break;}return true;
}
FTPClient.h:
#pragma once#include <stdbool.h> // 为布尔类型定义
#include <winsock2.h> // Windows套接字库头文件
#include <ws2tcpip.h> // 提供IP协议族的套接字操作函数// 定义服务器端口号
#define SERPORT 8080// 定义监听队列的最大连接数
#define LISTEN_NUM 10// 初始化套接字函数的声明
bool initSocket(void);// 关闭套接字函数的声明
bool closeSocket(void);// 连接到主机的函数声明
void ConnectToHost(void);// 处理客户端消息的函数声明
bool processMsg(SOCKET clientfd);// 消息类型标记的枚举定义
enum MSGTAG
{MSG_FILENAME = 1, // 文件名MSG_FILESIZE, // 文件大小MSG_READY_READ, // 准备接收MSG_SENDFILE, // 发送文件MSG_SUCCESSED, // 传输完成MSG_OPENFILE_FALID // 文件打开失败
};// 消息头结构体定义
typedef struct MsgHeader
{enum MSGTAG msgID; // 当前消息标记,用于标识消息的类型// 联合体,用于存储不同类型的消息内容union MyUnion{// 文件信息结构体struct{int filesize; // 文件大小char fileName[256]; // 文件名} fileinfo;// 文件传输数据包结构体struct{int filesize; // 文件大小char payload[1024 - sizeof(int) * 2]; // 文件内容(1024字节中减去两个int类型的空间)} packet;};} MSGHEADER;
main.c:
#include <stdio.h>
#include "FTPClient.h"int main(void)
{initSocket();ConnectToHost();closeSocket();return 0;
}
实现思路:
1.初始化Socket:通过initSocket函数初始化Winsock库。
2.建立连接:ConnectToHost函数创建一个TCP套接字,连接到指定的服务器IP和端口。
3.发送文件名:downloadFileName函数获取用户输入的文件名并发送给服务器。
4.处理消息:processMsg函数接收来自服务器的消息,并根据消息类型执行不同的操作。
5.准备接收文件:readyread函数分配内存以存储文件,并发送准备接收文件的确认消息。
6.写入文件:writefile函数将接收到的文件数据写入本地文件中,并通知服务器文件接收成功。
7.关闭Socket:closeSocket函数关闭套接字并清理Winsock库。
四、实现体验
能够进行正常的文件传输,但是我们这个FTP文件传输助手还是有一些缺陷的,他无法传输比较大的文件,那么在下篇文章我们来优化一下这个问题吧。
总结
本篇文章主要实现了基本的FTP文件传输功能,下篇文章我们继续优化代码,实现一些其他新的功能。