跳转至

文件下载


使用argparse库实现命令行

使用pathlib库完成目录、文件部分

使用socket库完成tcp下载

使用concurrent.futures库实现线程池

服务端

import argparse
import os
import pathlib
import socket
from concurrent.futures import ThreadPoolExecutor


class CLI(object):
    def __init__(self):
        parser = argparse.ArgumentParser(
            prog='FileTransfer',
            description='Download the file from server to client by socket',
            epilog='www.911205.xyz',
        )

        # 可选参数
        parser.add_argument('-i', dest='ip', help='服务器地址', default='0.0.0.0', type=str)
        parser.add_argument('-p', dest='port', help='服务器端口', default=9876, type=int)
        parser.add_argument('-f', dest='file', help='服务器文件, 支持相对路径和绝对路径', type=str)
        parser.add_argument('-s', dest='size', help='单次传输文件大小, 默认1024字节(Byte), 即1KB', default=1024, type=int)

        args = parser.parse_args()
        print(f'服务器监听地址: {args.ip}')
        print(f'服务器监听端口: {args.port}')
        print(f'服务器监听文件: {pathlib.Path(args.file).resolve()}')
        print(f'每次传送文件大小: {args.size}字节')

        SendFile(*args.__dict__.values())


class SendFile:
    def __init__(self, ip: str, port: int, file: str, size: int):
        self.file, self.file_name = self.get_file(file)

        # tcp服务端套接字
        self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 关闭后立即释放端口(不经历TIME_WAIT阶段)
        self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
        # 绑定端口
        self.tcp_server_socket.bind((ip, port))
        # 主动套接字变被动
        self.tcp_server_socket.listen(os.cpu_count()*2)


        # 提供一次性下载服务
        # # 获取客户端的套接字
        # self.client_socket, clinet_info = self.tcp_server_socket.accept()
        # self.client_socket.send(file_name.encode('utf-8'))
        # # 发送文件
        # self.send_file(file, clinet_info)

        # 启用线程池
        thread_pool = ThreadPoolExecutor(max_workers=os.cpu_count()) 

        # 提供多次下载服务(服务器不关闭, 多个客户端均可下载)
        while True:
            self.client_socket, clinet_info = self.tcp_server_socket.accept()
            # self.client_socket.send(file_name.encode('utf-8'))
            # self.send_file(file, size, clinet_info)

            # 使用线程池同时处理多个请求
            thread_pool.submit(self.send_file_in_thread_pool, *[size, clinet_info])

    def get_file(self, file):
        """获取文件的绝对路径和名称"""

        file, file_name = self.is_file(file)
        print(f'准备发送文件: {file_name}')
        return file, file_name

    @staticmethod
    def is_file(file):
        """判断文件是否存在并返回绝对路径和文件名称"""

        file = pathlib.Path(file)
        if not file.is_file():
            raise FileNotFoundError(f'文件{file}不存在') 
        return file.resolve(), file.name

    def send_file_in_thread_pool(self, size, clinet_info):
        """使用进程池, 可同时处理多个客户端请求"""

        self.client_socket.send(self.file_name.encode('utf-8'))
        self.send_file(size, clinet_info)

    def send_file(self, size, clinet_info):
        """发送文件到客户端"""

        with open(self.file, "rb") as f:
            while True:
                # 读取数据
                result = f.read(size)
                if result:
                    # 发送数据
                    self.client_socket.send(result)
                else:
                    print(f"文件传输完成...{clinet_info}")
                    break
        self.close_socket()

    def close_socket(self):
        """关闭套接字"""

        # 关闭套接字 -> 不再进行收发数据
        self.client_socket.close()
        # 关闭套接字 -> 不再接收其他客户端与其建立连接
        # self.tcp_server_socket.close()


def main():
    try:
        CLI()
    except FileNotFoundError as e:
        print(f'错误: {e}')
    except Exception as e:
        print(f'错误: {e}')


if __name__ == '__main__':
    main()

客户端

import argparse
import pathlib
import socket
from typing import Union


class CLI(object):
    def __init__(self):
        parser = argparse.ArgumentParser(
            prog='FileTransfer',
            description='Download the file from server to client by socket',
            epilog='www.911205.xyz',
        )

        # 位置参数
        parser.add_argument('ip', help='服务器地址', type=str)
        # 可选参数
        parser.add_argument('-p', dest='port', help='服务器端口', default=9876, type=int)
        parser.add_argument('-o', dest='output', help='文件输出位置, 后面根文件名则表示重命名', type=str)

        args = parser.parse_args()
        print(f'服务器监听地址: {args.ip}')
        print(f'服务器监听端口: {args.port}')

        ReceiveFile(*args.__dict__.values())


class ReceiveFile:
    def __init__(self, ip: str, port: int, output: Union[str, None]):
         # 创建tcp客户端套接字
        self.tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 建立连接
        self.tcp_client_socket.connect((ip, port))

        # 接收文件名称
        self.file_name = self.tcp_client_socket.recv(1024).decode('utf-8')
        print(f'准备下载文件: {self.file_name}')

        # 下载文件
        self.receive_file(output)

    def receive_file(self, output):
        if output:
            file = pathlib.Path(output)
            if file.is_dir():
                file = file.joinpath(self.file_name)
        else:
            file = pathlib.Path(self.file_name)
        file = file.resolve()
        print(f'文件另存为: {file}')

        with open(f'{file}', "wb") as f:
            while True:
                # 接收数据
                recv_data = self.tcp_client_socket.recv(1024)
                if recv_data:
                    # 写入数据
                    f.write(recv_data)
                else:
                    print("文件下载成功...")
                    break
        self.close_socket()

    def close_socket(self):
        """关闭套接字"""

        self.tcp_client_socket.close()


def main():
    try:
        CLI()
    except ConnectionRefusedError as e:
        print('错误: 无法连接服务器')
    except Exception as e:
        print(f'错误: {e}')


if __name__ == '__main__':
    main()