如何组建FTP服务器?支持多用户

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何组建FTP服务器?支持多用户相关的知识,希望对你有一定的参考价值。

我是个菜鸟,架设局域网FTP服务器需要什么要求?用什么软件最好?
另外我的IIS为什么没有FTP的选项?

推荐xlight,国产的,配置很简单,对个人用户免费,无需破解。

iis的那个需要安装时选择ftp,默认是没有的。不过我不建议用iis自带的ftp服务器,很烂,配置多用户的非常麻烦。
参考技术A 我也推荐 Serv-U 给你一个绿盟专用的,很好用没有后门Serv-U FTP Server V6.0.0.2_绿色汉化特别版_绿盟专用无后门无漏洞版本 http://www.xdowns.com/soft/1/98/2006/Soft_31503.html 参考技术B IIS安装时要详细选择上FTp才会安装上,你选择安装IIS时,FTP不会自动选择上的,可以看我上传的图片把选项选择. 参考技术C 使用serv-u吧,iis架设会很复杂的,要给windows建立好多用户,权限,用serv-u方便的多

开发一个支持多用户在线的FTP程序

一,项目题目:开发一个支持多用户在线的FTP程序

二,项目要求:

1.用户加密认证

2.允许同时多用户登录

3.每个用户有自己的家目录 ,且只能访问自己的家目录

4.对用户进行磁盘配额,每个用户的可用空间不同

5.允许用户在ftp server上随意切换目录

6.允许用户查看当前目录下文件

7.允许上传和下载文件,保证文件一致性(md5)

8.文件传输过程中显示进度条

9.附加功能:支持文件的断点续传

三,注意事项: 

基本要求. 完成1,2,3,5,6,7,8 
实力选手. 完成 上条 及需求4 ,     
大神操作. 完成 9 且项目目录结构良好、代码逻辑清晰,

  

四,项目分析:

1,用户加密认证

  这个肯定需要用到configparser 和hashlib模块,用md5进行加密,服务端与用户端
进行交互前,肯定需要进行认证,在服务端进行认证,客户端需要发送用户名及密码,但
是为了安全起见,服务端数据库中的密码应该是加密后的密文,客户端登陆认证时也应该
发送密文到服务端,服务端接受到密文与数据库中对应的密文进行比较。

  

2,查看自己的当前目录下的文件

  这个只需要写一个dir就ok
简单的说,使用configparse模块就可以完成

  

3,文件传输中显示进度条

  下载的进度条比较好实现,我们可以从服务端受到将要下载的文件的大小,

  上传的进度条,我们可以利用文件操作的tell()方法,获取当前指针位置(字节)

  

4,小编的主要思路

- 1 对于此项目,最初的想法是写出上传,和下载文件的程序,包括客户端和服务端。

-  2 在此基础上扩展程序,包括提出开始程序到bin里面,配置文件在config里面

-  3 然后把上传文件和下载文件的程序进行断点续传的程序重构

-   4 在此基础上,对文件进行加密

-   5 增加功能,包括设置进度条,增加查看功能,增加目录功能,删除文件功能,切换目录功能等

-   6 然后再设置磁盘分配功能,完善内容

-   7 然后添加用户登陆,包括对用户的密码加密等功能

-   8 写完后检查程序

  

 五,项目流程图

 

 

六,README文件

## 作者:zhanzhengrecheng
## 版本:示例版本 v0.1
## 程序介绍:
- 实现了支持多用户在线的FTP程序 常用功能
- 功能全部用python的基础知识实现,用到了socket\\hashlib\\configparse\\os\\sys\\pickle\\函数\\模块\\类知识

## 概述
本次作业文件夹一共包含了以下5个文件:
- 流程图: FTP_homework思路流程图
- 程序结构图:整个FTP_homework的程序文件结构
- 程序结构文件:整个FTP_homework的程序文件结构
- 程序文件: FTP_homework
- 程序说明文件:README.md

## 程序要求
- 1.用户加密认证
- 2.允许同时多用户登录
- 3.每个用户有自己的家目录 ,且只能访问自己的家目录
- 4.对用户进行磁盘配额,每个用户的可用空间不同
- 5.允许用户在ftp server上随意切换目录
- 6.允许用户查看当前目录下文件
- 7.允许上传和下载文件,保证文件一致性(md5)
- 8.文件传输过程中显示进度条
- 9.附加功能:支持文件的断点续传
## 本项目思路
- 1 对于此项目,最初的想法是写出上传,和下载文件的程序,包括客户端和服务端。
-  2 在此基础上扩展程序,包括提出开始程序到bin里面,配置文件在config里面
-  3 然后把上传文件和下载文件的程序进行断点续传的程序重构
-   4 在此基础上,对文件进行加密
-   5 增加功能,包括设置进度条,增加查看功能,增加目录功能,删除文件功能,切换目录功能等
-   6 然后再设置磁盘分配功能,完善内容
-   7 然后添加用户登陆,包括对用户的密码加密等功能
-   8 写完后检查程序


##### 备注(程序结构)
> 目前还不会把程序树放在README.md里面,所以做出程序结构的txt版本和图片版本,放在文件外面方便查看

## 对几个实例文件的说明
### 几个实例文件全是为了上传和下载使用,自己随便找的素材

## 不足及其改进的方面
### 每次程序从用户登陆到使用只能完成一次功能,不能重复使用

## 程序结构

│  FTP_homework
│  __init__.py
│  
├─client                # 客户端程序入口
│  │  __init__.py
│  ├─bin                # 可执行程序入口目录
│  │      run.py
│  │      __init__.py
│  ├─config             # 配置文件目录
│  │  │  settings.py    # 配置文件
│  │  │  __init__.py       
│  ├─core               # 主要逻辑程序目录
│  │  │  ftp_client.py  # client端主程序模块
│  │  │  __init__.py       
│  ├─download           # 下载内容模块
│  │      a.jpg 
│  │      a.txt
│  │      c.mp4  
│  └─upload             # 上传内容模块
│          a.txt
│          aa.avi
└─server                 # 服务端程序入口
    ├─bin
    │      run.py        # 可执行程序入口目录
    │      __init__.py 
    ├─config             # 配置文件目录
    │  │  accounts.ini   # 账号密码配置文件
    │  │  settings.py    # 配置文件
    │  │  __init__.py        
    ├─core               # 主要逻辑程序目录
    │  │  ftp_server.py  # server端主程序模块
    │  │  main.py        # 主程序模块
    │  │  user_handle.py # 用户注册登录模块  
    └─home               # 家目录
        │  __init__.py
        ├─curry          # curry用户的家目录
        │  │  aa.avi
        │  └─test
        └─james           # james用户的家目录
            │  a.jpg
            │  aa.avi
            │  c.mp4
            └─test1

  

七,程序结构图

 八,程序代码

1,server

1.1 bin

run.py

# _*_ coding: utf-8 _*_
import os
import sys


BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)


from core import ftp_server
from core import main
from config import settings

if __name__ == \'__main__\':
    a = main.Manager()
    a.interactive()

  

1.2config

settings.py

# _*_ coding: utf-8 _*_ 
import os
import sys
import socket

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)

ACCOUNTS_FILE = os.path.join(BASE_DIR,\'config\',\'accounts.ini\')

address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM

BIND_HOST = \'127.0.0.1\'
BIND_PORT = 9999
ip_port = (BIND_HOST,BIND_PORT)
coding = \'utf-8\'
listen_count = 5
max_recv_bytes = 8192
allow_reuser_address = False

  

1.3core

ftp_server.py

# _*_ coding: utf-8 _*_
import socket
import struct
import json
import os
import pickle
import subprocess
import hashlib

from config import settings
from core.user_handle import UserHandle

class FTPServer():

    def __init__(self,server_address,bind_and_listen = True):
        self.server_address = server_address
        self.socket = socket.socket(settings.address_family,settings.socket_type)
        if bind_and_listen:
            try:
                self.server_bind()
                self.server_listen()
            except Exception:
                self.server_close()

    def server_bind(self):
        allow_reuse_address = False
        if allow_reuse_address:
            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.bind(self.server_address)

    def server_listen(self):
        self.socket.listen(settings.listen_count)

    def server_close(self):
        self.socket.close()

    def server_accept(self):
        return self.socket.accept()

    def conn_close(self, conn):
        conn.close()

    def getfile_md5(self):
        \'\'\'获取文件的md5\'\'\'
        return hashlib.md5(self.readfile()).hexdigest()

    def readfile(self):
        \'\'\'读取文件,得到文件内容的bytes类型\'\'\'
        with open(self.file_path,\'rb\') as f:
            filedata = f.read()
        return filedata

    def send_filedata(self,exist_file_size=0):
        """下载时,将文件打开,send(data)"""
        with open(self.file_path, \'rb\') as f:
            f.seek(exist_file_size)
            while True:
                data = f.read(1024)
                if data:
                    self.conn.send(data)
                else:
                    break

    def get(self, cmds):
        \'\'\'
       下载,首先查看文件是否存在,然后上传文件的报头大小,上传文件,以读的方式发开文件
       找到下载的文件
    发送 header_size
    发送 header_bytes file_size
    读文件 rb 发送 send(line)
    若文件不存在,发送0 client提示:文件不存在
       :param cmds:
       :return:
               \'\'\'
        if len(cmds) > 1:
            filename = cmds[1]
            self.file_path = os.path.join(os.getcwd(), filename)
            if os.path.isfile(self.file_path):
                file_size = os.path.getsize(self.file_path)
                obj = self.conn.recv(4)
                exist_file_size = struct.unpack(\'i\', obj)[0]
                header = {
                    \'filename\': filename,
                    \'filemd5\': self.getfile_md5(),
                    \'file_size\': file_size
                }
                header_bytes = pickle.dumps(header)
                self.conn.send(struct.pack(\'i\', len(header_bytes)))
                self.conn.send(header_bytes)
                if exist_file_size:  # 表示之前被下载过 一部分
                    if exist_file_size != file_size:
                        self.send_filedata(exist_file_size)
                    else:
                        print(\'\\033[31;1mbreakpoint and file size are the same\\033[0m\')
                else:  # 文件第一次下载
                    self.send_filedata()
            else:
                print(\'\\033[31;1merror\\033[0m\')
                self.conn.send(struct.pack(\'i\', 0))

        else:
            print("\\033[31;1muser does not enter file name\\033[0m")

    def recursion_file(self, dir):
        """递归查询用户目录下的所有文件,算出文件的大小"""
        res = os.listdir(dir)
        for i in res:
            path = os.path.join(dir,i)
            if os.path.isdir(path):
                self.recursion_file(path)
            elif os.path.isfile(path):
                self.home_bytes_size += os.path.getsize(path)

    def current_home_size(self):
        """得到当前用户目录的大小,字节/M"""
        self.home_bytes_size =0
        self.recursion_file(self.homedir_path)
        home_m_size = round(self.home_bytes_size / 1024 / 1024, 1)

    def put(self,cmds):
        """从client上传文件到server当前工作目录下
        """
        if len(cmds) >1:
            obj = self.conn.recv(4)
            state_size = struct.unpack(\'i\', obj)[0]
            if state_size ==0:
                print("\\033[31;1mfile does not exist!\\033[0m")
            else:
                # 算出了home下已被占用的大小self.home_bytes_size
                self.current_home_size()
                header_bytes = self.conn.recv(struct.unpack(\'i\', self.conn.recv(4))[0])
                header_dic = pickle.loads(header_bytes)
                filename = header_dic.get(\'filename\')
                file_size = header_dic.get(\'file_size\')
                file_md5 = header_dic.get(\'file_md5\')
                self.file_path = os.path.join(os.getcwd(),filename)
                if os.path.exists(self.file_path):
                    self.conn.send(struct.pack(\'i\',1))
                    has_size = os.path.getsize(self.file_path)
                    if has_size == file_size:
                        print("\\033[31;1mfile already does exist!\\033[0m")
                        self.conn.send(struct.pack(\'i\', 0))
                    else:
                        print(\'\\033[31;1mLast file not finished,this time continue\\033[0m\')
                        self.conn.send(struct.pack(\'i\', 1))
                        if self.home_bytes_size + int(file_size-has_size)>self.quota_bytes:
                            print(\'\\033[31;1mSorry exceeding user quotas\\033[0m\')
                            self.conn.send(struct.pack(\'i\', 0))
                        else:
                            self.conn.send(struct.pack(\'i\', 1))
                            self.conn.send(struct.pack(\'i\', has_size))
                            with open(self.file_path, \'ab\') as f:
                                f.seek(has_size)
                                self.write_file(f, has_size, file_size)
                            self.verification_filemd5(file_md5)
                else:
                    self.conn.send(struct.pack(\'i\', 0))
                    print(\'\\033[31;1mSorry file does not exist now first put\\033[0m\')
                    if self.home_bytes_size + int(file_size) > self.quota_bytes:
                        print(\'\\033[31;1mSorry exceeding user quotas\\033[0m\')
                        self.conn.send(struct.pack(\'i\', 0))
                    else:
                        self.conn.send(struct.pack(\'i\', 1))
                        with open(self.file_path,\'wb\') as f:
                            recv_size = 0
                            self.write_file(f, recv_size, file_size)
                        self.verification_filemd5(file_md5)

        else:
            print("\\033[31;1muser does not enter file name\\033[0m")
    def write_file(self,f,recv_size,file_size):
        \'\'\'上传文件时,将文件内容写入到文件中\'\'\'
        while recv_size < file_size:
            res = self.conn.recv(settings.max_recv_bytes)
            f.write(res)
            recv_size += len(res)
            self.conn.send(struct.pack(\'i\', recv_size))  # 为了进度条的显示

    def verification_filemd5(self,filemd5):
        # 判断文件内容的md5
        if self.getfile_md5() == filemd5:
            print(\'\\033[31;1mCongratulations download success\\033[0m\')
            self.conn.send(struct.pack(\'i\', 1))
        else:
            print(\'\\033[31;1mSorry download failed\\033[0m\')
            self.conn.send(struct.pack(\'i\', 0))

    def ls(self,cmds):
        \'\'\'查看当前工作目录下,先返回文件列表的大小,在返回查询的结果\'\'\'
        print("\\033[34;1mview current working directory\\033[0m")
        subpro_obj = subprocess.Popen(\'dir\',shell=True,
                                      stdout=subprocess.PIPE,
                                      stderr=subprocess.PIPE)
        stdout = subpro_obj.stdout.read()
        stderr = subpro_obj.stderr.read()
        self.conn.send(struct.pack(\'i\',len(stdout + stderr)))
        self.conn.send(stdout)
        self.conn.send(stderr)

    def mkdir(self,cmds):
        \'\'\'增加目录
        在当前目录下,增加目录
        1.查看目录名是否已经存在
        2.增加目录成功,返回 1
        2.增加目录失败,返回 0\'\'\'
        print("\\033[34;1madd working directory\\033[0m")
        if len(cmds) >1:
            mkdir_path = os.path.join(os.getcwd(),cmds[1])
            if not os.path.exists(mkdir_path):
                os.mkdir(mkdir_path)
                print(\'\\033[31;1mCongratulations add directory success\\033[0m\')
                self.conn.send(struct.pack(\'i\',1))
            else:
                print("\\033[31;1muser directory already does exist\\033[0m")
                self.conn.send(struct.pack(\'i\',0))
        else:
            print("\\033[31;1muser does not enter file name\\033[0m")

    def cd(self,cmds):
        \'\'\'切换目录
        1.查看是否是目录名
        2.拿到当前目录,拿到目标目录,
        3.判断homedir是否在目标目录内,防止用户越过自己的home目录 eg: ../../....
        4.切换成功,返回 1
        5.切换失败,返回 0\'\'\'
        print("\\033[34;1mSwitch working directory\\033[0m")
        if len(cmds) > 1:
            dir_path = os.path.join(os.getcwd(),cmds[1])
            if os.path.isdir(dir_path):
                #os.getcwd 获取当前工作目录
                previous_path = os.getcwd()
                #os.chdir改变当前脚本目录
                os.chdir(dir_path)
                target_dir = os.getcwd()
                if self.homedir_path in target_dir:
                    print(\'\\033[31;1mCongratulations switch directory success\\033[0m\')
                    self.conn.send(struct.pack(\'i\', 1))
                else:
                    print(\'\\033[31;1mSorry switch directory failed\\033[0m\')
                    # 切换失败后,返回到之前的目录下
                    os.chdir(previous_path)
                    self.conn.send(struct.pack(\'i\', 0))
            else:
                print(\'\\033[31;1mSorry switch directory failed,the directory is not current directory\\033[0m\')
                self.conn.send(struct.pack(\'i\', 0))
        else:
            print("\\033[31;1muser does not enter file name\\033[0m")

    def remove(self,cmds):
        """删除指定的文件,或者空文件夹
               1.删除成功,返回 1
               2.删除失败,返回 0
               """
        print("\\033[34;1mRemove working directory\\033[0m")
        if len(cmds) > 1:
            file_name = cmds[1]
            file_path = os.path.join(os.getcwd(),file_name)
            if os.path.isfile(file_path):
                os.remove(file_path)
                self.conn.send(struct.pack(\'i\', 1))
            elif os.path.isdir(file_path):  # 删除空目录
                if not len(os.listdir(file_path)):
                    os.removedirs(file_path)
                    print(\'\\033[31;1mCongratulations remove success\\033[0m\')
                    self.conn.send(struct.pack(\'i\', 1))
                else:
                    print(\'\\033[31;1mSorry remove directory failed\\033[0m\')
                    self.conn.send(struct.pack(\'i\', 0))
            else:
                print(\'\\033[31;1mSorry remove directory failed\\033[0m\')
                self.conn.send(struct.pack(\'i\', 0))
        else:
            print("\\033[31;1muser does not enter file name\\033[0m")

    def get_recv(self):
        \'\'\'从client端接收发来的数据\'\'\'
        return pickle.loads(self.conn.recv(settings.max_recv_bytes ))

    def handle_data(self):
        \'\'\'处理接收到的数据,主要是将密码转化为md5的形式\'\'\'
        user_dic = self.get_recv()
        username = user_dic[\'username\']
        password = user_dic[\'password\']
        md5_obj = hashlib.md5()
        md5_obj.update(password)
        check_password = md5_obj.hexdigest()

    def auth(self):
        \'\'\'
        处理用户的认证请求
        1,根据username读取accounts.ini文件,然后查看用户是否存在
        2,将程序运行的目录从bin.user_auth修改到用户home/username方便之后查询
        3,把客户端返回用户的详细信息
        :return:
        \'\'\'
        while True:
            user_dic = self.get_recv()
            username = user_dic[\'username\']
            password = user_dic[\'password\']
            md5_obj = hashlib.md5(password.encode(\'utf-8\'))
            check_password = md5_obj.hexdigest()
            user_handle  = UserHandle(username)
            # 判断用户是否存在 返回列表,
            user_data = user_handle.judge_user()
            if user_data:
                if user_data[0][1] ==check_password:
                    self.conn.send(struct.pack(\'i\',1))  # 登录成功返回 1
                    self.homedir_path = os.path.join(settings.BASE_DIR,\'home\',username)
                    # 将程序运行的目录名修改到 用户home目录下
                    os.chdir(self.homedir_path)
                    # 将用户配额的大小从M 改到字节
                    self.quota_bytes = int(user_data[2][1])*1024*1024
                    user_info_dic = {
                        \'username\':username,
                        \'homedir\':user_data[1][1],
                        \'quota\':user_data[2][1]
                    }
                    # 用户的详细信息发送到客户端
                    self.conn.send(pickle.dumps(user_info_dic))
                    return True
                else:
                    self.conn.send(struct.pack(\'i\', 0))  # 登录失败返回 0
            else:
                self.conn.send(struct.pack(\'i\', 0))  # 登录失败返回 0

    def server_link(self):
        print("\\033[31;1mwaiting client .....\\033[0m")
        while True:  # 链接循环
            self.conn,self.client_addr = self.server_accept()
            while True:  # 通信循环
                try:
                    self.server_handle()
                except Exception:
                    break
            self.conn_close(self.conn)
    def server_handle(self):
        \'\'\'处理与用户的交互指令\'\'\'
        if self.auth():
            print("\\033[32;1m-------user authentication successfully-------\\033[0m")
            res = self.conn.recv(settings.max_recv_bytes)
            # 解析命令,提取相应的参数
            cmds = res.decode(settings.coding).split()
            if hasattr(self, cmds[0]):
                func = getattr(self, cmds[0])
                func(cmds)

  

main.py

# _*_ coding: utf-8 _*_ 
from core.user_handle import UserHandle
from core.ftp_server import FTPServer
from config import settings


class Manager():
    \'\'\'
    主程序,包括启动server,创建用户,退出
    :return:
    \'\'\'
    def start_ftp(self):
        \'\'\'启动server端\'\'\'
        server = FTPServer(settings.ip_port)
        server.server_link()
        server.close()

    def create_user(self):
        \'\'\'创建用户,执行创建用户的类\'\'\'
        username =  input("\\033[32;1mplease input your username>>>\\033[0m").strip()
        UserHandle(username).add_user()

    def logout(self):
        \'\'\'
        退出登陆
        :return:
        \'\'\'
        print("\\033[32;1m-------Looking forward to your next login-------\\033[0m")
        exit()
    def interactive(self):
        \'\'\'交互函数\'\'\'
        msg = \'\'\'\\033[32;1m
                       1   启动ftp服务端
                       2   创建用户
                       3   退出
               \\033[0m\'\'\'
        menu_dic = {
            "1": \'start_ftp\',
            "2": \'create_user\',
            "3": \'logout\',
        }
        exit_flag = False
        while not exit_flag:
            print(msg)
            user_choice = input("Please input a command>>>").strip()
            if user_choice in menu_dic:
                getattr(self,menu_dic[user_choice])()
            else:
                print("\\033[31;1myou choice doesn\'t exist\\033[0m")

  

user_handle.py

# _*_ coding: utf-8 _*_ 
import configparser
import hashlib
import os

from config import settings

class UserHandle():
    \'\'\'
    创建用户名称,密码
    如果用户存在,则返回,如果用户不存在,则注册成功
    \'\'\'
    def __init__(self,username):
        self.username = username
        self.config = configparser.ConfigParser()
        self.config.read(settings.ACCOUNTS_FILE)

    @property
    def password(self):
        \'\'\'生成用户的默认密码 \'\'\'
        password_inp = input("\\033[32;1mplease input your password>>>\\033[0m").strip()
        md5_obj = hashlib.md5()
        md5_obj.update(password_inp.encode())
        md5_password = md5_obj.hexdigest()
        return md5_password

    @property
    def disk_quota(self):
        \'\'\'生成每个用户的磁盘配额\'\'\'
        quota = input(\'\\033[32;1mplease input Disk quotas>>>:\\033[0m\').strip()
        if quota.isdigit():
            return quota
        else:
            exit(\'\\033[31;1mdisk quotas must be integer\\033[0m\')

    def add_user(self):
        """创建用户,存到accounts.ini"""
        if not self.config.has_section(self.username):
            print(\'\\033[31;1mcreating username is :%s \\033[0m\' %self.username)
            self.config.add_section(self.username)
            self.config.set(self.username, \'password\', self.password)
            self.config.set(self.username, \'homedir\', \'home/\' + self.username)
            self.config.set(self.username, \'quota\', self.disk_quota)
            with open(settings.ACCOUNTS_FILE, \'w\') as f:
                self.config.write(f)
            os.mkdir(os.path.join(settings.BASE_DIR, \'home\', self.username))  # 创建用户的home文件夹
            print(\'\\033[1;32msuccessfully create userdata\\033[0m\')
        else:
            print(\'\\033[1;31musername already existing\\033[0m\')

    def judge_user(self):
        """判断用户是否存在"""
        if self.config.has_section(self.username):
            return self.config.items(self.username)

  

2,client

2.1bin

run.py

# _*_ coding: utf-8 _*_
import os
import sys


BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)


from core import ftp_client
from config import settings
if __name__ == \'__main__\':
    run = ftp_client.FTPClient(settings.ip_port)
    run.execute()

  

2.2config

settings.py

# _*_ coding: utf-8 _*_
import os
import sys
import socket

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)
# 下载的文件存放路径
down_filepath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), \'download\')
# 上传的文件存放路径
upload_filepath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), \'upload\')
#绑定的IP地址
BIND_HOST = \'127.0.0.1\'
#绑定的端口号
BIND_PORT = 9999
ip_port = (BIND_HOST,BIND_PORT)
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM

coding = \'utf-8\'
listen_count = 5
max_recv_bytes = 8192
allow_reuser_address = False

  

2.3core

ftp_client.py

# _*_ coding: utf-8 _*_ 
import socket
import struct
import json
import os
import sys
import pickle
import hashlib

from config import settings

class FTPClient:

    def __init__(self,server_address,connect = True):
        self.server_address = server_address
        self.socket = socket.socket(settings.address_family,settings.socket_type)
        if connect:
            try:
                self.client_connect()
            except Exception:
                self.client_close()

    def client_connect(self):
        try:
            self.socket.connect(self.server_address)
        except Exception as e:
            print("\\033[31;1merror:%s\\033[0m"%e)
            exit("\\033[31;1m\\nThe server is not activated \\033[0m")

    def client_close(self):
        self.socket.close()

    def readfile(self):
        \'\'\'读取文件\'\'\'
        with open(self.file_path,\'rb\') as f:
            filedata = f.read()
        return filedata

    def appendfile_content(self,file_path,temp_file_size,file_size):
        \'\'\'追加文件内容\'\'\'
        with open(file_path,\'ab\') as f:
            f.seek(temp_file_size)
            get_size = temp_file_size
            while get_size < file_size:
                res = self.socket.recv(settings.max_recv_bytes)
                f.write(res)
                get_size += len(res)
                self.progress_bar(1,get_size,file_size)  #1表示下载

    def getfile_md5(self):
        \'\'\'对文件内容进行加密,也就是保持文件的一致性\'\'\'
        md5 = hashlib.md5(self.readfile())
        print("md5是:\\n",md5.hexdigest())
        return md5.hexdigest()

    def progress_bar(self,num,get_size,file_size):
        float_rate = float(get_size) / float(file_size)
        rate_num = round(float_rate * 100,2)
        if num ==1: #1表示下载
            sys.stdout.write(\'\\033[31;1m\\rfinish downloaded perentage:{0}%\\033[0m\'.format(rate_num))
        elif num ==2:  #2表示上传
            sys.stdout.write(\'\\033[31;1m\\rfinish uploaded perentage:{0}%\\033[0m\'.format(rate_num))
        sys.stdout.flush()

    def recv_file_header(self,header_size):
        """接收文件的header, filename file_size file_md5"""
        header_types = self.socket.recv(header_size)
        header_dic = pickle.loads(header_types)
        print(header_dic, type(header_dic))
        total_size = header_dic[\'file_size\']
        filename = header_dic[\'filename\']
        filemd5 = header_dic[\'filemd5\']
        return (filename,total_size,filemd5)

    def verification_filemd5(self,filemd5):
        # 判断下载下来的文件MD5值和server传过来的MD5值是否一致
        if self.getfile_md5() == filemd5:
            print(\'\\033[31;1mCongratulations download success\\033[0m\')
        else:
            print(\'\\033[31;1mSorry download failed,download again support breakpoint continuation\\033[0m\')
    def write_file(self,f,get_size,file_size):
        \'\'\'下载文件,将内容写入文件中\'\'\'
        while get_size < file_size:
            res = self.socket.recv(settings.max_recv_bytes)
            f.write(res)
            get_size += len(res)
            self.progress_bar(1,get_size,file_size)  #1表示下载

    def get(self,cmds):
        """从server下载文件到client
        """
        if len(cmds) >1:
            filename = cmds[1]
            self.file_path = os.path.join(settings.down_filepath, filename)
            if os.path.isfile(self.file_path):  #如果文件存在,支持断电续传
                temp_file_size = os.path.getsize(self.file_path)
                self.socket.send(struct.pack(\'i\',temp_file_size))
                header_size = struct.unpack(\'i\',self.socket.recv(4))[0]
                if header_size:
                    filename,file_size,filemd5 = self.recv_file_header(header_size)
                    if temp_file_size == file_size:
                        print(\'\\033[34;1mFile already does exist\\033[0m\')
                    else:
                        print(\'\\033[34;1mFile now is breakpoint continuation\\033[0m\')
                        self.appendfile_content(self.file_path,temp_file_size)
                        self.verification_filemd5(filemd5)
                else:
                    print("\\033[34;1mFile was downloaded before,but now server\'s file is not exist\\033[0m")
            else:#如果文件不存在,则是直接下载
                self.socket.send(struct.pack(\'i\',0))
                obj = self.socket.recv(1024)
                header_size = struct.unpack(\'i\', obj)[0]
                if header_size==0:
                    print("\\033[31;1mfile does not exist!\\033[0m")
                else:
                    filename, file_size, filemd5 = self.recv_file_header(header_size)
                    download_filepath = os.path.join(settings.down_filepath, filename)
                    with open(download_filepath, \'wb\') as f:
                        get_size = 0
                        self.write_file(f, get_size, file_size)
                    self.verification_filemd5(filemd5)
        else:
            print("\\033[31;1muser does not enter file name\\033[0m")

    def ls(self,cmds):
        \'\'\'查看当前工作目录,文件列表\'\'\'
        print("\\033[34;1mview current working directory\\033[0m")
        obj = self.socket.recv(4)
        dir_size = struct.unpack(\'i\',obj)[0]
        recv_size = 0
        recv_bytes = b\'\'
        while recv_size <dir_size:
            temp_bytes = self.socket.recv(settings.max_recv_bytes)
            recv_bytes +=temp_bytes
            recv_size += len(temp_bytes)
        print(recv_bytes.decode(\'gbk\'))

    def mkdir(self,cmds):
        \'\'\'增加目录
        1,server返回1 增加成功
        2,server返回2 增加失败\'\'\'
        print("\\033[34;1madd working directory\\033[0m")
        obj = self.socket.recv(4)
        res = struct.unpack(\'i\',obj)[0]
        if res:
            print(\'\\033[31;1mCongratulations add directory success\\033[0m\')
        else:
            print(\'\\033[31;1mSorry add directory failed\\033[0m\')

    def cd(self,cmds):
        \'\'\'切换目录\'\'\'
        print("\\033[34;1mSwitch working directory\\033[0m")
        if len(cmds) >1:
            obj = self.socket.recv(4)
            res = struct.unpack(\'i\', obj)[0]
            if res:
                print(\'\\033[31;1mCongratulations switch directory success\\033[0m\')
            else:
                print(\'\\033[31;1mSorry switch directory failed\\033[0m\')
        else:
            print("\\033[31;1muser does not enter file name\\033[0m")

    def remove(self,cmds):
        \'\'\'表示删除文件或空文件夹\'\'\'
        print("\\033[34;1mRemove working directory\\033[0m")
        obj = self.socket.recv(4)
        res = struct.unpack(\'i\', obj)[0]
        if res:
            print(\'\\033[31;1mCongratulations remove success\\033[0m\')
        else:
            print(\'\\033[31;1mSorry remove directory failed\\033[0m\')

    def open_sendfile(self,file_size,recv_size =0):
        \'\'\'打开要上传的文件(由于本程序上传文件的原理是先读取本地文件,再写到上传地址的文件)\'\'\'

        with open(self.file_path, \'rb\') as f:
            # send_bytes = b\'\'
            # send_size = 0
            f.seek(recv_size)
            while True:
                data = f.read(1024)
                if data:
                    self.socket.send(data)
                    obj = self.socket.recv(4)
                    recv_size = struct.unpack(\'i\', obj)[0]
                    self.progress_bar(2, recv_size, file_size)
                else:
                    break
        success_state = struct.unpack(\'i\', self.socket.recv(4))[0]
        if success_state:
            print(\'\\033[31;1mCongratulations upload success\\033[0m\')
        else:
            print(\'\\033[31;1mSorry upload directory failed\\033[0m\')

    def put_situation(self,file_size,condition=0):
        \'\'\'上传的时候有两种情况,文件已经存在,文件不存在\'\'\'
        quota_state= struct.unpack(\'i\', self.socket.recv(4))[0]
        if quota_state:
            if condition:
                obj = self.socket.recv(4)
                recv_size = struct.unpack(\'i\', obj)[0]
                self.open_sendfile(file_size,recv_size)
            else:
                self.open_sendfile(file_size)
        else:
            print(\'\\033[31;1mSorry exceeding user quotas\\033[0m\')

    def put(self,cmds):
        """往server端登录的用户目录下上传文件
        """
        if len(cmds) > 1:
            filename = cmds[1]
            self.file_path = os.path.join(settings.upload_filepath, filename)
            if os.path.isfile(self.file_path):  # 如果文件存在,支持断电续传
                self.socket.send(struct.pack(\'i\', 1))
                file_size = os.path.getsize(self.file_path)
                header_dic = {
                    \'filename\': os.path.basename(filename),
                    \'file_md5\': self.getfile_md5(),
                    \'file_size\': file_size
                }
                header_bytes = pickle.dumps(header_dic)
                self.socket.send(struct.pack(\'i\', len(header_bytes)))
                self.socket.send(header_bytes)
                state = struct.unpack(\'i\', self.socket.recv(4))[0]
                if state:  #已经存在
                    has_state = struct.unpack(\'i\', self.socket.recv(4))[0]
                    if has_state:
                        self.put_situation(file_size, 1)
                    else:  # 存在的大小 和文件大小一致 不必再传
                        print("\\033[31;1mfile already does exist!\\033[0m")
                else:  # 第一次传
                        self.put_situation(file_size)
            else:  # 文件不存在
                print("\\033[31;1mfile does not exist!\\033[0m")
                self.socket.send(struct.pack(\'i\', 0))
        else:
            print("\\033[31;1muser does not enter file name\\033[0m")

    def get_recv(self):
        \'\'\'从client端接受发来的数据\'\'\'
        return pickle.loads(self.socket.recv(settings.max_recv_bytes))

    def login(self):
        \'\'\'
        登陆函数,当登陆失败超过三次,则退出
        用户密码发送到server短
        接受server端返回的信息,如果成功返回1,失败返回0
        :return: 如果用户账号密码正确,则返回用户数据的字典
        \'\'\'
        retry_count = 0
        while retry_count <3:
            username = input(\'\\033[34;1mplease input Username:\\033[0m\').strip()
            if not username:
                continue
            password = input(\'\\033[34;1mplease input Password:\\033[0m\').strip()
            user_dic = {
                \'username\':username,
                \'password\':password
            }
            #将用户信息发送到客户端,然后接受客户端的数据
            data = pickle.dumps(user_dic)
            self.socket.send(pickle.dumps(user_dic))
            #为了防止出现黏包问题,所以先解压报头,读取报头,再读数据
            obj = self.socket.recv(4)
            res = struct.unpack(\'i\',obj)[0]
            #此处,如果返回的是代码4001,则成功 4002则失败
            if res:
                print("\\033[32;1m-----------------welcome to ftp client-------------------\\033[0m")
                user_info_dic = self.get_recv()
                recv_username = user_info_dic[\'username\']
                return True
            else:
                print("\\033[31;1mAccount or Passwordoes not correct!\\033[0m")
        retry_count +=1

    def execute(self):
        \'\'\'
        执行,或者实施
        :return:
        \'\'\'
        if self.login():
            while True:
                try:
                    self.help_info()
                    inp = input("Please input a command>>>").strip()
                    if not inp:
                        continue
                    self.socket.send(inp.encode(settings.coding))
                    cmds = inp.split()
                    if hasattr(self, cmds[0]):
                        func = getattr(self, cmds[0])
                        func(cmds)
                        break
                    else:
                        print(\'\\033[31;1mNo such command ,please try again\\033[0m\')
                except Exception as e:  # server关闭了
                    print(\'\\033[31;1m%s\\033[0m\'%e)
                    break

    def help_info(self):
        print (\'\'\'\\033[34;1m
              get + (文件名)    表示下载文件
              put + (文件名)    表示上传文件
              ls                 表示查询当前目录下的文件列表(只能访问自己的文件列表) 
              mkdir + (文件名)  表示创建文件夹  
              cd + (文件名)     表示切换目录(只能在自己的文件列表中切换)
              remove + (文件名) 表示删除文件或空文件夹
        \\033[0m\'\'\')

  

以上是关于如何组建FTP服务器?支持多用户的主要内容,如果未能解决你的问题,请参考以下文章

多用户登录ftp

支持多用户在线的Ftp程序

安装下载yum,组建FTP服务器

106 网络编程实战之基于socketserver实现多用户FTP服务器

Python 程序:ftp

windows环境下c语言支持ftp和http多线程下载的客户端