first commit
This commit is contained in:
501
专栏/陈天·Rust编程第一课/用户故事语言不仅是工具,还是思维方式.md
Normal file
501
专栏/陈天·Rust编程第一课/用户故事语言不仅是工具,还是思维方式.md
Normal file
@ -0,0 +1,501 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
用户故事 语言不仅是工具,还是思维方式
|
||||
你好,我是 Pedro,一名普普通通打工人,平平凡凡小码农。
|
||||
|
||||
可能你在课程留言区看到过我,也跟我讨论过问题。今天借着这篇用户故事的机会,正好能跟你再多聊几句。
|
||||
|
||||
我简单整理了一下自己入坑编程以来的一些思考,主要会从思维、语言和工具三个方面来聊一聊,最后也给你分享一点自己对 Rust 的看法,当然以下观点都是“主观”的,观点本身不重要,重要的是得到观点的过程。
|
||||
|
||||
从思维谈起
|
||||
|
||||
从接触编程开始,我们就已经开始与编程语言打交道,很多人学习编程的道路往往就是熟悉编程语言的过程。
|
||||
|
||||
在这个过程中,很多人会不适应,写出的代码往往都不能运行,更别提设计与抽象。出现这个现象最根本的原因是,代码体现的是计算机思维,而人脑思维和计算机思维差异巨大,很多人一开始无法接受两种思维差异带来的巨大冲击。
|
||||
|
||||
那么,究竟什么是计算机思维?
|
||||
|
||||
计算机思维是全方位的,体现在方方面面,我以个人视角来简单概括一下:
|
||||
|
||||
|
||||
自顶向下:自顶向下是计算机思维的精髓,人脑更加适合自底向上。计算机通过自顶向下思维将大而难的问题拆解为小问题,再将小问题逐一解决,从而最终解决大问题。
|
||||
多维度、多任务:人脑是线性的,看问题往往是单维的,我们很难同时处理和思考多个问题,但是计算机不一样,它可以有多个 CPU 核心,在保存上下文的基础上能够并发运行成百上千的任务。
|
||||
全局性:人的精力、脑容量是有限的,而计算机的容量几乎是无限的;人在思考问题时,限于自己的局部性,拿到局部解就开始做了,而计算机可以在海量数据的基础上再做决策,从而逼近全局最优。
|
||||
协作性:计算机本身就是一件极其精细化的工程艺术品,它复杂精巧,每个部分都只会做自己最擅长的事情,比如将计算和存储剥离,计算机高效运作的背后是每个部分协作的结果,而人更擅长单体作战,只有通过大量的训练,才能发挥群体的作用。
|
||||
迭代快:人类进化、成长是缓慢的,直到现在,很多人的思维方式仍旧停留在上个世纪,而计算机则不同,进入信息时代后,计算机就遵循着摩尔定律,每 18 个月翻一番,十年前的手机放在今天可能连微信都无法正常运行。
|
||||
取舍:在长期的社会发展中,人过分喜欢强调对与错,喜欢追求绝对的公平,讽刺的是,由二进制组成的计算机却不会做出非黑即白的决策,无论是计算机本身(硬件),还是里面运行的软件,每一个部分都是性能、成本、易用性多角度权衡的结果。
|
||||
So on…
|
||||
|
||||
|
||||
当这些思维直接体现在代码里面,比如,自顶向下体现在编程语言中就是递归、分治;多维度、多任务的体现就是分支、跳转、上下文;迭代、协作和取舍在编程中也处处可见。
|
||||
|
||||
而这些恰恰是人脑思维不擅长的点,所以很多人无法短时间内做到编程入门。想要熟练掌握编程,就必须认识到人脑与计算机思维的差异,强化计算机思维的训练,这个训练的过程是不太可能短暂的,因此编程入门必须要消耗大量的时间和精力。
|
||||
|
||||
语言
|
||||
|
||||
不过思维的训练和评估是需要有载体的,就好比评估你的英文水平,会考察你用英文听/说/读/写的表达能力。那我们的计算机思维怎么表达呢?
|
||||
|
||||
于人而言,我们可以通过肢体动作、神情、声音、文字等来表达思维。在漫长的人类史中,动作、神情、声音这几种载体很难传承和传播,直到近代,音、视频的兴起才开始慢慢解决这个问题。
|
||||
|
||||
文字,尤其是语言诞生后的文字,成了人类文明延续、发展的主要途径之一,直至今天,我们仍然可以通过文字来与先贤对话。当然,对话的前提是,这些文字你得看得懂。
|
||||
|
||||
而看得懂的前提是,我们使用了同一种或类似的语言。
|
||||
|
||||
回到计算机上来,现代计算机也是有通用语言的,也就是我们常说的二进制机器语言,专业一点叫指令集。二进制是计算机的灵魂,但是人类却很难理解、记忆和应用,因此为了辅助人类操纵计算机工作,上一代程序员们对机器语言做了第一次抽象,发明了汇编语言。
|
||||
|
||||
但伴随着硬件、软件的快速发展,程序代码越来越长,应用变得愈来愈庞大,汇编级别的抽象已经无法满足工程师对快速高效工作的需求了。历史的发展总是如此地相似,当发现语言抽象已经无法满足工作时,工程师们就会在原有层的基础上再抽象出一层,而这一层的著名佼佼者——C语言直接奠定了今天计算机系统的基石。
|
||||
|
||||
从此以后,不计其数的编程语言走向计算机的舞台,它们如同满天繁星,吸引了无数的编程爱好者,比如说迈向中年的 Java 和新生代的 Julia。虽然学习计算机最正确的途径不是从语言开始,但学习编程最好、最容易获取成就感的路径确实是应该从语言入手。因此编程语言的重要性不言而喻,它是我们走向编程世界的大门。
|
||||
|
||||
C 语言是一种命令式编程语言,命令式是一种编程范式;使用 C 写代码时,我们更多是在思考如何描述程序的运行,通过编程语言来告诉计算机如何执行。
|
||||
|
||||
举个例子,使用 C 语言来筛选出一个数组中大于 100 的数字。对应代码如下:
|
||||
|
||||
int main() {
|
||||
int arr[5] = { 100, 105, 110, 99, 0 };
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
if (arr[i] > 100) {
|
||||
// do something
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
在这个例子中,代码撰写者需要使用数组、循环、分支判断等逻辑来告诉计算机如何去筛选数字,写代码的过程往往就是计算机的执行过程。
|
||||
|
||||
而对于另一种语言而言,比如 JavaScript,筛选出大于 100 的数字的代码大概是这样的:
|
||||
|
||||
let arr = [ 100, 105, 110, 99, 0 ]
|
||||
let result = arr.filter(n => n > 100)
|
||||
|
||||
|
||||
相较于 C 来说,JavaScript 做出了更加高级的抽象,代码撰写者无需关心数组容量、数组遍历,只需将数字丢进容器里面,并在合适的地方加上筛选函数即可,这种编程方式被称为声明式编程。
|
||||
|
||||
可以看到的是,相较于命令式编程,声明式编程更倾向于表达在解决问题时应该做什么,而不是具体怎么做。这种更高级的抽象不仅能够给开发者带来更加良好的体验,也能让更多非专业人士进入编程这个领域。
|
||||
|
||||
不过命令式编程和声明式编程其实并没有优劣之分,主要区别体现在两者的语言特性相较于计算机指令集的抽象程度。
|
||||
|
||||
其中,命令式编程语言的抽象程度更低,这意味着该类语言的语法结构可以直接由相应的机器指令来实现,适合对性能极度敏感的场景。而声明式编程语言的抽象程度更高,这类语言更倾向于以叙事的方式来描述程序逻辑,开发者无需关心语言背后在机器指令层面的实现细节,适合于业务快速迭代的场景。
|
||||
|
||||
不过语言不是一成不变的。编程语言一直在进化,它的进化速度绝对超过了自然语言的进化速度。
|
||||
|
||||
在抽象层面上,编程语言一直都停留在机器码 -> 汇编 -> 高级语言这三层上。而对于我们广大开发者来说,我们的目光一直聚焦在高级语言这一层上,所以,高级编程语言也慢慢成为了狭隘的编程语言(当然,这是一件好事,每一类人都应该各司其职做好自己的事情,不用过多担心指令架构、指令集差异带来的麻烦)。
|
||||
|
||||
谈到这里,不知你是否发现了一个规律:抽象越低的编程语言越接近计算机思维,而抽象越高越接近人脑思维。
|
||||
|
||||
是的。现代层出不穷的编程语言,往往都是在人脑、计算机思维之间的平衡做取舍。那些设计语言的专家们似乎在这个毫无硝烟的战场上博弈,彼此对立却又彼此借鉴。不过哪怕再博弈,按照人类自然语言的趋势来看,也几乎不可能出现一家独大的可能,就像人类目前也是汉语、英语等多种语言共存,即使世界语于 1887 年就被发明,但我们似乎从未见过谁说世界语。
|
||||
|
||||
既然高级编程语言那么多,对于有选择困难症的我们,又该做出何种选择呢?
|
||||
|
||||
工具
|
||||
|
||||
一提到选语言,估计你常听这么一句话,语言是工具。很长一段时间里,我也这么告诫自己,无所谓一门语言的优劣,它仅仅只是一门工具,而我需要做的就是将这门工具用好。语言是表达思想的载体,只要有了思想,无论是何种语言,都能表达。
|
||||
|
||||
可当我接触了越来越多的编程语言,对代码、指令、抽象有了更深入的理解之后,我推翻了这个想法,认识到了“语言只是工具”这个说法的狭隘性。
|
||||
|
||||
编程语言,显然不仅只是工具,它一定程度上桎梏了我们的思维。
|
||||
|
||||
举例来说,使用 Java 或者 C# 的人能够很轻易地想到对象的设计与封装,那是因为 Java 和 C# 就是以类作为基本的组织单位,无论你是否有意识地去做这件事,你都已经做了。而对于 C 和 JavaScript 的使用者来说,大家似乎更倾向于使用函数来进行封装。
|
||||
|
||||
抛开语言本身的优劣,这是一种思维的惯性,恰恰也印证了上面我谈到的,语言一定程度上桎梏了我们的思维。其实如果从人类语言的角度出发,一个人说中文和说英文的思维方式是大相径庭的,甚至一个人分别说方言和普通话给别人的感觉也像是两个人一样。
|
||||
|
||||
Rust
|
||||
|
||||
所以如果说思维是我们创造的出发点,那么编程语言,在表达思维的同时,也在一定程度上桎梏了我们的思维。聊到这里,终于到我们今天的主角——Rust这门编程语言出场了。
|
||||
|
||||
Rust 是什么?
|
||||
|
||||
Rust 是一门高度抽象、性能与安全并重的现代化高级编程语言。我学习、推崇它的主要原因有三点:
|
||||
|
||||
|
||||
高度抽象、表达能力强,支持命令式、声明式、元编程、范型等多种编程范式;
|
||||
强大的工程能力,安全与性能并重;
|
||||
良好的底层能力,天然适合内核、数据库、网络。
|
||||
|
||||
|
||||
Rust 很好地迎合了人类思维,对指令集进行了高度抽象,抽象后的表达力能让我们以更接近人类思维的视角去写代码,而 Rust 负责将我们的思维翻译为计算机语言,并且性能和安全得到了极大的保证。简单说就是,完美兼顾了一门语言的思想性和工具性。
|
||||
|
||||
仍以前面“选出一个数组中大于 100 的数字”为例,如果使用 Rust,那么代码是这样的:
|
||||
|
||||
let arr = vec![ 100, 105, 110, 99, 0 ]
|
||||
let result = arr.iter().filter(n => n > 100).collect();
|
||||
|
||||
|
||||
如此简洁的代码会不会带来性能损耗,Rust 的答案是不会,甚至可以比 C 做到更快。
|
||||
|
||||
我们对应看三个小例子的实现思路/要点,来感受一下 Rust 的语言表达能力、工程能力和底层能力。
|
||||
|
||||
简单协程
|
||||
|
||||
Rust 可以无缝衔接到 C、汇编代码,这样我们就可以跟下层的硬件打交道从而实现协程。
|
||||
|
||||
实现也很清晰。首先,定义出协程的上下文:
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[repr(C)]
|
||||
struct Context {
|
||||
rsp: u64, // rsp 寄存器
|
||||
r15: u64,
|
||||
r14: u64,
|
||||
r13: u64,
|
||||
r12: u64,
|
||||
rbx: u64,
|
||||
rbp: u64,
|
||||
}
|
||||
#[naked]
|
||||
unsafe fn ctx_switch() {
|
||||
// 注意:16 进制
|
||||
llvm_asm!(
|
||||
"
|
||||
mov %rsp, 0x00(%rdi)
|
||||
mov %r15, 0x08(%rdi)
|
||||
mov %r14, 0x10(%rdi)
|
||||
mov %r13, 0x18(%rdi)
|
||||
mov %r12, 0x20(%rdi)
|
||||
mov %rbx, 0x28(%rdi)
|
||||
mov %rbp, 0x30(%rdi)
|
||||
|
||||
mov 0x00(%rsi), %rsp
|
||||
mov 0x08(%rsi), %r15
|
||||
mov 0x10(%rsi), %r14
|
||||
mov 0x18(%rsi), %r13
|
||||
mov 0x20(%rsi), %r12
|
||||
mov 0x28(%rsi), %rbx
|
||||
mov 0x30(%rsi), %rbp
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
结构体 Context 保存了协程的运行上下文信息(寄存器数据),通过函数 ctx_switch,当前协程就可以交出 CPU 使用权,下一个协程接管 CPU 并进入执行流。
|
||||
|
||||
然后我们给出协程的定义:
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Routine {
|
||||
id: usize,
|
||||
stack: Vec<u8>,
|
||||
state: State,
|
||||
ctx: Context,
|
||||
}
|
||||
|
||||
|
||||
协程 Routine 有自己唯一的 id、栈 stack、状态 state,以及上下文 ctx。Routine 通过 spawn 函数创建一个就绪协程,yield 函数会交出 CPU 执行权:
|
||||
|
||||
pub fn spawn(&mut self, f: fn()) {
|
||||
// 找到一个可用的
|
||||
// let avaliable = ....
|
||||
let sz = avaliable.stack.len();
|
||||
unsafe {
|
||||
let stack_bottom = avaliable.stack.as_mut_ptr().offset(sz as isize); // 高地址内存是栈顶
|
||||
let stack_aligned = (stack_bottom as usize & !15) as *mut u8;
|
||||
std::ptr::write(stack_aligned.offset(-16) as *mut u64, guard as u64);
|
||||
std::ptr::write(stack_aligned.offset(-24) as *mut u64, hello as u64);
|
||||
std::ptr::write(stack_aligned.offset(-32) as *mut u64, f as u64);
|
||||
avaliable.ctx.rsp = stack_aligned.offset(-32) as u64; // 16 字节对齐
|
||||
}
|
||||
avaliable.state = State::Ready;
|
||||
}
|
||||
|
||||
pub fn r#yield(&mut self) -> bool {
|
||||
// 找到一个 ready 的,然后让其运行
|
||||
let mut pos = self.current;
|
||||
//.....
|
||||
self.routines[pos].state = State::Running;
|
||||
let old_pos = self.current;
|
||||
self.current = pos;
|
||||
unsafe {
|
||||
let old: *mut Context = &mut self.routines[old_pos].ctx;
|
||||
let new: *const Context = &self.routines[pos].ctx;
|
||||
llvm_asm!(
|
||||
"mov $0, %rdi
|
||||
mov $1, %rsi"::"r"(old), "r"(new)
|
||||
);
|
||||
ctx_switch();
|
||||
}
|
||||
self.routines.len() > 0
|
||||
}
|
||||
|
||||
|
||||
运行结果如下:
|
||||
|
||||
1 STARTING
|
||||
routine: 1 counter: 0
|
||||
2 STARTING
|
||||
routine: 2 counter: 0
|
||||
routine: 1 counter: 1
|
||||
routine: 2 counter: 1
|
||||
routine: 1 counter: 2
|
||||
routine: 2 counter: 2
|
||||
routine: 1 counter: 3
|
||||
routine: 2 counter: 3
|
||||
routine: 1 counter: 4
|
||||
routine: 2 counter: 4
|
||||
routine: 1 counter: 5
|
||||
routine: 2 counter: 5
|
||||
routine: 1 counter: 6
|
||||
routine: 2 counter: 6
|
||||
routine: 1 counter: 7
|
||||
routine: 2 counter: 7
|
||||
routine: 1 counter: 8
|
||||
routine: 2 counter: 8
|
||||
routine: 1 counter: 9
|
||||
routine: 2 counter: 9
|
||||
1 FINISHED
|
||||
|
||||
|
||||
具体代码实现参考协程 。
|
||||
|
||||
简单内核
|
||||
|
||||
操作系统内核是一个极为庞大的工程,但是如果只是写个简单内核输出 Hello World,那么 Rust 就能很快完成这个任务。你可以自己体验一下。
|
||||
|
||||
首先,添加依赖工具:
|
||||
|
||||
rustup component add llvm-tools-preview
|
||||
cargo install bootimage
|
||||
|
||||
|
||||
然后编辑 main.rs 文件输出一个 Hello World:
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
use core::panic::PanicInfo;
|
||||
static HELLO:&[u8] = b"Hello World!";
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
let vga_buffer = 0xb8000 as *mut u8;
|
||||
for (i, &byte) in HELLO.iter().enumerate() {
|
||||
unsafe {
|
||||
*vga_buffer.offset(i as isize * 2) = byte;
|
||||
*vga_buffer.offset(i as isize * 2 + 1) = 0xb;
|
||||
}
|
||||
}
|
||||
loop{}
|
||||
}
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
|
||||
|
||||
然后编译、打包运行:
|
||||
|
||||
cargo bootimage
|
||||
cargo run
|
||||
|
||||
|
||||
运行结果如下:-
|
||||
|
||||
|
||||
具体代码实现参考内核 。
|
||||
|
||||
简单网络协议栈
|
||||
|
||||
同操作系统一样,网络协议栈也是一个庞大的工程系统。但是借助 Rust 和其完备的生态,我们可以迅速完成一个小巧的 HTTP 协议栈。
|
||||
|
||||
首先,在数据链路层,我们定义 Mac 地址结构体:
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MacAddress([u8; 6]);
|
||||
|
||||
impl MacAddress {
|
||||
pub fn new() -> MacAddress {
|
||||
let mut octets: [u8; 6] = [0; 6];
|
||||
rand::thread_rng().fill_bytes(&mut octets); // 1. 随机生成
|
||||
octets[0] |= 0b_0000_0010; // 2
|
||||
octets[1] &= 0b_1111_1110; // 3
|
||||
MacAddress { 0: octets }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MacAddress 用来表示网卡的物理地址,此处的 new 函数通过随机数来生成随机的物理地址。
|
||||
|
||||
然后实现 DNS 域名解析函数,通过 IP 地址获取 MAC 地址,如下:
|
||||
|
||||
pub fn resolve(
|
||||
dns_server_address: &str,
|
||||
domain_name: &str,
|
||||
) -> Result<Option<std::net::IpAddr>, Box<dyn Error>> {
|
||||
let domain_name = Name::from_ascii(domain_name).map_err(DnsError::ParseDomainName)?;
|
||||
|
||||
let dns_server_address = format!("{}:53", dns_server_address);
|
||||
let dns_server: SocketAddr = dns_server_address
|
||||
.parse()
|
||||
.map_err(DnsError::ParseDnsServerAddress)?;
|
||||
// ....
|
||||
let mut encoder = BinEncoder::new(&mut request_buffer);
|
||||
request.emit(&mut encoder).map_err(DnsError::Encoding)?;
|
||||
let _n_bytes_sent = localhost
|
||||
.send_to(&request_buffer, dns_server)
|
||||
.map_err(DnsError::Sending)?;
|
||||
loop {
|
||||
let (_b_bytes_recv, remote_port) = localhost
|
||||
.recv_from(&mut response_buffer)
|
||||
.map_err(DnsError::Receiving)?;
|
||||
if remote_port == dns_server {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let response = Message::from_vec(&response_buffer).map_err(DnsError::Decoding)?;
|
||||
for answer in response.answers() {
|
||||
if answer.record_type() == RecordType::A {
|
||||
let resource = answer.rdata();
|
||||
let server_ip = resource.to_ip_addr().expect("invalid IP address received");
|
||||
|
||||
return Ok(Some(server_ip));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
|
||||
接着实现 HTTP 协议的 GET 方法:
|
||||
|
||||
pub fn get(
|
||||
tap: TapInterface,
|
||||
mac: EthernetAddress,
|
||||
addr: IpAddr,
|
||||
url: Url,
|
||||
) -> Result<(), UpstreamError> {
|
||||
let domain_name = url.host_str().ok_or(UpstreamError::InvalidUrl)?;
|
||||
let neighbor_cache = NeighborCache::new(BTreeMap::new());
|
||||
// TCP 缓冲区
|
||||
let tcp_rx_buffer = TcpSocketBuffer::new(vec![0; 1024]);
|
||||
let tcp_tx_buffer = TcpSocketBuffer::new(vec![0; 1024]);
|
||||
let tcp_socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer);
|
||||
let ip_addrs = [IpCidr::new(IpAddress::v4(192, 168, 42, 1), 24)];
|
||||
let fd = tap.as_raw_fd();
|
||||
let mut routes = Routes::new(BTreeMap::new());
|
||||
let default_gateway = Ipv4Address::new(192, 168, 42, 100);
|
||||
routes.add_default_ipv4_route(default_gateway).unwrap();
|
||||
let mut iface = EthernetInterfaceBuilder::new(tap)
|
||||
.ethernet_addr(mac)
|
||||
.neighbor_cache(neighbor_cache)
|
||||
.ip_addrs(ip_addrs)
|
||||
.routes(routes)
|
||||
.finalize();
|
||||
let mut sockets = SocketSet::new(vec![]);
|
||||
let tcp_handle = sockets.add(tcp_socket);
|
||||
// HTTP 请求
|
||||
let http_header = format!(
|
||||
"GET {} HTTP/1.0\r\nHost: {}\r\nConnection: close\r\n\r\n",
|
||||
url.path(),
|
||||
domain_name,
|
||||
);
|
||||
let mut state = HttpState::Connect;
|
||||
'http: loop {
|
||||
let timestamp = Instant::now();
|
||||
match iface.poll(&mut sockets, timestamp) {
|
||||
Ok(_) => {}
|
||||
Err(smoltcp::Error::Unrecognized) => {}
|
||||
Err(e) => {
|
||||
eprintln!("error: {:?}", e);
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut socket = sockets.get::<TcpSocket>(tcp_handle);
|
||||
state = match state {
|
||||
HttpState::Connect if !socket.is_active() => {
|
||||
eprintln!("connecting");
|
||||
socket.connect((addr, 80), random_port())?;
|
||||
HttpState::Request
|
||||
}
|
||||
HttpState::Request if socket.may_send() => {
|
||||
eprintln!("sending request");
|
||||
socket.send_slice(http_header.as_ref())?;
|
||||
HttpState::Response
|
||||
}
|
||||
HttpState::Response if socket.can_recv() => {
|
||||
socket.recv(|raw_data| {
|
||||
let output = String::from_utf8_lossy(raw_data);
|
||||
println!("{}", output);
|
||||
(raw_data.len(), ())
|
||||
})?;
|
||||
HttpState::Response
|
||||
}
|
||||
HttpState::Response if !socket.may_recv() => {
|
||||
eprintln!("received complete response");
|
||||
break 'http;
|
||||
}
|
||||
_ => state,
|
||||
}
|
||||
}
|
||||
phy_wait(fd, iface.poll_delay(&sockets, timestamp)).expect("wait error");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
最后在 main 函数中使用 HTTP GET 方法:
|
||||
|
||||
fn main() {
|
||||
// ...
|
||||
let tap = TapInterface::new(&tap_text).expect(
|
||||
"error: unable to use <tap-device> as a \
|
||||
network interface",
|
||||
);
|
||||
let domain_name = url.host_str().expect("domain name required");
|
||||
let _dns_server: std::net::Ipv4Addr = dns_server_text.parse().expect(
|
||||
"error: unable to parse <dns-server> as an \
|
||||
IPv4 address",
|
||||
);
|
||||
let addr = dns::resolve(dns_server_text, domain_name).unwrap().unwrap();
|
||||
let mac = ethernet::MacAddress::new().into();
|
||||
http::get(tap, mac, addr, url).unwrap();
|
||||
}
|
||||
|
||||
|
||||
运行程序,结果如下:
|
||||
|
||||
$ ./target/debug/rget http://www.baidu.com tap-rust
|
||||
|
||||
HTTP/1.0 200 OK
|
||||
Accept-Ranges: bytes
|
||||
Cache-Control: no-cache
|
||||
Content-Length: 9508
|
||||
Content-Type: text/html
|
||||
|
||||
|
||||
具体代码实现参考协议栈 。
|
||||
|
||||
通过这三个简单的小例子,无论是协程、内核还是协议栈,这些听上去都很高大上的技术,在 Rust 强大的表现力、生态和底层能力面前显得如此简单和方便。
|
||||
|
||||
思维是出发点,语言是表达体,工具是媒介,而 Rust 完美兼顾了一门语言的思想性和工具性,赋予了我们极强的工程表达能力和完成能力。
|
||||
|
||||
总结
|
||||
|
||||
作为极其现代的语言,Rust 集百家之长而成,将性能、安全、语言表达力都做到了极致,但同时也带来了巨大的学习曲线。
|
||||
|
||||
初学时,每天都要和编译器做斗争,每次编译都是满屏的错误信息;攻克一个陡坡后,发现后面有更大的陡坡,学习的道路似乎无穷无尽。那我们为什么要学习 Rust ?
|
||||
|
||||
这里引用左耳朵耗子的一句话:
|
||||
|
||||
|
||||
如果你对 Rust 的概念认识得不完整,你完全写不出程序,那怕就是很简单的一段代码。这逼着程序员必须了解所有的概念才能编码。
|
||||
|
||||
|
||||
Rust 是一个对开发者极其严格的语言,严格到你学的不扎实,就不能写程序,但这无疑也是一个巨大的机会,改掉你不好的编码习惯,锻炼你的思维,让你成为真正的大师。
|
||||
|
||||
聊到这里,你是否已经对 Rust 有了更深的认识和更多的激情,那么放手去做吧!期待你与 Rust 擦出更加明亮的火花!
|
||||
|
||||
参考资料
|
||||
|
||||
|
||||
Writing an OS in Rust
|
||||
green-threads-explained-in-200-lines-of-rust
|
||||
https://github.com/PedroGao/rust-examples
|
||||
《深入理解计算机系统》
|
||||
《Rust in Action》
|
||||
《硅谷来信》
|
||||
《浪潮之巅》
|
||||
|
||||
|
||||
|
||||
|
||||
|
163
专栏/陈天·Rust编程第一课/结束语永续之原:Rust学习,如何持续精进?.md
Normal file
163
专栏/陈天·Rust编程第一课/结束语永续之原:Rust学习,如何持续精进?.md
Normal file
@ -0,0 +1,163 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
结束语 永续之原:Rust学习,如何持续精进?
|
||||
你好,我是陈天。
|
||||
|
||||
首先,恭喜你完成了这门课程!
|
||||
|
||||
六月底,我确定了自己会在极客时间上出这个 Rust 的专栏。
|
||||
|
||||
其实以前我对这样子的付费课程不是太感冒,因为自己随性惯了,写公众号自由洒脱,想写就写,想停就停,一个主题写腻了还可以毫无理由地切换到另一个主题上。但一旦写付费的专栏签下合同,就意味着品味、质量、内容以及更新的速度都不能随心所欲,得按照人家的要求来。
|
||||
|
||||
最要命的是更新的速度——我没有专职做过文字工作者,想来和代码工作者性质类似,一些开创性工作的开始特别需要灵感,非常依赖妙手偶得的那个契机。这种不稳定的输出模式,遇到进度的压力,就很折磨人。所以之前很多机会我都婉拒了。
|
||||
|
||||
但这次思来想去,我还是接下了 Rust 第一课这个挑战。
|
||||
|
||||
大部分原因是我越来越喜爱 Rust 这门语言,想让更多的人也能爱上它,于是之前在公众号和 B 站上,也做了不少输出。但这样的输出,左一块右一块的,没有一个完整的体系,所以有这样一个机会,来构建出我个人总结的Rust学习体系,也许对大家的学习会有很大的帮助。
|
||||
|
||||
另外一部分原因也是出于我的私心。自从 2016 年《途客圈创业记》出版后,我就没有正式出版过东西,很多口头答应甚至签下合同的选题,也都因为各种原因被我终止或者搁置了。我特别想知道,自己究竟是否还能拿起笔写下严肃的可以流传更广、持续更久的文字。
|
||||
|
||||
可是——介绍一门语言的文字可以有持久的生命力么?
|
||||
|
||||
你一定有这个疑问。
|
||||
|
||||
撰写介绍一门编程语言的文字,却想让它拥有持久的生命力,这听上去似乎是痴人说梦。现代编程语言的进化速度相比二十年前,可谓是一日千里。就拿 Rust 来说,稳定的六周一个版本,三年一个版次,别说是拥有若干年的生命力了,就算是专栏连载的几个月,都会过去两三个版本,也就意味着有不少新鲜的东西被加入到语言中。
|
||||
|
||||
不过好在 Rust 极其注重向后兼容,也就意味着我现在介绍的代码,只要是 Rust 语言或者标准库中稳定的内容,若干年后(应该)还是可以有效的。Rust 这种不停迭代却一直保持向后兼容的做法,让它相对于其它语言在教学上有一些优势,所以,撰写介绍 Rust 的文字,生命力会更加持久一些。
|
||||
|
||||
当然这还远远不够。让介绍一门编程语言的文字更持久的方式就是,从本原出发,帮助大家理解语言表层概念背后的思想或者机理,这也是这个专栏最核心的设计思路。
|
||||
|
||||
通用型计算机诞生后差不多七十年了,当时的冯诺依曼结构依然有效;从 C 语言诞生到现在也有快五十年了,编程语言处理内存的方式还是堆和栈,常用的算法和数据结构也还是那些。虽然编程语言在不断进化,但解决问题的主要手段还都是差不多的。
|
||||
|
||||
比如说,引用计数,你如果在之前学习的任何一门语言中弄明白了它的思路,那么理解 Rust 下的 Rc/Arc 也不在话下。所以,只要我们把基础知识夯实,很多看似难懂的问题,只不过是在同样本质上套了让人迷惑的外衣而已。
|
||||
|
||||
那么如何拨开迷雾抵达事物的本原呢?我的方法有两个:一曰问,二曰切。对,就是中医“望闻问切”后两个字。
|
||||
|
||||
问就是刨根追底,根据已有的认知,发出直击要害的疑问,这样才能为后续的探索(切)叩开大门。比如你知道引用计数通行的实现方法,也知道 Rust 的单一所有权机制把堆内存的生命周期和栈内存绑定在一起,栈在值在,栈亡值亡。
|
||||
|
||||
那么你稍微思考一下就会产生疑问:Rc/Arc 又是怎么打破单一所有权机制,做到让堆上的内存跳脱了栈上内存的限制呢?问了这个问题,你就有机会往下“切”。
|
||||
|
||||
“切”是什么呢,就是深入查看源代码,顺着脉络找出问题的答案。初学者往往不看标准库的源码,实际上,看源代码是最能帮助你成长的。无论是学习一门语言,还是学习 Linux 内核或者别的什么,源码都是第一手资料。别人的分析讲得再好,也是嚼过的饭,受限于他的理解能力和表达能力,这口嚼过的饭还真不一定比你自己亲自上嘴更好下咽。
|
||||
|
||||
比如想知道上面Rc/Arc的问题,自然要看 Rc::new 的源码实现:
|
||||
|
||||
pub fn new(value: T) -> Rc<T> {
|
||||
// There is an implicit weak pointer owned by all the strong
|
||||
// pointers, which ensures that the weak destructor never frees
|
||||
// the allocation while the strong destructor is running, even
|
||||
// if the weak pointer is stored inside the strong one.
|
||||
Self::from_inner(
|
||||
Box::leak(box RcBox { strong: Cell::new(1), weak: Cell::new(1), value }).into(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
不看不知道,一看吓一跳。可疑的 Box::leak 出现在我们眼前。这个 Box::leak 又是干什么的呢?顺着这个线索追溯下去,我们发现了一个宝贵的金矿(你可以回顾生命周期的那一讲)。
|
||||
|
||||
思
|
||||
|
||||
在追溯本原的基础上,我们还要学会分析问题和解决问题的正确方法。我觉得编程语言的学习不应该只局限于学习语法本身,更应该在这个过程中,不断提升自己学习知识和处理问题的能力。
|
||||
|
||||
如果你还记得 HashMap 那一讲,我们先是宏观介绍解决哈希冲突的主要思路,它是构建哈希表的核心算法;然后使用 transmute 来了解 Rust HashMap 的组织结构,通过 gdb 查看内存布局,再结合代码去找到 HashMap 构建和扩容的具体思路。
|
||||
|
||||
这样一层层剥茧抽丝,边学习,边探索,边总结,最终我们得到了对 Rust 哈希表非常扎实的掌握。这种掌握程度,哪怕你十年都不碰 Rust,十年后有人问你 Rust 的哈希表怎么工作的,你也能回答个八九不离十。
|
||||
|
||||
我希望你能够掌握这种学习的方式,这是终生受益的方式。2006 年,我在 Juniper 工作时,用类似的方式,把 ScreenOS 系统的数据平面的处理流程总结出来了,到现在很多细节我记忆犹新。
|
||||
|
||||
很多时候面试一些同学,详细询问他们三五年前设计和实现过的一些项目时,他们会答不上来,经常给出“这个项目太久了,我记不太清楚”这样的答复,让我觉得好奇怪。对我而言,只要是做过的项目、阅读过的代码,不管多久,都能回忆起很多细节,就好像它们是自己的一部分一样。
|
||||
|
||||
尽管快有 20 年没有碰,我还记得第一份工作中 OSPFv2 和 IGMPv3 协议的部分细节,知道 netlink 如何工作,也对 Linux VMM 管理的流程有一个基本印象。现在想来,可能就是我掌握了正确的学习方法而已。
|
||||
|
||||
所以,在这门介绍语言的课程中,我还夹带了很多方法论相关的私货,它们大多散落在文章的各个角落,除了刚刚谈到的分析问题/解决问题的方法外,还有阅读代码的方法、架构设计的方法、撰写和迭代接口的方法、撰写测试的方法、代码重构的方法等等。希望这些私货能够让你产生共鸣,结合你自己在职业生涯中总结出来的方法,更好地服务于你的学习和工作。
|
||||
|
||||
读
|
||||
|
||||
在撰写这个专栏的过程中,我参考了不少书籍。比如《Programming Rust》、《Designing Data-intensive Applications》以及《Fundamentals of Software Architecture》。可惜 Jon Gjengset 的《Rust for Rustaceans》姗姗来迟,否则这个专栏的水准可以更上一个台阶。
|
||||
|
||||
我们做软件开发的,似乎到了一定年纪就不怎么阅读,这样不好。毕加索说:“good artists copy; great artists steal.”当你从一个人身上学习时,你在模仿;当你从一大群人身上学习时,你自己就慢慢融会贯通,成为大师。
|
||||
|
||||
所以,不要指望学了这门Rust 第一课,就大功告成,这门课仅仅是一个把你接引至 Rust 世界的敲门砖,接下来你还要进一步从各个方面学习和夯实更多的知识。
|
||||
|
||||
就像我回答一个读者的问题所说的:很多时候,我们缺乏的不是对 Rust 知识的理解,更多是对软件开发更广阔知识的理解。所以,不要拘泥于 Rust 本身,对你自己感兴趣的,以及你未来会涉猎的场景广泛阅读、深度思考。
|
||||
|
||||
行
|
||||
|
||||
伴随着学习,阅读,思考,我们还要广泛地实践。不要一有问题就求助,想想看,自己能不能构造足够简单的代码来帮助解决问题。
|
||||
|
||||
比如有人问:HTTP/2 是怎么工作的?这样的问题,你除了可以看 RFC,阅读别人总结的经验,还可以动动手,几行代码就可以获得很多信息。比如:
|
||||
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.init();
|
||||
|
||||
let url = "<https://www.rust-lang.org/>";
|
||||
|
||||
let _body = reqwest::blocking::get(url).unwrap().text().unwrap();
|
||||
info!("Fetching url: {}", url);
|
||||
}
|
||||
|
||||
|
||||
这段代码相信你肯定能写得出来,但你是否尝试过 RUST_LOG=debug 甚至 RUST_LOG=trace 来看看输出的日志呢?又有没有尝试着顺着日志的脉络,去分析涉及的库呢?
|
||||
|
||||
下面是这几行代码 RUST_LOG=debug 的输出,可以让你看到 HTTP/2 基本的运作方式,我建议你试试 RUST_LOG=trace(内容太多就不贴了),如果你能搞清楚输出的信息,那么 Rust 下用 hyper 处理 HTTP/2 的主流程你就比较明白了。
|
||||
|
||||
❯ RUST_LOG=debug cargo run --quiet
|
||||
2021-12-12T21:28:00.612897Z DEBUG reqwest::connect: starting new connection: <https://www.rust-lang.org/>
|
||||
2021-12-12T21:28:00.613124Z DEBUG hyper::client::connect::dns: resolving host="www.rust-lang.org"
|
||||
2021-12-12T21:28:00.629392Z DEBUG hyper::client::connect::http: connecting to 13.224.7.43:443
|
||||
2021-12-12T21:28:00.641156Z DEBUG hyper::client::connect::http: connected to 13.224.7.43:443
|
||||
2021-12-12T21:28:00.641346Z DEBUG rustls::client::hs: No cached session for DnsName(DnsName(DnsName("www.rust-lang.org")))
|
||||
2021-12-12T21:28:00.641683Z DEBUG rustls::client::hs: Not resuming any session
|
||||
2021-12-12T21:28:00.656251Z DEBUG rustls::client::hs: Using ciphersuite Tls13(Tls13CipherSuite { suite: TLS13_AES_128_GCM_SHA256, bulk: Aes128Gcm })
|
||||
2021-12-12T21:28:00.656754Z DEBUG rustls::client::tls13: Not resuming
|
||||
2021-12-12T21:28:00.657046Z DEBUG rustls::client::tls13: TLS1.3 encrypted extensions: [ServerNameAck, Protocols([PayloadU8([104, 50])])]
|
||||
2021-12-12T21:28:00.657151Z DEBUG rustls::client::hs: ALPN protocol is Some(b"h2")
|
||||
2021-12-12T21:28:00.658435Z DEBUG h2::client: binding client connection
|
||||
2021-12-12T21:28:00.658526Z DEBUG h2::client: client connection bound
|
||||
2021-12-12T21:28:00.658602Z DEBUG h2::codec::framed_write: send frame=Settings { flags: (0x0), enable_push: 0, initial_window_size: 2097152, max_frame_size: 16384 }
|
||||
2021-12-12T21:28:00.659062Z DEBUG Connection{peer=Client}: h2::codec::framed_write: send frame=WindowUpdate { stream_id: StreamId(0), size_increment: 5177345 }
|
||||
2021-12-12T21:28:00.659327Z DEBUG hyper::client::pool: pooling idle connection for ("https", www.rust-lang.org)
|
||||
2021-12-12T21:28:00.659674Z DEBUG Connection{peer=Client}: h2::codec::framed_write: send frame=Headers { stream_id: StreamId(1), flags: (0x5: END_HEADERS | END_STREAM) }
|
||||
2021-12-12T21:28:00.672087Z DEBUG Connection{peer=Client}: h2::codec::framed_read: received frame=Settings { flags: (0x0), max_concurrent_streams: 128, initial_window_size: 65536, max_frame_size: 16777215 }
|
||||
2021-12-12T21:28:00.672173Z DEBUG Connection{peer=Client}: h2::codec::framed_write: send frame=Settings { flags: (0x1: ACK) }
|
||||
2021-12-12T21:28:00.672244Z DEBUG Connection{peer=Client}: h2::codec::framed_read: received frame=WindowUpdate { stream_id: StreamId(0), size_increment: 2147418112 }
|
||||
2021-12-12T21:28:00.672308Z DEBUG Connection{peer=Client}: h2::codec::framed_read: received frame=Settings { flags: (0x1: ACK) }
|
||||
2021-12-12T21:28:00.672351Z DEBUG Connection{peer=Client}: h2::proto::settings: received settings ACK; applying Settings { flags: (0x0), enable_push: 0, initial_window_size: 2097152, max_frame_size: 16384 }
|
||||
2021-12-12T21:28:00.956751Z DEBUG Connection{peer=Client}: h2::codec::framed_read: received frame=Headers { stream_id: StreamId(1), flags: (0x4: END_HEADERS) }
|
||||
2021-12-12T21:28:00.956921Z DEBUG Connection{peer=Client}: h2::codec::framed_read: received frame=Data { stream_id: StreamId(1) }
|
||||
2021-12-12T21:28:00.957015Z DEBUG Connection{peer=Client}: h2::codec::framed_read: received frame=Data { stream_id: StreamId(1) }
|
||||
2021-12-12T21:28:00.957079Z DEBUG Connection{peer=Client}: h2::codec::framed_read: received frame=Data { stream_id: StreamId(1) }
|
||||
2021-12-12T21:28:00.957316Z DEBUG reqwest::async_impl::client: response '200 OK' for <https://www.rust-lang.org/>
|
||||
2021-12-12T21:28:01.018665Z DEBUG Connection{peer=Client}: h2::codec::framed_read: received frame=Data { stream_id: StreamId(1) }
|
||||
2021-12-12T21:28:01.018885Z DEBUG Connection{peer=Client}: h2::codec::framed_read: received frame=Data { stream_id: StreamId(1), flags: (0x1: END_STREAM) }
|
||||
2021-12-12T21:28:01.020158Z INFO http2: Fetching url: <https://www.rust-lang.org/>
|
||||
|
||||
|
||||
所以,很多时候,知识就在我们身边,我们写一写代码就能获取。
|
||||
|
||||
在这个过程中,你自己思考之后撰写的探索性的代码、你分析输出过程中付出的思考和深度的阅读,以及最后在梳理过程中进行的总结,都会让知识牢牢变成你自己的。
|
||||
|
||||
最后我们聊一聊写代码这个事。
|
||||
|
||||
学习任何语言,最重要的步骤都是用学到的知识,解决实际的问题。Rust 能不能胜任你需要完成的各种任务?大概率能。但你能不能用 Rust 来完成这些任务?不一定。每个十指俱全的人都能学习弹钢琴,但不是每个学弹钢琴的人都能达到十级的水平。这其中现实和理想间巨大的鸿沟就是“刻意练习”。
|
||||
|
||||
想要成为 Rust 专家,想让 Rust 成为你职业生涯中的一项重要技能,刻意练习必不可少,需要不断地撰写代码。的确,Rust 的所有权和生命周期学习和使用起来让人难于理解,所有权、生命周期,跟类型系统(包括泛型、trait),以及异步开发结合起来,更是障碍重重,但通过不断学习和不断练习,你一定会发现,它们不过是你的一段伟大旅程中越过的一个小山丘而已。
|
||||
|
||||
最后的最后,估计很多同学都是在艰难斗争、默默学习,在专栏要结束的今天,欢迎你在留言区留言,我非常希望能听到你的声音,听听你学习这个专栏的感受和收获,见到你的身影。点这里还可以提出你对课程的反馈与建议。
|
||||
|
||||
感谢你选择我的 Rust 第一课。感谢你陪我们一路走到这里。接下来,就看你的了。
|
||||
|
||||
|
||||
“Go where you must go, and hope!”— Gandalf
|
||||
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user