当前位置:   article > 正文

基于python的socket网络通信【1】_c python socket 通信

c python socket 通信

一、Socket原理

学习了大佬的知识,简单记一些笔记
https://www.jianshu.com/p/066d99da7cbd
http://c.biancheng.net/view/2351.html

1.1什么是Socket

在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据
  socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。
  我的理解就是Socket就是该模式的一个实现:即socket是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。
  Socket()函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。

百度百科:套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合

1.2Unix/Lunix中的socket

在 UNIX/Linux 系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。

你也许听很多高手说过,UNIX/Linux 中的一切都是文件!那个家伙说的没错。

为了表示和区分已经打开的文件,UNIX/Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)。例如:
通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;
通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。

UNIX/Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。

请注意,网络连接也是一个文件,它也有文件描述符!你必须理解这句话。

我们可以通过 socket() 函数来创建一个网络连接,或者说打开一个网络文件,socket() 的返回值就是文件描述符。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:
用 read() 读取从远程计算机传来的数据;
用 write() 向远程计算机写入数据。

你看,只要用 socket() 创建了连接,剩下的就是文件操作了,网络编程原来就是如此简单!

1.3Windows中的socket

Windows 也有类似“文件描述符”的概念,但通常被称为“文件句柄”。因此,本教程如果涉及 Windows 平台将使用“句柄”,如果涉及 Linux 平台则使用“描述符”。

与 UNIX/Linux 不同的是,Windows 会区分 socket 和文件,Windows 就把 socket 当做一个网络连接来对待,因此需要调用专门针对 socket 而设计的数据传输函数,针对普通文件的输入输出函数就无效了

2.网络中进程如何通信

2.1、本地进程间通信

a、消息传递(管道、消息队列、FIFO)
  b、同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
  c、共享内存(匿名的和具名的,eg:channel)
  d、远程过程调用(RPC)

2.2、 网络中进程如何通信

我们要理解网络中进程如何通信,得解决两个问题:
  a、我们要如何标识一台主机,即怎样确定我们将要通信的进程是在那一台主机上运行。
  b、我们要如何标识唯一进程,本地通过pid标识,网络中应该怎样标识?
解决办法:
  a、TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机
  b、传输层的“协议+端口”可以唯一标识主机中的应用程序(进程),因此,我们利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互

3、Socket通信分类

网络中进程间利用三元组【ip地址,协议,端口】可以进行网络间通信,socket就是利用三元组解决网络通信的一个中间件工具,就目前而言,几乎所有的应用程序都是采用Socket,Socket通信的数据传输方式,常用的有两种:
a、SOCK_STREAM:对应TCP协议,表示面向连接的数据传输方式。数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送,但效率相对较慢。常见的 http 协议就使用 SOCK_STREAM 传输数据,因为要确保数据的正确性,否则网页不能正常解析。
  b、SOCK_DGRAM:对应UDP协议,表示无连接的数据传输方式。计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。因为 SOCK_DGRAM 所做的校验工作少,所以效率比 SOCK_STREAM 高。
  例如:QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响
  
Socket 编程是基于 TCP 和 UDP 协议的,它们的层级关系如下图所示:
在这里插入图片描述

图解Socket() 函数

在这里插入图片描述

4、Socket(TCP)建立连接的三次握手

Socket的功能简化为三个:建立连接、发送数据以及接收数据,下链接为建立连接的流程
http://c.biancheng.net/view/2351.html

Socket缓冲区

每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。

write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。

TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发在这里插入图片描述
送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。

read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取

这些I/O缓冲区特性可整理如下:

  1. I/O缓冲区在每个TCP套接字中单独存在;
  2. I/O缓冲区在创建套接字时自动生成;
  3. 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
  4. 关闭套接字将丢失输入缓冲区中的数据。

一般情况下,不用关系默认缓冲区的大小,但也可以用如下代码查看与修改

sock.getsockopt()        # 获取缓冲区大小
sock.setsockopt()         #更改缓冲区大小
  • 1
  • 2

收发函数特性:

recv特征:

  1. 如果建立的另一端链接被断开, 则recv立即返回空字符串

  2. recv是从接受缓冲区取出内容,当缓冲区为空则阻塞

  3. recv如果一次接受不完缓冲区的内容,下次执行会自动接受

send特征:

  1. 如果发送的另一端不存在则会产生Pipe Broken异常

  2. send是从发送缓冲区发送内容,当缓冲区为满则堵塞

这就是TCP套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性,TCP套接字默认情况下是阻塞模式,也是最常用的。

TCP协议的粘包问题

socket缓冲区和数据的传递过程,可以看到数据的接收和发送是无关的,read()/recv() 函数不管数据发送了多少次,都会尽可能多的接收数据。也就是说,read()/recv() 和 write()/send() 的执行次数可能不同。
例如,write()/send() 重复执行三次,每次都发送字符串"abc",那么目标机器上的 read()/recv() 可能分三次接收,每次都接收"abc";也可能分两次接收,第一次接收"abcab",第二次接收"cabc";也可能一次就接收到字符串"abcabcabc"。

假设我们希望客户端每次发送一位学生的学号,让服务器端返回该学生的姓名、住址、成绩等信息,这时候可能就会出现问题,服务器端不能区分学生的学号。例如第一次发送 1,第二次发送 3,服务器可能当成 13 来处理,返回的信息显然是错误的。

这就是数据的“粘包”问题,客户端发送的多个数据包被当做一个数据包接收。也称数据的无边界性,read()/recv() 函数不知道数据包的开始或结束标志(实际上也没有任何开始或结束标志),只把它们当做连续的数据流来处理。

真正的关闭socket

close方法可以释放一个连接的资源,但是不是立即释放,如果想立即释放,那么请在close之前使用shutdown方法
shutdown方法是用来实现通信模式的,模式分三种,SHUT_RD 关闭接收消息通道,SHUT_WR 关闭发送消息通道,SHUT_RDWR 两个通道都关闭
也就是说,想要关闭一个连接,首先把通道全部关闭,然后在release连接,以上三个静态变量分别对应数字常量:0,1,2

self.tcpClient.shutdown(2)      #关闭消息发送通道
self.tcpClient.close()            #关闭套接字连接
  • 1
  • 2

python socket编程实例1——文本传输

客户端:

#port = str(input('please input sever port:'))
host = '192.168.2.107'    #客户端连接到服务器的ip
port = 5270  #端口
sever_address = (host, port)     #元组定义服务器地址,用于作为socket.connect()函数参数 连接到服务器
text_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)     #创建一个socket对象为text_client

text_client.connect(sever_address)       #连接到服务器
succeed_flag='cok'                       #设置消息发送成功标志
text = 'connect succeed'
while True :
    try:
        text_client.send(text.encode())  # 发送文本数据,用 encode() 方法将str变为byte
        text = input('please input the message')
        receive_text = text_client.recv(1024).decode()
        print(receive_text)

    finally:
        print('send over!')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

服务器:

import socket

#可以手动输入本机ip地址,若有多个网口,服务器想从那个网口接收数据,就输入那个网口的ip
#hostname = socket.gethostname()  #可以用 .gethostname()函数来自动得到主机ip,不用手动输入了
#host = socket.gethostbyname(hostname)
host = '192.168.2.107'    #客户端连接到服务器的ip
port = 5270  #端口
sever_address = (host, port)     #创建元组作为 socket.bind()函数的输入,

text_sever = socket.socket(socket.AF_INET, socket.SOCK_STREAM)     #创建一个socket对象为text_sever 为服务器
text_sever.bind(sever_address)    #.bind() 函数绑定端口,该服务器监听此端口
text_sever.listen(4)         #开启监听,同时接入数量最多为4

succeed_flag = 'sok'
while True :
    try:
        print(host)
        print('waiting connect')
        text_client_socket, text_client_address = text_sever.accept()              #accept() 函数,堵塞等待client的连接,连接到后才会执行下一条语句
        print(text_client_address[0] + 'is connected!')

        while True :
            receive_text = text_client_socket.recv(1024)  .decode()            #接收client发送的数据,数据最大为1024 ;此处可以看出接收用户数据测试
            print(receive_text)
            text_client_socket.send(succeed_flag.encode())                     #发送给client ok ,反馈自己确实接收到数据

    finally:
        print('work over!')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

python socket编程实例2——视频传输

客户端:

#-*- coding: UTF-8 -*-
import cv2                    #opencv2库,用于进行各种图像处理
import time
import socket                 #socket库,用于构建tcp/ip  通信

# 服务端ip地址
HOST = '192.168.2.102'      #字符串类型存储 host ip,tcp/ip通信服务器需要固定的ip与port
# 服务端端口号
PORT = 8080
ADDRESS = (HOST, PORT)       #元组方式存储ip与port

# 创建一个套接字,命名为tcpClient
tcpClient = socket.socket(socket.AF_INET, socket.SOCK_STREAM)     #socket.socket() 函数:socket.AF_INET:基于IPv4  ,socket.SOCK_STREAM对应TCP
# 连接远程ip
#tcpClient.bind(('192.168.3.122', 8080))
tcpClient.connect(ADDRESS)    #客户端向服务端发起连接。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误

#cap = cv2.VideoCapture('test1080.mp4')   #要发送的视频,如果是0为摄像头
cap = cv2.VideoCapture(0)
while True:
    # 计时
    start = time.perf_counter()         #计时器,第一次调用的时间存储在start里
    # 读取图像
    ref, cv_image = cap.read()           #返回第一个ref为true或false,表示是否读到了图像 ,第二个参数表示截取到一帧的图片数据,是一个三维数组
    # 压缩图像
    #cv2.imdecode()函数从指定的内存缓存中读取数据,并把数据转换(解码)成图像格式;主要用于从网络传输数据中恢复出图像。
    #cv2.imencode()函数是将图片格式转换(编码)成流数据,赋值到内存缓存中;主要用于图像数据格式的压缩,方便网络传输。
    img_encode = cv2.imencode('.jpg', cv_image, [cv2.IMWRITE_JPEG_QUALITY, 40])[1]        #第一个参数是压缩为什么格式,第二个参数是要压缩的数据源,最后一个参数是解码压缩参数,数字越大图片质量越好
    # 转换为字节流
    bytedata = img_encode.tostring()                     #将图像转换为字节流
    # 标志数据,包括待发送的字节流长度等数据,用‘,’隔开  ,发送的数据类型是 str
    flag_data = (str(len(bytedata))).encode() + ",".encode() + " ".encode()
    tcpClient.send(flag_data)                                  #客户端发送标志数据,服务器接收后知晓将要发送数据
    # 接收服务端的应答
    data = tcpClient.recv(1024)                #接收数据,数据以bytes类型返回,bufsize指定要接收的最大数据量为1024字节
    if ("ok" == data.decode()):               #接收到服务器返回'OK'后,发送全部图片数据,这里用decode进行了解码,因为socket传输字节流,对收到的字节解码才能得到字符串......
 #   if (data.decode()):
        # 服务端已经收到标志数据,开始发送图像字节流数据
        tcpClient.send(bytedata)
    # 接收服务端的应答
    data = tcpClient.recv(1024)
    if ("ok" == data.decode()):
        # 计算发送完成的延时
        print("延时:" + str(int((time.perf_counter() - start) * 1000)) + "ms")    #再次调用该计时函数,返回与上一次调用的时间间隔
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

服务器:

#-*- coding: UTF-8 -*-
import socket
import cv2
import numpy as np

HOST = '192.168.2.102'
PORT = 8080
ADDRESS = (HOST, PORT)
# 创建一个套接字
tcpServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定本地ip
tcpServer.bind(ADDRESS)
# 开始监听
tcpServer.listen(5)

while True:
    print("等待连接……")
    client_socket, client_address = tcpServer.accept()           #accept() 函数,堵塞等待客户端连接;反回(conn,address)二元元组,其中conn是一个通信对象,可以用来接收和发送数据。address是连接客户端的地址。
    print("连接成功!")
    try:                                                          #使用try   expect  便于处理异常
        while True:
            # 接收标志数据
            data = client_socket.recv(1024)
            if data:                                              #接收到的不为空就进入
                # 通知客户端“已收到标志数据,可以发送图像数据”
                client_socket.send(b"ok")                          #关于b"ok"      看https://www.delftstack.com/zh/howto/python/python-b-in-front-of-string/    ,这里是将ok变为byte
                # 处理标志数据
                flag = data.decode().split(",")                  #  strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列  ,这里将结尾的 '' 移除
                # 图像字节流数据的总长度
                total = int(flag[0])                         #flag[0]是第一个列表元素,flag是一个列表,返回第一个元素就是数据总长度的字符串形式,再用int()转为整数
                # 接收到的数据计数
                cnt = 0
                # 存放接收到的数据
                img_bytes = b""

                while cnt < total:
                    # 当接收到的数据少于数据总长度时,则循环接收图像数据,直到接收完毕
                    data = client_socket.recv(256000)                 #一次能接收的最大数据量为256000byte的数据
                    img_bytes += data                                #对总数据量计数
                    cnt += len(data)
                    print("receive:" + str(cnt) + "/" + flag[0])     #打印接收/需接收
                # 通知客户端“已经接收完毕,可以开始下一帧图像的传输”
                client_socket.send(b"ok")

                # 解析接收到的字节流数据,并显示图像
                img = np.asarray(bytearray(img_bytes), dtype="uint8")
                img = cv2.imdecode(img, cv2.IMREAD_COLOR)
                cv2.namedWindow("img", 0)
                cv2.resizeWindow("img", 1280, 720)             #重设置显示界面的大小
                cv2.imshow("img", img)                         #第一个参数是窗口的名字,第二个是图像
                cv2.waitKey(1)

            else:
                print("已断开!")
                break
    finally:
        client_socket.close()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57

python socket 编程实例3——tcp proxy 代理服务器

#https://www.youtube.com/watch?v=iApNzWZG-10

import socket
from threading import Thread
import os
#线程2

class Proxy2Server(Thread):
    #首先设置服务器连接(用_init_方法来构造)
    #参考https://www.cnblogs.com/ant-colonies/p/6718388.html
    def __init__(self, host, port):#如果没有在__init__中初始化对应的实例变量的话,导致后续引用实例变量会出错
        super(Proxy2Server,self).__init__()
        self.game = None #设置为连接用户的套接字,但是该套接字是由Game2Proxy线程创建的
        self.port = port
        self.host = host #连接服务器的ip和端口
        self.server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        self.server.connect((host,port))

    #在这个线程中执行的函数
    def run(self):
        #创建一个循环来执行数据处理和网络连接
        while True:
            data  = self.server.recv(4096)#最多接收4k的数据
            if data:
                #转发所有数据到用户
                print("[{}] <- {}")  #.format(self.port,data[:100].encode('hex'))#用作测试,可以打印出数据的流向
                self.game.sendall(data)




#线程1(监听用户是否与代理服务器连接)
class Game2Proxy(Thread):

    def __init__(self,host,port):
        super(Game2Proxy,self).__init__()
        self.server = None #设置为连接服务器的套接字,但是该套接字是由线程2创建的
        self.port = port
        self.host = host #连接用户的ip和端口
        sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
        sock.bind((host,port))
        sock.listen(1)#这些都是上面官方文档里面调用的例程实现的
        #等待用户的连接 
        self.game ,addr = sock.accept() #sock.accept接收套接字
        #当客户端连接之后我们将获得代理服务器与客户端通信的套接字,并将其分配给self.game,然后在下面的线程中利用永久循环来接收用户端的数据
    
    def run(self):
        while True: #死循环接收用户的数据
            data = self.game.recv(4096)#最大数据量4k
            if data:    #如果真的接收到了用户发送过来的数据,那麽我们会尝试将此数据转发到服务器的套接字,即另外一个线程的套接字
                #转发给服务器
                print("[{}] -> {}")       #.format(self.port,data[:100].encode('hex'))#用作测试,可以打印出数据的流向
                self.server.sendall(data)



#上面的两个线程创建完毕之后,需要为每一个线程提供对另外一个套接字的引用
#为此,我创建了一个更通用的类,命名为Proxy
class Proxy(Thread):
    def __init__(self,from_host,to_host,port):#如果没有在__init__中初始化对应的实例变量的话,导致后续引用实例变量会出错
        super(Proxy, self).__init__()
        self.from_host = from_host 
        self.to_host = to_host
        self.port = port 

    def run(self):
        while True:
            #print ("[proxy({})] setting up")
            print ("代理服务器设置完毕,等待设备接入...")
            #用户会连接到下面这个
            self.g2p = Game2Proxy(self.from_host, self.port) #运行我们创建的这个线程,它等待用户端连接到指定端口
            #如果代理服务器与用户建立连接之后,另外一个线程将建立到服务器的转发连接
            self.p2s = Proxy2Server(self.to_host, self.port)
            #print ("[proxy({})] connection established")
            print ("代理服务器已和设备连接,正在传输...")
            #现在两个线程都创建了套接字,我们接下来要做的就是交换他们
            self.g2p.server = self.p2s.server   #将与客户端建立的套接字转发给真实服务器
            self.p2s.game = self.g2p.game       #将服务器传回的套接字转发到客户端

            #线程设置完毕,现在我们来真正启动它
            self.g2p.start()
            self.p2s.start()


#写到这里的时候,唯一缺少的就是创建一个或多个代理线程,我们先从主服务器开始
master_server = Proxy('0.0.0.0', '192.168.2.222', 5555)
#监听自己所有本机端口3333,并将它转发到真实的服务器ip 192.168.178.54
master_server.start()   #启动

#_game_server = Proxy('0.0.0.0', '192.168.2.222', 5555)
#_game_server.start()

'''
#除此之外,客户端想要连接多个服务器的时候,我们可以启动多个代理(多分配几个不同端口即可)
for port in range(3000,3006):
    _game_server = Proxy('0.0.0.0','192.168.178.54',port)  
    _game_server.start()
#写到这里就已经可以工作了
'''
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100

一些项目

python socket实现ftp客户端和服务器收发文件及md5加密文件

python 基于socket与opencv实现视频传输

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小舞很执着/article/detail/791281
推荐阅读
相关标签
  

闽ICP备14008679号