前言

本篇文章带大家来完成一下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;
}


代码思路:

这段代码的实现思路可以分为几个主要部分:

  1. 初始化和关闭套接字:

    • initSocket(void):设置服务器套接字,通常包括创建套接字、绑定到指定端口、设置监听等步骤。
    • closeSocket(void):关闭套接字,释放资源,结束网络通信。
  2. 监听客户端连接:

    • listenToClient(void):使服务器开始监听传入的客户端连接请求,并将请求排入队列。
  3. 处理客户端消息:

    • processMsg(SOCKET clientfd):处理客户端发送的消息,包括接收数据、解析消息头和内容,并根据 msgID 执行相应的操作(例如接收文件名、文件大小,发送文件数据等)。
  4. 消息定义和数据结构:

    • 使用 enum MSGTAG 定义消息标识符,区分不同的消息类型。
    • MsgHeader 结构体封装了消息头和消息体,其中包括文件信息和数据包内容,利用 union 来处理不同消息类型的具体数据。
  5. 网络协议设计:

    • 通过 MsgHeader 结构体定义消息格式,确保客户端和服务器之间的数据传输具有一致的结构,避免因数据布局不同而产生的问题。
  6. 内存对齐:

    • 使用 #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文件传输助手还是有一些缺陷的,他无法传输比较大的文件,那么在下篇文章我们来优化一下这个问题吧。
C语言FTP文件传输(完成基本文件传输的功能)-LMLPHP

总结

本篇文章主要实现了基本的FTP文件传输功能,下篇文章我们继续优化代码,实现一些其他新的功能。

08-11 10:39