python socket基于TCP的文件传输程序

TCP,Transmission Control Protoco

TCP,英文Transmission Control Protocol,简称传输控制协议。是HTTP协议中的一种,TCP/IP协议族是网络运作的基础。

python socket 字节序 python用socket传输字节_python网络编程


图源自《图解HTTP》 [(日)上野宣著]

TCP的特点

  1. 面向连接
  • 通信的双方必须先建立好连接才能进行数据的传输,数据传输完成后,双方必须断开此连接,以释放系统资源

  1. 可靠传输
  • TCP采用发送应答机制
  • 超时重传
  • 错误校验
  • 流量控制和阻塞管理

TCP程序的开发

TCP是面向连接的,那么就必须要有连接的双方,即客户端与服务端。

python socket 字节序 python用socket传输字节_socket_02


TCP网络应用程序开发分为

  • 客户端应用程序开发
  • 服务端应用程序开发
  • 客户端指运行在用户设备上的程序;服务端是运行在服务器设备的程序,专门为客户端提供数据服务。

服务端是处于被动状态的,从服务端的运行开始,就一直等待客户端的连接,服务端会一直阻塞在等待客户端连接的状态,直到有客户端进行连接。

客户端发起主动连接请求,直到服务端回应请求,便会建立连接,否则也会处于阻塞状态。
一个客户端只能在一个时间内与一个服务端建立连接。要与其他服务端建立连接,必须要先断开现有的连接。

python socket

socket是python自带的库,可以实现进程之间的网络通信。socket简称套接字,我还是比较喜欢读成socket。

python socket 字节序 python用socket传输字节_socket文件传输_03


那么怎么使用socket实现进程间的网络通信呢?

socket客户端的开发

根据前面的开发流程,客户端的开发流程比较简陋。

  1. 创建客户端的socket
  2. 和服务端socket建立连接
  3. 传输数据(发送和接收)
  4. 关闭客户端的socket
import socket
# 创建tcp客户端套接字
# 1. AF_INET:表示ipv4
# 2. SOCK_STREAM: tcp传输协议
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM
# 和服务端应用程序建立连接
# connect方法需要传入一个元组对象,元组的元素为服务端的IP地址,服务端的端口号
tcp_client_socket.connect(("192.168.131.62", 8080))
# 客户端与服务端建立连接的过程是一直处于阻塞的,直到建立连接

# 代码执行到此,说明连接建立成功
# 准备发送的数据
send_data = "你好服务端,我是客户端小黑!".encode("gbk")
# 发送数据
tcp_client_socket.send(send_data)
# 接收数据, 这次接收的数据最大字节数是1024
recv_data = tcp_client_socket.recv(1024)
# 返回的直接是服务端程序发送的二进制数据
print(recv_data)
# 对数据进行解码
recv_content = recv_data.decode("gbk")
print("接收服务端的数据为:", recv_content)
# 关闭套接字
tcp_client_socket.close()

TCP只支持传输字节流数据
那么这里就可以进行文件的传输了。

socket服务端程序的开发

服务端程序开发流程:

  1. 创建服务端socket对象
  2. 绑定服务器设备的端口号(设置端口重用)
  3. 设置监听
  4. 等待客户端连接请求
  5. 传输数据
  6. 关闭socket
import socket


# 创建tcp服务端套接字
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用,让程序退出端口号立即释放
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 给程序绑定端口号
tcp_server_socket.bind(("", 8989))
# 设置监听
# 128:最大等待建立连接的个数, 提示: 目前是单任务的服务端,同一时刻只能服务与一个客户端,后续使用多任务能够让服务端同时服务于多个客户端
# 不需要让客户端进行等待建立连接
# listen后的这个套接字只负责接收客户端连接请求,不能收发消息,收发消息使用返回的这个新套接字来完成
tcp_server_socket.listen(128)
# 等待客户端建立连接的请求, 只有客户端和服务端建立连接成功代码才会解阻塞,代码才能继续往下执行
# 1. 专门和客户端通信的套接字: service_client_socket
# 2. 客户端的ip地址和端口号: ip_port
service_client_socket, ip_port = tcp_server_socket.accept()
# 代码执行到此说明连接建立成功
print("客户端的ip地址和端口号:", ip_port)
# 接收客户端发送的数据, 这次接收数据的最大字节数是1024
recv_data = service_client_socket.recv(1024)
# 获取数据的长度
recv_data_length = len(recv_data)
print("接收数据的长度为:", recv_data_length)
# 对二进制数据进行解码
recv_content = recv_data.decode("gbk")
print("接收客户端的数据为:", recv_content)
# 准备发送的数据
send_data = "ok, 问题正在处理中...".encode("gbk")
# 发送数据给客户端
service_client_socket.send(send_data)
# 关闭服务与客户端的套接字, 终止和客户端通信的服务
service_client_socket.close()
# 关闭服务端的套接字, 终止和客户端提供建立连接请求的服务
tcp_server_socket.close()

这里需要注意的一点就是,当客户端和服务端建立连接后,服务端程序退出后端口不会立即释放,需要等待1-2分钟。
我们可以每次关闭后短时间内再次开启服务端程序,可以选择更换端口号,防止端口被占用导致程序异常停止;
除了这种方法之外,我们可以设置端口号复用,这样在退出服务端程序后端口会立即被释放,就不需要重复设置端口号了。

了解了简单的客户端与服务端的简单数据传输之后,来回归标题,怎么实现文件传输。

TCP的文件传输程序

文件,包括图片、视频、压缩文件等在计算机中都是以二进制的形式存储的,而TCP传输的数据也是字节流,那么就可以将读取到的文件内容(数据)直接通过socket传输到另一端(客户端或服务端)。
但是由于socket的限制,在同一时间只能进行传输的一种,即一端只能进行文件的发送而另一端进行文件的接收,如果想要反过来,则需要在一次传输任务结束后再次进行传输。

这里的程序与上面的开发流程一致,只是在发送之前需要将文件读取出来,接收后把接收到的文件存放到磁盘中。

传输处理程序

传输处理程序分为两种,一种为文件接收的处理程序,一种为文件发送的处理程序。
文件接收的处理程序受制于文件发送的处理程序(文件怎么发送决定了要怎么进行接收)。

将传输处理程序封装成函数,那么客户端跟服务端都可以复用。
首先是文件的发送处理程序:

import socket
import os
import time


# Resource 文件夹保存接收的文件
def creat_folder(path):
    if os.path.exists(path):
        return
    else:
        os.mkdir(path)


def send_handle(file_name, file_size, client_server):
    """
    处理传输文件数据,将文件读取并发送到接收端,只允许单次发送
    单次发送失败后需要进行重连再重新发送
    :param file_name: 要发送的文件名
    :param file_size: 要发送的文件的大小
    :param client_server: 用于传输数据的socket,发送端的socket
    :return: 发送文件的结果,1为发送成功,0为发送失败
    """
    al_read_size = 0  # 保存已读取的文件大小,显示读取的进度
    if file_name and file_size:
        # 判断传入的文件信息是否空
        client_server.send(b"starting send file")
        with open(file_name, "rb") as f:
            while True:
                # 循环读取文件
                file_content = f.read(1048576)  # 每次从文件种读取1M数据
                al_read_size += len(file_content)  # 计算总共读取的数据的大小
                if file_content:  # 判断文件是否读取完了
                    print("{}%".format(al_read_size/file_size))  # 输出读取文件的进度
                    client_server.send(file_content)  # 将读取的文件发送到服务端
                else:
                    print("100%")  # 判断文件读取完了,输出读取的进度
                    return 1  # 文件读取发送完了,返回处理情况
    else:
        print("Can't find the file or the file is empty.")  # 打开文件失败,文件或文件名为空,则退出发送服务
        client_server.send(b'cancel send file.')  # 通知服务端取消文件的发送
        return 0  # 文件未发送成功,返回0


def send_server(client_server):
    # 输入需要发送的文件名,包括文件后缀。仅限二进制文件,包括图片、视频、压缩文件等
    file_name = input("Please enter the file path or the file name:")
    if os.path.exists(file_name) and (not os.path.isdir(file_name)):  # 判断文件是否存在,是否文件夹
        # 获取文件的大小
        file_size = os.path.getsize(file_name)
        file_message = file_name + "|" + str(file_size)
        # 与服务端建立连接后,先将文件名字与文件的大小发送给服务端
        client_server.send(file_message.encode())
        # 对方接收到了file_message的信息后返回一个“copy”,接收不成功会返回别的信息
        recv_data = client_server.recv(1024)
        # 判断对方是否接收信息成功
        if recv_data.decode() == "copy":
            print("start to send data...")
            start_time = time.time()  # 计算发送文件的开始时间
            send_flag = send_handle(file_name, file_size, client_server)  # 发送文件的请求处理,返回处理结果
            end_time = time.time()  # 计算发送文件的结束时间
            spend_time = end_time - start_time  # 计算发送文件的耗时
            print("sending file spend {} s".format(spend_time))  # 在控制台输出发送文件的耗时

            if send_flag:  # 判断文件是否发送成功
                recv_message = client_server.recv(1024)
                if recv_message == "ok":
                    # 文件发送成功
                    print("send file successful, close the client server.")
                    client_server.close()
                    return 1
                else:
                    # 对方文件接收不成功
                    print("server recv file failed.")
                    client_server.close()
                    return 0
            else:
                # 文件发送不成功
                print("Error,failed to send the file.")
                client_server.close()
                return 0

        else:
            # 对方没有接收到文件名及文件大小,或者对方断开了连接,取消发送文件,并关闭socket,退出发送服务
            print("Can't recv the server answer.")
            print("The client don't send the file data and close the server.")
            client_server.close()
            return 0
    try:
        client_server.close()  # 尝试关闭本方的socket,防止前面没有进行关闭,如果前面已经关闭了,直接退出函数
    except Exception:
        pass

接收处理程序:

def recv_handle(file_path, file_size, client_server):
    """
    接收文件的处理函数,只允许单次接收,一次接收失败后需要重新建立连接后重新发送
    :param file_path: 保存文件的路径
    :param file_size: 要接收的文件的大小
    :param client_server: 传输服务的socket
    :return: 接收文件的结果,1表示接收成功,0表示接收失败
    """
    print("Start to recv th file...")
    recv_size = 0  # 保存接收的文件的大小
    start_time = time.time()  # 保存开始接收文件的时间
    with open(file_path, "ab") as f:
        while True:
            # 循环接收文件数据
            file_content = client_server.recv(1048576)
            if file_content:  # 判断文件是否接收完了
                recv_size += len(file_content)  # 累计接收的文件大小
                f.write(file_content)  # 将接收的数据保存到文件中
            else:
                # 如果文件接收完了,则退出循环
                end_time = time.time()  # 保存文件接收结束的时间
                print("spend time:{}".format(end_time - start_time))
                break

    if recv_size == file_size:  # 判断接收的文件大小与对方发送的文件大小是否一致
        print("文件全部接收完毕,耗时:{}".format(end_time - start_time))
        client_server.send(b'ok')
        return 1
    else:
        print("文件未接收完成,只接收了{}%".format(recv_size/file_size))
        print("Failed to recv the file.")
        client_server.send(b'fail')
        return 0


def recv_server(client_server):
    print("Ready to recv the file...")
    # 接收发送端发送的文件名及文件大小
    file_name, file_size = client_server.recv(1024).decode().split("|")
    creat_folder("Resource")
    file_path = os.path.join("Resource", file_name)
    # 判断文件名及文件大小是否为空
    if file_name and file_size:
        client_server.send(b'copy')  # 反馈文件发送端,已收到文件名及文件大小
        start_flag = client_server.recv(1024).decode()
        if start_flag == "starting send file":
            recv_flag = recv_handle(file_path, file_size, client_server)  # 启用文件接收服务
            # 判断文件的接收结果
            if recv_flag:
                client_server.close()
                print("文件接收成功,断开连接")
                return
            else:
                print("文件接收失败,断开连接")
                client_server.close()
        else:
            print("对方拒绝发送文件,取消连接")
            client_server.close()
            return
    else:
        # 文件名或文件大小为空,拒绝接收文件,断开连接
        client_server.send(b'refuse')
        client_server.colse()
        return

主要的发送跟接收的代码不需要这么多,我在其中增加了很多发送跟接收的结果判断。

这里使用了循环去读取、发送、接收文件,这是因为socket的发送跟接收机制决定的,这样的好处就是可以发送大文件而不会导致内存溢出。

socket的发送接收机制

socket的send都会先将数据传给缓冲区,缓冲区再传给网卡,通过网卡将数据发送出去;recv方法接收通过网卡将数据接收到接收缓冲区,再从缓冲区中将数据接收。

如下图所示:

python socket 字节序 python用socket传输字节_python socket 字节序_04

缓冲区的大小是由内存分配的,所以如果一次发送过多、或接收过多的数据会造成内存溢出。这一点需要注意。

还可以使用面对对象来实现。
这里就不再赘述了。

总结

这是我写的第一篇博客,谢谢你能看到最后。
目前正在学习中。后续也会将学习的内容写成博客的。emmm有空的话。

我写这个的时候,也参考了CSDN另一位博主的代码,先将文件名跟文件大小用“|”拼接并发送过去就是参考了他的做法,当初忘记保存他的链接地址了。非常不好意思。

如果在上面发现了问题欢迎与我联系。