14.3.5 互斥锁
1)线程安全问题
线程之间共享数据会存在线程安全的问题。
比如下面这段代码,3个线程,每个线程都将g_num +1 十次:
import time
import threading
def func():
global g_num
for _ in range(10):
tmp = g_num + 1
# time.sleep(0.01)
g_num = tmp
print(f"{threading.current_thread().name}: {g_num}\n", end=“”)
if name == “main”:
g_num = 0
threads = [threading.Thread(target=func, name=f"线程{i}") for i in range(3)]
[t.start() for t in threads]
[t.join() for t in threads]
print(g_num) # 30
结果为30,看似没有问题,这是因为这个修改操作花费的时间太短了,短到我们无法想象。所以,线程间轮询执行时,都能获取到最新的 g_num 值。因此暴露问题的概率就变得微乎其微。
我们添加0.01秒的延迟时间:
import time
import threading
def func():
global g_num
for _ in range(10):
tmp = g_num + 1
time.sleep(0.01)
g_num = tmp
print(f"{threading.current_thread().name}: {g_num}\n", end=“”)
if name == “main”:
g_num = 0
threads = [threading.Thread(target=func, name=f"线程{i}") for i in range(3)]
[t.start() for t in threads]
[t.join() for t in threads]
print(g_num) # 10
可以看到最终结果并不是30。这是因为在修改 g_num 前,有0.01秒的休眠时间,某个线程延时后,CPU立即分配计算资源给其他线程。此时0.01秒的休眠还未结束,这个线程还未将修改后的数据赋值给 g_num,因此其他线程获取到的并不是最新值,所以才出现上面的结果。
2)互斥锁的概念
某个线程要更改共享数据时,先将其锁定,此时其他线程不能更改。直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
3)互斥锁的使用
可以通过 threading.Lock() 创建互斥锁。
使用 lock.acquire([blocking=True][, timeout=-1]) 来获取锁(blocking 如果为 True,线程会阻塞直到获取到锁。如果为 False,线程立即返回。获取锁成功返回 True,否则返回 False。timeout 为等待的超时时间,单位为秒。如果超时仍未获取到锁,则返回 False。)。
使用 lock.release() 释放锁。
案例:
import time
import threading
def func():
global g_num
for _ in range(10):
lock.acquire() # 获取锁
tmp = g_num + 1
time.sleep(0.01)
g_num = tmp
lock.release() # 释放锁
print(f"{threading.current_thread().name}: {g_num}\n", end=“”)
if name == “main”:
g_num = 0
lock = threading.Lock() # 创建锁
threads = [threading.Thread(target=func, name=f"线程{i}") for i in range(3)]
[t.start() for t in threads]
[t.join() for t in threads]
print(g_num) # 30
14.3.6 GIL
Python 全局解释器锁(Global Interpreter Lock, 简称 GIL)是一个锁,同一时间只允许一个线程保持 Python 解释器的控制权,这意味着在任何时间点都只能有一个线程处于执行状态。执行单线程程序时看不到 GIL 的影响,但它可能是 CPU 密集型和多线程代码中的性能瓶颈。GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。
Python于1991年诞生,从操作系统没有线程概念的时代就已经存在了。由于物理上的限制,各CPU厂商在核心频率上的比赛已经被多核所取代。为了利用多核,Python开始支持多线程。而为了解决多线程之间数据完整性和状态同步,于是有了GIL,GIL 提供了线程安全的内存管理。
GIL 的存在会对多线程的效率有不小影响。甚至就几乎等于Python是个单线程的程序。我们可能会想 GIL只要释放的勤快效率也不会差,至少也不会比单线程的效率差。理论上是这样。
但实际上,Python为了让各个线程能够平均利用CPU时间,会计算当前已执行的微代码数量,达到一定阈值后就强制释放GIL。而这时也会触发一次操作系统的线程调度(当然是否真正进行上下文切换由操作系统自主决定)。从释放 GIL 到获取 GIL 之间几乎是没有间隙的。所以当其他在其他核心上的线程被唤醒时,大部分情况下主线程已经又再一次获取到 GIL 了。这个时候被唤醒执行的线程只能白白的浪费CPU时间,看着另一个线程拿着 GIL 执行。然后达到切换时间后进入待调度状态,再被唤醒,再等待,以此往复恶性循环。
上述实现方式是较为原始的,Python的每个版本中也在逐渐改进GIL和线程调度之间的互动关系。例如先尝试持有GIL在做线程上下文切换,在IO等待时释放GIL等尝试。但是无法改变的是GIL的存在使得操作系统线程调度的这个本来就昂贵的操作变得更奢侈了。
总之,当你的程序需要进行大量的CPU计算时,GIL会成为性能的瓶颈。即使你有多个线程,GIL也会阻止它们在多个CPU核心上并行执行。实际上,多个线程会轮流获取GIL,这样就不能真正并行地使用多个处理器核心。而对于涉及I/O操作(如文件读写、网络请求等)的程序,GIL的影响较小。因为在I/O操作时,线程会释放GIL,其他线程可以在此时执行,这使得多线程在I/O密集型任务中能更有效地并发。
第 15 章 网络编程
15.1 网络
使用网络能够把多方电脑等设备链接在一起进行数据传递。网络编程就是让在不同的电脑上的软件能够进行数据传递,即进程之间的通信。
15.1.1 网络编程三要素
IP:网络中每台计算机的唯一标识,通过IP地址可以找到计算机。
端口:标识进程的逻辑地址,通过端口找到计算机中指定的进程(应用软件)。
协议:定义通信规则。
15.1.2 TCP/IP协议族
1)通信协议
通信协议是一组用于规定不同设备或计算机之间如何进行数据交换和通信的规则和约定。它定义了通信的各个方面,包括数据的格式、传输的顺序、错误检查机制、如何处理不同情况(如重传丢失的数据包)等。协议的目的是确保在网络中传输的数据能够被正确、可靠地理解和处理。
通信协议可以应用于计算机网络、电话网络、无线通信等领域。在不同的应用场景下,会使用不同的协议来实现数据交换、控制信息传递等任务。
2)TCP/IP
TCP/IP 协议族,简称TCP/IP,是一组通信协议,用于互联网的数据传输和网络通信,定义了数据如何在不同的计算机之间传输和路由。是现代计算机网络中最常用的网络协议之一。TCP/IP得名于该协议家族的两个核心协议:TCP(传输控制协议)和IP(网际协议)。
3)分层网络模型
OSI 七层网络模型由国际标准化组织制定,但其实现过于复杂,且制定周期过长,在其整套标准推出之前,TCP/IP 模型已经在全球范围内被广泛使用。TCP/IP 模型定义了应用层、传输层、网络层、网络接口层这四层网络结构,但并没有给出网络接口层的具体内容,因此在学习和开发中,通常将网络接口层替换为 OSI 七层模型中的数据链路层和物理层来进行理解,这就是五层网络模型。
4)常见网络协议
15.2 IP
15.2.1 什么是IP
IP地址由一串数字组成,用来标识一台电脑在网络中的位置。当设备连接网络,设备将被分配一个IP地址,用作标识。通过IP地址设备间可以互相通讯。IP地址有两个主要功能:标识设备或网络,以及寻址。
Windows下可以在命令提示符中使用ipconfig查看网络适配器的IP。
Linux下可以在终端中使用ifconfig或ip addr查看IP。
15.2.2 子网掩码
IP网络可以在IPv4和IPv6中划分子网。为此将IP地址识别成由两部分组成:网络前缀和主机编号。子网掩码(subnet mask)或无类别域间路由(CIDR)表示法确定了IP地址如何分为网络部分和主机部分。
子网掩码一词仅用于IPv4地址中。但是 IPv4和IPv6都使用CIDR概念和符号。在此,在IP地址后面加斜杠和用于标识网络部分的位数(十进制)。例如:IPv4地址及其子网掩码分别可以是 192.168.10.2 和 255.255.255.0 。因为IP地址的前24位表示网络和子网,所以相同的IP地址和子网的CIDR表示法为192.168.10.2/24。
主机编号全为0,表示网络号,主机编号全为1,表示网络广播。
15.2.3 IPv4地址的分类
15.2.4 公网与私网
公网IP在任何地方都可以访问。而私网IP只能在局域网内访问。
国际规定有一部分IP地址是用于局域网使用,也就是属于私网IP,不在公网中使用的,它们的范围是:
10.0.0.0~10.255.255.255
172.16.0.0~172.31.255.255
192.168.0.0~192.168.255.255
其中127.0.0.1~127.255.255.255用于回路测试,如127.0.0.1可以代表本机IP地址。
网络地址转换(NAT)是一种在IP数据包通过路由器或防火墙时重写来源或目的IP地址或端口的技术。这种技术普遍应用于有多台主机,但只通过一个公有IP地址访问互联网的私有网络中。1990年代中期,NAT是作为一种解决IPv4地址短缺以避免保留IP地址困难的方案而流行起来的,并成了家庭和小型办公室网络连接上的路由器的一个标准特征,因为对他们来说,申请独立的IP地址的代价要高于所带来的效益。
15.2.5 IPv4与IPv6
常见的IP地址分为IPv4与IPv6两大类。IPv4为32位长,通常书写时以四组十进制数字组成,并以点分隔,如:172.16.254.1。IPv6为128位长,通常书写时以八组十六进制数字组成,以冒号分割,如:2001:db8:0🔢0:567:8:1。
随着互联网的快速成长,IPv4的42亿个地址最终于2011年2月3日用尽。相应的科研组织已研究出128位的IPv6,其IP地址数量最高可达3.402823669×1038个,届时每个人家居中的每件电器,每件对象,甚至地球上每一粒沙子都可以拥有自己的IP地址。
15.3 端口
15.3.1 什么是端口
这里的端口指的是逻辑端口,即TCP/IP协议中的端口。端口用于进程(应用软件)在同一设备或不同设备之间通信。每个端口有一个对应的端口号。端口号有65536个。
可以使用netstat -ano查看端口信息。
15.3.2 端口号的分配
1)公认端口
01023,它们紧密绑定于一些服务。通常这些端口的通讯明确表明了某种服务的协议。端口号0是被保留的,不可使用。11023系统保留,只能由root用户使用。
2)动态端口
1024~65536,之所以称为动态端口,是因为它一般不固定分配某种服务,而是动态分配。当一个系统进程或应用程序进程需要网络通信时,它向主机申请一个端口,主机从可用的端口号中分配一个供它使用。当这个进程关闭时,同时也就释放了所占用的端口号。
3)常见端口
端口 服务
0/TCP,UDP 保留端口,不使用
7/TCP,UDP Echo(回显)协议
21/TCP,UDP FTP文件传输协议
22/TCP,UDP SSH安全远程登录协议
23/TCP,UDP Telnet终端仿真协议
25/TCP,UDP SMTP简单邮件传输协议
53/TCP,UDP DNS域名服务系统
80/TCP,UDP HTTP超文本传输协议
110/TCP POP3邮局协议第3版
137/TCP,UDP NetBIOS名称服务
138/TCP,UDP NetBIOS数据报文服务
139/TCP,UDP NetBIOS会话服务
143/TCP,UDP IMAP用于检索电子邮件
445/TCP Microsoft-DS (Active Directory、Windows 共享、震荡波蠕虫、Agobot、Zobotworm)
445/UDP Microsoft-DS服务器消息块(SMB)文件共享
666/UDP 毁灭战士,电脑平台上的一系列第一人称射击游戏。
873/TCP Rsync文件同步协议
902 VMware服务器控制台
3306/TCP,UDP MySQL数据库系统
3389/TCP 远程桌面协议(RDP)
15.4 socket套接字
15.4.1 什么是socket
socket(套接字)是同一或不同电脑的进程(任务、应用软件)间通信的一个工具,进程之间想要进行网络通信需要基于socket。只要与网络相关的应用程序或者软件都使用到了socket。
15.4.2 socket的使用
Python中提供了socket模块用于创建套接字。
import socket
AF_INET 用于 Internet 进程间通信;SOCK_STREAM 流式套接字,TCP
tcp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
AF_INET 用于 Internet 进程间通信;SOCK_DGRAM 数据报套接字,UDP
udp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
15.5 UDP
15.5.1 什么是UDP
用户数据报协议(UDP:User Datagram Protocol)是一个简单的面向数据报的通信协议。UDP只提供数据的不可靠传递,它一旦把应用程序发给网络层的数据发送出去,就不保留数据备份。
UDP避免了协议栈中执行错误检查和纠正处理的开销,适用于对时间有较高要求的应用程序,因为某些场景下丢弃数据包比等待或重传导致延迟更可取。流媒体、在线游戏流量通常使用UDP传输。
15.5.2 UDP编程
1)UDP编程流程
2)案例
UDP服务端:
“”“udp服务端”“”
import socket
创建udp套接字
udp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
绑定ip和端口
udp_socket.bind((“127.0.0.1”, 8080))
while True:
# 接收数据
recv_data, client_addr = udp_socket.recvfrom(1024)
client_ip = client_addr[0]
client_port = client_addr[1]
print(f"{client_ip}:{client_port}>> {recv_data.decode(“utf-8”)}")
# 发送数据
udp_socket.sendto(“你好”.encode(“utf-8”), client_addr)
关闭套接字
udp_socket.close()
UDP客户端:
“”“udp客户端”“”
import socket
创建udp套接字
udp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
while True:
try:
# 发送数据
server_ip = “127.0.0.1”
server_port = 8080
udp_socket.sendto(input(f"{server_ip}:{server_port}<< “).encode(“utf-8”), (server_ip, server_port))
# 接收数据
recv_data, client_addr = udp_socket.recvfrom(1024)
client_ip = client_addr[0]
client_port = client_addr[1]
print(f”{client_ip}:{client_port}>> {recv_data.decode(“utf-8”)}")
except KeyboardInterrupt:
break
关闭套接字
udp_socket.close()
15.6 TCP
15.6.1 什么是TCP
传输控制协议(TCP:Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP协议的运行可划分为三个阶段:连接建立、数据传送和连接终止。
很多重要的机制保证了TCP的可靠性和强壮性,包括:
使用序号,对收到的TCP报文段进行排序以及检测重复的数据。
使用校验和检测报文段的错误,即无错传输。
使用确认和计时器来检测和纠正丢包或延时。
流控制。
拥塞控制。
丢失包的重传。
15.6.2 TCP编程
1)TCP编程流程
2)案例
TCP服务端:
“”“tcp服务端”“”
import socket
创建tcp套接字
tcp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
绑定ip和端口
tcp_socket.bind((“127.0.0.1”, 8080))
设置监听
tcp_socket.listen(2)
等待客户端连接
client_socket, client_addr = tcp_socket.accept()
while True:
# 接收数据
recv_data = client_socket.recv(1024)
print(f"{client_addr[0]}:{client_addr[1]}>> {recv_data.decode(‘utf-8’)}")
# 发送数据
client_socket.send(“你好”.encode(“utf-8”))
关闭套接字
tcp_socket.close()
TCP客户端:
“”“tcp客户端”“”
import socket
创建tcp套接字
tcp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
连接服务器
server_ip = “127.0.0.1”
server_port = 8080
tcp_socket.connect((server_ip, server_port))
while True:
try:
# 发送数据
tcp_socket.send(input(f"{server_ip}:{server_port}<< “).encode(“utf-8”))
# 接收数据
recv_data = tcp_socket.recv(1024)
print(f”{server_ip}:{server_port}>> {recv_data.decode(“utf-8”)}")
except KeyboardInterrupt:
break
关闭套接字
tcp_socket.close()
15.7 HTTP
15.7.1 什么是HTTP
HTTP(超文本传输协议)是一种用于分布式、协作式和超媒体信息系统的应用层协议。是万维网的数据通信的基础。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。通过HTTP或者HTTPS协议请求的资源由统一资源标识符(Uniform Resource Identifiers,URI)来标识。
HTTP 上的一个典型工作流程是客户端计算机向服务器发出请求,然后服务器发送响应消息。通常,由HTTP客户端发起一个请求,建立一个到服务器指定端口(默认是80端口)的TCP连接。HTTP服务器则在那个端口监听客户端的请求。一旦收到请求,服务器会向客户端返回一个状态,比如“HTTP/1.1 200 OK”,以及返回的内容,如请求的文件、错误消息、或者其它信息。
15.7.2 HTTP消息结构
1)客户端请求消息
客户端发送一个 HTTP 请求到服务器的请求消息包括以下格式:请求行、请求头、空行和请求体四个部分组成。
(1)请求行
请求方法:如 GET、POST、PUT、DELETE等,指定要执行的操作。
请求 URI:请求的资源路径,通常包括主机名、端口号(如果非默认)、路径和查询字符串。
协议版本:如 HTTP/1.1 或 HTTP/2。
请求行的格式示例:GET /index.html HTTP/1.1
(2)请求头
包含了客户端环境信息、请求体的大小(如果有)、客户端支持的压缩类型等。
常见的请求头包括Host、User-Agent、Accept、Accept-Encoding、Content-Length等。
(3)空行
请求头和请求体之间的分隔符,表示请求头的结束。
(4)请求体
在某些类型的HTTP请求(如 POST 和 PUT)中,请求体包含要发送给服务器的数据。
2)服务端响应消息
HTTP 响应由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
(1)状态行
HTTP 版本:与请求消息中的版本相匹配。
状态码:三位数,表示请求的处理结果,如 200 表示成功,404 表示未找到资源。
状态信息:状态码的简短描述。
状态行的格式示例:HTTP/1.1 200 OK
(2)响应头
包含了服务器环境信息、响应体的大小、服务器支持的压缩类型等。
常见的响应头包括Content-Type、Content-Length、Server、Set-Cookie等。
(3)空行
响应头和响应体之间的分隔符,表示响应头的结束。
(4)响应体
包含服务器返回的数据,如请求的网页内容、图片、JSON数据等。
15.7.3 HTTP请求方法
HTTP/1.1 协议中共定义了八种方法来以不同方式操作指定的资源,HTTP 服务器至少应该实现 GET 和 HEAD 方法,其他方法都是可选的。
1)GET
向指定的资源发出“显示”请求。使用 GET 方法应该只用在读取资料,而不应当被用于产生“副作用”的操作中,例如在网络应用程序中。其中一个原因是 GET 可能会被网络爬虫等随意访问。
2)HEAD
与 GET 方法一样,都是向服务器发出指定资源的请求。只不过服务器将不传回资源的本文部分。它的好处在于,使用这个方法可以在不必传输全部内容的情况下,就可以获取其中“关于该资源的元信息(或称元数据)”。
3)POST
向指定资源提交数据,请求服务器进行处理(例如提交表单或者上传文件)。数据被包含在请求本文中。这个请求可能会建立新的资源或修改现有资源,或二者皆有。每次提交,表单的数据被浏览器用编码到 HTTP 请求的 body 里。
4)PUT
向指定资源位置上传其最新内容。
5)DELETE
请求服务器删除 Request-URI 所标识的资源。
6)TRACE
回显服务器收到的请求,主要用于测试或诊断。
7)OPTIONS
这个方法可使服务器传回该资源所支持的所有 HTTP 请求方法。用“*”来代替资源名称,向 Web 服务器发送 OPTIONS 请求,可以测试服务器功能是否正常运作。
8)CONNECT
HTTP/1.1协议中预留给能够将连接改为隧道方式的代理服务器。通常用于SSL加密服务器的链接(经由非加密的HTTP代理服务器)。
15.7.4 HTTP状态码
HTTP状态码是服务器对客户端请求的响应,状态码分为五类:
1)1xx(信息状态码)
表示接收的请求正在处理。例如:
100:继续。客户端应继续其请求。
101:切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议。
2)2xx(成功状态码)
表示请求正常处理完毕。例如:
200:请求成功。一般用于 GET 与 POST 请求。
202:已接受。已经接受请求,但未处理完成。
3)3xx(重定向状态码)
需要后续操作才能完成这一请求。例如:
300:多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择。
301:永久移动。请求的资源已被永久的移动到新 URI,返回信息会包括新的 URI,浏览器会自动定向到新 URI。今后任何新的请求都应使用新的 URI代替。
302:临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有 URI。
304:未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源。
305:使用代理。所请求的资源必须通过代理访问。
4)4xx(客户端错误状态码)
表示请求包含语法错误或无法完成。例如:
400:客户端请求的语法错误,服务器无法理解。
403:服务器理解请求客户端的请求,但是拒绝执行此请求。
404:服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置“您所请求的资源无法找到”的个性页面。
405:客户端请求中的方法被禁止。
5)5xx(服务器错误状态码)
服务器在处理请求的过程中发生了错误。例如:
500:服务器内部错误,无法完成请求。
501:服务器不支持请求的功能,无法完成请求。
502:作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应。
15.8 案例:发送HTTP请求以及获取响应数据
import requests
一言网的 API 地址
url = ‘https://v1.hitokoto.cn/’
请求参数,指定返回中文内容,这里使用默认的所有类型
params = {
‘c’: ‘a’, # 可以根据需要修改类型,a 代表动画,b 代表漫画等
‘encode’: ‘json’
}
try:
print(f"正在发送 GET 请求到: {url},参数: {params}“)
response = requests.get(url, params=params)
status_code = response.status_code
if status_code == 200:
print(f"请求成功!状态码: {status_code}”)
data = response.json()
hitokoto = data[‘hitokoto’]
from_who = data[‘from_who’] if data[‘from_who’] else ‘未知’
print(f"随机名言: {hitokoto} - {from_who}“)
elif status_code == 404:
print(f"请求的资源未找到!状态码: {status_code}”)
elif status_code == 500:
print(f"服务器内部错误!状态码: {status_code}“)
else:
print(f"发生未知错误,状态码: {status_code}”)
except requests.RequestException as e:
print(f"请求过程中出现错误: {e}")
15.9 案例:通过Starlette 构建web接口
Starlette 是一个轻量级的 Python 异步 Web 框架,专为构建高性能的异步应用程序而设计,它具有简洁、灵活的特点,并且可以与其他库(如 FastAPI 就是基于 Starlette 构建的)很好地集成。我们可以结合 Starlette 构建一个Web 服务,将上面获取随机名言的功能封装成一个 API 接口,这样可以带来一些优势,例如实现更灵活的交互、支持多用户访问。
Uvicorn:它是一个基于 Python 的 ASGI(Asynchronous Server Gateway Interface)服务器。ASGI 是 Python 中用于异步 Web 应用的标准接口,Uvicorn 能够高效地处理并发请求,基于 uvloop(一个快速的异步事件循环)和 httptools(一个快速的 HTTP 解析器)构建,为 Python 异步 Web 应用提供了高性能的运行环境。
Starlette:是一个轻量级的 Python 异步 Web 框架,它遵循 ASGI 标准,专注于提供简洁、灵活的 API 来构建 Web 应用和服务。Starlette 提供了路由、中间件、请求和响应处理等核心功能,允许开发者快速搭建 Web 应用的逻辑。
协作方式:Uvicorn 为 Starlette 应用提供了运行的基础环境。当你使用 Starlette 编写好一个 Web 应用后,无法直接运行,需要借助像 Uvicorn 这样的 ASGI 服务器来启动和部署
1)安装依赖包
pip install starlette uvicorn requests
2)代码实现
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
import requests
import uvicorn
一言网的 API 地址
HITOKOTO_URL = ‘https://v1.hitokoto.cn/’
定义异步函数来获取随机名言
async def get_hitokoto():
try:
# 请求参数,指定返回中文内容,这里使用默认的所有类型
params = {
‘c’: ‘a’, # 可以根据需要修改类型,a 代表动画,b 代表漫画等
‘encode’: ‘json’
}
response = requests.get(HITOKOTO_URL, params=params)
status_code = response.status_code
if status_code == 200:
data = response.json()
hitokoto = data[‘hitokoto’]
from_who = data[‘from_who’] if data[‘from_who’] else ‘未知’
return {‘hitokoto’: hitokoto, ‘from_who’: from_who}
else:
return {‘error’: f’请求一言网 API 失败,状态码: {status_code}‘}
except requests.RequestException as e:
return {‘error’: f’请求过程中出现错误: {str(e)}’}
定义处理根路径请求的异步函数
async def homepage(request):
result = await get_hitokoto()
return JSONResponse(result)
创建 Starlette 应用实例
app = Starlette(debug=True, routes=[
Route(‘/’, homepage),
])
if name == “main”:
# 使用 uvicorn 运行应用
uvicorn.run(app, host=‘0.0.0.0’, port=8000)
3)代码说明
get_hitokoto 函数
该函数负责发送 HTTP 请求到一言网的 API,获取随机名言。处理请求过程中可能出现的错误,包括请求失败和网络异常。返回一个包含名言和来源信息的字典,或者包含错误信息的字典。
homepage 函数
作为 Web 服务的根路径处理函数。调用 get_hitokoto 函数获取随机名言,并将结果封装成 JSON 响应返回给客户端。
Starlette 应用
创建 Starlette 应用实例,并定义路由规则,将根路径 / 映射到 homepage 处理函数。使用 uvicorn 作为ASGI服务器运行应用。
通过 Starlette 构建 Web 服务,将获取随机名言的功能封装成 API 接口,方便其他应用程序调用。虽然 requests 库是同步的,但 Starlette 本身支持异步处理