当前位置: 首页 > news >正文

深入 Actix-web 源码:解密 Rust Web 框架的高性能内核

深入 Actix-web 源码:解密 Rust Web 框架的高性能内核

目录

深入 Actix-web 源码:解密 Rust Web 框架的高性能内核

摘要

一、Actix-web 探索起点

1.1 宏观架构:连接器、Acceptor 与 Worker

1.2 与 Tokio 的共生关系

二、核心抽象:Service、Actor 与请求处理

2.1 Service Trait:一切皆服务

2.2 Actor 模型:轻量级并发单元

2.3 Handler 与 Extractor 机制:开发者的生产力引擎

三、源码深度拆解:从请求到响应

3.1 请求的诞生:Acceptor 到 Worker 的分发

3.2 中间件链的洋葱模型执行

3.3 路由与处理器:从函数到 Service 的魔法

四、Actix-web 与其他 Web 框架的深度对比

4.1 核心依赖与运行时模型

4.2 中间件模型与组合性

4.3 开发者体验与类型安全

4.4 综合对比总结

五、总结与思考

参考链接

关键词标签


摘要

在 Rust 的 Web 框架生态中,Actix-web 以其“极其快速”(extremely fast)的性能标签脱颖而出,常年在 TechEmpower 基准测试中名列前茅。作为一名追求极致性能的开发者,我对其背后的实现原理充满了好奇。它究竟是如何将 Rust 的零成本抽象与异步运行时结合,构建出如此高效的 Web 服务的?本文将带您深入 Actix-web 的源码世界,从其核心的 Actor 模型、Service 抽象,到与 Tokio 运行时的深度集成,层层剖析其高性能内核的奥秘。通过这次探索,我们不仅能理解 Actix-web 的设计哲学,更能掌握构建高性能异步服务的通用模式。

一、Actix-web 探索起点

我的 Actix-web 之旅始于一个经典的 Hello World 示例。简洁的几行代码就能启动一个高性能的 HTTP 服务器,这让我惊叹于其易用性。但我知道,真正的魔法隐藏在 #[actix_web::main]HttpServer::new 之下。

1.1 宏观架构:连接器、Acceptor 与 Worker

通过阅读源码和官方文档,我逐渐勾勒出 Actix-web 的宏观架构。其核心组件包括:

  • HttpServer: 服务器的入口点,负责绑定端口、启动监听。
  • Acceptor: 一个专门的循环,负责接受(Accept)新的 TCP 连接。
  • Worker: 多个工作线程,每个都运行在一个独立的 Tokio 运行时上,负责处理具体的 HTTP 请求。

HttpServer 启动后,它会创建一个或多个 Acceptor 线程。每个 Acceptor 线程会监听一个或多个套接字。一旦有新的连接到来,Acceptor 会将这个连接(一个 TcpStream)分发给一个 Worker。这个分发过程是通过一个无锁的通道(MPMC queue)完成的,确保了极高的分发效率。

图1:Actix-web 服务器架构 - 类型:流程图 - 简短说明:展示了 Actix-web 中 Acceptor 线程接收连接并通过无锁队列分发给多个 Worker 线程的流程,每个 Worker 线程拥有独立的 Tokio 运行时。

这种架构设计巧妙地将连接接收(I/O 密集型)和请求处理(CPU/网络 I/O 混合型)分离,避免了单一事件循环的瓶颈,是其高性能的关键之一 。

1.2 与 Tokio 的共生关系

一个常见的误解是 Actix-web 拥有自己的运行时。实际上,Actix-web 是构建在 Tokio 之上的。每个 Worker 线程内部都启动了一个独立的 Tokio 单线程运行时(current_thread scheduler)。这意味着 Actix-web 充分利用了 Tokio 成熟的异步 I/O、定时器和任务调度能力。

这种设计带来了两个好处:

  1. 性能隔离:每个 Worker 的 Tokio 运行时是独立的,一个 Worker 的任务不会影响其他 Worker,保证了服务的稳定性。
  2. 生态兼容:开发者可以直接在 Actix-web 的 handler 中使用任何基于 Tokio 的库(如 reqwest, sqlx),无缝集成到庞大的 Tokio 生态中 。

二、核心抽象:Service、Actor 与请求处理

在我深入 Actix-web 的源码后,我发现其强大的能力并非来自单一的魔法,而是源于几个精心设计、相互协作的核心抽象。Service trait 提供了统一的处理模型,Actor 模型赋予了其优雅的并发能力,而 Handler 与 Extractor 机制则极大地提升了开发者的生产力。这三者共同构成了 Actix-web 坚实的内核。

2.1 Service Trait:一切皆服务

在 Actix-web 的世界观里,一切皆服务(Everything is a Service)。这是我理解其架构最关键的一步。从最顶层的 App,到中间件(Middleware),再到具体的路由处理器(Handler),它们无一例外地实现了 Service trait。

Service trait 的定义简洁而强大:

pub trait Service<Request> {type Response;type Error;// 一个 Future,代表异步处理的结果type Future: Future<Output = Result<Self::Response, Self::Error>>;// 检查服务是否准备好处理请求,是背压(Backpressure)机制的基础fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;// 处理请求的核心方法fn call(&mut self, req: Request) -> Self::Future;
}

这个设计的精妙之处在于其组合性。一个中间件本质上就是一个 Service,它持有一个内层 Service 的引用。在它的 call 方法中,它可以先对请求进行预处理(如记录日志、验证身份),然后调用内层服务的 call 方法,最后再对响应进行后处理(如压缩、添加 CORS 头)。这种“洋葱模型”的嵌套结构,使得功能的组合变得异常灵活和清晰。更重要的是,这种设计与 tower 生态完全兼容,这意味着我们可以直接复用 tower 社区中海量的、经过生产验证的中间件组件。

2.2 Actor 模型:轻量级并发单元

Actix-web 的名字本身就揭示了其血统——它源自 actix 这个强大的 Actor 框架。虽然在处理标准的 HTTP 请求时,我们很少直接与 Actor 打交道,但 Actor 模型的思想已经深深烙印在框架的基因里。

一个 Actor 是一个独立的、封装了自身状态的并发单元。Actor 之间不共享内存,而是通过异步消息传递进行通信。这种模式天然地避免了数据竞争和锁的开销,使得编写高并发、高可靠的应用变得更为简单和安全。

在我的实践中,Actor 模型在处理长连接(如 WebSocket)和后台任务时大放异彩。例如,我可以为每个 WebSocket 客户端创建一个 Actor,该 Actor 负责管理客户端的状态、处理消息广播等。通过 Addr(Actor 的地址),我可以在任何地方(比如一个 HTTP handler)向这个 Actor 发送消息,实现 HTTP 与 WebSocket 的无缝集成。这种解耦的设计,让复杂的实时交互逻辑变得清晰而易于维护。

2.3 Handler 与 Extractor 机制:开发者的生产力引擎

如果说 Service 和 Actor 是 Actix-web 的“内功”,那么 Handler 与 Extractor 机制就是其“招式”,直接决定了开发者的体验。

在 Actix-web 中,我们编写的业务逻辑通常是一个普通的异步函数,即 Handler。例如:

async fn get_user(user_id: web::Path<i32>) -> impl Responder {// 业务逻辑format!("User ID: {}", user_id)
}

这里最神奇的部分是函数参数 user_id: web::Path<i32>。Actix-web 如何知道要从 URL 路径中提取一个 i32 并传递给这个参数?答案就是 Extractor(提取器)。

Extractor 是实现了 FromRequest trait 的类型。Actix-web 在调用 Handler 之前,会检查其所有参数的类型,并为每个参数调用对应的 FromRequest::from_request 方法。web::Path<T> 就是一个内置的 Extractor,它会自动解析 URL 路径,并尝试将其反序列化为类型 T

这种机制的强大之处在于其可扩展性。框架内置了大量 Extractor,如 web::Json<T>(从请求体解析 JSON)、web::Query<T>(从查询字符串解析)、web::Data<T>(访问应用状态)等。更重要的是,我们可以轻松地自定义 Extractor。例如,我可以创建一个 CurrentUser Extractor,它会自动从请求头中解析 JWT 令牌、验证其有效性,并返回一个用户对象。一旦定义好,我就可以在任何需要用户认证的 Handler 中直接使用 current_user: CurrentUser 作为参数,极大地简化了业务代码,使其专注于核心逻辑,而非繁琐的请求解析和验证。

正是这种将底层复杂性(Service 组合、Actor 通信)与上层简洁性(直观的 Handler 和 Extractor)完美结合的设计,让 Actix-web 既能满足对性能的极致追求,又能提供愉悦的开发体验。

三、源码深度拆解:从请求到响应

为了真正理解 Actix-web 的内核,我决定追踪一个 HTTP 请求从网络到达,到最终生成响应的完整生命周期。这个过程涉及多个核心模块的协同工作,通过分析其关键源码片段,我们可以窥见其高性能设计的精髓。

3.1 请求的诞生:Acceptor 到 Worker 的分发

一切始于 HttpServer::new().bind().run().await 这行代码。run 方法内部会启动一个或多个 Acceptor 线程。每个 Acceptor 的核心任务是一个无限循环,它不断地调用 TcpListener::accept().await 来接收新的 TCP 连接。

一旦连接建立,Acceptor 不会自己处理这个连接,而是将其封装成一个 Stream 对象,并通过一个高效的、无锁的多生产者多消费者(MPMC)队列(在源码中通常是一个 crossbeam channel)将其分发给后台的 Worker 池。这种设计将 I/O 密集型的连接接收操作与 CPU/网络混合型的请求处理操作完全解耦。

下面是我在源码中找到的、经过简化的概念性代码,它展示了 Worker 如何从队列中获取连接并启动处理流程:

// 概念性伪代码:Worker 的主循环
async fn worker_loop(mut rx: mpsc::Receiver<Stream>, // 从 Acceptor 接收连接的通道app_factory: Arc<dyn Fn() -> App + Send + Sync>, // 用于创建 App 的工厂
) {// 为当前 Worker 创建一个独立的 Tokio 单线程运行时let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();rt.block_on(async move {while let Some(stream) = rx.recv().await {// 1. 使用工厂函数创建一个新的 App 实例let app_service = app_factory();// 2. 将 App 服务和 TCP 流包装成一个 HttpServicelet http_service = HttpService::new(app_service);// 3. 启动一个异步任务来处理这个连接tokio::spawn(async move {// 4. HttpService 的 call 方法会驱动整个 HTTP 请求/响应周期if let Err(e) = http_service.call(stream).await {log::error!("Error handling connection: {}", e);}});}});
}

这段代码清晰地揭示了 Actix-web 的核心并发模型:每个 Worker 拥有独立的 Tokio 运行时,每个 TCP 连接都在一个独立的 tokio::spawn 任务中被处理。这种隔离性保证了高并发下的稳定性和性能。

3.2 中间件链的洋葱模型执行

HttpService 开始处理一个连接时,它首先会解析出 HTTP 请求,然后将其传递给用户定义的 App 服务。但这里的 App 服务并非原始的 App,而是被一系列中间件层层包装后的最终形态。

在 Actix-web 内部,中间件通过实现 Transform trait 来工作。Transform 负责将一个内层的 Service 转换(wrap)成一个新的、带有额外逻辑的 Service。让我们来看一个自定义日志中间件的简化实现:

use actix_web::{dev::{Service, ServiceRequest, ServiceResponse, Transform},Error,
};
use futures_util::future::LocalBoxFuture;
use std::{future::Future, pin::Pin, rc::Rc};// 日志中间件的结构体
pub struct Logger;// Transform trait 负责创建中间件实例
impl<S, B> Transform<S, ServiceRequest> for Logger
whereS: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,S::Future: 'static,B: 'static,
{type Response = ServiceResponse<B>;type Error = Error;type InitError = ();type Transform = LoggerMiddleware<S>;type Future = std::future::Ready<Result<Self::Transform, Self::InitError>>;fn new_transform(&self, service: S) -> Self::Future {// 创建包装了内层服务的中间件std::future::ready(Ok(LoggerMiddleware {service: Rc::new(service),}))}
}// 中间件本身也是一个 Service
pub struct LoggerMiddleware<S> {service: Rc<S>,
}impl<S, B> Service<ServiceRequest> for LoggerMiddleware<S>
whereS: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,S::Future: 'static,B: 'static,
{type Response = ServiceResponse<B>;type Error = Error;type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;// 使用宏简化 poll_ready 的实现actix_web::dev::forward_ready!(service);fn call(&self, req: ServiceRequest) -> Self::Future {let start = std::time::Instant::now();let path = req.path().to_string();// 1. 在请求处理前记录日志log::info!("Started {} {}", req.method(), path);let fut = self.service.call(req);Box::pin(async move {let res = fut.await;// 2. 在请求处理后记录日志let status = res.as_ref().map(|r| r.status()).unwrap_or(500);let elapsed = start.elapsed();log::info!("Finished {} {} in {:?}", status, path, elapsed);res})}
}

这个例子完美诠释了“洋葱模型”。call 方法首先记录请求开始,然后调用内层服务 self.service.call(req),最后在 Futureawait 之后记录响应结束。每个中间件都像洋葱的一层,请求从外向内穿透,响应从内向外返回。这种模式使得每个中间件的职责单一且清晰。

3.3 路由与处理器:从函数到 Service 的魔法

最终,请求会到达具体的路由处理器(Handler)。开发者编写的 Handler 通常是一个签名如 async fn handler(...) -> impl Responder 的函数。Actix-web 是如何将这样一个普通函数变成一个 Service 的呢?

答案在于其强大的宏系统。当我们使用 web::get().to(handler) 注册路由时,to 方法内部会利用过程宏(Procedural Macro)对 handler 函数进行分析和转换。

宏会检查函数的参数列表,并为每个参数生成对应的 FromRequest 调用代码。然后,它会生成一个匿名的、实现了 Service trait 的结构体。这个结构体的 call 方法会执行以下步骤:

  1. 并发地(或按需)调用所有参数 Extractor 的 from_request 方法。
  2. 将提取出的参数值传递给原始的 handler 函数。
  3. 等待 handler 函数返回一个 impl Responder
  4. 调用 Responder::respond_to 方法,将返回值转换为最终的 HttpResponse

下面是一个由宏生成的、概念性的 Service 实现:

// 概念性伪代码:由宏为 `async fn greet(name: web::Path<String>) -> String` 生成的 Service
struct GreetHandlerService;impl Service<ServiceRequest> for GreetHandlerService {type Response = ServiceResponse;type Error = Error;type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;fn call(&self, req: ServiceRequest) -> Self::Future {Box::pin(async move {// 1. 调用 Extractor 提取参数let name = web::Path::<String>::from_request(&req).await?;// 2. 调用原始 handler 函数let result: String = greet(name).await;// 3. 将结果转换为 HttpResponselet response = result.respond_to(&req);Ok(ServiceResponse::new(response))})}
}

这种自动转换是 Actix-web 开发体验如此流畅的关键。它隐藏了 ServiceFromRequest 的复杂性,让开发者可以像编写同步函数一样编写异步 Handler,同时又能享受到底层高性能异步运行时带来的所有好处。这种“零成本抽象”的理念,正是 Rust 语言哲学在 Web 框架领域的完美体现。

四、Actix-web 与其他 Web 框架的深度对比

为了更全面地理解 Actix-web 的设计哲学和适用场景,我将其与 Rust 生态中另外两个主流 Web 框架——AxumRocket——进行了深入对比。通过分析它们在核心依赖、中间件模型和开发体验上的异同,我们可以更清晰地把握各自的优势。

4.1 核心依赖与运行时模型

框架的底层依赖决定了其性能特性和生态兼容性。

  • Actix-web 构建在 actix Actor 框架之上,并深度依赖 tokio 作为其异步运行时。如前所述,它为每个 Worker 启动一个独立的 Tokio 单线程运行时,这种隔离模型是其高性能的基石。这种设计使其能无缝集成任何基于 Tokio 的库。
  • Axum 则采取了更为“纯粹”的路径,它直接构建在 tokiotower 之上。tower 是一个专注于 Service trait 的中间件生态,这使得 Axum 的中间件模型极其灵活和标准化。Axum 通常运行在一个共享的多线程 Tokio 运行时上。
  • Rocket 在最新版本中也全面拥抱了 tokio,但其内部抽象层更为厚重,旨在为开发者屏蔽底层细节,提供开箱即用的体验。

下面是一个使用 sqlx(一个基于 Tokio 的异步数据库驱动)的示例,展示了三者在集成上的共通性:

// 三者都可以无缝使用基于 Tokio 的库,如 sqlx
use sqlx::PgPool;// Actix-web
async fn actix_handler(pool: web::Data<PgPool>) -> Result<impl Responder, Error> {let user = sqlx::query("SELECT * FROM users LIMIT 1").fetch_one(pool.get_ref()).await?;Ok(web::Json(user))
}// Axum
async fn axum_handler(State(pool): State<PgPool>,
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {let user = sqlx::query("SELECT * FROM users LIMIT 1").fetch_one(&pool).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;Ok(Json(user))
}// Rocket
#[get("/user")]
async fn rocket_handler(pool: &State<PgPool>) -> Result<Json<serde_json::Value>, Status> {let user = sqlx::query("SELECT * FROM users LIMIT 1").fetch_one(pool.inner()).await.map_err(|_| Status::InternalServerError)?;Ok(Json(user))
}

这段代码表明,尽管框架不同,但得益于 Tokio 生态的统一,它们在与底层异步库的集成上并无本质障碍。

4.2 中间件模型与组合性

中间件是 Web 框架扩展功能的核心机制,三者的设计哲学在此处体现得淋漓尽致。

  • Actix-web 的中间件基于自定义的 Transform/Service trait,但其设计与 towerLayer/Service 高度兼容。这使得开发者既可以使用 Actix-web 生态的中间件,也可以直接复用 tower 社区的丰富资源。
  • Axum 则完全拥抱 tower,其中间件就是标准的 tower::Layer。这种“标准化”策略极大地增强了其生态的互操作性,任何 tower 中间件都可以直接用于 Axum。
  • Rocket 拥有自己独特的中间件系统(Fairing),它通过生命周期钩子(如 on_request, on_response)来介入请求处理流程。这种方式非常直观,但与 tower 生态不兼容,形成了自己的小闭环。

下面对比 towerTimeout 中间件在 Actix-web 和 Axum 中的用法:

// Axum: 直接使用 tower::timeout::TimeoutLayer
use tower::timeout::TimeoutLayer;
use std::time::Duration;let app = Router::new().route("/slow", get(slow_handler)).layer(TimeoutLayer::new(Duration::from_secs(5)));// Actix-web: 需要使用 actix-web 的包装器或兼容层
use actix_web::middleware::Compat;
use tower::timeout::Timeout;let timeout_service = Timeout::new(your_app_service,Duration::from_secs(5),
);
// 然后通过 Compat 或自定义 Transform 将其集成到 Actix-web 的 App 中
// 这比 Axum 稍显繁琐,但仍然是可行的。

这个例子说明,Axum 在 tower 生态的集成上更为直接,而 Actix-web 虽然兼容,但可能需要额外的适配步骤。

4.3 开发者体验与类型安全

开发者体验是框架能否流行的关键因素。

  • Rocket 以其“零配置”和丰富的宏(如 #[get("/hello/<name>")])著称,提供了最接近同步框架的开发体验。它的类型检查非常强大,许多错误(如路由冲突、类型不匹配)能在编译期被捕获。
  • Axum 在类型安全和简洁性之间取得了很好的平衡。它利用 Rust 强大的类型系统和模式匹配来提取路径参数和查询参数,代码非常清晰。例如,Path((user_id, post_id)) 这样的解构语法既简洁又类型安全。
  • Actix-web 的 Extractor 机制同样强大且灵活,但其 API 相对更显式一些(如 web::Path<i32>)。对于习惯了显式优于隐式的开发者来说,这可能更易理解。

下面是一个处理路径参数的对比:

// Rocket: 使用宏直接在路径中声明参数
#[get("/users/<user_id>/posts/<post_id>")]
fn rocket_handler(user_id: i32, post_id: i32) -> String {format!("User: {}, Post: {}", user_id, post_id)
}// Axum: 利用类型解构
async fn axum_handler(Path((user_id, post_id)): Path<(i32, i32)>) -> String {format!("User: {}, Post: {}", user_id, post_id)
}// Actix-web: 使用 Extractor
async fn actix_handler(path: web::Path<(i32, i32)>) -> impl Responder {let (user_id, post_id) = path.into_inner();format!("User: {}, Post: {}", user_id, post_id)
}

Rocket 的方式最为简洁,Axum 的方式在类型安全和简洁性上表现优异,而 Actix-web 的方式则更为显式和灵活。

4.4 综合对比总结

综合以上分析,我们可以得出以下结论:

特性/框架

Actix-web

Axum

Rocket

核心依赖

actix, tokio

tokio, tower

tokio

设计理念

高性能、成熟稳定、Actor 模型

简洁、类型安全、tower 生态

开发者体验优先、语法糖丰富

中间件模型

自定义 Service,兼容 tower

原生 tower Layer

自定义 Fairing

学习曲线

中等(需理解 Service/Actor)

低到中(概念清晰)

低(API 直观)

适用场景

高并发、高性能 API 服务、需要 Actor 模型的场景

现代 Web API、微服务、tower 生态用户

快速原型、中小型 Web 应用、追求极致开发体验

选择框架就是选择一种工作方式和一套约束。” 对于需要榨取服务器每一分性能、构建高并发服务的团队,Actix-web 凭借其经过实战检验的稳定性和卓越性能,依然是一个极具吸引力的选择。

五、总结与思考

通过这次对 Actix-web 源码的深入探索,我深刻体会到其高性能并非偶然,而是源于一系列精妙的设计决策:将连接接收与请求处理分离的多 Worker 架构、统一且强大的 Service 抽象、以及建立在成熟 Tokio 生态之上的坚实基础。

Actix-web 不仅仅是一个 Web 框架,它更是一个展示如何在 Rust 中构建高性能、高并发网络服务的绝佳范例。它教会我们,通过组合简单的抽象(如 Service),并利用语言和运行时的特性(如所有权、异步),可以构建出既高效又安全的复杂系统。

对于我而言,这次源码之旅不仅解答了最初的疑问,更让我对 Rust 异步生态有了更系统的理解。未来在构建自己的服务时,我会更有信心地运用这些从 Actix-web 中学到的设计模式和最佳实践。

参考链接

  1. Actix-web 官方文档
  2. Actix-web GitHub 仓库
  3. Tokio 官方文档
  4. Tower: A library of modular and reusable components for building robust networking clients and servers
  5. TechEmpower Web Framework Benchmarks

关键词标签

#Rust #ActixWeb #Web框架 #源码分析 #高性能编程

http://www.dtcms.com/a/541426.html

相关文章:

  • Linux远程控制Windows桌面的cpolar实战指南
  • 焦作网站建设哪家好自己怎么用h5做网站
  • 论坛程序做导航网站photoshop安卓版
  • FP16 vs INT8:Llama-2-7b 昇腾 NPU 精度性能基准报告
  • Steering Llama 2 via Contrastive Activation Addition
  • 座舱出行Agent实战(三):专能化架构如何实现效率与稳定性的双重飞跃
  • 淘宝联盟怎么新建网站网站设计教程
  • 一篇文章深入理解Elasticsearch高级用法
  • 【数据工程】14. Stream Data Processing
  • Elasticsearch入门指南:从零到精通
  • wordpress 非插件七牛cdn全站加速东至网站建设
  • 进出口网站贸易平台有哪些个人网站可以做推广吗
  • 游戏网站首页设计服务器有了网站怎么做
  • 计算机组成原理---存储系统
  • Vector深度剖析及模拟实现
  • Linux进程:进程属性
  • word文档做网站建立网站地图
  • 大连建设网节能办公室网站随州网站seo
  • SSM老年公寓管理系统4do68(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • 网站制作佛山海拉尔网站建设平台
  • 推客系统开发:从技术架构到业务落地的全栈实现指南
  • Spring Boot 日志体系 Logback + SLF4J 深入剖析
  • Perl 格式化输出
  • 网站建设三种方法广州h5网站制作
  • 一文讲透BOM、MRP、WMS
  • ai最新的发展趋势有哪些
  • 简化AI服务构建的Python框架leptonai
  • 解决 Vray for 3ds Max 三大常见生产问题,提升渲染工作流效率
  • 做网站搭建环境网站建设在学校中的作用
  • 优秀的网站建设吉林建设厅官方网站