前言
本篇文章带大家来完成一下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; // 使用 IPv4
seraddr.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)
{
// 初始化 Winsock
int 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文件传输功能,下篇文章我们继续优化代码,实现一些其他新的功能。