我们将使用 Python 来实现,因为它简洁且标准库功能强大,非常适合演示核心概念。

FTP 协议简介
FTP (File Transfer Protocol) 是一种用于在客户端和服务器之间传输文件的网络协议,它使用 两个 TCP 连接:
-
控制连接:
- 端口:通常为 21。
- 用途:传输 FTP 命令(如
USER,PASS,LIST,RETR,STOR)和服务器响应(如200 OK,550 File not found),这个连接在整个 FTP 会话期间保持打开状态。
-
数据连接:
- 端口:通常是 20(主动模式)或一个临时端口(被动模式)。
- 用途:实际传输文件内容或目录列表,每次传输文件或列表时,都会建立一个新的数据连接,传输完成后立即关闭。
我们将实现一个主动模式的 FTP 服务器,因为它更简单。

项目结构
我们的服务器将包含以下核心功能:
- 监听特定端口(如 2121),等待客户端连接。
- 处理用户认证(用户名和密码)。
- 解析客户端发送的 FTP 命令。
- 处理文件列表 (
LIST/LS)。 - 处理文件上传 (
STOR)。 - 处理文件下载 (
RETR)。 - 管理数据连接的建立和关闭。
代码实现
下面是一个功能完整的、基于 Socket 的 FTP 服务器端代码。
ftp_server.py
import socket
import os
import threading
import time
# --- 服务器配置 ---
HOST = '0.0.0.0' # 监听所有可用的网络接口
PORT = 2121 # FTP 控制端口
DATA_PORT = 2122 # FTP 数据端口 (主动模式)
BUFFER_SIZE = 4096
# --- 用户认证 (简单示例) ---
USERS = {
"user1": "password123",
"admin": "admin123"
}
# --- 每个客户端的会话状态 ---
class ClientSession:
def __init__(self):
self.username = None
self.is_logged_in = False
self.current_dir = os.getcwd() # 默认为服务器启动时的目录
# --- 主 FTP 服务器逻辑 ---
def handle_client(control_socket, client_address):
"""处理单个客户端连接的函数"""
print(f"[新连接] {client_address} 已连接。")
session = ClientSession()
# 发送欢迎消息
control_socket.sendall(b"220 Welcome to the Simple FTP Server\r\n")
try:
while True:
# 接收客户端命令
command = control_socket.recv(BUFFER_SIZE).decode('utf-8').strip()
if not command:
break # 客户端断开连接
print(f"[{client_address}] 收到命令: {command}")
# 解析命令
parts = command.split()
cmd = parts[0].upper()
arg = ' '.join(parts[1:]) if len(parts) > 1 else ''
# --- 处理各种 FTP 命令 ---
if cmd == "USER":
response = f"331 Username okay, need password for {arg}.\r\n"
session.username = arg
elif cmd == "PASS":
if session.username and session.username in USERS and USERS[session.username] == arg:
session.is_logged_in = True
response = f"230 User {session.username} logged in.\r\n"
else:
response = "530 Not logged in.\r\n"
elif cmd == "QUIT":
response = "221 Goodbye!\r\n"
control_socket.sendall(response.encode('utf-8'))
break
elif not session.is_logged_in:
response = "530 Please login with USER and PASS.\r\n"
# --- 需要登录后才能执行的命令 ---
elif cmd == "PASV":
# 主动模式不需要 PASV 命令,这里我们忽略或返回错误
# 真正的主动模式是服务器主动连接客户端的数据端口
# 为了简化,我们在这里模拟一个被动模式的行为
# 但实际数据连接还是由服务器主动发起
response = "227 Entering Passive Mode (127,0,0,1,83,100)\r\n" # 2122 = 83*256 + 100
# 在实际实现中,PASV会告诉客户端一个端口,然后客户端监听这个端口,服务器再连过去
# 这里我们简化,直接使用预定义的 DATA_PORT
elif cmd == "LIST" or cmd == "LS":
# 创建数据 socket
data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
data_socket.bind((HOST, DATA_PORT))
data_socket.listen(1)
control_socket.sendall(b"150 Here comes the directory listing.\r\n")
# 接受客户端的数据连接
try:
data_conn, data_addr = data_socket.accept()
print(f"[数据连接] 已建立与 {data_addr} 的连接。")
# 获取当前目录列表
dir_list = os.listdir(session.current_dir)
# 发送列表数据
list_data = "\r\n".join(dir_list) + "\r\n"
data_conn.sendall(list_data.encode('utf-8'))
data_conn.close()
print(f"[数据连接] 已关闭与 {data_addr} 的连接。")
except Exception as e:
print(f"[错误] LIST 数据连接失败: {e}")
control_socket.sendall(b"425 Can't open data connection.\r\n")
finally:
data_socket.close()
response = b"226 Directory send OK.\r\n"
elif cmd == "CWD" or cmd == "CDUP":
# CWD: Change Working Directory
# CDUP: Change to Parent Directory
target_dir = arg if cmd == "CWD" else os.path.join(session.current_dir, "..")
try:
os.chdir(target_dir)
session.current_dir = os.getcwd()
response = f"250 Directory changed to {session.current_dir}.\r\n"
except FileNotFoundError:
response = "550 No such file or directory.\r\n"
except Exception as e:
response = f"550 Failed to change directory: {e}\r\n"
elif cmd == "PWD":
response = f'257 "{session.current_dir}" is the current directory.\r\n'
elif cmd == "RETR": # 下载文件
filename = arg
filepath = os.path.join(session.current_dir, filename)
if not os.path.exists(filepath) or not os.path.isfile(filepath):
response = f"550 File not found: {filename}.\r\n"
else:
data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
data_socket.bind((HOST, DATA_PORT))
data_socket.listen(1)
control_socket.sendall(b"150 Opening data connection for file download.\r\n")
try:
data_conn, data_addr = data_socket.accept()
print(f"[数据连接] 已建立与 {data_addr} 的连接。")
with open(filepath, 'rb') as f:
while True:
chunk = f.read(BUFFER_SIZE)
if not chunk:
break
data_conn.sendall(chunk)
data_conn.close()
print(f"[数据连接] 已关闭与 {data_addr} 的连接。")
except Exception as e:
print(f"[错误] RETR 数据连接失败: {e}")
control_socket.sendall(b"425 Can't open data connection.\r\n")
finally:
data_socket.close()
response = b"226 File transfer successful.\r\n"
elif cmd == "STOR": # 上传文件
filename = arg
filepath = os.path.join(session.current_dir, filename)
data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
data_socket.bind((HOST, DATA_PORT))
data_socket.listen(1)
control_socket.sendall(b"150 Opening data connection for file upload.\r\n")
try:
data_conn, data_addr = data_socket.accept()
print(f"[数据连接] 已建立与 {data_addr} 的连接。")
with open(filepath, 'wb') as f:
while True:
chunk = data_conn.recv(BUFFER_SIZE)
if not chunk:
break
f.write(chunk)
data_conn.close()
print(f"[数据连接] 已关闭与 {data_addr} 的连接。")
except Exception as e:
print(f"[错误] STOR 数据连接失败: {e}")
control_socket.sendall(b"425 Can't open data connection.\r\n")
finally:
data_socket.close()
response = b"226 File transfer successful.\r\n"
else:
response = f'500 Command "{cmd}" not understood.\r\n'
# 发送响应给客户端
control_socket.sendall(response.encode('utf-8'))
except (ConnectionResetError, BrokenPipeError):
print(f"[{client_address}] 客户端异常断开。")
finally:
print(f"[{client_address}] 连接已关闭。")
control_socket.close()
# --- 服务器启动 ---
def start_server():
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((HOST, PORT))
server_socket.listen(5)
print(f"FTP 服务器启动在 {HOST}:{PORT}")
try:
while True:
control_socket, client_address = server_socket.accept()
# 为每个客户端创建一个新线程来处理
client_thread = threading.Thread(target=handle_client, args=(control_socket, client_address))
client_thread.start()
except KeyboardInterrupt:
print("\n服务器正在关闭...")
finally:
server_socket.close()
print("服务器已关闭。")
if __name__ == "__main__":
start_server()
如何运行和使用
运行服务器
- 将上面的代码保存为
ftp_server.py。 - 打开终端或命令提示符。
- 运行服务器:
python ftp_server.py
你会看到输出:
FTP 服务器启动在 0.0.0.0:2121
使用 FTP 客户端连接
你可以使用任何 FTP 客户端来连接这个服务器,

- Windows: 文件资源管理器的地址栏输入
ftp://localhost:2121 - macOS/Linux: 使用
ftp命令行工具 - 图形化工具: FileZilla, Cyberduck 等。
这里我们使用 ftp 命令行工具进行演示:
-
打开另一个终端。
-
连接到服务器:
ftp localhost 2121
-
按照提示输入用户名和密码(代码中定义的):
Name (localhost:your_local_username): user1 331 Username okay, need password for user1. Password: 230 User user1 logged in. -
你现在可以输入 FTP 命令了:
- 查看文件列表:
ftp> ls 150 Here comes the directory listing. . .. ftp_server.py 226 Directory send OK.
- 切换目录:
ftp> cd /path/to/your/directory 250 Directory changed to /path/to/your/directory.
- 查看当前目录:
ftp> pwd 257 "/path/to/your/directory" is the current directory.
- 下载文件:
ftp> get ftp_server.py local: ftp_server.py remote: ftp_server.py 150 Opening data connection for file download. 226 File transfer successful. 2242 bytes received in 0.00 secs (5.5 MB/s)
- 上传文件:
ftp> put my_local_file.txt local: my_local_file.txt remote: my_local_file.txt 150 Opening data connection for file upload. 226 File transfer successful. 1024 bytes sent in 0.00 secs (1.0 MB/s)
- 退出:
ftp> quit 221 Goodbye!
- 查看文件列表:
代码核心逻辑解析
-
start_server():- 创建一个
socket对象,绑定到HOST和PORT。 - 开始监听客户端连接 (
listen())。 - 在一个无限循环中,使用
accept()等待新的客户端连接。 - 当有客户端连接时,创建一个新的
threading.Thread,并将handle_client函数作为目标,这使得服务器可以同时为多个客户端服务,而不会阻塞。
- 创建一个
-
handle_client(control_socket, client_address):- 这是每个客户端连接的核心处理函数。
- 它创建一个
ClientSession对象来跟踪该客户端的状态(用户名、是否登录、当前目录)。 - 在一个循环中,通过
control_socket接收客户端发来的命令。 - 解析命令(如
USER,PASS,LIST)。 - 根据命令执行相应的操作,并通过
control_socket发送 FTP 响应码和消息。
-
数据连接处理 (
LIST,RETR,STOR):- 关键点:当需要传输数据时,服务器会创建一个新的
data_socket,绑定到DATA_PORT,并开始监听。 - 服务器通过
control_socket向客户端发送一个响应,150 Opening data connection...,告诉客户端数据连接即将建立。 - 服务器调用
data_socket.accept()来主动接受客户端发来的数据连接请求(这是主动模式的特点)。 - 一旦数据连接 (
data_conn) 建立成功,服务器就可以通过这个data_conn来发送或接收文件数据。 - 数据传输完成后,关闭
data_conn和data_socket。
- 关键点:当需要传输数据时,服务器会创建一个新的
重要注意事项与改进方向
-
安全性:
- 密码明文: 当前代码中的用户名和密码是硬编码的明文,非常不安全,应使用数据库或配置文件存储,并考虑哈希加盐。
- 路径遍历: 没有对客户端提供的文件路径进行严格的验证,存在目录遍历攻击的风险(
STOR ../../etc/passwd),应确保文件操作限制在允许的目录范围内。 - 被动模式: 当前实现的是简化的主动模式,一个更完整的 FTP 服务器需要支持被动模式,因为很多客户端(尤其是位于NAT后面的)无法主动连接服务器的20端口,在被动模式下,服务器会告诉客户端一个随机端口,让客户端去连接。
-
功能完善:
- 更多命令: 支持
DELE(删除文件),MKD(创建目录),RMD(删除目录),TYPE(设置传输类型) 等更多标准 FTP 命令。 - 并发控制: 虽然使用了多线程,但对于高并发场景,可能需要使用线程池来管理资源。
- 日志: 添加更详细的日志记录,便于调试和监控。
- 更多命令: 支持
这个项目为你提供了一个坚实的基础,你可以基于这个框架,逐步添加更多功能和安全性改进,最终构建一个更健壮的 FTP 服务器。
