关于rust-lang:远程-开源项目-AppFlowy-招-Flutter-Rust-实习生

招聘 Rust / Flutter 实习生(全职岗请通过官网投递简历) 公司简介: AppFlowy Inc 成立于 2022年1月,是一家初创开源软件厂商,咱们致力打造一款无代码利用搭建协同办公类软件,同时提供安全可靠、凋谢灵便的解决方案。企业或集体可能在 AppFlowy 上轻松搭建贴合本人需要的利用,从而满足工作治理、内容治理、知识库治理等需要。 AppFlowy 作为开源我的项目,于 2021年11月在 GitHub 平台上公布至今,已积攒30,000+ stars、120+ 社区贡献者,荣获了 GitHub 颁发的2022年年度开源奖 - Noteworthy Newcomer Award。公司Base在新加坡,反对近程办公,同时有机会转正并提供海内倒退机会。 职位形容: 开发和保护咱们的开源我的项目,次要负责性能的实现与迭代、与开源社区开发者共创职位要求: 计算机专业在校生英文书面语流畅相熟 Flutter/Rust/C/C++ 其中一种或几种编程语言或框架咱们看重的: 酷爱技术,富裕工匠精力成长性思维,凋谢、求真虚浮、乐观、谦虚、长期主义加分项: 积极参与并奉献开源我的项目素日应用效率工具工作工夫、形式、内容、待遇: 工作工夫:9:00 ~ 18:00,双休近程办公,通过 GitHub 进行合作,外部沟通用 Slack薪水:400元 / 天招聘信息: 请发送邮件 career at appflowy.io邮件题目:学校-业余-姓名-职位邮箱内容: 简历在邮件中请简短答复:为什么想退出 AppFlowy 以及相干经验与背景

January 18, 2023 · 1 min · jiezi

关于rust-lang:转Rustc-源码学习-Lint-与-LintPass

工夫:2022.8.11撰稿:张正@KusionStack开发组背景在 KusionStack 技术栈中, KCL 配置策略语言是重要的组成部分之一。为了帮忙用户更好的编写 KCL 代码,咱们也为 KCL 语言开发了一些语言工具,Lint 就是其中一种。Lint 工具帮忙用户查看代码中潜在的问题和谬误,同时也能够用于自动化的代码查看,保障仓库代码标准和品质。因为 KCL 语言由 Rust 实现,一些性能也学习和参考了 Rustc。本文是在学习 Rustc 过程中的一些思考和积淀,在这里做一些分享。 RustcRustc 是 Rust Compiler 的简称,即 Rust 编程语言的编译器。Rust 的编译器是自举的,即 Rustc 由 Rust 语言编写而成,能够通过旧版本编译出新版本。因而,Rustc 能够说是用 Rust 语言编写编译器的最佳实际。 Lint 工具Lint 是代码动态剖析工具的一种,最早是来源于 C 语言。Lint 工具通常会查看代码中潜在的问题和谬误,包含(但不限于)编程格调(缩进、空行、空格)、代码品质(定义未应用的变量、文档缺失)以及错误代码(除0谬误、反复定义、循环援用)等问题。通常来说,Lint 工具除了标识谬误外,还会带有肯定的 fix/refactor suggest 和 auto-fix 的能力。在工程中引入 Lint 工具能够无效的缩小谬误,进步整体的工程质量。此外,对一种编程语言来说,Lint 工具通常也是其余工具研发的前置条件,例如 IDE 插件的谬误提醒,CI 的 Pipeline 检测等。 Lint vs. LintPass概念与关系Rustc 中对于 Lint 最次要的构造有两个, Lint 和 LintPass。首先须要辨别 Lint 和 LintPass 的概念。Rustc 的很多文档中都将它们统称为 Lint,这很容易造成混同。对于这两者之间的区别,rustc-dev-guide 给出的解释是: ...

August 15, 2022 · 4 min · jiezi

关于rust-lang:精通不了C咱还学不了Rust吗入门Rust必备中文资-艺多不压身

入门Rust必备中文材料大家可能都学过C,然而精通的很少。个人感觉C也没什么社区,包治理也很麻烦。(纯属个人感觉勿喷。)Rust 如果想入门不放弃,咱还是整顿点中文材料学习比拟好。起码中语看起来没有累赘,比拟难受。自己整顿了Rust中文材料站,申明大部分材料都是链接的社区的都是前人的功绩。大家看了材料好别忘给他们点个star。网站链接Rust中文网集体倡议入门教程后再开始写代码,不要看完根底语法就写代码可能编译通过的。能够拿本人的go我的项目转成rust代码达到疾速入门。把编译器当成敌人,它的谬误提醒精确具体。

September 14, 2021 · 1 min · jiezi

关于rust-lang:Rustlang-no-override-and-no-default-toolchain-set

起源: Rustlang "no override and no default toolchain set"公布工夫: 2021-06-08 14:01:38当运行 rustc --version就报错了,报错内容如下: no override and no default toolchain set报错起因,rust没有正确装置,咱们能够做如下操作: 通过 rustup 装置 stable版本的 $ rustup install stable设置stable为默认的版本 $ rustup default stable

June 10, 2021 · 1 min · jiezi

关于rust-lang:Rust的Blanket-Implements通用实现

在Rust中的实现,您能够扩大实现的类型的性能。实现是应用impl关键字定义的,并且蕴含属于类型实例的函数 或者 属于以后类型实例的函数。 With implementations in Rust, you can extend the functionality of an implementation type. Implementations are defined with the impl keyword and contain functions that belong to an instance of a type, statically, or to an instance that is being implemented. With blanket implementations you can save writing similar implementations for multiple types.你能够应用blanket impl 保留对于多种类型类似的实现。 什么是blanket implementations 官网定义:We can also conditionally implement a trait for any type that implements another trait. Implementations of a trait on any type that satisfies the trait bounds are called _blanket implementations_ and are extensively used in the Rust standard library. For example, the standard library implements the ToString trait on any type that implements the Display trait.咱们能够有条件地为任何一个实现了另一个Trait的类型实现一个Trait。 为任何一个满足 Trait bound的类型实现一个Trait, 称为通用实现(_blanket implementations_)。 且被宽泛地应用于Rust规范库。 举个例子, 规范库为任何一个实现了Display Trait的类型实现了 ToString Trait。 ...

October 17, 2020 · 2 min · jiezi

Tensorflow-Rust实战下篇整合actixweb提供http服务

上一篇我写的文章how to use tensorflow with rust. 这一次我们看看使用tensorflow建立了什么,并通过http接口提供服务。随着Actix Web1.0版本发布,我认为用它构建一些东西将是一个很好的时机。 本文假设您对Futures及其运作方式有一定的了解。我将尽量用更简单的术语解释,但理解Futures生态系统将非常有效地帮助阅读本文。为此,我建议你从tokio开始。 有些人建议在深入Futures之前等待async/await和friends功能发布。我认为你现在应该亲自动手:异步编程总是很有挑战性。再一次为了不耐烦的人,您可以在actix-web分支上找到参考代码:https://github.com/cetra3/mtc... 一、API定义这里的API非常简单。我们想模仿我们在命令行上所做的事情:提交一张图片,返回结果是一张图片。为了使事情变得有趣,我们将提供一种方式:将边界框以JSON数组返回。 关于通过http协议提交二进制数据,我首先想到了几种选择: 只需提交原始数据即可使用multipart/form-data序列化为JSON格式提交我认为最简单是原始数据,所以让我们这样做! multipart/form-data可能ok,但是你必须处理多个图像的时候呢? JSON格式似乎有点浪费,因为您不可避免地必须使用base64或类似的方式转换二进制数据。 所以我们的API是这样的: 提交POST请求作为原始文件提交运行会话,通过MTCNN算法以提取人脸将边界框作以JSON格式返回;或者命令行示例一样,将图像叠加以JPEG格式返回。二、MTCNN的结构体(struct)在我们上一篇博客中,我们只是简单地使用main函数来执行所有操作,但我们必须一些重构才能与actix一起使用。我们希望将MTCNN行为封装为结构,可以传递和转移。最终目标是在应用程序状态下使用它。 2.1结构体(struct)定义让我们将结构包含我们想要的一切: 图版会话一些多个请求中共用的Tensor框架的输入参数首先,我们创建一个新文件mtcnn.rs并加上结构体定义。 use tensorflow::{Graph, Session, Tensor};pub struct Mtcnn { graph: Graph, session: Session, min_size: Tensor<f32>, thresholds: Tensor<f32>, factor: Tensor<f32>}然后,现在我们只是用new方法填充初始化内容。由于其中一些值的初始化并非绝对可靠,我们将返回Result: pub fn new() -> Result<Self, Box<dyn Error>> { let model = include_bytes!("mtcnn.pb"); let mut graph = Graph::new(); graph.import_graph_def(&*model, &ImportGraphDefOptions::new())?; let session = Session::new(&SessionOptions::new(), &graph)?; let min_size = Tensor::new(&[]).with_values(&[40f32])?; let thresholds = Tensor::new(&[3]).with_values(&[0.6f32, 0.7f32, 0.7f32])?; let factor = Tensor::new(&[]).with_values(&[0.709f32])?; Ok(Self { graph, session, min_size, thresholds, factor })}2.2Run方法我将在这里开始加快节奏,所以如果你遇到困难或不确定发生了什么,请查看face-detection-with-tensorflow-rust,以解释这里发生的事情。我们已经添加了所有需要跑一个会话的东西。让我们创建一个需要API做什么的方法:提交一张图片,响应一些边界框(框出人脸的位置): ...

June 29, 2019 · 6 min · jiezi

Rust的所有权

作为一个Ruby开发者,我所知道的关于内存分配的所有内容都是由一些称为垃圾收集的process处理的,这是Aaron Patterson的问题,而不是我的问题。因此,当我打开Rust Book并看到Rust没有垃圾收集时,我变得有点担心。处理记忆管理的责任是否应该堆积在我身上?显然,对于C这样的系统编程语言来说,处理内存分配是一件大事,如果做得不好就会产生重大影响。 1、Stack(栈) & Heap(堆) 栈和堆是在运行时管理内存的方法。栈被认为是快速高效的,因为它有序地存储和访问数据。栈顶是在栈中添加、删除数据的唯一位置。这被称为LIFO,后进先出,意味着我们在释放内存时只需知道栈顶的地址。栈快速的另一个原因,栈所需的内存空间在编译时是已知的。这意味着我们可以在将数据存储到其中之前分配一块固定大小的内存。例如,如果你知道有四个人参加您的晚宴,你可以提前决定每个人的座位,准备多少食物,并在他们到达之前练习他们的名字。这是超级高效的!如果你不能提前确切地知道有多少人参加您的晚宴,你可以使用堆。使用堆意味着需要准备足够多但未知数目的椅子并向到来的人发出名称标签(name字段)。 当在运行时期间需要存储未知大小的数据时,计算机会在堆上搜索内存,对其进行标记并返回一个指针,该指针指向内存中的位置。这称为分配内存。然后,您可以在栈上使用该指针,但是,当你要检索真实数据时,需要读取指针指向的内存位置的数据。 当我不断深入了解栈和堆时,管理堆中的数据会很困难。例如,你需要确保在完成使用一块内存后允许计算机重新分配这块内存。但是,如果其中一个代码块释放了内存中的某个位置,而另一个代码块仍然持有该内存的指针,则会出现悬空指针(dangling pointer)。 跟踪哪部分代码正在使用堆上的哪些数据,最小化堆上的数据拷贝,以及清理堆上未被使用的数据使其不会耗尽内存空间,这是所有权解决的所有问题。Rust Book2、Ownership(所有权) & Scope(作用域)Rust关于所有权的三个规则: Rust中的每个值都有一个称为其所有者的变量名。(let name = "xxx")同一时间只能有一个所有者。当所有者超出作用域时,该值将被删除。2.1所有权的最简单的例子所有权的最简单的例子是关于变量的作用域:一旦当前函数作用域结束,由}表示,变量hello超出作用域会被删除。这一点和大多数编程语言的本地变量是一致的。但这不是所有权的全部,当我们需要在传递值,并将字符串常量(暂时认为数据存储在栈中,其实在永久的常量池中)切换到String类型(数据存储在堆上的)时,事情会变得更有趣。 2.2所有权发生改变当使用字符串常量时,正如我们所期望的那样,Rust将hello的值复制到hello1中。 但是当使用String类型时,Rust会移交该值的所有值。编译时会抛出错误:error[E0382]: use of moved value: 'hello’。 看起来在使用字符串常量时,Rust会将该一个变量的值复制到另一个变量中,但是当我们使用String类型时,它会移交所有权。关于这个话题在论坛里有相关讨论,请阅读此贴子 The Copy trait - what does it actually copy?. 以下是我对讨论后的总结:字符串常量“Hello,World!”存储在只读内存中的某个位置(既不在栈中也不在堆中),并且指向该字符串的指针存储在栈中。 这里的指针通常是称为引用,这意味着我们使用指向存储在永久内存中的字符串常量的引用(参见Ownership in Rust, Part 2中有关引用的更多信息),并保证它在整个程序的运行时间里是有效的(它有一个静态的生命周期)。 变量hello和hello1存储在栈。 当我们使用=运算符时,Rust会将存储在hello中的指针值的副本绑定到变量hello1。 在作用域的最后,Rust会调用drop方法从栈中删除变量以释放内存。 这些变量可以存储并轻松地在栈中进行复制,因为它们的大小在编译时是已知的。 在堆上,字符串类型的值为“hello,world!”使用 string:from 方法绑定到变量hello。但是,与字符串常量不同,绑定到变量hello的是数据本身而不仅仅是指针,并且这些数据的大小可以在运行时更改。=运算符将变量hello指向的数据绑定到新变量hello1,有效地将数据的所有权从一个变量移交给另一个变量。变量hello现在是无效的,根据所有权规则2:“同一时间只能有一个所有者。” 2.3为什么不总是copy数据?但为什么这样呢?为什么Rust不始终复制数据并将其绑定到新变量?回想一下栈和堆之间的差异,堆上存储的数据大小在编译时是不可知的,这意味着我们需要在运行时进行一些内存分配步骤。这可能会代价很高。根据我们的数据量,如果我们整天都在copy数据,可能会很快耗尽内存。除此之外,Rust的默认行为会保护我们免受内存问题的影响(可能在其他语言中遇到)。 将数据存储在堆上并在栈上存储指向该数据的指针。但是,与使用指针指向只读内存(存储字符串常量)不同,堆上的数据可能会发生变化。指针值<< DATA >>绑定到存储String类型的hello变量。如果我们将相同的指针值绑定到两个不同的变量,看起来像这样: 我们有两个变量hello和hello1,它们共享相同值的所有权。 这违反了规则2:“同一时间只能有一个所有者”,但让我们继续。在变量hello和hello1的作用域结束时,我们必须将他们在堆上的内存释放。 首先,我们将hello1指向的堆上内存数据释放,现在当我们释放hello时会发生什么? 这称为双重释放错误(double free error),我认为在这个StackOverflow答案中有最好的总结:https://stackoverflow.com/a/2... A double free in C, technically speaking, leads to undefined behavior. This means that the program can behave completely arbitrarily and all bets are off about what happens. That’s certainly a bad thing to have happen! In practice, double-freeing a block of memory will corrupt the state of the memory manager, which might cause existing blocks of memory to get corrupted or for future allocations to fail in bizarre ways (for example, the same memory getting handed out on two different successive calls of malloc).Double frees can happen in all sorts of cases. A fairly common one is when multiple different objects all have pointers to one another and start getting cleaned up by calls to free. When this happens, if you aren't careful, you might free the same pointer multiple times when cleaning up the objects. There are lots of other cases as well, though.— templatetypedefRust就是要避免犯这类错误。通过使hello无效,编译器知道只在hello1上发出一个释放内存的调用(drop)。 ...

June 23, 2019 · 2 min · jiezi

使用Rust和Elixir实现高效的下发好友列表

去年,Discord的后端基础设施团队努力提高核心实时通信基础设施的可扩展性和性能。 我们进行的一个大项目是改变我们更新会员列表的方式(屏幕右侧的那些漂亮的头像)。我们可以直接发送会员列表中可见部分的更新(分页),而不是为会员列表中的每个人发送更新。这样做的好处很明显,例如网络流量更少,CPU使用率更低,电池寿命更长等等。 然而,这在服务器端造成了一个大问题:我们需要一个能够容纳数十万个条目的数据结构,以一种可以处理大量更新的方式进行排序,并且可以上报会员的位置索引添加和删除。 Elixir是一种函数式语言,它的数据结构是不可变的。这对推理代码并支撑大量并发性都非常好。不可变数据结构的双刃剑。现有的数据结构的更新是通过创建全新数据结构来实现的,该全新数据结构是将该操作应用于现有的数据结构的结果。 这意味着当有人加入服务器(内部称为公会)并拥有100,000名成员的成员列表时,我们必须构建一个包含100,001名成员的新列表。 BEAM VM非常快速,并且每天都在变得更快。Elixir试图在可能的情况下利用persistent data structure。但是在我们的运营规模下,这样的更新效率是无法被接受的。 将Elixir推至极限两位工程师接受了制作纯Elixir数据结构的挑战,该数据结构可以容纳大型sorted sets并支持快速更新操作。这说起来容易做起来难。 Elixir附带一个名为MapSet的set实现。 MapSet是构建在Map数据结构之上的通用数据结构。它对许多Set操作很有用,但它不能保证有序,但这是成员列表的关键要求,排除MapSet。 接下来将是古老的List类型:对List做一层封装,强制保证唯一性并在插入新元素后对列表进行排序。这种方法的压测数据表明,对于小型列表(5,000个元素) ,插入时间在500s和3,000s之间。这太慢了,不可行。更糟糕的是,插入的性能与列表的大小和列表中的位置深度成正比。在250,000个元素的末尾添加一个新元素,大约170,000s:基本上是恒定的。 两次失败,但BEAM尚未退出竞争。 Erlang附带一个名为ordsets的模块。 Ordsets是有序sets,所以听起来我们找到了解决问题的方法:让我们压测一下以检查可行性。当列表很小时,性能看起来相当不错,测量范围在0.008s和288s之间。遗憾的是,当测试的大小增加到250,000时,最坏情况下的性能提高到27,000s,这比我们的自定义List实现速度提高了五倍,但仍然不够快。 尝试了语言附带的所有候选者,粗略地搜索了lib,看看其他人是否已经解决了这个问题并开源。看了一些lib,但它们都没有提供所需的属性和性能。值得庆幸的是,计算机科学领域一直在优化用于存储和分类数据的算法和数据结构,因此有很多关于如何进行的想法。 SkipListordset在小数据下表现非常出色。也许有一些方法可以将一堆非常小的ordsets链接在一起,并在访问特定位置时快速访问正确的ordset。这类似于一个skiplist。 这个新数据结构的第一个版本非常简单。 OrderedSet是一个Cell列表的封装,每个Cell内部都是一个小的ordset:ordset的第一项,ordset的最后一项,以及count。这允许OrderedSet快速遍历Cells列表以找到适当的Cell,然后执行非常快速的ordset操作。在250,000项目列表的末尾插入项目从27,000s降至5,000s,比原始ordsets快5倍,比原始List实现快34倍。 性能有所提升,但是在列表的头部Cell创建250,000个元素,单个插入时间仍为19,000s。 这是有道理的。当你在OrderedSet的前面插入一个项目时,它会在第一个Cell中结束,但是Cell已经满了,所以它将它的最后一个项目驱逐到下一个Cell,但是Cell已经满了,所以它将它的最后一个项目驱逐到下一个Cell,依此类推。 OrderedSet问题在于,当元素填满时,操作会从Cell级联到下一个Cell。如果我们允许Cell分裂,在列表中间动态插入新Cell呢?好处是:最坏的情况是Cell分裂,而不是级联。 优化后的情况:在小列表时,这个新的OrderedSet可以在列表中的任何点执行4s和34s之间的插入,不错。我们将尺寸调整到250,000。在列表的开头插入,第一个插入为4s,后面会逐惭变慢。最终在列表末尾插入一个项目需要640s,看起来还行。 必须更快!上面的解决方案适用于高达250,000名成员的公会,但我们想要更多!Discord一直在使用Rust来让事情变得更快,我们可以使用Rust来加快速度吗? Rust不是一种函数式语言,可以使用可变数据结构。它也没有运行时并提供“zero-cost abstractions”。如果我们用Rust,它可能会表现得更好。 我们的核心服务不是用Rust编写的,它们是基于Elixir的。 Elixir非常适合调用Rust,幸运的是,BEAM VM还有另一个漂亮的技巧。 BEAM VM有三种类型的函数: 用Erlang或Elixir编写的函数。这些是简单的用户空间函数。内置于语言中的函数,充当用户空间函数的构建块。这些被称为BIF或内置函数。NIF或native函数。这些是使用C或Rust构建并编译到BEAM VM中的函数。调用这些函数就像调用BIF一样,但是你可以控制它的功能。有一个名为Rustler的Elixir项目。它为Elixir和Rust提供了很好的支持,可以创建一个表现良好的安全的NIF,并保证使用Rust不会VM崩溃或内存泄漏。 我们预留了一个星期,看看这是否值得付出努力。到本周末,我们给出一个非常有限的概念验证。压测数据看上去很有希望,与OrderedSet的4s至640s相比,向SortedSet添加元素的最佳情况是0.4s,最差情况为2.85s。这只是使用integer的测试,但它足以证明优于Elixir的实现。 有了数据支撑,我们决定继续扩展程序支持更多的Elixir数据类型。最后我们的测试数据如下:我们将数量一直增加到1,000,000。最后打印出结果:SortedSet最佳情况为0.61s,最差情况为3.68s,其基于多种大小的sets,大小从5,000到1,000,000。 我们使最坏的情况与先前的迭代最佳情况一样好!Rust支持的NIF提供了巨大的性能优势,而无需牺牲易用性或内存。 喜讯今天,Rust版的SortedSet为每一个Discord公会提供支持:从计划到日本旅行的3人公会到享受最新、有趣的游戏的20万人公会。 自布署SortedSet以来,我们已经看到性能全面提升,不会对内存压力产生影响。我们了解到Rust和Elixir可以并肩工作。我们仍然可以将我们的核心实时通信逻辑保留在更高级别的Elixir中,它具有出色的保护和简单的并发实现,同时在需要时可以使用Rust。 如果你需要一个高效更新的SortedSet,我们已经开源了SortedSet。

June 1, 2019 · 1 min · jiezi

Rust-Rc-方法整理

ref: alloc::rc::Rc - Rust <!-- toc --> 方法 newpintry_unwrapinto_rawfrom_rawdowngradeweak_countstrong_countget_mutptr_eqmake_mutdowncast<!-- tocstop --> std::rc::Rc 是单线程引用计数指针。'RC' 代表 'Reference Counted'。翻阅 module-level-documentation 查看更多信息Rc 的固有方法都是关联函数,这意味在使用应该是用类似 Rc::get_mut(&mut value) 而不是 value.get_mut() 的方式调用。这可以避免与其包含的类型方法冲突。 方法newpub fn new(value: T) -> Rc<T> 构造一个 Rc<T> 例子 use std::rc::Rc;let five = Rc::new(5);pinpub fn pin(value: T) -> Pin<Rc<T>> 构建一个新的 Pin<Rc<T>>。如果 T 没有实现 Unpin,那么 value 将会固定在内存中不可移动。 try_unwrappub fn try_unwrap(this: Self) -> Result<T, Self> 如果 Rc 有且只有1个强引用,则返回包含的值,否则返回 Err<T>。不管 Rc 有多少弱引用,只要符合上述条件,该函数都将成功。 use std::rc::Rc;fn main() { let x = Rc::new(3); assert_eq!(Rc::try_unwrap(x), Ok(3)); let x = Rc::new(4); let _y = Rc::clone(&x); // 调用 clone 增强强引用 assert_eq!(*Rc::try_unwrap(x).unwrap_err(), 4); // Rc::try_unwrap(x) 返回 Err(4)}into_rawpub fn into_raw(this: Self) -> *const T ...

May 12, 2019 · 2 min · jiezi

Rustweekly20190509

????????>欢迎参与Rust中文:Rust-weekly 参与入口,本文同步于Rust-weekly-2019-05-09新闻Actix系列在最新的Web框架性能排行榜实现所有选项屠榜The RustBridge Roadmap for 2019This week in Amethyst 16A final proposal for await syntaxThis Week in Rust 285文章用Wasmer进行插件开发1 - Rust中文阅读理解 Rust 中的 Closure试试看 - 搭建自己的 Rust Cargo Crate 反向代理源RUST语言在Windows上的编译安装(GCC ABI)Windows Linux子系统使用rustup重新安装RustRust parallelism for non-C/C++ developersExtending Python with RustRust: How to build a Docker image with private Cargo dependenciesExplained: Futures in Rust for Web Development用Rust写脚本语言 (一):一切存在皆对象(二):垃圾收集器(三):作用域与栈帧(四):插播一则通知和一个脑洞(五):方法对象(六):休养生息(七):迈向多线程的准备——初级阶段从零开始写 OS (0) —— 随便聊聊(1) —— 独立式可执行程序(2) —— 最小化内核(3) —— 格式化输出(4) —— Trap(4-1) —— 中断跳转(5) —— 时钟中断(6) —— 页表简介(7) —— 内存分配(8) —— 创建页表(9) —— 内核线程Kubernetes operators in rustUsing Rust for Gamedev6 useful Rust macros that you might not have seen beforeWorld's First Private Cargo Registry w/ Cloudsmith + Rust如何理解 rust 中的 Sync、Send?[[译] Rust如何解决依赖地狱](https://segmentfault.com/a/11...Compile-time coprocessor codegen, with Rust macrosRust Patterns: Enums Instead Of BooleansHow to build sitemap for images with RustCross-platform RustWriting your first WebAssembly ProjectHow to use datas to build sitemap with Rust Diesel2D Graphics on Modern GPUHawk-Rust Series: Kafka with RustHAWK-Rust Series: Automate Infrastructure using TerraformHawk: Image Recognition Application using Rust and AWS ServicesWebAssembly, Rust, and Edge ComputingAnother Way of Creating struct Instances in RustI underestimated the dangers of Rust FFICratesvtext - NLP in Rust with Python bindingsmicroamp -A (micro) framework for building bare-metal AMP (Asymmetric Multi-Processing) applications ...

May 10, 2019 · 2 min · jiezi

译-Rust如何解决依赖地狱

每隔一段时间我就会参与一个关于依赖管理和版本的对话,通常是在工作中,其中会出现“依赖地狱”的主题。如果你对这个术语不熟悉,那么我建议你查一下。简要总结可能是:“处理应用程序依赖版本和依赖冲突所带来的挫败感”。带着这个,让我们先获得关于依赖解析的一些技术。 问题在讨论包应该具有哪种依赖关系以及哪些依赖关系可能导致问题时,本主题通常会进入讨论。作为一个真实的例子,在 Widen Enterprises,我们有一个内部的,可重用的Java框架,它由几个软件包组成,为我们提供了创建许多内部服务的基础(如果你愿意的话,微服务)。这很好,但是如果你想创建一个依赖于框架中某些东西的可重用共享代码库呢?如果你尝试在应用程序中使用这样的库,最终可能会得到如下依赖关系图: <!--more--> 就像在这个例子中一样,每当你试图在服务中使用库时,你的服务和库很可能依赖于不同版本的框架,这就是“依赖地狱”的开始。 现在,在这一点上,一个好的开发平台将为你提供以下两种选择的组合: 使构建失败并警告我们framework版本21.1.1和21.2.0相互冲突。使用语义版本控制允许包定义与其兼容的 一系列 版本。如果幸运的话,两个软件包都兼容的版本集是非空的,你最终可以在应用程序中自动使用其中一个版本。这两个看起来都合理,对吧?如果两个软件包确实彼此不兼容,那么我们根本无法在不修改其中一个的情况下将它们一起使用。这是一个艰难的情况,但替代方案往往更糟糕。事实上,Java是不该学习的一个很好的例子: 默认行为是允许将依赖项的多个版本添加到类路径(Java的定位类的方式)。当应用程序需要库中的类时,实际使用哪个版本?在实践中,类的加载顺序因环境而异,甚至以非确定的方式运行,因此你实际上不知道将使用哪一个。哎呀!我们在Widen使用的另一个选择是强制版本对齐。这类似于之前的第二个合理选择,在Java中,依赖关系无法表达兼容性范围,因此我们只选择较新的可能依赖项并祈祷它仍然有效。在前面显示的依赖关系图示例中,我们将强制app升级到framework 21.2.0。这看起来像是一个双输的情况,所以你可以想象,这对添加依赖项非常不利,并且使之成为一个事实上的策略,除了实际的应用程序之外什么都不允许依赖我们的核心框架。 Rust的解决方案在进行这些讨论时,我会经常提到这是一个不适用于所有语言的问题,作为一个例子,Rust“解决”了这个问题。我常常拿Rust如何解决世界上所有的问题开玩笑,但在那里通常有一个真实的核心。因此,当我说Rust“解决”了这个问题以及它是如何工作的时候,让我们深入了解一下我的意思。 Rust的解决方案涉及相当多的动人的部分,但它基本上归结为挑战我们在此之前做出的核心假设: 最终应用程序中只应存在任何给定包的一个版本。Rust挑战了这一点,以便重构问题,看看是否有一个在依赖地狱之外更好的解决方案。Rust平台主要有两个功能可以协同工作,为解决这些依赖问题提供基础,现在我们将分别研究并看看最终结果是怎样的。 Cargo和Crates难题的第一部分当然是Cargo,Rust官方依赖管理器。Cargo类似于NPM或Maven之类的工具,并且有一些有趣的功能使它成为一个真正高质量的依赖管理器(这里我最喜欢的是Composer,一个非常精心设计的PHP依赖管理器)。Cargo负责下载项目依赖的Rust库,称为crates,并协调调用Rust编译器以获得最终结果。 请注意,crates是编译器中的第一类构造。这在以后很重要。 与NPM和Composer一样,Cargo允许你根据语义版本控制的兼容性规则指定项目兼容的一系列依赖项版本。这允许你描述与你的代码兼容(或可能)兼容的一个或多个版本。例如,我可能会添加 [dependencies]log = "0.4.*"到Cargo.toml文件,表明我的代码适用于0.4系列中log包的任何补丁版本。也许在最终的应用程序中,我们得到了这个依赖树 因为在my-project中我声明了与log版本0.4.*的兼容性,我们可以安全地为log选择版本0.4.4,因为它满足所有要求。(如果log包遵循语义版本控制的原则,这个原则对于已发布的库而言并不总是如此,那么我们可以确信这个发布不包括任何会破坏我们代码的重大更改。)你可以在Cargo文档中找到一个更好地解释版本范围以及它们如何应用于Cargo。 太棒了,所以我们可以选择满足每个项目版本要求的最新版本,而不是选择避开遇到版本冲突或只是选择更新的版本并祈祷。但是,如果我们遇到无法解决的问题,例如: 没有可以选择满足所有要求的log版本!我们接下来做什么? 名字修饰为了回答这个问题,我们需要讨论名字修饰。一般来说,名字修饰是一些编译器用于各种语言的过程,它将符号名称作为输入,并生成一个更简单的字符串作为输出,可用于在链接时消除类似命名符号的歧义。例如,Rust允许你在不同模块之间重用标识符: mod en { fn greet() { println!("Hello"); }}mod es { fn greet() { println!("Hola"); }}这里我们有两个不同的函数,名为greet(),但当然这很好,因为它们在不同的模块中。这很方便,但通常应用程序二进制格式没有模块的概念;相反,所有符号都存在于单个全局命名空间中,非常类似于C中的名称。由于greet()在最终二进制文件中不能显示两次,因此编译器可能使用比源代码更明确的名称。例如: en::greet()成为en__greetes::greet()成为es__greet问题解决了!只要我们确保这个名字修饰方案是确定性的并且在编译期间到处使用,代码就会知道如何获得正确的函数。 现在这不是一个完全完整的名字修饰方案,因为我们还没有考虑很多其他的东西,比如泛型类型参数,重载等等。此功能也不是Rust独有的,并且确实在C++和Fortran等语言中使用了很长时间。 名字修饰如何帮助Rust解决依赖地狱?这一切都在Rust的名字管理体系中,这似乎在我所研究的语言中相当独特。那么让我们来看看? 在Rust编译器中查找名字修饰的代码很简单;它位于一个名为symbol_names.rs的文件中。如果你想学习更多内容,我建议你阅读这个文件中的注释,但我会包括重点。似乎有四个基本组件包含在一个修饰符号名称中: 符号的完全限定名称。通用类型参数。包含符号的crate的名称。(还记得crates在编译器中是一流的吗?)可以通过命令行传入的任意“歧义消除器(disambiguator)”字符串。使用Cargo时,Cargo本身会将“歧义消除器”提供给编译器,所以让我们看一下compilation_files.rs包含的内容: 包名字包源包版本启用编译时功能一堆其他的东西这个复杂系统的最终结果是,即使是不同版本的crate中的相同功能也具有不同的修饰符号名称,因此只要每个组件知道要调用的函数版本,就可以在单个应用程序中共存。 合在一起现在回到我们之前的“无法解决的”依赖图: 借助依赖范围的强大功能,以及Cargo和Rust编译器协同工作,我们现在可以通过在我们的应用程序中包含log 0.5.0和log 0.4.4来实际解决此依赖关系图。app内部使用log的任何代码都将被编译以达到从0.5.0版生成的符号,而my-project中的代码将使用为0.4.4版生成的符号。 现在我们看到了大局,这实际上看起来非常直观,并解决了一大堆依赖问题,这些问题会困扰其他语言的用户。这个解决方案并不完美: 由于不同版本生成不同的唯一标识符,因此我们无法在库的不同版本之间传递对象。例如,我们无法创建一个log 0.5.0的LogLevel并将其传递给my-project使用,因为它期望LogLevel来自log 0.4.4,并且它们必须被视为单独的类型。对于库的每个实例,任何静态变量或全局状态都将被复制,如果没有一些特殊方法,它们就无法通信。我们的二进制大小必然会因为我们应用程序中包含的库的每个实例而增加。由于这些缺点,Cargo仅在需要时才采用这种技术来解决依赖图。 为了解决一般用例,这些似乎值得为Rust做出权衡,但对于其他语言,采用这样的东西可能会更加困难。以Java为例,Java严重依赖于静态字段和全局状态,因此简单地大规模采用Rust的方法肯定会增加破坏代码的次数,而Rust则将全局状态限制在最低限度。这种设计也没有对在运行时或反射时加载任意库进行说明,这两者都是许多其他语言提供的流行功能。 结论Rust在编译和打包方面的精心设计以(主要)无痛依赖管理的形式带来红利,这通常消除了可能成为开发人员在其他语言中最糟糕的噩梦的整类问题。当我第一次开始玩Rust的时候,我当然很喜欢我所看到的,深入了解内部,看到宏大的架构,周到的设计,以及合理的权衡取舍对我来说更令人印象深刻。这只是其中的一个例子。 即使你没有使用Rust,希望这会让你对依赖管理器,编译器以及他们必须解决的棘手问题给予新的重视。(虽然我鼓励你至少尝试一下Rust,当然......) GitHub repo: qiwihui/blogFollow me: @qiwihui ...

April 30, 2019 · 1 min · jiezi

Tensorflow Rust实战上篇

机器学习的一个方向是能够将它用于照片中的对象识别。这包括能够挑选动物,建筑物甚至人脸等特征。本文将引导您使用一些现有模型来使用rust和tensorflow完成人脸检测。我们将使用一个名为mtcnn的预训练模型进行人脸检测(注意:训练新模型不是我们在本文中关注的内容)。 挑战我们想要读取照片,检测到人脸,然后返回带有绘制边界框的图像。换句话说,我们想转换它(图片使用RustFest的许可,由FionaCastiñeira拍摄): Tensorflow and MTCNN最初的MTCNN模型是使用Caffe编写的,但幸运的是mtcnn有许多tensorflow python实现。我将选择tf-mtcnn,因为它是直接转换为单个图形模型文件。 首先,我们要添加tensorflow rust作为依赖。从Cargo.toml开始: [package]name = "mtcnn"version = "0.1.0"edition = "2018"[dependencies]tensorflow = "0.12.0"我们要做的是加载一个Graph,它是预先训练好的MTCNN,并运行一个会话。要点是Graph是用于计算的模型,Session是Graph的一次运行。有关这些概念的更多信息可以在这里找到。我喜欢将Graph视为大桶中的人造大脑,用途只是在您输入和输出时获得一些很棒的图像。 因此,让我们首先抓住现有的mtcnn.pb模型并尝试加载它。 Tensorflow图以protobuf格式序列化,可以使用Graph::import_graph_def加载。 use std::error::Error;use tensorflow::Graph;use tensorflow::ImportGraphDefOptions;fn main() -> Result<(), Box<dyn Error>> { //First, we load up the graph as a byte array let model = include_bytes!("mtcnn.pb"); //Then we create a tensorflow graph from the model let mut graph = Graph::new(); graph.import_graph_def(&*model, &ImportGraphDefOptions::new())? Ok(())}跑cargo run命令,我们应该看到没有任何错误: $ cargo run Compiling mtcnn v0.1.0 (~/mtcnn) Finished dev [unoptimized + debuginfo] target(s) in 0.89s Running `target/debug/mtcnn`太棒了!看起来我们可以加载此图像! ...

April 21, 2019 · 3 min · jiezi

rust使用vscode调试环境

需安装vscode插件 RLS(vscode搜索插件rls) 、 lldb(vscode搜索插件codelldb)lldb的launch.json配置内容"version": “0.2.0”, “configurations”: [ { “name”: “rust”, // 配置名称,将会在调试配置下拉列表中显示 “type”: “lldb”, // 调试器类型:Windows表示器使用cppvsdbg;GDB和LLDB使用cppdbg。该值自动生成 “request”: “launch”, // 调试方式 “program”: “${workspaceRoot}/target/debug/helloworld”, // 要调试的程序(完整路径,支持相对路径) “args”: [], // 传递给上面程序的参数,没有参数留空即可 “stopAtEntry”: false, // 是否停在程序入口点(即停在main函数开始)(目前为不停下) “cwd”: “${workspaceRoot}”, // 调试程序时的工作目录 “environment”: [], “externalConsole”: false, // 调试时是否显示控制台窗口(目前为不显示) //“preLaunchTask”: “build”, //预先执行task.json “MIMode”: “lldb” //MAC下的debug程序 } ]

April 15, 2019 · 1 min · jiezi

Rust 实战 - 使用套接字联网API(二)

上一节,我们已经实现了一个最小可运行版本。之所以使用Rust而不是C,是因为Rust具备了必要的抽象能力,还能获得跟C差不多的性能。这一节,我们对上一节的代码做必要的封装,顺便还能把unsafe的代码包装成safe的API。我将上一节的源码放到了这里,你可以去查看。还记得上一节,我们把使用到的libc中的函数socket、bind、connect和结构体sockaddr、sockaddr_in、in_addr等,在Rust这边定义了出来。实际上,几乎libc中的函数,libc这个crate都帮我们定义好了。你可以去这里查看。编译器和标准库本身也使用了这个crate,我们也使用这个。首先在Cargo.toml文件的[dependencies]下面加入libc = “0.2”:[dependencies]libc = “0.2"接着在main.rs文件上方加入use libc;,也可以use libc as c;。或者你直接简单粗暴use libc::,并不推荐这样,除非你明确知道你使用的函数来自哪里。并将我们定义的与libc中对用的常量、函数、结构体删除。再添加libc::或c::到我们使用那些常量、结构体、函数的地方。如果你是直接use libc::,除了直接删除那部分代码外,几乎什么都不用做。目前的代码:use std::ffi::c_void;use libc as c;fn main() { use std::io::Error; use std::mem; use std::thread; use std::time::Duration; thread::spawn(|| { // server unsafe { let socket = c::socket(c::AF_INET, c::SOCK_STREAM, c::IPPROTO_TCP); if socket < 0 { panic!(“last OS error: {:?}”, Error::last_os_error()); } let servaddr = c::sockaddr_in { sin_family: c::AF_INET as u16, sin_port: 8080u16.to_be(), sin_addr: c::in_addr { s_addr: u32::from_be_bytes([127, 0, 0, 1]).to_be() }, sin_zero: mem::zeroed() }; let result = c::bind(socket, &servaddr as *const c::sockaddr_in as *const c::sockaddr, mem::size_of_val(&servaddr) as u32); if result < 0 { println!(“last OS error: {:?}”, Error::last_os_error()); c::close(socket); } c::listen(socket, 128); loop { let mut cliaddr: c::sockaddr_storage = mem::zeroed(); let mut len = mem::size_of_val(&cliaddr) as u32; let client_socket = c::accept(socket, &mut cliaddr as *mut c::sockaddr_storage as *mut c::sockaddr, &mut len); if client_socket < 0 { println!(“last OS error: {:?}”, Error::last_os_error()); break; } thread::spawn(move || { loop { let mut buf = [0u8; 64]; let n = c::read(client_socket, &mut buf as *mut _ as *mut c_void, buf.len()); if n <= 0 { break; } println!(”{:?}", String::from_utf8_lossy(&buf[0..n as usize])); let msg = b"Hi, client!"; let n = c::write(client_socket, msg as *const _ as *const c_void, msg.len()); if n <= 0 { break; } } c::close(client_socket); }); } c::close(socket); } }); thread::sleep(Duration::from_secs(1)); // client unsafe { let socket = c::socket(c::AF_INET, c::SOCK_STREAM, c::IPPROTO_TCP); if socket < 0 { panic!(“last OS error: {:?}”, Error::last_os_error()); } let servaddr = c::sockaddr_in { sin_family: c::AF_INET as u16, sin_port: 8080u16.to_be(), sin_addr: c::in_addr { s_addr: u32::from_be_bytes([127, 0, 0, 1]).to_be() }, sin_zero: mem::zeroed() }; let result = c::connect(socket, &servaddr as *const c::sockaddr_in as *const c::sockaddr, mem::size_of_val(&servaddr) as u32); if result < 0 { println!(“last OS error: {:?}”, Error::last_os_error()); c::close(socket); } let msg = b"Hello, server!"; let n = c::write(socket, msg as *const _ as *const c_void, msg.len()); if n <= 0 { println!(“last OS error: {:?}”, Error::last_os_error()); c::close(socket); } let mut buf = [0u8; 64]; let n = c::read(socket, &mut buf as *mut _ as *mut c_void, buf.len()); if n <= 0 { println!(“last OS error: {:?}”, Error::last_os_error()); } println!("{:?}", String::from_utf8_lossy(&buf[0..n as usize])); c::close(socket); }}你编译运行,应该能得到与上一节同样的结果。接下来,我们尝试把上面代码中函数,封装成更具Rust风格的API,除了TCP外,也还要考虑之后把UDP、UNIX域和SCTP也增加进来。同时,我们跟标准库里 net相关的API保持一致的风格。我们暂时不考虑跨平台,只考虑Linux,因此可以大胆的将一些linux独有的API添加进来。UNIX中一切皆文件,套接字也不例外。字节流套接字上的read和write函数所表现出来的行为,不同于通常的文件I/O。字节流套接字上调用read和write输入或输出字节数可能比请求的要少,这个现象的原因在于内核中用于套接字的缓冲区可能已经达到了极限。不过,这并不是我们正真关心的。我们来看看标准库中 File的实现:pub struct File(FileDesc);impl File { … pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> { self.0.read(buf) } pub fn write(&self, buf: &[u8]) -> io::Result<usize> { self.0.write(buf) } pub fn duplicate(&self) -> io::Result<File> { self.0.duplicate().map(File) } …}File 是一个元组结构体,标准库已经实现了read和write,以及duplicate。duplicate很有用,用于复制出一个新的描述符。我们继续看File中"包裹的FileDesc:pub struct FileDesc { fd: c_int,}impl File { … pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> { let ret = cvt(unsafe { libc::read(self.fd, buf.as_mut_ptr() as *mut c_void, cmp::min(buf.len(), max_len())) })?; Ok(ret as usize) } pub fn write(&self, buf: &[u8]) -> io::Result<usize> { let ret = cvt(unsafe { libc::write(self.fd, buf.as_ptr() as const c_void, cmp::min(buf.len(), max_len())) })?; Ok(ret as usize) } pub fn set_cloexec(&self) -> io::Result<()> { unsafe { cvt(libc::ioctl(self.fd, libc::FIOCLEX))?; Ok(()) } } pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> { unsafe { let v = nonblocking as c_int; cvt(libc::ioctl(self.fd, libc::FIONBIO, &v))?; Ok(()) } }}这一层应该是到头了,你可以看到,Rust中的File也是直接对libc的封装,不过你不用担心,一开始就提到,Rust 的ABI与C的ABI是兼容的,也就意味着Rust和C互相调用是几乎是零开销的。FileDesc的read和write中的实现,与我们之前对sockfd的read和write基本是一样的。除了read和write外,还有两个很有用的方法set_cloexec和set_nonblocking。我把“依附于”某个类型的函数叫做方法,与普通函数不同的是,依附于某个类型的函数,必须通过它所依附的类型调用。Rust通过这种方式来实现OOP,但是与某些语言的OOP不同的是,Rust的这种实现是零开销的。也就是,你将一些函数依附到某个类型上,并不会对运行时造成额外的开销,这些都在编译时去处理。set_cloexec方法会对描述符设置FD_CLOEXEC。我们经常会碰到需要fork子进程的情况,而且子进程很可能会继续exec新的程序。对描述符设置FD_CLOEXEC,就意味着,我们fork子进程时,父子进程中相同的文件描述符指向系统文件表的同一项,但是,我们如果调用exec执行另一个程序,此时会用全新的程序替换子进程的正文。为了较少不必要的麻烦,我们以后要对打开的描述符设置FD_CLOEXEC,除非遇到特殊情况。set_nonblocking用于将描述符设置为非阻塞模式,如果我们要使用poll、epoll等api的话。既然标准库已经封装好了FileDesc,我想直接使用的,然而FileDesc在标准库之外是不可见的。如果使用File的话,set_cloexec 和 set_nonblocking 还是要我们再写一次,但是File并不是“我自己”的类型,我没法直接给File附加方法,为此还需要一个额外的Tarit或者用一个“我自己”的类型,去包裹它。挺绕的。那既然这样,我们还是自己来吧。不过我们已经有了参考,可以将标准库里的FileDecs直接复制出来,然后去掉与Linux无关的代码,当然你也可以自由发挥一下。要注意的是,这段代码中还调用了一个函数cvt,我们把相关代码也复制过来:use std::io::{self, ErrorKind};#[doc(hidden)]pub trait IsMinusOne { fn is_minus_one(&self) -> bool;}macro_rules! impl_is_minus_one { ($($t:ident)) => ($(impl IsMinusOne for $t { fn is_minus_one(&self) -> bool { self == -1 } }))}impl_is_minus_one! { i8 i16 i32 i64 isize }pub fn cvt<T: IsMinusOne>(t: T) -> io::Result<T> { if t.is_minus_one() { Err(io::Error::last_os_error()) } else { Ok(t) }}pub fn cvt_r<T, F>(mut f: F) -> io::Result<T> where T: IsMinusOne, F: FnMut() -> T{ loop { match cvt(f()) { Err(ref e) if e.kind() == ErrorKind::Interrupted => {} other => return other, } }}还记得上一节我们使用过的last_os_error()方法么,这段代码通过宏impl_is_minus_one为 i32等常见类型实现了IsMinusOne这个Tarit,然后我们就可以使用cvt函数更便捷得调用last_os_error()取得错误。 我将这段代码放到util.rs文件中,并在main.rs文件上方加入pub mod util;然后再来看FileDesc最终的实现:use std::mem;use std::io;use std::cmp;use std::os::unix::io::FromRawFd;use libc as c;use crate::util::cvt;#[derive(Debug)]pub struct FileDesc(c::c_int);pub fn max_len() -> usize { <c::ssize_t>::max_value() as usize}impl FileDesc { pub fn raw(&self) -> c::c_int { self.0 } pub fn into_raw(self) -> c::c_int { let fd = self.0; mem::forget(self); fd } pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> { let ret = cvt(unsafe { c::read( self.0, buf.as_mut_ptr() as *mut c::c_void, cmp::min(buf.len(), max_len()) ) })?; Ok(ret as usize) } pub fn write(&self, buf: &[u8]) -> io::Result<usize> { let ret = cvt(unsafe { c::write( self.0, buf.as_ptr() as *const c::c_void, cmp::min(buf.len(), max_len()) ) })?; Ok(ret as usize) } pub fn get_cloexec(&self) -> io::Result<bool> { unsafe { Ok((cvt(libc::fcntl(self.0, c::F_GETFD))? & libc::FD_CLOEXEC) != 0) } } pub fn set_cloexec(&self) -> io::Result<()> { unsafe { cvt(c::ioctl(self.0, c::FIOCLEX))?; Ok(()) } } pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> { unsafe { let v = nonblocking as c::c_int; cvt(c::ioctl(self.0, c::FIONBIO, &v))?; Ok(()) } } pub fn duplicate(&self) -> io::Result<FileDesc> { cvt(unsafe { c::fcntl(self.0, c::F_DUPFD_CLOEXEC, 0) }).and_then(|fd| { let fd = FileDesc(fd); Ok(fd) }) }}impl FromRawFd for FileDesc { unsafe fn from_raw_fd(fd: c::c_int) -> FileDesc { FileDesc(fd) }}impl Drop for FileDesc { fn drop(&mut self) { let _ = unsafe { c::close(self.0) }; }}我已经将与Linux不相关的代码删除掉了。之所以原有duplicate那么冗长,是因为旧的Linux内核不支持F_DUPFD_CLOEXEC这个设置。fcntl这个函数,用来设置控制文件描述符的选项,我们稍后还会遇到用来设置和获取套接字的getsockopt和setsockopt。还有read_at和write_at等实现比较复杂的函数,我们用不到,也将他们删除。还有impl<‘a> Read for &‘a FileDesc ,因为内部使了一个Unstable的API,我也将其去掉了。我自由发挥了一下,把:pub struct FileDesc { fd: c_int,}替换成了:pub struct FileDesc(c::c_int);它们是等效的。不知你注意到没有,我把pub fn new(…)函数给去掉了,因为这个函数是unsafe的—-如果我们今后将这些代码作为库让别人使用的话,他可能传入了一个不存在的描述符,并由此可能引起程序崩溃—-但他们并不一定知道。我们可以通过在这个函数前面加unsafe来告诉使用者这个函数是unsafe的: pub unsafe fn new(…)。不过,Rust的开发者们已经考虑到了这一点,我们用约定俗成的from_raw_fd来代替pub unsafe fn new(…),于是才有了下面这一段:impl FromRawFd for FileDesc { unsafe fn from_raw_fd(fd: c::c_int) -> FileDesc { FileDesc(fd) }}最后,还利用Rust的drop实现了close函数,也就意味着,描述符离开作用域后,会自动close,就不再需要我们手动close了。与之先关的是into_raw方法,意思是把FileDesc转换为“未加工的”或者说是“裸的”描述符,也就是C的描述符。这个方法里面调用了forget,之后变量离开作用域后,就不会调用drop了。当你使用这个方法拿到描述符,使用完请不要忘记手动close或者再次from_raw_fd。pub fn into_raw(self) -> c::c_int { let fd = self.0; mem::forget(self); fd}我将这段代码放到了一个新的文件fd.rs中,并在main.rs文件上方加入pub mod fd;。接着,我们还需一个Socket类型,将socket、bind、connect等函数附加上去。这一步应该简单多了。同时你也会发现,我们已经把unsafe的代码,封装成了safe的代码。use std::io;use std::mem;use std::os::unix::io::{RawFd, AsRawFd, FromRawFd};use libc as c;use crate::fd::FileDesc;use crate::util::cvt;pub struct Socket(FileDesc);impl Socket { pub fn new(family: c::c_int, ty: c::c_int, protocol: c::c_int) -> io::Result<Socket> { unsafe { cvt(c::socket(family, ty | c::SOCK_CLOEXEC, protocol)) .map(|fd| Socket(FileDesc::from_raw_fd(fd))) } } pub fn bind(&self, storage: *const c::sockaddr, len: c::socklen_t) -> io::Result<()> { self.setsockopt(c::SOL_SOCKET, c::SO_REUSEADDR, 1)?; cvt(unsafe { c::bind(self.0.raw(), storage, len) })?; Ok(()) } pub fn listen(&self, backlog: c::c_int) -> io::Result<()> { cvt(unsafe { c::listen(self.0.raw(), backlog) })?; Ok(()) } pub fn accept(&self, storage: *mut c::sockaddr, len: *mut c::socklen_t) -> io::Result<Socket> { let fd = cvt(unsafe { c::accept4(self.0.raw(), storage, len, c::SOCK_CLOEXEC) })?; Ok(Socket(unsafe { FileDesc::from_raw_fd(fd) })) } pub fn connect(&self, storage: *const c::sockaddr, len: c::socklen_t) -> io::Result<()> { cvt(unsafe { c::connect(self.0.raw(), storage, len) })?; Ok(()) } pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> { self.0.read(buf) } pub fn write(&self, buf: &[u8]) -> io::Result<usize> { self.0.write(buf) } pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> { self.0.set_nonblocking(nonblocking) } pub fn get_cloexec(&self) -> io::Result<bool> { self.0.get_cloexec() } pub fn set_cloexec(&self) -> io::Result<()> { self.0.set_cloexec() } pub fn setsockopt<T>(&self, opt: libc::c_int, val: libc::c_int, payload: T) -> io::Result<()> { unsafe { let payload = &payload as *const T as *const libc::c_void; cvt(libc::setsockopt( self.0.raw(), opt, val, payload, mem::size_of::<T>() as libc::socklen_t ))?; Ok(()) } } pub fn getsockopt<T: Copy>(&self, opt: libc::c_int, val: libc::c_int) -> io::Result<T> { unsafe { let mut slot: T = mem::zeroed(); let mut len = mem::size_of::<T>() as libc::socklen_t; cvt(libc::getsockopt( self.0.raw(), opt, val, &mut slot as *mut T as *mut libc::c_void, &mut len ))?; assert_eq!(len as usize, mem::size_of::<T>()); Ok(slot) } }}impl FromRawFd for Socket { unsafe fn from_raw_fd(fd: RawFd) -> Socket { Socket(FileDesc::from_raw_fd(fd)) }}impl AsRawFd for Socket { fn as_raw_fd(&self) -> RawFd { self.0.raw() }}我已经将上一节中我们使用到的socket相关的主要的5个函数,外加read,write,等几个描述符设置的函数,“依附”到了Socket上。保存在 socket.rs 文件里。要说明的是,我在new和accept方法中,通过flags直接为新创建的描述符设置了SOCK_CLOEXEC选项,如果不想一步设置的话,就需要创建出描述符后,再调用set_cloexec方法。bind中,在调用c::bind之前,我给套接字设置了个选项SO_REUSEADDR,意为允许重用本地地址,这里不展开讲,如果你细心的话就会发现,上一节的例子,如果没有正常关闭socket的话,就可能会出现error:98,Address already in use,等一会儿才会好。accept4不是个标准的方法,只有Linux才支持,我们暂时不考虑兼容性。setsockopt和getsockopt方法中涉及到了类型转换,结合前面的例子,这里应该难不倒你了。除了from_raw_fd,我还又给Socket实现了又一个约定俗成的方法as_raw_fd。我已经将远吗放到了这里,你可以去查看。你还可以尝试将上一节的例子,修改成我们今天封装的Socket。这一节到这里就结束了。 ...

April 15, 2019 · 6 min · jiezi

Rust 实战 - 使用套接字联网API (一)

虽然标准库已经封装好了 TcpListener 和TcpStream 等基础api,但作为Rust 的爱好者,我们可以去一探究竟。本文假设你已经对 Rust 和 Linux 操作系统有了一定了解。在 Linux 上 Rust 默认会链接的系统的 libc 以及一些其他的库,这就意味着,你可以直接使用libc中的函数。比如,你可以使用 gethostname 获取你电脑的 “hostname”:use std::os::raw::c_char;use std::ffi::CStr;extern { pub fn gethostname(name: *mut c_char, len: usize) -> i32;}fn main() { let len = 255; let mut buf = Vec::<u8>::with_capacity(len); let ptr = buf.as_mut_ptr() as *mut c_char; unsafe { gethostname(ptr, len); println!("{:?}", CStr::from_ptr(ptr)); }}解释一下上面的代码。extren 表示“外部块(External blocks)”,用来申明外部非 Rust 库中的符号。我们需要使用 Rust 以外的函数,比如 libc ,就需要在 extren 中将需要用到的函数定义出来,然后就可以像使用本地函数一样使用外部函数,编译器会负责帮我们转换,是不是很方便呢。但是,调用一个外部函数是unsafe的,编译器不能提供足够的保证,所以要放到unsafe块中。如果外部函数有可变参数,可以这么申明:extern { fn foo(x: i32, …);}不过 Rust 中的函数目前还不支持可变参数。实际上,这里应该是 extern “C” { .. },因为默认值就是"C",我们就可以将其省略。还有一些其他的可选值,因为这里不会用到,暂且不讨论,你可以去这儿这儿查看。再来说说类型。“gethostname” 函数在 C 头文件中的原型是:int gethostname(char name, size_t len);在 Linux 64位平台上,C中的int对应于Rust中的int,size_t对应Rust中的usize,但C中的char与Rust中的char是完全不同的,C中的char始终是i8或者u8,而 Rust 中的char是一个unicode标量值。你也可以去标准库查看。对于指针,Rust 中的裸指针 与C中的指针几乎是一样的,Rust的mut对应C的普通指针,*const 对应C的const指针。因此我们将类型一一对应,函数的参数名称不要求一致。pub fn gethostname(name: *mut i8, len: usize) -> i32;但是,我们后面会使用CStr::from_ptr()将C中的字符串转换为 Rust 本地字符串,这个函数的定义是:pub unsafe fn from_pt<‘a>(ptr: *const c_char) -> &‘a CStr为了“好看”一点,我就写成了c_char,但是,c_char只是i8的别名,你写成i8也没有问题的。type c_char = i8;你可以看这里。不过,如果你要是考虑跨平台的话,可能需要吧 i32 换成 std::os::raw::c_int,并不是所有平台上C中的int都对应Rust中的i32。不过,如果你没有一一对应类型,一定程度上是可行的,如果没有发生越界的话。比如像这样:use std::os::raw::c_char;use std::ffi::CStr;extern { pub fn gethostname(name: *mut c_char, len: u16) -> u16;}fn main() { let len = 255; let mut buf = Vec::<u8>::with_capacity(len); let ptr = buf.as_mut_ptr() as *mut c_char; unsafe { gethostname(ptr, len as u16); println!("{:?}", CStr::from_ptr(ptr)); }}我把 size_t 和 int 都对应成了 u16,这段代码是可以编译通过,并正确输出你的hostname的,但我建议,你最好是将类型一一对应上,以减少一些不必要的麻烦。当然,你把那个 *mut c_char 换成 *mut i32,也没问题,反正都是个指针,你可以试试:use std::os::raw::c_char;use std::ffi::CStr;extern { pub fn gethostname(name: *mut i32, len: u16) -> u16;}fn main() { let len = 255; let mut buf = Vec::<u8>::with_capacity(len); let ptr = buf.as_mut_ptr() as *mut i32; unsafe { gethostname(ptr, len as u16); println!("{:?}", CStr::from_ptr(ptr as *const i8)); }}你还可以把 Vec::<u8>换成Vec::<i32> 看看结果。int gethostname(char *name, size_t len) 这个函数,是接收一个char数组和数组长度,也可以说成接收缓冲区和接收缓冲区的最大长度。我是创建了一个容量为255的Vec<u8>,将其可变指针转换为裸指针。你也可以创建可以长度为255的u8数组,也没有问题: let len = 255; let mut buf = [0u8; 255]; let ptr = buf.as_mut_ptr() as *mut i32; unsafe { gethostname(ptr, len as u16); println!("{:?}", CStr::from_ptr(ptr as *const i8)); }为什么这样可以,因为Rust的Slice和Vec的底层内存布局,跟C是一样的。(注意,Rust中Slice与Array的关系,就像&str与str的关系)。我们可以看看Vec和Slice在源码中的定义:pub struct Vec<T> { buf: RawVec<T>, len: usize,}pub struct RawVec<T, A: Alloc = Global> { ptr: Unique<T>, cap: usize, a: A,}pub struct Unique<T: ?Sized> { pointer: *const T, _marker: PhantomData<T>,}struct FatPtr<T> { data: const T, len: usize,}Vec是一个结构体,里面包含buf和len两个字段,len用来表示Vec的长度,buf又指向另一个结构体RawVec,其中有三个字段,第三个字段a是一个Tarit,不占内存。cap用来表示Vec的容量,ptr指向另一个结构体Unique,其中的pointer字段就是一个裸指针了,_marker是给编译器看的一个标记,也不占内存,暂时不讨论这个,你可以去看文档。Slice的结构更简单,就一个裸指针和长度。虽然RawVec和Unique在标准库外部是不可见的,但我们还是能用一定的“手段”取出里面值,那就是定义一个内存布局跟Vec一样的结构体,“强行”转换。#[derive(Debug)]struct MyVec<T> { ptr: mut T, cap: usize, len: usize}我定义了一个叫做MyVec的结构体,忽略了Vec中两个不占用内存的字段,他们的内存布局是相同的,在64位平台上都是24(ptr占8个,另外两个usize个8个)个字节。你可以试试:#[derive(Debug)]struct MyVec<T> { ptr: mut T, cap: usize, len: usize}println!("{:?}", std::mem::size_of::<Vec<u8>>());println!("{:?}", std::mem::size_of::<MyVec<u8>>());我先创建一个Vec<u8>,拿到Vec<u8>的裸指针const Vec<u8>,再将const Vec<u8>转换为const MyVec<u8>,之后,解引用,就能得到MyVec<u8>了。不过,解引裸指针是unsafe的,要谨慎!!! 你还可以看看标准库中讲述pointer的文档。fn main() { let vec = Vec::<u8>::with_capacity(255); println!(“vec ptr: {:?}”, vec.as_ptr()); #[derive(Debug)] struct MyVec<T> { ptr: *mut T, cap: usize, len: usize } let ptr: *const Vec<u8> = &vec; let my_vec_ptr: *const MyVec<u8> = ptr as _; unsafe { println!("{:?}", *my_vec_ptr); }}然后编译运行,是否可以看到类似下面的输出呢:vec ptr: 0x557933de6b40MyVec { ptr: 0x557933de6b40, cap: 255, len: 0 }你可以看到,我们调用vec.as_ptr()得到的就是Vec内部的那个裸指针。对于std::mem::size_of 相等的两个类型,你也可以使用std::mem::transmute 这个函数转换,跟上面的通过裸指针间接转换,几乎是等效的,只是会多加一个验证,如果两个类型size_of不相等的话,是无法通过编译的。这个函数是unsafe的。你还可以继续尝试,比如把Vec<u8>转换为长度为3(或者更小更大)的usize数组,像是这样:fn main() { let vec = Vec::<u8>::with_capacity(255); println!(“vec ptr: {:?}”, vec.as_ptr()); let ptr: *const Vec<u8> = &vec; unsafe { let aaa_ptr: *const [usize; 2] = ptr as _; println!("{:?}", (*aaa_ptr)[0] as *const u8); }}不过,由于Rust中Vec的扩容机制,这段代码是存在一定问题的:fn main() { let len = 255; let mut buf = Vec::<u8>::with_capacity(len); let ptr = buf.as_mut_ptr() as *mut c_char; unsafe { gethostname(ptr, len); println!("{:?}", CStr::from_ptr(ptr)); } println!("{:?}", buf);}虽然获取到了正确的主机名,但是之后你打印buf会发现,buf是空的,这个问题留给你去探究。你已经看到,Rust已经变得“不安全”,这又不小心又引入了另一个话题–《 Meet Safe and Unsafe》。不过,还是尽快回归正题,等之后有机会再说这个话题。说起套接字API,主要包括TCP、UDP、SCTP相关的函数,I/O复用函数和高级I/O函数。其中大部分函数Rust标准里是没有的,如果标准库不能满足你的需求,你可以直接调用libc中的函数。实际上,标准库中,网络这一块,也基本是对libc中相关函数的封装。先从TCP开始。TCP套接字编程主要会涉及到socket、connect、bind、listen、accept、close、getsockname、getpeername等函数。先来看看这些函数的定义:// socket 函数用来指定期望的通信协议类型,并返回套接字描述符int socket(int family, int type, int protocol); // 成功返回监听描述符。用来设置监听,出错为-1// family是表示socket使用的协议类型,对于TCP,通常设置为 AF_INET 或AF_INET6,表示IPv4和IPv6// type是创建的套接字类型,TCP是字节流套接字,所以这里设置为SOCK_STREAM,可选的值还有// SOCK_DGRAM用于UDP,SOCK_SEQPACKET用于SCTP// protocol协议的标识,可以设置为0,让系统选择默认值。可选的值有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_SCTP// connect 函数被客户端用来联立与TCP服务器的连接int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen); // 成功返回0 ,出错为-1// sockfd 是由 socket 函数返回的套接字描述符,第二和第三个参数分别指向一个指向套接字地址结构的指针和该指针的长度// bind 函数把一个本地协议地址赋予一个套接字。int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); // 成功返回0 ,出错为-1// 第二个和第三个参数分别是指向特点于协议的地址结构的指针和指针的长度// listen 函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。int listen(int sockfd, int backlog); // 成功返回0 ,出错为-1// 第二个参数指定内核该为相应套接字排队的最大连接个数。// accept 函数由TCP服务器调用,用于从已完成连接的队列头返回下一个已完成的连接。int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); // 成功返回非负描述符,错误返回-1// 第二个和第三个参数用来返回客户端的协议地址和该地址的大小// close 用来关闭套接字,并终止TCP连接int close(int sockfd); // 成功返回0 ,出错为-1// getsockname 和 getpeername 函数返回与某个套接字关联的本地协议地址和外地协议地址int getsockname(int sockfd,struct sockaddr *localaddr,socklen_t *addrlen); // 成功返回0 ,出错为-1int getpeername(int sockfd,struct sockaddr *peeraddr,socklen_t *addelen); // 成功返回0 ,出错为-1还有一对常见的函数,read 和 write 用于读写数据。另外还有三对高级I/O函数,recv/send、readv/writev和recvmsg/sendmsg等需要的时候再加。ssize_t read(int fd, void *buf, size_t count);ssize_t write(int fd, const void *buf, size_t count);除了函数外,还有几个常量和sockaddr这个结构体。常量我们需要在Rust这边定义出来,只定义出需要的:const AF_INET: i32 = 2;const AF_INET6: i32 = 10;const SOCK_STREAM: i32 = 1;const IPPROTO_TCP: i32 = 6;除了sockaddr外,还有几个与之相关的结构体,他们在C中的定是:struct sockaddr{ unsigned short int sa_family; // 地址族 unsigned char sa_data[14]; // 包含套接字中的目标地址和端口信息};struct sockaddr_in{ sa_family_t sin_family; uint16_t sin_port; struct in_addr sin addr; char sin_zero[8];};struct in_addr{ In_addr_t s_addr;};struct sockaddr_in6{ sa_family_t sin_family; in_port_t sin6_port; uint32_t sin6_flowinfo; struct in6_addr sin6_addr; uint32_t sin6_scope_id;};struct in6_addr{ uint8_t s6_addr[16]};struct sockaddr_storage { sa_family_t ss_family; // address family // all this is padding, implementation specific, ignore it: char __ss_pad1[_SS_PAD1SIZE]; int64_t __ss_align; char __ss_pad2[_SS_PAD2SIZE];};然后,需要在Rust中定义出布局相同的结构体:#[repr(C)]#[derive(Debug, Clone, Copy)]pub struct sockaddr { pub sa_family: u16, pub sa_data: [c_char; 14],}#[repr(C)]#[derive(Debug, Clone, Copy)]pub struct sockaddr_in { pub sin_family: u16, pub sin_port: u16, pub sin_addr: in_addr, pub sin_zero: [u8; 8],}#[repr(C)]#[derive(Debug, Clone, Copy)]pub struct in_addr { pub s_addr: u32,}#[repr(C)]#[derive(Debug, Clone, Copy)]pub struct sockaddr_in6 { pub sin6_family: u16, pub sin6_port: u16, pub sin6_flowinfo: u32, pub sin6_addr: in6_addr, pub sin6_scope_id: u32,}#[repr(C)]#[derive(Debug, Clone, Copy)]pub struct in6_addr { pub s6_addr: [u8; 16],}#[repr(C)]#[derive(Debug, Clone, Copy)]pub struct sockaddr_storage { pub ss_family: u16, _unused: [u8; 126]}你需要在结构体前面加一个#[repr(C)]标签,以确保结构体的内存布局跟C一致,因为,Rust结构体的内存对齐规则,可能跟C是不一样的。#[derive(Debug, Clone, Copy)] 不是必须的。对于最后一个结构体sockaddr_storage,我也很迷,我不知道在Rust中如何定义出来,但是我知道它占128个字节,然后我就定义一个长度为126的u8数组,凑够128位。接下来,继续把那几个函数定义出来:extern { pub fn socket(fanily: i32, ty: i32, protocol: i32) -> i32; pub fn connect(sockfd: i32, servaddr: *const sockaddr, addrlen: u32) -> i32; pub fn bind(sockfd: i32, myaddr: *const sockaddr, addrlen: u32) -> i32; pub fn listen(sockfd: i32, backlog: i32); pub fn accept(sockfd: i32, cliaddr: *mut sockaddr, addrlen: u32) -> i32; pub fn close(sockfd: i32) -> i32; pub fn getsockname(sockfd: i32, localaddr: *mut sockaddr, addrlen: *mut u32) -> i32; pub fn getpeername(sockfd: i32, peeraddr: *mut sockaddr, addrlen: *mut u32) -> i32; pub fn read(fd: i32, buf: *mut std::ffi::c_void, count: usize) -> isize; pub fn write(fd: i32, buf: const std::ffi::c_void, count: usize) -> isize;}对于read 和 write 里的参数buf类型void, 可以使用标准库提供的 std::ffi::c_void,也可以是mut u8/*const u8,像是下面这样:pub fn read(fd: i32, buf: *mut u8, count: usize) -> isize;pub fn write(fd: i32, buf: *const u8, count: usize) -> isize;或者,既然void本身是个“动态类型”,也可以传个其他类型的指针进去的,之后你可以试试,不过可能会有点危险。看看目前的代码:use std::os::raw::c_char;use std::ffi::c_void;pub const AF_INET: i32 = 2;pub const AF_INET6: i32 = 10;pub const SOCK_STREAM: i32 = 1;pub const IPPRPTO_TCP: i32 = 6;#[repr(C)]#[derive(Debug, Clone, Copy)]pub struct sockaddr { pub sa_family: u16, pub sa_data: [c_char; 14],}#[repr(C)]#[derive(Debug, Clone, Copy)]pub struct sockaddr_in { pub sin_family: u16, pub sin_port: u16, pub sin_addr: in_addr, pub sin_zero: [u8; 8],}#[repr(C)]#[derive(Debug, Clone, Copy)]pub struct in_addr { pub s_addr: u32,}#[repr(C)]#[derive(Debug, Clone, Copy)]pub struct sockaddr_in6 { pub sin6_family: u16, pub sin6_port: u16, pub sin6_flowinfo: u32, pub sin6_addr: in6_addr, pub sin6_scope_id: u32,}#[repr(C)]#[derive(Debug, Clone, Copy)]pub struct in6_addr { pub s6_addr: [u8; 16],}#[repr(C)]#[derive(Clone, Copy)]pub struct sockaddr_storage { pub ss_family: u16, _unused: [u8; 126]}extern { pub fn socket(fanily: i32, ty: i32, protocol: i32) -> i32; pub fn connect(sockfd: i32, servaddr: *const sockaddr, addrlen: u32) -> i32; pub fn bind(sockfd: i32, myaddr: *const sockaddr, addrlen: u32) -> i32; pub fn listen(sockfd: i32, backlog: i32); pub fn accept(sockfd: i32, cliaddr: *mut sockaddr, addrlen: *mut u32) -> i32; pub fn close(sockfd: i32) -> i32; pub fn getsockname(sockfd: i32, localaddr: *mut sockaddr, addrlen: *mut u32) -> i32; pub fn getpeername(sockfd: i32, peeraddr: *mut sockaddr, addrlen: *mut u32) -> i32; pub fn read(fd: i32, buf: *mut std::ffi::c_void, count: usize) -> isize; pub fn write(fd: i32, buf: *const std::ffi::c_void, count: usize) -> isize;}然后,我们可以写一个简单的服务器和客户端程序:服务器监听一个地址,客户端连接服务器,然后向服务器发送“Hello, server!”,服务器回应“Hi,client!”,客户端收到后断开连接。fn main() { use std::io::Error; use std::mem; use std::thread; use std::time::Duration; thread::spawn(|| { // server unsafe { let socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if socket < 0 { panic!(“last OS error: {:?}”, Error::last_os_error()); } let servaddr = sockaddr_in { sin_family: AF_INET as u16, sin_port: 8080u16.to_be(), sin_addr: in_addr { s_addr: u32::from_be_bytes([127, 0, 0, 1]).to_be() }, sin_zero: mem::zeroed() }; let result = bind(socket, &servaddr as *const sockaddr_in as *const sockaddr, mem::size_of_val(&servaddr) as u32); if result < 0 { println!(“last OS error: {:?}”, Error::last_os_error()); close(socket); } listen(socket, 128); loop { let mut cliaddr: sockaddr_storage = mem::zeroed(); let mut len = mem::size_of_val(&cliaddr) as u32; let client_socket = accept(socket, &mut cliaddr as *mut sockaddr_storage as *mut sockaddr, &mut len); if client_socket < 0 { println!(“last OS error: {:?}”, Error::last_os_error()); break; } thread::spawn(move || { loop { let mut buf = [0u8; 64]; let n = read(client_socket, &mut buf as *mut _ as *mut c_void, buf.len()); if n <= 0 { break; } println!("{:?}", String::from_utf8_lossy(&buf[0..n as usize])); let msg = b"Hi, client!"; let n = write(client_socket, msg as *const _ as *const c_void, msg.len()); if n <= 0 { break; } } close(client_socket); }); } close(socket); } }); thread::sleep(Duration::from_secs(1)); // client unsafe { let socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if socket < 0 { panic!(“last OS error: {:?}”, Error::last_os_error()); } let servaddr = sockaddr_in { sin_family: AF_INET as u16, sin_port: 8080u16.to_be(), sin_addr: in_addr { s_addr: u32::from_be_bytes([127, 0, 0, 1]).to_be() }, sin_zero: mem::zeroed() }; let result = connect(socket, &servaddr as *const sockaddr_in as *const sockaddr, mem::size_of_val(&servaddr) as u32); if result < 0 { println!(“last OS error: {:?}”, Error::last_os_error()); close(socket); } let msg = b"Hello, server!"; let n = write(socket, msg as *const _ as *const c_void, msg.len()); if n <= 0 { println!(“last OS error: {:?}”, Error::last_os_error()); close(socket); } let mut buf = [0u8; 64]; let n = read(socket, &mut buf as mut _ as mut c_void, buf.len()); if n <= 0 { println!(“last OS error: {:?}”, Error::last_os_error()); } println!("{:?}", String::from_utf8_lossy(&buf[0..n as usize])); close(socket); }}调用外部函数是unsafe的,我为了简单省事,暂时把代码放到了一个大的unsafe {} 中,之后我们再把他们封装成safe的API。为了方便测试,我把服务器程序放到了一个线程里,然后等待1秒后,再让客户端建立连接。std::io::Error::last_os_error 这个函数,是用来捕获函数操作失败后,内核反馈给我们的错误。在调用bind 和 connect 函数时,先要创建sockaddr_in结构体,端口(sin_port)和IP地址(s_addr) 是网络字节序(big endian),于是我调用了u16和u32的to_be()方法将其转换为网络字节序。u32::from_be_bytes 函数是将[127u8, 0u8, 0u8, 1u8] 转换为u32整数,由于我们看到的已经是大端了,转换回去会变成小端,于是后面又调用了to_be(),你也可以直接u32::from_le_bytes([127, 0, 0, 1])。然后使用了std::mem::zeroed 函数创建一个[0u8; 8] 数组,你也可以直接[0u8; 8],在这里他们是等效的。接着,我们进行强制类型转换,将&sockaddr_in 转换为const sockaddr_in类型,又继续转换为const sockaddr,如果你理解了一开始“gethostname”那个例子话,这里应该很好理解。这里还可以简写成&servaddr as *const _ as *const _,编译器会自动推导类型。在调用accept函数时,先创建了一个mut sockaddr_storage,同样进行类型转换。之所以用sockaddr_storage 而不是sockaddr_in和sockaddr_in6是因为sockaddr_storage这个通用结构足够大,能承载sockaddr_in和sockaddr_in6等任何套接字的地址结构,因此,我们如果把套接bind到一个IPv6地址上的话,这里的代码是不需要修改的。我还是用std::mem::zeroed 函数初始化sockaddr_storage,它的结构我也很迷惑,所以就借助了这个函数,这个函数是unsafe的,使用的时候要小心。你也可以继续尝试这个函数:let mut a: Vec<u8> = unsafe { std::mem::zeroed() };a.push(123);println!("{:?}", a);在read 和 write 时,同样要类型转换。很多时候,类型根本“强不起来”。OK,这一节的内容就先到这里。 ...

April 14, 2019 · 7 min · jiezi

rust的私有代码仓库

背景公司内部要使用或推广必须要满足私有性和保密性,公司内部的代码是不可以公开到公网上的。rust的公开代码仓库是crates.io,那么私有仓库的解决方案是什么?我们一起来探索一下。国外网站reddit有人发出了对私有仓库的需求并提问到:Private “crates.io"Hi,I’m trying to advocate for Rust in my company, and one of the needs will be to have our own “crates.io” so our (closed source) crates can be managed by cargo.Is there a way to do that appart from specifying git urls?另外,这位程序员还不想通过git来管理仓库,因为他的公司不用git。withoutboats大牛的解决方案来了!2017年10月,他说道:本周,cargo获得了一个新功能!现在您可以从其他仓库下载依赖包也可以从ractes.io下载的依赖包。这是重要的一步,使组织能够通过cargo分发其内部代码库,而无需将这些内部代码库上传到公共仓库。此功能将仅在夜间可用,并用alternative-registries这个feature gate来进行封闭。自1.0版本以来,我们已经使用feature gate 迭代rustc中新的不稳定的特性,但这是我们第一次在Cargo中使用feature gate。这里我解释一下此段话。因为rust的发布版本是很有节奏的,按每六周一个版本的节奏分为nightly、beta、stable三种版本。rust还在发展阶段,版本迭代节奏快,开发团队想要快速发新功能并在外部快速得到验证,每个在开发的功能都会发布在nightly版本。nightly版本是不稳定的,发布后再过六周就会自动变为beta版本(期间会修改bug)。beta版本再过六周就会成功稳定版本。节奏图如下:nightly: * - - * - - * - - * - - * - - * - * - * | |beta: * - - - - - - - - * * |stable: *我们继续大牛的讲述:自1.0之前起,cargo已与crate.io仓库结合成一对。这个仓库包含数千个crate,它们构成了rust的代库库生态系统的大部分,cargo使下载和使用crates.io中的代码库变得非常方便。但是,有很多原因使用户可能不希望将其代码库上传到cractes.io。特别是,专有代码不适用于crates.io,crates.io允许任何人下载它包含的任何crate。因此,cargo总是支持其他来源的仓库。具体来说,它支持从文件路径或Git仓库中拉取依赖包。好了,也就是说可以使用git仓库来做私有仓库。哈哈另一方面,到目前为止,cargo只允许你一次依靠一个仓库的crate。在许多情况下,像crates.io这样的仓库比git更好,因为它们能够更有效地管理多个版本的crate,并为您执行版本解析。使用这个新功能,您将能够从ractes.io以外的仓库中拉取crate。好了,而且这个功能出来后,对于同一个rust项目用户可以同时使用两种上以的仓库。实操… ...

April 13, 2019 · 1 min · jiezi

与顶尖 Rust 开发者面基指南(一) | RustCon Asia

距离 4 月 20 日 RustCon Asia 大会 开启只剩下约两周的时间了,你准备好了吗?此次 RustCon Asia 是首次在亚洲举办的 Rust 语言开发者大会,也是目前亚洲地区规格最高,参与人数规模最大的 Rust 语言大会。不仅有来自亚洲社区的大神,还有从欧洲、澳洲、北美远道而来的顶尖开发者。现场特地配备了中英双语同声传译,以便更流畅地传达演讲内容,希望大家没有顾虑的与讲师们面基!随着大会日期的不断临近,我们也逐一介绍了部分讲师及其议题,方便大家提前了解更多信息(做好功课勾搭大神:D )。今天先为大家介绍其中 8 位讲师和议题,快来看看大神们的庐山真面目吧!Nick CameronRust 语言团队核心成员Rust dev-tools 和 Cargo 团队负责人前 Mozilla Research 研究工程师此次 RustCon Asia 大会,Nick Cameron 将带来的演讲主题是《Making Rust Delightful》。Rust 的设计目标是成为符合人机工程学语言,那种易于阅读、易编写和维护的、并且是令人愉悦的编程语言!那么,语言和库的设计者是如何决定一个新的特性是否符合人机工程学?如何考虑人机工程学与其它设计需求(比如安全、性能)之间的权衡呢?Nick 将会向大家介绍 Rust 的设计理念以及一些关于语言本身、库和工具的人机工程学研究案例。另外还将和大家一起聊聊 Rust 语言团队和其他团队是如何做决策的。以及大家所关心的 Rust 的“显性与隐性”、“语法糖”和“一致性”等话题。孙茗珅美国百度 X-Lab 高级安全研究员此次 RustCon Asia 大会,孙茗珅将带来的演讲主题是《Linux From Scratch in Rust》。Rust 在储存安全和零抽象方面的出色使其成为系统编程的最佳候选者。为了提供安全的执行环境,我们使用 Rust 从头开始构建 Linux 发行版,包括构建系统,用户空间实用程序和简单的包管理系统。本次演讲主题,孙茗珅将主要关注用户空间工具箱(核心系统实用程序的集合),和大家讨论在构建工具箱时会遇到的设计挑战和问题,例如处理 I/O 标准,动态调度与静态泛通用类型、测试和覆盖问题等。Ravi ShankarMozillian开源运动支持者Servo 项目贡献者此次 RustCon Asia 大会,Ravi Shankar 将带来的演讲主题是《How Rust taught me to think about systems》。所有 Rustaceans 都知道 Rust 的 borrow checker 对新手来说是很难的。这个演讲涵盖了他作为 Rust 新手时遇到的各种各样的情况,这些情况在许多高级语言中是完全正常的,但在 Rust 中却会出现问题:为什么同样的代码在 Rust 中编译会不一样,如何理解 Rust 中的编译错误,以及最后这些又是如何改变 Ravi 的思考方式的?不撸兔子网红 B 大Erlang 粉Porus 项目作者此次 RustCon Asia 大会,不撸兔子将带来的演讲主题是《Rust for competitive programming》。competitive programming 要求开发者在极短时间内保质保量的解决问题。由于没有一个单独为 competitive programming 设计的代码库,contenders 通常必须从头开始执行数据结构和算法,十分繁琐且容易出错。 这个演讲将会告诉大家为什么对于competitive programming,Rust 是不可替代的。荆一明美国百度 X-Lab 安全科学家Rust 开源项目 MesaLink 作者此次 RustCon Asia 大会,荆一明将带来的演讲主题是《Cargo meets Autotools》。从 1.10 版本开始,只要在 Cargo.toml 中指定了cdylib crate 类型,rustc 就可以生成一个动态库,供 C 或 C FFI 使用。虽然 cargo install 命令使分发可执行文件(例如ripgrep)变得轻而易举,但它不适用于 cdylib 动态库。早在2018年,为了构建和分发用 Rust 编写的动态库,团队一直在努力实现有效的基础架构。最终使 autotools 与 Rust 工具链完美结合。现在用户可以下载源代码压缩包,解压缩并安装运行./configure && make && make install 。那么在这次分享中,他会详细聊一聊这里面的故事,也希望对社区带来帮助。Rahul Thakoor树莓派粉IRR 计划参与者在这次 RustCon Asia 大会上,Rahul 将会为大家带来《Introduction to IoT using Blynk, Rust and your Smartphone》主题分享。想要用 Rust 来利用智能手机的传感器和执行器来学习物联网的基础,并建立虚拟和物理世界的桥梁吗?在第三天的 Workshop 中,参与者不需要特别准备就可以体验嵌入式世界。Rahul 将使用 Blynk,这是一个免费的智能手机应用程序,为你的物联网项目提供拖放小部件。参与者只需要智能手机(iOS 或 Android)和运行Linux,macOS 或 Windows 的笔记本电脑就行了。Rahul 将介绍物联网的基础知识。参与者将配置虚拟 LED 和按钮,收集 GPS stream 或加速计等传感器数据,或将事件和数据发送到手机。最后,参与者将能够使用你的技能学习原型(your skills learned prototyping)制作更多有创意和有趣的项目,开辟自己的道路。参与者将更好地了解物联网项目,并从微控制器或其他硬件上开始使用嵌入式 Rust 开发。黄旭东May 项目作者在此次 RustCon Asia 大会上,黄旭东即将带来的演讲主题是《Stackful Coroutine Based Rust Async Story》。他将和大家分享基于 stackful generators 和 coroutine 的异步故事,也就是 May 的设计与实现,包括有关 generator 和 coroutine 的基本理论,coroutine 调度的整体结构,IO 子系统,同步抢占子系统以及取消机制等方方面面。同时,也会将 May 与当前 Rust 官方的异步 future 系统进行对比分析。也欢迎大家来 GitHub 给 May 提 PR,我们都爱 ka 贡献者。孙晓光知乎解压师知乎搜索工程团队负责人在本届 RustCon Asia 大会上,孙晓光将会给大家带来《Search Engine in production with Rust》主题演讲,分享知乎团队在用 Rust 开发实用搜索引擎过程中的设计选型和经验教训,也让其他 Rust 开发者能够尽可能避免知乎团队已踩过的坑,以及更顺利地将 Rust 用到开发生产中去。此次 RustCon Asia 大会为期四天,包括 20 日全天和 21 日上午的主题演讲和 22-23 日的多个主题 workshop 环节。其中主题演讲讲师来自于国内外资深 Rust 开发者和社区活跃贡献者;workshop 主题将覆盖到 Rust 开发入门和成熟技术栈或产品的实战操作和演示。活动时间:4 月 20-23 日活动地点:北京 · 朝阳广顺南大街 8 号北京望京凯悦酒店目前 RustCon Asia 还有少量余票,扫描下方二维码购买。大会官网:https://rustcon.asia/Twitter @RustConAsia ...

April 4, 2019 · 2 min · jiezi

与 Rust 社区亲密接触!首届 RustCon Asia 来了

首先抢票:www.huodongxing.com/event/64794…大会官网:rustcon.asia2019 年 4 月 20 日在北京望京凯悦,在秘猿科技与 PingCAP 携手下,我们开始即将开启第一场 RustCon Asia。大会为期 4 天,1.5 天主题演讲,2 天动手研讨 workshop。Rust 是一门比较现代的,吸收了最近几十年在语言设计和编译器方面的优秀精华的语言。我们非常喜欢他的设计,尤其是 Rust 在工程方面展现出的强大优势:Performance 高性能!Reliability 可靠!Productivity 效率!Rust 是被生产环境验证并且取得成功的语言,秘猿科技基于 Rust 开发的系统在国内最顶级的金融系统中跑的一些项目,已经充分验证了 Rust 的能力。另外,Rust 社区是一个我们最推崇的那种包容、开放的社区,Rust 的每一项改进,都是通过 RFC 去讨论、去达成共识再实现。Rust 这种基于 RFC 的机制给了这门语言极为旺盛的生命力和开放度,我们信仰开放的文化更有机会成功,所以我们在做自己的项目的时候,也借鉴了 RFC 机制。Rust 海外社区非常活跃,我们一直在吸收,现在是合适的时间去反哺社区。从去年开始,我们趁着各种机会开始投入 Rust 社区。我们开始了每月一次的 Rust Dev Sharing,第一场的直播达到了 2000 人同时在线;第二次,Bilibili 的雪松老师在工作日从上海跑来杭州加入了分享。我们感受到了 Rust 社区的激情,看到国内越来越多的 Rust 项目,也笃定 Rust 是未来。去年 11 月,秘猿科技赞助并参与了罗马的 RustFest 为期 4 天的大会,会上我们碰到了 Rust team 的 Aaron、Pascal、Steve、Florian,社区活跃开发者,当然还有 PingCAP 的伙伴 Ana(Hoverbear)。即便有着跨文化沟通的障碍存在,大家对亚洲的 Rust 大会都表达出非常地期待,以至于在活动官网还没上线的时候,欧美的 Rust 社区就已经知晓并传播中国 Rust 大会的声音。Rust Language 的 Twitter @rustlang 还在除夕夜发了一条「猪年大吉」的推文!经过春节假期,我们在 2 月 15 日上线 RustCon Asia 官网(http://rustcon.asia),接下来 2 周时间我们的 CFP(Call For Proposals) 流程共计收到中英文 proposal 50 份。社区小伙伴热情帮推,除了各种群、朋友圈、知乎、Twitter 帮扩散,《Rust 编程之道》作者汉东老师将大会信息发到了 Rust 的每日订阅频道,有国外来不了的小伙伴购买了 donation 票,作为对大会的支持。与其说这是我们组织的大会,不如说是社区驱动的结果。在此也特别感谢帮助推广 RustCon Asia 信息的社区合作伙伴我们在此再次介绍一下大会详情安排: 4.20-21 为一天半的 keynote 演讲——要从 50 份 proposal 里面评选出 14 位优秀讲师!不用担心语言问题,我们有同传!4.22-23 为两天的 workshop,3 个房间同时进行——你可以有更丰富更自由的选择!这是一场属于社区的大会,除了正经的大会安排,我们也强烈建议现场及时行乐,各种组小队约饭、约咖啡、约酒,其它约什么我们不管(邪恶微笑)。毕竟一年仅此一次!扩散起来,邀请更多的小伙伴一起来嗨!Rust 爱好者、开源爱好者、技术社区贡献者、来旁观大家怎么嗨的,就是你们了!Rust 可以让人重新爱上编程!请肆无忌惮地炫耀,Rust 社区真的很棒。另外八卦一下大会门票 399,包含大会的 350 自助午餐!限额四百张,购票进行中~ ...

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中文社刊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内存分配器的不同行为

本文出自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

Rust crates.io换国内镜像源

由于众所周知的原因,crates.io在国内访问十分糟糕,轻则编译缓慢,要很久很久,重则直接超时报错,编译不了.下面就为rust crates.io换上国内中科大的源1. 进入当前用户的 .cargo 目录 cd ~/.cargo2. 新建名字叫 config 的文件3. 编辑 config 文件写入[source.crates-io]registry = “https://github.com/rust-lang/crates.io-index"replace-with = ‘ustc’[source.ustc]registry = “git://mirrors.ustc.edu.cn/crates.io-index”======= end ======参考中科大官方说明:https://lug.ustc.edu.cn/wiki/…

December 22, 2018 · 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

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