一、FTP 协议概述
文件传输协议(File Transfer Protocol, FTP)是用于在网络上进行文件传输的标准协议,它基于客户端 - 服务器模型,使用 TCP 作为传输层协议,默认通过 20 和 21 两个端口进行通信。其中,端口 21 用于控制连接(发送命令和接收响应),端口 20 用于数据传输(上传或下载文件)。
FTP 协议的主要特点包括:
-
命令与响应机制:客户端通过发送标准命令(如 USER、PASS、LIST、RETR、STOR 等)与服务器交互,服务器返回三位数字的状态码及描述信息。
-
两种工作模式:
- 主动模式(Active Mode):服务器主动连接客户端的数据端口(通常是 20)。
- 被动模式(Passive Mode):服务器开放一个临时端口,客户端主动连接该端口进行数据传输,适合在防火墙环境下使用。
-
文件传输类型:支持 ASCII 文本模式和二进制模式,确保不同系统间文件格式的兼容性。
-
用户认证:通常需要用户名和密码进行身份验证,也支持匿名访问(使用 anonymous 作为用户名)。
二、FTP 协议的工作流程
FTP 会话的基本工作流程如下:
-
建立控制连接:客户端连接到服务器的 21 端口,进行初始握手和协议协商。
-
用户认证:客户端发送 USER 和 PASS 命令进行身份验证。
-
命令交互:客户端发送各种命令(如改变目录 CWD、列出文件 LIST 等),服务器返回响应。
-
数据传输:当需要上传或下载文件时,客户端和服务器建立数据连接(主动或被动模式),完成文件传输。
-
关闭连接:会话结束后,关闭数据连接和控制连接。
三、C 语言实现 FTP 客户端
下面我们用 C 语言实现一个简单的 FTP 客户端,支持基本的文件传输功能。这个示例将展示如何使用 socket 编程实现 FTP 协议的核心功能。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
#define FTP_CONTROL_PORT 21
#define BUFFER_SIZE 1024
#define COMMAND_SIZE 256
// 全局变量
int control_socket;
char response[BUFFER_SIZE];
// 函数声明
int connect_to_server(const char *host, int port);
void send_command(const char *command);
int receive_response();
void login(const char *user, const char *pass);
void list_files();
void download_file(const char *filename);
void upload_file(const char *filename);
void disconnect();
// 连接到FTP服务器
int connect_to_server(const char *host, int port) {
struct hostent *server;
struct sockaddr_in server_addr;
// 创建套接字
if ((control_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket creation error");
return -1;
}
// 获取主机信息
if ((server = gethostbyname(host)) == NULL) {
perror("host not found");
close(control_socket);
return -1;
}
// 设置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
memcpy(&server_addr.sin_addr.s_addr, server->h_addr, server->h_length);
server_addr.sin_port = htons(port);
// 连接服务器
if (connect(control_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("connection failed");
close(control_socket);
return -1;
}
// 接收欢迎信息
if (receive_response() != 220) {
fprintf(stderr, "Unexpected response from server\n");
close(control_socket);
return -1;
}
printf("Connected to %s:%d\n", host, port);
return 0;
}
// 发送命令到服务器
void send_command(const char *command) {
printf("Sending: %s\n", command);
if (send(control_socket, command, strlen(command), 0) < 0) {
perror("send failed");
}
}
// 接收服务器响应
int receive_response() {
memset(response, 0, BUFFER_SIZE);
ssize_t bytes_received = recv(control_socket, response, BUFFER_SIZE - 1, 0);
if (bytes_received < 0) {
perror("receive failed");
return -1;
}
printf("Server response: %s", response);
// 提取状态码
int status_code = 0;
if (sscanf(response, "%d", &status_code) != 1) {
fprintf(stderr, "Failed to parse status code\n");
return -1;
}
return status_code;
}
// 用户登录
void login(const char *user, const char *pass) {
char command[COMMAND_SIZE];
// 发送用户名
snprintf(command, COMMAND_SIZE, "USER %s\r\n", user);
send_command(command);
if (receive_response() != 331) {
fprintf(stderr, "Username not accepted\n");
return;
}
// 发送密码
snprintf(command, COMMAND_SIZE, "PASS %s\r\n", pass);
send_command(command);
if (receive_response() != 230) {
fprintf(stderr, "Password not accepted\n");
return;
}
printf("Login successful\n");
}
// 列出远程目录文件
void list_files() {
char command[COMMAND_SIZE];
int data_socket;
struct sockaddr_in data_addr;
socklen_t data_addr_len = sizeof(data_addr);
// 进入被动模式
send_command("PASV\r\n");
if (receive_response() != 227) {
fprintf(stderr, "Failed to enter passive mode\n");
return;
}
// 解析被动模式返回的IP和端口
int h1, h2, h3, h4, p1, p2;
char *p = strstr(response, "(");
if (p == NULL || sscanf(p, "(%d,%d,%d,%d,%d,%d)", &h1, &h2, &h3, &h4, &p1, &p2) != 6) {
fprintf(stderr, "Failed to parse passive mode response\n");
return;
}
// 计算数据端口
int data_port = p1 * 256 + p2;
// 创建数据套接字并连接
if ((data_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("data socket creation error");
return;
}
memset(&data_addr, 0, sizeof(data_addr));
data_addr.sin_family = AF_INET;
data_addr.sin_addr.s_addr = htonl((h1 << 24) | (h2 << 16) | (h3 << 8) | h4);
data_addr.sin_port = htons(data_port);
if (connect(data_socket, (struct sockaddr *)&data_addr, sizeof(data_addr)) < 0) {
perror("data connection failed");
close(data_socket);
return;
}
// 发送LIST命令
send_command("LIST\r\n");
if (receive_response() != 150) {
fprintf(stderr, "Failed to start listing files\n");
close(data_socket);
return;
}
// 接收文件列表
char buffer[BUFFER_SIZE];
ssize_t bytes;
while ((bytes = recv(data_socket, buffer, BUFFER_SIZE - 1, 0)) > 0) {
buffer[bytes] = '\0';
printf("%s", buffer);
}
close(data_socket);
// 接收完成响应
receive_response();
}
// 下载文件
void download_file(const char *filename) {
char command[COMMAND_SIZE];
int data_socket;
struct sockaddr_in data_addr;
socklen_t data_addr_len = sizeof(data_addr);
// 进入被动模式
send_command("PASV\r\n");
if (receive_response() != 227) {
fprintf(stderr, "Failed to enter passive mode\n");
return;
}
// 解析被动模式返回的IP和端口
int h1, h2, h3, h4, p1, p2;
char *p = strstr(response, "(");
if (p == NULL || sscanf(p, "(%d,%d,%d,%d,%d,%d)", &h1, &h2, &h3, &h4, &p1, &p2) != 6) {
fprintf(stderr, "Failed to parse passive mode response\n");
return;
}
// 计算数据端口
int data_port = p1 * 256 + p2;
// 创建数据套接字并连接
if ((data_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("data socket creation error");
return;
}
memset(&data_addr, 0, sizeof(data_addr));
data_addr.sin_family = AF_INET;
data_addr.sin_addr.s_addr = htonl((h1 << 24) | (h2 << 16) | (h3 << 8) | h4);
data_addr.sin_port = htons(data_port);
if (connect(data_socket, (struct sockaddr *)&data_addr, sizeof(data_addr)) < 0) {
perror("data connection failed");
close(data_socket);
return;
}
// 发送RETR命令
snprintf(command, COMMAND_SIZE, "RETR %s\r\n", filename);
send_command(command);
if (receive_response() != 150) {
fprintf(stderr, "Failed to start file transfer\n");
close(data_socket);
return;
}
// 接收文件数据并写入本地文件
FILE *file = fopen(filename, "wb");
if (file == NULL) {
perror("failed to open file");
close(data_socket);
return;
}
char buffer[BUFFER_SIZE];
ssize_t bytes;
while ((bytes = recv(data_socket, buffer, BUFFER_SIZE, 0)) > 0) {
fwrite(buffer, 1, bytes, file);
}
fclose(file);
close(data_socket);
// 接收完成响应
receive_response();
printf("File %s downloaded successfully\n", filename);
}
// 上传文件
void upload_file(const char *filename) {
char command[COMMAND_SIZE];
int data_socket;
struct sockaddr_in data_addr;
socklen_t data_addr_len = sizeof(data_addr);
// 进入被动模式
send_command("PASV\r\n");
if (receive_response() != 227) {
fprintf(stderr, "Failed to enter passive mode\n");
return;
}
// 解析被动模式返回的IP和端口
int h1, h2, h3, h4, p1, p2;
char *p = strstr(response, "(");
if (p == NULL || sscanf(p, "(%d,%d,%d,%d,%d,%d)", &h1, &h2, &h3, &h4, &p1, &p2) != 6) {
fprintf(stderr, "Failed to parse passive mode response\n");
return;
}
// 计算数据端口
int data_port = p1 * 256 + p2;
// 创建数据套接字并连接
if ((data_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("data socket creation error");
return;
}
memset(&data_addr, 0, sizeof(data_addr));
data_addr.sin_family = AF_INET;
data_addr.sin_addr.s_addr = htonl((h1 << 24) | (h2 << 16) | (h3 << 8) | h4);
data_addr.sin_port = htons(data_port);
if (connect(data_socket, (struct sockaddr *)&data_addr, sizeof(data_addr)) < 0) {
perror("data connection failed");
close(data_socket);
return;
}
// 发送STOR命令
snprintf(command, COMMAND_SIZE, "STOR %s\r\n", filename);
send_command(command);
if (receive_response() != 150) {
fprintf(stderr, "Failed to start file transfer\n");
close(data_socket);
return;
}
// 打开本地文件
FILE *file = fopen(filename, "rb");
if (file == NULL) {
perror("failed to open file");
close(data_socket);
return;
}
// 读取文件内容并发送
char buffer[BUFFER_SIZE];
size_t bytes;
while ((bytes = fread(buffer, 1, BUFFER_SIZE, file)) > 0) {
send(data_socket, buffer, bytes, 0);
}
fclose(file);
close(data_socket);
// 接收完成响应
receive_response();
printf("File %s uploaded successfully\n", filename);
}
// 断开连接
void disconnect() {
send_command("QUIT\r\n");
receive_response();
close(control_socket);
printf("Disconnected from server\n");
}
int main(int argc, char *argv[]) {
if (argc < 4) {
printf("Usage: %s <host> <username> <password> [filename]\n", argv[0]);
printf("If filename is provided, it will be downloaded or uploaded.\n");
printf("If no filename is provided, the remote directory will be listed.\n");
return 1;
}
const char *host = argv[1];
const char *username = argv[2];
const char *password = argv[3];
const char *filename = (argc > 4) ? argv[4] : NULL;
// 连接到服务器
if (connect_to_server(host, FTP_CONTROL_PORT) != 0) {
return 1;
}
// 登录
login(username, password);
// 根据参数执行操作
if (filename) {
// 检查文件是否存在,存在则上传,不存在则下载
if (access(filename, F_OK) != -1) {
upload_file(filename);
} else {
download_file(filename);
}
} else {
// 列出文件
list_files();
}
// 断开连接
disconnect();
return 0;
}
四、代码说明
上述代码实现了一个简单但功能完整的 FTP 客户端,主要包含以下几个部分:
-
连接管理 :
connect_to_server
函数负责建立与 FTP 服务器的控制连接,并接收欢迎信息。 -
命令处理 :
send_command
和receive_response
函数分别用于发送命令和接收服务器响应,处理状态码。 -
用户认证 :
login
函数实现了用户名和密码的认证过程。 -
文件列表 :
list_files
函数通过被动模式建立数据连接,并获取远程目录的文件列表。 -
文件下载 :
download_file
函数实现了从服务器下载文件的功能,使用二进制模式传输数据。 -
文件上传 :
upload_file
函数实现了向服务器上传文件的功能,同样使用二进制模式。 -
连接断开 :
disconnect
函数发送 QUIT 命令并关闭控制连接。
五、编译和使用方法
编译程序:
gcc ftp_client.c -o ftp_client
使用示例:
- 列出远程目录:
./ftp_client ftp.example.com username password
- 下载文件:
./ftp_client ftp.example.com username password remote_file.txt
- 上传文件:
./ftp_client ftp.example.com username password local_file.txt
六、FTP 协议的安全性考虑
虽然 FTP 是一个广泛使用的协议,但它有一个明显的安全缺陷:所有数据(包括用户名、密码和文件内容)都是以明文形式传输的。为了解决这个问题,出现了以下几种改进方案:
-
FTPS:在 FTP 基础上加入 SSL/TLS 加密,提供数据传输的安全性。
-
SFTP:基于 SSH 协议的文件传输协议,提供更安全的文件传输环境。
-
HTTPS:虽然不是专门的文件传输协议,但可以通过 Web 服务器实现安全的文件上传和下载。
在实际应用中,特别是涉及敏感数据时,建议使用更安全的替代方案。
七、总结
FTP 协议是互联网上最早的文件传输协议之一,虽然存在安全问题,但由于其简单性和广泛支持,仍然在许多场景中被使用。通过 C 语言实现 FTP 客户端,我们可以深入理解网络协议的工作原理和 socket 编程技术。这个示例展示了如何使用 C 语言实现基本的网络通信、协议解析和文件操作,为进一步开发更复杂的网络应用提供了基础。