小程序5:FTP程序

目录

1.FTP程序所需要的知识点

2.FTP程序具体实现过程

  2.1 FTP程序之注册功能

  2.2 FTP程序之登录功能

  2.3 FTP程序之下载功能

3.FTP程序源代码

FTP程序所需要的知识点

1.socketserver并发编程

2.连续send,recv黏包现象:struct

3.hashlib模块的md5加密

4.静态方法staticmethod和类方法classmethod

5.json序列化

6.反射:hasattr,setattr

7.os模块相关方法

FTP程序具体实现过程

FTP程序之注册功能

1.要明确,FTP程序是要实现服务端的并发的,所以需要引入socketserver模块来实现并发

2.写服务端下socketserver的基本语法[day31:socketserver的基本语法]

# 服务端
import socketserver

class FTPServer(socketserver.BaseRequestHandler):
    def handle(self):
        pass

myserver = socketserver.ThreadingTCPServer(("127.0.0.1",9000),FTPServer)
myserver.serve_forever()


# 客户端
import socket
sk = socket.socket()
sk.connect(("127.0.0.1",9000))

sk.close()

3.用户需要自己输入账号和密码,所以在客户端需要写输入用户名和密码的方法(输入用户名和密码后,发送给服务端)

4.在客户端定义auth方法,先写两个input输入用户名和密码

5.输入完用户名密码之后,怎样将用户信息传给服务端呢?

将用户名和密码以及操作做成一个字典,并用json序列化成字符串,并encode后,使用sk.send()发送给服务端

这部分的具体代码如下所示:

# 客户端
def auth(opt):
    usr = input("username:").strip()
    pwd = input("password:").strip()
    dic = {"user":usr,"passwd":pwd,"operate":opt}
    str_dic = json.dumps(dic) # 将字典序列化成字符串
    sk.send(str_dic.encode()) # 将字符串转化成字节流并发送出去

auth("register")

6.服务端已经将用户名密码和操作发过去了,所以现在服务端需要接收一下,服务端的整体逻辑写在类中的handle方法

再定义一个专门用来接收的方法myrecv,并使用handle方法去调用myrecv方法

这部分的具体代码如下所示:

# 服务端
class FTPServer(socketserver.BaseRequestHandler):
    def handle(self):
        opt_dic = self.myrecv()
        print(opt_dic)

    def myrecv(self):
        info = self.request.recv(1024)
        opt_str = info.decode()
        opt_dic = json.loads(opt_str)
        return opt_dic

通过以上步骤,我们实现了一収一发

7.接收到了客户端发来的数据,我们就可以在服务端写一些关于注册的逻辑

在服务端定义Auth类,专门用来实现注册登录,在handler方法也可以去调用类中的成员

那么Auth类中应该写什么呢?

1.首先在当前目录创建db文件夹,并在db问文件夹中创建userinfo.txt用来存放用户名和密码

2.对密码使用md5加密

8.在Auth类中定义md5方法,用来对密码进行一个加密操作

# 服务端
class Auth():
    def md5(usr,pwd):
        md5_obj = hashlib.md5(usr.encode())
        md5_obj.update(pwd.encode())
        return md5_obj.hexdigest()

我们先加密一份数据存放到userinfo.txt中

9.现在已经对每个用户名的密码加密了,但是还有一个问题需要考虑,在注册的时候,不能注册已经存在的用户名,所以需要对用户名进行判断

10.定义register方法,并使用classmethod装饰器,当其他类调用register方法时,会自动传递类参数.

11.拼接出一个userinfo所在文件的完整路径

1.首先获取当前文件(server.py)所在的位置

两种方法:

  方法一:os.getcwd()

  方法二:os.path.dirname(__file__)

print(os.getcwd()) # F:OldBoyPythonweek6day36
print(__file__) # F:/OldBoyPython/week6/day36/ceshi.py
print(os.path.dirname(__file__)) # F:/OldBoyPython/week6/day36

2.使用os.path.join进行路径拼接

base_path = os.getcwd()
userinfo = os.path.join(base_path,"db","userinfo.txt")
print(userinfo) # F:OldBoyPythonweek6day36dbuserinfo.txt

这样,我们就获取到了userinfo.txt的绝对路径了

12.当有了userinfo.txt的绝对路径后,我们就可以开始文件操作

在第9步,我们说到要检测用户名是否存在,现在我们就可以实现了

当用户名存在时,返回一个状态False和一个用户名已存在信息提示

@classmethod
def register(cls, opt_dic):
    with open(userinfo, mode='r', encoding='utf-8') as fp:
        for line in fp:
            username = line.split(":")[0]
            if username == opt_dic["user"]:
                return {"result": False, "info": "用户名存在了"}

13.用户名存在的逻辑已经写完,接下来就是用户名可以使用的逻辑

要注意:密码需要加密后再写入

with open(userinfo, mode='a+', encoding='utf-8') as fp:
    # 账号就是字典的账号,密码使用md5加密处理后再写入文件
    strvar = "%s:%s
" % (opt_dic["user"], cls.md5(opt_dic["user"], opt_dic["passwd"]))
    fp.write(strvar)

如果登录成功了,返回一个状态True和一个注册成功信息提示

到此,注册部分的逻辑就已经写完了,具体代码如下所示:

@classmethod
def register(cls, opt_dic):
    # 1.检测注册的用户是否存在
    with open(userinfo, mode='r', encoding='utf-8') as fp:
        for line in fp:
            username = line.split(":")[0]
            if username == opt_dic["user"]:
                return {"result": False, "info": "用户名存在了"}
    # 2.当前用户可以注册
    with open(userinfo, mode='a+', encoding='utf-8') as fp:
        # 账号就是字典的账号,密码使用md5加密处理后再写入文件
        strvar = "%s:%s
" % (opt_dic["user"], cls.md5(opt_dic["user"], opt_dic["passwd"]))
        fp.write(strvar)

    # 3.返回一个注册成功的状态
    return {"result": True, "info": "注册成功"}

14.注册的register方法已经写完,但是现在我们需要将register方法和下面的FTPServer类建立联系,这个时候就需要使用反射来实现了

换句话来说:就是想在FTPServer的handle方法中使用Auth中的register方法

15.构建出反射,代码如下所示

到目前为止,基本的代码已经实现,现进行测试,代码如下所示

# 服务端
import socketserver
import json
import hashlib
import os


# 找当前数据库文件所在的绝对路径
base_path = os.getcwd()
# F:OldBoyPythonweek6day36dbuserinfo.txt
userinfo = os.path.join(base_path,"db","userinfo.txt")

class Auth():
    @ staticmethod
    def md5(usr,pwd):
        md5_obj = hashlib.md5(usr.encode())
        md5_obj.update(pwd.encode())
        return md5_obj.hexdigest()

    @ classmethod
    def register(cls,opt_dic):
        # 1.检测注册的用户是否存在
        with open(userinfo,mode='r',encoding='utf-8') as fp:
            for line in fp:
                username = line.split(":")[0]
                if username == opt_dic["user"]:
                    return {"result":False,"info":"用户名存在了"}
        # 2.当前用户可以注册
        with open(userinfo, mode='a+', encoding='utf-8') as fp:
            # 账号就是字典的账号,密码使用md5加密处理后再写入文件
            strvar = "%s:%s
" % (opt_dic["user"],cls.md5(opt_dic["user"],opt_dic["passwd"]))
            fp.write(strvar)

        # 3.返回一个注册成功的状态
        return {"result":True,"info":"注册成功"}

class FTPServer(socketserver.BaseRequestHandler):
    def handle(self):
        opt_dic = self.myrecv()
        print(opt_dic)
        if hasattr(Auth,"register"):
            res = getattr(Auth,"register")(opt_dic)
            print(res)


    def myrecv(self):
        info = self.request.recv(1024)
        opt_str = info.decode()
        opt_dic = json.loads(opt_str)
        return opt_dic

myserver = socketserver.ThreadingTCPServer(("127.0.0.1",9000),FTPServer)
myserver.serve_forever()
# 客户端
import socket
import json

sk = socket.socket()
sk.connect(("127.0.0.1",9000))

# 处理収发数据的逻辑
def auth(opt):
    usr = input("username:").strip()
    pwd = input("password:").strip()
    dic = {"user":usr,"passwd":pwd,"operate":opt}
    str_dic = json.dumps(dic) # 将字典序列化成字符串
    sk.send(str_dic.encode()) # 将字符串转化成字节流并发送出去

auth("register")

sk.close()

运行结果如下图所示

客户端输入用户名和密码

服务端接收到客户端发来的数据

并且userinfo.txt也已经写入了你刚才在客户端输入的用户名和密码

16.在服务端我们可以看到注册成功/注册失败的信息了,现在我们想把这个信息发回给客户端,在客户端也能显示出来

和服务端的myrecv方法一样,我们需要自定义一个接収方法mysend

既然在服务端发数据,当然要在客户端接收数据

好的,到此第一部分注册功能就全部完成了。让我们看一下运行结果

所有的信息都应该是显示在客户端上的

FTP程序之登录功能

1.现在添加了登录功能,所以反射的时候就要动态起来。

2.Auth类中只有注册和登录两个方法,如果用户在客户端传入其他方法,必须要给予错误的提示

下面,我们来测试一下结果

3.现在就可以开始写登录函数的逻辑了。。。

登录嘛,肯定是要验证用户名和密码的,所以肯定需要从userinfo.txt中取出用户名和密码进行比对

所以先进行文件操作,将用户名和密码取出来,在进行验证

@ classmethod
def login(cls,opt_dic):
    with open(userinfo,mode='r',encoding='utf-8') as fp:
        for line in fp:
            username,password = line.strip().split(":")
            if username == opt_dic["user"] and password == cls.md5(opt_dic["user"],opt_dic["passwd"]):
                return {"result":True,"info":"登陆成功"}

        return {"result":False,"info":"登录失败"}

其他的部分都不用改,定义了login函数,FTPServer就会自己识别是什么操作,并且通过反射获取到对应方法的返回值,将返回值发送给客户端,然后客户端接收后,打印出来

运行结果如下图所示

4.到此,登录部分的逻辑也已经完成了!!

但是在客户端调用时,还是非常死板的

这种调用方式非常的lowb,所以需要改进一下。。

我们需要搞一个界面。

5.先在客户端定义login函数和register函数,在函数里进行调用。

6.除了登录和注册函数,还需要搞一个退出的功能

在客户端定义myexit函数,用来实现退出的功能

现在我们在客户端已经定义了退出函数,但是在服务端我们也要让服务端知道退出的状态。

我们在客户端发送了一个opt_dic给服务端,然后服务端接收这个opt_dic

到此,退出功能就已经实现完了。

7.现在我们需要把登录,注册和退出形成一套界面

def main():
    # 生成菜单界面
    for i,tup in enumerate(operate_lst,start=1):
        print(i,tup[0])
    
    # 输入相应序号,实现对应操作
    num = int(input("请选择您要进行的操作>>>"))
    res = operate_lst[num-1][1]()
    return res # 将对应操作的返回值返回出来

while True:
    res = main() # 调用main获取到对应的返回值
    print(res)

在客户端我们可以通过while True实现循环调用main,进而可以进行循环登录注册和退出。

那么在服务端我们也应该是循环进行调用注册登录和退出

所以需要在服务端也加上一个while True

8.到此为止,登录,注册和退出的功能就都已经实现了。

代码如下所示

# 服务端
import socketserver
import json
import hashlib
import os

# 找当前数据库文件所在的绝对路径
base_path = os.path.dirname(__file__)
# /mnt/hgfs/python31_gx/day36/db/userinfo.txt
userinfo = os.path.join(base_path,"db","userinfo.txt")

class Auth():
    @staticmethod
    def md5(usr,pwd):
        md5_obj = hashlib.md5(usr.encode())
        md5_obj.update(pwd.encode())
        return md5_obj.hexdigest()

    @classmethod
    def register(cls,opt_dic):
        # 1.检测注册的用户是否存在
        with open(userinfo,mode="r",encoding="utf-8") as fp:
            for line in fp:
                username = line.split(":")[0]
                if username == opt_dic["user"]:
                    return {"result":False,"info":"用户名存在了"}
                    
        # 2.当前用户可以注册
        with open(userinfo,mode="a+",encoding="utf-8") as fp:
            strvar = "%s:%s
" % (   opt_dic["user"] , cls.md5(   opt_dic["user"],opt_dic["passwd"]   )    )
            fp.write(strvar)
                    
        """
            当用户上传的时候,给他创建一个专属文件夹,存放数据
        """
        
        # 3.返回状态
        return {"result":True,"info":"注册成功"}
        
    @classmethod
    def login(cls,opt_dic):
        with open(userinfo , mode="r" , encoding="utf-8") as fp:
            for line in fp:
                username,password = line.strip().split(":")
                if username == opt_dic["user"] and password == cls.md5( opt_dic["user"] , opt_dic["passwd"] ) :
                    return {"result":True,"info":"登录成功"}
             
            return {"result":False,"info":"登录失败"}
        
    @classmethod
    def myexit(cls,opt_dic):
        return {"result":"myexit"}
        

class FTPServer(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            opt_dic = self.myrecv()
            print(opt_dic) # {'user': 'wangwen', 'passwd': '111', 'operate': 'register'}
            if hasattr(Auth,opt_dic["operate"]):
                # print(  getattr(Auth,"register")   )
                res = getattr(Auth,opt_dic["operate"])(opt_dic) # login(opt_dic)
                
                # 如果接受的操作是myexit,代表退出
                if res["result"] == "myexit":
                    return 
                
                # 把注册的状态发送给客户端
                self.mysend(res)
            else:
                dic = {"result":False,"info":"没有该操作"}
                self.mysend(dic)

    # 接收方法
    def myrecv(self):
        info = self.request.recv(1024)
        opt_str = info.decode()
        opt_dic = json.loads(opt_str)
        return opt_dic        
        
    # 发送方法
    def mysend(self,send_info):
        send_info = json.dumps(send_info).encode()
        self.request.send(send_info)

# 设置一个端口可以绑定多个程序
# socketserver.TCPServer.allow_reuse_address = True
myserver = socketserver.ThreadingTCPServer( ("127.0.0.1",9000) , FTPServer)
myserver.serve_forever()
# ### 客户端
import socket
import json
""""""
sk = socket.socket()
sk.connect( ("127.0.0.1",9000) )

# 处理收发数据的逻辑
def auth(opt):
    usr = input("username: ").strip()
    pwd = input("password: ").strip()
    dic = {"user":usr,"passwd":pwd,"operate":opt}
    str_dic = json.dumps(dic)
    # 发送数据
    sk.send(str_dic.encode("utf-8"))
    
    # 接受服务端响应的数据
    file_info = sk.recv(1024).decode()
    file_dic = json.loads(file_info)
    return file_dic
    


# 注册
def register():
    res = auth("register")
    return res

# 登录
def login():
    res = auth("login")
    return res
    
# 退出
def myexit():    
    opt_dic = {"operate":"myexit"}
    sk.send(json.dumps(opt_dic).encode())
    exit("欢迎下次再来")
    

# 第一套操作界面
#                      0                  1                2
operate_lst1 = [ ("登录",login) ,("注册",register) , ("退出",myexit) ]



"""
1.登录
2.注册
3.退出

1 ('登录', <function login at 0x7ff7cf171a60>)
2 ('注册', <function register at 0x7ff7cf17e620>)
3 ('退出', <function myexit at 0x7ff7cf171ae8>)
"""



def main():
    for i,tup in enumerate(operate_lst1,start=1):
        print(i , tup[0])
    num = int(input("请选择执行的操作>>> ").strip()) # 1 2 3
    # 调用函数
    # print(operate_lst1[num-1]) ('退出', <function myexit at 0x7f801e34aa60>)
    # print(operate_lst1[num-1][1]) <function myexit at 0x7f801e34aa60>
    # operate_lst1[num-1][1]() myexit()
    res = operate_lst1[num-1][1]()
    return res
    
while True:
    # 开启第一套操作界面
    res = main()
    print(res)

sk.close()

执行结果如下图所示

FTP注册之下载功能

1.当你登录成功后,要跳转到另一套界面,让用户选择下载上传还是退出

所以我们需要像登录注册退出那套界面逻辑一样,再搞一个operate_lst2

只有登录成功的时候,才能出现第二套界面。

2.客户端现在已经发送过去了,那么对应的服务端也应该有所接收

 

3.download我们后面再说,先把界面2的退出搞定

同理,客户端的myexit有exit()直接终止程序,在服务端也要及时终止程序

直接搞上一个return,连循环加函数全都退出

到此,界面2的退出也已经搞定了,接下来就搞最复杂的download

4.下载,先搞一下这个客户端

在客户端定义一个download方法,定义一个字典,字典里写入操作和下载的文件名

5.客户端定义了下载方法将字典发送过去,服务端也应该定义download下载方法来接收这个字典并进行逻辑操作

# 服务端
def download(self, opt_dic):
    filename = opt_dic["filename"]  # 获取用户在客户端输入的文件名
    file_abs = os.path.join(base_path, "video", filename)  # 获取到要下载视频的绝对路径
    if os.path.exists(file_abs):  # 如果文件存在
        dic = {"result": True, "info": "文件存在,可以下载"}
        self.mysend()
    else:  # 如果文件不存在
        pass

6.如果文件存在可以下载,那么就可以执行下载的流程了

在下载时,服务端需要将视频发送给客户端,因为视频很大,且需要分段发送,所以可能会存在黏包现象。

所以需要引入struct模块,并改造mysend方法,以解决黏包现象

# 服务端
def mysend(self, send_info, sign=False):
    send_info = json.dumps(send_info).encode()
    if sign:
        # 1.发送数据的长度
        res = struct.pack("i", len(send_info))
        self.request.send(res)
    # 2.发送真实的数据
    self.request.send(send_info)
# 客户端
def myrecv(info_len=1024,sign=False):
    if sign:
        # 1.接受数据的长度
        info_len = sk.recv(4)
        info_len = struct.unpack("i",info_len)[0]

    # 2.接受真实的数据
    file_info = sk.recv(info_len).decode()
    file_dic = json.loads(file_info)
    return file_dic

7.客户端向服务端发送下载操作和要下载的文件名,服务端接收到文件名称,返回一个可以下载的状态给客户端

8.刚才服务端已经将文件存在,可以下载的提示信息发给客户端了,接下来服务端要发送客户端要下载的视频的文件名字和文件大小

9.现在该发的都发了,最后一步就是发送真实的内容

10.现在几乎是已经大功告成了,还差最后一点小瑕疵

登录功能的第7步,我们说到,要想进行循环操作(循环选择下载上传和退出),需要在客户端和服务端加while True

11.到此!!所有功能实现完毕

运行结果如下图所示

这个时候,我们去download文件夹,可以查看到下载的视频

FTP程序源代码

客户端

# 客户端
import socket
import json
import struct
import os

sk = socket.socket()
sk.connect(("127.0.0.1",9000))

def myrecv(info_len=1024,sign=False):
    if sign:
        info_len = sk.recv(4)
        info_len = struct.unpack("i",info_len)[0]

    file_info = sk.recv(info_len).decode()
    file_dic = json.loads(file_info)
    return file_dic


# 处理収发数据的逻辑
def auth(opt):
    usr = input("username:").strip()
    pwd = input("password:").strip()
    dic = {"user":usr,"passwd":pwd,"operate":opt}
    str_dic = json.dumps(dic) # 将字典序列化成字符串
    sk.send(str_dic.encode()) # 将字符串转化成字节流并发送出去
    return myrecv()


def login():
    res = auth("login")
    return res

def register():
    res = auth("register")
    return res

def myexit():
    opt_dic = {"operate":"myexit"}
    sk.send(json.dumps(opt_dic).encode())
    exit("欢迎下次再来")

def download():
    operate_dict = {
        "operate":"download",
        "filename":"ceshi123.mp4"
    }
    # 把要下载的文件名称传递给服务端
    operate_str = json.dumps(operate_dict)
    sk.send(operate_str.encode("utf-8"))

    # 接受服务端发过来的数据(是否可以操作)
    res = myrecv(sign=True)
    print(res)

    # 1.如果收到了服务端的可以下载的提示,就创建一个文件夹用来存放下载的视频
    if res["result"]:
        try:
            os.mkdir("mydownload")
        except:
            pass
    else:
        print("没有该文件")
    # 2.接受文件名字和文件大小
    dic = myrecv(sign=True)
    print(dic)
    # 3.接収真实的文件
    with open("./mydownload/" + dic["filename"],mode='wb') as fp:
        while dic["filesize"]:
            content = sk.recv(102400)
            fp.write(content)
            dic["filesize"] -= len(content)
    print("客户端下载完毕")





operate_lst1 = [("注册",register),
               ("登录",login),
               ("退出",myexit)]
operate_lst2 = [("下载",download),
                ("退出",myexit)]

def main(operate_lst):
    for i,tup in enumerate(operate_lst,start=1):
        print(i,tup[0])

    num = int(input("请选择您要进行的操作>>>"))
    res = operate_lst[num-1][1]()
    return res

while True:
    res = main(operate_lst1)
    if res["result"]:
        while True:
            res = main(operate_lst2)


sk.close()

服务端

# 服务端
import socketserver
import json
import hashlib
import os
import struct


# 找当前数据库文件所在的绝对路径
base_path = os.getcwd()
# F:OldBoyPythonweek6day36dbuserinfo.txt
userinfo = os.path.join(base_path,"db","userinfo.txt")

class Auth():
    @ staticmethod
    def md5(usr,pwd):
        md5_obj = hashlib.md5(usr.encode())
        md5_obj.update(pwd.encode())
        return md5_obj.hexdigest()

    @ classmethod
    def register(cls,opt_dic):
        # 1.检测注册的用户是否存在
        with open(userinfo,mode='r',encoding='utf-8') as fp:
            for line in fp:
                username = line.split(":")[0]
                if username == opt_dic["user"]:
                    return {"result":False,"info":"用户名存在了"}
        # 2.当前用户可以注册
        with open(userinfo, mode='a+', encoding='utf-8') as fp:
            # 账号就是字典的账号,密码使用md5加密处理后再写入文件
            strvar = "%s:%s
" % (opt_dic["user"],cls.md5(opt_dic["user"],opt_dic["passwd"]))
            fp.write(strvar)

        # 3.返回一个注册成功的状态
        return {"result":True,"info":"注册成功"}

    @ classmethod
    def login(cls,opt_dic):
        with open(userinfo,mode='r',encoding='utf-8') as fp:
            for line in fp:
                username,password = line.strip().split(":")
                if username == opt_dic["user"] and password == cls.md5(opt_dic["user"],opt_dic["passwd"]):
                    return {"result":True,"info":"登陆成功"}

            return {"result":False,"info":"登录失败"}

    @ classmethod
    def myexit(cls,opt_dic):
        return {"result":"myexit"}

class FTPServer(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            opt_dic = self.myrecv()
            print(opt_dic) # {'user': 'libolun', 'passwd': '111', 'operate': 'register'}
            if hasattr(Auth,opt_dic["operate"]):
                res = getattr(Auth,opt_dic["operate"])(opt_dic)
                if res["result"] == "myexit":
                    return
                self.mysend(res)

                if res["result"]: # 接受界面2数据
                    while True:
                        opt_dic = self.myrecv()
                        print(opt_dic)

                        if opt_dic["operate"] == "myexit":
                            return

                        if hasattr(self,opt_dic["operate"]):
                            getattr(self,opt_dic["operate"])(opt_dic)



            else:
                dic = {"result":False,"info":"没有该操作"}
                self.mysend(dic)


    def myrecv(self):
        info = self.request.recv(1024)
        opt_str = info.decode()
        opt_dic = json.loads(opt_str)
        return opt_dic

    def mysend(self,send_info,sign=False):
        send_info = json.dumps(send_info).encode()
        if sign:
            res = struct.pack("i",len(send_info))
            self.request.send(res)
        self.request.send(send_info)

    def download(self,opt_dic):
        filename = opt_dic["filename"] # 获取用户在客户端输入的文件名
        file_abs = os.path.join(base_path,"video",filename) # 获取到要下载视频的绝对路径
        if os.path.exists(file_abs): # 如果文件存在
            # 1.告诉客户端,文件存在,可以下载
            dic = {"result":True,"info":"文件存在,可以下载"}
            self.mysend(dic,sign=True)

            # 2.发送文件的名字和文件的大小
            filesize = os.path.getsize(file_abs)
            dic = {"filename":filename,"filesize":filesize}
            self.mysend(dic,sign=True)

            # 3.真正开始发送数据
            with open(file_abs,mode='rb') as fp:
                while filesize:
                    content = fp.read(102400)
                    self.request.send(content)
                    filesize -= len(content)
            print("服务器下载完毕")
        else:
            dic = {"result":False,"info":"文件不存在"}
            self.mysend(dic,sign=True)


myserver = socketserver.ThreadingTCPServer(("127.0.0.1",9000),FTPServer)
myserver.serve_forever()
原文地址:https://www.cnblogs.com/libolun/p/13543225.html