Python网络编程

1. socket套接字

1.1 socket简介

1)网络中的进程间通信

在本地可以通过进程PID来唯一标识一个进程,但是在网络中这却是行不通的。

TCP/IP协议族已经解决了这个问题,网络层的“IP地址”可以唯一标识网络中的主机,而传输层“协议+端口”可以唯一标识主机中的应用程序(进程)。

这样利用  IP地址,协议,端口  就可以表示网络中的进程了,网络中的进程通信就可以利用这个标志与其他进程进行交互。

2)什么是socket

socket简称套接字,是进程间通信的一种方式;它能实现不同主机间的进程间通信。

socket是应用层与TCP/IP协议族通信中间软件抽象层,它是一组接口

在设计模式中,socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在socket接口后面,对于用户来说,一组简单的接口就是全部,让socket去组织数据,以符合指定的协议;进而,我们无需去关心TCP/UDP协议的细节,因为socket已经封装好了,我们只需要遵循socket的规定去编程,自然就是遵循tcp/udp标准的。

3)socket层

1.2 套接字的分类

基于文件类型的套接字家族:AF_UNIX

  • 在泛unix系统上,基于文件的套接字调用的就是底层的文件系统来取数据,
  • 两个套接字进程运行在同一机器上,可以通过访问同一个文件来间接完成通信。

基于网络类型的套接字家族:AF_INET

  • 还有AF_INET6被用于ipv6

1.3 套接字的工作流程

1)工作流程图示

2)工作流程解释

  • 服务器端:服务端先初始化socket,然后与端口绑定(bind),对端口进行监听(listen),再调用accept阻塞,等待客户端连接
  • 客户端:服务端初始化完毕后,如果有个客户端初始化一个socket,然后连接服务器(connect),如果简连接成功,这时客户端与服务端的连接就建立了
  • 客户端与服务端进行交互:客户端发送数据请求,服务端接受请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束

1.4 创建socket

1)创建socket示例

  • socket_family:可以选择AF_INET或者AF_UNIX
  • socket_type:可以选择SOCK_STREAM(流式套接字,主要用于TCP协议)或者SOCK_DGRAM(数据报套接字,主要用于UDP协议)
import socket
socket.socket(socket_family,socket_type,protocal=0)
# socket_family 可以是 AF_UNIX 或 AF_INET
# socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM
# protocol 一般不填,默认值为 0

# 获取tcp/ip套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 获取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 由于 socket 模块中有太多的属性,在这里破例使用了'from module import *'语句
# 使用 'from socket import *', 就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码
# 例如tcpSock = socket(AF_INET, SOCK_STREAM)

2)服务端套接字函数

s.bind()     # 绑定(主机,端口号)到套接字
s.listen()   # 开始TCP监听
s.accept()   # 被动接受TCP客户的连接,(阻塞式)等待连接的到来

3)客户端套接字函数

s.connect()     # 主动初始化TCP服务器连接
s.connect_ex()  # connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

4)公共用途的套接字函数

s.recv()            # 接收TCP数据
s.send()            # 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
s.sendall()         # 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
s.recvfrom()        # 接收UDP数据
s.sendto()          # 发送UDP数据
s.getpeername()     # 连接到当前套接字的远端的地址
s.getsockname()     # 当前套接字的地址
s.getsockopt()      # 返回指定套接字的参数
s.setsockopt()      # 设置指定套接字的参数
s.close()           # 关闭套接字

5)面向锁的套接字方法

s.setblocking()     # 设置套接字的阻塞与非阻塞模式
s.settimeout()      # 设置阻塞套接字操作的超时时间
s.gettimeout()      # 得到阻塞套接字操作的超时时间

6)面向文件的套接字的函数

s.fileno()          # 套接字的文件描述符
s.makefile()        # 创建一个与该套接字相关的文件

2. 基于TCP的套接字

2.1 简单的tcp服务端&客户端

  • tcp是基于连接的,必须先启动服务端,然后再启动客户端去连接服务端。

1)socket服务端

import socket

# 创建tcp套接字
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 

# 绑定地址,这里接受的参数是一个元组的形式
phone.bind(('127.0.0.1', 8000))

# 监听连接
phone.listen(5) 
# 此处的listen中的值表示处于半连接和已连接状态的client总和
# 等待中的半连接和已连接都保存在backlog中,此处的linsten的数量相当于就是在指定backlog的大小
# 如果当前已建立连接数和半连接数达到设定值,那么新客户端就不会connect成功,而是等待服务器
# 接受客户端连接,在这个位置等待接收客户端发送的消息 conn, addr = phone.accept() # conn表示为这个客户端创建出了包含tcp三次握手信息的新的套接字 # addr 包含这个客户端的信息 msg = conn.recv(1024) # 接受客户端发来的消息 print('客户端发来的消息是:', msg) conn.send(msg.upper()) # 将接受到的消息转换成大写的形式再发给客户端 conn.close() # 触发四次挥手 phone.close() # 关闭socket

2)socket客户端

import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

phone.connect(('127.0.0.1', 8000)) # 链接服务端的相应端口下的应用

phone.send('hello'.encode('utf-8')) # 接受和发送消息时数据都应该是字节格式

data = phone.recv(1024)

print('收到服务端发来的消息:', data)  
phone.close()

2.2 循环收发消息

1)socket服务端(循环收发)

  • 接收消息的的本质其实是在本机的内核空间内提取内容
  • 发送消息的本质其实是将消息内容由用户空间发送到内核空间,然后由内核来完成消息的发送

 

from socket import *

tcp_server = socket(AF_INET, SOCK_STREAM)

ip_port = ('127.0.0.1', 9999)
listen_buffer = 5
recv_buffer = 1024

tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 解决端口重用的问题
tcp_server.bind(ip_port)
tcp_server.listen(listen_buffer)

while True:
    print('服务端开始运行----->')
    conn, addr = tcp_server.accept()
    while True:
         try:
            print('conn----->',conn)
            print('addr----->',addr)

            date = conn.recv(recv_buffer) # 这里的收消息其实是在本机的内核空间内提取内容
            print(date.decode('utf-8'))

            conn.send('hello world!?'.encode('utf-8')) # 发消息的本质是将消息内容由用户空间发送到内核空间,然后由内核来完成消息的发送
            # conn.sendall(data)                       # sendall函数可以将一整个数据循环着发送

         except Exception:  # 捕获当客户端意外断开时的异常,若发生异常,则退出内循环,而重新尝试连接
     # 如果是在Linux系统上,客户端意外断开链接不会抛出异常,而是一直收空,这时就需要对收到的数据做判断,如果为空就直接break
             break

    conn.close()
tcp_server.close()

 

2)socket客户端(循环收发)

from socket import *

tcp_client = socket(AF_INET, SOCK_STREAM)

ip_port = ('127.0.0.1', 9999)
buffer_size = 1024

tcp_client.connect(ip_port)

while True:
    msg = input("请输入要发送的内容:").strip()
    if not msg : continue

    tcp_client.send(msg.encode("utf-8")) # 这里如果发送的消息为空(直接敲回车),则没有任何内容发往本机内核空间,所以消息根本就不会发送

    data = tcp_client.recv(buffer_size)

    print("接收到的消息为:", data.decode('utf-8'))
tcp_client.close()

2.3 关于端口重用

1)关于Address already in use的报错

  • 由于服务端仍然存在四次挥手的time_wait状态在占用地址
  • 服务器高并发情况下会有大量的time_wait状态的优化方法

2)端口重用的解决方法一:添加socket配置

# 加一条socket配置,重用ip和端口
phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) # 在bind前加上这一句
phone.bind(('127.0.0.1',8080))

3)端口重用的解决方法二:修改内核参数

# 发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决,
vi /etc/sysctl.conf

# 编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
# 然后执行 /sbin/sysctl -p 让参数生效

# 参数说明
net.ipv4.tcp_syncookies = 1 # 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1   # 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1 # 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout    # 修改系統默认的 TIMEOUT 时间

3. 基于UDP的套接字

3.1 UDP简介

1)UDP

UDP(用户数据保协议)是一个无连接的简单面向数据报的传输层协议;

UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地;

由于UDP在传输数据前不用在客户端和服务端之间建立一个连接,且没有超时重发等机制,故而传输速度很快;

UDP是一种面向无连接的协议,每一个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。

2)UDP的特点

UDP是面向无连接的通讯协议,UDP数据包括目的端口号和源端口信息,由于通讯不需要连接,所以可以实现广播发送;

UDP传输数据时有大小限制,每个被传输的数据报必须限定在64KB之内;

UDP是一个不可靠的协议,发送方所发送的数据并不一定以相同的次序到达接收方。

3)适用情况

UDP一般多用于多点通信和实时的数据业务,它注重速度流畅。

  • 语⾳⼴播
  • 视频
  • QQ
  • TFTP(简单⽂件传送)
  • SNMP(简单⽹络管理协议)
  • RIP(路由信息协议,如报告股票市场,航空信息)
  • DNS(域名解释)

3.2 UDP的通信过程

1)图示

  •  创建客户端套接字
  • 发送/接收数据
  • 关闭套接字

2)UDP是无链接的,先启动哪一端都不会报错

3)udp的服务器和客户端的区分

UDP的服务端和客户端往往是通过 请求服务 和 提供服务 来进⾏区分

  • 请求服务的⼀⽅称为:客户端
  • 提供服务的⼀⽅称为:服务器

4)关于UDP的端口绑定

  • ⼀般情况下,服务器端,需要绑定端⼝,⽬的是为了让其他的客户端能够正确发送到此进程
  • 客户端⼀般不需要绑定,⽽是让操作系统随机分配,这样就不会因为需要绑定的端⼝被占⽤⽽导致程序⽆法运⾏的情况

3.3 UDP服务端&客户端

1)服务端

from socket import *

ip_port = ('127.0.0.1', 9999)
buffer_size = 1024

udp_server = socket(AF_INET, SOCK_DGRAM) # 数据报
udp_server.bind(ip_port)

# udp之所以不发生粘包的现象,是因为每次发送消息都封装一个报文头信息

# recv在自己这端的缓冲区为空时,阻塞
# recvfrom在自己这端的缓冲区为空时,就收一个空?(此处描述的不准确)

# udp在发送一个空时,发送的不仅仅只是一个空,而是一个带了报文头信息的空
# 所以在接受时,都接受了一个带报文头信息的空

while True:
    data, addr = udp_server.recvfrom(buffer_size) # recvfrom接受信息的格式为 (b'消息内容', ('ip地址',端口))
    print(data)

    udp_server.sendto(data.upper(), addr)

2)客户端

from socket import *

ip_port = ('127.0.0.1', 9999)
buffer_size = 1024

udp_client = socket(AF_INET, SOCK_DGRAM)

while True:
    msg = input('>>>').strip()
    udp_client.sendto(msg.encode('utf-8'), ip_port) # sendto每次发送时都要指定ip和端口,作为第二个参数

    data, addr = udp_client.recvfrom(buffer_size)
    print(data.decode('utf-8'))

4. 粘包现象

  • 注意:结果的编码是以当前所在的系统为准的,如果是windows,那么读出的信息就是GBK编码的,在接收端要用GBK解码。且只能从管道里读一次结果。

4.1 粘包的产生

在发送和接收数据的过程中,发送端可以1K 、1K的发送数据,而接收端的应用程序可以2K、2K或者一次3K、6K等等的取走数据;

应用程序所看到的数据是一个整体(或者是一个流stream),一条消息有多少字节对于应用程序是不可见的,因为TCP是面向流的协议,就很容易出现粘包;

而UDP段都是一条一条消息(因为每次发送消息都封装了一个报文头信息),应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,所以UDP永远不会发生粘包。

可以认为对方一次性write/send的数据为一个消息,当对方send一条信息时,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

4.2 粘包产生的原因

粘包的主要原因是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据造成的。

发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段,若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就接收到了粘包数据。

4.3 详细分析粘包

  1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
  2. UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
  3. tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,

udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是 y>x 数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。

tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。(tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的)

在tcp中send和recv其实都是在操作各自的缓冲区,并不需要客户端与服务端之间一一对应;而udp中的sendto和recvfrom要一一对应,因为没有将多个数据合并

应用程序产生的数据会拷贝一份个操作系统,然后由操作系统来发送。

4.4 发生粘包的情况

  1. 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
  2. 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

4.5 拆包的发生情况

当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。

4.6 send(字节流)和recv(1024)及sendall

recv里指定的1024意思是从缓存里一次拿出1024个字节的数据。

send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失。

4.7 低配版解决粘包问题

  • 分析:问题的根源在于接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。
  • 缺点:程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗。

1)服务端

from socket import *
import subprocess

ip_port = ('127.0.0.1', 8080)
back_log = 5
buffer_size = 1024

tcp_server = socket(AF_INET, SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)

while True:
    conn, addr = tcp_server.accept()
    print('新的客户端连接', addr)
    while True:
        try:  # 异常处理用于解决客户端意外断开时报异常的问题
            cmd = conn.recv(buffer_size)
            if not cmd:break  # 解决客户端如果退出,服务端一直收空的问题
            print('收到客户端的命令', cmd)

            # 执行命令,得到命令的结果cmd_res
            res = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                stderr = subprocess.PIPE,  # 将标准输入输出和错误全部扔到subprocess的管道中
                                stdout = subprocess.PIPE,
                                stdin = subprocess.PIPE
                                )
            err = res.stdout.read()
            if err:
                cmd_res = err
            else:
                cmd_res = res.stdout.read()

                if not cmd_res: # 若命令执行成功,但返回值为空(如cd .. 命令),我们就自己设置一个返回值发送给客户端以解决卡死的问题
                    cmd_res = "执行成功"

                # 解决粘包的问题---> 将要发送的消息的具体大小发送给客户端,告知其需要接收多少信息
                length = len(cmd_res)
                conn.send(str(length).encode('utf-8'))

                client_ready = conn.recv(buffer_size) # 中间插入了一个recv,造成中断,使得两次send的操作不会合并

                if client_ready == b'ready':          # 如果接收到客户端发送过来的ready信号,服务端就开始发送真正的消息内容
                    conn.send(cmd_res)

        except Exception as e:
            print(e)
            break
    conn.close()

2)客户端

from socket import *

ip_port = ('127.0.0.1', 8080)
back_log = 5
buffer_size = 1024

tcp_client = socket(AF_INET, SOCK_STREAM)
tcp_client.connect(ip_port)

while True:
    cmd = input('>>:').strip()
    if not cmd:continue
    if cmd == "quit":break

    tcp_client.send(cmd.encode('utf-8'))

    # 解决粘包的问题---> 知道到底需要接收多少数据就可以解决
    length = tcp_client.recv(buffer_size) # 接收服务端发送过来的数据的长度
    tcp_client.send(b'ready')

    length = length.decode('utf-8')

    recv_size = 0 # 设定一个标识,以判断数据到底是否接收完毕
    recv_msg = b''

    while recv_size < length: 
        recv_msg += tcp_client.recv(buffer_size) # 将每次接收到的数据合并
        recv_size = len(recv_msg) 

    print('命令的执行结果是', recv_msg.decode('gbk'))
    
tcp_client.close()

4.8 高配版解决粘包问题

  • 分析:每次客户端接收到服务端发送过来的数据长度之后,客户端还要再进行一次回应后才能接收数据,这样如果有众多的客户端,每个客户端在接收数据前都要回应服务端,会增加服务端的负荷。
  • 解决:用 struct 模块将服务端要发送数据的长度打包发送给客户端,然后再把数据发送过去,这样客户端从自己的缓冲区提取数据时就可以知道要提取多少字节的数据了。

1)服务端

#!/usr/bin/python3
#
import socket
import subprocess
import struct

ip_port = ("10.0.0.200", 1778)
back_log = 5
buffer_size = 1024

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(ip_port)
server.listen(back_log)

while True:
    conn, addr = server.accept()
    print("Connection: ", addr)

    while True:
        try:
            cmd = conn.recv(buffer_size)
            if not cmd: break
            print(cmd.decode("utf-8"))
            cmd_res = subprocess.Popen(cmd.decode("utf-8"),shell=True,
                    stdin=subprocess.PIPE,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE)
            err = cmd_res.stderr.read()
            if err:
                result = err
            else:
                result = cmd_res.stdout.read()
                if not result:
                    result = b"Exec Success!"
            length = len(result)
       # 使用这个模块让发送的数值打包为4字节(int类型为4字节),这里的i意味着int类型,length为要转换的对象
            data_len = struct.pack('i', length)
            conn.send(data_len)
            conn.send(result)
        except Exception as e:
            print(e)
            break
    conn.close()

2)客户端

#!/usr/bin/python3
#
import socket
import struct

ip_port = ("10.0.0.200", 1778)
buffer_size = 1024

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(ip_port)

while True:
    cmd = input("CMD: ").strip()
    if not cmd: continue
    if cmd == "q": break
    client.send(cmd.encode("utf-8"))
    length = client.recv(4)
  # 用struct模块的unpack方法来解包,解析出数据包的长度,返回的是一个元组的形式,我们打包的值在0索引位置
    data_len = struct.unpack("i", length)[0]

    recv_data = b""
    recv_len = 0
    while recv_len < data_len:
        recv_data += client.recv(buffer_size)
        recv_len = len(recv_data)
    print(recv_data.decode("utf-8"))
client.close()

 4.9 自定制报文头

  • 以下都是伪代码
# ----------------------发送端------------------------
fileinfo = {  # 先以字典的形式保存报文头
    'filename': 'hgzero.txt',
    'filesize': '2666'
}

data_head = pickle.dumps(fileinfo) # 以pickle的格式将报文头信息转换成二进制信息
length = len(data_head) # 计算出报文头的长度

data_length = struck.pack('i', length) # 将报文头的长度信息以int类型格式(4字节)打包起来

conn.send(data_length) # 报文头长度信息
conn.send(data_head) # 报文头的真实信息
# 上面的两次连续的send必然会发送粘包现象

 # 发送真实数据
conn.send(ret) 


# ----------------------接受端------------------------

data_length = tcp_client.recv(4) 
data_length = truckt.unpack('i', data_length)[0] # 接受到了报文头的长度信息,取出长度信息

picks_data = tcp_client.recv(data_length) # 取出以pickle格式保存的报头信息

fileinfo = pickle.loads(picks_data) # 将pickle格式的信息反序列化成原本的格式

fileinfo['filename'] # 取出字典中的信息
fileinfo['filesize']

5. socketserver实现并发

5.1 socketserver的基本使用

import socketserver

class MyServer(socketserver.BaseRequestHandler):
    def handle(self):     # 必须要自己实现handle方法,在这个方法中进行消息的接收和发送
        print('conn is', self.request)
        print('addr is', self.client_address)

        while  True:
            try:
            # 收消息
                self.request.recv(1024)
                print('收到客户端的消息是', data.self.client_address)

                # 发消息
                self.request.sendall(data.upper())

            except Exception as e:
                print(e)


if __name__ == '__main__':
    s = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MyServer)
    s.serve_forever()   # 相当于 while True

# server类:处理连接
# request类:处理通信

 

 

 

 

posted @ 2020-08-14 15:54  Praywu  阅读(244)  评论(0编辑  收藏  举报