Tornado Web框架入门实战指南
Tornado是一个Python编写的开源web框架和异步网络库,最初由FriendFeed公司开发。它非常适合需要长时间运行的异步服务。本章将概述Tornado的特性,包括其灵活的请求处理和可扩展的结构。Tornado的核心优势在于其非阻塞网络I/O能力,允许在有限的线程内处理大量的并发连接。我们会讨论它在现代web应用中的关键作用,并探索其基本的设计理念和构建模块。这一章为后文更深入的讨论To
简介:《tornado-guide-examples:龙卷风指南入门示例》是一份面向Python初学者的Tornado Web框架教程。Tornado以其高并发性能和对长连接的优秀支持而知名,本教程通过实例解析深入浅出地介绍Tornado的基本用法和核心概念,包括异步非阻塞I/O模型、HTTP服务器、路由配置、异步处理、模板渲染、WebSocket通信、静态文件服务、会话管理以及错误处理等。教程旨在帮助用户快速掌握Tornado框架,以构建高性能、可扩展的Python Web应用。 
1. Tornado框架概述
Tornado是一个Python编写的开源web框架和异步网络库,最初由FriendFeed公司开发。它非常适合需要长时间运行的异步服务。本章将概述Tornado的特性,包括其灵活的请求处理和可扩展的结构。Tornado的核心优势在于其非阻塞网络I/O能力,允许在有限的线程内处理大量的并发连接。我们会讨论它在现代web应用中的关键作用,并探索其基本的设计理念和构建模块。这一章为后文更深入的讨论Tornado的异步编程模型、HTTP服务器用法、路由配置、模板渲染技术以及WebSocket通信等特性打下基础。
# 示例代码:Tornado应用的基础结构
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
上面的示例代码展示了如何快速构建一个Tornado web应用,这个应用能够响应根URL(’/’)的GET请求。这将引导我们逐步深入了解Tornado提供的各种工具和功能。
2. 异步非阻塞I/O模型实现
2.1 异步编程基础
异步编程是一种允许代码在等待长时间运行的任务(如I/O操作)完成时继续执行的编程范式。它是提高应用程序性能和资源利用率的关键技术之一,特别是在需要处理大量并发请求的网络服务器上。
2.1.1 阻塞与非阻塞IO的概念
在阻塞IO模型中,当一个线程执行一个IO操作时,线程会阻塞,直到操作完成。这会导致线程的资源利用率低下,特别是在I/O密集型应用中。
非阻塞IO模型,与之相反,当执行I/O操作时,线程不会阻塞,而是继续执行其他任务。操作完成后,再通过回调或轮询的方式获取结果。Tornado框架使用的正是非阻塞IO模型,它允许服务器在等待请求处理时继续处理其他客户端的请求。
2.1.2 回调函数的角色和作用
回调函数是非阻塞编程中的关键概念之一。在非阻塞操作发起后,回调函数被指定为当操作完成时所调用的函数。它们使得线程能够在不等待阻塞操作完成的情况下继续执行其他工作。
在异步编程中,回调函数通常用于处理异步I/O操作的完成事件。在Tornado中,可以通过设置回调来处理HTTP请求、数据库操作等异步操作。
2.2 Tornado的协程支持
Tornado框架通过协程提供了对异步编程的强大支持。协程是一种轻量级的线程,它们的调度由程序控制,而不是操作系统的调度器。
2.2.1 协程的基本原理和优势
协程使得程序能够在不同的任务之间协作,而不需要上下文切换的开销。与传统的多线程模型相比,协程可以在单个线程中实现并发执行。这极大地减少了内存占用,并且提高了程序的运行效率。
2.2.2 Tornado协程的具体实现方法
Tornado通过 @gen.coroutine 装饰器实现了协程。开发者可以使用 yield 关键字来挂起协程,并等待异步操作完成。当异步操作完成时,协程继续执行。这种模型简化了异步代码的编写,使得代码的逻辑更加清晰。
import tornado.ioloop
import tornado.web
@gen.coroutine
def get_data():
# 这里执行一个异步数据库查询操作
response = yield tornado.gen.Task(db.query)
return response
class MainHandler(tornado.web.RequestHandler):
@gen.coroutine
def get(self):
data = yield get_data()
self.write("Response from database: %s" % data)
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
2.3 异步非阻塞I/O实战演练
异步非阻塞I/O模型的实战演练需要我们创建一个异步的HTTP客户端,并实现一个自定义的异步任务处理。
2.3.1 创建异步HTTP请求实例
使用Tornado的 AsyncHTTPClient 可以发起异步HTTP请求。它提供了多种实现,包括 httpclient.AsyncHTTPClient 和 httpclient.LocalAsyncHTTPClient 等。我们可以用以下方式发起一个异步的GET请求:
import tornado.ioloop
import tornado.web
import tornado.httpclient
async def fetch_url(url):
http_client = tornado.httpclient.AsyncHTTPClient()
response = await http_client.fetch(url)
return response.body
class MainHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
async def get(self):
try:
body = await fetch_url("http://example.com")
self.write(body)
except Exception as e:
self.write("Error fetching URL: %s" % e)
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
2.3.2 实现自定义的异步任务处理
我们可以将自定义的异步逻辑封装为一个函数,并在Tornado的RequestHandler中使用协程来调用它。这样就可以在处理HTTP请求的同时执行复杂的异步操作。
通过这一章的介绍,我们理解了阻塞与非阻塞IO的区别,以及Tornado框架中协程的使用方法。在接下来的章节中,我们将探讨HTTPServer的基本用法和如何在Tornado中配置路由。
3. HTTPServer基本用法
3.1 HTTPServer的创建和配置
3.1.1 设置监听端口和处理类
在Tornado框架中,创建HTTPServer的第一步是定义一个处理类,该类继承自 RequestHandler 。处理类中需要定义各种HTTP方法的处理函数,如 get() , post() , put() , delete() 等。然后,使用 http_server 模块中的 HTTPServer 类来创建服务器,并将处理类的实例作为参数传递给 HTTPServer 。
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
app = make_app()
http_server = tornado.httpserver.HTTPServer(app)
http_server.listen(8888)
tornado.ioloop.IOLoop.current().start()
在上述代码中,我们定义了一个 MainHandler 类来处理根URL的GET请求,并通过 HTTPServer 类的 listen 方法设置了监听的端口。我们使用 8888 端口来监听HTTP请求。
3.1.2 配置SSL/TLS支持
为了保证网络安全,我们可以通过配置SSL/TLS来启用HTTPS。这需要提供一个SSL证书和对应的私钥。在Tornado中,可以通过 http_server 模块的 HTTPSConnection 类来实现。
http_server = tornado.httpserver.HTTPServer(app)
http_server.listen(443, ssl_options={
"certfile": "path/to/certfile",
"keyfile": "path/to/keyfile"
})
在实际部署时, certfile 和 keyfile 参数应指向有效的SSL证书文件和私钥文件。这样配置后,HTTPServer将通过SSL/TLS加密通信,确保数据传输的安全性。
3.1.3 设置日志记录
在开发和维护Web应用时,日志记录是非常重要的。Tornado支持使用Python标准库中的 logging 模块来记录日志。可以通过 Application 类的 log_function 参数来设置日志记录。
import logging
def log_request(handler):
logging.info("%d %s %s", handler.get_status(),
handler.request.path, handler.request.remote_ip)
app = tornado.web.Application([
# ... (定义处理类)
], log_function=log_request)
在这个例子中,我们定义了一个简单的日志记录函数 log_request ,它会记录每个请求的状态码、路径以及客户端IP地址。然后,我们将这个函数传递给 Application 类的 log_function 参数,从而实现请求日志的记录。
3.2 请求和响应的处理
3.2.1 Request对象和常用属性
Tornado中的 RequestHandler 类处理请求时会自动创建一个 Request 对象,该对象封装了HTTP请求的信息,包括HTTP头部、GET/POST参数等。 Request 对象提供了许多有用的属性和方法来获取请求信息。
class MyHandler(tornado.web.RequestHandler):
def get(self):
# 获取GET参数
query = self.request.arguments("query")[0]
self.write("Your query was: " + query.decode())
def post(self):
# 获取POST参数
post_arg = self.get_argument("post_arg")
self.write("You posted: " + post_arg)
在 get 方法中,我们使用 request.arguments() 方法来获取名为 query 的GET参数。在 post 方法中,使用 get_argument() 方法来获取名为 post_arg 的POST参数。这两个方法都是处理请求数据的常用方法。
3.2.2 Response对象的设置和返回
RequestHandler 对象的 write 方法用于向客户端发送响应内容。除了返回简单的文本或HTML内容,还可以返回JSON、二进制文件等多种格式的数据。
import json
class MyHandler(tornado.web.RequestHandler):
def get(self):
data = {"message": "Hello, world"}
self.write(json.dumps(data))
在这个示例中,我们创建了一个包含消息的字典 data ,然后使用 json.dumps() 将其转换为JSON字符串,并通过 write 方法发送给客户端。
3.3 网络安全最佳实践
3.3.1 防止常见的网络攻击
为了提高Web应用的安全性,开发者需要采取多种措施来防范常见的网络攻击,比如XSS攻击(跨站脚本攻击)、CSRF攻击(跨站请求伪造)、SQL注入等。Tornado框架提供了相应的机制来帮助开发者增强安全性。
例如,为了防止XSS攻击,开发者可以使用Tornado提供的 xhtml_escape 方法对输出内容进行转义处理:
from tornado.escape import xhtml_escape
class MyHandler(tornado.web.RequestHandler):
def get(self):
user_input = self.get_argument("user_input")
escaped_input = xhtml_escape(user_input)
self.write("You entered: " + escaped_input)
在这个例子中,我们使用 xhtml_escape 对用户输入进行转义,从而降低了XSS攻击的风险。
3.3.2 使用中间件增强安全性
Tornado框架支持中间件的概念,中间件可以在请求处理之前或之后执行一些操作,例如验证用户身份、记录请求日志等。开发者可以通过定义中间件来实现安全检查,比如验证请求头中的令牌(token)是否有效。
class SecurityMiddleware(tornado.web.RequestHandler):
def prepare(self):
# 检查请求头中的令牌
token = self.request.headers.get("Token")
if not self.is_valid_token(token):
raise tornado.web.HTTPError(403) # 403 Forbidden
def is_valid_token(self, token):
# 这里应有验证令牌的逻辑
return True
在上面的示例中,我们定义了一个名为 SecurityMiddleware 的中间件类,其中的 prepare 方法会在请求处理之前执行。在这个方法中,我们检查了HTTP头中的 Token 字段,并使用 is_valid_token 方法来验证令牌的有效性。如果令牌无效,则通过抛出 HTTPError 异常来阻止请求的进一步处理。
通过中间件和各种内置方法,开发者可以大幅增强Tornado Web应用的安全性。在实际应用中,还需要考虑其他安全措施,比如使用HTTPS、设置合理的过期时间、限制上传文件类型、防御DDoS攻击等。
4. Application路由配置
4.1 路由系统的基本概念
路由系统在Web开发中扮演着至关重要的角色,它负责将用户请求的URL映射到后端对应的处理函数。在Tornado框架中,路由功能是由Application类负责管理的,通过定义路由规则,开发者可以轻松地将外部请求映射到正确的处理函数,从而实现请求与响应的逻辑。
URL路由的工作原理
URL路由的工作原理涉及到了“路由表”的概念,路由表中存储了URL模式与对应的处理函数(RequestHandler类的子类)之间的映射关系。当一个HTTP请求到达服务器时,Tornado会根据请求的URL在路由表中查找匹配的路由规则,并调用相应的处理函数来生成响应。
路由表的构建和管理
路由表的构建和管理是通过Tornado的Application构造函数中的 routes 参数完成的。这个参数是一个路由规则的列表,每个规则是一个包含 url 、 request_class 和可选的其他关键字参数的元组。例如:
from tornado.web import Application
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")
application = tornado.web.Application([
(r"/", MainHandler),
], debug=True)
在这个简单的例子中,我们定义了一个路由规则,将根路径(”/”)的请求映射到了 MainHandler 类。Tornado会自动将路径和类的实例化绑定,创建一个处理请求的实例。
路由表支持动态路径,可以通过正则表达式来匹配更复杂的URL模式。例如,如果想为每个用户的个人资料页面设置一个路由,可以这样定义:
(r"/user/([0-9]+)", UserHandler),
在这个规则中, ([0-9]+) 是一个捕获组,它会匹配一个或多个数字,并将匹配到的数字作为参数传递给 UserHandler 的实例。
4.2 路由参数和动态路由
动态路由是指那些包含参数的URL模式,这些参数通常在实际的HTTP请求中会被替换为特定的值。在Tornado中,动态路由参数可以通过正则表达式进行匹配和提取。
使用正则表达式匹配动态路径
Tornado的路由系统使用正则表达式来匹配URL和处理函数。开发者可以在路由规则中使用正则表达式来定义动态路径参数,这样就可以从URL中捕获特定的数据片段,并将它们作为参数传递给对应的处理函数。
下面是一个使用正则表达式匹配动态路径的例子:
(r"/post/([0-9]+)", PostHandler),
在这个例子中,任何形如 /post/123 的URL都会被路由到 PostHandler 类。数字 123 是动态捕获的,它会被作为参数传递给 PostHandler 的 __init__ 方法。
捕获参数和默认值的设置
路由规则的动态部分不仅限于捕获数字或字符串,还可以设置默认值。这样,即使URL不包含某些参数,路由系统也可以为这些参数提供默认值,确保应用的鲁棒性。
例如,假设我们想要创建一个博客文章的处理器,我们可以允许文章的ID是可选的,并为其提供一个默认值:
(r"/post(/([0-9]+))?", PostHandler),
在这个规则中,文章ID是可选的。如果请求的URL中包含了文章ID,它将被捕获并传递给 PostHandler ;如果没有提供,那么传递的值将会是 None 。我们可以在 PostHandler 的 __init__ 方法中检查这个参数来决定是展示文章详情还是文章列表。
4.3 高级路由技巧
随着Web应用复杂度的增加,仅靠基础的路由规则可能不足以应对所有场景。Tornado提供了一些高级路由技巧来帮助开发者更有效地组织和管理路由规则。
分组路由和命名空间
当应用规模较大时,将相关联的路由组合在一起可以提高可管理性。Tornado允许开发者将路由分组,并通过命名空间来组织这些路由。
from tornado.web import make_app
app = make_app([
(r"/group1", {
"module": "handlers.group1",
"namespace": "group1",
}),
(r"/group2", {
"module": "handlers.group2",
"namespace": "group2",
}),
], debug=True)
在这个例子中,我们为两个不同的路由组指定了模块和命名空间。这样,在对应的处理函数中,我们可以使用命名空间来区分不同的请求处理逻辑。
路由处理器的选择和自定义
有时候,一个URL可能匹配多个路由规则。Tornado允许开发者通过优先级来选择最适合的处理函数。此外,开发者也可以根据实际需要自定义路由匹配逻辑。
假设我们有两个处理函数都可以处理某个URL:
class HandlerA(tornado.web.RequestHandler):
def get(self):
self.write("This is HandlerA")
class HandlerB(tornado.web.RequestHandler):
def get(self):
self.write("This is HandlerB")
application = tornado.web.Application([
(r"/", HandlerA),
(r"/", HandlerB), # HandlerB never gets called because it has the same rule as HandlerA
], debug=True)
为了避免这种情况,Tornado为每个路由规则提供了一个 name 参数,允许开发者通过名称来唯一标识路由规则:
application = tornado.web.Application([
(r"/", HandlerA, {"name": "handler_a"}),
(r"/", HandlerB, {"name": "handler_b"}),
], debug=True)
现在,如果需要通过名称引用特定的路由,可以使用 reverse_url 函数:
reverse_url("handler_a")
通过高级路由技巧,开发者可以更加灵活地管理复杂的Web应用,确保路由规则的可维护性和扩展性。
5. RequestHandler类使用
5.1 RequestHandler的生命周期
RequestHandler类是Tornado框架的核心组成部分,负责处理客户端的请求,执行特定的业务逻辑,并生成响应。理解RequestHandler的生命周期是开发高效Web应用的关键。
5.1.1 处理请求的各个阶段
一个请求从进入Tornado服务器到最终响应的过程可以分为以下几个阶段:
- 初始化阶段 :当一个请求到来时,Tornado首先会创建一个RequestHandler的实例。
- 处理阶段 :随后,Tornado会根据请求的类型(例如GET或POST)调用相应的方法(如get()或post())。
- 渲染阶段 :在业务逻辑执行完毕后,通常会调用render()方法来渲染模板,并将结果返回给客户端。
- 结束阶段 :请求处理完毕后,RequestHandler实例会被销毁。
5.1.2 自定义初始化和清理方法
Tornado允许开发者重写一些生命周期方法来自定义请求处理过程。例如:
prepare():在请求的方法(如get或post)被调用前执行,适合做一些通用的请求准备操作。on_finish():在请求处理完毕后调用,可以用于清理资源或记录日志。
class MainHandler(tornado.web.RequestHandler):
def prepare(self):
# 在处理请求之前执行
pass
def on_finish(self):
# 请求处理完毕后执行
pass
5.2 HTTP方法的处理
5.2.1 GET、POST等请求的处理函数
不同的HTTP方法应该有不同的处理逻辑。Tornado允许开发者通过定义特定的方法来处理不同的HTTP请求。
class MainHandler(tornado.web.RequestHandler):
def get(self):
# 处理GET请求
self.write("Hello, world")
def post(self):
# 处理POST请求
self.write("Received POST data: " + self.request.body)
5.2.2 文件上传和下载的处理
Tornado也提供了用于处理文件上传和下载的方法:
class FileHandler(tornado.web.RequestHandler):
def post(self):
# 处理文件上传
file_info = self.request.files['file'][0]
self.write("File uploaded successfully: " + file_info['filename'])
def get(self):
# 处理文件下载
self.set_header('Content-Type', 'application/octet-stream')
self.set_header('Content-Disposition', 'attachment; filename="example.txt"')
self.write("Hello, file!")
5.3 高级用法和最佳实践
5.3.1 使用RequestHandler提供的工具方法
Tornado的RequestHandler提供了多种工具方法,用于简化开发:
self.redirect(url, permanent=False):用于重定向请求到另一个URL。self.set_cookie(name, value, expires_days=None, path='/', domain=None, secure=False, httponly=False):用于设置cookie。
5.3.2 优化性能和维护性的建议
为了优化性能和保持代码的可维护性,以下是一些实践建议:
- 避免在Handler中写过多逻辑 :将复杂的业务逻辑拆分到单独的服务类中。
- 使用异步方法处理I/O密集型任务 :例如数据库查询和外部API调用,以避免阻塞事件循环。
- 使用模板缓存 :对不经常变化的模板内容使用缓存,减少渲染时间。
以上是关于RequestHandler类使用的基础和高级技巧。掌握这些方法将帮助你更好地利用Tornado框架构建高性能的Web应用。
6. 异步处理编程技巧
在现代的Web开发中,异步处理是一个关键的概念,因为它允许Web服务器同时处理大量的并发连接而不产生性能瓶颈。Tornado框架内置了对异步操作的支持,这使得开发者可以构建出响应快速且能处理大量并发连接的Web应用。
6.1 异步处理的优势与挑战
6.1.1 介绍异步编程带来的性能提升
异步编程的主要优势在于,它允许程序在等待I/O操作(如数据库查询或网络请求)完成时,继续执行其他任务。这样,CPU的资源可以得到更高效的利用,避免了传统同步模型中的“阻塞”状态,即程序等待I/O操作完成时不做任何事情。
在Tornado中,这种异步行为通常通过协程(coroutines)来实现。协程是一种可以暂停和恢复执行的函数。它们是轻量级的,不需要线程的开销,因此可以在有限的资源上运行大量的协程。
6.1.2 异步编程可能遇到的问题
尽管异步编程可以带来性能上的显著提升,但它也带来了一些挑战。首先,异步代码往往比较难以理解和维护,因为它们经常包含许多回调和复杂的控制流。其次,错误处理也变得更加困难,因为异常可能不会立即发生,而是在未来的某个异步操作中被触发。
6.2 异步任务的创建和管理
6.2.1 使用Tornado的asyncio库
Tornado的异步特性主要是基于 asyncio 库实现的。开发者可以使用 @gen.coroutine 装饰器来定义协程,并使用 yield 关键字来挂起协程直到异步操作完成。
以下是一个简单的例子:
import tornado.ioloop
import tornado.web
import tornado.gen
class MainHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def get(self):
response = yield tornado.httpclient.AsyncHTTPClient().fetch("http://www.google.com")
self.write("The response body was: " + response.body.decode())
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
6.2.2 实现并行和顺序异步任务
在处理多个异步任务时,开发者需要决定是要并行处理这些任务还是顺序执行。并行处理通常用于相互独立的任务,可以通过创建多个 yield 语句来实现。而顺序执行则需要通过串行的 yield 调用来完成。
@tornado.gen.coroutine
def parallel_tasks():
# 启动两个异步任务,它们将并行执行。
response1, response2 = yield [
tornado.httpclient.AsyncHTTPClient().fetch("http://example.com/page1"),
tornado.httpclient.AsyncHTTPClient().fetch("http://example.com/page2")
]
# 处理响应
process_responses(response1, response2)
@tornado.gen.coroutine
def sequential_tasks():
# 顺序执行两个异步任务。
response1 = yield tornado.httpclient.AsyncHTTPClient().fetch("http://example.com/page1")
response2 = yield tornado.httpclient.AsyncHTTPClient().fetch("http://example.com/page2")
# 处理响应
process_responses(response1, response2)
6.3 异步编程实践案例
6.3.1 异步数据库访问
在处理数据库查询时,异步访问可以显著提高应用程序的响应能力。由于数据库操作通常是I/O密集型的,使用异步方法可以避免在等待数据库响应时阻塞程序。
使用Tornado进行异步数据库操作的示例代码如下:
@tornado.gen.coroutine
def query_database(self):
db = tornadodbo Database() # 假设tornadodbo是一个异步数据库库
result = yield db.query_async("SELECT * FROM users WHERE id = %s", (self.id,))
self.write(result)
6.3.2 异步调用外部服务的策略
调用外部API或服务时,异步编程同样能发挥作用。比如在微服务架构中,不同的服务可能通过HTTP RESTful API进行通信。使用异步方式调用这些服务可以提高系统的吞吐量。
@tornado.gen.coroutine
def call_external_service(url):
response = yield tornado.httpclient.AsyncHTTPClient().fetch(url)
# 处理响应
通过上述示例,我们可以看到异步编程在Web开发中的实际应用。虽然这可能看起来增加了代码的复杂性,但它为现代Web应用提供了高效、响应迅速的解决方案。在实际应用中,开发者需要权衡异步编程带来的性能提升和维护成本。
简介:《tornado-guide-examples:龙卷风指南入门示例》是一份面向Python初学者的Tornado Web框架教程。Tornado以其高并发性能和对长连接的优秀支持而知名,本教程通过实例解析深入浅出地介绍Tornado的基本用法和核心概念,包括异步非阻塞I/O模型、HTTP服务器、路由配置、异步处理、模板渲染、WebSocket通信、静态文件服务、会话管理以及错误处理等。教程旨在帮助用户快速掌握Tornado框架,以构建高性能、可扩展的Python Web应用。
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐

所有评论(0)