最新版本查看:https://www.cnblogs.com/dotnetcrazy/p/9919202.htmlhtml
官方文档:https://docs.python.org/3/library/ipc.html(进程间通讯和网络)python
实例代码:https://github.com/lotapp/BaseCode/tree/master/python/6.netgit
已经讲了不少Python的知识了,那Python能干啥呢?这个是我眼中的Python:程序员
Python方向:github
物联网系
(lot
万物互联)安全
与测试
)若是想专攻Web
、爬虫
、物联网
、游戏
等等方向,网络
这块是硬条件,So ==> 不要不急,我们继续学习~redis
多句嘴,通常状况下须要什么库就去官方看下,没有再考虑第三方:https://docs.python.org/3/library
编程
技术前景:(注意加粗方向)小程序
Data
LoT
AI
Web
System
小程序
移动端
Web端
高并发
、区块链
)、C(基础
)NetCore
(WebAPI
、EFCore
)总的来讲:Python最吃香,Go最有潜力,Web必不可少,NetCore性价比高
缓存
如今基本没有单一方向的程序员了,若是有能够默默思考几分钟,通常都是JS
and Python
and (Go
or NetCore
)【二选一】安全
其余行业:(仅表明逆天我的见解)
影视制做
(剪辑师、合成师、特效师)【目前最火,性价比很高】修图师
(商业修片、影楼后期)【大咖特别多,创业很吃香】UI|UE
(最容易找工做)平面设计
(最多见)室内设计
(高手很吃香)幼儿编程
和中医课
最火琴棋书画武
+国学
需求颇高英语
一直是出国必学新媒体+短视频
出国游
OSI
7层模型我用PPT画了个图:(物
数
网
传
会
表
应
)
TCP/IP
4层模型物、数
)
会、表、应
)咱们基本上都是关注这个
计算机和计算机网络通讯前达成的一种约定,举个例子:以汉语为交流语言
再举个发送文件的例子,PPT作个动画:(自定义协议-文件传输演示)
B/S
基本上都是HTTP
协议,C/S
开发的时候有时会使用本身的协议,好比某大型游戏,好比不少框架都有本身的协议:
redis://
dubbo://
协议总的来讲,基本上都是HTTP
协议,对性能要求高的就使用TCP
协议,更高性能要求就本身封装协议了,好比腾讯在UDP
基础上封装了本身的协议来保证通讯的可靠性
先看一个老外
的动画(忽略水印广告):https://v.qq.com/x/page/w01984zbrmy.html
以TCP/IP四层协议为例:数据包的逐层封装
和解包
都是操做系统
来作的,咱们只管应用层
发送过程:
TCP
段首IP
报头PPT动画示意:
接收过程:
IP
的报头TCP
的段首PPT动画示意:
咱们下面按照解包顺序简单说说各类格式:
先看一下这个是啥?用上面动画内容表示:
以太网帧协议:根据MAC
地址完成数据包传递
若是只知道IP,并不知道MAC
地址,可使用ARP
请求来获取:
ARP
数据报:根据IP
获取MAC
地址(网卡编号)ARP
只适合IPv4
,IPv6
用ICMPV6
来代替ARP
TCP/IP
模型中,ARP
协议属于IP
层;在OSI
模型中,ARP
协议属于链路层PPT画一张图:1bit = 8byte
(1字节=8位)
课后思考:根据ARP原理想一想ARP欺骗
到底扎回事?(IP进行ARP请求后会缓存,缓存失效前不会再去ARP请求)
扩展:
RARP 是反向地址转换协议,经过 MAC 地址肯定 IP 地址
MAC
地址就是硬件地址,厂商向全球组织申请惟一编号(相似于身份证)先贴一IP段格式图片(网络):
咱们在这不去详细讲解,扩展部分有课后拓展,我就说一个大多数人困惑的点:
查看IP
信息的时候常常会看到192.168.36.235/24
,这个/24
一直争议很大
咱们来简单解释一下:IP为192.168.36.235
192.168.36
:网络标识235
:主机标识/24
:标识从头数到多少位为止属于网络标识(剩下的就是可分配的主机数了)
11111111 11111111 11111111 00000000
(24个1)255.255.255.0
(/多少
就数多少个1,而后转化)扩展:IP属于面向无链接行(IP
协议不保证传输的可靠性,数据包在传输过程当中可能丢失,可靠性能够在上层协议或应用程序中提供支持)
面向链接
和面向无链接
区别如图:(图片来自网络)
关于TCP和UDP的内容下次继续~
课外拓展:
图解TCP/IP第五版 连接: https://pan.baidu.com/s/1C4kpNd2MvljxfwTKO082lw 提取码: 7qce Python网络编程第三版 Code:https://github.com/brandon-rhodes/fopnp PDF:连接: https://pan.baidu.com/s/1jhW-Te-GCEFKrZVf46S_Tw 提取码: d7fw 网络基础-含书签(网络文档) 连接: https://pan.baidu.com/s/1WZ1D4BthA4qBk2QXBAjm4w 提取码: jmdg 老外讲解网络数据包解析: 下载:https://pan.baidu.com/s/1uUjahs_b05y9Re9ROtzzIw 中文:http://video.tudou.com/v/XMjE3MTg0NzkzNg==.html 英文:http://video.tudou.com/v/XMTkyNjU5NDYwOA==.html
实例代码:https://github.com/lotapp/BaseCode/tree/master/python/6.net/1.UDP
UDP
是无链接的传输协议,不保证可靠性。使用UDP
协议的应用程序须要本身完成丢包重发、消息排序等工做(有点像寄信)
看个UDP的简单案例:
import socket def main(): # AF_INET ==> IPV4;SOCK_STREAM ==> 类型是TCP,stream 流 # SOCK_DGRAM ==> 类型是UDP,dgram 数据报、数据报套接字 with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_sock: udp_sock.sendto("大兄弟,你好啊".encode("utf-8"), ("192.168.36.235", 8080)) print("over") if __name__ == '__main__': main()
接收到的消息:这时候端口是随机的
看起来代码还挺麻烦,我稍微分析下你就知道对比其余语言真的太简单了:
标识:
AF_INET
==> IPV4
SOCK_DGRAM
==> 类型是UDP
SOCK_STREAM
==> 类型是TCP
代码三步走:
udp_sock=socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_sock.sendto(Bytes内容,(IP,Port))
接收:udp_sock.recvfrom(count)
udp_sock.close()
借助调试工具
(点我下载)能够知道:上面程序每次运行,端口都不固定
那怎么使用固定端口呢?==> udp_socket.bind(('', 5400))
import socket def main(): with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_socket: # 绑定固定端口 udp_socket.bind(('', 5400)) # 发送消息 udp_socket.sendto("小明,你知道小张的生日吗?\n".encode("utf-8"), ("192.168.36.235", 8080)) print("over") if __name__ == '__main__': main()
消息图示:nc -ul 8080
(nc -l
是监听TCP)
调试工具:
先看一个简单版本的:udp_socket.recvfrom(1024)
from socket import socket, AF_INET, SOCK_DGRAM def main(): with socket(AF_INET, SOCK_DGRAM) as udp_socket: # 绑定端口 udp_socket.bind(('', 5400)) while True: # 发送消息 udp_socket.sendto("你能够给我离线留言了\n".encode("utf-8"), ("192.168.36.235", 8080)) # 接收消息(data,(ip,port)) data, info = udp_socket.recvfrom(1024) print(f"[来自{info[0]}:{info[1]}的消息]:\n{data.decode('utf-8')}") if __name__ == '__main__': main()
图示:接收消息(data,(ip,port))
其实若是你使用Nmap
来扫描的话并不能发现nc
打开的UDP
端口:
稍微解释一下:扫描其实就是发了几个空消息过去
-sU
表明扫描UDP,-sT
表明扫描TCP-Pn
这个主要是针对有些服务器禁用ping的处理(ping不通也尝试)-p
指定端口号,若是是全部端口可使用-p-
sudo
是由于在Ubuntu
下没权限,kali
下能够直接使用nmap
可能有人对nc
输出的你能够给离线留意了
有疑惑,其实就是在给5400端口发空消息的时候~True循环了两次
来张对比图:
扫描TCP和UDP端口:sudo nmap -sTU 192.168.36.235 -Pn
课后扩展:
NC命令扩展:https://www.cnblogs.com/nmap/p/6148306.html Nmap基础:https://www.cnblogs.com/dunitian/p/5074784.html
若是仍是用True循环来实现:
from socket import socket, AF_INET, SOCK_DGRAM def main(): with socket(AF_INET, SOCK_DGRAM) as udp_socket: # 绑定端口 udp_socket.bind(('', 5400)) while True: msg = input("请输入发送的内容:") if msg == "dotnetcrazy": break else: udp_socket.sendto( msg.encode("utf-8"), ("192.168.36.235", 8080)) data, info = udp_socket.recvfrom(1024) print(f"[来自{info[0]}:{info[1]}的消息]:\n{data.decode('utf-8')}") if __name__ == '__main__': main()
你会发现,消息不能轮流发送,只能等对方方式后再发,虽然有处理方式,但太麻烦,这时候就可使用咱们以前说的多线程来改写一下了:
from socket import socket, AF_INET, SOCK_DGRAM from multiprocessing.dummy import Pool as ThreadPool def send_msg(udp_socket): while True: msg = input("输入须要发送的消息:\n") udp_socket.sendto(msg.encode("utf-8"), ("192.168.36.235", 8080)) def recv_msg(udp_socket): while True: data, info = udp_socket.recvfrom(1024) print(f"[来自{info[0]}:{info[1]}的消息]:\n{data.decode('utf-8')}") def main(): # 建立一个Socket with socket(AF_INET, SOCK_DGRAM) as udp_socket: # 绑定端口 udp_socket.bind(('', 5400)) # 建立一个线程池 pool = ThreadPool() # 接收消息 pool.apply_async(recv_msg, args=(udp_socket, )) # 发送消息 pool.apply_async(send_msg, args=(udp_socket, )) pool.close() # 再也不添加任务 pool.join() # 等待线程池执行完毕 print("over") if __name__ == '__main__': main()
输出:(就一个注意点~socket在pool以后关闭
)
调试工具功能比较简单,咱们手写一个UDP
版的:
from socket import socket, AF_INET, SOCK_DGRAM from multiprocessing.dummy import Pool as ThreadPool def get_port(msg): """获取用户输入的端口号""" while True: port = input(msg) try: port = int(port) except Exception as ex: print(ex) else: return port # 没有错误就退出死循环 def recv_msg(udp_socket): """接收消息""" while True: data, info = udp_socket.recvfrom(1024) print(f"[来自{info[0]}:{info[1]}的消息]:\n{data.decode('utf-8')}") def send_msg(udp_socket): """发送消息""" ip = input("请输入对方IP:") port = get_port("请输入对方端口号:") while True: msg = input("请输入发送的消息:\n") udp_socket.sendto(msg.encode("utf-8"), (ip, port)) def main(): with socket(AF_INET, SOCK_DGRAM) as udp_socket: # 绑定端口 udp_socket.bind(('', get_port("请输网络助手的端口号:"))) # 建立一个线程池 pool = ThreadPool() # 接收消息 pool.apply_async(recv_msg, args=(udp_socket, )) # 发送消息 pool.apply_async(send_msg, args=(udp_socket, )) pool.close() pool.join() if __name__ == '__main__': main()
CentOSIP
和Port
(192.168.36.123:5400
)
演示:(多PC演示)
简单说下本机IP的绑定:
Net里面习惯使用localhost
,不少人不知道究竟是啥,其实你打开host
文件就能够看到 ==> 127.0.0.1
被重定向为localhost
,在Linux里面也是这样的,每一个PC对应的都是lo
回环地址:
本机通讯时,对方ip就可使用127.0.0.1
了,固然了绑定本机ip的时候也可使用127.0.0.1
(bind(('',))
中的空其实填的就是这个)(不少地方也会使用0.0.0.0
)
_LOCALHOST = '127.0.0.1' # 看这 _LOCALHOST_V6 = '::1' def socketpair(family=AF_INET, type=SOCK_STREAM, proto=0): if family == AF_INET: host = _LOCALHOST # 看这 elif family == AF_INET6: host = _LOCALHOST_V6 .... lsock = socket(family, type, proto) try: lsock.bind((host, 0)) # 看这 lsock.listen() ...
快速实现一下:
using System.Net; using System.Text; using System.Net.Sockets; namespace netcore { class Program { static void Main(string[] args) { // UDP通讯 using (var udp_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) { var ip_addr = IPAddress.Parse("192.168.36.235"); // 绑定本地端口 udp_socket.Bind(new IPEndPoint(ip_addr, 5400)); // UDP发送消息 int i = udp_socket.SendTo(Encoding.UTF8.GetBytes("小明你好啊~"), new IPEndPoint(ip_addr, 8080)); Console.WriteLine($"发送计数:{i}"); } Console.WriteLine("over"); } } }
示例代码:https://github.com/lotapp/BaseCode/tree/master/python/6.net/2.TCP
TCP
是一种面向链接的、可靠的协议,TCP
传输的双方须要首先创建链接,以后由TCP
协议保证数据收发的可靠性,丢失的数据包自动重发,上层应用程序收到的老是可靠的数据流,通信以后关闭链接(有点像打电话)
用过下载软件的可能遇到过一种‘Bug’
==> 不少人为了防止本身本地文件归入共享大军,通常都是直接把网络上传给禁了,而后发现文件常常出问题?
其实这个就是TCP
的一个应用,文件通常都很大,因此进行分割后批量下载,那少许的网络上传实际上是为了校验一下文件 ==> 正确作法是限制上传速度而不是禁止(学生时代那会还常常蛋疼这个问题,如今想一想还挺好玩的O(∩_∩)O
)
大多数链接都是可靠的TCP链接。建立TCP链接时,主动发起链接的叫客户端,被动响应链接的叫服务器
上面那个例子里,咱们的下载工具就是客户端,每一小段文件接收完毕后都会向服务器发送一个完成的指令来保证文件的完整性
来看一个简单的入门案例:
from socket import socket def main(): # 默认就是建立TCP Socket with socket() as tcp_socket: # 链接服务器(没有返回值) tcp_socket.connect(("192.168.36.235", 8080)) # 发送消息(返回发送的字节数) tcp_socket.send("小张生日快乐~".encode("utf-8")) # 接收消息 msg = tcp_socket.recv(1024) print(f"服务器:{msg.decode('utf-8')}") if __name__ == '__main__': main()
输出:(socket()
默认就是建立TCP Socket
)
归纳来讲:
connect
)才能通讯(send
,recv
),以后的通讯不用再拨号连通了ip
+port
)代码四步走:(TCP客户端其实建立Socket
以后connect
一下服务器就OK了)
tcp_sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_sock.connect((IP, Port))
tcp_sock.send(Bytes内容)
接收:tcp_sock.recv(count)
tcp_sock.close()
from socket import socket def get_buffer(tcp_socket): buffers = b'' while True: b = tcp_socket.recv(1024) if b: buffers += b else: break # 返回bytes return buffers def main(): with socket() as tcp_socket: # 链接服务器 tcp_socket.connect(("dotnetcrazy.cnblogs.com", 80)) # 发送消息(模拟HTTP) tcp_socket.send( b'GET / HTTP/1.1\r\nHost: dotnetcrazy.cnblogs.com\r\nConnection: close\r\n\r\n' ) # 以"\r\n\r\n"分割一次 header, data = get_buffer(tcp_socket).split(b"\r\n\r\n", 1) print(header.decode("utf-8")) with open("test.html", "wb") as f: f.write(data) print("over") if __name__ == '__main__': main()
输出:(test.html
就是页面源码)
HTTP/1.1 200 OK Date: Thu, 01 Nov 2018 03:10:48 GMT Content-Type: text/html; charset=utf-8 Content-Length: 20059 Connection: close Vary: Accept-Encoding Cache-Control: private, max-age=10 Expires: Thu, 01 Nov 2018 03:10:58 GMT Last-Modified: Thu, 01 Nov 2018 03:10:48 GMT X-UA-Compatible: IE=10 X-Frame-Options: SAMEORIGIN over
注意\r\n
和Connection:close
;split("",分割次数)
服务端代码相比于UDP,多了一个监听和等待客户端,其余基本上同样:
客户端Code:(若是你想固定端口也能够绑定一下Port
)
from socket import socket def main(): # 默认就是建立TCP Socket with socket() as tcp_socket: # 链接服务器(没有返回值) tcp_socket.connect(("192.168.36.235", 8080)) print("Connected TCP Server...") # 链接提示 # 发送消息(返回发送的字节数) tcp_socket.send("小张生日快乐~\n".encode("utf-8")) # 接收消息 msg = tcp_socket.recv(1024) print(f"服务器:{msg.decode('utf-8')}") if __name__ == '__main__': main()
服务端Code:
from socket import socket def main(): with socket() as tcp_socket: # 绑定端口(便于客户端找到) tcp_socket.bind(('', 8080)) # 变成被动接收消息(监听) tcp_socket.listen() # 不指定链接最大数则会设置默认值 print("TCP Server is Running...") # 运行后提示 # 等待客户端发信息 client_socket, client_addr = tcp_socket.accept() with client_socket: # 客户端链接提示 print(f"[来自{client_addr[0]}:{client_addr[1]}的消息]\n") # 接收客户端消息 data = client_socket.recv(1024) print(data.decode("utf-8")) # 回复客户端 client_socket.send("知道了".encode("utf-8")) if __name__ == '__main__': main()
输出:(先运行服务端,再运行客户端。客户端发了一个生日快乐的祝福,服务端回复了一句)
若是像上面那般,并不能多客户端通讯
这时候能够稍微改造一下:
from time import sleep from socket import socket from multiprocessing.dummy import Pool def send_msg(tcp_socket): with tcp_socket: while True: try: tcp_socket.send("小明同志\n".encode("utf-8")) sleep(2) # send是非阻塞的 print("向服务器问候了一下") except Exception as ex: print("服务端链接已断开:", ex) break def recv_msg(tcp_socket): with tcp_socket: while True: # 这边能够不捕获异常: # 服务端关闭时,send_msg会关闭,而后这边也就关闭了 try: data = tcp_socket.recv(1024) if data: print("服务端回复:", data.decode("utf-8")) except Exception as ex: print("tcp_socket已断开:", ex) break def main(): with socket() as tcp_socket: # 链接TCP Server tcp_socket.connect(("192.168.36.235", 8080)) print("Connected TCP Server...") # 链接提示 pool = Pool() pool.apply_async(send_msg, args=(tcp_socket,)) pool.apply_async(recv_msg, args=(tcp_socket,)) pool.close() pool.join() if __name__ == '__main__': main()
服务器须要同时响应多个客户端的请求,那么每一个链接都须要一个新的进程或者线程来处理
from socket import socket from multiprocessing.dummy import Pool def wait_client(client_socket, ip_port): with client_socket: while True: data = client_socket.recv(1024) print(f"[来自{ip_port}的消息]:\n{data.decode('utf-8')}") client_socket.send(b"I Know") # bytes类型 def main(): with socket() as tcp_socket: # 绑定端口 tcp_socket.bind(('', 8080)) # 服务器监听 tcp_socket.listen() print("TCP Server is Running...") # 运行后提示 p = Pool() while True: # 等待客户端链接 client_socket, client_addr = tcp_socket.accept() ip_port = f"{client_addr[0]}:{client_addr[1]}" print(f"客户端{ip_port}已链接") # 响应多个客户端则须要多个线程来处理 p.apply_async(wait_client, args=(client_socket, ip_port)) if __name__ == '__main__': main()
演示:(死循环,Pool
都不用管了)
服务器挂了客户端也会自动退出:
用TCP协议进行Socket
编程在Python中十分简单:
大致流程和Python同样:
using System; using System.Text; using System.Net; using System.Net.Sockets; using System.Threading.Tasks; namespace _2_TCP { class Program { static void Main(string[] args) { using (var tcp_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) { var ip_addr = IPAddress.Parse("192.168.36.235"); // 服务器端绑定Port tcp_socket.Bind(new IPEndPoint(ip_addr, 8080)); // 服务器监听 tcp_socket.Listen(5); while (true) { // 等待客户端链接 var client_socket = tcp_socket.Accept(); // 远程端口 var client_point = client_socket.RemoteEndPoint; Task.Run(() => { while (true) { byte[] buffer = new byte[1024]; int count = client_socket.Receive(buffer); Console.WriteLine($"来自{client_socket.RemoteEndPoint.ToString()}的消息:\n{Encoding.UTF8.GetString(buffer, 0, count)}"); client_socket.Send(Encoding.UTF8.GetBytes("知道了~")); } }); } } } } }
using System; using System.Text; using System.Net; using System.Net.Sockets; namespace client { class Program { static void Main(string[] args) { using (var tcp_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) { // 链接服务器 tcp_socket.Connect(new IPEndPoint(IPAddress.Parse("192.168.36.235"), 8080)); while (true) { // 发送消息 tcp_socket.Send(Encoding.UTF8.GetBytes("服务器你好")); // 接收服务器消息 byte[] buffer = new byte[1024]; int count = tcp_socket.Receive(buffer); Console.WriteLine($"来自服务器的消息:{Encoding.UTF8.GetString(buffer, 0, count)}"); } } } } }
图示:
示例代码:https://github.com/lotapp/BaseCode/tree/master/python/6.net/3.Ext
上面忘记说了,Socket
是能够设置超时时间的,eg:tcp_socket.settimeout(3)
localhost
代码不变,若是把TCP客户端的链接服务器IP
空着或者改为127.0.0.1
,我们再看看效果:tcp_socket.connect(('', 8080))
图示:(怎么样,这回知道本机问啥能够不写IP了吧)
端口扫描你们不陌生,本身实现一个简单的TCP端口扫描工具:
from socket import socket from multiprocessing.dummy import Pool ip = "127.0.0.1" def tcp_port(port): """IP:服务端IP,Port:服务端Port""" with socket() as tcp_socket: try: tcp_socket.connect((ip, port)) print(f"[TCP Port:{port} is open]") except Exception: pass def main(): # 查看系统本地可用端口极限值 cat /proc/sys/net/ipv4/ip_local_port_range max_port = 60999 global ip ip = input("请输入要扫描的IP地址:") print(f"正在对IP:{ip}进行端口扫描...") pool = Pool() pool.map_async(tcp_port, range(max_port)) pool.close() pool.join() if __name__ == '__main__': main()
输出:(你把端口换成经常使用端口列表就知道服务器开了哪些服务了)
dnt@MZY-PC:~/桌面/work/BaseCode/python/6.net/3.Ext python3 1.port_scan.py 请输入要扫描的IP地址:192.168.36.235 正在对IP:192.168.36.235进行端口扫描... [TCP Port:22 is open] [TCP Port:41004 is open] dnt@MZY-PC:~/桌面/work/BaseCode/python/6.net/3.Ext sudo nmap -sT 192.168.36.235 -Pn -p- Starting Nmap 7.60 ( https://nmap.org ) at 2018-11-02 18:15 CST Nmap scan report for MZY-PC (192.168.36.235) Host is up (0.000086s latency). Not shown: 65534 closed ports PORT STATE SERVICE 22/tcp open ssh Nmap done: 1 IP address (1 host up) scanned in 2.07 seconds
能够自行研究拓展:
send
、sendto
)和接收(recv
、recvfrom
)都是两个方法?(提示:方法名
、阻塞
)send
和sendall
有啥区别?内网映射
或者ShellCode
实现一个远控课外拓展:
官方Socket编程文档【推荐】 https://docs.python.org/3/library/socket.html Python核心编程之~网络编程【推荐】 https://wizardforcel.gitbooks.io/core-python-2e/content/19.html TCP编程知识 https://dwz.cn/dDkXzqcV 网络编程-基础 https://www.jianshu.com/p/55c171ebe5f1 网络编程-UDP https://www.jianshu.com/p/594870b1634b 网络编程-TCP https://www.jianshu.com/p/be36d4db5618 Python总结之 recv与recv_from https://www.jianshu.com/p/5643e810123f https://blog.csdn.net/xvd217/article/details/38902081 https://blog.csdn.net/pengluer/article/details/8812333 端口扫描扩展:(Python2) https://thief.one/2018/05/17/1 Python socket借助ngrok创建外网TCP链接 https://www.jianshu.com/p/913b2013a38f TCP协议知识: https://www.cnblogs.com/wcd144140/category/1313090.html