代码之旅

I love Coding !

由于评论从畅言云评迁移到waline,导致评论数据丢失。非本人删除,各位大佬见谅!!!

先来看一段 Java 代码,Application中有version和 logger。logger 依赖了 Version。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Application
{
public Version version;
public Logger logger;
public Application() {
version = new Version(1);
logger = new Logger(version);
}

public static void main(String[] args)
{
Application application = new Application();
application.logger.log("Hello World!");
// console output:
// [version 1] Hello World!
}
class Version {
public int ver;
public Version (int ver){
this.ver = ver;
}
public String toString() {
return String.format("version %d",ver);
}
}
class Logger {
public Version version;
public Logger (Version version){
this.version = version;
}
void log(String msg) {
System.out.println(String.format("[%s] %s",version,msg));
}
}
}

那么问题来了,如何在 Rust 中实现相同的代码?

阅读全文 »

生命周期,简而言之就是引用的有效作用域。在大多数时候,我们无需手动的声明生命周期,因为编译器可以自动进行推导。当多个生命周期存在,且编译器无法推导出某个引用的生命周期时,就需要我们手动标明生命周期。

悬垂指针和生命周期

生命周期的主要作用是避免悬垂引用,它会导致程序引用了本不该引用的数据:

1
2
3
4
5
6
7
8
{
let r;
{
let x = 5;
r = &x;
} // ^^ borrowed value does not live long enough
println!("r: {}", r);
}

此处 r 就是一个悬垂指针,它引用了提前被释放的变量 x。

借用检查

为了保证 Rust 的所有权和借用的正确性,Rust 使用了一个借用检查器(Borrow checker),来检查我们程序的借用正确性:

1
2
3
4
5
6
7
8
9
10
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+

r 变量被赋予了生命周期 'a,x 被赋予了生命周期 'b,从图示上可以明显看出生命周期 'b 比 'a 小很多。在编译期,Rust 会比较两个变量的生命周期,结果发现 r 明明拥有生命周期 'a,但是却引用了一个小得多的生命周期 'b,在这种情况下,编译器会认为我们的程序存在风险,因此拒绝运行。如果想要编译通过,也很简单,只要 'b 比 'a 大就好。总之,x 变量只要比 r 活得久,那么 r 就能随意引用 x 且不会存在危险:

1
2
3
4
5
6
7
8
{
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+
阅读全文 »

TCP协议是一种面向连接的有状态网络协议。对于发送的每个数据包,一旦TCP堆栈收到特定数据包的ACK,它就认为它已成功传递。

TCP使用指数退避超时重传一个未确认的数据包,最多tcp_retries2时间(默认为15),每次重传超时在TCP_RTO_MIN(200毫秒)和TCP_RTO_MAX(120秒)之间。一旦第15次重试到期(默认情况下),TCP堆栈将通知上面的层(即应用程序)断开连接。

tcp_retries2可以通过修改文件/proc/sys/net/ipv4/tcp_retries2sysctl net.ipv4.tcp_retries2进行调优。

TCP_RTO_MIN和TCP_RTO_MAX的值在Linux内核中硬编码,并由以下常量定义:

1
2
#define TCP_RTO_MAX ((unsigned)(120*HZ))
#define TCP_RTO_MIN ((unsigned)(HZ/5))

Linux2.6+使用1000毫秒的HZ,所以TCP_RTO_MIN是200毫秒,TCP_RTO_MAX是120秒。给定tcp_retries设置为15的默认值,这意味着在将断开的网络链路通知给上层(即应用程序)之前需要924.6秒,因为在最后一次(第15次)重试到期时检测到连接断开。

RetransmissionRTO(ms)Time before a timeout(sec)Time before a timeout(min)
12000.2 secs0.0 mins
24000.6 secs0.0 mins
38001.4 secs0.0 mins
416003.0secs0.1 mins
532006.2 secs0.1 mins
6640012.6 secs0.2 mins
71280025.4secs0.4 mins
82560051.0 secs0.9 mins
951200102.2 secs1.7 mins
10102400204.6 secs3.4 mins
11120000324.6 secs5.4 mins
12120000444.6 secs7.4 mins
13120000564.6 secs9.4mins
14120000684.6 secs11.4 mins
15120000804.6 secs13.4 mins
16120000924.6 secs15.4 mins

真正起到限制重传次数的并不是真正的重传次数。而是以tcp_retries2为boundary,以rto_base(如TCP_RTO_MIN 200ms)为初始RTO,计算得到一个timeout值出来。如果重传间隔超过这个timeout,则认为超过了阈值。实际TCP 的 RTO 是动态计算[1]的,也就是说:

  • 如果RTT比较小,那么RTO初始值就约等于下限200ms。由于timeout总时长是924600ms,表现出来的现象刚好就是重传了15次,超过了timeout值,从而放弃TCP流
  • 如果RTT较大,比如RTO初始值计算得到的是1000ms。那么根本不需要重传15次,重传总间隔就会超过924600ms。例如一个RTT=400ms的情况,当tcp_retries2=10时,仅重传了3次就放弃了TCP流

由于tcp_retries2是个系统级参数,在实际使用中,可以针对应用修改TCP_USER_TIMEOUT[2]

建议公式为(开启了 KeepAlive 的情况):

1
TCP_USER_TIMEOUT >= TCP_KEEPIDLE + TCP_KEEPINTVL * TCP_KEEPCNT

TCP_USER_TIMEOUT 是RFC 54288 规定的 TCP option,用来扩展 TCP RFC 7939 协议中本身的 “User Timeout” 参数(原协议不允许配置参数大小)。其用来控制已经发送,但是尚未被 ACK的数据包的存活时间,超过这个时间则会强制关闭连接。


  1. RTO的计算方法(基于RFC6298和Linux 3.10) ↩︎

  2. When TCP sockets refuse to die ↩︎

如果不同的类型具有相同的行为,那么我们就可以定义一个特征,然后为这些类型实现该特征。定义特征是把一些方法组合在一起,目的是定义一个实现某些目标所必需的行为的集合。

例如,我们现在有文章 Post 和微博 Weibo 两种内容载体,而我们想对相应的内容进行总结,也就是无论是文章内容,还是微博内容,都可以在某个时间点进行总结,那么总结这个行为就是共享的,因此可以用特征来定义:

1
2
3
pub trait Summary {
fn summarize(&self) -> String;
}

特征只定义行为看起来是什么样的,而不定义行为具体是怎么样的。因此我们需要为实现特征的类型,定义行为具体是怎么样的。

1
2
3
4
5
6
7
8
9
10
pub struct Post {
pub title: String,
pub author: String,
pub content: String,
}
impl Summary for Post {
fn summarize(&self) -> String {
format!("文章{}, 作者是{}", self.title, self.author)
}
}
阅读全文 »

指针是一个包含了内存地址的变量,该内存地址引用或者指向了另外的数据。

在 Rust 中,最常见的指针类型是引用,引用通过 & 符号表示。不同于其它语言,引用在 Rust 中被赋予了更深层次的含义,那就是:借用其它变量的值。引用本身很简单,除了指向某个值外并没有其它的功能,也不会造成性能上的额外损耗,因此是 Rust 中使用最多的指针类型。

而智能指针则不然,它虽然也号称指针,但是它是一个复杂的家伙:通过比引用更复杂的数据结构,包含比引用更多的信息,例如元数据,当前长度,最大可用长度等。智能指针往往是基于结构体实现,它与我们自定义的结构体最大的区别在于它实现了 Deref 和 Drop 特征:

  • Deref 可以让智能指针像引用那样工作,这样你就可以写出同时支持智能指针和引用的代码,例如 *T
  • Drop 允许你指定智能指针超出作用域后自动执行的代码,例如做一些数据清除等收尾工作

而 Box 指针是最简单的智能指针。本文将介绍 Box 指针以及 Deref 和 Drop 特征。

阅读全文 »

本文总结了如何使用 Java 客户端操作 HDFS,以及一些注意点。

阅读全文 »

Rust 为了解决内存安全问题,引入了所有权系统。

所有的程序都必须和计算机内存打交道,如何从内存中申请空间来存放程序的运行内容,如何在不需要的时候释放这些空间,成了重中之重,也是所有编程语言设计的难点之一。在计算机语言不断演变过程中,出现了三种流派:

  • 垃圾回收机制(GC),在程序运行时不断寻找不再使用的内存,典型代表:Java、Go
  • 手动管理内存的分配和释放, 在程序中,通过函数调用的方式来申请和释放内存,典型代表:C++
  • 通过所有权来管理内存,编译器在编译时会根据一系列规则进行检查。

Rust 选择了第三种,最妙的是,这种检查只发生在编译期,因此对于程序运行期,不会有任何性能上的损失。

阅读全文 »