简介

  本示例主要是用Python的socket,使用UDP协议实现一个FTP服务端、FTP客户端,用来实现文件的传输。在公司内网下,可以不适用U盘的情况下,纯粹使用网络,来实现文件服务器的搭建,进而实现文件的网络传输。同时用来理解Python的socket使用。

  服务端运行起来后,会把服务器上面的指定目录作为根目录提供给客户端使用,即客户端可以访问、下载服务端设置的根目录里面的文件内容

  客户端和服务端之间支持的一些简单命令如下:

  • “ll”或者“ls”  查看当前目录下的所有文件或者目录
  • “pwd”  查看当前所在的目录(根目录是服务端设置的“D:\var”目录)
  • “get 文件名”  下载指定的文件到客户端配置的目录(客户端指定的根目录,在运行时配置)
  • “get 目录”  下载指定的目录到客户端配置的目录
  • “get all”  把当前所在的目录的所有文件、目录下载到客户端配置的目录
  • “cd”  把客户端的目录切换到根目录
  • “cd 目录” 把客户端的目录切换到指定的目录
  • “cd ..” 把客户端的目录切换到上一级目录

  客户端和服务端之间的通信,是把dict格式使用pickle.dumps()和pickle.loads()转成对应的bytes类型进行传输的。dict格式参考代码。

代码

file_handler.py

  该文件就是把对文件的一些操作进行提取出来,供UDP服务端使用

  1.  
  1. import os
    import logging
    import traceback

    LOG_FORMAT = "%(asctime)s - %(levelname)s [%(filename)s-%(funcName)s] Line: %(lineno)s] - %(message)s"
    logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)
    _logger = logging.getLogger()

    class FileHandler:
    def __init__(self, logger=None):
    """
    由于设置成可以支持自定义的logger,因此就没有设置成工具类形式,使用的时候,还是要实例化该类的
    :param logger:
    """
    self.__logger = logger if logger else _logger

    def list_dir(self, dir_path: str, return_absolute_path=False) -> list:
    """
    获取指定目录下面的文件或者目录的列表
    返回列表,里面中的每一个元素都是元组,元组的第一个值是文件或者目录名,第二个值是"d"(代表目录)或者"f"(代表文件)
    :param dir_path:
    :param return_absolute_path: True: 返回值是返回文件或者目录的绝对路径; False: 只返回文件名或者目录名
    :return: [('download', 'd'), ('mylog.txt', 'f')]
    """
    ret_list = []
    try:
    if os.path.exists(dir_path):
    ret = os.listdir(dir_path)
    for item in ret:
    if os.path.isdir(os.path.join(dir_path, item)):
    if return_absolute_path:
    ret_list.append((os.path.join(dir_path, item), 0, "d"))
    else:
    ret_list.append((item, 0, "d"))
    if os.path.isfile(os.path.join(dir_path, item)):
    size = os.path.getsize(os.path.join(dir_path, item))
    if return_absolute_path:
    ret_list.append((os.path.join(dir_path, item), size, "f"))
    else:
    ret_list.append((item, size, "f"))
    except Exception:
    self.__logger.error("Can not list dir: [%s]" % dir_path + traceback.format_exc())
    finally:
    return ret_list

    def seek_file(self, file_path: str, start_index: int, length=1024) -> tuple:
    """
    通过二进制格式读取指定一个文件指定范围的内容
    :param file_path:
    :param start_index:
    :param length: 读取的字节数
    :return:
    """
    # 下一次访问的时候的起始start_index值。 -1代表已经访问到文件结尾了,不用再访问该文件了。
    content_bytes = b''
    next_index = -1
    if not os.path.exists(file_path):
    message = "File[%s] not exists !!!" % file_path
    self.__logger.error(message)
    raise Exception(message)
    file_size = os.path.getsize(file_path) # 文件大小

    if start_index >= file_size:
    return content_bytes, next_index
    try:
    # print("### file_size: ", file_size)
    with open(file_path, "rb") as fh:
    fh.seek(start_index) # 游标跳到指定位置
    content_bytes = fh.read(length) # 读取文件内容
    # print("content_bytes: ", content_bytes)
    # print("type(content_bytes): ", type(content_bytes))
    if start_index + length < file_size:
    next_index = start_index + length
    except Exception:
    self.__logger.error("Seek file exception !!! " + traceback.format_exc())
    finally:
    return content_bytes, next_index

    if __name__ == '__main__':
    file = r"D:\var\download\system.log"
    file_target = r"D:\var\download\system.txt"
    file = r"D:\软件安装包\NetAssist.exe"
    file_target = r"D:\软件安装包\NetAssist_copy.exe"
    file_obj = FileHandler()
    # ret = file_obj.seek_file(file, start_index=17, length=30)
    # print("ret: ", ret)
    # file_obj.copy_file(file, file_target, 1024 * 1000)
  1.  

服务端代码

  1. """
  2. 使用socket的udp协议实现的一个ftp服务端。
  3. 服务器和客户端之间传递数据格式:
  4. 1、服务端和客户端统一使用Python的字典格式(也就是本例中自定义的"通信协议"),格式形如:
  5. {
  6. "type": "cmd", # 支持的值有: "cmd"、"download"
  7. "body": "ll", # 在cmd模式下,常用的命令有: ll、ls、cd 指定目录、pwd
  8. "pwd": ["folder1", "folder2"],
  9. "status": 1,
  10. "uuid": "b93e21e659f711ee9285a46bb6f59f55" # uuid.uuid1().hex,用来保证客户端和服务端
  11. }
  12. 2、客户端使用pickle模块的pickle.dumps(dict类型数据)把要发送的数据转成bytes类型
  13. 3、客户端使用socket的udp传输转成的bytes数据给服务端
  14. 4、服务端接收到从客户端发送过来的bytes类型的数据,再使用pickle.loads(bytes类型数据)把数据转成原始的dict类型。
  15.  
  16. 使用socket的udp,既能接收数据,又能发送数据,因此服务端和客户端都是相对的。
  17. """
  18.  
  19. import os
  20. import pickle
  21. import sys
  22. import socket
  23. import logging
  24. import time
  25. import traceback
  26. from file_handler import FileHandler
  27.  
  28. LOG_FORMAT = "%(asctime)s - %(levelname)s [%(filename)s-%(funcName)s] Line: %(lineno)s] - %(message)s"
  29. logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)
  30. _logger = logging.getLogger()
  31.  
  32. _HOST = "127.0.0.1"
  33. _PORT = 8090 # 服务器端口号
  34. _ROOT_DIR = r"D:\var" # 服务端给客户端展示的可以进行下载的文件路径
  35.  
  36. __all__ = ["FTPServerUDP"]
  37.  
  38. class FTPServerUDP:
  39. def __init__(self, host="", port=None, root_dir="", logger=None):
  40. self.__host = host if host else _HOST
  41. self.__port = port if port else _PORT
  42. self.__root_dir = root_dir if root_dir else _ROOT_DIR
  43. self.__logger = logger if logger else _logger
  44. self.__file_handler = FileHandler()
  45.  
  46. self.__socket_obj = self.__get_socket_obj()
  47.  
  48. self.__message_type_unsupported = "Unsupported message type"
  49. self.__message_type_server_inner_error = "Server internal error"
  50. self.__message_type_path_not_exists = "Target path not exists"
  51. self.__message_type_ok = "ok"
  52.  
  53. def __get_socket_obj(self) -> socket.socket:
  54. socket_obj = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP
  55. socket_obj.bind((self.__host, self.__port)) # 服务端必须绑定IP和端口
  56. return socket_obj
  57.  
  58. def __message_handler(self, message_tuple: tuple):
  59. # 给客户端返回的数据类型,最终还需要使用pickle.dumps()把字典类型转成bytes类型进行发送
  60. response_message_dict = {
  61. "type": "cmd",
  62. "body": self.__message_type_unsupported,
  63. "pwd": [], # 客户端的相对于服务端self.__root_dir的相对路径
  64. "status": 0 # 0:代表后端处理结果异常; 1:代表后端处理结果正常
  65. }
  66.  
  67. recv_message_bytes = message_tuple[0]
  68. client_tuple = message_tuple[1] # 客户端元组信息,形如 ('127.0.0.1', 59699)
  69.  
  70. # 确保接收到的消息是 pickle.dumps() 转成的bytes类型,并把数据转成dict类型
  71. # recv_message_dict 的格式形如上面的response_message_dict格式
  72. recv_message_dict = self.__check_recv_message_type(recv_message_bytes)
  73. if not recv_message_dict:
  74. response_message_dict["body"] = self.__message_type_unsupported
  75. response_message_dict["status"] = 0
  76. self.__send_bytes_message(response_message_dict, client_tuple)
  77. return
  78. # 把客户端的进入的目录,赋值给返回值的目录。
  79. response_message_dict["pwd"] = recv_message_dict.get("pwd", [])
  80.  
  81. # 把客户端传递进来的uuid,赋值给返回值的目录
  82. response_message_dict["uuid"] = recv_message_dict.get("uuid", "")
  83.  
  84. # 接收到的消息符合规范,就需要根据"type"类型进行分类。
  85. try:
  86. print("recv_message_dict: ", recv_message_dict)
  87. if recv_message_dict.get("type", "") == "cmd":
  88. self.__cmd_handler(recv_message_dict, response_message_dict, client_tuple)
  89. elif recv_message_dict.get("type", "") == "download":
  90. self.__download_handler(recv_message_dict, response_message_dict, client_tuple)
  91. pass
  92. except Exception:
  93. self.__logger.error("Server message handler exception !!!" + traceback.format_exc())
  94. response_message_dict["status"] = 0
  95. response_message_dict["body"] = self.__message_type_server_inner_error
  96. self.__send_bytes_message(response_message_dict, client_tuple)
  97.  
  98. def __check_recv_message_type(self, recv_message_bytes: bytes) -> dict:
  99. """
  100. 确保接收到的消息是 pickle.dumps() 转成的bytes类型,并且通过pickle.loads()把bytes类型的数据转成dict后,
  101. dict类型数据中要有: "type"、"body"、"pwd"、"uuid" 等字段
  102. :param recv_message_bytes:
  103. :return:
  104. """
  105. ret_dict = {}
  106. try:
  107. message_dict = pickle.loads(recv_message_bytes)
  108. except Exception:
  109. return ret_dict
  110.  
  111. if not isinstance(message_dict, dict):
  112. return ret_dict
  113.  
  114. # 接收到的dict类型的消息中,必须要有如下的字段
  115. if not {"type", "body", "pwd", "uuid"}.issubset(set(message_dict.keys())):
  116. return ret_dict
  117.  
  118. return message_dict
  119.  
  120. def __send_bytes_message(self, message_dict: dict, client_tuple: tuple):
  121. """
  122. 使用pickle.dumps()把字典格式的消息(message_dict),转成bytes发送给客户端(client_tuple)
  123. :param message_dict:
  124. :param client_tuple:
  125. :return:
  126. """
  127. try:
  128. message_bytes = pickle.dumps(message_dict)
  129. self.__socket_obj.sendto(message_bytes, client_tuple)
  130. except Exception:
  131. self.__logger.error("Send message to client exception !!! " + traceback.format_exc())
  132. message_dict["status"] = 0
  133. message_dict["body"] = self.__message_type_server_inner_error
  134. message_bytes = pickle.dumps(message_dict)
  135. self.__socket_obj.sendto(message_bytes, client_tuple)
  136.  
  137. def __cmd_handler(self, recv_message_dict: dict, response_message_dict: dict, client_tuple: tuple):
  138. """
  139. 处理消息体的type参数值是"cmd",这种命令行的消息
  140. :param recv_message_dict:
  141. :param response_message_dict:
  142. :param client_tuple:
  143. :return:
  144. """
  145. cmd_str = recv_message_dict.get("body", "").lower().strip()
  146. if not cmd_str or (not isinstance(cmd_str, str)):
  147. # 返回值形如: {'type': 'cmd', 'body': 'Unsupported message type', 'pwd': ['download']}
  148. response_message_dict["body"] = self.__message_type_unsupported
  149. response_message_dict["status"] = 0
  150. self.__send_bytes_message(response_message_dict, client_tuple)
  151. return
  152.  
  153. if cmd_str in ("ls", "ll"): # 查看当前目录的文件目录列表
  154. # 返回值形如: {'type': 'cmd', 'body': [('folder1', 'd'), ('file1.txt', 'f')]}
  155. customer_dir = self.__root_dir
  156. if recv_message_dict.get("pwd", []):
  157. customer_dir = os.path.join(self.__root_dir, *recv_message_dict.get("pwd", []))
  158. response_message_dict["body"] = self.__file_handler.list_dir(customer_dir)
  159. response_message_dict["status"] = 1 # 处理结果符合预期,状态设置成1
  160. self.__send_bytes_message(response_message_dict, client_tuple)
  161. return
  162.  
  163. if cmd_str.startswith("cd"): # 切换到下一个目录
  164. if cmd_str.strip() == "cd":
  165. response_message_dict["pwd"] = []
  166. response_message_dict["body"] = self.__message_type_ok
  167. response_message_dict["status"] = 1 # 处理结果符合预期,状态设置成1
  168. self.__send_bytes_message(response_message_dict, client_tuple)
  169. return
  170.  
  171. target_dir = cmd_str.split(" ")[-1]
  172. if target_dir == "..":
  173. response_message_dict["pwd"] = response_message_dict["pwd"][0:-1]
  174. response_message_dict["body"] = self.__message_type_ok
  175. response_message_dict["status"] = 1 # 处理结果符合预期,状态设置成1
  176. self.__send_bytes_message(response_message_dict, client_tuple)
  177. return
  178.  
  179. if target_dir == "/":
  180. response_message_dict["body"] = self.__message_type_ok
  181. response_message_dict["status"] = 1 # 处理结果符合预期,状态设置成1
  182. self.__send_bytes_message(response_message_dict, client_tuple)
  183. return
  184. if not os.path.exists(os.path.join(self.__root_dir, *recv_message_dict.get("pwd", []), target_dir)):
  185. # 返回值形如: {'type': 'cmd', 'body': 'Target path not exists', 'pwd': ['folder1']}
  186. response_message_dict["body"] = self.__message_type_path_not_exists
  187. response_message_dict["status"] = 0
  188. self.__send_bytes_message(response_message_dict, client_tuple)
  189. return
  190. elif not (os.path.join(self.__root_dir, *recv_message_dict.get("pwd", []), target_dir).startswith(self.__root_dir)):
  191. # 客户进入的目录,必须是以 self.__root_dir 开头,不能进入到其他目录
  192. # 返回值形如: {'type': 'cmd', 'body': 'Target path not exists', 'pwd': ['folder1']}
  193. # response_message_dict["pwd"] = []
  194. response_message_dict["body"] = self.__message_type_path_not_exists
  195. response_message_dict["status"] = 0
  196. self.__send_bytes_message(response_message_dict, client_tuple)
  197. return
  198. elif os.path.isfile(os.path.join(self.__root_dir, *recv_message_dict.get("pwd", []), target_dir)): # 文件
  199. # 返回值形如: {'type': 'cmd', 'body': 'Target path not exists', 'pwd': ['folder1']}
  200. response_message_dict["body"] = self.__message_type_path_not_exists
  201. response_message_dict["status"] = 0
  202. self.__send_bytes_message(response_message_dict, client_tuple)
  203. return
  204. else:
  205. # 返回值形如: {'type': 'cmd', 'body': 'ok', 'pwd': ['folder1', 'folder2']}
  206. response_message_dict["pwd"].append(target_dir)
  207. response_message_dict["body"] = self.__message_type_ok
  208. response_message_dict["status"] = 1 # 处理结果符合预期,状态设置成1
  209. self.__send_bytes_message(response_message_dict, client_tuple)
  210. return
  211.  
  212. if cmd_str == "pwd":
  213. # 返回值形如 {'type': 'cmd', 'body': ['folder1'], 'pwd': ['folder1']}
  214. response_message_dict["body"] = response_message_dict.get("pwd", [])
  215. response_message_dict["status"] = 1 # 处理结果符合预期,状态设置成1
  216. self.__send_bytes_message(response_message_dict, client_tuple)
  217. return
  218.  
  219. self.__send_bytes_message(response_message_dict, client_tuple)
  220. return
  221.  
  222. def __download_handler(self, recv_message_dict: dict, response_message_dict: dict, client_tuple: tuple):
  223. """
  224. 处理消息体的type参数值是"download",这种下载单个文件的命令行的消息
  225. :param recv_message_dict:
  226. :param response_message_dict:
  227. :param client_tuple:
  228. :return:
  229. """
  230. response_message_dict["type"] = "download"
  231. try:
  232. file_name = recv_message_dict["body"]["file_name"] # 要下载的文件的名字
  233. pwd_list = recv_message_dict["pwd"] # 要下载的文件所在的路径
  234. start_index = recv_message_dict["body"]["start_index"] # 要下载的文件的起始位置索引
  235. length = recv_message_dict["body"]["length"] # 单次下载的字节数
  236. if length > 1024 * 10: # 单次传输的数据太大的话,会报错。通过测试,单次发送10KB数据比较合适。
  237. length = 1024 * 10
  238. file_name_absolute = os.path.join(self.__root_dir, *pwd_list, file_name) # 要下载的文件的绝对路径
  239.  
  240. content_bytes, next_index = self.__file_handler.seek_file(file_path=file_name_absolute,
  241. start_index=start_index,
  242. length=length)
  243. response_message_dict["body"] = {
  244. "file_name": file_name,
  245. "pwd": pwd_list,
  246. "content_bytes": content_bytes,
  247. "start_index": start_index,
  248. "next_index": next_index
  249. }
  250. response_message_dict["status"] = 1
  251. self.__send_bytes_message(response_message_dict, client_tuple)
  252. except Exception:
  253. response_message_dict["body"] = self.__message_type_server_inner_error
  254. response_message_dict["status"] = 0
  255. self.__logger.error("Download file exception !!!" + traceback.format_exc())
  256. self.__send_bytes_message(response_message_dict, client_tuple)
  257.  
  258. def run(self):
  259. """
  260. 调用该方法,启动服务端
  261. :return:
  262. """
  263. self.__logger.info("Server is running at [%s@%s], wating for client ..." % (self.__host, self.__port))
  264. while True:
  265. try:
  266. # message_tuple 是一个元组类型: (b'消息体', ('127.0.0.1', 61040)),第一个值是bytes类型的消息,第二个值是客户端信息
  267. message_tuple = self.__socket_obj.recvfrom(1024 * 1024) # 从客户端接收到的消息
  268. self.__message_handler(message_tuple)
  269. except Exception:
  270. self.__logger.error("FTP Server Error: " + traceback.format_exc())
  271. time.sleep(1)
  272.  
  273. if __name__ == '__main__':
  274. ftp_server_obj = FTPServerUDP()
  275. ftp_server_obj.run()

客户端代码

  1. """
  2. 使用socket的udp协议实现的一个ftp客户端。
  3. 服务器和客户端之间传递数据格式:
  4. 1、服务端和客户端统一使用Python的字典格式(也就是本例中自定义的"通信协议"),格式形如:
  5. {
  6. "type": "cmd", # 支持的值有: "cmd"、"download"
  7. "body": "ll", # 在cmd模式下,常用的命令有: ll、ls、cd 指定目录、pwd
  8. "pwd": ["folder1", "folder2"],
  9. "status": 1,
  10. "uuid": "b93e21e659f711ee9285a46bb6f59f55" # uuid.uuid1().hex,用来保证客户端和服务端
  11. }
  12. 2、客户端使用pickle模块的pickle.dumps(dict类型数据)把要发送的数据转成bytes类型
  13. 3、客户端使用socket的udp传输转成的bytes数据给服务端
  14. 4、服务端接收到从客户端发送过来的bytes类型的数据,再使用pickle.loads(bytes类型数据)把数据转成原始的dict类型。
  15.  
  16. 使用socket的udp,既能接收数据,又能发送数据,因此服务端和客户端都是相对的。
  17. """
  18.  
  19. import socket
  20. import sys
  21. import time
  22. import pickle
  23. import os
  24. import logging
  25. import traceback
  26. import uuid
  27.  
  28. LOG_FORMAT = "%(asctime)s - %(levelname)s [%(filename)s-%(funcName)s] Line: %(lineno)s] - %(message)s"
  29. logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)
  30. _logger = logging.getLogger()
  31.  
  32. class FTPClientUDP:
  33. def __init__(self, host="127.0.0.1", port=8090, download_path="D:\my_download", logger=None):
  34. self.__host = host # 服务器的IP地址
  35. self.__port = port # 服务器的端口号
  36. self.__download_root_path = download_path # 要下载的文件路径
  37. self.__logger = logger if logger else _logger
  38. if not os.path.exists(download_path):
  39. os.makedirs(download_path, True)
  40.  
  41. self.__socket_obj = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP连接对象
  42. self.__is_downloading = False # 用来标识是否在下载文件,当在下载文件的时候,就不会再接收输入参数
  43. self.__pwd_list = [] # 用来存储用户所在的当前目录
  44. self.__server_tuple = (self.__host, self.__port) # 服务端的地址和端口号组成的元组
  45.  
  46. def __cmd_handler(self, cmd_dict: dict):
  47. """
  48.  
  49. :param cmd_dict: 形如:{'type': 'cmd', 'body': 'll', 'pwd': [], 'status': 1,
  50. 'uuid': 'f5c2f4d263f111ee877aa46bb6f59f55'}
  51. :return:
  52. """
  53. # print("cmd_dict: ", cmd_dict)
  54. cmd_bytes = pickle.dumps(cmd_dict)
  55. if cmd_dict["body"] in ["ll", "ls"]:
  56. self.__cmd_handler_ls(cmd_dict, cmd_bytes)
  57. elif cmd_dict["body"].startswith("cd "):
  58. self.__cmd_handler_cd(cmd_dict, cmd_bytes)
  59. elif cmd_dict["body"] == "pwd":
  60. self.__cmd_handler_pwd(cmd_dict, cmd_bytes)
  61. elif cmd_dict["body"].startswith("get "):
  62. self.__cmd_handler_get(cmd_dict, cmd_bytes)
  63. else:
  64. print("Command [%s] not supported !!!" % cmd_dict["body"])
  65. print()
  66.  
  67. def __cmd_handler_ls(self, cmd_dict: dict, cmd_bytes: bytes):
  68. """
  69. 发送类似于"ls -la" 命令给服务端,并把返回值打印到终端
  70. :param cmd_dict:
  71. :param cmd_bytes:
  72. :return:
  73. """
  74. recv_message_dict = self.__recv_normal_cmd_handler(cmd_dict, cmd_bytes)
  75. self.__print_normal_info(recv_message_dict)
  76.  
  77. def __cmd_handler_cd(self, cmd_dict: dict, cmd_bytes: bytes):
  78. """
  79. 发送类似于"cd 指定目录"命令给服务端,并把返回值打印到终端
  80. :param cmd_dict:
  81. :param cmd_bytes:
  82. :return:
  83. """
  84. recv_message_dict = self.__recv_normal_cmd_handler(cmd_dict, cmd_bytes)
  85. self.__print_normal_info(recv_message_dict)
  86. if recv_message_dict.get("status", 0):
  87. self.__pwd_list = recv_message_dict["pwd"] # 保存客户端进入的目录
  88.  
  89. def __cmd_handler_pwd(self, cmd_dict: dict, cmd_bytes: bytes):
  90. """
  91. 发送 "pwd" 命令给服务端,并把返回值打印到终端
  92. :param cmd_dict:
  93. :param cmd_bytes:
  94. :return:
  95. """
  96. recv_message_dict = self.__recv_normal_cmd_handler(cmd_dict, cmd_bytes)
  97. self.__print_pwd_info(recv_message_dict)
  98. if recv_message_dict.get("status", 0):
  99. self.__pwd_list = recv_message_dict["pwd"] # 保存客户端进入的目录
  100.  
  101. def __cmd_handler_get(self, cmd_dict: dict, cmd_bytes: bytes):
  102. # 获取当前目录下面的所有的文件,包括文件夹
  103. if cmd_dict["body"].lower().strip() == "get all":
  104. self.__download_handler_all_file()
  105. return
  106.  
  107. # 获取当前目录下面"get "命令后面的一个文件
  108. command = cmd_dict["body"]
  109. file_name = cmd_dict["body"].split(" ")[-1] # 字符串类型的要下载的文件名
  110. if not file_name:
  111. return
  112. # 首先获取当前目录"ll"的返回值
  113. cmd_dict_ll = {'type': 'cmd',
  114. 'body': 'll',
  115. 'pwd': cmd_dict["pwd"],
  116. 'status': 1,
  117. 'uuid': uuid.uuid1().hex
  118. }
  119. ret_dict = self.__recv_normal_cmd_handler(cmd_dict_ll)
  120. can_download = False
  121. file_size = 0 # 文件大小
  122. pwd = [] # 文件所处路径
  123. file_type = "f" # f:文件 d:目录
  124. for item_tuple in ret_dict["body"]: # item_tuple形如 ('aa.txt', 34, 'f')
  125. if item_tuple[0] == file_name:
  126. can_download = True
  127. file_size = item_tuple[1]
  128. pwd = ret_dict["pwd"]
  129. file_type = item_tuple[-1]
  130. if can_download and file_type == "f":
  131. self.__download_handler_one_file(file_name, file_size, pwd)
  132. print()
  133. if can_download and file_type == "d":
  134. raw_pwd = self.__pwd_list # 存储用户最原始的pwd目录。
  135. self.__download_handler_one_folder(file_name, pwd)
  136. self.__pwd_list = raw_pwd # 把用户最原始的pwd目录再重新赋给self.__pwd_list变量中。
  137. else:
  138. print()
  139.  
  140. def __download_handler_one_file(self, file_name: str, file_size=0, pwd=[]):
  141. # 发送下载单个文件的命令格式,下载的时候,要来回更新 download_cmd_dict['body']['start_index']的值
  142. download_cmd_dict = {
  143. 'type': 'download',
  144. 'body': {
  145. "file_name": file_name,
  146. "pwd": pwd,
  147. "start_index": 0, #
  148. "length": 1024 * 1024 # 使用UDP传输的数据,单次数据太大的话会报错
  149. },
  150. 'pwd': pwd,
  151. 'status': 1,
  152. 'uuid': uuid.uuid1().hex
  153. }
  154. try:
  155. file_absolute = os.path.join(self.__download_root_path, *pwd, file_name)
  156. if not os.path.exists(os.path.dirname(file_absolute)):
  157. os.mkdir(os.path.dirname(file_absolute))
  158. with open(file_absolute, "wb") as fh:
  159. while True:
  160. download_cmd_bytes = pickle.dumps(download_cmd_dict)
  161. ret_dict = self.__recv_normal_cmd_handler(download_cmd_dict, download_cmd_bytes)
  162. if not ret_dict["status"]: # 下载途中失败了
  163. self.__logger.error("Download exception: " + str(ret_dict["body"]))
  164. break
  165. fh.write(ret_dict["body"]["content_bytes"])
  166. next_index = ret_dict["body"]["next_index"]
  167. if ret_dict["body"]["next_index"] == -1: # 说明已经把文件下载完了
  168. break
  169.  
  170. download_cmd_dict["body"]["start_index"] = ret_dict["body"]["next_index"]
  171. download_cmd_dict["uuid"] = uuid.uuid1().hex
  172. self.__print_download_info(file_absolute, file_size, next_index)
  173. self.__print_download_successfully_info(file_absolute)
  174. except Exception:
  175. self.__logger.error("Download file exception !!!" + traceback.format_exc())
  176.  
  177. def __download_handler_one_folder(self, file_name: str, pwd=[]):
  178. """
  179. 下载一个文件夹。
  180. :param file_name: 即文件夹名字。进入到该方法,默认文件名是存在的
  181. :param pwd:
  182. :return:
  183. """
  184. if not os.path.exists(os.path.join(self.__download_root_path, *pwd, file_name)):
  185. os.makedirs(os.path.join(self.__download_root_path, *pwd, file_name))
  186.  
  187. cmd_dict_ll = {'type': 'cmd',
  188. 'body': 'll',
  189. 'pwd': pwd, # 该参数会在后面的代码中进行替换更新
  190. 'status': 1,
  191. 'uuid': uuid.uuid1().hex
  192. }
  193. cmd_dict_cd = {'type': 'cmd',
  194. 'body': 'cd ' + file_name, # 进入到的目录
  195. 'pwd': pwd,
  196. 'status': 1,
  197. 'uuid': uuid.uuid1().hex
  198. }
  199.  
  200. # 先进入指定目录
  201. recv_message_dict_cd = self.__recv_normal_cmd_handler(cmd_dict_cd, None)
  202. pwd = recv_message_dict_cd["pwd"] # 保存进入的目录
  203. # 获取指定目录的文件列表
  204. cmd_dict_ll["pwd"] = pwd
  205. recv_message_dict_ll = self.__recv_normal_cmd_handler(cmd_dict_ll, None)
  206. # print("&&&&&&&v recv_message_dict_ll: ", recv_message_dict_ll)
  207. if recv_message_dict_ll["status"] and recv_message_dict_ll["body"]:
  208. for item_tuple in recv_message_dict_ll["body"]:
  209. # print("@@@@ item_tuple: ", item_tuple)
  210. if item_tuple[-1] == "f":
  211. self.__download_handler_one_file(file_name=item_tuple[0],
  212. file_size=item_tuple[1],
  213. pwd=pwd
  214. )
  215. elif item_tuple[-1] == "d":
  216. self.__download_handler_one_folder(file_name=item_tuple[0],
  217. pwd=pwd)
  218.  
  219. def __download_handler_all_file(self):
  220. """
  221. 下载当前目录下面的所有文件及文件夹
  222. :return:
  223. """
  224. if not os.path.exists(os.path.join(self.__download_root_path, *self.__pwd_list)):
  225. os.makedirs(os.path.join(self.__download_root_path, *self.__pwd_list))
  226.  
  227. cmd_dict_ll = {'type': 'cmd',
  228. 'body': 'll',
  229. 'pwd': self.__pwd_list, # 该参数会在后面的代码中进行替换更新
  230. 'status': 1,
  231. 'uuid': uuid.uuid1().hex
  232. }
  233. recv_message_dict_ll = self.__recv_normal_cmd_handler(cmd_dict_ll, None)
  234. if recv_message_dict_ll["status"] and recv_message_dict_ll["body"]:
  235. for item_tuple in recv_message_dict_ll["body"]:
  236. if item_tuple[-1] == "f":
  237. self.__download_handler_one_file(file_name=item_tuple[0],
  238. file_size=item_tuple[1],
  239. pwd=recv_message_dict_ll["pwd"]
  240. )
  241. elif item_tuple[-1] == "d":
  242. self.__download_handler_one_folder(file_name=item_tuple[0],
  243. pwd=recv_message_dict_ll["pwd"]
  244. )
  245.  
  246. def __recv_normal_cmd_handler(self, cmd_dict: dict, cmd_bytes: bytes=None, try_times=3) -> dict:
  247. """
  248. 持续发送一条命令到客户端,直到正常接收到数据后结束
  249. :param cmd_dict: 形如 {'type': 'cmd', 'body': 'get aa.txt', 'pwd': [], 'status': 1,
  250. 'uuid': '464899225dcb11ee9a91a46bb6f59f55'}
  251. :param cmd_bytes:
  252. :param try_times: 重试的次数,即
  253.  
  254. :return: 形如 {'type': 'cmd', 'body': [('log', 0, 'd'), ('mylog.txt', 8, 'f')],
  255. 'pwd': [], 'status': 1, 'uuid': '464899235dcb11eebf99a46bb6f59f55'}
  256. 或者 {'type': 'cmd', 'body': {'file_name': 'logger.log', 'pwd': ['log'], 'content_bytes': b'2023-',
  257. 'start_index': 0, 'next_index': 5},
  258. 'pwd': [], 'status': 1, 'uuid': '464899235dcb11eebf99a46bb6f59f55'}
  259. """
  260. ret_dict = {}
  261. cmd_bytes = cmd_bytes if cmd_bytes else pickle.dumps(cmd_dict)
  262. try:
  263. for x in range(try_times):
  264. # print("cmd_dict_ll: ", cmd_dict)
  265. self.__socket_obj.sendto(cmd_bytes, self.__server_tuple) # 服务器端的地址
  266. recv_message_tuple = self.__socket_obj.recvfrom(1024 * 1024)
  267. recv_message_bytes = recv_message_tuple[0]
  268. recv_message_dict = pickle.loads(recv_message_bytes)
  269. if cmd_dict.get("uuid", "False") == recv_message_dict.get("uuid", "True"):
  270. ret_dict = recv_message_dict
  271. break
  272. time.sleep(0.1)
  273. except Exception:
  274. self.__logger.error("Recv normal cmd info exception !!!" + traceback.format_exc())
  275. finally:
  276. return ret_dict
  277.  
  278. def __print_normal_info(self, recv_message_dict: dict):
  279. message_body = recv_message_dict.get("body", None)
  280. if isinstance(message_body, list):
  281. for item in message_body:
  282. print("%-20s %-20s %-20s" % (item[0], item[1], item[2]))
  283. print()
  284. return True
  285. if isinstance(message_body, str):
  286. print("%-20s" % message_body)
  287. print()
  288. return True
  289. return True
  290.  
  291. def __print_pwd_info(self, recv_message_dict: dict):
  292. pwd_list = recv_message_dict.get("pwd", [])
  293. if not pwd_list:
  294. print("%-20s" % "/")
  295. else:
  296. pwd_str = '/' + '/'.join(pwd_list)
  297. print("%-20s" % pwd_str)
  298. print()
  299. return True
  300.  
  301. def __print_download_info(self, file_name, file_size, next_index):
  302. sys.stdout.write("\r[%s] -->: %s" % (file_name, ("%.3f" % (next_index / file_size* 100) + "%")))
  303.  
  304. def __print_download_successfully_info(self, file_name):
  305. """
  306. 最终打印100%
  307. :param file_name:
  308. :return:
  309. """
  310. sys.stdout.write("\r[%s] -->: %s" % (file_name, "100%"))
  311. print()
  312.  
  313. def run(self):
  314. while True:
  315. try:
  316. # 没有下载任务的话,则接收输入新的命令
  317. if not self.__is_downloading:
  318. cmd = input("Input: ")
  319. cmd_dict = {
  320. "type": "cmd",
  321. "body": cmd,
  322. "pwd": self.__pwd_list,
  323. "status": 1,
  324. "uuid": uuid.uuid1().hex
  325. }
  326. self.__cmd_handler(cmd_dict)
  327. print("================================================")
  328. else:
  329. time.sleep(1)
  330. except Exception:
  331. self.__logger.error("Client exception: " + traceback.format_exc())
  332. time.sleep(1)
  333.  
  334. if __name__ == '__main__':
  335. ftp_client_obj = FTPClientUDP()
  336. ftp_client_obj.run()

使用

先运行服务端代码,再运行客户端代码。然后再在客户端输入响应的命令即可

Python使用socket的UDP协议实现FTP文件服务的更多相关文章

  1. python中socket使用UDP协议简单实现服务端与客户端通信

    UDP为不可靠传输,也就是发送方不关心对方是否收到消息,一般用于聊天软件.但现在的聊天软件虽然使用的是UDP协议,但已从代码层面上解决了丢失信息的问题. 下面使用python代码简单实现了服务端与客户 ...

  2. socket之UDP协议,并发编程介绍,操作系统发展史

    socket之UDP协议 1.UDP协议 UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议,是OSI(Open System Interconnection 参考 ...

  3. Python进阶----SOCKET套接字基础, 客户端与服务端通信, 执行远端命令.

    Python进阶----SOCKET套接字基础, 客户端与服务端通信, 执行远端命令. 一丶socket套接字 什么是socket套接字: ​ ​  ​ 专业理解: socket是应用层与TCP/IP ...

  4. 腾讯云-搭建 FTP 文件服务

    搭建 FTP 文件服务 目的:搭建认证登录的FTP具有读写权限 安装并启动 FTP 服务 任务时间:5min ~ 10min 安装 VSFTPD 使用 yum 安装 vsftpd: yum insta ...

  5. 腾讯云,搭建 FTP 文件服务

    腾讯云,搭建 FTP 文件服务 腾讯云,搭建 FTP 文件服务 安装并启动 FTP 服务 任务时间:5min ~ 10min 安装 VSFTPD 使用 yum 安装 vsftpd: yum insta ...

  6. 基于 Ubuntu 搭建 FTP 文件服务

    搭建 FTP 文件服务 安装并启动 FTP 服务 任务时间:5min ~ 10min 安装 VSFTPD 使用 apt-get 安装 vsftpd: sudo apt-get install vsft ...

  7. 腾讯云:ubuntu搭建 FTP 文件服务

    搭建 FTP 文件服务 安装并启动 FTP 服务 任务时间:5min ~ 10min 安装 VSFTPD 使用 apt-get 安装 vsftpd: sudo apt-get install vsft ...

  8. C#本地文件下载以及FTP文件服务下载(以Pdf文件为例)

    一.C#实现本地文件下载 1.文件下载的路径  文件名称 以及文件下载之后要放的位置  这三个变量是必须要的 2.定义以下四个对象: FileWebRequest ftpWebRequest = nu ...

  9. C#的Socket实现UDP协议通信

    今天稍花化了一点时间,利用C#的Socket验证了UDP的通信,为接下来特地利用UDP做个分布式的通信仿真系统打下基础.众所周知,UDP 就是用户数据报协议,在互联网参考模型的第四层——传输层.与TC ...

  10. python网络-Socket之udp编程(24)

    一.udp简介 udp --- 用户数据报协议,是一个无连接的简单的面向数据报的运输层协议. udp不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地. udp在 ...

随机推荐

  1. WWDC2023 Session系列:探索XCode15新特性

    一.版本说明 XCode 15 beta 发布于 2023 年 6月5日, 可支持 macOS 13.3 或以上版本, 你可以按需下载需要的平台. 二.新增特性 1.代码智能提示 (Code comp ...

  2. 企业battle宝典Q1 每一个身处于抑郁与困难的朋友必看!

    里面有看不懂的可以留言,我会解释!

  3. 【有奖调研】HarmonyOS新物种,鸿蒙流量新阵地——元服务邀你来答题!

    "聊技术无话不谈,一起来吹吹元服务!畅聊你对元服务的想法,说不定,你就能撬动元服务的爆发增长!" 元服务(即原子化服务)是华为"轻量化"服务的新物种,可提供全新 ...

  4. Windows 10 开启子系统Ubuntu

    卸载原有的wsl 分发子系统 # 查看已安装的wsl子系统 wsl --list # 依次删除wsl 子系统 wsl --unregister <子系统名称> 结果 安装子系统Ubuntu ...

  5. 衔尾法解决当无法使用空闲中断以及DMA中断时配置DMA接收串口不定长数据

    [Ooonly新人贴]记录工作中遇到的问题,话不多说先上干货 问题:类似K线与蓝牙接收部门模块,要求由原来的接收串口中断改为DMA接收.据说要用到空闲中断与DMA中断,但是经仿真发现DMA每完成传输一 ...

  6. Linux系统运维之Web服务器Nginx安装

    一.介绍 Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,并在一个BSD-like 协议下发行.本文先整理web服务器内容. 二.环境及软件版本 操作 ...

  7. WPF复习知识点记录

    WPF复习知识点记录 由于近几年主要在做Web项目,客户端的项目主要是以维护为主,感觉对于基础知识的掌握没有那么牢靠,趁着这个周末重新复习下WPF的相关知识. 文章内容主要来自大佬刘铁锰老师的经典著作 ...

  8. SSE图像算法优化系列三十一:RGB2HSL/RGB2HSV及HSL2RGB/HSV2RGB的指令集优化-上。

    RGB和HSL/HSV颜色空间的相互转换在我们的图像处理中是有着非常广泛的应用的,无论是是图像调节,还是做一些肤色算法,HSL/HSV颜色空间都非常有用,他提供了RGB颜色空间不具有的一些独特的特性, ...

  9. Blazor 跨平台的、共享一套UI的天气预报 Demo

    1. 前言 很久之前就读过 dotnet9 大佬的一篇文章,MAUI与Blazor共享一套UI,媲美Flutter,实现Windows.macOS.Android.iOS.Web通用UI,没读过的可以 ...

  10. pip install mysqlclient命令安装mysqlclient失败的解决办法

    错误情况: 解决方法: 到这个地址下载自己版本对应的资源 https://www.lfd.uci.edu/~gohlke/pythonlibs/#mysqlclient 如下图: 我这里首先下载了  ...