rust语言这两年在“安全、并发、性能”方面吸足了眼球,但在主流的web应用领域表现如何?有哪些可以推荐的web框架?下面就这个话题深入展开。
背景
web框架
我们先简单回顾下web框架: web框架主要用于动态web开发,开发人员在框架基础上实现自己的业务逻辑。 web框架需要实现接收到请求后,能够提供参数校验的能力,然后根据请求参数从底层拿到数据,最后以特定格式返回。web框架旨在简化web开发流程,让开发人员更专注于自己的业务逻辑。
其他语言现状
其他主流语言,web框架都已经发展非常成熟,大家耳熟能详的比如:
-
php语言: laravel
-
java语言:spring mvc
-
go语言: gin/beego
这些框架介绍的文章已经满大街了,在此就不赘述。
rust常见web框架
rust目前已知的web框架也有几十种,在flosse的rust-web-framework-comparison开源项目里面详细列出(见文末参考资料的链接),感兴趣的可以查看。但遗憾的是官方也没有给出支持或者推荐的web框架,所以我们就实际项目简单使用的情况,挑出几个比较下,希望给大家选型框架时参考下。
rust web框架的难点
在比较这些框架之前,我们先回顾下rust语言处理web流程困难的地方。众所周知,rust近年发展迅猛,同时也带来一些新的概念,比如生命周期等,另外rust没有全局状态,或者说实现比较困难,以及编译检查比较严格,相对速度也比较慢,这样对实现web框架带来一些困难,下来我们看下这些框架的实现情况。
rust web框架分类
rust web框架中,hyper、h2、tiny-http属于底层一些的框架,比如hyper,很多框架都是基于它开发的,它也是rust语言中比较老牌的框架;rocket框架相对比较专注, 大名鼎鼎的tokio的作者实现的tower,目前跟warp交流较多,有可能会合并大家也可以持续关注;iron、gotham、nickel、rouille、actix-web功能相对全面些,就像其中actix框架整个体系庞大,下面又拆分出许多子框架:web、http、net等。
rust主流web框架的比较
下面我们终于进入正题,挑出几个我们实际项目中使用过的框架进行比较。当然,可能有些框架的特性我们并未涉猎,文中有不妥之处欢迎指正。
hyper
第一个出场的就是hyper,它的特点就是高性能,后面会给出的压测结果,跟actix-web差不多;另外首先实现了client组件,方便写单元测试验证;前面也提到很多web框架基于hyper实现,侧面说明他底层的封装还是不错的。不过它也有些缺点:
-
hyper应用侧的功能相对少,所以会导致很多框架又在他的基础上继续封装;
-
通过match block实现路由,这个笔者认为是一个比较明显的缺点; 比如下面的例子:
async fn response_examples(
req: request,client: client
) -> result> {
match (req.method(), req.uri().path()) {
(&method::get, "/index.html") => {
ok(response::new(index.into()))
},
(&method::get, "/json_api") => {
api_get_response().await
}
_ => {
ok(response::builder().status(statuscode::not_found)
.body(notfound.into())
.unwrap())
}
}
}
这是一个典型的hyper的实现,但实际项目中的match块处理较复杂的流程时往往需要翻一两页,这样开发和review都相对困难。
actix-web
actix-web是已知的所有web框架实现了actor模型,由微软的工程师 nikolay 开发,azure用的比较多;超级快是另一个优点,在web性能评测网站刷榜,但有取巧嫌疑,下面会展开说下他怎么做的;底层基于tokio。整体层级结构如下:
-
tokio && futures -> actix-net/actix-rt -> actix-net/其他子crate -> actix-web
对于整个actix来说,功能还是比较丰富;今年6月发布的的1.0,进一步简化actor模块,service替代handle,大量的简化代码。
-
缺点:大量的unsafe(如下图),导致经常有开发爆出堆栈溢出的bug;这也是他性能最好的原因之一;
他的另外一个缺点,代码质量不高,频繁变动,至少是web模块这块,文档和实例也不全;比如0.7版的handle,到1.0版变成service,他封装的responder,也不稳定,下面跟rocket的实现一起展开比较。
rocket
rocket是目前rust主流的web框架之一,github项目有8.9k的star。而它的http处理部分就是基于前面提到的hyper。从官方资料看,具有如下三个特点:
-
类型安全;
-
上手简单,让你更专注于自己的业务;
-
组件丰富,且几乎都可以自定义;
rocket从笔者使用经验来看:确实上手非常快,对各种语言背景的开发人员都相对友好;扩展容易,他的组件几乎都可以自定义,requestguard、state、fairing 都可以定制;另外,文档、example都非常详细,预定义很多宏,非常方便; rocket的缺点:性能上会略差些,后面会给出压测数据。不过他的async分支也快发布了,都打磨了几个月,大家可以关注;
汇总
简单汇总一个表格(如下图),总结下:从大家的关注度上,rocket胜出;actix-web的功能会多些,比如websocket等;从使用和应用层的周边支持上,rocket做的最好;所以不太在意性能的话,建议选择rocket。下来我们就就详细讨论下rocket。
rocket
rocket设计原则
首先看下rocket的设计原则,这也是其他框架没有的,而且他们实际代码落地上也履行的不错,具体原则如下:
-
security, correctness, and developer experience are paramount.
-
all request handling information should be typed and self-contained
-
decisions should not be forced
笔者的理解:
-
安全性,正确性和开发人员经验至关重要。这就充分挖掘了rust安全方面的优势,且对各语言开发人员友好, 后面讲到request、guards这两个组件再展开下;
-
所有被处理请求信息都必须指定类型。这样也对开发人员有所约束, 比如在使用responder组件就深有感触;
-
不应该强行限制。他的模板、序列化、会话等组件,几乎所有的功能都是可选择的插件形式。对于这些,rocket都有官方库和支持,完全可以自由选择和替换。 所以,rocket是rust web框架里面,完美的平衡了自由和约束。下面我们就几个重要组件详细展开。
requestguards
requestguards有些类似java的spring框架的velidator,是代表任意验证策略的类型,验证策略通过fromrequest实现。requestguards没有数量限制,实际使用中根据自己需求添加,也可以自定义guards。举个例子:
#[get("/")]
fn index(param: isize, a: a, b: b, c: c) -> ... { ... }
上述例子中的a、b、c都是具体的实现,比如a验证auth,b验证计数,c具体业务校验等;也可以用框架已经实现的guard,或者自己定义,整体还是非常灵活。guard组件也是履行他的第一个设计原则:正确性、安全性;
responder
我们直接看下responder的定义:
pub trait responder {
/// returns `ok` if a `response` could be generated successfully. otherwise,
/// returns an `err` with a failing `status`.
///
/// the `request` parameter is the `request` that this `responder` is
/// responding to.
///
/// when using rocket's code generation, if an `ok(response)` is returned,
/// the response will be written out to the client. if an `err(status)` is
/// returned, the error catcher for the given status is retrieved and called
/// to generate a final error response, which is then written out to the
/// client.
fn respond_to(self, request: &request) -> response::result;
}
笔者翻了一下这个trait的代码记录,从16年最开始设计就已经确定,非常稳定,之后再没有更新过。respond_to返回的result,他封装了一下,要么是ok,要么是一个err的status。 另外内置实现了常用的类型( str 、string、 [u8] 、 file、 option、 status ),基本覆盖绝大部分业务场景;如果还不能满足,那你也可以实现自定义的responder。 这也就体现他的第二设计原则:类型约束。不像其他的框架,比如actix-web也有responder,但也是最近的版本才稳定下来。如下要想自定义怎么办? 这是一个自定义的例子:
impl responder for person {
fn respond_to(self, _: &request) -> response::result {
response::build()
.sized_body(cursor::new(format!("{}:{}", self.name, self.age)))
.raw_header("x-person-name", self.name)
.header(contenttype::new("application", "x-person"))
.ok()
}
}
#[get("/person")]
fn person() -> person { person { name: "a".to_string(), age: 20 } }
这就是自定义的一个responder,直接返回一个person对象;也可以加上err的处理;看起来还是比较简单吧。我们可以对比下actix-web的responder的实现:
pub trait responder {
/// the associated error which can be returned.
type error: into;
/// the future response value.
type future: future
actix-web是19年3月份才有这个组件的,status,header还是后来加;实现的起来复杂的多;
state
我们看下rocket的state组件,也是最后一个原则的体现。直接举例说明:
use rocket::state;
use rocket::response::content;
struct hitcount(atomicusize);
#[get("/")]
fn index(hit_count: state) -> content::html {
hit_count.0.fetch_add(1, ordering::relaxed);
let msg = "your visit has been recorded!";
let count = format!("visits: {}", count(hit_count));
content::html(format!("{}
{}", msg, count))
}
#[get("/count")]
fn count(hit_count: state) -> string {
hit_count.0.load(ordering::relaxed).to_string()
}
fn rocket() -> rocket::rocket {
rocket::ignite()
.mount("/", routes![index, count])
.manage(hitcount(atomicusize::new(0)))
}
这是一个计数器的简单实现,通过state注入到handle中,每次访问/都加1;也可以通过/cout接口拿到当前计数;这两接口是无状态的,但count是全局的,这就是state的魅力; 当然state可以干的事情很多,另外也内置实现了request-local state,类似于thread-local 可以做链路跟踪;
fairing
rocket还有个fairing组件,结合上面提到的state组件,可以实现应用启动时,或者state的attach时,请求和响应时的一些定制;比如定制filter,日志等插件都可以实现;
#[derive(default)]
struct counter {
get: atomicusize,
post: atomicusize,
}
impl fairing for counter {
fn on_request(&self, request: &mut request, _: &data) {
… …}
fn on_response(&self, request: request, response: mut response)
{… …}
}
fn rocket() -> rocket::rocket {
rocket::ignite()
.mount("/", routes![… , …])
.attach(counter::default())
… …
}
这是一个根据请求方法不同而分别计数功能,你也可以实现counter的attach的事件处理;但跟filter不同,fairing更通用些。其特点如下:
-
挂载在request的生命周期
-
不能终止或者直接响应request
-
不能将非请求数据注入request
-
可以检查或者修改配置
-
只有attach了才能触发
-
顺序很重要
fairing可以应用在一些动态改配置的场景,或者多种环境的复杂配置场景;且官方贴心的内置了adhoc实现,方便开发人员快速实现。
至此,rocket的特性就讲完了,所以建议大家根据自己的实际需求,来选择自己合适的框架。
压测数据参考
最后我们给出之前做的压测结果:
上述表格中rocket sync(master分支)并发只能到50,如果业务场景非常注重性能的话,那你就慎重考虑,或者你持续关注async分支的进展;另外比较了hyper框架debug和release方式的差异,其他的差异比例也类似。
参考资料:
-
hyperag真人试玩娱乐官网
-
rocketag真人试玩娱乐官网
-
actix-webag真人试玩娱乐官网
-
rust web框架的比较
-
the-best-rust-frameworks-to-check-out-in-2019
westar实验室成立于2018年,是分层区块链的先行者。团队致力于创建下一代分层区块链及金融基础设施。通过先进技术和工程能力,构建产品和基础网络,从而减少对信任的依赖,提高金融市场的效率。对于区块链生态,通过分层区块链技术,在保证安全性的同时,保证处理速度,达到可以商用的目的。
参考阅读:
-
解决并发编程之痛的良药--结构化并发编程
-
hulu如何扩展influxdb使其支持每秒百万tps
-
深入浅出rust异步编程之tokio
-
聊聊用 uuid/guid 作为主键那些坑
-
为了戒网,我给每个网站自动添加3-25秒的访问延迟
-
如何成为一名在家办公的高效工程师(一个国外团队的远程经验)
-
1500字节为何成为互联网mtu标准?