IO网络编程
[toc]

1×00 IO

1×01 定义

在内存中存在数据交换的操作认为是IO操作,比如和终端交互 ,和磁盘交互,和网络交互等

1×02 程序分类

  • IO密集型程序:在程序执行中有大量IO操作,而cpu运算较少。消耗cpu较少,耗时长。
  • 计算密集型程序:程序运行中计算较多,IO操作相对较少。cpu消耗多,执行速度快,几乎没有阻塞。

2×00 文件

文件是保存在持久化存储设备(硬盘、U盘、光盘..)上的一段数据。从功能角度分为文本文件(打开后会自动解码为字符)、二进制文件(视频、音频等)。在Python里把文件视作一种类型的对象,类似之前学习过的其它类型。

2×01 字节串(bytes)

在python3中引入了字节串的概念,与str不同,字节串以字节序列值表达数据,更方便用来处理二进程数据。因此在python3中字节串是常见的二进制数据展现方式。

  • 普通的ascii编码字符串可以在前面加b转换为字节串,例如:b’hello’
  • 字符串转换为字节串方法 :encode()
  • 字节串转换为字符串方法 : decode()

2×02 文件读写

对文件实现读写的基本操作步骤为:打开文件—>读写文件—>关闭文件

打开文件的方法:open(file_name, access_mode='r', buffering=-1),这是一个内建函数,可以在py文件中直接

使用示例:file_object = open(file_name, access_mode='r', buffering=-1)

其中返回的是一个文件对象。

第一个参数file_name:str类型-文件的路径

第二个参数access_mode:文件打开的方式。具体参数见下表格

文件模式操作
r以读方式打开 文件必须存在
w以写方式打开
文件不存在则创建,存在清空原有内容
a以追加模式打开
r+以读写模式打开 文件必须存在
w+以读写模式打开文件
不存在则创建,存在清空原有内容
a+以读写模式打开 追加模式
rb以二进制读模式打开 同r
wb以二进制写模式打开 同w
ab以二进制追加模式打开 同a
rb+以二进制读写模式打开 同r+
wb+以二进制读写模式打开 同w+
ab+以二进制读写模式打开 同a+

第三个参数buffering: 1表示有行缓冲,如果不写或为负数则表示使用系统默认提供的缓冲机制。

缓冲:系统自动的在内存中为每一个正在使用的文件开辟一个缓冲区,从内存向磁盘输出数据必须先送到内存缓冲区,再由缓冲区送到磁盘中去。从磁盘中读数据,则一次从磁盘文件将一批数据读入到内存缓冲区中,然后再从缓冲区将数据送到程序的数据区。

2x02x01 文件的读

read([size])

readline([size])

readlines([sizeint])

功能: 用来直接读取文件中字符

参数:如果没有给定size参数(默认值-1)或者size为负,给定size表示最多读取指定的字符(字节)。返回值:返回读取到的内容

功能: 用来读取文件中一行

参数: 如果没有给定size参数(默认值为-1)或者size值为负,表示读取一行,给定size表示最多读取制定的字符(字节)。

返回值: 返回读取到的内容

功能: 读取文件中的每一行作为列表中的一项

参数: 如果没有给定size参数(默认值为-1)或者size值为负,文件将被读取直至末尾,给定size表示读取到size字符所在行为止。

返回值: list类型-返回读取到的内容

文件对象本身也是一个可迭代对象,在for循环中可以迭代文件的每一行。

for line in f:
     print(line)

2x02x02文件的写

write(string)

writelines(str_list)

功能: 把文本数据或二进制数据块的字符串写入到文件中去

参数:要写入的内容

如果需要换行要自己在写入内容中添加\n

功能:接受一个字符串列表作为参数,将它们写入文件。

参数: list类型–要写入的内容。一个代表一行

2x02x03 关闭文件

打开一个文件后我们就可以通过文件对象对文件进行操作了,当操作结束后使用close()关闭这个对象可以防止一些误操作,也可以节省资源。

file_object.close()

2x02x04 with的操作

python中的with语句使用于对资源进行访问的场合,保证不管处理过程中是否发生错误或者异常都会执行规定的“清理”操作,释放被访问的资源,比如有文件读写后自动关闭、线程中锁的自动获取和释放等。

with语句的语法格式如下:

with context_expression [as target(s)]:
    with-body

通过with方法可以不用close(),因为with生成的对象在语句块结束后会自动处理,所以也就不需要close了,但是这个文件对象只能在with语句块内使用。

with open('file','r+') as f:
    f.read()

2×03 其他操作

2x03x01 刷新缓冲区

flush()

该函数调用后会进行一次磁盘交互,将缓冲区中的内容写入到磁盘。

缓冲区的好处:可以减少和磁盘的交互

file_object = open(file_name, access_mode='r', buffering=-1)
buffer=0 (无缓存)
buffer=1 表示行缓冲则在写入数据时遇到\n自动刷新缓冲区,即缓存一行写入一行
# 采用系统默认缓存需要缓冲区满后才能自动写入磁盘
# 无论什么缓冲,当程序结束后或者文件被关闭都会将缓冲区内容写入磁盘
# 通过调用flush()可以立刻刷新缓冲
fd = open("test", "w", 12)
while True:
    s = input(">>")
    fd.write(s)
    fd.flush()  # 立即刷新缓冲,将内容写入磁盘

2x03x02 文件偏移量

定义

打开一个文件进行操作时系统会自动生成一个记录,记录中描述了我们对文件的一系列操作。其中包括每次操作到的文件位置。文件的读写操作都是从这个位置开始进行的。

即打开文件的模式中带有+号的模式 —在文件最后追加,即是将文件偏移量移动到了文件的末尾

基本操作

tell()

功能:获取文件偏移量大小

seek(offset[,whence])

功能:移动文件偏移量位置

参数:offset 代表相对于某个位置移动的字节数。负数表示向前移动,正数表示向后移动。

whence是基准位置的默认值为 0,代表从文件开头算起,1代表从当前位置算起,2 代表从文件末尾算起。

注意:当whence这个参数不是默认值的时候,打开文件的方式要使用二进制的方式,即带有“b”的打开方式

2x03x03 文件描述符

定义

系统中每一个IO操作都会分配一个整数作为编号,该整数即这个IO操作的文件描述符。

获取文件描述符

fileno()
通过IO对象获取对应的文件描述符

2×04 文件管理函数

这些函数需要导入包os——>import os

功能函数
获取文件大小 os.path.getsize(file)
查看文件列表 os.listdir(dir)
查看文件是否存在os.path.exists(file)
判断文件类型os.path.isfile(file)
删除文件os.remove(file)

3×00 网络编程基础

计算机网络功能主要包括实现资源共享,实现数据信息的快速传递。

3×01 OSI七层模型

制定组织: ISO(国际标准化组织)

作用:使网络通信工作流程标准化

应用层提供用户服务,具体功能有应用程序实现
表示层数据的压缩优化加密
会话层建立用户级的连接,选择适当的传输服务
传输层提供传输服务
网络层路由选择,网络互联
链路层进行数据交换,控制具体数据的发送
物理层提供数据传输的硬件保证,网卡接口,传输介质

优点

  1. 建立了统一的工作流程
  2. 分部清晰,各司其职,每个步骤分工明确
  3. 降低了各个模块之间的耦合度,便于开发

3×02 四层模型(TCP/IP模型)

成因: 实际工作中工程师无法完全按照七层模型要求操作,逐渐演化为更符合实际情况的四层

%title插图%num

数据传输过程

  1. 发送端由应用程序发送消息,逐层添加首部信息,最终在物理层发送消息包。
  2. 发送的消息经过多个节点(交换机,路由器)传输,最终到达目标主机。
  3. 目标主机由物理层逐层解析首部消息包,最终到应用程序呈现消息。
%title插图%num

网络协议

在网络数据传输中,都遵循的规定,包括建立什么样的数据结构,什么样的特殊标志等。

3×03 网络基础概念

网络主机(host)

功能:标识一台主机在网络中的位置(地址)

本地地址 : ‘localhost’ , ‘127.0.0.1’
网络地址 : ‘172.40.91.185’
自动获取地址: ‘0.0.0.0’

查看本机网络地址命令: ifconfig

IP地址

功能:确定一台主机的网络路由位置

结构

IPv4 点分十进制表示 172.40.91.185 每部分取值范围0–255
IPv6 128位 扩大了地址范围

特殊IP

127.0.0.1 本机测试IP
0.0.0.0 自动获取本机网卡地址
172.40.91.0 通常表示一个网段
172.40.91.1 通常表示一个网关
172.40.91.255 用作广播地址

域名

定义: 给网络服务器地址起的名字

†作用: 方便记忆,表达一定的含义

ping [ip] : 测试和某个主机是否联通

端口号(port)

作用:端口是网络地址的一部分,用于区分主机上不同的网络应用程序。

特点:一个系统中的应用监听端口不能重复

取值范围: 1 — 65535

1–1023 系统应用或者大众程序监听端口
1024–65535 自用端口

4×00 传输层服务

4×01 面向连接的传输服务(基于TCP协议的数据传输)

  1. 传输特征 : 提供了可靠的数据传输,可靠性指数据传输过程中无丢失,无失序,无差错,无重复。
  2. 实现手段 : 在通信前需要建立数据连接,通信结束要正常断开连接。
  3. 适用情况 : 对数据传输准确性有明确要求,传数文件较大,需要确保可靠性的情况。比如:网页获取,文件下载,邮件收发。

三次握手(建立连接)

客户端向服务器发送消息报文请求连接
服务器收到请求后,回复报文确定可以连接
客户端收到回复,发送最终报文连接建立

%title插图%num

四次挥手(断开连接)

主动方发送报文请求断开连接
被动方收到请求后,立即回复,表示准备断开
被动方准备就绪,再次发送报文表示可以断开
主动方收到确定,发送最终报文完成断开

%title插图%num

4×02 面向无连接的传输服务(基于UDP协议的数据传输)

  1. 传输特点 : 不保证传输的可靠性,传输过程没有连接和断开,数据收发自由随意。
  2. 适用情况 : 网络较差,对传输可靠性要求不高。比如:网络视频,群聊,广播

面试要求

  • OSI七层模型介绍一下,tcp/ip模型是什么?
  • tcp服务和udp服务有什么区别?
  • 三次握手和四次挥手指什么,过程是怎样的?

5×00 Socket套接字编程

5×01 套接字介绍

  1. 套接字 : 实现网络编程进行数据传输的一种技术手段
  2. Python实现套接字编程:import socket
  3. 套接字分类

流式套接字(SOCK_STREAM): 以字节流方式传输数据,实现tcp网络传输方案。(面向连接–tcp协议–可靠的–流式套接字)

数据报套接字(SOCK_DGRAM):以数据报形式传输数据,实现udp网络传输方案。(无连接–udp协议–不可靠–数据报套接字)

5×02 tcp套接字编程

服务端流程

%title插图%num

创建套接字

sockfd=socket.socket(socket_family=AF_INET,socket_type=SOCK_STREAM,proto=0)
功能:创建套接字
参数:  socket_family  网络地址类型 AF_INET表示ipv4
    socket_type  套接字类型 SOCK_STREAM 流式SOCK_DGRAM 数据报
    proto  通常为0  选择子协议
返回值: 套接字对象

绑定地址

sockfd.bind(addr)
功能: 绑定本机网络地址
参数: 二元元组 (ip,port)  ('0.0.0.0',8888)

设置监听

sockfd.listen(n)
功能 : 将套接字设置为监听套接字,确定监听队列大小
参数 : 监听队列大小

等待处理客户端连接请求

connfd,addr = sockfd.accept()
功能: 阻塞等待处理客户端请求
返回值: connfd  客户端连接套接字
         addr  连接的客户端地址

消息收发

data = connfd.recv(buffersize)
功能 : 接受客户端消息
参数 :每次最多接收消息的大小
返回值: 接收到的内容

n = connfd.send(data)
功能 : 发送消息
参数 :要发送的内容  bytes格式
返回值: 发送的字节数

关闭套接字

sockfd.close()
功能:关闭套接字
"""
TCP套接字服务端
重点代码
"""
import socket

#  创建套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#  绑定地址
sock.bind(("0.0.0.0", 8888))
#  设置监听
sock.listen(5)
while True:
    #  等待处理客户端连接
    print("Waiting for connect....")
    try:
        connfd, addr = sock.accept()
        print("Connect from:", addr)
    except KeyboardInterrupt:
        print("退出服务")
        break

    while True:
        #  收发消息
        data = connfd.recv(1024)
        #  得到空则退出循环
        if not data:
            break
        print("接受到消息:", data.decode())

        n = connfd.send(b"Receive your message")
        print("发生了%d个字节数据" % n)

    #  关闭套接字
    connfd.close()
sock.close()

客户端流程

%title插图%num
  1. 创建套接字

注意:只有相同类型的套接字才能进行通信

  1. 请求连接
sockfd.connect(server_addr)
功能:连接服务器
参数:元组  服务器地址
  1. 收发消息

注意: 防止两端都阻塞,recv send要配合

  1. 关闭套接字
"""
TCP 客户端程序
重点代码
"""
from socket import *

#  创建套接字
sock = socket()
#  发起连接
server_addr = ("192.168.8.109", 8888)
sock.connect(server_addr)
while True:

    #  消息的收发
    msg = str(input(">>"))
    if not msg:
        break
    sock.send(msg.encode())
    data = sock.recv(1024)
    print("From server:", data.decode())
#  关闭
sock.close()

tcp 套接字数据传输特点

  • tcp连接中当一端退出,另一端如果阻塞在recv,此时recv会立即返回一个空字串。
  • tcp连接中如果一端已经不存在,让然试图通过send发送则会产生BrokenPipeError
  • 一个监听套接字可以同时连接多个客户端,也能够重复被连接

网络收发缓冲区

  1. 网络缓冲区有效的协调了消息的收发速度
  2. send和recv实际是向缓冲区发送接收消息,当缓冲区不为空recv就不会阻塞。

tcp粘包

原因:tcp以字节流方式传输,没有消息边界。多次发送的消息被一次接收,此时就会形成粘包。

影响:如果每次发送内容是一个独立的含义,需要接收端独立解析此时粘包会有影响。

处理方法

  1. 人为的添加消息边界
  2. 控制发送速度

5×03 UDP套接字编程

服务端流程

%title插图%num
  1. 创建数据报套接字
sockfd = socket(AF_INET,SOCK_DGRAM)
  1. 绑定地址
sockfd.bind(addr)
  1. 消息收发

“`python
data,addr = sockfd.recvfrom(buffersize)
功能: 接收UDP消息
参数: 每次最多接收多少字节
返回值: data 接收到的内容
addr 消息发送方地址

n = sockfd.sendto(data,addr)
功能: 发送UDP消息
参数: data 发送的内容 bytes格式
addr 目标地址
返回值:发送的字节数

4. 关闭套接字

python
sockfd.close()
“`

"""
UDP套接字
"""

from socket import *

# 创建数据报套接字
sock = socket(AF_INET, SOCK_DGRAM)

# 绑定地址
server_addr = ("0.0.0.0", 9999)
sock.bind(server_addr)

# 收发消息
while True:
    data, addr = sock.recvfrom(1024)
    print("收到消息:", data.decode())
    sock.sendto(b"Thanks", addr)

# 关闭套接字
sock.close()

客户端流程

%title插图%num
  1. 创建套接字
  2. 收发消息
  3. 关闭套接字
"""
udp-client
"""
from socket import *

# 服务器信息
HOST = "0.0.0.0"
PORT = 9999
ADDR = (HOST, PORT)
# 创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM)

# 收发消息
while True:
    data = input(">??>")
    if not data:
        break
    sockfd.sendto(data.encode(), ADDR)

    msg, addr = sockfd.recvfrom(1024)
    print("From Server:", msg.decode())

sockfd.close()

总结 :tcp套接字和udp套接字编程区别

  1. 流式套接字是以字节流方式传输数据,数据报套接字以数据报形式传输
  2. tcp套接字会有粘包,udp套接字有消息边界不会粘包
  3. tcp套接字保证消息的完整性,udp套接字则不能
  4. tcp套接字依赖listen accept建立连接才能收发消息,udp套接字则不需要
  5. tcp套接字使用send,recv收发消息,udp套接字使用sendto,recvfrom

5×04 socket模块方法和socket套接字属性

部分socket模块方法

gethostname() 获取计算机名
gethostbyname('www.baidu.com') 获取主机ip地址
getservbyname('mysql') 获取服务端口号
getservbyport(3306) 获取端口对应服务
inet_aton('192.168.1.2') 将IP转换为bytes子串
inet_ntoa(b'\xc0\xa8\x01\x02') 将bytes子串转换为IP地址

套接字属性

sockfd.type 套接字类型
sockfd.family 套接字地址类型
sockfd.getsockname() 获取套接字绑定地址
sockfd.fileno() 获取套接字的文件描述符
sockfd.getpeername() 获取连接套接字客户端地址
sockfd.getsockopt(level,option) 功能 : 获取套接字选项值

sockfd.setsockopt(level,option,value)
功能:设置套接字选项
参数: level 选项类别 SOL_SOCKET
option 具体选项内容
value 选项值

%title插图%num

5×05 UDP套接字广播

# 广播接受

"""
1.创建udp套接字
2.选择接受端口
3.设置套接字为可以接受广播
"""
from socket import *

s = socket(AF_INET, SOCK_DGRAM)

# 让套接字可以接受广播
s.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)

s.bind(("0.0.0.0", 9999))

while True:
    try:
        msg,addr = s.recvfrom(1024)
    except KeyboardInterrupt:
        break
    else:
        print(msg.decode())

s.close()
# 广播发送

from socket import *
from time import sleep

# 广播地址
dest = ("192.168.8.255", 9999)

s = socket(AF_INET, SOCK_DGRAM)

s.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)

data = "蔡徐坤6666"
while True:
    sleep(2)
    s.sendto(data.encode(),dest)

s.close()

广播定义 : 一端发送多点接收

  • 广播地址 : 每个网络的最大地址为发送广播的地址,向该地址发送,则网段内所有主机都能接收。

5×06 TCP套接字之HTTP传输

HTTP协议 (超文本传输协议)

  1. 用途 : 网页获取,数据的传输
  2. 特点
    1. 应用层协议,传输层使用tcp传输简单,灵活,很多语言都有HTTP专门接口无状态,协议不记录传输内容http1.1 支持持久连接,丰富了请求类型
  3. 网页请求过程
    1. 客户端(浏览器)通过tcp传输,发送http请求给服务端
    2. 服务端接收到http请求后进行解析
    3. 服务端处理请求内容,组织响应内容
    4. 服务端将响应内容以http响应格式发送给浏览器
    5. 浏览器接收到响应内容,解析展示
%title插图%num

HTTP请求代码实现

from socket import *


# 处理浏览器的http请求
def handle(connfd):
    print("Request from", connfd.getpeername())
    request = connfd.recv(4096)  # 接受http请求
    # 防止客户端断开
    if not request:
        return
    request_line = request.splitlines()[0].decode()  # 按照行进行分割
    # 获取请求内容
    info = request_line.split(" ")[1]
    if info == "/":
        f = open("index.html", "r")
        response = "HTTP/1.1 200 OK\r\n"
        response += "Content-Type: text/html\r\n"
        response += "\r\n"
        response += f.read()
    else:
        response = "HTTP/1.1 200 OK\r\n"
        response += "Content-Type: text/html\r\n"
        response += "\r\n"
        response += "<h1>Sorry 404 Not Found</h1>"

    # 想浏览器发送内容
    connfd.send(response.encode())
    print(info)


#  搭建tcp网络
def main():
    sockfd = socket()
    sockfd.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    sockfd.bind(("0.0.0.0", 8888))
    sockfd.listen(3)
    print("listen 8888")
    while True:
        connfd, addr = sockfd.accept()
        handle(connfd)  # 处理浏览器请求
        connfd.close()


if __name__ == "__main__":
    main()
"""
http客户端实现
"""
from socket import *

# 创建tcp套接字

s = socket(AF_INET, SOCK_STREAM)
s.bind(("0.0.0.0", 8888))
s.listen(3)

c, addr = s.accept()
data = c.recv(4096)
print(data.decode())
# http响应格式
data = """HTTP/1.1 200 OK
Content-Type:text/html

<h1>hello world</h1>
"""
c.send(data.encode("utf-8"))
fr = open("index.html", "r")
data = fr.read()
c.send(data.encode("utf-8"))

c.close()
s.close()

请求行 : 具体的请求类别和请求内容

%title插图%num

请求类别:每个请求类别表示要做不同的事情

GET获取网络资源
POST提交一定的信息,得到反馈
HEAD只获取网络资源的响应头
PUT更新服务器资源
DELETE删除服务器资源
CONNECTHTTP/1.1预留
TRACE测试
OPTIONS获取服务器性能信息

请求头:对请求的进一步解释和描述

Accept-Encoding: gzip

空行

请求体: 请求参数或者提交内容

http响应(response)

响应格式:响应行,响应头,空行,响应体

响应行 : 反馈基本的响应情况

%title插图%num

响应码

1xx提示信息,表示请求被接收
2xx响应成功
3xx响应需要进一步操作,重定向
4xx客户端错误
5xx服务器错误

响应头:对响应内容的描述

Content-Type: text/html

响应体:响应的主体内容信息

6×00 struct模块的使用

原理: 将一组简单数据进行打包,转换为bytes格式发送。或者将一组bytes格式数据,进行解析。

可以用与不同语言的数据传输

接口使用

Struct(fmt)
功能: 生成结构化对象
参数:fmt  定制的数据结构

st.pack(v1,v2,v3....)
功能: 将一组数据按照指定格式打包转换为bytes
参数:要打包的数据
返回值: bytes字节串

st.unpack(bytes_data)
功能: 将bytes字节串按照指定的格式解析
参数: 要解析的字节串
返回值: 解析后的内容

struct.pack(fmt,v1,v2,v3...)
struct.unpack(fmt,bytes_data)

参数fmt:订制的数据结构——-> 假如数据为(1 zhangsan 23 34.1)———>对应的参数为(i8sif)。

i–>int类型:1。 8s–>str类型:zhangsan。 i–>int类型:23。 f–>float类型:34.1

说明: 可以使用struct模块直接调用pack unpack。此时这两函数第一个参数传入fmt。其他用法功能相同

%title插图%num
"""
struct信息接收端
"""
from socket import *
import struct

soc = socket(AF_INET, SOCK_DGRAM)

soc.bind(('0.0.0.0', 8888))

while True:
    try:
        msg, addr = soc.recvfrom(1024)
        st = struct.Struct("i32sif")
        msg = st.unpack(msg)
        info = "%d %s %d %.2f \n" % (msg[0], msg[1].decode(), msg[2], msg[3])
        if msg:
            fw = open("student_info.txt", "a+")
            fw.write(info)
        else:
            break
    except KeyboardInterrupt:
        break

fw.close()
soc.close()
"""
struct信息发送端
"""
from socket import *
import struct

HOST = "0.0.0.0"
PORT = 8888
ADDR = (HOST, PORT)
soc = socket(AF_INET, SOCK_DGRAM)

while True:
    try:
        id = int(input("请输入学生id>>"))
        name = input("请输入学生名字>>")
        age = int(input("请输入学生年纪>>"))
        score = float(input("请输入学生成绩>>"))
        st = struct.Struct("i32sif")
        data = st.pack(id, name.encode(), age, score)
        soc.sendto(data, ADDR)
    except KeyboardInterrupt:
        break
soc.close()
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇