凌峰创科服务平台

socket ftp 服务器端如何实现?

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

socket ftp 服务器端如何实现?-图1
(图片来源网络,侵删)

FTP 协议简介

FTP (File Transfer Protocol) 是一种用于在客户端和服务器之间传输文件的网络协议,它使用 两个 TCP 连接

  1. 控制连接

    • 端口:通常为 21。
    • 用途:传输 FTP 命令(如 USER, PASS, LIST, RETR, STOR)和服务器响应(如 200 OK, 550 File not found),这个连接在整个 FTP 会话期间保持打开状态。
  2. 数据连接

    • 端口:通常是 20(主动模式)或一个临时端口(被动模式)。
    • 用途:实际传输文件内容或目录列表,每次传输文件或列表时,都会建立一个新的数据连接,传输完成后立即关闭。

我们将实现一个主动模式的 FTP 服务器,因为它更简单。

socket ftp 服务器端如何实现?-图2
(图片来源网络,侵删)

项目结构

我们的服务器将包含以下核心功能:

  • 监听特定端口(如 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()

如何运行和使用

运行服务器

  1. 将上面的代码保存为 ftp_server.py
  2. 打开终端或命令提示符。
  3. 运行服务器:
    python ftp_server.py

    你会看到输出:FTP 服务器启动在 0.0.0.0:2121

使用 FTP 客户端连接

你可以使用任何 FTP 客户端来连接这个服务器,

socket ftp 服务器端如何实现?-图3
(图片来源网络,侵删)
  • Windows: 文件资源管理器的地址栏输入 ftp://localhost:2121
  • macOS/Linux: 使用 ftp 命令行工具
  • 图形化工具: FileZilla, Cyberduck 等。

这里我们使用 ftp 命令行工具进行演示:

  1. 打开另一个终端。

  2. 连接到服务器:

    ftp localhost 2121
  3. 按照提示输入用户名和密码(代码中定义的):

    Name (localhost:your_local_username): user1
    331 Username okay, need password for user1.
    Password:
    230 User user1 logged in.
  4. 你现在可以输入 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!

代码核心逻辑解析

  1. start_server():

    • 创建一个 socket 对象,绑定到 HOSTPORT
    • 开始监听客户端连接 (listen())。
    • 在一个无限循环中,使用 accept() 等待新的客户端连接。
    • 当有客户端连接时,创建一个新的 threading.Thread,并将 handle_client 函数作为目标,这使得服务器可以同时为多个客户端服务,而不会阻塞。
  2. handle_client(control_socket, client_address):

    • 这是每个客户端连接的核心处理函数。
    • 它创建一个 ClientSession 对象来跟踪该客户端的状态(用户名、是否登录、当前目录)。
    • 在一个循环中,通过 control_socket 接收客户端发来的命令。
    • 解析命令(如 USER, PASS, LIST)。
    • 根据命令执行相应的操作,并通过 control_socket 发送 FTP 响应码和消息。
  3. 数据连接处理 (LIST, RETR, STOR):

    • 关键点:当需要传输数据时,服务器会创建一个新的 data_socket,绑定到 DATA_PORT,并开始监听。
    • 服务器通过 control_socket 向客户端发送一个响应,150 Opening data connection...,告诉客户端数据连接即将建立。
    • 服务器调用 data_socket.accept()主动接受客户端发来的数据连接请求(这是主动模式的特点)。
    • 一旦数据连接 (data_conn) 建立成功,服务器就可以通过这个 data_conn 来发送或接收文件数据。
    • 数据传输完成后,关闭 data_conndata_socket

重要注意事项与改进方向

  1. 安全性:

    • 密码明文: 当前代码中的用户名和密码是硬编码的明文,非常不安全,应使用数据库或配置文件存储,并考虑哈希加盐。
    • 路径遍历: 没有对客户端提供的文件路径进行严格的验证,存在目录遍历攻击的风险(STOR ../../etc/passwd),应确保文件操作限制在允许的目录范围内。
    • 被动模式: 当前实现的是简化的主动模式,一个更完整的 FTP 服务器需要支持被动模式,因为很多客户端(尤其是位于NAT后面的)无法主动连接服务器的20端口,在被动模式下,服务器会告诉客户端一个随机端口,让客户端去连接。
  2. 功能完善:

    • 更多命令: 支持 DELE (删除文件), MKD (创建目录), RMD (删除目录), TYPE (设置传输类型) 等更多标准 FTP 命令。
    • 并发控制: 虽然使用了多线程,但对于高并发场景,可能需要使用线程池来管理资源。
    • 日志: 添加更详细的日志记录,便于调试和监控。

这个项目为你提供了一个坚实的基础,你可以基于这个框架,逐步添加更多功能和安全性改进,最终构建一个更健壮的 FTP 服务器。

分享:
扫描分享到社交APP
上一篇
下一篇