理解deno-基础篇

deno介绍deno是一个基于v8、rust和Tokio的Javascript/Typescript的安全运行时。它在内部嵌入了一个typescript的编译器。可以将typescript编译成js然后运行在v8上,并通过c++ libdeno实现js与rust的通信交互,当然deno也可以直接运行Javascript代码。deno安装linux/maccurl -fsSL https://deno.land/x/install/install.sh | shwindowsiwr https://deno.land/x/install/install.ps1 | iex详细的细节请参考官网安装介绍deno的简单例子计算斐波那契数列公式摘抄自维基百科斐波那契数列run javascripttest.jsfunction fibo(n) { if (n === 0) return 0; let fn = 0, nextFn = 1; let tmp_fn = 0, tmp_nextFn = 0; for (var i = 0; i < n; i++) { tmp_fn = nextFn; tmp_nextFn = nextFn + fn; fn = tmp_fn; nextFn = tmp_nextFn; } return tmp_fn;}console.log(fibo(13));deno ./test.jsrun typescripttest.tsfunction fibo(n: number) { if (n === 0) return 0; let fn: number = 0, nextFn: number = 1; let tmp_fn: number = 0, tmp_nextFn: number = 1; for (let i: number = 0; i < n; i++) { tmp_fn = nextFn; tmp_nextFn = nextFn + fn; fn = tmp_fn; nextFn = tmp_nextFn; } return tmp_fn;}console.log(fibo(13));deno ./test.tsdeno内部结构下图1是deno的部分文件截图图1 deno文件的部分截图上图中圈出来的三个文件夹分别是jslibdenosrc分别对应deno的api层、中间层、和实现层,其中js中主要是typescript的代码,包含typescript的编译器和deno暴露给用户的api。libdeno中主要是c++代码,用来加载v8实例,实现typescript和rust的通信。src文件中主要是rust的代码,是deno功能的具体实现。例如用户使用File实例的write方法来写文件,实际上是api层(typescript)通过中间层(libdeno)将数据传输给实现层(rust),最终写文件操作由rust去完成。deno结合了Typescript/Javascript的易用性和rust的系统语言能力。下图2可以清晰的表示js和rust之间的逻辑关系。图来自于官网,图2 deno的架构图预告~~~接下来还会有两篇文章分析deno的内部原理~~~ ...

March 6, 2019 · 1 min · jiezi

一篇Rust的30分钟介绍

我最近向Rust的文档提交了一个提案。 我通篇提案中一个重要组成部分是对可能听说过Rust的人简短而简单的介绍,以便他们能够确定Rust是否适合他们。 前几天,我看到了一个精彩的演讲,并认为它可以作为这个介绍的一个很好的基础。 将此视为此类介绍的RFC(Request For Comments)。 非常欢迎反馈在rust-dev或Twitter上。这个教程已经成为官方教程。Rust是一种系统编程语言,专注于强大的编译时正确性保证。 它通过提供非常强大的编译时保证和对内存生命周期的明确控制,改进了其他系统语言(如C ++,D和Cyclone)的思想。 强大的内存保证使编写正确的并发Rust代码比使用其他语言更容易。 这可能听起来非常复杂,但它比听起来更容易! 本教程将让您在大约30分钟内了解Rust。 希望你至少模糊地熟悉以前的“大括号”语言。 这些概念比语法更重要,所以如果你没有得到每一个细节,请不要担心:本教程可以帮助你解决这个问题。让我们来谈谈Rust中最重要的概念:“所有权”,以及它对并发编程(对程序员来讲通常是非常困难的任务)的启发。所有权所有权是Rust的核心,也是其更有趣和独特的功能之一。 “所有权”是指允许哪部分的代码修改内存。 让我们从查看一些C ++代码开始:int *dangling(void){ int i = 1234; return &i;}int add_one(void){ int *num = dangling(); return *num + 1;}dangling函数在栈上分配了一个整型,然后保存给一个变量i,最后返回了这个变量i的引用。这里有一个问题:当函数返回时栈内存变成失效。意味着在函数add_one第二行,指针num指向了垃圾值,我们将无法得到想要的结果。虽然这个一个简单的例子,但是在C++的代码里会经常发生。当堆上的内存使用malloc(或new)分配,然后使用free(或delete)释放时,会出现类似的问题,但是您的代码会尝试使用指向该内存的指针执行某些操作。 更现代的C ++使用RAII和构造函数/析构函数,但它们无法完全避免“悬空指针”。 这个问题被称为“悬空指针”,并且不可能编写出现“悬空指针”的Rust代码。 我们试试吧:fn dangling() -> &int { let i = 1234; return &i;}fn add_one() -> int { let num = dangling(); return *num + 1;}当你尝试编译这个程序时,你会得到一个有趣和非常长的错误信息:temp.rs:3:11: 3:13 error: borrowed value does not live long enoughtemp.rs:3 return &i;temp.rs:1:22: 4:1 note: borrowed pointer must be valid for the anonymous lifetime #1 defined on the block at 1:22…temp.rs:1 fn dangling() -> &int {temp.rs:2 let i = 1234;temp.rs:3 return &i;temp.rs:4 }temp.rs:1:22: 4:1 note: …but borrowed value is only valid for the block at 1:22temp.rs:1 fn dangling() -> &int { temp.rs:2 let i = 1234; temp.rs:3 return &i; temp.rs:4 } error: aborting due to previous error为了完全理解这个错误信息,我们需要谈谈“拥有”某些东西意味着什么。 所以现在,让我们接受Rust不允许我们用悬空指针编写代码,一旦我们理解了所有权,我们就会回来看这块代码。让我们先放下编程一会儿,先聊聊书籍。 我喜欢读实体书,有时候我真的很喜欢一本书,并告诉我的朋友他们应该阅读它。 当我读我的书时,我拥有它:这本书是我所拥有的。 当我把书借给别人一段时间,他们向我“借用”这本书。 当你借用一本书时,在特定的一段时间它是属于你的,然后你把它还给我,我又拥有它了。 对吗?这个概念也直接应用于Rust代码:一些代码“拥有”一个指向内存的特定指针。 它是该指针的唯一所有者。 它还可以暂时将该内存借给其他代码:代码“借用”它。 借用它一段时间,称为“生命周期”。这是关于所有权的所有。 那似乎并不那么难,对吧? 让我们回到那条错误信息:error: borrowed value does not live long enough。 我们试图使用Rust的借用指针&,借出一个特定的变量i。 但Rust知道函数返回后该变量无效,因此它告诉我们:borrowed pointer must be valid for the anonymous lifetime #1… but borrowed value is only valid for the block。 优美!这是栈内存的一个很好的例子,但堆内存呢? Rust有第二种指针,一个’唯一’指针,你可以用〜创建。 看看这个:fn dangling() -> ~int { let i = ~1234; return i;}fn add_one() -> int { let num = dangling(); return *num + 1;}此代码将成功编译。 请注意,我们使用指针指向该值而不是将1234分配给栈:~1234。 你可以大致比较这两行:// rustlet i = ~1234;// C++int *i = new int;*i = 1234;Rust能够推断出类型的大小,然后分配正确的内存大小并将其设置为您要求的值。 这意味着无法分配未初始化的内存:Rust没有null的概念。万岁! Rust和C ++之间还有另外一个区别:Rust编译器还计算了i的生命周期,然后在它无效后插入相应的free调用,就像C ++中的析构函数一样。 您可以获得手动分配堆内存的所有好处,而无需自己完成所有工作。 此外,所有这些检查都是在编译时完成的,因此没有运行时开销。 如果你编写了正确的C ++代码,你将编写出与C++代码基本上相同的Rust代码。而且由于编译器的帮忙,编写错误的代码版本是不可能的。你已经看到了一种情况,所有权和生命周期有利于防止在不太严格的语言中通常会出现的危险代码。现在让我们谈谈另一种情况:并发。并发并发是当前软件世界中一个令人难以置信的热门话题。 对于计算机科学家来说,它一直是一个有趣的研究领域,但随着互联网的使用爆炸式增长,人们正在寻求改善给定的服务可以处理的用户数量。 并发是实现这一目标的一种方式。 但并发代码有一个很大的缺点:它很难推理,因为它是非确定性的。 编写好的并发代码有几种不同的方法,但让我们来谈谈Rust的所有权和生命周期的概念如何帮助实现正确并且并发的代码。首先,让我们回顾一下Rust中的简单并发示例。 Rust允许你启动task,这是轻量级的“绿色”线程。 这些任务没有任何共享内存,因此,我们使用“通道”在task之间进行通信。 像这样:fn main() { let numbers = [1,2,3]; let (port, chan) = Chan::new(); chan.send(numbers); do spawn { let numbers = port.recv(); println!("{:d}", numbers[0]); }}在这个例子中,我们创建了一个数字的vector。 然后我们创建一个新的Chan,这是Rust实现通道的包名。 这将返回通道的两个不同端:通道(channel)和端口(port)。 您将数据发送到通道端(channel),它从端口端(port)发出。 spawn函数可以开启一个task。 正如你在代码中看到的那样,我们在新任务中调用port.recv()(‘receive’的缩写),我们在外面调用chan.send(),传入vector。 然后我们打印vector的第一个元素。这样做是因为Rust在通过channel发送时copy了vector。 这样,如果它是可变的,就不会有竞争条件。 但是,如果我们正在启动很多task,或者我们的数据非常庞大,那么为每个任务都copy副本会使我们的内存使用量膨胀而没有任何实际好处。引入Arc。 Arc代表“原子引用计数”,它是一种在多个task之间共享不可变数据的方法。 这是一些代码:extern mod extra;use extra::arc::Arc;fn main() { let numbers = [1,2,3]; let numbers_arc = Arc::new(numbers); for num in range(0, 3) { let (port, chan) = Chan::new(); chan.send(numbers_arc.clone()); do spawn { let local_arc = port.recv(); let task_numbers = local_arc.get(); println!("{:d}", task_numbers[num]); } }}这与我们之前的代码非常相似,除了现在我们循环三次,启动三个task,并在它们之间发送一个Arc。 Arc :: new创建一个新的Arc,.clone()返回Arc的新的引用,而.get()从Arc中获取该值。 因此,我们为每个task创建一个新的引用,将该引用发送到通道,然后使用引用打印出一个数字。 现在我们不copy vector。Arcs非常适合不可变数据,但可变数据呢? 共享可变状态是并发程序的祸根。 您可以使用互斥锁(mutex)来保护共享的可变状态,但是如果您忘记获取互斥锁(mutex),则可能会发生错误。Rust为共享可变状态提供了一个工具:RWArc。 Arc的这个变种允许Arc的内容发生变异。 看看这个:extern mod extra;use extra::arc::RWArc;fn main() { let numbers = [1,2,3]; let numbers_arc = RWArc::new(numbers); for num in range(0, 3) { let (port, chan) = Chan::new(); chan.send(numbers_arc.clone()); do spawn { let local_arc = port.recv(); local_arc.write(|nums| { nums[num] += 1 }); local_arc.read(|nums| { println!("{:d}", nums[num]); }) } }}我们现在使用RWArc包来获取读/写Arc。 RWArc的API与Arc略有不同:读和写允许您读取和写入数据。 它们都将闭包作为参数,并且在写入的情况下,RWArc将获取互斥锁,然后将数据传递给此闭包。 闭包完成后,互斥锁被释放。你可以看到在不记得获取锁的情况下是不可能改变状态的。 我们获得了共享可变状态的便利,同时保持不允许共享可变状态的安全性。但等等,这怎么可能? 我们不能同时允许和禁止可变状态。 是什么赋予这个能力的?unsafe因此,Rust语言不允许共享可变状态,但我刚刚向您展示了一些允许共享可变状态的代码。 这怎么可能? 答案:unsafe你看,虽然Rust编译器非常聪明,并且可以避免你通常犯的错误,但它不是人工智能。 因为我们比编译器更聪明,有时候,我们需要克服这种安全行为。 为此,Rust有一个unsafe关键字。 在一个unsafe的代码块里,Rust关闭了许多安全检查。 如果您的程序出现问题,您只需要审核您在不安全范围内所做的事情,而不是整个程序。如果Rust的主要目标之一是安全,为什么要关闭安全? 嗯,实际上只有三个主要原因:与外部代码连接,例如将FFI写入C库,性能(在某些情况下),以及围绕通常不安全的操作提供安全抽象。 我们的Arcs是最后一个目的的一个例子。 我们可以安全地分发对Arc的多个引用,因为我们确信数据是不可变的,因此可以安全地共享。 我们可以分发对RWArc的多个引用,因为我们知道我们已经将数据包装在互斥锁中,因此可以安全地共享。 但Rust编译器无法知道我们已经做出了这些选择,所以在Arcs的实现中,我们使用不安全的块来做(通常)危险的事情。 但是我们暴露了一个安全的接口,这意味着Arcs不可能被错误地使用。这就是Rust的类型系统如何让你不会犯一些使并发编程变得困难的错误,同时也能获得像C ++等语言一样的效率。总而言之,伙计们我希望这个对Rust的尝试能让您了解Rust是否适合您。 如果这是真的,我建议您查看完整的教程,以便对Rust的语法和概念进行全面,深入的探索。 ...

February 26, 2019 · 2 min · jiezi

一些关于Rust在2019年的思考

每年,我们都会要求社区撰写有关他们希望在Rust的明年路线图中看到的内容的博客文章。 A call for Rust 2019 Roadmap blog posts这是我在2019年的Rust帖子。Rust 2021: 成熟今年也有点特别; 在2018年,我们对Rust推出了大约三年的版本时间表。 所以现在不仅是思考2019年的好时机,而且也是2020年和2021年的时候。 Rust在2015年的一些思考是关于“稳定性”的。 Rust在2018年的一些思考是关于“生产力”的。我希望Rust在2021年的一些思考能够是关于“成熟”的。为了实现这一目标,这是我们在2019年所需要的。No new features(新特性,但不是新事物)Emphasis on “new” here. What do I mean by this? Well, there are a few features that are in the pipeline that I do think should land:这里强调“新”。 这是什么意思? 好吧,我认为应该落地一些关于 pipeline 的功能:async/awaitGATsconst genericsAnd possibly(可能的特性)Specialization这些功能都不是新的; 我们已经有了他们的基本设计。 这些特征也具有重要意义和基础性; 我们需要 sync/await(或者GATs)来建立一个伟大的网络编程体系,我们需要const、泛型来获得一个优秀的数值系统。但那之后呢? 如果可以的话,我更愿意我们受限在2020年或某一年,在这之前暂停主要功能,我们已经到了一个甜蜜期。 我们总是说Rust 1.0是稳定的而不是完整的。 我想我们正在快速接近完整。也就是说,我不认为语言团队应该解散; 我认为他们的工作应该过渡到详细说明我们已有的东西。 我不确定我们是否可以在2019年完成 reference 的编写(稍后会详细介绍),但我希望它能够更进一步。 这只能在语言团队的帮助下进行,他们只有在有时间的情况下才能进行这项工作。Refine the RFC processThe RFC process needs to be significantly re-vamped. Niko wrote a great post on this back in June, and I think it’s really, really, really important. I’d like to work on an RFC to propose this, so if you’re interested, we should talk.Niko already makes the case and lays out some foundations, so I won’t say more there.Pay down organizational debtConsider this section an endorsement of everything boats said. I cannot say it better, so I will just leave it at that.Figure out documentation sustainabilityThis year was a bad year for the docs team. The book got shipped, and that was great. We had some people pitch in on the reference, and their work was amazing. Some of the other documentation writers moved on to work on rustdoc, which was important.But a lot of the goals we wanted to do about writing more docs themselves never came to fruition. For example, contributing to major ecosystem crates? Never happened. The cookbook isn’t done. Rust by Example still languishes. The standard library could use a lot of love.We just don’t have the people to make this happen. We’ve tried, but nothing has worked. It’s possible that this is simply unfixable, after all, most programmers don’t like writing docs. But I also don’t want to give up on it either. I’m not sure what to do here, but I do know that it is a major problem.ConclusionThere’s a lot of work to do, and I’m very excited to do it. I think Rust is in an excellent place, and with some work, we can make Rust even more amazing in a year’s time. ...

February 24, 2019 · 2 min · jiezi

亚洲第一届 Rust 大会将于 4 月 20 日在 [北京] 开启

RustCon Asia 来了!由秘猿科技与 PingCAP 联合主办,亚洲第一届 Rust 大会将于 4 月 20 日在中国北京开启。大会为期 4 天,包括 20 日全天和 21 日上午的主题演讲以及 22-23 日的多个主题 workshop 环节。其中主题演讲讲师来自于国内外资深 Rust 开发者和社区活跃贡献者;workshop 主题将覆盖到 Rust 开发入门和成熟技术栈或产品的实战操作和演示。关于 RustCon Asia我们受欧洲 RustFest、美国东部 Rust Belt Rust、俄罗斯 RustRush、美国西部 RustConf 和拉美 Rust LatAm 的影响和激励,开启亚洲的第一场 Rust 大会,并期望 RustCon Asia 未来能够周期性持续举办,连接亚洲的 Rust 的开发者与全球的 Rust 社区,相互扶持,共同布道 Rust 开发语言。在亚洲,我们已有不少 Rust 开发的优秀案例。一些 Rust 项目已经在生产环境中使用多年,包括中国的银行核心系统、信任链、分布式系统、网络和云服务基础设施等。我们选择北京作为 RustCon Asia 的第一站,首先因为我们的组织者秘猿科技和 PingCAP 都来自中国;其次也因为我们对中国的开发者和开发社区文化特别熟悉。秘猿科技和 PingCAP 都非常重视开发者社区,除了产品本身的号召力之外,核心团队的开发者也在各种开发者社区特别活跃,持续贡献技术知识和组织多种开发者活动。未来,我们将 RustCon Asia 推进到亚洲的其他国家,更好的促进当地社区与全球社区的合作和互助。RustCon Asia 目前已开启讲师席位,欢迎关注官网信息,并通过 CFP 提交您的议题信息,支持中英文双语。会议其它细节我们还在逐步确定,请随时关注我们的动态。此次大会期望能够满足你的以下期待:与国内社区的老友面基,与国际社区的开发者见面;中英文主题演讲,并有双向同传支持;实操 workshop(无同传);涵盖从新人友好到高级的技术内容;匿名议题提交和筛选,以便将最优秀内容呈现给大家;与生产环境中使用 Rust 的项目成员交流;开放、温馨的氛围;有机会与新、老朋友一起探索北京城!讲师席位和研讨会席位还在接受报名中,请于官网 CFP 处提交(支持中英文双语):https://cfp.rustcon.asia/events/rustcon-asia大会官网:https://rustcon.asia/中文直达:http://www.huodongxing.com/event/6479456003900Twitter @RustConAsia合作咨询:aimee@cryptape.com关于秘猿科技杭州秘猿科技有限公司(Cryptape Co.,Ltd.)的使命是用技术创造信任,为加密经济提供基础设施和服务。公司成立于 2016 年 ,核心团队从 2011 年开始参与或主导各种区块链项目,实践经验丰富。秘猿科技具备深厚的区块链技术研发和工程实力,核心技术人员均有超过 10 年以上开发经验。公司完全自主研发了区块链基础平台 CITA,并于 2017 年开源,其创新的架构设计解决了区块链底层扩展性问题。关于 PingCAPPingCAP 是一家开源的新型分布式数据库公司,秉承开源是基础软件的未来这一理念,PingCAP 持续扩大社区影响力,致力于前沿技术领域的创新实现。其研发的分布式关系型数据库 TiDB 项目,具备「分布式强一致性事务、在线弹性水平扩展、故障自恢复的高可用、跨数据中心多活」等核心特性,是大数据时代理想的数据库集群和云数据库解决方案。 ...

February 22, 2019 · 1 min · jiezi

第一届 RustCon Asia 来啦!

RustCon Asia 来了!由 秘猿科技 与 PingCAP 联合主办,亚洲第一届 Rust 大会将 于 4 月 20 日在中国·北京 正式开启。本次大会为期 4 天,包括 20 日全天和 21 日上午的主题演讲以及 22-23 日的多个主题 workshop 环节。其中主题演讲讲师来自于国内外资深 Rust 开发者和社区活跃贡献者;workshop 主题将覆盖到 Rust 开发入门和成熟技术栈或产品的实战操作和演示。关于 RustCon AsiaRust 爱好者已经遍布全球,在欧美等地已经有不同规模的开发者大会。但是在亚洲地区还是空白。我们受欧洲 RustFest、美国东部 Rust Belt Rust、俄罗斯 RustRush、美国西部 RustConf 和拉美 Rust LatAm 的影响和激励,开启亚洲的第一场 Rust 大会,并期望 RustCon Asia 未来能够周期性持续举办, 连接亚洲的 Rust 的开发者与全球的 Rust 社区,相互扶持,共同布道 Rust 开发语言。在亚洲,我们已有不少 Rust 开发的优秀案例。一些 Rust 项目已经在生产环境中使用多年,包括中国的银行核心系统、信任链、分布式系统、网络和云服务基础设施等。我们选择北京作为 RustCon Asia 的第一站,首先因为我们的组织者秘猿科技和 PingCAP 都来自中国;其次也因为我们对中国的开发者和开发社区文化特别熟悉。秘猿科技和 PingCAP 都非常重视开发者社区,除了产品本身的号召力之外,核心团队的开发者也在各种开发者社区特别活跃,持续贡献技术知识和组织多种开发者活动。未来,我们将 RustCon Asia 推进到亚洲的其他国家,更好的促进当地社区与全球社区的合作和互助。RustCon Asia 目前已开启讲师席位,欢迎关注官网信息,并通过 CFP 提交您的议题信息,支持中英文双语。会议其它细节我们还在逐步确定,请随时关注我们的动态。此次大会期望能够满足你的以下期待:与国内社区的老友面基,与国际社区的开发者见面;中英文主题演讲,并有双向同传支持;实操 workshop(无同传);涵盖从新人友好到高级的技术内容;匿名议题提交和筛选,以便将最优秀内容呈现给大家;与生产环境中使用 Rust 的项目成员交流;开放、温馨的氛围;有机会与新、老朋友一起探索北京城!讲师席位和研讨会席位还在接受报名中,请于官网 CFP 处提交( https://cfp.rustcon.asia/even… ),支持中英文双语。大会官网:https://rustcon.asia/中文直达:http://www.huodongxing.com/ev…Twitter @RustConAsia合作咨询:aimee@cryptape.com关于秘猿科技杭州秘猿科技有限公司(Cryptape Co.,Ltd.)的使命是用技术创造信任,为加密经济提供基础设施和服务。公司成立于 2016 年 ,核心团队从 2011 年开始参与或主导各种区块链项目,实践经验丰富。秘猿科技具备深厚的区块链技术研发和工程实力,核心技术人员均有超过 10 年以上开发经验。公司完全自主研发了区块链基础平台 CITA,并于 2017 年开源,其创新的架构设计解决了区块链底层扩展性问题。关于 PingCAPPingCAP 是一家开源的新型分布式数据库公司,秉承开源是基础软件的未来这一理念,PingCAP 持续扩大社区影响力,致力于前沿技术领域的创新实现。其研发的分布式关系型数据库 TiDB 项目,具备「分布式强一致性事务、在线弹性水平扩展、故障自恢复的高可用、跨数据中心多活」等核心特性,是大数据时代理想的数据库集群和云数据库解决方案。 ...

February 20, 2019 · 1 min · jiezi

TiKV 源码解析系列文章(二)raft-rs proposal 示例情景分析

作者:屈鹏本文为 TiKV 源码解析系列的第二篇,按照计划首先将为大家介绍 TiKV 依赖的周边库 raft-rs 。raft-rs 是 Raft 算法的 Rust 语言实现。Raft 是分布式领域中应用非常广泛的一种共识算法,相比于此类算法的鼻祖 Paxos,具有更简单、更容易理解和实现的特点。分布式系统的共识算法会将数据的写入复制到多个副本,从而在网络隔离或节点失败的时候仍然提供可用性。具体到 Raft 算法中,发起一个读写请求称为一次 proposal。本文将以 raft-rs 的公共 API 作为切入点,介绍一般 proposal 过程的实现原理,让用户可以深刻理解并掌握 raft-rs API 的使用, 以便用户开发自己的分布式应用,或者优化、定制 TiKV。文中引用的代码片段的完整实现可以参见 raft-rs 仓库中的 source-code 分支。Public API 简述仓库中的 examples/five_mem_node/main.rs 文件是一个包含了主要 API 用法的简单示例。它创建了一个 5 节点的 Raft 系统,并进行了 100 个 proposal 的请求和提交。经过进一步精简之后,主要的类型封装和运行逻辑如下:struct Node { // 持有一个 RawNode 实例 raft_group: Option<RawNode<MemStorage>>, // 接收其他节点发来的 Raft 消息 my_mailbox: Receiver<Message>, // 发送 Raft 消息给其他节点 mailboxes: HashMap<u64, Sender<Message>>,}let mut t = Instant::now();// 在 Node 实例上运行一个循环,周期性地处理 Raft 消息、tick 和 Ready。loop { thread::sleep(Duration::from_millis(10)); while let Ok(msg) = node.my_mailbox.try_recv() { // 处理收到的 Raft 消息 node.step(msg); } let raft_group = match node.raft_group.as_mut().unwrap(); if t.elapsed() >= Duration::from_millis(100) { raft_group.tick(); t = Instant::now(); } // 处理 Raft 产生的 Ready,并将处理进度更新回 Raft 中 let mut ready = raft_group.ready(); persist(ready.entries()); // 处理刚刚收到的 Raft Log send_all(ready.messages); // 将 Raft 产生的消息发送给其他节点 handle_committed_entries(ready.committed_entries.take()); raft_group.advance(ready);}这段代码中值得注意的地方是:RawNode 是 raft-rs 库与应用交互的主要界面。要在自己的应用中使用 raft-rs,首先就需要持有一个 RawNode 实例,正如 Node 结构体所做的那样。RawNode 的范型参数是一个满足 Storage 约束的类型,可以认为是一个存储了 Raft Log 的存储引擎,示例中使用的是 MemStorage。在收到 Raft 消息之后,调用 RawNode::step 方法来处理这条消息。每隔一段时间(称为一个 tick),调用 RawNode::tick 方法使 Raft 的逻辑时钟前进一步。使用 RawNode::ready 接口从 Raft 中获取收到的最新日志(Ready::entries),已经提交的日志(Ready::committed_entries),以及需要发送给其他节点的消息等内容。在确保一个 Ready 中的所有进度被正确处理完成之后,调用 RawNode::advance 接口。接下来的几节将展开详细描述。Storage traitRaft 算法中的日志复制部分抽象了一个可以不断追加写入新日志的持久化数组,这一数组在 raft-rs 中即对应 Storage。使用一个表格可以直观地展示这个 trait 的各个方法分别可以从这个持久化数组中获取哪些信息:方法描述initial_state获取这个 Raft 节点的初始化信息,比如 Raft group 中都有哪些成员等。这个方法在应用程序启动时会用到。entries给定一个范围,获取这个范围内持久化之后的 Raft Log。term给定一个日志的下标,查看这个位置的日志的 term。first_index由于数组中陈旧的日志会被清理掉,这个方法会返回数组中未被清理掉的最小的位置。last_index返回数组中最后一条日志的位置。snapshot返回一个 Snapshot,以便发送给日志落后过多的 Follower。值得注意的是,这个 Storage 中并不包括持久化 Raft Log,也不会将 Raft Log 应用到应用程序自己的状态机的接口。这些内容需要应用程序自行处理。RawNode::step 接口这个接口处理从该 Raft group 中其他节点收到的消息。比如,当 Follower 收到 Leader 发来的日志时,需要把日志存储起来并回复相应的 ACK;或者当节点收到 term 更高的选举消息时,应该进入选举状态并回复自己的投票。这个接口和它调用的子函数的详细逻辑几乎涵盖了 Raft 协议的全部内容,代码较多,因此这里仅阐述在 Leader 上发生的日志复制过程。当应用程序希望向 Raft 系统提交一个写入时,需要在 Leader 上调用 RawNode::propose 方法,后者就会调用 RawNode::step,而参数是一个类型为 MessageType::MsgPropose 的消息;应用程序要写入的内容被封装到了这个消息中。对于这一消息类型,后续会调用 Raft::step_leader 函数,将这个消息作为一个 Raft Log 暂存起来,同时广播到 Follower 的信箱中。到这一步,propose 的过程就可以返回了,注意,此时这个 Raft Log 并没有持久化,同时广播给 Follower 的 MsgAppend 消息也并未真正发出去。应用程序需要设法将这个写入挂起,等到从 Raft 中获知这个写入已经被集群中的过半成员确认之后,再向这个写入的发起者返回写入成功的响应。那么, 如何能够让 Raft 把消息真正发出去,并接收 Follower 的确认呢?RawNode::ready 和 RawNode::advance 接口这个接口返回一个 Ready 结构体:pub struct Ready { pub committed_entries: Option<Vec<Entry>>, pub messages: Vec<Message>, // some other fields…}impl Ready { pub fn entries(&self) -> &[Entry] { &self.entries } // some other methods…}一些暂时无关的字段和方法已经略去,在 propose 过程中主要用到的方法和字段分别是:方法/字段作用entries(方法)取出上一步发到 Raft 中,但尚未持久化的 Raft Log。committed_entries取出已经持久化,并经过集群确认的 Raft Log。messages取出 Raft 产生的消息,以便真正发给其他节点。对照 examples/five_mem_node/main.rs 中的示例,可以知道应用程序在 propose 一个消息之后,应该调用 RawNode::ready 并在返回的 Ready 上继续进行处理:包括持久化 Raft Log,将 Raft 消息发送到网络上等。而在 Follower 上,也不断运行着示例代码中与 Leader 相同的循环:接收 Raft 消息,从 Ready 中收集回复并发回给 Leader……对于 propose 过程而言,当 Leader 收到了足够的确认这一 Raft Log 的回复,便能够认为这一 Raft Log 已经被确认了,这一逻辑体现在 Raft::handle_append_response 之后的 Raft::maybe_commit 方法中。在下一次这个 Raft 节点调用 RawNode::ready 时,便可以取出这部分被确认的消息,并应用到状态机中了。在将一个 Ready 结构体中的内容处理完成之后,应用程序即可调用这个方法更新 Raft 中的一些进度,包括 last index、commit index 和 apply index 等。RawNode::tick 接口这是本文最后要介绍的一个接口,它的作用是驱动 Raft 内部的逻辑时钟前进,并对超时进行处理。比如对于 Follower 而言,如果它在 tick 的时候发现 Leader 已经失联很久了,便会发起一次选举;而 Leader 为了避免自己被取代,也会在一个更短的超时之后给 Follower 发送心跳。值得注意的是,tick 也是会产生 Raft 消息的,为了使这部分 Raft 消息能够及时发送出去,在应用程序的每一轮循环中一般应该先处理 tick,然后处理 Ready,正如示例程序中所做的那样。总结最后用一张图展示在 Leader 上是通过哪些 API 进行 propose 的:本期关于 raft-rs 的源码解析就到此结束了,我们非常鼓励大家在自己的分布式应用中尝试 raft-rs 这个库,同时提出宝贵的意见和建议。后续关于 raft-rs 我们还会深入介绍 Configuration Change 和 Snapshot 的实现与优化等内容,展示更深入的设计原理、更详细的优化细节,方便大家分析定位 raft-rs 和 TiKV 使用中的潜在问题。 ...

February 15, 2019 · 2 min · jiezi

Rust中文社刊2019-01发布

欢迎来到Rust中文社刊,欢迎加入Rust中文社区,共建Rust语言中文网络!. 本文同步于Rust中文社刊2019-01Rust中文社区新年快乐!Rust中文社区主页已经焕然一新,追随Rust官方的Modern风格,刚过去的1月,有37位成员加入 rustlang-cn 的github组织,并开始贡献!恭喜!!特别感谢VitalyAnkh为论坛上线提供服务器1-阅读Rust: Dedicated GC threadRust内存分配器的不同行为2-文档增加官方Rust书-中文版页面与内容增加教程中文版《Rust编程语言》增加教程中文版 《Rust高级编程》同步更新完成教程中文版《Rust异步编程》增加并开始翻译中文版《Cargo教程》增加Why Rust页面与内容增加命令行页面与内容增加WebAssembly页面与内容增加网络页面与内容增加嵌入式页面与内容增加客户端页面与内容增加游戏页面与内容3-生态增加Rust生态库-中文版页面与内容增加Actix-net页面与内容增加并翻译完成Riker框架教程增加并翻译部分Serde框架教程同步更新完成Tokio框架教程4-网络更新完善Awesome栏目,目前以包括3个分类:Awesome-RustAwesome-TokioAwesome-Actix更新完善Crates栏目更新完善Rust资源栏目5-论坛特别感谢VitalyAnkh为论坛上线提供服务器Rust中文社区论坛重新上线,积极为社区服务,欢迎社区功能: 目前三大功能论坛:板块有:最美/博客/分享/问答/招聘/未回复 (论坛具有扩展性模块支持一键添加)博客:博客具有独立页面展示,具有收藏/喜欢属性,具有强大的热榜功能,最美模块分别根据最近一段时间内收藏量排行和全站收藏量2个排行榜,同时侧边栏根据收藏量展示最美的人排行榜文档:文档功能是具有自定义的html页面,可以不断添加海量wiki信息.其他:可视化与markdown二合一编辑器丰富的个人中心6-ithubrustlang-cn 接受到11位成员的300多commit,Rust中文文档越来越丰富,欢迎参与!!Rust-webapp-starter更新到Rust2018ruster以更新且论坛也已上线运行resourses更新添加了Rust基础知识图片大全7-感谢参与贡献者!(按字母排序)angelrain1g0ne150gengtenggmg137kobeHubkrirccluoverpeng1999SwoorupVitalyAnkh

February 11, 2019 · 1 min · jiezi

使用Rust + Electron开发跨平台桌面应用 ( 二 )

前言在上一篇文章使用Rust + Electron开发跨平台桌面应用 ( 一 )中,我们将Rust + Electron结合起来,使用Rust编写核心业务逻辑,并编译成node库提供给Electron的UI界面调用,但是在上一篇文章中发现遇到了很多问题,尤其是Electron 的版本和 Rust编译出来的版本必须要一致,否则会无法调用成功,这就很坑了,所以为了改变这一情况,今天我们将使用另一种方式将Rust的代码提供给Js进行调用,这就是FFI。FFI是什么FFI(Foreign Function Interface)是用来与其它语言交互的接口,由于现实中很多程序是由不同编程语言写的,必然会涉及到跨语言调用,这时一般有两种解决方案:1、将函数做成一个服务,通过进程间通信(IPC)或网络协议通信(RPC, RESTful等);2、直接通过 FFI 调用。前者需要至少两个独立的进程才能实现,而后者直接将其它语言的接口内嵌到本语言中,所以调用效率比前者高。Rust作为系统级编程语言,也是对FFI提供了完善的支持。mangle由于rust支持重载,所以函数名会被编译器进行混淆,就像c++一样。因此当你的函数被编译完毕后,函数名会带上一串表明函数签名的字符串。这样的函数名为ffi调用带来了困难,因此,rust提供了#[no_mangle]属性为函数修饰。 对于带有#[no_mangle]属性的函数,rust编译器不会为它进行函数名混淆, 如:#[no_mangle]pub extern fn test() {}下面我们来编写一个thread_count.rs,其实跟寻常的rust代码没有什么区别:#[no_mangle]pub extern fn threadcount(x: i32) -> i32 { let result: i32 = num_cpus::get() as i32; return result * x;}指定库类型rust默认编译成rust自用的rlib格式库,要让rust编译成动态链接库或者静态链接库,需要显示指定,一共有三种方式,我这里采用的是直接在Cargo.Toml文件中指定,如下:[lib]name = “thread_count"crate-type = [“dylib”]需要注意的是name,必须符合rust的包结构,能够在src目录下找到。我们执行cargo build命令,可以看到,在/target/debug目录下生成了我们需要的文件libthread_count.dylibJS使用rust的动态链接库那么我们要如何在JS中调用rust生成dylib呢?答案就是ffi-napi,我们使用ffi-napi这个包来在js中调用ffi,话不多说,直接看代码let ffi = require(‘ffi-napi’);let path = require(‘path’);let threadCount = ffi.Library(path.join(__dirname, ‘./target/debug/libthread_count’), { threadcount: [‘int’, [‘int’]]});let result = threadCount.threadcount(12);console.log(“thead_count: " + result);结果如下:好了,到此为止,我们就成功的将rust编译成动态链接库给JS调用了,这种方式是我觉得比较好的一种方式,虽然引入函数的方式比较丑,但是我们不用担心node版本的问题。结语虽然FFI是一种我认为比较好的方式,但是它也不是完美无缺的,例如,在跨越FFI的过程中,我们会丢失rust的类型信息,从而引发安全性问题,当然这也不是没有解决办法,我们可以使用rust的Box来包装我们的类型,这个可以单独开一篇文章来讲述,就不展开了(先挖个坑,哪天想起来再填)

January 31, 2019 · 1 min · jiezi

使用Rust + Electron开发跨平台桌面应用 ( 一 )

前言近段时间学习了Rust,一直想着做点什么东西深入学习,因为是刚学习,很多地方都不熟悉,所以也就不能拿它来做编译器这些,至于web开发,实际上我并不建议拿这个来学习一门语言,大概有几个方面,一是web开发的套路无非也就那么几个,对学习一门语言并不会有多大的帮助。二是web开发大多已经被封装了很多东西,对学习语言本身其实不利,真的要深入学习的话还是建议从语言本身出发,尽量不要用封装好的东西,当然,标准库除外。为什么是Rust + Electron原因其实很简单,我不想做太复杂的东西,因为大部分的精力还是要放在工作上,其次是希望做一个我日常能用的东西,当然现在还没想好,可能是个音乐播放器,也可能是个天气展示的app,这样我就可以每天使用了,这也会更有动力促使我开发好它。Rust 和 Electron 想必就不用我多介绍了吧,至于为什么是这个组合可以查看知乎的这个问题,我赞同的是的方案是使用 C/Cpp/Rust 开发的核心 + Electron / Qt 开发界面本期目标本期的目标非常简单,将Rust 和 Electron结合起来,使用Rust获取电脑cpu核数,Electron将数据绘制在界面上展示。初始化Electron项目Electron项目的初始化我用的工具是electron-forge,首先我们按照electron-forge的官网介绍来npm install -g electron-forgeelectron-forge init my-new-projectcd my-new-projectelectron-forge start解释一下,首先我们要安装electron-forge,这是一个脚手架工具,类似于Vue-cli。然后我们初始化一个项目,项目名称为my-new-project。需要注意的是这初始化的过程中electron-forge会构建package.json, 然后下载依赖,我第一次下载依赖的时候卡在了electron-runtime,第二次重试的时候就好了。第二个是electron-forge中的依赖会对Python版本有要求,只能要求Python2,这里要注意的一点是,我十分不建议使用pyenv来控制Python版本,会出现以下错误,我的解决方式是使用virtualenv新建一个Python2 的环境。Fatal Python error: PyThreadState_Get: no current thread现在我们来看一下项目结构整个项目结构非常简单,src中是我们的源文件,index.html是界面文件,index.js是界面逻辑文件,大家打开index.js就可以看到一段自动生成的代码,主要是创建了一个app,以及监听app的活动,需要注意到的是其中对mac的处理。app.on(‘window-all-closed’, () => { // On OS X it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== ‘darwin’) { app.quit(); }});好了,现在让我们把项目跑起来,在项目目录下执行electron-forge start命令,稍等一会我们就可以看到界面运行起来了初始化Rust项目在开发之前我们要知道,JS是无法直接运行Rust的,就像JS无法直接运行C++一样。所以我们需要将Rust打包成Node模块提供给JS进行调用。所以我们会使用neon来做到这件事,neon的github地址在这里首先我们需要安装neon,注意,neon对python版本也是有要求的,如果你是mac,python版本必须要是Python2.7,不支持Python3,同样,这里也会出现上面说过的no current thread问题,所以我们在开发时最好用virtualenv新建一个Python2的环境。安装完neon之后我们执行neon new thread-count,新建一个项目。看一下项目结构lib是我们最终的导出文件,提供给electron进行调用,native下则是我们的rust代码,注意,这里的入口文件是native/src/lib.rs,因为我们建立的是一个库而不是一个可执行的应用程序。让我们先编译项目,在文件目录下执行neon build –release命令。让我们进入终端调用一下项目试试:成功!到现在我们就成功的将rust写的代码封装成node库,使得JS可以进行调用了,接下来我们回到上面说过的,将rust的功能更改为获取CPU核数,然后将它封装成一个函数并进行导出。首先我们要修改Cargo.toml,在[dependencies]下增加一个num_cpus = “1.4.0"的依赖项,然后修改native/src/lib.rs文件如下#[macro_use]extern crate neon;use neon::prelude::*;fn thread_count(mut cx: FunctionContext) -> JsResult<JsNumber> { Ok(cx.number(num_cpus::get() as f64))}register_module!(mut cx, { cx.export_function(“thread_count”, thread_count)});修改lib/index.js如下:var addon = require(’../native’);module.exports = addon.thread_count;然后我们再进行编译,执行neon build –release命令,然后再进入终端调用这个函数试试成功啦,至此,我们就成功的将rust代码封装给JS进行了调用。需要注意的是编译rust的node版本需要与运行electron的node版本一致,否则会出现无法调用的情况。好了,到此第一期就结束了,代码我会抽空整理到github,以供有需要的同学查看。最后看一下效果图吧ps: 现在Rust的各项工具和库都不是很成熟,所以大家再实践过程中会遇到各种问题,都可以评论到下面大家一起讨论。 ...

January 30, 2019 · 1 min · jiezi

TiKV 源码解析系列文章(一)序

作者:唐刘TiKV 是一个支持事务的分布式 Key-Value 数据库,有很多社区开发者基于 TiKV 来开发自己的应用,譬如 titan、tidis。尤其是在 TiKV 成为 CNCF 的 Sandbox 项目之后,吸引了越来越多开发者的目光,很多同学都想参与到 TiKV 的研发中来。这时候,就会遇到两个比较大的拦路虎:Rust 语言:众所周知,TiKV 是使用 Rust 语言来进行开发的,而 Rust 语言的学习难度相对较高,有些人认为其学习曲线大于 C++,所以很多同学在这一步就直接放弃了。文档:最开始 TiKV 是作为 HTAP 数据库 TiDB 的一个底层存储引擎设计并开发出来的,属于内部系统,缺乏详细的文档,以至于同学们不知道 TiKV 是怎么设计的,以及代码为什么要这么写。对于第一个问题,我们内部正在制作一系列的 Rust 培训课程,由 Rust 作者以及 Rust 社区知名的开发者亲自操刀,预计会在今年第一季度对外发布。希望通过该课程的学习,大家能快速入门 Rust,使用 Rust 开发自己的应用。而对于第二个问题,我们会启动 《TiKV 源码解析系列文章》以及 《Deep Dive TiKV 系列文章》计划,在《Deep Dive TiKV 系列文章》中,我们会详细介绍与解释 TiKV 所使用技术的基本原理,譬如 Raft 协议的说明,以及我们是如何对 Raft 做扩展和优化的。而 《TiKV 源码解析系列文章》则是会从源码层面给大家抽丝剥茧,让大家知道我们内部到底是如何实现的。我们希望,通过这两个系列,能让大家对 TiKV 有更深刻的理解,再加上 Rust 培训,能让大家很好的参与到 TiKV 的开发中来。结构本篇文章是《TiKV 源码解析系列文章》的序篇,会简单的给大家讲一下 TiKV 的基本模块,让大家对这个系统有一个整体的了解。要理解 TiKV,只是了解 https://github.com/tikv/tikv 这一个项目是远远不够的,通常,我们也需要了解很多其他的项目,包括但不限于:https://github.com/pingcap/raft-rshttps://github.com/pingcap/rust-prometheushttps://github.com/pingcap/rust-rocksdbhttps://github.com/pingcap/fail-rshttps://github.com/pingcap/rocksdbhttps://github.com/pingcap/grpc-rshttps://github.com/pingcap/pd在这个系列里面,我们首先会从 TiKV 使用的周边库开始介绍,然后介绍 TiKV,最后会介绍 PD。下面简单来说下我们的一些介绍计划。Storage EngineTiKV 现在使用 RocksDB 作为底层数据存储方案。在 pingcap/rust-rocksdb 这个库里面,我们会简单说明 Rust 是如何通过 Foreign Function Interface (FFI) 来跟 C library 进行交互,以及我们是如何将 RocksDB 的 C API 封装好给 Rust 使用的。另外,在 pingcap/rocksdb 这个库里面,我们会详细的介绍我们自己研发的 Key-Value 分离引擎 - Titan,同时也会让大家知道如何使用 RocksDB 对外提供的接口来构建自己的 engine。RaftTiKV 使用的是 Raft 一致性协议。为了保证算法的正确性,我们直接将 etcd 的 Go 实现 port 成了 Rust。在 pingcap/raft-rs,我们会详细介绍 Raft 的选举,Log 复制,snapshot 这些基本的功能是如何实现的。另外,我们还会介绍对 Raft 的一些优化,譬如 pre-vote,check quorum 机制,batch 以及 pipeline。最后,我们会说明如何去使用这个 Raft 库,这样大家就能在自己的应用里面集成 Raft 了。gRPCTiKV 使用的是 gRPC 作为通讯框架,我们直接把 Google C gRPC 库封装在 grpc-rs 这个库里面。我们会详细告诉大家如何去封装和操作 C gRPC 库,启动一个 gRPC 服务。另外,我们还会介绍如何使用 Rust 的 futures-rs 来将异步逻辑变成类似同步的方式来处理,以及如何通过解析 protobuf 文件来生成对应的 API 代码。最后,我们会介绍如何基于该库构建一个简单的 gRPC 服务。PrometheusTiKV 使用 Prometheus 作为其监控系统, rust-prometheus 这个库是 Prometheus 的 Rust client。在这个库里面,我们会介绍如果支持不同的 Prometheus 的数据类型(Coutner,Gauge,Historgram)。另外,我们会重点介绍我们是如何通过使用 Rust 的 Macro 来支持 Prometheus 的 Vector metrics 的。最后,我们会介绍如何在自己的项目里面集成 Prometheus client,将自己的 metrics 存到 Prometheus 里面,方便后续分析。FailFail 是一个错误注入的库。通过这个库,我们能很方便的在代码的某些地方加上 hook,注入错误,然后在系统运行的时候触发相关的错误,看系统是否稳定。我们会详细的介绍 Fail 是如何通过 macro 来注入错误,会告诉大家如何添加自己的 hook,以及在外面进行触发TiKVTiKV 是一个非常复杂的系统,这块我们会重点介绍,主要包括:Raftstore,该模块里面我们会介绍 TiKV 如何使用 Raft,如何支持 Multi-Raft。Storage,该模块里面我们会介绍 Multiversion concurrency control (MVCC),基于 Percolator 的分布式事务的实现,数据在 engine 里面的存储方式,engine 操作相关的 API 等。Server,该模块我们会介绍 TiKV 的 gRPC API,以及不同函数执行流程。Coprocessor,该模块我们会详细介绍 TiKV 是如何处理 TiDB 的下推请求的,如何通过不同的表达式进行数据读取以及计算的。PD,该模块我们会介绍 TiKV 是如何跟 PD 进行交互的。Import,该模块我们会介绍 TiKV 如何处理大量数据的导入,以及如何跟 TiDB 数据导入工具 lightning 交互的。Util,该模块我们会介绍一些 TiKV 使用的基本功能库。PDPD 用来负责整个 TiKV 的调度,我们会详细的介绍 PD 内部是如何使用 etcd 来进行元数据存取和高可用支持,也会介绍 PD 如何跟 TiKV 交互,如何生成全局的 ID 以及 timestamp。最后,我们会详细的介绍 PD 提供的 scheduler,以及不同的 scheudler 所负责的事情,让大家能通过配置 scheduler 来让系统更加的稳定。小结上面简单的介绍了源码解析涉及的模块,还有一些模块譬如 https://github.com/tikv/client-rust 仍在开发中,等完成之后我们也会进行源码解析。我们希望通过该源码解析系列,能让大家对 TiKV 有一个更深刻的理解。当然,TiKV 的源码也是一直在不停的演化,我们也会尽量保证文档的及时更新。最后,欢迎大家参与 TiKV 的开发。 ...

January 28, 2019 · 2 min · jiezi

【RUST官方语言中文翻译】前言

前言尽管不能100%的确定,但Rust从基本上来说是一种让人如虎添翼的语言。不论你现在用什么语言进行编码,rust都能让你达到更高的高度。跟之前相比,能自信的在更宽广的领域进行编程。举个例子,一些“系统级别”的工作,比如对底层的内存管理,数据表达和并发,传统上来说,这些编程领域是个神秘地带,只有那些在这些领域投入了多年经历的人员才能避免一些常见的陷阱。甚至说,哪怕是这些人员小心翼翼的处理上述问题,也难免他们的代码没有漏洞,容易崩溃并且不易维护。通过消除老式的陷阱以及提供一个更友好的,简易的工具集,Rust排除了这些障碍,以此让你远离了这些问题。对于那些需要深入了解底层的程序员,他们不需要处理崩溃或者安全漏洞带来的常见风险。也不需要去学习那些经常更新的工具链路。更棒的是,这门语言就是被设计出用来指导人们更自然的构建可信赖的代码,且保持运行速度和内存使用习惯。对于那些经常处理底层代码的程序员,Rust将会扩大他们的舞台。举个例子,在Rust中引入并发会是一个相对低风险的操作,因为编译器将会替你捕获那些常见的错误,你也可以放心的代码里引进更激进的优化手段,因为你会更有信心相信自己不会引入漏洞或者崩溃。Rust也不会局限于底层系统编程。它有足够的表达能力以及友好程度去让入很舒服的变形命令行界面应用,网络服务器和很多其他类型的代码。本书稍后的内容中就会有很大案例。编写Rust,会让你从一个领域到另外一个领域的能力。你可以学习用Rust编写一个网络应用,然后移植学到的经验到树莓派中。这本书也适合那些潜在的Rust用户。里面会用简单易懂的语言去帮助你提升自己,不单单是Rust的相关知识,还包括作为一个程序员需要的方法和自信心。 准备好开始学习,欢迎来到Rust社区。

January 18, 2019 · 1 min · jiezi

Mac上Cargo编译错误: failed to run custom build command for '*.*'

编译Rust的项目时候出现了错误:error: failed to run custom build command for croaring-sys v0.3.7process didn’t exit successfully: /Users/…/grin/target/release/build/croaring-sys-20d6d5c35e3a436a/build-script-build (exit code: 101)— stdoutTARGET = Some(“x86_64-apple-darwin”)OPT_LEVEL = Some(“3”)HOST = Some(“x86_64-apple-darwin”)CC_x86_64-apple-darwin = NoneCC_x86_64_apple_darwin = NoneHOST_CC = NoneCC = NoneCFLAGS_x86_64-apple-darwin = NoneCFLAGS_x86_64_apple_darwin = NoneHOST_CFLAGS = NoneCFLAGS = NoneDEBUG = Some(“false”)running: “cc” “-O3” “-ffunction-sections” “-fdata-sections” “-fPIC” “-m64” “-Wall” “-Wextra” “-std=c11” “-march=native” “-O3” “-o” “/Users/…/grin/target/release/build/croaring-sys-4f7af44253f571e8/out/CRoaring/roaring.o” “-c” “CRoaring/roaring.c"关键的错误信息是:error: unknown type name ‘uint64_t’ cargo:warning= uint64_t ri resident_size;原因是升级了Mac系统之后 C++ .h 不正确造成的。解决的方式,是删除clang相关的编译环境,并重新安装,首先删除头文件:rm -rf /usr/local/include/*再卸载LLVM相关的工具链brew uninstall llvm最后需要卸载掉Xcode命令行工具:rm -rf /Library/Developer/CommandLineTools卸载掉clang相关工具之后,再重新安装。安装Xcode命令行工具:xcode-select –install安装llvmbrew install –with-toolchain llvm重新编译正常。 ...

January 12, 2019 · 1 min · jiezi

Rust中文社区上线了在线中文论坛

现在Rust中文社区形成了Rust中文文档中心和在线中文论坛,相辅相成致力于Rust语言中文网络!访问论坛是直接访问https://rustlang-cn.org通过导航栏论坛访问,所以不需要在多记住一个地址!同时论坛的文档即为跳转到主站所以文档中心与中文论坛相辅相成!

January 12, 2019 · 1 min · jiezi

使用IntelliJ做为Rust IDE

Rust 是一个由Mozilla主导开发的通用编译型编译语言。它的设计准则为"安全,并发,实用",支持函数式,并发式,过程式以及面向对象的编程风格。IntelliJ 是最好的使用的Java IDE之一 ,它支持各种插件,其中intellij-rust就是Rust在IntelliJ上的插件,可以使用该插件在IntelliJ上面进行Rust开发,下面就简单介绍下如何安装Rust并使用IntelliJ做为其IDE。安装Rust安装Rust不要直接Rust语言本身,例如使用brew install rust就只是安装了rust语言本身而已,应该安装的是rustup,rustup是rust官方版本的管理工具,是安装rust的首选。它的主要特点是:管理Rust二进制文件配置Rust工具链管理Rust相关组件只依赖bash,curl和常见的unix工具支持多平台在使用Rust开发过程中常常是用到的工具有rustc,rust-src,cargo,这些都可以使用rustup进行管理。其中cargo是Rust项目管理的工具,提供了一系列的工具,从项目的建立,构建到测试,运行到部署,都为Rust项目的管理提供尽可能完成的手段。rustc是rust语言的编译器。rust-src是rust标准库。安装rustup:curl https://sh.rustup.rs -sSf | sh安装过程中会让选择安装方式,使用默认方式安装即可,默认安装cargo。安装之后需要设置两个目录到PATH变量中:$HOME/.cargo/bin,cargo的bin目录$HOME/.cargo/env,为shell配置的目录通过rustup help可以看到rustup的相关命令,上述的默认按照并不包含组件rust-src的安装,需要单独安装组件rust-src:rustup component add rust-src这样Rust的环境安装都已经完成,在使用IntelliJ做为Rust的IDE中要用的组件包括:rustc,cargo和rust-src。安装IntelliJ插件需要安装两个插件 intellij-rust和intellij-toml, intellij-rust是Rust语言插件,intellij-toml是为Toml语言的插件,是为cargo的配置文件cargo.toml使用。安装方式:Perferences.. -> Plugins 在Marketplact中直接搜索Rust同样方式搜索toml并安装。安装完插件之后就可以新建一个项目选择Rust:可以看到 Toolchain location 是配置的$HOME/.cargo/bin,而Standard library是之前安装的rust-src的目录。创建项目成功可以看到一个完整的rust项目:引用维基百科:https://zh.wikipedia.org/wiki…Rust lang: https://www.rust-lang.org/intellij-rust :https://github.com/intellij-r…intellij-tom :https://github.com/intellij-r…

January 6, 2019 · 1 min · jiezi

Rust内存分配器的不同行为

本文出自Rust内存分配器的不同行为,同步于Rust中文社区专栏:Rust内存分配器的不同行为 ,本文时间:2019-01-04, 作者:Pslydhh,简介:Pslydhh欢迎加入Rust中文社区,共建Rust语言中文网络!欢迎向Rust中文社区专栏投稿,投稿地址 ,好文在以下地方直接展示, 欢迎访问Rust中文论坛,QQ群:570065685Rust中文社区首页Rust中文社区文章专栏对于如下的代码,采用nightly version:use std::sync::mpsc;use std::thread;fn main() { const STEPS: usize = 1000000; thread::sleep(std::time::Duration::from_millis(10)); let now = std::time::Instant::now(); let (tx1, rx1) = mpsc::channel(); for _ in 0..STEPS { let _ = tx1.send(1);// }// for _ in 0..STEPS { assert_eq!(rx1.try_recv().unwrap(), 1); } let elapsed = now.elapsed(); println!(“recv duration: {} secs {} nanosecs\n”, elapsed.as_secs(), elapsed.subsec_nanos()); thread::sleep(std::time::Duration::from_millis(10000));}在我的linux上,观察输出"recv duration…" 之后进程RES的占用量:如图注释:1824 kb删除注释:48540 kb直觉上来说这是令人奇怪的,因为不管先send 1000000个元素,再try_recv这1000000个元素,或者 send/recv成对操作,操作完之后进程的内存占用应该是几乎一致的。于是我提交了这个Issue:Different behaviors of allocator in nuanced snippets对方表示在删除注释的情况下,一开始就send了百万级别的对象,在进程中开辟了一块非常大的内存,于是随后的try_recv就不会回收内存了,这是一个几乎所有内存分配器都会做的优化,因为很可能你的程序随后就会再次使用那一大块内存。这么说也算是一种合理的选择吧,在我们上面这样单线程程序下没什么问题,但是在多线程的情况下呢?这种对于内存的重用能不能跨线程重用呢?毕竟假如一个线程保留了一大块内存,另一个线程又保留一大块内存,那么进程本身会不会直接被killed呢?我们来看与上述类似的一个例子:use std::sync::mpsc;use std::thread;fn main() { const STEPS: usize = 1000000; thread::sleep(std::time::Duration::from_millis(10)); let now = std::time::Instant::now(); let t = thread::spawn(|| { let t = thread::spawn(|| { let (tx1, rx1) = mpsc::channel(); for _ in 0..STEPS { let _ = tx1.send(1); } for _ in 0..STEPS { assert_eq!(rx1.try_recv().unwrap(), 1); } }); t.join().unwrap(); let (tx1, rx1) = mpsc::channel(); for _ in 0..STEPS { let _ = tx1.send(1); } for _ in 0..STEPS { assert_eq!(rx1.try_recv().unwrap(), 1); } }); t.join().unwrap(); let (tx1, rx1) = mpsc::channel(); for _ in 0..STEPS { let _ = tx1.send(1); } for _ in 0..STEPS { assert_eq!(rx1.try_recv().unwrap(), 1); } let elapsed = now.elapsed(); println!(“recv duration: {} secs {} nanosecs\n”, elapsed.as_secs(), elapsed.subsec_nanos()); thread::sleep(std::time::Duration::from_millis(10000));}观察输出"recv duration…" 之后进程RES的占用量:RES: 142364 kb差不多是前面那个例子的三倍,注意本例子启用了三个线程,也就是说,每个线程已经结束,它之前保留的内存居然还未归还给OS,即使这些内存再也不能被使用到。到这里还只是内存不能使用。按照这个方式,有没有可能在程序合理的情况下,进程直接被killed呢?我们把上面的STEPS改为50000000,在我的机器上直接被killed。如果把例子改一下,减少一个线程,那么它又能合理的运行了。对于这个问题,对方回应称 This is nothing Rust can control。也许通过修改内存分配器能够修改它的行为?好在对于后面这个例子,目前的stable版本(rustc 1.31.1)是能够回收内存的 ...

January 4, 2019 · 1 min · jiezi

智能合约开发新趋势【2019】

智能合约开发语言已经被Solidity统治了一段时间,它用于开发可以在以太坊虚拟机EVM上运行的智能合约。不过Solidity有一些严重的问题,包括算术溢出、类型错误以及曾经冻结了3亿美元的delegatecall漏洞。所有这些漏洞都是在开发语言层面存在的问题。换句话说,如果有一个好点的开发语言,本来应该可以创造更安全的智能合约。文本将列出在2019年值得关注的区块链智能合约开发技术趋势。上汇智网,用互动方式学习以太坊、比特币、EOS、tendermint等更多区块链开发教程。2019年,Solidity的挑战者终于来了。Waves RIDEWaves RIDE是一个图灵不完备(没有循环或递归)的、收Haskell启发的函数式编程语言,用于Waves区块链。它的特点包括静态类型、惰性评估、模式匹配和用于决定交易是否允许完成的断言表达式。目前图灵完备的版本也在开发中。Wave的智能合约支持目前在主网上已经激活。我们应当可以在2019年看到第一批Waves的dApp。官方地址:https://docs.wavesplatform.co…Plutus (Cardano)Plutus是另一个类Haskell的函数式编程语言,用于Cardano区块链。Cardano计划在2019年有两个大的发布:Shelley提供完全去中心化和抵押功能,而Cardano-CL则是支持可编程智能合约的虚拟机。官方地址:https://cardanodocs.com/techn…Scilla (Zilliqa)Scilla是一个认证过的智能合约开发语言,它在设计时就考虑了分离计算过程与效果,这意味着计算和状态迁移的通信是严格隔离的,这使得Scilla智能合约更容易测试,并且可以静态验证以最小化发生错误的机会。Zilliqa的主网计划在2019年1月底上线。官方地址:https://scilla-lang.org/ewasm (Ethereum)ewasm不是一个智能合约开发语言,而是一个编译器的生成目标,它允许以太坊开发者使用其他语言(例如Rust、C++等)开发智能合约并编译为以太坊接受的WebAssembly。ewasm是WebAssembly的一个安全子集,它是web平台上相对新出现的编译目标。方便的是,wams(以及ewasm)模块可以在任何JavaScript项目中使用。对于大多数区块链代码来说,通常75%以上的代码根本都不是智能合约 —— 而是使用JavaScript与智能合约进行通信的代码。ewasm和JavaScript使用同样的绑定和模块支持机制。官方地址:https://github.com/ewasm/designJavaScript (Lisk)List是一个区块链开发平台,它支持开发者使用JavaScript为特定应用开发并创建定制区块链,从而避免了以太坊的扩容问题。List允许开发者创建自己的侧链来管理所有与特定应用的区块链操作,因此它不需要与其他应用竞争主链上的计算资源。目前List没有开发自己的智能合约编程语言或者VM,其交易能力类似于比特币。官方地址:https://lisk.io/Rust (via ewasm, Cardano client)Rust是一个类似C的底层开发语言,包含一些类似Haskel的安全特性,例如得到保证的常量引用以避免意外修改、静态阻止空指针异常、有状态类型只允许访问当前状态下的有效操作、模式匹配分析以保证函数完整性(一个不匹配的模式将导致编译时错误)…基本上Rust类似于C++和Haskell的纯优点的继承者。Rust可以编译为ewasm,或者用于构建区块链的客户端代码,例如Cardano。List的模块也可以使用Rust开发,然后编译为wasm导入到List项目中。官方地址:https://www.rust-lang.org/汇智网翻译整理,转载请标明出处。

January 3, 2019 · 1 min · jiezi

Rust异步编程 一

本文同步于Rust中文社区 ,本文时间:2018-12-20, 作者:Rust中文社区,简介:Rust中文社区欢迎向Rust中文社区投稿,投稿地址 ,好文将在以下地方直接展示Rust中文社区首页Rust中文社区文章专栏知乎专栏Rust中文社区思否专栏Rust中文社区简书专题Rust中文社区微博Rustlang-cn欢迎来到 Rust 异步编程!如果你正打算用 Rust 写一些异步代码,那你就来对地方了。不管你打算构建Web服务器,数据库,还是操作系统,这本书都能帮助你运用 Rust 的异步编程工具最大化利用你的硬件。开始几章主要介绍异步编程,以及在 Rust 中的特别之处。中间几章讨论用于异步编程的关键的基础设施和控制流工具,并详细介绍了一些构建类库、应用时将性能和复用性最大化的最佳实践。本书的最后章节介绍了 Rust 的异步生态并提供了一些实现常见功能的例子。接下来,让我们打开 Rust 异步编程世界的大门吧!Rust异步编程中文版 同步官方更新,欢迎参与!同时欢迎加入Rust中文社区,共建Rust语言中文网络!Why 异步我们喜欢 Rust,因为它能让我们写出高效、安全的代码,但为什么要异步呢? 因为异步代码能让我们在同一个系统线程上并发执行多项任务。在一个典型的多线程应用里,如果你想同时下载两个不同的网页,你必须将这两项工作分配到两个不同的线程上,像这样:fn get_two_sites() { // 创建两个线程分别执行各自的下载任务 let thread_one = thread::spawn(|| download(“https:://www.foo.com”)); let thread_two = thread::spawn(|| download(“https:://www.bar.com”)); // 等待两个线程完成任务 thread_one.join(); thread_two.join();}对很多应用来说这就足够了——这样一来,多线程就被设计为只用来一次性执行多个不同任务。但这也带来了一些限制。在线程切换和跨线程共享数据上会产生很多额外开销。即使是一个什么都不做的线程也会用尽珍贵的系统资源,而这就是异步代码要减少的开销。我们可以使用 Rust 的 async/await! 重写上面的函数,实现执行多个任务的目标而不用创建多个线程:async fn get_two_sites() { // Create a two different “futures” which, when run to completion, // will asynchronously download the webpages. // 创建两个不同的 future,当它们被完成执行时会异步下载不同的网页 let future_one = download_async(“https:://www.foo.com”); let future_two = download_async(“https:://www.bar.com”); // 同时执行两个 future 使它们完成 join!(future_one, future_two);}总之,相比多线程实现来说,异步实现的应用具有使用更少的资源获得更高性能的潜力。线程由操作系统支持,使用它们并不需要特别的编程模型——任何函数都可以创建一个线程,而调用一个使用多线程的函数就像调用一个普通函数一样简单。但是异步函数就需要语言层面或者类库层面提供特殊的支持才能工作。在 Rust 中,async fn 会创建一个异步函数,当它被调用时,会返回一个需要依次执行函数体来完成的 future 对象。 传统多线程应用也可以非常有效,Rust的较小的内存占用以及可预测性意味着你可以做更多的事,即使不使用 async 关键字。然而,异步编程模型增长的复杂性并不总是值得的,想清楚你的应用采用简单多线程模型是否会更好仍然是很重要的。async/await! 入门async/await! 是 Rust 语言用于编写像同步代码一样的异步函数的内置工具。async 将一个代码块转化为一个实现了名为 Future 的特质(trait)的状态机。虽然在同步方法中调用阻塞函数会阻塞整个线程,但阻塞的Futures将让出线程控制权,允许其他Futures运行。要创建异步函数,可以使用async fn语法:async fn do_something() { … }async fn返回的值是一个Future,需要在执行着上运行才能起作用:// block_on blocks the current thread until the provided future has run to// completion. Other executors provide more complex behavior, like scheudling// multiple futures onto the same thread.use futures::executor::block_on;async fn hello_world() { println!(“hello, world!”);}fn main() { let future = hello_world(); // Nothing is printed block_on(future); // future is run and “hello, world!” is printed}在async fn中,你可以使用await! 等待另一种实现Future特性的类型完成,例如另一个async fn的输出。 与block_on不同,await! 不会阻止当前线程,而是异步等待Future完成,如果Future无法取得进展,则允许其他任务运行。例如,假设我们有三个async fn:learn_song,sing_song和dance:async fn learn_song() -> Song { … }async fn sing_song(song: Song) { … }async fn dance() { … }一种执行“学习”、“唱歌” 和 “跳舞” 的方法是,在执行每一项任务时阻塞:fn main() { let song = block_on(learn_song()); block_on(sing_song(song)); block_on(dance);}但是,我们使用这种方式并没有发挥出最大的性能——我们只是把它们一个个执行了。很明显,我们唱歌之前必须要学会它,但是在学歌和唱歌的同时我们也是可以跳舞的。要实现这样的效果,我们可以分别创建两个 async fn 来并发地执行:async fn learn_and_sing() { // 在唱歌之前等待学歌完成 // 这里我们使用 await! 而不是 block_on 来防止阻塞线程,这样就可以同时执行 dance 了。 let song = await!(learn_song()); await!(sing_song(song));} async fn async_main() { let f1 = learn_and_sing(); let f2 = dance(); // join! 类似于 await! ,但是可以等待多个 future 并发完成 join!(f1, f2)} fn main() { block_on(async_main());}在本例中,学歌必须发生在唱歌之前,但是学习和唱歌当同时都可以跳舞。如果我们在 learn_and_sing 中使用 block_on(learn_song()) 而不是 await!(learn_song()) 的话,它的执行将阻塞至学歌结束,就无法同时跳舞了。通过 await! 学歌这一操作,我们允许其他任务并发执行。到目前为止你已经学会了 async/await! 的基本用法,现在我们尝试写一个例子。应用:简单HTTP服务器让我们使用async/ await!构建一个echo服务器!首先,请rustup update nightly确保您的Rust最新 – 我们正在使用最前沿的功能,因此保持最新状态至关重要。完成后,运行 cargo +nightly new async-await-echo以创建新项目,然后打开生成的async-await-echo文件夹。让我们在Cargo.toml文件中添加一些依赖项:[dependencies]# The latest version of the “futures” library, which has lots of utilities# for writing async code. Enable the “tokio-compat” feature to include the# functions for using futures 0.3 and async/await with the Tokio library.futures-preview = { version = “0.3.0-alpha.9”, features = “tokio-compat”] }# Hyper is an asynchronous HTTP library. We’ll use it to power our HTTP# server and to make HTTP requests.hyper = “0.12.9”# Tokio is a runtime for asynchronous I/O applications. Hyper uses# it for the default server runtime. The tokio crate also provides an# an await! macro similar to the one in std, but it supports await!ing# both futures 0.1 futures (the kind used by Hyper and Tokio) and# futures 0.3 futures (the kind produced by the new async/await! language# feature).tokio = { version = “0.1.11”, features = [“async-await-preview”] }现在我们已经完成依赖项了,让我们开始编写一些代码。打开src/main.rs并在文件的顶部启用以下功能:#![feature(async_await, await_macro, futures_api)]async_await 增加了对 async fn 语法的支持。await_macro 增加了对 await! 宏的支持。futures_api 增加了对 nightly std::future 和 std::task 模块的支持,这些模块定义了核心Future特征和依赖类型。另外,我们还要添加一些导入:use { hyper::{ // Miscellaneous types from Hyper for working with HTTP. Body, Client, Request, Response, Server, Uri, // This function turns a closure which returns a future into an // implementation of the the Hyper Service trait, which is an // asynchronous function from a generic Request to a Response. service::service_fn, // A function which runs a future to completion using the Hyper runtime. rt::run, }, futures::{ // TokioDefaultSpawner tells futures 0.3 futures how to spawn tasks // onto the Tokio runtime. compat::TokioDefaultSpawner, // Extension traits providing additional methods on futures. // FutureExt adds methods that work for all futures, whereas // TryFutureExt adds methods to futures that return Result types. future::{FutureExt, TryFutureExt}, }, std::net::SocketAddr, // This is the redefinition of the await! macro which supports both // futures 0.1 (used by Hyper and Tokio) and futures 0.3 (the new API // exposed by std::future and implemented by async fn syntax). tokio::await,};一旦导入完成,我们就可以开始整理样板,以便我们提供请求服务:async fn serve_req(req: Request<Body>) -> Result<Response<Body>, hyper::Error> { unimplemented!()}async fn run_server(addr: SocketAddr) { println!(“Listening on http://{}”, addr); // Create a server bound on the provided address let serve_future = Server::bind(&addr) // Serve requests using our async serve_req function. // serve takes a closure which returns a type implementing the // Service trait. service_fn returns a value implementing the // Service trait, and accepts a closure which goes from request // to a future of the response. In order to use our serve_req // function with Hyper, we have to box it and put it in a compatability // wrapper to go from a futures 0.3 future (the kind returned by // async fn) to a futures 0.1 future (the kind used by Hyper). .serve(|| service_fn(|req| serve_req(req).boxed().compat(TokioDefaultSpawner) )); // Wait for the server to complete serving or exit with an error. // If an error occurred, print it to stderr. if let Err(e) = await!(serve_future) { eprintln!(“server error: {}”, e); }}fn main() { // Set the address to run our socket on. let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); // Call our run_server function, which returns a future. // As with every async fn, we need to run that future in order for // run_server to do anything. Additionally, since run_server is an // async fn, we need to convert it from a futures 0.3 future into a // futures 0.1 future. let futures_03_future = run_server(addr); let futures_01_future = futures_03_future.unit_error().boxed().compat(TokioDefaultSpawner); // Finally, we can run the future to completion using the run function // provided by Hyper. run(futures_01_future);}如果您现在cargo run,您应该在终端上看到"Listening on http://127.0.0.1:300"消息。如果您在所选择的浏览器中打开该URL,您将看到"thread … panicked at ’not yet implemented’." 现在我们只需要实际处理请求。首先,让我们返回一条静态消息:async fn serve_req(req: Request<Body>) -> Result<Response<Body>, hyper::Error> { // Always return successfully with a response containing a body with // a friendly greeting ;) Ok(Response::new(Body::from(“hello, world!”)))}如果你cargo run再次刷新页面,你应该看到"hello, world!" 出现在浏览器中。恭喜!您刚刚在Rust中编写了第一个异步Web服务器。您还可以检查请求本身,其中包含请求URI,HTTP版本,标头和其他元数据等信息。例如,我们可以打印出请求的URI,如下所示:println!(“Got request at {:?}”, req.uri());你可能已经注意到我们在处理请求时还没有做任何异步 - 我们只是立即响应,所以我们没有利用async fn给我们的灵活性。让我们尝试使用Hyper的HTTP客户端将用户的请求代理到另一个网站,而不仅仅是返回静态消息。我们首先解析出我们想要请求的URL:let url_str = “http://www.rust-lang.org/en-US/";let url = url_str.parse::<Uri>().expect(“failed to parse URL”);然后我们可以创建一个新的hyper::Client并使用它来发出GET请求,将响应返回给用户:let res = await!(Client::new().get(url));// Return the result of the request directly to the userprintln!(“request finished –returning response”);resClient::get返回hyper::client::FutureResponse,他实现了 Future<Output = Result<Response, Error>> (或Future<Item = Response, Error = Error> 在futures 0.1)。当我们await!这个future时,HTTP请求已发出,当前任务被暂停,并且一旦响应可用,任务就排队等待继续。现在,如果cargo run您在浏览器中打开http://127.0.0.1:3000/foo,您将看到Rust主页,以及以下终端输出:\Listening on http://127.0.0.1:3000Got request at /foomaking request to http://www.rust-lang.org/en-US/request finished– returning response恭喜!您刚刚完成了代理HTTP请求。 ...

December 20, 2018 · 5 min · jiezi

Rust错误处理

Rust错误处理本文同步于Rust中文社区专栏文章:Rust错误处理 ,本文时间:2018-12-14, 译者:krircc,简介:天青色,原文出处欢迎向Rust中文社区投稿,投稿地址 ,好文将在以下地方直接展示Rust中文社区首页Rust中文社区文章栏目知乎专栏Rust中文社区思否专栏Rust中文社区简书专题Rust中文社区微博Rustlang-cn智能编译器Rust编译器最重要的工作是防止Rust程序中的错误。如果代码没有正确遵循内存管理规则或生命周期注释,它会在编译时分析代码并发出警告。例如,#[allow(unused_variables)] //???? A lint attribute used to suppress the warning; unused variable: bfn main() { let a = vec![1, 2, 3]; let b = a; println!("{:?}", a);}// —— Compile time error ——error[E0382]: use of moved value: a –> src/main.rs:6:22 |3 | let b = a; | - value moved here4 |5 | println!("{:?}", a); | ^ value used here after move | = note: move occurs because a has type std::vec::Vec&lt;i32&gt;, which does not implement the Copy traiterror: aborting due to previous errorFor more information about this error, try rustc --explain E0382.// ⭐ instead using #[allow(unused_variables)], consider using “let b = a;” in line 4. // Also you can use “let _ =” to completely ignore return valuesRust编译器不仅检查与生命周期或内存管理相关的问题,还检查常见的编码错误,如下面的代码。struct Color { r: u8, g: u8, b: u8,}fn main() { let yellow = Color { r: 255, g: 255, d: 0, }; println!(“Yellow = rgb({},{},{})”, yellow.r, yellow.g, yellow.b);}// ———— Compile time error ————error[E0560]: struct Color has no field named d –> src/main.rs:11:9 |11 | d: 0, | ^ field does not exist - did you mean b?error: aborting due to previous errorFor more information about this error, try rustc --explain E0560.以上错误消息非常具有描述性,我们可以很容易地看出错误在哪里。但是,虽然我们无法通过错误消息识别问题,但rustc –explain 命令通过显示表达相同问题的简单代码示例以及我们必须使用的解决方案来帮助我们识别错误类型以及如何解决它。例如,在控制台中显示以下输出。rustc –explain E0571// A break statement with an argument appeared in a non-loop loop.// Example of erroneous code:let result = while true { if satisfied(i) { break 2i; // error: break with value from a while loop } i += 1;};// The break statement can take an argument (which will be the value of the loop expression if the break statement is executed) in loop loops, but not for, while, or while let loops.Make sure break value; statements only occur in loop loops:let result = loop { // ok! if satisfied(i) { break 2i; } i += 1;};????您也可以通过Rust Compiler Error Index阅读相同的解释 。例如,要检查E0571错误的解释,您可以使用https://doc.rust-lang.org/error-index.html#E0571 Panickingpanic!()▸ 在某些情况下,当发生错误时,我们无法做任何事情来处理它,如果错误是某种情况,那就不应该发生。换句话说,如果这是一个不可恢复的错误。▸ 当我们不使用功能丰富的调试器或正确的日志时,有时我们需要通过打印特定的消息或变量绑定的值从特定的代码行退出程序来调试代码以了解当前的程序的流程。对于上述情况,我们可以使用panic!宏。让我们看几个例子。⭐ panic!() 运行基于线程。一个线程可能会被恐慌,而其他线程正在运行。01.从特定行退出。fn main() { // some code // if we need to debug in here panic!();}// ————– Compile time error ————–thread ‘main’ panicked at ’explicit panic’, src/main.rs:5:502.退出并显示自定义错误消息。#[allow(unused_mut)] // ???? A lint attribute used to suppress the warning; username variable does not need to be mutablefn main() { let mut username = String::new(); // some code to get the name if username.is_empty() { panic!(“Username is empty!”); } println!("{}", username);}// ————– Compile time error ————–thread ‘main’ panicked at ‘Username is empty!’, src/main.rs:8:903.退出附带代码元素的值。#[derive(Debug)] // ???? A lint attribute which use to implement std::fmt::Debug to Colorstruct Color { r: u8, g: u8, b: u8,}#[allow(unreachable_code)] // ???? A lint attribute used to suppress the warning; unreachable statementfn main() { let some_color: Color; // some code to get the color. ex some_color = Color {r: 255, g: 255, b: 0}; // if we need to debug in here panic!("{:?}", some_color); println!( “The color = rgb({},{},{})”, some_color.r, some_color.g, some_color.b );}// ————– Compile time error ————–thread ‘main’ panicked at ‘Color { r: 255, g: 255, b: 0 }’, src/main.rs:16:5正如您在上面的示例中所看到的,panic!()支持println!()类型样式参数 。默认情况下,它会输出错误消息,文件路径以及发生错误的行号和列号。unimplemented!()如果您的代码具有未完成的代码段,则有一个标准化宏unimplemented!()来标记这些路径。如果程序通过这些路径运行,程序将panicked并返回"not yet implemented"的错误消息。// error messages with panic!()thread ‘main’ panicked at ’explicit panic’, src/main.rs:6:5thread ‘main’ panicked at ‘Username is empty!’, src/main.rs:9:9thread ‘main’ panicked at ‘Color { r: 255, g: 255, b: 0 }’, src/main.rs:17:5// error messages with unimplemented!()thread ‘main’ panicked at ’not yet implemented’, src/main.rs:6:5thread ‘main’ panicked at ’not yet implemented: Username is empty!’, src/main.rs:9:9thread ‘main’ panicked at ’not yet implemented: Color { r: 255, g: 255, b: 0 }’, src/main.rs:17:5unreachable!()这是标记程序不应输入的路径的标准宏。如果程序进入这些路径,程序将panicked并返回"‘internal error: entered unreachable code’“错误消息。fn main() { let level = 22; let stage = match level { 1…5 => “beginner”, 6…10 => “intermediate”, 11…20 => “expert”, _ => unreachable!(), }; println!(”{}", stage);}// ————– Compile time error ————–thread ‘main’ panicked at ‘internal error: entered unreachable code’, src/main.rs:7:20我们也可以为此设置自定义错误消息。// — with a custom message — => unreachable!(“Custom message”),// ————– Compile time error ————–thread ‘main’ panicked at ‘internal error: entered unreachable code: Custom message’, src/main.rs:7:20// — with debug data —_ => unreachable!(“level is {}”, level),// ————– Compile time error ————–thread ‘main’ panicked at ‘internal error: entered unreachable code: level is 22’, src/main.rs:7:14assert!(), assert_eq!(), assert_ne!()这些是标准宏,通常与测试断言一起使用。assert!()确保布尔表达式为true。如果表达式为false,则会发生panics。fn main() { let f = false; assert!(f)}// ————– Compile time error ————–thread ‘main’ panicked at ‘assertion failed: f’, src/main.rs:4:5assert_eq!()确保两个表达式相等。如果表达式不相等则会发生panics。fn main() { let a = 10; let b = 20; assert_eq!(a, b);}// ————– Compile time error ————–thread ‘main’ panicked at ‘assertion failed: (left == right) left: 10, right: 20’, src/main.rs:5:5assert_ne!()确保两个表达式不相等。如果表达式相等,它会发生panics。fn main() { let a = 10; let b = 10; assert_ne!(a, b);}// ————– Compile time error ————–thread ‘main’ panicked at ‘assertion failed: (left != right) left: 10, right: 10’, src/main.rs:5:5⭐使用表达式assert_ne!()和assert_eq!()应返回相同的数据类型。我们也可以为这些宏设置自定义错误消息。举些例子,带有自定义消息 assert_eq!()fn main() { let a = 10; let b = 20; assert_eq!(a, b, “a and b should be equal”);}// ————– Compile time error ————–thread ‘main’ panicked at ‘assertion failed: (left == right) left: 10, right: 20: a and b should be equal’, src/main.rs:5:5assert_eq!()带有调试数据fn main() { let a = 10; let b = 20; let c = 40; assert_eq!(a+b, c, “a = {} ; b = {}”, a, b);}// ————– Compile time error ————–thread ‘main’ panicked at ‘assertion failed: (left == right) left: 30, right: 40: a = 10 ; b = 20’, src/main.rs:7:5debug_assert!(), debug_assert_eq!(), debug_assert_ne!()????这些与上面的assert宏类似。但默认情况下,这些语句仅在非优化构建中启用。debug_assert除非我们传递-C debug-assertions给编译器,否则在发布版本中将省略所有这些宏。Option and Result许多语言使用null\ nil\ undefined 类型来表示空输出和Exceptions处理错误。Rust会同时使用两者,特别是为了防止诸如空指针异常,异常等敏感数据泄漏等问题。相反,Rust提供了两个特殊的通用枚举 ; Option和Result处理上述案件。如您所知:▸ Option可以包含某个值Some或没有值/ None。▸ Result可以表示成功/ Ok 或失败/Err。// An output can have either Some value or no value/ None.enum Option<T> { // T is a generic and it can contain any type of value. Some(T), None,}// A result can represent either success/ Ok or failure/ Err.enum Result<T, E> { // T and E are generics. T can contain any type of value, E can be any error. Ok(T), Err(E),}Option的基本用法编写函数或数据类型时:如果函数的参数是可选的,如果函数为非空,并且返回的输出可以为空,如果数据类型的属性的值可以是空,我们不得不使用他们的数据类型为Option类型例如,如果函数输出一个&str值并且输出可以为空,则函数的返回类型应设置为Option<&str>fn get_an_optional_value() -> Option<&str> { //if the optional value is not empty return Some(“Some value”); //else None}同样,如果数据类型的属性值可以为空或者像下面示例中middle_name的Name数据类型那样可选,我们应该将其数据类型设置为Option类型。struct Name { first_name: String, middle_name: Option<String>, // middle_name can be empty last_name: String,}????如您所知,我们可以使用模式匹配match来捕获相关的返回类型(Some/ None) 。有一个函数来获取当前用户的主目录在std::env为home_dir() 。由于所有用户在Linux等系统中都没有主目录,因此用户的主目录可以是可选的。所以它返回一个Option类型; Option<PathBuf>.use std::env;fn main() { let home_path = env::home_dir(); match home_path { Some(p) => println!("{:?}", p), // This prints “/root”, if you run this in Rust playground None => println!(“Can not find the home directory!”), }}⭐但是,当在函数中使用可选参数时,我们必须None在调用函数时传递空参数的值。fn get_full_name(fname: &str, lname: &str, mname: Option<&str>) -> String { // middle name can be empty match mname { Some(n) => format!("{} {} {}", fname, n, lname), None => format!("{} {}", fname, lname), }}fn main() { println!("{}", get_full_name(“Galileo”, “Galilei”, None)); println!("{}", get_full_name(“Leonardo”, “Vinci”, Some(“Da”)));}// ???? Better create a struct as Person with fname, lname, mname fields and create a impl function as full_name()????除此之外,Option类型与Rust中的可空指针一起使用。由于Rust中没有空指针,因此指针类型应指向有效位置。因此,如果指针可以为空,我们就可以使用了Option<Box<T>> 。Result的基本用法如果函数可以产生错误,我们必须Result通过组合有效输出的数据类型和错误的数据类型来使用类型。例如,如果有效输出的数据类型为u64且错误类型为String ,则返回类型应为Result<u64, String> 。fn function_with_error() -> Result<u64, String> { //if error happens return Err(“The error message”.to_string()); // else, return valid output Ok(255)}????如您所知,我们可以使用模式匹配match来捕获相关的返回类型(Ok/ Err)。有一个函数可以获取std::env 任何环境变量中的值是var() 。它的输入是环境变量名称。如果我们传递了错误的环境变量,或者程序在运行时无法提取环境变量的值,则会产生错误。所以它的返回类型是一种Result类型; Result<String, VarError>.use std::env;fn main() { let key = “HOME”; match env::var(key) { Ok(v) => println!("{}", v), // This prints “/root”, if you run this in Rust playground Err(e) => println!("{}", e), // This prints “environment variable not found”, if you give a nonexistent environment variable }}is_some(), is_none(), is_ok(), is_err()除了match表情,rust还提供is_some() ,is_none()并且is_ok() ,is_err()功能,以确定返回类型。fn main() { let x: Option<&str> = Some(“Hello, world!”); assert_eq!(x.is_some(), true); assert_eq!(x.is_none(), false); let y: Result<i8, &str> = Ok(10); assert_eq!(y.is_ok(), true); assert_eq!(y.is_err(), false);}ok(), err() for Result typesrust另外提供ok()和err()为Result类型。它们将Result类型的Ok<T>值和Err<E>值转换为Option类型。fn main() { let o: Result<i8, &str> = Ok(8); let e: Result<i8, &str> = Err(“message”); assert_eq!(o.ok(), Some(8)); // Ok(v) ok = Some(v) assert_eq!(e.ok(), None); // Err(v) ok = None assert_eq!(o.err(), None); // Ok(v) err = None assert_eq!(e.err(), Some(“message”)); // Err(v) err = Some(v)}Unwrap and Expectunwrap()▸如果Option类型具有Some值或Result类型具有Ok值,则其中的值将传递到下一步。▸如果Option类型具有None值或Result类型具有Err值,则编程panics ; 如果Err,panics携带错误消息。该功能与以下代码类似,使用match而不是使用unwrap() 。示例使用Option和matchfn main() { let x; match get_an_optional_value() { Some(v) => x = v, // if Some(“abc”), set x to “abc” None => panic!(), // if None, panic without any message } println!("{}", x); // “abc” ; if you change line 14 false to true}fn get_an_optional_value() -> Option<&‘static str> { //if the optional value is not empty if false { return Some(“abc”); } //else None}// ————— Compile time error —————thread ‘main’ panicked at ’explicit panic’, src/main.rs:5:17示例使用Result和matchfn main() { let x; match function_with_error() { Ok(v) => x = v, // if Ok(255), set x to 255 Err(e) => panic!(e), // if Err(“some message”), panic with error message “some message” } println!("{}", x); // 255 ; if you change line 13 true to false}fn function_with_error() -> Result<u64, String> { //if error happens if true { return Err(“some message”.to_string()); } // else, return valid output Ok(255)}// ———- Compile time error ———-thread ‘main’ panicked at ‘some message’, src/main.rs:5:19上述main函数中的相同代码可以使用unwrap()两行来编写。// 01. unwrap error message for Nonefn main() { let x = get_an_optional_value().unwrap(); println!("{}", x);}// ————— Compile time error —————thread ‘main’ panicked at ‘called Option::unwrap() on a None value’, libcore/option.rs:345:21// 02. unwrap error message for Errfn main() { let x = function_with_error().unwrap(); println!("{}", x);}// ————— Compile time error —————thread ‘main’ panicked at ‘called Result::unwrap() on an Err value: “some message”’, libcore/result.rs:945:5⭐但是正如您所看到的,当使用unwrap()错误消息时,没有显示发生恐慌的确切行号。expect()类似unwrap()但可以为恐慌设置自定义消息。// 01. expect error message for Nonefn main() { let n: Option<i8> = None; n.expect(“empty value returned”);}// ————— Compile time error —————thread ‘main’ panicked at ’empty value returned’, libcore/option.rs:989:5// 02. expect error message for Errfn main() { let e: Result<i8, &str> = Err(“some message”); e.expect(“expect error message”);}// ————— Compile time error —————thread ‘main’ panicked at ’expect error message: “some message”’, libcore/result.rs:945:5unwrap_err() and expect_err() for Result typesunwrap()和expect()相反的情况; Ok时恐慌而不是Err时。两者都在Ok错误消息中打印内部值。????通常用于测试。// 01. unwrap_err error message for Okfn main() { let o: Result<i8, &str> = Ok(8); o.unwrap_err();}// ———- Compile time error ———-thread ‘main’ panicked at ‘called Result::unwrap_err() on an Ok value: 8’, libcore/result.rs:945:5// 02. expect_err error message for Okfn main() { let o: Result<i8, &str> = Ok(8); o.expect_err(“Should not get Ok value”);}// ———- Compile time error ———-thread ‘main’ panicked at ‘Should not get Ok value: 8’, libcore/result.rs:945:5unwrap_or(), unwrap_or_default() and unwrap_or_else()????这些有点类似于unwrap(),如果Option类型有Some值或Result类型有Ok值,则它们内部的值传递到下一步。但是当有None 或者 Err,功能有点不同。unwrap_or() :使用None或Err,您传递给的unwrap_or()值将传递到下一步。但是,您传递的值的数据类型应与相关Some或Ok的数据类型匹配。fn main() { let v1 = 8; let v2 = 16; let s_v1 = Some(8); let n = None; assert_eq!(s_v1.unwrap_or(v2), v1); // Some(v1) unwrap_or v2 = v1 assert_eq!(n.unwrap_or(v2), v2); // None unwrap_or v2 = v2 let o_v1: Result<i8, &str> = Ok(8); let e: Result<i8, &str> = Err(“error”); assert_eq!(o_v1.unwrap_or(v2), v1); // Ok(v1) unwrap_or v2 = v1 assert_eq!(e.unwrap_or(v2), v2); // Err unwrap_or v2 = v2}unwrap_or_default() :使用None或Err,相关的数据类型的默认值Some或者Ok,传递到下一步。fn main() { let v = 8; let v_default = 0; let s_v: Option<i8> = Some(8); let n: Option<i8> = None; assert_eq!(s_v.unwrap_or_default(), v); // Some(v) unwrap_or_default = v assert_eq!(n.unwrap_or_default(), v_default); // None unwrap_or_default = default value of v let o_v: Result<i8, &str> = Ok(8); let e: Result<i8, &str> = Err(“error”); assert_eq!(o_v.unwrap_or_default(), v); // Ok(v) unwrap_or_default = v assert_eq!(e.unwrap_or_default(), v_default); // Err unwrap_or_default = default value of v}unwrap_or_else() :类似于unwrap_or()。唯一的区别是,您必须传递一个闭包,它返回一个具有Some或Ok相关数据类型的值,而不是传递一个值。fn main() { let v1 = 8; let v2 = 16; let s_v1 = Some(8); let n = None; let fn_v2_for_option = || 16; assert_eq!(s_v1.unwrap_or_else(fn_v2_for_option), v1); // Some(v1) unwrap_or_else fn_v2 = v1 assert_eq!(n.unwrap_or_else(fn_v2_for_option), v2); // None unwrap_or_else fn_v2 = v2 let o_v1: Result<i8, &str> = Ok(8); let e: Result<i8, &str> = Err(“error”); let fn_v2_for_result = || 16; assert_eq!(o_v1.unwrap_or_else(fn_v2_for_result), v1); // Ok(v1) unwrap_or_else fn_v2 = v1 assert_eq!(e.unwrap_or_else(fn_v2_for_result), v2); // Err unwrap_or_else fn_v2 = v2}Error and None Propagation我们应该使用恐慌panic!(),unwrap(),expect()只有当我们没有一个更好处理办法的情况。此外如果一个函数包含表达式既能产生None也能产生Err, ▸我们可以在同一函数中处理▸我们可以立即返回None 和Err给调用者。因此调用者可以决定如何处理它们。???? None类型无需始终由函数的调用者处理。但Rusts处理Err类型的约定是,立即将它们返回给调用者,以便给调用者更多的控制权来决定如何处理它们。?操作符▸如果Option类型具有Some值或Result类型具有Ok值,则其中的值将传递到下一步。▸如果Option类型具有None值或Result类型具有Err值,则立即将它们返回给函数的调用者。示例Option类型,fn main() { if complex_function().is_none() { println!(“X not exists!”); }}fn complex_function() -> Option<&‘static str> { let x = get_an_optional_value()?; // if None, returns immidiately; if Some(“abc”), set x to “abc” // some other code, ex println!("{}", x); // “abc” ; if you change line 19 false to true Some("")}fn get_an_optional_value() -> Option<&‘static str> { //if the optional value is not empty if false { return Some(“abc”); } //else None}示例Result类型,fn main() { // main function is the caller of complex_function function // So we handle errors of complex_function(), inside main() if complex_function().is_err() { println!(“Can not calculate X!”); }}fn complex_function() -> Result<u64, String> { let x = function_with_error()?; // if Err, returns immidiately; if Ok(255), set x to 255 // some other code, ex println!("{}", x); // 255 ; if you change line 20 true to false Ok(0)}fn function_with_error() -> Result<u64, String> { //if error happens if true { return Err(“some message”.to_string()); } // else, return valid output Ok(255)}从main()传播错误在Rust版本1.26之前,我们无法从main()函数传播Result和Option。但是现在,我们可以从main()函数中传播Result类型,并打印出Err的Debug表示形式。use std::fs::File;fn main() -> std::io::Result<()> { let _ = File::open(“not-existing-file.txt”)?; Ok(()) // Because of the default return value of Rust functions is an empty tuple/ ()}// Because of the program can not find not-existing-file.txt , it produces,// Err(Os { code: 2, kind: NotFound, message: “No such file or directory” })// While propagating error, the program prints,// Error: Os { code: 2, kind: NotFound, message: “No such file or directory” }Combinators让我们看看组合器是什么,“组合者”的一个含义是更加非正式的意义,指的是组合模式,一种以组合事物的思想为中心组织图书馆的风格。通常存在一些类型T,一些用于构造类型T的“原始”值的函数,以及一些可以以各种方式组合类型T的值以构建类型T的更复杂值的 “ 组合器 ” 。另一个定义是“ 没有自由变量的函数 ”(wiki.haskell.org)组合子是一个函数,其从程序片段构建程序片段 ; 从某种意义上说,使用组合器的程序员自动构建了大部分所需的程序,而不是手工编写每个细节。Rust生态系统中“组合子”的确切定义有点不清楚。▸ or(),and(),or_else(),and_then()组合类型为T的两个值并返回相同类型T。▸ filter()对于Option类型使用闭包作为条件函数来过滤类型T.返回相同的类型T.▸ map(),map_err()通过使用闭包转换类型T。可以更改T内部值的数据类型。例如:Some<&str>可转化为Some<usize>或者Err<&str>可转化为Err<isize>等▸ map_or(),map_or_else()通过应用闭包转换类型T并返回类型T内的值。对None 和Err,应用默认值或其他闭包。▸ ok_or(),ok_or_else()对于Option类型将Option类型转换为Result类型。▸ as_ref(),as_mut()将类型T转换为引用或可变引用。or() and and()组合两个表达式返回Option/ Result▸or() :如果任何一个得到Some或Ok`,该值立即返回。▸ and() :如果两者都得到Some或Ok,则返回第二个表达式中的值。如果任何一个获得None或Err该值立即返回。fn main() { let s1 = Some(“some1”); let s2 = Some(“some2”); let n: Option<&str> = None; let o1: Result<&str, &str> = Ok(“ok1”); let o2: Result<&str, &str> = Ok(“ok2”); let e1: Result<&str, &str> = Err(“error1”); let e2: Result<&str, &str> = Err(“error2”); assert_eq!(s1.or(s2), s1); // Some1 or Some2 = Some1 assert_eq!(s1.or(n), s1); // Some or None = Some assert_eq!(n.or(s1), s1); // None or Some = Some assert_eq!(n.or(n), n); // None1 or None2 = None2 assert_eq!(o1.or(o2), o1); // Ok1 or Ok2 = Ok1 assert_eq!(o1.or(e1), o1); // Ok or Err = Ok assert_eq!(e1.or(o1), o1); // Err or Ok = Ok assert_eq!(e1.or(e2), e2); // Err1 or Err2 = Err2 assert_eq!(s1.and(s2), s2); // Some1 and Some2 = Some2 assert_eq!(s1.and(n), n); // Some and None = None assert_eq!(n.and(s1), n); // None and Some = None assert_eq!(n.and(n), n); // None1 and None2 = None1 assert_eq!(o1.and(o2), o2); // Ok1 and Ok2 = Ok2 assert_eq!(o1.and(e1), e1); // Ok and Err = Err assert_eq!(e1.and(o1), e1); // Err and Ok = Err assert_eq!(e1.and(e2), e1); // Err1 and Err2 = Err1}????nightly支持Option类型的xor(),它返回Some当只有一个表达式返回Some,而不是两个。or_else()类似于or()。唯一的区别是,第二个表达式应该是一个返回相同类型T 的闭包。fn main() { // or_else with Option let s1 = Some(“some1”); let s2 = Some(“some2”); let fn_some = || Some(“some2”); // similar to: let fn_some = || -> Option<&str> { Some(“some2”) }; let n: Option<&str> = None; let fn_none = || None; assert_eq!(s1.or_else(fn_some), s1); // Some1 or_else Some2 = Some1 assert_eq!(s1.or_else(fn_none), s1); // Some or_else None = Some assert_eq!(n.or_else(fn_some), s2); // None or_else Some = Some assert_eq!(n.or_else(fn_none), None); // None1 or_else None2 = None2 // or_else with Result let o1: Result<&str, &str> = Ok(“ok1”); let o2: Result<&str, &str> = Ok(“ok2”); let fn_ok = || Ok(“ok2”); // similar to: let fn_ok = || -> Result<&str, &str> { Ok(“ok2”) }; let e1: Result<&str, &str> = Err(“error1”); let e2: Result<&str, &str> = Err(“error2”); let fn_err = || Err(“error2”); assert_eq!(o1.or_else(fn_ok), o1); // Ok1 or_else Ok2 = Ok1 assert_eq!(o1.or_else(fn_err), o1); // Ok or_else Err = Ok assert_eq!(e1.or_else(fn_ok), o2); // Err or_else Ok = Ok assert_eq!(e1.or_else(fn_err), e2); // Err1 or_else Err2 = Err2}and_then()类似于and()。唯一的区别是,第二个表达式应该是一个返回相同类型T 的闭包。fn main() { // and_then with Option let s1 = Some(“some1”); let s2 = Some(“some2”); let fn_some = || Some(“some2”); // similar to: let fn_some = || -> Option<&str> { Some(“some2”) }; let n: Option<&str> = None; let fn_none = || None; assert_eq!(s1.and_then(fn_some), s2); // Some1 and_then Some2 = Some2 assert_eq!(s1.and_then(fn_none), n); // Some and_then None = None assert_eq!(n.and_then(fn_some), n); // None and_then Some = None assert_eq!(n.and_then(fn_none), n); // None1 and_then None2 = None1 // and_then with Result let o1: Result<&str, &str> = Ok(“ok1”); let o2: Result<&str, &str> = Ok(“ok2”); let fn_ok = || Ok(“ok2”); // similar to: let fn_ok = || -> Result<&str, &str> { Ok(“ok2”) }; let e1: Result<&str, &str> = Err(“error1”); let e2: Result<&str, &str> = Err(“error2”); let fn_err = || Err(“error2”); assert_eq!(o1.and_then(fn_ok), o2); // Ok1 and_then Ok2 = Ok2 assert_eq!(o1.and_then(fn_err), e2); // Ok and_then Err = Err assert_eq!(e1.and_then(fn_ok), e1); // Err and_then Ok = Err assert_eq!(e1.and_then(fn_err), e1); // Err1 and_then Err2 = Err1}filter()????通常在编程语言中,filter函数与数组或迭代器一起使用,通过函数/闭包过滤自己的元素来创建新的数组/迭代器。Rust还提供了一个filter()迭代器适配器,用于在迭代器的每个元素上应用闭包,将其转换为另一个迭代器。然而,在这里,我们正在谈论filter()函数与Option类型。仅当我们传递一个Some值并且给定的闭包为它返回true时,返回相同的Some类型。如果None传递类型或闭包返回false,返回None。闭包使用Some里面的值作为参数。Rust仍然支持filter()只支持Option的类型。fn main() { let s1 = Some(3); let s2 = Some(6); let n = None; let fn_is_even = |x: &i8| x % 2 == 0; assert_eq!(s1.filter(fn_is_even), n); // Some(3) -> 3 is not even -> None assert_eq!(s2.filter(fn_is_even), s2); // Some(6) -> 6 is even -> Some(6) assert_eq!(n.filter(fn_is_even), n); // None -> no value -> None}map() and map_err()????通常在编程语言中,map()函数与数组或迭代器一起使用,以在数组或迭代器的每个元素上应用闭包。Rust还提供了一个map()迭代器适配器,用于在迭代器的每个元素上应用闭包,将其转换为另一个迭代器。但是在这里我们讨论的是map()函数与Option和Result类型。map() :通过应用闭包来转换类型T. 可以根据闭包的返回类型更改Some或Ok块数据类型。转换Option<T>为Option<U> ,转换Result<T, E>为Result<U, E>⭐ map(),仅仅 Some和Ok值改变。对Err内部值没有影响(None根本不包含任何值)。fn main() { let s1 = Some(“abcde”); let s2 = Some(5); let n1: Option<&str> = None; let n2: Option<usize> = None; let o1: Result<&str, &str> = Ok(“abcde”); let o2: Result<usize, &str> = Ok(5); let e1: Result<&str, &str> = Err(“abcde”); let e2: Result<usize, &str> = Err(“abcde”); let fn_character_count = |s: &str| s.chars().count(); assert_eq!(s1.map(fn_character_count), s2); // Some1 map = Some2 assert_eq!(n1.map(fn_character_count), n2); // None1 map = None2 assert_eq!(o1.map(fn_character_count), o2); // Ok1 map = Ok2 assert_eq!(e1.map(fn_character_count), e2); // Err1 map = Err2}map_err()对于Result类型:Err块的数据类型可以根据闭包的返回类型进行更改。转换Result<T, E>为Result<T, F>。⭐map_err(),只有Err值会发生变化。对Ok内部的值没有影响。fn main() { let o1: Result<&str, &str> = Ok(“abcde”); let o2: Result<&str, isize> = Ok(“abcde”); let e1: Result<&str, &str> = Err(“404”); let e2: Result<&str, isize> = Err(404); let fn_character_count = |s: &str| -> isize { s.parse().unwrap() }; // convert str to isize assert_eq!(o1.map_err(fn_character_count), o2); // Ok1 map = Ok2 assert_eq!(e1.map_err(fn_character_count), e2); // Err1 map = Err2}map_or() and map_or_else()这些功能也与unwrap_or()和unwrap_or_else()相似。但是map_or()和map_or_else()在Some,Ok值上应用闭包和返回类型T内的值。map_or() :仅支持Option类型(不支持Result)。将闭包应用于Some内部值并根据闭包返回输出。为None类型返回给定的默认值。fn main() { const V_DEFAULT: i8 = 1; let s = Some(10); let n: Option<i8> = None; let fn_closure = |v: i8| v + 2; assert_eq!(s.map_or(V_DEFAULT, fn_closure), 12); assert_eq!(n.map_or(V_DEFAULT, fn_closure), V_DEFAULT);}map_or_else() :支持两种Option和Result类型(Result仅限nightly)。类似map_or()但应该提供另一个闭包而不是第一个参数的默认值。⭐ None类型不包含任何值。所以不需要将任何东西传递给闭包作为输入Option类型。但是Err类型在其中包含一些值。因此,默认闭包应该能够将其作为输入读取,同时将其与Result类型一起使用。#![feature(result_map_or_else)] // enable unstable library feature ‘result_map_or_else’ on nightlyfn main() { let s = Some(10); let n: Option<i8> = None; let fn_closure = |v: i8| v + 2; let fn_default = || 1; // None doesn’t contain any value. So no need to pass anything to closure as input. assert_eq!(s.map_or_else(fn_default, fn_closure), 12); assert_eq!(n.map_or_else(fn_default, fn_closure), 1); let o = Ok(10); let e = Err(5); let fn_default_for_result = |v: i8| v + 1; // Err contain some value inside it. So default closure should able to read it as input assert_eq!(o.map_or_else(fn_default_for_result, fn_closure), 12); assert_eq!(e.map_or_else(fn_default_for_result, fn_closure), 6);}ok_or() and ok_or_else()如前所述ok_or(),ok_or_else()将Option类型转换为Result类型。Some对Ok和None对Err 。ok_or() :默认Err消息应作为参数传递.fn main() { const ERR_DEFAULT: &str = “error message”; let s = Some(“abcde”); let n: Option<&str> = None; let o: Result<&str, &str> = Ok(“abcde”); let e: Result<&str, &str> = Err(ERR_DEFAULT); assert_eq!(s.ok_or(ERR_DEFAULT), o); // Some(T) -> Ok(T) assert_eq!(n.ok_or(ERR_DEFAULT), e); // None -> Err(default)}ok_or_else() :类似于ok_or()。应该将闭包作为参数传递。fn main() { let s = Some(“abcde”); let n: Option<&str> = None; let fn_err_message = || “error message”; let o: Result<&str, &str> = Ok(“abcde”); let e: Result<&str, &str> = Err(“error message”); assert_eq!(s.ok_or_else(fn_err_message), o); // Some(T) -> Ok(T) assert_eq!(n.ok_or_else(fn_err_message), e); // None -> Err(default)}as_ref() and as_mut()????如前所述,这些函数用于借用类型T作为引用或作为可变引用。as_ref() :转换Option<T>到Option<&T>和Result<T, E>到Result<&T, &E>as_mut() :转换Option<T>到Option<&mut T>和Result<T, E>到Result<&mut T, &mut E>自定义错误类型Rust允许我们创建自己的Err类型。我们称之为“ 自定义错误类型”。Error trait如您所知,traits定义了类型必须提供的功能。但是我们不需要总是为常用功能定义新的特性,因为Rust 标准库提供了一些可以在我们自己的类型上实现的可重用特性。创建自定义错误类型时,std::error::Error trait可帮助我们将任何类型转换为Err类型。use std::fmt::{Debug, Display};pub trait Error: Debug + Display { fn source(&self) -> Option<&(Error + ‘static)> { … }}一个特质可以从另一个特质继承。trait Error: Debug + Display意味着Error特质继承fmt::Debug和fmt::Display特质。// traits inside Rust standard library core fmt module/ std::fmtpub trait Display { fn fmt(&self, f: &mut Formatter) -> Result<(), Error>;}pub trait Debug { fn fmt(&self, f: &mut Formatter) -> Result<(), Error>;}▸ Display最终用户应如何将此错误视为面向消息/面向用户的输出。通常通过println!("{}")或打印eprintln!("{}")▸ Debug如何显示Errwhile调试/面向程序员的输出。通常打印println!("{:?}")或eprintln!("{:?}") 漂亮打印,可以使用println!("{:#?}")或eprintln!("{:#?}")。▸ source()此错误的较低级别来源(如果有)。可选的。首先,让我们看看如何std::error::Error在最简单的自定义错误类型上实现特征。use std::fmt;// Custom error type; can be any type which defined in the current crate// ???? In here, we use a simple “unit struct” to simplify the examplestruct AppError;// Implement std::fmt::Display for AppErrorimpl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, “An Error Occurred, Please Try Again!”) // user-facing output }}// Implement std::fmt::Debug for AppErrorimpl fmt::Debug for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, “{{ file: {}, line: {} }}”, file!(), line!()) // programmer-facing output }}// A sample function to produce an AppError Errfn produce_error() -> Result<(), AppError> { Err(AppError)}fn main() { match produce_error() { Err(e) => eprintln!("{}", e), // An Error Occurred, Please Try Again! _ => println!(“No error”), } eprintln!("{:?}", produce_error()); // Err({ file: src/main.rs, line: 17 })}希望你理解要点。现在,让我们看一些带有错误代码和错误消息的自定义错误类型。use std::fmt;struct AppError { code: usize, message: String,}// Different error messages according to AppError.codeimpl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let err_msg = match self.code { 404 => “Sorry, Can not find the Page!”, _ => “Sorry, something is wrong! Please Try Again!”, }; write!(f, “{}”, err_msg) }}// A unique format for dubugging outputimpl fmt::Debug for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, “AppError {{ code: {}, message: {} }}”, self.code, self.message ) }}fn produce_error() -> Result<(), AppError> { Err(AppError { code: 404, message: String::from(“Page not found”), })}fn main() { match produce_error() { Err(e) => eprintln!("{}", e), // Sorry, Can not find the Page! _ => println!(“No error”), } eprintln!("{:?}", produce_error()); // Err(AppError { code: 404, message: Page not found }) eprintln!("{:#?}", produce_error()); // Err( // AppError { code: 404, message: Page not found } // )}⭐️Rust标准库不仅提供了可重用的特性,而且还有助于通过#[derive]属性神奇地生成少数特征的实现。Rust支持derive std::fmt::Debug,为调试消息提供默认格式。因此,我们可以在struct前声明使用#[derive(Debug)]跳过实现std::fmt::Debug 自定义错误类型。use std::fmt;#[derive(Debug)] // derive std::fmt::Debug on AppErrorstruct AppError { code: usize, message: String,}impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let err_msg = match self.code { 404 => “Sorry, Can not find the Page!”, _ => “Sorry, something is wrong! Please Try Again!”, }; write!(f, “{}”, err_msg) }}fn produce_error() -> Result<(), AppError> { Err(AppError { code: 404, message: String::from(“Page not found”), })}fn main() { match produce_error() { Err(e) => eprintln!("{}", e), // Sorry, Can not find the Page! _ => println!(“No error”), } eprintln!("{:?}", produce_error()); // Err(AppError { code: 404, message: Page not found }) eprintln!("{:#?}", produce_error()); // Err( // AppError { // code: 404, // message: “Page not found” // } // )}From trait在编写真实的程序时,我们大多数时候必须同时处理不同的模块,不同的std和第三方的板条箱。但是每个包都使用自己的错误类型,如果我们使用自己的错误类型,我们应该将这些错误转换为错误类型。我们可以使用std::convert::From标准化特征进行这些转换。// traits inside Rust standard library core convert module/ std::convertpub trait From<T>: Sized { fn from(_: T) -> Self;}????如您所知,String::from()函数用于创建String from &str数据类型。实际上这也是std::convert::From特质的实现。让我们看看如何在自定义错误类型上实现std::convert::From特征。use std::fs::File;use std::io;#[derive(Debug)]struct AppError { kind: String, // type of the error message: String, // error message}// Implement std::convert::From for AppError; from io::Errorimpl From<io::Error> for AppError { fn from(error: io::Error) -> Self { AppError { kind: String::from(“io”), message: error.to_string(), } }}fn main() -> Result<(), AppError> { let _file = File::open(“nonexistent_file.txt”)?; // This generates an io::Error. But because of return type is Result<(), AppError>, it converts to AppError Ok(())}// ————— Run time error —————Error: AppError { kind: “io”, message: “No such file or directory (os error 2)” }在上面的例子中,File::open(“nonexistent.txt”)?产生std::io::Error。但由于返回类型是Result<(), AppError>,它转换为AppError。因为我们正在从main()函数传播错误,所以它会打印出Err的Debug表示形式。在上面的例子中,我们只处理一种std错误类型std::io::Error。让我们看一些处理多种std错误类型的例子。use std::fs::File;use std::io::{self, Read};use std::num;#[derive(Debug)]struct AppError { kind: String, message: String,}// Implement std::convert::From for AppError; from io::Errorimpl From<io::Error> for AppError { fn from(error: io::Error) -> Self { AppError { kind: String::from(“io”), message: error.to_string(), } }}// Implement std::convert::From for AppError; from num::ParseIntErrorimpl From<num::ParseIntError> for AppError { fn from(error: num::ParseIntError) -> Self { AppError { kind: String::from(“parse”), message: error.to_string(), } }}fn main() -> Result<(), AppError> { let mut file = File::open(“hello_world.txt”)?; // generates an io::Error, if can not open the file and converts to an AppError let mut content = String::new(); file.read_to_string(&mut content)?; // generates an io::Error, if can not read file content and converts to an AppError let _number: usize; _number = content.parse()?; // generates num::ParseIntError, if can not convert file content to usize and converts to an AppError Ok(())}// ————— Few possible run time errors —————// 01. If hello_world.txt is a nonexistent fileError: AppError { kind: “io”, message: “No such file or directory (os error 2)” }// 02. If user doesn’t have relevant permission to access hello_world.txtError: AppError { kind: “io”, message: “Permission denied (os error 13)” }// 03. If hello_world.txt contains non-numeric content. ex Hello, world!Error: AppError { kind: “parse”, message: “invalid digit found in string” }???? 搜索有关实现的内容std::io::ErrorKind,以了解如何进一步组织错误类型。 ...

December 14, 2018 · 18 min · jiezi

支付宝移动端动态化方案实践

小蚂蚁说:此前分享的《模块化与解耦式开发在蚂蚁金服 mPaaS 深度实践探讨》(想要了解更多相关内容,欢迎关注公众号:mPaaS )已经对支付宝在移动端开发架构的设计思路有了初步了解。本文将结合在 iWeb 武汉站的分享,带领大家进一步了解 mPaaS在移动端动态化方案设计。分享内容将分为以下三个方面:支付宝动态化方案的探索;Nebula 框架浅析;mPaaS 科技赋能1.支付宝动态化方案的探索任何一种技术方案都是在业务的发展和架构的演化中逐渐探索出来的,因此我们来看一下支付宝架构的演进:支付宝从最开始的工具型应用,逐渐发展成平台型应用,一直到现在,已经成为了一个超级 App,它拥有多应用的生态,更加开放和动态化,并且保持着高可用,高性能,高灵敏的强大特性。随着 App 的逐渐庞大,整个应用的架构也在进行不断的调整,来适应各种特性。现在的支付宝客户端的架构如图所示:整体架构分为五层:容器层、组件层、框架层、服务层、应用层。客户端整体采用统一的框架开发,模块化的开发模式,完全插件式的容器,支持模块独立发布,方便大规模团队的并行开发。在这样的框架结构中,同样包括了我们的动态化方案。支付宝中的动态化方案主要有两种框架:Nebula 和小程序。这两种方案不仅解决了需求迭代速度和发版周期之间的矛盾、跨平台开发、实时发布等一些普适问题,而且有效地保证了发布质量,对线上问题进行紧急止血,同时也有助于建立良好的开放生态。Nebula 是支付宝移动端 Hybrid 解决方案,它提供了良好的外部扩展功能,拥有功能插件化、事件机制、JSApi 定制和 H5App 推送更新管理能力。主要功能包括:接入 H5App 后台,方便管理离线或者在线 H5App,实现 H5 应用的推送、更新。加载 H5 页面,并按照 Session 的概念进行管理各个页面。Android 使用 UCWebView,拥有解决系统级 Webview Crash 的能力,内存管理更合理,网络加载提升更快,兼容性更好。彻底告别了在Android下兼容不同 Webview 的问题。支持自定义网络库,自定义网络通道;支持自定义键盘,自定义 Native View替换 H5 标签。提供丰富的内置 JSAPI,实现譬如页面 push、pop,标题设置等功能;支持用户自定义 JSAPI 和插件功能,扩展业务需求。自带埋点功能,接入 H5 监控平台,能够实时看到页面加载的性能、错误报警和监控。还有一种动态化方案就是支付宝小程序:支付宝小程序是一种全新的开发模式,融合了 H5 的易开发性、跨平台性、Native 性能,让开发者可以快速开发高性能的页面,提供优异的用户体验。通过使用支付宝小程序,开发者为支付宝开发了大量优质的小程序,丰富了支付宝生态能力。小程序开放给开发者更多的 JSAPI 和 OpenAPI 能力,通过小程序可以为用户提供多样化便捷服务。2.Nebula框架浅析Nebula 的架构如图所示,从上至下依次为 H5 应用层、服务层、原生框架层:H5 应用层:基于 HTML 和 JavaScript 技术开发,在 H5 容器上运行的手机应用,它拥有跨平台的特性,配合离线包的使用可以完成实时热修复的功能。服务层:为开发者提供了高阶语言的 API 来使用手机系统资源,包括: 视窗系统,开发者可以使用它来创造应用 UI,包括文字,图片,按键及定制 View 资源管理,通过它开发者可以方便的访问如多语言文字,图片和布局等非代码资源 应用生命周期管理,它决定应用在手机系统里的开始,结束以及强制关闭的时机 H5 容器原生框架层:是手机系统的基础层,它提供了标准 API 来让高阶语言(比如 Java 和 Object-C)使用底层的硬件,并包含了许多为硬件访问的专有软件库。当上层调用某个框架 API 来访问硬件时,手机系统将加载相应的软件库。整个 Nebula 框架的核心部分就是 H5 容器,下面看一下 H5 容器的结构:在容器里面有 H5Service、H5Session 和 H5Page 这样三个概念H5Service,H5Session 和 H5Page 都是从 H5CoreNode 类扩展而来,以 H5Service 为根节点,它与其他类一同形成了树状结构构成了页面流程和导航。在一般情况下,H5Pages 是 H5Session 的子节点,而 H5Sessions 是 H5Service 的子节点,在任何情况下只有一个 H5Service 根节点存在。H5Service:是 Nebula 里维护 H5 应用全局状态的基础类, 在 H5 应用的生命周期内只有一个 H5Service 的单例全局实例,H5Service 可以进行的操作包括: 创建且打开一个新的 Web activity 创建且开启一个新的 Web page 从共享空间存储和读取数据 注册插件和 Provider 监听应用的生命周期H5Session:一个 H5Session 是由一叠 H5Pages 组成的完整业务流程。例如一个收银台的流程包括:一个购物车的小结页面,一个结账方式的选择页面,和最后一个结账确认页面。所有的页面都可以独立存在运作, H5Session 在其中的作用是把这些页面组织起立,按照业务逻辑把它们按序排列来完成业务。当你使用 H5Service 创建且开启一个新的 Web page 时,如果当前没有 H5Session 的话,一个新的 H5Session 实例将被创建,并为后续创建的 Web page 共享。你可以从 H5Session 中移除页面直到页面叠为空,也可以使用 H5Session 所提供的方法来获取首页,以及监听该 H5Session 的生命周期。H5Page:是用户看得见,摸得着的页面,也是应用生命周期中最重要的一部分。你可以通过 URL 来加载内容,用 H5Param 来定制页面的外观和行为,甚至可以通过获取 H5Page 的视图层次,把 H5Page 视图和其他原生 UI 部件一起内嵌到同一个布局中。下面是 H5 容器的几个重要组成部分:API 管理器主要管理 JS API:Nebula 中已经提供许多内置的 JS API 供开发者使用,比如操控 UI,显示对话框和 Toast,以及使用网络 RCP 服务等。插件管理器主要管理 Plugin:如果现有的 JS API 无法满足你的业务需求,你也可以选择创造一个新的插件。你只需把原生代码打包在插件中,在管理器里注册该插件,便可在 Javascript 层使用新的 JS API 了JS Bridge 是连接原生层和 Javascript 的沟通桥梁:它将 Javascript 代码转译成能在系统框架运行的字节码,同时也把原生层的数据结构转成 Javascript 对象使其能在 Javascript 层处理。这里 Nebula 针对JS Bridge 做了一些优化: 在 Android 里,js 调用 native 的通讯是通过 console.log 的方式,这个和其他容器实现不一样,其他容器一般通过 prompt 的方式来实现的,但是使用 prompt 的方式,有两个弊端: 使用 prompt 会阻断整个浏览器的进程,如果 native 处理时间一长,会导致页面假死。 prompt 是在 UI 层面上会弹出一个模态窗口,在 native 没有对其进行捕获处理的话,会造成一个问题。一旦这个页面放在非此容器的环境下,就会出现一个很诡异的 prompt 弹窗。在支付宝内,曾经出现过这个问题,天猫页面在支付宝 app 里的时候,由于容器机制不同,页面中 bridge 脚本没有判断环境,导致页面中 js 调用 API 的时候,在页面上出现了 prompt 的模态对话框,严重影响了用户体验,但是如果使用 console.log 的话,就不会出现这个问题。console 的方式避免了不兼容环境的体验问题和同时也避免了页面的假死。 jsbridge 注入的时机,由于业务逻辑要依赖 bridge,所以业务的所有逻辑都会在 bridge ready 之后才会触发,而 bridge js 本身运行是要一定的时间的,因此注入的时机对于性能的影响显得非常的重要。但由于 H5 页面的生命周期和容器的生命周期是相互独立的,因此在 H5 生命周期的哪个阶段注入这段 bridgejs,对于性能的影响就显得异常重要。 现在在支付宝内使用的方式为监听 H5 生活周期的事件,比如说当 Webview 设置 title 结束之后,Android 会放出一个 onReceivedTitle、shouldInterceptRequest 等事件,iOS 会尝试在 webViewDidStartLoad 事件,在监听到这些事件之后,立即注入 bridgejs,让其在 H5 生命周期尽早运行。通过这种方式的注入,经过测试,最早能在页面加载开始后, 50ms 以内就能成功注入 bridgejs。Event 机制:Nebula 提供了一套事件机制来管理事件在 H5Page,H5Session 和 H5Service 之间的流通顺序。一个 H5Event 可以在 H5Page, H5Session 或 H5Service 任何一层发生,事件派遣分为两步完成事件拦截。这个步骤中事件派遣的顺序为 H5Service -> H5Session or H5Page。事件可以在任何节点被拦截 (如果 interceptEvent() 返回 true ),也可以在任何节点被处理 (如果 handleEvent() 返回 true ):如果事件在派遣过程中被拦截或处理,该事件将被视为已被消费且不再继续流通。如果在派遣过程后事件依旧没有被拦截或处理,会有错误抛给呼叫方处理。仅仅使用传统的 H5 技术展示在线页面,很容易受到网络环境影响,因而降低 H5 页面的性能。在 Neblua 中我们使用离线包技术来解决这个问题。离线包是将包括 HTML、Javascript、CSS 等页面内静态资源打包到一个压缩包内,它的目录结构如图所示:使用离线包可以使容器内的 H5 应用具有接近 Native 的体验,主要优势如下::减少网络环境对 H5 应用的影响:通过下载离线包到本地,然后在客户端打开,把打开H5页面的操作从网络 IO,变成磁盘 IO。直接从本地加载离线包,不仅最大程度地摆脱网络环境对 H5 页面的影响,而且增强了用户体验。提升用户打开 H5 应用的体验:通过离线包的方式把页面内静态资源嵌入到应用中并发布,当用户第一次开启应用的时候,就无需依赖网络环境下载该资源,而是马上开始使用该应用。实现动态更新:在推出新版本或是紧急发布的时候,您可以把修改的资源放入离线包,通过更新配置让应用自动下载更新。因此,您无需通过应用商店审核,就能让用户及早接收更新。下面介绍一下离线包的渲染过程当 H5 容器发出资源请求时,其访问本地资源或线上资源所使用的 URL 是一致的。H5 容器会先截获该请求,截获请求后,发生如下情况:如果本地有资源可以满足该请求的话,H5 容器会使用本地资源。如果没有可以满足请求的本地资源,H5 容器会使用线上资源。因此,无论资源是在本地或者是线上,WebView 都是无感知的。离线包的下载依赖用户当前的网络。一般情况下,只有在连接 WIFI 的情况下才会在后台下载离线包。如果用户处于移动网络下,不会在后台下载离线包。如果当前用户点击 APP,离线包没有下载好,用户就要等待离线包下载好才能用。为了解决离线包不可用的场景,fallback 技术应运而生。每个离线包发布的时候,都会同步在 CDN 发布一个对应的线上版本,目录结构和离线包结构一致。fallback 地址会随离线包信息下发到本地。在离线包没有下载好的场景下,客户端会拦截页面请求,转向对应的 CDN 地址, 实现在线页面和离线页面随时切换。那么本地资源如何寻址呢,我们设计了独特的虚拟域名机制,仅对离线应用有效。当页面保存在客户端之后,WebView 如果要访问的话,是通过 file schema 来从本地加载访问的。然而,用户就能在地址栏里直接看到 file 的路径,这就会导致以下问题:用户体验问题:当用户看到了 file 地址,会对暴露的地址产生不安全感和不自在。安全性问题:由于 file 协议上直接带上了本地路径,任何用户都可以看到这个文件所在的文件路径,会存在一定的安全隐患。基于如上问题的考虑,采用虚拟域名的机制而不直接使用 file 路径来访问。虚拟域名是一个符合 URL Scheme 规范的 HTTPS 域名地址,例如 https://xxxxxxx.h5app.example.comNebula 里面的 H5 容器和离线包,在传统的 Hybrid 框架的基础上进行了极致的优化,使整个 H5 应用具有如下特点:对网络链路强依赖的弱化增强对设备能力的支持增强的用户体验在性能方面,Nebula 在支付宝中经过了亿级用户的考验,crash 和 anr 以及其他稳定性指标有保障。Android 平台基于 UCWebview 深度定制,crash 率和 anr 量远低于系统webview,拥有解决系统 Webview 问题的能力。图中展示的就是在 Android 端,UCWebview 和系统 Webview 之间崩溃率和 ANR 率的对比,稳定性的优势显而易见。最后看一下小程序与 Nebula支付宝小程序复用 Nebula 容器技术栈,重构了开发方式,对外暴露有限个 jsapi 接口,让 app 开发更简单,更加便捷利用支付宝的能力,进而发布、推广、运营。小程序本质上是也是一个 H5 App 离线包,但是有一些自己的特点。小程序是为第三方 App 服务的,运行在独立进程,它的稳定和闪退不会影响到主 App,也支持二方 App 运行在主进程。小程序是支持保活的,极大的提升二次打开的体验。3. mPaaS技术架构与助力Nebula 有这么大优势,现在不仅在蚂蚁金服内部使用,也能够提供给外部来使用。首先什么是 mPaaS 呢,mPaaS 全称是 Mobile Platform as a Service 。是蚂蚁金服独创的移动研发平台,它源于支付宝 App 近 10 年的移动技术实践和思考,为移动开发、测试、运营及运维提供云到端的一站式解决方案,能有效降低技术门槛、减少研发成本、提升开发效率,协助生态伙伴快速搭建稳定高质量的移动 App。在 mPaaS 中,我们将 Nebula 的 H5 容器、jsapi 、离线包、小程序这些模块作为一个单独的组件来进行输出,在客户端中进行配置。任何一个 App 通过 mPaaS 插件,添加对应的模块,集成这些功能,只需要这样简单的操作,就可以让你的应用具有和支付宝一样强大的动态化能力。 同时 mPaaS 提供的小程序模块,允许用户把运行在支付宝上的小程序,无缝的迁移到自己的 App 中,做到【跨平台跨应用】开发,提高代码的复用能力Nebula 组件化输出,配合 mPaaS 提供的 MDS (移动发布服务) 来实现动态更新。mPaaS 提供的 MDS 服务,能够让每次发布更新就像发邮件一样简单。MDS 具有智能灰度发布的能力,可以通过内部灰度,外部灰度多重验证,保证在正式发布之前,发布的产品质量有充分的保证,同时提供多种升级策略,包括指定人群地域、机型,系统版本,网络环境等多种规则。对于离线包来说,更新离线包的下载对网络环境要求较高,包的大小越大,更新的成功率越低,在 mPaaS 中,我们采用增量差分的更新能力,减少数据冗余及设备带宽,在移动网络条件下优势明显。同时保证更新功能的高可用性,升级接口可用率达 99.99%,在线分钟级触达。下面是 Nebula 的生态基础,首先在集团内部,我们已经支持了不少产品,同时通过 mPaaS ,我们也与外部客户合作,将我们的技术能力输出给他们,典型的几个案例,包括 12306 客户端,广发发现精彩客户端,上海地铁,苏州银行等。尤其是 12306,使用 mPaaS 改版之后,客户端整体的体验更加优越。12306 整个客户端绝大部分都是使用的 H5 技术,他们就是使用 Nebula H5 容器 配合离线包来实现,无论是页面打开速度,还是UI事件响应,体验几乎接近 native。在更新发布方面,12306 的 app 包很少更新,以 AppStore 上的发布记录来看,今年只提交了两个版本,基本上都是通过动态化的方式完成业务的迭代发布。总结下来,蚂蚁金服 mPaaS 中就是通过「Nebula H5容器 + 离线包 + 小程序 + MDS」这样的方式来实现移动端的动态化方案。想要上手体验看看吗?欢迎下载 Android Demo:注:暂时不支持 iOS 用户扫码下载 ...

November 17, 2018 · 3 min · jiezi

Rust使用Actix-Web验证Auth Web微服务 - 完整教程第2部分

本文同步于Rust中文社区验证用户电子邮件从第一部分开始,我们现在拥有一个服务器,它从请求中获取一个电子邮件地址,并使用invitation(邀请)对象发出JSON响应。在第一部分中,我说我们将向用户发送一封电子邮件,经过一番思考和反馈,我们现在将跳过这一部分(请注意第3部分)。我使用的服务是sparkpost,你作为本教程的读者可能没有他们的帐户(免费用于小用途)。警告:如果没有正确的电子邮件验证,请不要在任何真实应用中使用此解决方法解决方法现在我们将使用来自服务器的http响应来验证电子邮件。创建电子邮件验证的最简单方法是让我们的服务器使用通过电子邮件发送到用户电子邮件的某种秘密,并让他们单击带有秘密的链接进行验证。我们可以使用UUID邀请对象作为秘密。假设客户在使用uuid输入电子邮件后收到邀请67a68837-a059-43e6-a0b8-6e57e6260f0d。我们可以发送请求UUID在网址中注册具有上述内容的新用户。我们的服务器可以获取该id并在数据库中找到Invitation对象,然后将到期日期与当前时间进行比较。如果所有这些条件都成立,我们将让用户注册,否则返回错误响应。现在我们将邀请对象作为解决方法返回给客户端。电子邮件支持将在第3部分中实现。我们可以发送请求UUID在网址中注册具有上述内容的新用户。我们的服务器可以获取该id并在数据库中找到Invitation对象,然后将到期日期与当前时间进行比较。如果所有这些条件都成立,我们将让用户注册,否则返回错误响应。现在我们将邀请对象作为解决方法返回给客户端。电子邮件支持将在第3部分中实现。错误处理和FROMTraitRust提供了非常强大的工具,我们可以使用它们将一种错误转换为另一种错误。在这个应用程序中,我们将使用不同的插入操作进行一些操作,即使用柴油保存数据,使用bcrypt保存密码等。这些操作可能会返回错误,但我们需要将它们转换为我们的自定义错误类型。std::convert::From是一个Trait,允许我们转换它。在这里阅读更多有关From特征的信息。通过实现From特征,我们可以使用?运算符来传播将转换为我们的ServiceError类型的许多不同类型的错误。我们的错误定义在errors.rs,让我们通过为uuid和diesel错误添加impl From来实现一些From特性,我们还将为ServiceError枚举添加一个Unauthorized变量。该文件如下所示:// errors.rsuse actix_web::{error::ResponseError, HttpResponse};use std::convert::From;use diesel::result::{DatabaseErrorKind, Error};use uuid::ParseError;#[derive(Fail, Debug)]pub enum ServiceError { #[fail(display = “Internal Server Error”)] InternalServerError, #[fail(display = “BadRequest: {}”, 0)] BadRequest(String), #[fail(display = “Unauthorized”)] Unauthorized,}// impl ResponseError trait allows to convert our errors into http responses with appropriate dataimpl ResponseError for ServiceError { fn error_response(&self) -> HttpResponse { match *self { ServiceError::InternalServerError => HttpResponse::InternalServerError().json(“Internal Server Error, Please try later”), ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message), ServiceError::Unauthorized => HttpResponse::Unauthorized().json(“Unauthorized”) } }}// we can return early in our handlers if UUID provided by the user is not valid// and provide a custom messageimpl From<ParseError> for ServiceError { fn from(: ParseError) -> ServiceError { ServiceError::BadRequest(“Invalid UUID”.into()) }}impl From<Error> for ServiceError { fn from(error: Error) -> ServiceError { // Right now we just care about UniqueViolation from diesel // But this would be helpful to easily map errors as our app grows match error { Error::DatabaseError(kind, info) => { if let DatabaseErrorKind::UniqueViolation = kind { let message = info.details().unwrap_or_else(|| info.message()).to_string(); return ServiceError::BadRequest(message); } ServiceError::InternalServerError } _ => ServiceError::InternalServerError } }}这一切都将让我们做事变得方便。得到一些帮助我们有时需要一些帮助。在将密码存储到数据库之前,我们需要对密码进行哈希处理。在Reddit rust community有一个建议可以使用什么算法。在这里建议argon2。但为了简单起见,我决定使用bcrypt。bcrypt算法在生产中被广泛使用,并且bcrypt crate提供了一个非常好的接口来散列和验证密码。为了将一些问题分开,我们创建一个新文件src/utils.rs并定义一个帮助程序哈希函数,如下所示。//utils.rsuse bcrypt::{hash, DEFAULT_COST};use errors::ServiceError;use std::env;pub fn hash_password(plain: &str) -> Result<String, ServiceError> { // get the hashing cost from the env variable or use default let hashing_cost: u32 = match env::var(“HASH_ROUNDS”) { Ok(cost) => cost.parse().unwrap_or(DEFAULT_COST), _ => DEFAULT_COST, }; hash(plain, hashing_cost).map_err(|_| ServiceError::InternalServerError)}您可能已经注意到我们返回一个Result并使用map_error()来返回我们的自定义错误。这是为了允许稍后在我们调用此函数时使用?运算符(另一种转换错误的方法是为Frombcrypt函数返回的错误实现特征)。当我们在这里时,让我们为上一个教程models.rs中定义的User结构添加一个方便的方法。我们还删除了remove_pwd()方法,而是定义了另一个SlimUser没有密码字段的结构。我们实现Fromtrait来从User生成SlimUser。当我们使用它时,一切都会变得清晰。use chrono::{NaiveDateTime, Local};use std::convert::From;//… snipimpl User { pub fn with_details(email: String, password: String) -> Self { User { email, password, created_at: Local::now().naive_local(), } }}//–snip#[derive(Debug, Serialize, Deserialize)]pub struct SlimUser { pub email: String,}impl From<User> for SlimUser { fn from(user: User) -> Self { SlimUser { email: user.email } }}不要忘记添加extern crate bcrypt;并mod utils在您的main.rs。我在第一部分忘记了另一个是登录到控制台。要启用它,请将以下内容添加到main.rsextern crate env_logger;// –snipfn main(){ dotenv().ok(); std::env::set_var(“RUST_LOG”, “simple-auth-server=debug,actix_web=info”); env_logger::init(); //–snip}注册用户如果您还记得上一个教程,我们为Invitation创建了一个handler 程序,现在让我们创建一个注册用户的处理程序。我们将创建一个RegisterUser包含一些数据的结构,允许我们验证邀请,然后从数据库创建并返回一个用户。创建一个新文件src/register_handler.rs并添加mod register_handler到您的文件中main.rs。// register_handler.rsuse actix::{Handler, Message};use chrono::Local;use diesel::prelude::;use errors::ServiceError;use models::{DbExecutor, Invitation, User, SlimUser};use uuid::Uuid;use utils::hash_password;// UserData is used to extract data from a post request by the client#[derive(Debug, Deserialize)]pub struct UserData { pub password: String,}// to be used to send data via the Actix actor system#[derive(Debug)]pub struct RegisterUser { pub invitation_id: String, pub password: String,}impl Message for RegisterUser { type Result = Result<SlimUser, ServiceError>;}impl Handler<RegisterUser> for DbExecutor { type Result = Result<SlimUser, ServiceError>; fn handle(&mut self, msg: RegisterUser, : &mut Self::Context) -> Self::Result { use schema::invitations::dsl::{invitations, id}; use schema::users::dsl::users; let conn: &PgConnection = &self.0.get().unwrap(); // try parsing the string provided by the user as url parameter // return early with error that will be converted to ServiceError let invitation_id = Uuid::parse_str(&msg.invitation_id)?; invitations.filter(id.eq(invitation_id)) .load::<Invitation>(conn) .map_err(|db_error| ServiceError::BadRequest(“Invalid Invitation”.into())) .and_then(|mut result| { if let Some(invitation) = result.pop() { // if invitation is not expired if invitation.expires_at > Local::now().naive_local() { // try hashing the password, else return the error that will be converted to ServiceError let password: String = hash_password(&msg.password)?; let user = User::with_details(invitation.email, password); let inserted_user: User = diesel::insert_into(users) .values(&user) .get_result(conn)?; return Ok(inserted_user.into()); // convert User to SlimUser } } Err(ServiceError::BadRequest(“Invalid Invitation”.into())) }) }}解析URL参数actix-web有许多简单的方法可以从请求中提取数据。其中一种方法是使用Path提取器。Path提供可从Request的路径中提取的信息。您可以从路径反序列化任何变量段。这将允许我们为每个要注册为用户的邀请创建唯一路径。让我们修改app.rs文件中的寄存器路由,并添加一个稍后我们将实现的处理函数。// app.rs/// creates and returns the app after mounting all routes/resources// add use statement at the top.use register_routes::register_user;//…snippub fn create_app(db: Addr<DbExecutor>) -> App<AppState> { App::with_state(AppState { db }) //… snip // routes to register as a user .resource("/register/{invitation_id}", |r| { r.method(Method::POST).with(register_user); })}您可能希望暂时注释更改,因为事情未实现并保持您的应用程序已编译并运行。(我尽可能地做,持续反馈)。我们现在需要的是实现register_user()函数,该函数从客户端发送的请求中提取数据,通过向RegisterUserActor 发送消息来调用处理程序。除了url参数,我们还需要从客户端提取密码。我们已经为此创建了一个UserData结构体在register_handler.rs。我们将使用类型Json来创建UserData结构。Json允许将请求主体反序列化为结构体。要从请求的主体中提取类型信息,类型T必须实现serde的反序列化特征。创建一个新文件src/register_routes.rs并添加mod register_routes到您的文件中main.rs。use actix_web::{AsyncResponder, FutureResponse, HttpResponse, ResponseError, State, Json, Path};use futures::future::Future;use app::AppState;use register_handler::{RegisterUser, UserData};pub fn register_user((invitation_id, user_data, state): (Path<String>, Json<UserData>, State<AppState>)) -> FutureResponse<HttpResponse> { let msg = RegisterUser { // into_inner() returns the inner string value from Path invitation_id: invitation_id.into_inner(), password: user_data.password.clone(), }; state.db.send(msg) .from_err() .and_then(|db_response| match db_response { Ok(slim_user) => Ok(HttpResponse::Ok().json(slim_user)), Err(service_error) => Ok(service_error.error_response()), }).responder()}测试您的实现如果你有任何错误,在处理完错误后,让我们给它一个测试curl –request POST \ –url http://localhost:3000/invitation \ –header ‘content-type: application/json’ \ –data ‘{ “email”:“name@domain.com”}‘应该返回类似的东西{ “id”: “f87910d7-0e33-4ded-a8d8-2264800d1783”, “email”: “name@domain.com”, “expires_at”: “2018-10-27T13:02:00.909757”}想象一下,我们通过创建一个链接来向用户发送电子邮件,该链接包含一个供用户填写的表单。从那里我们会让我们的客户向http:// localhost:3000 / register / f87910d7-0e33-4ded-a8d8-2264800d1783发送请求。为了演示本演示,您可以使用以下测试命令测试您的应用程序。curl –request POST \ –url http://localhost:3000/register/f87910d7-0e33-4ded-a8d8-2264800d1783 \ –header ‘content-type: application/json’ \ –data ‘{“password”:“password”}‘应该返回类似的东西{ “email”: “name@domain.com”}再次运行该命令将导致错误"Key (email)=(name@domain.com) already exists.“恭喜您现在拥有一个可以邀请,验证和创建用户的Web服务,甚至可以向您发送半有用的错误消息。????????我们来做AUTH根据w3.org:基于令牌的身份验证系统背后的一般概念很简单。允许用户输入用户名和密码以获取允许他们获取特定资源的令牌 - 而无需使用他们的用户名和密码。一旦获得其令牌,用户就可以向远程站点提供令牌 - 其提供对特定资源的访问一段时间。现在,您如何选择交换该令牌可能会产生安全隐患。您会在互联网上找到许多讨论/辩论以及人们使用的许多方式。我非常警惕在客户端存储可由客户端JavaScript访问的东西。不幸的是,这种方法在各地成千上万的教程中提出 这是一个很好的阅读停止使用JWT进行会话。这里我不确定,除了你之外,作为读者还有什么建议你don’t follow online tutorials blindly and do your own research。本教程的目的是了解Actix-web和rust,而不是如何防止服务器漏洞。为了本教程的目的,我们将仅使用http的cookie来交换令牌。请不要在生产中使用。现在,这就是????,让我们看看我们能在这里做些什么。actix-web为我们提供了一种巧妙的方法,作为处理会话cookie的中间件,这里记录了actix_web :: middleware :: identity。要启用此功能,我们修改app.rs文件如下。use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService};use chrono::Duration;//–snippub fn create_app(db: Addr<DbExecutor>) -> App<AppState> { // secret is a random 32 character long base 64 string let secret: String = std::env::var(“SECRET_KEY”).unwrap_or_else(|| “0”.repeat(32)); let domain: String = std::env::var(“DOMAIN”).unwrap_or_else(|| “localhost”.to_string()); App::with_state(AppState { db }) .middleware(Logger::default()) .middleware(IdentityService::new( CookieIdentityPolicy::new(secret.as_bytes()) .name(“auth”) .path(”/") .domain(domain.as_str()) .max_age(Duration::days(1)) // just for testing .secure(false), )) //–snip}很方便的方法,如req.remember(data),req.identity()和req.forget()等操作HttpRequest的路由参数。这反过来将设置和删除客户端的cookie身份验证。JWT在编写本教程时,我遇到了一些关于要使用什么JWT lib的讨论。从一个简单的搜索我发现了一些,并决定与frank_jwt一起,然后文森特指出不完整性,建议使用jsonwebtoken。使用该lib后遇到问题我得到了很好的响应。现在repo有工作示例,我能够实现以下默认解决方案。请注意,这不是JWT最安全的实现,您可能希望查找资源以使其更好地满足您的需求。在我们创建auth处理程序和路由函数之前,让我们为util.rs添加一些jwt编码和解码辅助函数。别忘了加入extern crate jsonwebtoken as jwt在你的main.rs。如果有人有更好的实施,我会很乐意接受。// utils.rsuse models::SlimUser;use std::convert::From;use jwt::{decode, encode, Header, Validation};use chrono::{Local, Duration};//–snip#[derive(Debug, Serialize, Deserialize)]struct Claims { // issuer iss: String, // subject sub: String, //issued at iat: i64, // expiry exp: i64, // user email email: String,}// struct to get converted to token and backimpl Claims { fn with_email(email: &str) -> Self { Claims { iss: “localhost”.into(), sub: “auth”.into(), email: email.to_owned(), iat: Local::now().timestamp(), exp: (Local::now() + Duration::hours(24)).timestamp(), } }}impl From<Claims> for SlimUser { fn from(claims: Claims) -> Self { SlimUser { email: claims.email } }}pub fn create_token(data: &SlimUser) -> Result<String, ServiceError> { let claims = Claims::with_email(data.email.as_str()); encode(&Header::default(), &claims, get_secret().as_ref()) .map_err(|_err| ServiceError::InternalServerError)}pub fn decode_token(token: &str) -> Result<SlimUser, ServiceError> { decode::<Claims>(token, get_secret().as_ref(), &Validation::default()) .map(|data| Ok(data.claims.into())) .map_err(|_err| ServiceError::Unauthorized)?}// take a string from env variablefn get_secret() -> String { env::var(“JWT_SECRET”).unwrap_or(“my secret”.into())}验证处理让我们创建一个新文件src/auth_handler.rs并给你main.rs添加mod auth_handler。//auth_handler.rsuse actix::{Handler, Message};use diesel::prelude::;use errors::ServiceError;use models::{DbExecutor, User, SlimUser};use bcrypt::verify;use actix_web::{FromRequest, HttpRequest, middleware::identity::RequestIdentity};#[derive(Debug, Deserialize)]pub struct AuthData { pub email: String, pub password: String,}impl Message for AuthData { type Result = Result<SlimUser, ServiceError>;}impl Handler<AuthData> for DbExecutor { type Result = Result<SlimUser, ServiceError>; fn handle(&mut self, msg: AuthData, : &mut Self::Context) -> Self::Result { use schema::users::dsl::{users, email}; let conn: &PgConnection = &self.0.get().unwrap(); let mismatch_error = Err(ServiceError::BadRequest(“Username and Password don’t match”.into())); let mut items = users .filter(email.eq(&msg.email)) .load::<User>(conn)?; if let Some(user) = items.pop() { match verify(&msg.password, &user.password) { Ok(matching) => { if matching { return Ok(user.into()); } else { return mismatch_error; } } Err() => { return mismatch_error; } } } mismatch_error }}上面的处理程序采用AuthData包含客户端发送的电子邮件和密码的结构。我们使用该电子邮件从数据库中提取用户并使用bcrypt verify函数来匹配密码。如果一切顺利,我们返回用户或我们返回BadRequest错误。现在让我们创建src/auth_routes.rs以下内容:// auth_routes.rsuse actix_web::{AsyncResponder, FutureResponse, HttpResponse, HttpRequest, ResponseError, Json};use actix_web::middleware::identity::RequestIdentity;use futures::future::Future;use utils::create_token;use app::AppState;use auth_handler::AuthData;pub fn login((auth_data, req): (Json<AuthData>, HttpRequest<AppState>)) -> FutureResponse<HttpResponse> { req.state() .db .send(auth_data.into_inner()) .from_err() .and_then(move |res| match res { Ok(slim_user) => { let token = create_token(&slim_user)?; req.remember(token); Ok(HttpResponse::Ok().into()) } Err(err) => Ok(err.error_response()), }).responder()}pub fn logout(req: HttpRequest<AppState>) -> HttpResponse { req.forget(); HttpResponse::Ok().into()}我们的login方法提取AuthData from请求并向我们在auth_handler.rs中实现的DbEexcutorActor处理程序发送消息。如果一切都很好,我们会让用户返回给我们,我们使用我们之前在utils.rs中定义的辅助函数来创建一个令牌和调用req.remember(token`)。这又设置了一个带有令牌的cookie头,供客户端保存。我们现在需要做的最后一件事是app.rs使用我们的登录/注销功能。将.rsource("/auth")闭包更改为以下内容:.resource("/auth", |r| { r.method(Method::POST).with(login); r.method(Method::DELETE).with(logout); })不要忘记在文件的顶部添加use auth_routes::{login, logout};试运行AUTH如果您一直关注本教程,那么您已经创建了一个使用电子邮件和密码的用户。使用以下curl命令测试我们的服务器。curl -i –request POST \ –url http://localhost:3000/auth \ –header ‘content-type: application/json’ \ –data ‘{ “email”: “name@domain.com”, “password”:“password”}’## responseHTTP/1.1 200 OKset-cookie: auth=iqsB4KUUjXUjnNRl1dVx9lKiRfH24itiNdJjTAJsU4CcaetPpaSWfrNq6IIoVR5+qKPEVTrUeg==; HttpOnly; Path=/; Domain=localhost; Max-Age=86400content-length: 0date: Sun, 28 Oct 2018 12:36:43 GMT如果你收到了如上所述的带有set-cookie标头的200响应,恭喜你已成功登录。为了测试注销,我们向/auth它发送一个DELETE请求,确保你得到带有空数据和即时到期日的set-cookie头。curl -i –request DELETE \ –url http://localhost:3000/auth## responseHTTP/1.1 200 OKset-cookie: auth=; HttpOnly; Path=/; Domain=localhost; Max-Age=0; Expires=Fri, 27 Oct 2017 13:01:52 GMTcontent-length: 0date: Sat, 27 Oct 2018 13:01:52 GMT实现受保护的路由使Auth的全部意义在于验证请求是否来自经过身份验证的客户端。Actix-web有一个特性FromRequest,我们可以在任何类型上实现,然后使用它从请求中提取数据。见文档这里。我们将在auth_handler.rs底部添加以下内容。//auth_handler.rs//–snipuse actix_web::FromRequest;use utils::decode_token;//–snip// we need the same data as SlimUser// simple aliasing makes the intentions clear and its more readablepub type LoggedUser = SlimUser;impl<S> FromRequest<S> for LoggedUser { type Config = (); type Result = Result<LoggedUser, ServiceError>; fn from_request(req: &HttpRequest<S>, _: &Self::Config) -> Self::Result { if let Some(identity) = req.identity() { let user: SlimUser = decode_token(&identity)?; return Ok(user as LoggedUser); } Err(ServiceError::Unauthorized) }}我们选择使用类型别名而不是创建一个全新的类型。当我们LoggedUser从请求中提取时,读者会知道它是经过身份验证的用户。FromRequest trait只是尝试将cookie中的字符串反序列化为我们的结构,如果失败则只返回Unauthorized错误。为了测试这个,我们需要添加一个实际路由或app。我们只是auth_routes.rs添加另一个函数//auth_routes.rs//–snippub fn get_me(logged_user: LoggedUser) -> HttpResponse { HttpResponse::Ok().json(logged_user)}要调用它,我们在app.rs资源中注册此方法。它看起来像是以下。//app.rsuse auth_routes::{login, logout, get_me};//–snip.resource("/auth", |r| { r.method(Method::POST).with(login); r.method(Method::DELETE).with(logout); r.method(Method::GET).with(get_me);})//–snip测试登录用户在终端中尝试以下Curl命令。curl -i –request POST \ –url http://localhost:3000/auth \ –header ‘content-type: application/json’ \ –data ‘{ “email”: “name@domain.com”, “password”:“password”}’# result would be something likeHTTP/1.1 200 OKset-cookie: auth=HdS0iPKTBL/4MpTmoUKQ5H7wft5kP7OjP6vbyd05Ex5flLvAkKd+P2GchG1jpvV6p9GQtzPEcg==; HttpOnly; Path=/; Domain=localhost; Max-Age=86400content-length: 0date: Sun, 28 Oct 2018 19:16:12 GMT## and then pass the cookie back for a get requestcurl -i –request GET \ –url http://localhost:3000/auth \ –cookie auth=HdS0iPKTBL/4MpTmoUKQ5H7wft5kP7OjP6vbyd05Ex5flLvAkKd+P2GchG1jpvV6p9GQtzPEcg==## resultHTTP/1.1 200 OKcontent-length: 27content-type: application/jsondate: Sun, 28 Oct 2018 19:21:04 GMT{“email”:“name@domain.com”}它应该以json的形式成功返回您的电子邮件。只有登录的用户或具有有效cookie身份验证和令牌的请求才会通过您提取的LoggedUser路由。下一步是什么在本教程的第3部分中,我们将为此应用程序创建电子邮件验证和前端。我希望使用某种rust的html模板系统。与此同时,我正在学习Angular,所以我可能会在它前面做一个小应用程序。英文原文 ...

November 16, 2018 · 6 min · jiezi

Rust使用Actix-Web验证Auth Web微服务 - 完整教程第1部分

Rust使用Actix-Web验证Auth Web微服务 - 完整教程第1部分文章出自Rust中文社区我们将创建一个rust仅处理用户注册和身份验证的Web服务器。我将在逐步解释每个文件中的步骤。完整的项目代码在这里。事件的流程如下所示:使用电子邮件地址注册➡接收带有链接的????进行验证点击链接➡使相同的电子邮件和密码注册使用电子邮件和密码登录➡获取验证并接收jwt令牌我们打算使用的包actix // Actix是一个Rust actor框架。actix-web // Actix web是Rust的一个简单,实用且极其快速的Web框架。brcypt //使用bcrypt轻松散列和验证密码。chrono // Rust的日期和时间库。diesel //用于PostgreSQL,SQLite和MySQL的安全,可扩展的ORM和查询生成器。dotenv // Rust的dotenv实现。env_logger //通过环境变量配置的日志记录实现。failure //实验性错误处理抽象。jsonwebtoken //以强类型方式创建和解析JWT。futures //future和Stream的实现,具有零分配,可组合性和类似迭代器的接口。r2d2 //通用连接池。serde //通用序列化/反序列化框架。serde_json // JSON序列化文件格式。serde_derive //#[derive(Serialize,Deserialize)]的宏1.1实现。sparkpost //用于sparkpost电子邮件api v1的Rust绑定。uuid //用于生成和解析UUID的库。我从他们的官方说明中提供了有关正在使用的包的简要信息。如果您想了解更多有关这些板条箱的信息,请转到crates.io。准备我将在这里假设您对编程有一些了解,最好还有一些Rust。需要进行工作设置rust。查看https://rustup.rs用于rust设置。我们将使用diesel来创建模型并处理数据库,查询和迁移。请前往http://diesel.rs/guides/getting-started/开始使用并进行设置diesel_cli。在本教程中我们将使用postgresql,请按照说明设置postgres。您需要有一个正在运行的postgres服务器,并且可以创建一个数据库来完成本教程。另一个很好的工具是Cargo Watch,它允许您在进行任何更改时观看文件系统并重新编译并重新运行应用程序。如果您的系统上已经没有安装Curl,请在本地测试api。让我们开始检查你的rust和cargo版本并创建一个新的项目# at the time of writing this tutorial my setup is rustc –version && cargo –version# rustc 1.29.1 (b801ae664 2018-09-20)# cargo 1.29.0 (524a578d7 2018-08-05)cargo new simple-auth-server# Created binary (application) simple-auth-server projectcd simple-auth-server # and then # watch for changes re-compile and runcargo watch -x run 用以下内容填写cargo依赖关系,我将在项目中使用它们。我正在使用crate的显式版本,因为你知道包变旧了并且发生了变化。(如果你在很长一段时间之后阅读本教程)。在本教程的第1部分中,我们不会使用所有这些,但它们在最终的应用程序中都会变得很方便。[dependencies]actix = “0.7.4"actix-web = “0.7.8"bcrypt = “0.2.0"chrono = { version = “0.4.6”, features = [“serde”] }diesel = { version = “1.3.3”, features = [“postgres”, “uuid”, “r2d2”, “chrono”] }dotenv = “0.13.0"env_logger = “0.5.13"failure = “0.1.2"frank_jwt = “3.0"futures = “0.1"r2d2 = “0.8.2"serde_derive=“1.0.79"serde_json=“1.0"serde=“1.0"sparkpost = “0.4"uuid = { version = “0.6.5”, features = [“serde”, “v4”] }设置基本APP创建新文件src/models.rs与src/app.rs。// models.rsuse actix::{Actor, SyncContext};use diesel::pg::PgConnection;use diesel::r2d2::{ConnectionManager, Pool};/// This is db executor actor. can be run in parallelpub struct DbExecutor(pub Pool<ConnectionManager<PgConnection>>);// Actors communicate exclusively by exchanging messages. // The sending actor can optionally wait for the response. // Actors are not referenced directly, but by means of addresses.// Any rust type can be an actor, it only needs to implement the Actor trait.impl Actor for DbExecutor { type Context = SyncContext<Self>;}要使用此Actor,我们需要设置actix-web服务器。我们有以下内容src/app.rs。我们暂时将资源构建者留空。这就是路由的核心所在。// app.rsuse actix::prelude::;use actix_web::{http::Method, middleware, App};use models::DbExecutor;pub struct AppState { pub db: Addr<DbExecutor>,}// helper function to create and returns the app after mounting all routes/resourcespub fn create_app(db: Addr<DbExecutor>) -> App<AppState> { App::with_state(AppState { db }) // setup builtin logger to get nice logging for each request .middleware(middleware::Logger::new(”"%r" %s %b %Dms”)) // routes for authentication .resource("/auth”, |r| { }) // routes to invitation .resource("/invitation/”, |r| { }) // routes to register as a user after the .resource("/register/”, |r| { })}main.rs// main.rs// to avoid the warning from diesel macros#![allow(proc_macro_derive_resolution_fallback)]extern crate actix;extern crate actix_web;extern crate serde;extern crate chrono;extern crate dotenv;extern crate futures;extern crate r2d2;extern crate uuid;#[macro_use] extern crate diesel;#[macro_use] extern crate serde_derive;#[macro_use] extern crate failure;mod app;mod models;mod schema;// mod errors;// mod invitation_handler;// mod invitation_routes;use models::DbExecutor;use actix::prelude::;use actix_web::server;use diesel::{r2d2::ConnectionManager, PgConnection};use dotenv::dotenv;use std::env;fn main() { dotenv().ok(); let database_url = env::var(“DATABASE_URL”).expect(“DATABASE_URL must be set”); let sys = actix::System::new(“Actix_Tutorial”); // create db connection pool let manager = ConnectionManager::<PgConnection>::new(database_url); let pool = r2d2::Pool::builder() .build(manager) .expect(“Failed to create pool.”); let address :Addr<DbExecutor> = SyncArbiter::start(4, move || DbExecutor(pool.clone())); server::new(move || app::create_app(address.clone())) .bind(“127.0.0.1:3000”) .expect(“Can not bind to ‘127.0.0.1:3000’”) .start(); sys.run();}在此阶段,您的服务器应该编译并运行127.0.0.1:3000。让我们创建一些模型。设置diesel并创建我们的用户模型我们首先为用户创建模型。假设您已经完成postgres并diesel-cli安装并正常工作。在您的终端中echo DATABASE_URL=postgres://username:password@localhost/database_name > .env,在设置时替换database_name,username和password。然后我们在终端跑diesel setup。这将创建我们的数据库并设置迁移目录等。我们来写一些吧SQL。通过diesel migration generate users和创建迁移diesel migration generate invitations。在migrations文件夹中打开up.sql和down.sql文件,并分别添加以下sql。–migrations/TIMESTAMP_users/up.sqlCREATE TABLE users ( email VARCHAR(100) NOT NULL PRIMARY KEY, password VARCHAR(64) NOT NULL, –bcrypt hash created_at TIMESTAMP NOT NULL);–migrations/TIMESTAMP_users/down.sqlDROP TABLE users;–migrations/TIMESTAMP_invitations/up.sqlCREATE TABLE invitations ( id UUID NOT NULL PRIMARY KEY, email VARCHAR(100) NOT NULL, expires_at TIMESTAMP NOT NULL);–migrations/TIMESTAMP_invitations/down.sqlDROP TABLE invitations;在您的终端中 diesel migration run将在DB和src/schema.rs文件中创建表。这将进行diesel和migrations。请阅读他们的文档以了解更多信息。在这个阶段,我们已经在db中创建了表,让我们编写一些代码来创建users和invitations的表示。在models.rs我们添加以下内容。// models.rs…// — snipuse chrono::NaiveDateTime;use uuid::Uuid;use schema::{users,invitations};#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]#[table_name = “users”]pub struct User { pub email: String, pub password: String, pub created_at: NaiveDateTime, // only NaiveDateTime works here due to diesel limitations}impl User { // this is just a helper function to remove password from user just before we return the value out later pub fn remove_pwd(mut self) -> Self { self.password = “".to_string(); self }}#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]#[table_name = “invitations”]pub struct Invitation { pub id: Uuid, pub email: String, pub expires_at: NaiveDateTime,}检查您的实现是否没有错误/警告,并密切关注终端中的cargo watch -x run命令。我们自己的错误响应类型在我们开始为应用程序的各种路由实现处理程序之前,我们首先设置一般错误响应。它不是强制性要求,但随着您的应用程序的增长,将来可能会有用。Actix-web提供与failure库的自动兼容性,以便错误导出失败将自动转换为actix错误。请记住,这些错误将使用默认的500状态代码呈现,除非您还为它们提供了自己的error_response()实现。这将允许我们使用自定义消息发送http错误响应。创建errors.rs包含以下内容的文件。// errors.rsuse actix_web::{error::ResponseError, HttpResponse};#[derive(Fail, Debug)]pub enum ServiceError { #[fail(display = “Internal Server Error”)] InternalServerError, #[fail(display = “BadRequest: {}”, _0)] BadRequest(String),}// impl ResponseError trait allows to convert our errors into http responses with appropriate dataimpl ResponseError for ServiceError { fn error_response(&self) -> HttpResponse { match self { ServiceError::InternalServerError => { HttpResponse::InternalServerError().json(“Internal Server Error”) }, ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message), } }}不要忘记添加mod errors;到您的main.rs文件中以便能够使用自定义错误消息。实现handler处理程序我们希望我们的服务器从客户端收到一封电子邮件,并在数据库中的invitations表中创建。在此实现中,我们将向用户发送电子邮件。如果您没有设置电子邮件服务,则可以忽略电子邮件功能,只需使用服务器上的响应数据。从actix文档:Actor通过发送消息与其他actor通信。在actix中,所有消息具有类型。消息可以是实现Message trait的任何Rust类型。并且请求处理程序可以是实现Handler trait的任何对象。请求处理分两个阶段进行。首先调用handler对象,返回实现Responder trait的任何对象。然后,在返回的对象上调用respond_to(),将自身转换为AsyncResult或Error。让我们实现Handler这样的请求。首先创建一个新文件src/invitation_handler.rs并在其中创建以下结构。// invitation_handler.rsuse actix::{Handler, Message};use chrono::{Duration, Local};use diesel::result::{DatabaseErrorKind, Error::DatabaseError};use diesel::{self, prelude::};use errors::ServiceError;use models::{DbExecutor, Invitation};use uuid::Uuid;#[derive(Debug, Deserialize)]pub struct CreateInvitation { pub email: String,}// impl Message trait allows us to make use if the Actix message system andimpl Message for CreateInvitation { type Result = Result<Invitation, ServiceError>;}impl Handler<CreateInvitation> for DbExecutor { type Result = Result<Invitation, ServiceError>; fn handle(&mut self, msg: CreateInvitation, : &mut Self::Context) -> Self::Result { use schema::invitations::dsl::*; let conn: &PgConnection = &self.0.get().unwrap(); // creating a new Invitation object with expired at time that is 24 hours from now // this could be any duration from current time we will use it later to see if the invitation is still valid let new_invitation = Invitation { id: Uuid::new_v4(), email: msg.email.clone(), expires_at: Local::now().naive_local() + Duration::hours(24), }; diesel::insert_into(invitations) .values(&new_invitation) .execute(conn) .map_err(|error| { println!(”{:#?}",error); // for debugging purposes ServiceError::InternalServerError })?; let mut items = invitations .filter(email.eq(&new_invitation.email)) .load::<Invitation>(conn) .map_err(|| ServiceError::InternalServerError)?; Ok(items.pop().unwrap()) }}不要忘记在main.rs文件中添加mod invitation_handler。现在我们有一个处理程序来插入和返回DB的invitations。使用以下内容创建另一个文件。register_email()接收CreateInvitation结构和保存DB地址的状态。我们通过调用into_inner()发送实际的signup_invitation结构。此函数以异步方式返回invitations或我们的Handler处理程序中定义的错误.// invitation_routes.rsuse actix_web::{AsyncResponder, FutureResponse, HttpResponse, Json, ResponseError, State};use futures::future::Future;use app::AppState;use invitation_handler::CreateInvitation;pub fn register_email((signup_invitation, state): (Json<CreateInvitation>, State<AppState>)) -> FutureResponse<HttpResponse> { state .db .send(signup_invitation.into_inner()) .from_err() .and_then(|db_response| match db_response { Ok(invitation) => Ok(HttpResponse::Ok().json(invitation)), Err(err) => Ok(err.error_response()), }).responder()}测试你的服务器你应该能够使用以下curl命令测试http://localhost:3000/invitation路由。curl –request POST \ –url http://localhost:3000/invitation \ –header ‘content-type: application/json’ \ –data ‘{“email”:“test@test.com”}’# result would look something like{ “id”: “67a68837-a059-43e6-a0b8-6e57e6260f0d”, “email”: “test@test.com”, “expires_at”: “2018-10-23T09:49:12.167510”}结束第1部分在下一部分中,我们将扩展我们的应用程序以生成电子邮件并将其发送给注册用户进行验证。我们还允许用户在验证后注册和验证。英文原文 ...

November 13, 2018 · 4 min · jiezi

Google开发者大会:你不得不知的Tensorflow小技巧

Google Development Days China 2018近日在中国召开了。非常遗憾,小编因为不可抗性因素滞留在合肥,没办法去参加。但是小编的朋友有幸参加了会议,带来了关于tensorlfow的一手资料。这里跟随小编来关注tensorflow在生产环境下的最佳应用情况。Google Brain软件工程师冯亦菲为我们带来了题为“用Tensorflow高层API来进行模型原型设计、训练和生产投入”的精彩报告。冯亦菲姐姐给我们讲了一些tensorflwo的新的API的变动,最重要的是提出了一些使用tensorflow的建议。总结出来有六个方面,分别是:用Eager模式搭建原型用Datasets处理数据用Feature Columns提取特征用Keras搭建模型借用Canned Estimators用SavedModel打包模型下面我们依次来了解下这六个方面。用Eager模式搭建原型作为计算机界的一份子,我们知道静态图的效率自然是快快的,但是动态图的使用为我们的使用带来的很多方便。17年的时候,各大框架动态图大行其道,于是Google提出了tf.contrib.eager应对挑战。使用Eager有什么好处呢?回想之前我们在调试tensorflow的程序时,不得不使用sess.run(),麻烦的要死,而使用Eager就可以直接的将变量打印出来,大大方便了我们的调试;好处不止这么多,在进行模型搭建的时候,以前我们需要仔细考虑下Tensor的shape,一旦出错要定位也很不容易。而使用Eager可以一边搭建网络结构,一边将shape打印出来确认下是否正确。这就使我们在搭建网络时更加方面快捷了;此外,使用Eager后,自定义Operation和Gradient也会方便很多。下面举个简单的小例子。首先使用pip install tf-nightly(或GPU版本pip install tf-nightly-gpu)来安装Eager。import tensorflow as tfimport tensorflow.contrib.eager as tfetfe.enable_eager_execution() #开启Eager模式a = tf.constant([5], dtype=tf.int32)for i in range(a): print (i)使用Eager后我们可以很顺利的执行上述代码。但是如果没有Eager,就会报Tensor对象不能解释为integer的错误。从缺点上来讲,Eager的引入也势必造成额外的成本。用Datasets处理数据tensorflow的数据读入有三种方式:通过feeding的方式;通过管道(pipeline)的方式;直接读取变量或常量中保存的数据。Datasets属于上面提出的第二种方式,可以简化数据输入过程,而且能够提高数据的读入效率。Datasets的组成如上如所示。其中:Dataset:创建和转换数据集的基本;TextLineDataset:从文本文件中读取行;TFRecordDataset:读取TFRecord文件;FixedLengthRecordDataset:从二进制文件读取固定大小的记录;Iterator:提供一种一次访问一个数据集元素的方法。对于Datasets的使用,我们可以使用Dataset的子类提供的方法,也可以直接使用基类的方法:tf.data.Dataset.from_tensors()或者tf.data.Dataset.from_tensor_slices()。用Feature Columns提取特征Feature Columns实际上是一个数据结构,一个用于描述特征的数据结构。利用Feature Columns可以很方便的对输入训练模型前的特征进行处理。比如鸢尾花的识别,对于输入数据,每列表示不同的特征,如花瓣的长度,花萼的长度等等,我们想要对不同的列分别进行处理(或者对所有的列进行处理),使用Feature Columns就可以轻松的实现。如上图所示,Feature Columns形成了对输入数据集的结构性描述。可以方便我们对每列数据进行处理,而且使得代码的可读性更强。用Keras搭建模型想必大家对Keras已经比较了解了,使用Keras来构建一个神经网络,简直是飞一般地速度,而且完美的兼容tensorflow。simple_model=Sequential()simple_model.add(Dense(3,input_shape=(x.shape[1],),activation=‘relu’,name=‘layer1’))simple_model.add(Dense(5,activation=‘relu’,name=‘layer2’))simple_model.add(Dense(1,activation=‘sigmoid’,name=‘layer3’))构建一个模型就是如上面这么简单,而且调用API中定义好的模型更是只需要一句话,极其的方便。借用Canned EstimatorsEstimators API提供了模型选择、评估、训练等一些列功能。在1.3版本后,Google又增加了一层,称之为Canned Estimators。只需要一行代码就能够创建深度模型。Estimators可以结合上面提到的Feature Columns一起使用。tf.estimator.Estimator是基类;Pre-made Estimators是基类的子类,是已经定义好的模型,我们可以直接拿来使用;Custom Estimators是基类的实列,并不是定义好的,需要我们自己实现模型的定义。对于这里的模型,由三部分组成:Input function:输入函数,即我们前面所说的Datasets,对于数据进行表示;Model function: 实验模型的训练、验证、测试以及监控模型的参数;Estimators: 控制数据流以及模型的各种运算。用SavedModel打包模型相比于tensorflow原版的tf.train.Saver保存模型的方式,SavedModel提供了更好的将模型部署到生成环境的手段,更适用于商业目的。如上图右下方部分,在使用SavedModel打包模型时,可以产生两种模型:对应于第一种模型,Tensorflow Model Analysis可以方便我们对模型进行分析,是不是存在参数的问题,抑或是模型哪里设计的不合适等等;通过分析后,感觉模型不错,我们就可以通过Tensorflow Serving进行部署。此外,相比于Saver的方式,我们在inference时不需要再重新定义Graph(模型),如果使用Saver的话,在使用该模型时就需要再定义该模型,如果是一个程序猿设计并使用的还好,如果换成另一个猿去用这个模型,他又不知道模型的tensor的情况,那就尴尬了。所以使用SavedModel可以让我们更轻松地去使用模型。总结Google Developer Days给我们提供了一场盛宴,希望和大家一起学习其中的知识。如果可以,请为这篇文章点个赞吧。据说点赞的都能进Google。阅读更多react-native技术的优劣 一招教你读懂JVM和Dalvik之间的区别NDK项目实战—高仿360手机助手之卸载监听(Android)面试题级答案(精选版)非科班出身程序员:如何获取职业资源、进入好公司?相信自己,没有做不到的,只有想不到的在这里获得的不仅仅是技术!

September 26, 2018 · 1 min · jiezi