开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

【译】Python的enumerate()函数揭秘

发表于 2017-12-14

今天的原文的作者是来自国外的以为Python“布道师”Dan Bader,他的博客完全就是一个个人品牌的学校。有跟多Python技巧,有很多他录制的Youtube视频,国内的Pythonista们,不妨订阅一下他的每周邮件推送。订阅链接

今天的译文是他博客中的一篇,点击查看原文

如何以去写以及为什么你应该使用Python中的内置枚举函数来编写更干净更加Pythonic的循环语句?

python-enumerate.jpg

Python的enumerate函数是一个神话般的存在,以至于它很难用一句话去总结它的目的和用处。

但是,它是一个非常有用的函数,许多初学者,甚至中级Pythonistas是并没有真正意识到。简单来说,enumerate()是用来遍历一个可迭代容器中的元素,同时通过一个计数器变量记录当前元素所对应的索引值。

让我们来看一个示例:

1
2
3
复制代码names = ['Bob', 'Alice', 'Guido']
for index, value in enumerate(names):
print(f'{index}: {value}')

这段代码会输入如下内容:

1
2
3
复制代码0: Bob
1: Alice
2: Guido

正如你所看到的,这个循环遍历了names列表的所有元素,并通过增加从零开始的计数器变量来为每个元素生成索引。

[如果您想知道上面例子中使用的f’…’字符串语法,这是Python 3.6及更高版本中提供的一种新的字符串格式化技巧。]

用enumerate()让你的循环更加Pythonic

那么为什么用enumerate()函数去保存运行中的索引很有用呢?

我发现,有很多从C或Java背景转过来的新的Python开发人员有时使用下面这种range(len(...))方法来保存运行中每个元素的索引,同时再用for循环遍历列表:

1
2
3
复制代码# 警告: 不建议这么写
for i in range(len(my_items)):
print(i, my_items[i])

通过巧妙地使用enumerate()函数,就像我在上面的“names”例子中写的那样,你可以使你的循环结构看起来更Pythonic和地道。

你不再需要在Python代码中专门去生成元素索引,而是将所有这些工作都交给enumerate()函数处理即可。这样,你的代码将更容易被阅读,而且减少写错代码的影响。(译者注:写的代码越多,出错几率越高,尽量将自己的代码看起来简洁,易读,Pythonic,才是我们的追求)

修改起始索引

另一个有用的特性是,enumerate()函数允许我们为循环自定义起始索引值。enumerate()函数中接受一个可选参数,该参数允许你为本次循环中的计数器变量设置初始值:

1
2
3
复制代码names = ['Bob', 'Alice', 'Guido']
for index, value in enumerate(names, 1):
print(f'{index}: {value}')

在上面的例子中,我将函数调用改为enumerate(names, 1),后面的参数1就是本次循环的起始索引,替换默认的0:

1
2
3
复制代码1: Bob
2: Alice
3: Guido

OK,这段代码演示的就是如何将Python的enumerate()函数默认0起始索引值修改为1(或者其他任何整形值,根据需求去设置不同值)

enumerate()背后是如何工作的

你可能想知道enumerate()函数背后是如何工作的。事实上他的部分魔法是通过Python迭代器来实现的。意思就是每个元素的索引是懒加载的(一个接一个,用的时候生成),这使得内存使用量很低并且保持这个结构运行很快。

让我们演示一些更多的代码来表达我的意思:

1
2
3
复制代码>>> names = ['Bob', 'Alice', 'Guido']
>>> enumerate(names)
<enumerate object at 0x1057f4120>

在上面这个代码片段中,正如你所见,我使用了和前面一样的示例代码。但是,调用enumerate()函数并不会立即返回循环的结果,而只是在控制台中返回了一个enumerate对象。

正如你所看到的,这是一个“枚举对象”。它的确是一个迭代器。就像我说的,它会在循环请求时懒加载地输出每个元素。

为了验证,我们可以取出那些“懒加载”的元素,我计划在这个迭代器上调用Python的内置函数list()

1
2
复制代码>>> list(enumerate(names))
[(0, 'Bob'), (1, 'Alice'), (2, 'Guido')]

对于输入list()中的每个enumerate()迭代器元素,迭代器会返回一个形式为(index,element)的元组作为list的元素。在典型的for-in循环中,你可以利用Python的数据结构解包功能来充分利用这一点特性:

1
2
复制代码for index, element in enumerate(iterable):
# ...

总结:Python中的enumerate函数 - 关键点

  • enumerate是Python的一个内置函数。你应该充分利用它通过循环迭代自动生成的索引变量。
  • 索引值默认从0开始,但也可以将其设置为任何整数。
  • enumerate函数是从2.3版本开始被添加到Python中的,详情见PEP279。
  • Python的enumerate函数可以帮助你编写出更加Pythonic和地道的循环结构,避免使用笨重且容易出错的手动生成索引。
  • 为了充分利用enumerate的特性,一定要研究Python的迭代器和数据结构解包功能。

译者博客:vimiix.com

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

游戏开发—协议设计 一、知识图谱 二、协议三个层次 二、安

发表于 2017-12-14

本篇是游戏开发系列第一篇,如若你有兴趣,请持续关注,后期会持续更新。其他文章列表如下:

游戏开发—协议设计

游戏开发—协议-protobuf

游戏开发-协议-protobuf原理详解

通俗地说,协议就是通信双方能够理解的一种数据格式。维基百科这么定义网络协议:

网络协议为计算机网络中进行数据交换而建立的规则、标准或约定的集合。

协议设计包含三要素:

语法:语法是用户数据与控制信息的结构与格式,以及数据出现的顺序。

语义:解释控制信息每个部分的意义。它规定了需要发出何种控制信息,以及完成的动作与做出什么样的响应。

时序:时序是对事件发生顺序的详细说明

也就是说,语义表示要做什么,语法表示要怎么做,时序表示做的顺序。我们要基于此来设计我的协议。

通常游戏有一些特殊性,比如流量要尽量的少,安全性要求更高,以及对平台支持足够多等等。这一切的需求就要求游戏协议设计,尽量简单、通用,以及代码层上易扩展、解析效率足够高等特点。

基于此,我需要从以下几个层次来考虑游戏协议的设计方案。

一、知识图谱

因为知识点比较多,建议先读知识图谱,对整体结构有一个清晰的综括。

二、协议三个层次

应用层

应用层主要是常用是解析方式定义和解析,主要的选型,主要是看你基于什么需求了,适用于实际需求就好。我们常用的协议类型,主要有这两种:文本协议、二进制协议

1、文本协议:

文本协议设计的目的就是方便人们理解,读懂。如常见的http协议,一般的常见http协议如下:

1
2
3
4
5
6
7
8
复制代码GET/sample.Jsp HTTP/1.1
Accept:image/gif.image/jpeg,*/*
Accept-Language:zh-cn
Connection:Keep-Alive
Host:localhost
User-Agent:Mozila/4.0(compatible;MSIE5.01;Window NT5.0)
Accept-Encoding:gzip,deflate
username=test&password=1234

这种格式非常贴近我们的文字描述,方便阅读,而且目前HTTP也是客户端浏览器或其他程序与Web服务器之间的应用层通信协议,适用非常广泛。但你也看到,有时候基于一个很简单应答,就要带上很多其他的头信息,这对于对流量有要求的游戏应用来说,还是很浪费的。

优点:

1、通用,适用广泛

2、方便理解,可读性好

缺点:

1、基于行读,解析效率一般

2、携带附带信息过多,传输的效率低下

3、无状态,服务器不知道客户端的状态,必须基于客户端的请求来回应,实时性低

4、很难嵌入其他数据,对二进制支持差

如果你的游戏对实时性要求不高,而且对流量要求不也是太高,文本协议也是个不错的方式。一般短连接游戏多适用这个。

2、二进制协议

二进制协议就是一串字节流,是一个典型的Ip协议,一般通常包括消息头(header)和变长的消息体(body),消息头的长度固定,消息体长度不固定,包含主要的内容主体。一般消息头会包含消息体的长度,这样就能基于头信息从数据流中解析出一个完整的二机制消息了。

一般的格式如下:

我们看到head部分定义包含:

cmd:命令字

sign:验证串

content-leg:消息体长度

HeaderCRC:头验证(不是必须)

其中命令字是双方协议文档中规定好的,比如0x01表示登陆,0x02表示注册等等,这些就是一个命令号。

sign是一个验证字符串,对消息体数据进行一定加密验证,保证数据安全。

content-leg是本次消息体的长度。

body部分,比如我们如下定义:

message:login{

string username;

int64 passwoard;

}

我们看到,因为字段的数据类型有定义,顺序也有定义(第一个是string,第二个是int64),整个二进制流读取的的时候,基于顺序读取就可以很快的取出了。

优点:

1、没有冗余字段,传输高效,耗费流量小

2、解析速度快,基于基础数据类型操作

缺点:

1、可读性差,不利于调试

2、扩展性差,对复杂数据结构支持不够

如果你的游戏,对实时性要求比较高,流量有要求,用二进制比较好,一般大型多人网游,使用二进制协议来设计。

3、数据格式

以上我们看到了两种协议类型,但对于消息体的解析介绍很少,消息体的格式决定了的他的语义和时序,格式不同数据的序列化和反序列化也是不同。比如message:login,你可以基于json来定义,也可以基于xml来定义,定义不同解析方式也各不相同。

一般的消息体格式主要有以下几种:json、protocolBuff、xml、自定义

json

json 是一种轻量级的数据交换格式,互联网应用的很广泛了。常用的框架也很多,推荐fastJson,解析速度还是不错的。json的好处是,开源,格式统一,解析速度也还可以。缺点就是会有一些冗余字符,不够简洁。

protocolBuff

protocolBuff是是google提供的一个开源序列化框架,类似于XML,JSON这样的数据表示语言。但是比这些占用空间都小,没有冗余字段。而且好处是灵活,解析速度快,易于开发(基于配置自动生成代码),可支持语言也比较多。一条消息数据,用protobuf序列化后的大小是json的10分之一,xml格式的20分之一,是二进制序列化的10分之一

xml

不多解释了,大家都用有过,强烈不建议使用这种,除了无效字符过多(标签),而且解析效率比上面两种都是很低的。

自定义

自己定义就是自己定义解析方式,比如通过文档定义好一个消息的结构,第一个字段是什么类型,第二个字段什么类型…等等,基于此自己写工具解析。好处是对外协议不透明,解析效率和传送效率都还不错,缺点就是开发难度高,不容易维护。

各种格式优缺点如下:

二、安全层

游戏通信,安全也很重要,不然协议被破解,用户刷资源,整个游戏的平衡性就被破坏了,轻者影响其他玩家体验,重则游戏直接被废。

一般的安全处理就是对协议进行加密。一般有以下几种:

1、常规加密

采用对称加密或者hash加密来对消息内容进行加密,两端采用同一种加密算法,基于同一个密钥对消息体进行加密换算,以此来查看数据是否一致。

密钥可以用户登陆的时候获取一次。还有一种是基于每个用户密钥不同,以此防止密钥泄露大范围影响全服玩家。

2、动态加密

动态加密,可以提前设置一个私有密钥库,里面包含一定数量的密钥,每次客户端请求的时候,基于协议号来设计一个算法获取其中一个密钥。每个协议的密钥都是在协议到达的时候时时获取的,这样即便某一个协议的密钥被破解,对其他协议依然无效。

3、其他

采用非对称加密,或者加盐处理。非对称加密速度太慢了,不建议。

三、传送层

考虑服务端的承载成本,以及手机游戏上网络环境差,原则上UDP是比TCP更适合的方式。但是由于游戏对于数据完整性、安全性要求比较高,采用TCP的又可靠与安全。

目前采用netty作为推送服务器的也有支持上百万连接的应用了,tcp这块性能对于一般游戏支持足够了。长链接游戏多采用分区分服来应对高并发压力,短链接多采用分布式来应对。

四、一些问题

1、字节序

二进制协议中,字节序需要注意,跨语言、平台通信的时候会出现乱码问题。目前的字节序主要有,Little endian和Big endian之分,也就是常说的大头和小头之分。

具体是大头在前还是小头在前,这个和主机的cpu有关系PowerPC系列采用big endian方式存储数据,而x86系列则采用little endian方式存储数据。这个在手机主机上也会出现。

应对方案是:

客户端和服务端强制采用一种字节序,一般采用网络字节序(big endian)

2、浮点数

协议中出现浮点类型要特别注意,浮点类型的传送上面字节序处理OK了,还得注意浮点数的多平台运算不一致问题。

比如游戏中要对寻路、战斗等公式计算,牵扯到浮点数了,有可能前后端算出的不一致,以Arm为例,Arm的浮点数就有软模拟、硬件IEEE-754兼容、SIMD下IEEE-754不兼容三种情况。

此时解决方案,

1、统一一种格式,比如前后端都采用软模拟,或者强制采用硬件IEEE-754(软模拟速度慢)

2、转换为定点数,也就是浮点转换为整数(速度快)

扫描关注我的公众号

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go 中 slice 的那些事

发表于 2017-12-14

Go

一、定义

我们都知道在 Go 语言中,数组的长度是不可变的,那么为了更加灵活的处理数据,Go 提供了一种功能强悍的类型切片(slice),slice 可以理解为 “动态数组”。但是 slice 并不是真正意义上的动态数组,而是一个引用类型。slice 总是指向一个底层 array,slice 的声明也可以像 array 一样,只是不需要长度。slice 的声明和数组类似,如下

1
复制代码var iSlice []int

这里的声明和数组一样,只是少了长度,注意两者的比较

1
2
3
4
5
复制代码//声明一个保存 int 的 slice
var iSlice []int

//声明一个长度为 10 的 int 数组
var iArray [10]int

还有一种声明的方法是使用 make() 函数,如下

1
复制代码slice1 := make([]int, 5, 10)

用 make() 函数创建的时候有三个参数,make(type, len[, cap]) ,依次是类型、长度、容量。

slice1

如图所示,上图表示创建了 slice1 ,长度是 5,默认的值都是 0,容量是 10,这样声明就开辟了一块容量是 10
的连续的一块内存。当然如果我们不指定容量也是可以的,如下

1
复制代码slice2 := make([]int, 5)

这样就会根据实际情况动态分配内存,而不是最开始指定一块固定大小的内存。需要注意的是我们一般使用 make() 函数来创建 slice,因为我们可以指定 slice 的容量,这样在最开始创建的时候就分配好空间,避免数据多次改变导致多次重新改变 cap 分配空间带来不必要的开销。

二、slice 的特性

关于 slice 的一些基本特性,《Go Web 编程》 这本书里已经讲的很详细,有对基本知识不清楚的童鞋可以去补习一下,这里就不一一叙述了。我么来看一个例子,

1
2
3
4
5
6
7
8
9
10
11
12
复制代码package main

import (
"fmt"
)

func main() {
aSlice := []int{1, 2, 3, 4, 5}
fmt.Printf("aSlice length = %d, cap = %d, self = %v\n", len(aSlice), cap(aSlice), aSlice)
aSlice = append(aSlice, 6)
fmt.Printf("aSlice length= %d, cap = %d, self = %v", len(aSlice), cap(aSlice), aSlice)
}

这个时候我们运行,控制台打印

我们会看到 aSlice 进行 append 操作以后,它的容量增加了一倍,cap 并没有变成我们想象中的 6 ,而是变成了 10
aSlice

如果我们最开始 slice 的容量是 10,长度是 5 ,那么再加一个元素是不会改变切片的容量的。也就是说,当我们往 slice中增加元素超过原来的容量时,slice 会自增容量,当现有长度 < 1024 时 cap 增长是翻倍的,当超过 1024,cap 的增长是 1.25 倍增长。我们来看一下 slice.go 的源码会发现有这样一个函数,里面说明了 cap 的增长规则

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
复制代码func growslice(et *_type, old slice, cap int) slice {
/**
....省略....
**/
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
/**
....省略....
**/
}

从上面的源码,在对 slice 进行 append 等操作时,可能会造成 slice 的自动扩容。其扩容时的大小增长规则是:

  • 如果新的 slice 大小是当前大小2倍以上,则大小增长为新大小
  • 否则循环以下操作:如果当前slice大小小于1024,按每次 2 倍增长,否则每次按当前大小 1/4 增长,直到增长的大小超过或等于新大小。
  • append 的实现只是简单的在内存中将旧 slice 复制给新 slice

来看一个例子,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码package main

import "fmt"

func main() {

aSlice := make([]int, 3, 5)
bSlice := append(aSlice, 1, 2)
fmt.Printf("a %v , cap = %d, len = %d\n", aSlice, cap(aSlice), len(aSlice))
fmt.Printf("b %v , cap = %d, len = %d\n", bSlice, cap(bSlice), len(bSlice))
aSlice[0] = 6
fmt.Printf("a %v , cap = %d, len = %d\n", aSlice, cap(aSlice), len(aSlice))
fmt.Printf("b %v , cap = %d, len = %d", bSlice, cap(bSlice), len(bSlice))

}

我们会看到控制台输出

print

变化过程如下图所示
slice变化

上面说明,在 slice 的 cap 范围内增加元素, slice 只会发生 len 的变化不会发生 cap 的变化,同样也说明 slice 实际上是指向一个底层的数组,当多个 slice 指向同一个底层数组的时候,其中一个改变,其余的也会跟着改变,这里需要注意一下。我们同样从 slice.go 的源码中 slice 的定义可以看出,

1
2
3
4
5
复制代码type slice struct {
array unsafe.Pointer
len int
cap int
}

这里关于底层的东西就不多叙述,有兴趣的可以看看 一缕殇流化隐半边冰霜 冰霜的 深入解析 Go 中 Slice 底层实现 这篇文章,对 slice 的底层实现的讲解。接下来我们把上面的代码改变一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码package main

import "fmt"

func main() {

aSlice := make([]int, 3, 5)
bSlice := append(aSlice, 1, 2, 3, 4)
fmt.Printf("a %v , cap = %d, len = %d\n", aSlice, cap(aSlice), len(aSlice))
fmt.Printf("b %v , cap = %d, len = %d\n", bSlice, cap(bSlice), len(bSlice))
aSlice[0] = 6
fmt.Printf("a %v , cap = %d, len = %d\n", aSlice, cap(aSlice), len(aSlice))
fmt.Printf("b %v , cap = %d, len = %d", bSlice, cap(bSlice), len(bSlice))

}

我们可以看到下面的输出

改变后的 print

上面代码可以用下图说明
slice append

也就是说,当 append 的数据超过原来的容量以后,就会重新分配一块新的内存,并把原来的数据 copy 过来,并且保留原来的空间,供原来的 slice(aSlice) 使用这样 aSlice 和 bSlice 就各自指向不同的地址,当 aSlice 改变时,bSlice 不会改变。
关于 cap 还有一点需要注意,我们来用一个例子说明

1
2
3
4
5
6
7
8
9
10
11
12
复制代码package main

import "fmt"

func main() {

Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
Slice_a := Array_a[2:5]
Slice_b := Slice_a[6:7]
fmt.Printf("Slice_a %v , cap = %d, len = %d\n", string(Slice_a), cap(Slice_a), len(Slice_a))
fmt.Printf("Slice_b %v , cap = %d, len = %d\n", string(Slice_b), cap(Slice_b), len(Slice_b))
}

控制台打印

reslice print

这里我们会发现 Slice_b 对 Slice_a 进行重新切片后,并没有报错,而是还有输出,这是因为 Slice_a 的 cap 是 8 ,并不是我们想象的 3,slice 指向的是一块连续的内存,所以 Slice_a 的容量其实是一直到 Array_a 的最后的。所以这里 Array_b 对 Array_a 进行切片后会得到值,《Go Web 编程》 上这张图形象的解释了对数组的切片结果,这里是需要注意的一个点。

slice

三、关于 copy

我们来看下面代码

1
2
3
4
5
6
7
8
9
10
11
12
复制代码package main

import "fmt"

func main() {

aSlice := []int{1, 2, 3}
bSlice := []int{4, 5, 6, 7, 8, 9}
copy(bSlice, aSlice)
fmt.Println(aSlice, bSlice)//[1 2 3] [1 2 3 7 8 9]
//如果是 copy( aSlice, bSlice) 则结果是 [4 5 6]
}

也就是说 copy() 函数有两个参数,一个是 to 一个是 from,就是将第二个 copy 到第一个上面,如果第一个长度小于第二个,那么就会 copy 与第一个等长度的值,如 copy( aSlice, bSlice) 的结果是 [4 5 6] ,反之则是短的覆盖长的前几位。当然我们也可以指定复制长度

1
2
3
4
5
6
7
8
9
10
11
复制代码package main

import "fmt"

func main() {

aSlice := []int{1, 2, 3}
bSlice := []int{4, 5, 6, 7, 8, 9}
copy(bSlice[2:5], aSlice)
fmt.Println(aSlice, bSlice)//[1 2 3] [4 5 1 2 3 9]
}

关于 slice 的 copy 的规则逻辑我们也可以在源码中看出

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
复制代码func slicecopy(to, fm slice, width uintptr) int {
if fm.len == 0 || to.len == 0 {
return 0
}

n := fm.len
if to.len < n {
n = to.len
}

if width == 0 {
return n
}

if raceenabled {
callerpc := getcallerpc()
pc := funcPC(slicecopy)
racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
}
if msanenabled {
msanwrite(to.array, uintptr(n*int(width)))
msanread(fm.array, uintptr(n*int(width)))
}

size := uintptr(n) * width
if size == 1 { // common case worth about 2x to do here
// TODO: is this still worth it with new memmove impl?
*(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
} else {
memmove(to.array, fm.array, size)
}
return n
}

我们看源码接着往下看会发现这样一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码func slicestringcopy(to []byte, fm string) int {
if len(fm) == 0 || len(to) == 0 {
return 0
}

n := len(fm)
if len(to) < n {
n = len(to)
}

if raceenabled {
callerpc := getcallerpc()
pc := funcPC(slicestringcopy)
racewriterangepc(unsafe.Pointer(&to[0]), uintptr(n), callerpc, pc)
}
if msanenabled {
msanwrite(unsafe.Pointer(&to[0]), uintptr(n))
}

memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n))
return n
}

我们会发现这个函数的两个参数分别是 []byte 和 string ,这里其实是 Go 实现了一个将 string 复制到 []byte 上的方法,这个方法有什么用,我们来看个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码package main

import "fmt"

func main() {

s := "hello"
c := []byte(s) // 将字符串 s 转换为 []byte 类型
c[0] = 'c'
s2 := string(c) // 再转换回 string 类型
fmt.Printf("%s\n", s2)
fmt.Printf("s-%x, c-%x, s2-%x", &s, &c, &s2)
}

控制台输出

字符串改变

在 Go 中字符串是不可以改变的,我们可以用上面的方法来改变字符串,这里可以看到是实现了 string 和 []byte 的互相转换,达到了修改 string 的目的。我们去看看 string.go 的源码会发现,有下面的方法

1
2
3
4
5
6
7
8
9
10
11
复制代码func stringtoslicebyte(buf *tmpBuf, s string) []byte {
var b []byte
if buf != nil && len(s) <= len(buf) {
*buf = tmpBuf{}
b = buf[:len(s)]
} else {
b = rawbyteslice(len(s))
}
copy(b, s)
return b
}

可以看到上面有个 copy(b, s) ,这里就是将 string 复制到 []byte 上,在 slice.go 已经实现过了的。从源码中我们也可以看出每次 b 都是重新分配的,然后将 s 复制 给 b,从我们上面程控制台输出也可以看到每次地址都有变化,所以说 string 和 []byte 的相互转换是有内存开销的,不过对于现在的机器来说,这点开销也不算什么。

最后,这是我学习 Go 的 slice 的一些理解与总结,由于能力有限,如果有理解不到位的地方,可以随时留言与我交流。

参考:

  • 1、 build-web-application-with-golang
  • 2、go

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

源码 批量执行invokeAll()&&多选一invokeA

发表于 2017-12-14

ExecutorService中定义了两个批量执行任务的方法,invokeAll()和invokeAny(),在批量执行或多选一的业务场景中非常方便。invokeAll()在所有任务都完成(包括成功/被中断/超时)后才会返回,invokeAny()在任意一个任务成功(或ExecutorService被中断/超时)后就会返回。

AbstractExecutorService实现了这两个方法,本文将先后分析invokeAll()和invokeAny()两个方法的源码实现。

JDK版本:oracle java 1.8.0_102

invokeAll()

invokeAll()在所有任务都完成(包括成功/被中断/超时)后才会返回。有不限时和限时版本,从更简单的不限时版入手。

不限时版

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
复制代码public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException {
if (tasks == null)
throw new NullPointerException();
ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
boolean done = false;
try {
for (Callable<T> t : tasks) {
RunnableFuture<T> f = newTaskFor(t);
futures.add(f);
execute(f);
}
for (int i = 0, size = futures.size(); i < size; i++) {
Future<T> f = futures.get(i);
if (!f.isDone()) {
try {
f.get(); // 无所谓先执行哪个任务的get()方法
} catch (CancellationException ignore) {
} catch (ExecutionException ignore) {
}
}
}
done = true;
return futures;
} finally {
if (!done)
for (int i = 0, size = futures.size(); i < size; i++)
futures.get(i).cancel(true);
}
}

8-12行,先将所有任务都提交到线程池(当然,任何ExecutorService均可)中。

严格来说,不是“提交”,而是“执行”。执行可能是同步或异步的,取决于线程池的策略。不过由于我们仅讨论异步情况(同步同理),用“提交”一词更容易理解。下同。

13-22行,for循环的目的是阻塞调用invokeAll的线程,直到所有任务都执行完毕。当然我们也可以使用其他方式实现阻塞,不过这种方式是最简单的:

  • 15行如果f.isDone()返回true,则当前任务已结束,继续检查下一个任务;否则,调用f.get()让线程阻塞,直到当前任务结束。
  • 17行无所谓先执行哪一个FutureTask实例的get()方法。由于所有任务并发执行,总体阻塞时间取决于于是耗时最长的任务,从而实现了invodeAll的阻塞调用。
  • 18-20行没有捕获InterruptedException。如果有任务被中断,主线程将抛出InterruptedException,以响应中断。

最后,为防止在全部任务结束之前过早退出,23行、25-29行相配合,如果done不为true(未执行到40行就退出了)则取消全部任务。

限时版

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
37
38
39
40
41
42
43
44
45
46
47
复制代码public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException {
if (tasks == null)
throw new NullPointerException();
long nanos = unit.toNanos(timeout);
ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
boolean done = false;
try {
for (Callable<T> t : tasks)
futures.add(newTaskFor(t));

final long deadline = System.nanoTime() + nanos;
final int size = futures.size();

// Interleave time checks and calls to execute in case
// executor doesn't have any/much parallelism.
for (int i = 0; i < size; i++) {
execute((Runnable)futures.get(i));
nanos = deadline - System.nanoTime();
if (nanos <= 0L) // 及时检查是否超时
return futures;
}

for (int i = 0; i < size; i++) {
Future<T> f = futures.get(i);
if (!f.isDone()) {
if (nanos <= 0L) // 及时检查是否超时
return futures;
try {
f.get(nanos, TimeUnit.NANOSECONDS);
} catch (CancellationException ignore) {
} catch (ExecutionException ignore) {
} catch (TimeoutException toe) {
return futures;
}
nanos = deadline - System.nanoTime();
}
}
done = true;
return futures;
} finally {
if (!done)
for (int i = 0, size = futures.size(); i < size; i++)
futures.get(i).cancel(true);
}
}

10-11行,先将所有任务封装为FutureTask,添加到futures列表中。

18-23行,每提交一个任务,就立刻判断是否超时。这样的话,如果在任务全部提交到线程池中之前,就已经达到了超时时间,则能够尽快检查出超时,结束提交并退出。

对于限时版,将封装任务与提交任务拆开是必要的。

28-29行,每次在调用限时版f.get()进入阻塞状态之前,先检查是否超时。这里也是希望超时后,能够尽快发现并退出。

其他同不限时版。

invokeAny()

invokeAny()在任意一个任务成功(或ExecutorService被中断/超时)后就会返回。也分为不限时和限时版本,但为了进一步保障性能,invokeAny()的实现思路与invokeAll()略有不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException {
try {
return doInvokeAny(tasks, false, 0);
} catch (TimeoutException cannotHappen) {
assert false;
return null;
}
}
public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
return doInvokeAny(tasks, true, unit.toNanos(timeout));
}

内部调用了doInvokeAny()。

学习5-8行的写法,代码自解释。

doInvokeAny()

简化如下:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
复制代码private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,
boolean timed, long nanos)
throws InterruptedException, ExecutionException, TimeoutException {
...
ArrayList<Future<T>> futures = new ArrayList<Future<T>>(ntasks);
ExecutorCompletionService<T> ecs =
new ExecutorCompletionService<T>(this);
...
try {
ExecutionException ee = null;
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Iterator<? extends Callable<T>> it = tasks.iterator();

futures.add(ecs.submit(it.next()));
--ntasks;
int active = 1;

for (;;) {
Future<T> f = ecs.poll();
if (f == null) {
if (ntasks > 0) {
--ntasks;
futures.add(ecs.submit(it.next()));
++active;
}
else if (active == 0)
break;
else if (timed) {
f = ecs.poll(nanos, TimeUnit.NANOSECONDS);
if (f == null)
throw new TimeoutException();
nanos = deadline - System.nanoTime();
}
else
f = ecs.take();
}
if (f != null) {
--active;
try {
return f.get();
} catch (...) {
ee = ...;
}
}
}
...
throw ee;
} finally {
for (int i = 0, size = futures.size(); i < size; i++)
futures.get(i).cancel(true);
}
}

要点:

  • ntasks维护未提交的任务数,active维护已提交未结束的任务数。
  • 内部使用ExecutorCompletionService维护已完成的任务。
  • 如果没有任务成功结束,则返回捕获的最后一个异常。
  • 第一个任务是必将被执行的,其他任务按照迭代器顺序增量提交。

增量提交有什么好处呢?节省资源,如果在提交过程中就有任务完成了,那么没必要继续提交任务耗费时间和空间;降低延迟,如果有任务完成,与全量提交相比,能更早被发现。

14行先向线程池提交一个任务(迭代器第一个),ntasks–,active=1:

1
2
3
复制代码futures.add(ecs.submit(it.next()));
--ntasks;
int active = 1;

这里是真“提交”了,不是“执行”。

然后18-45行循环检查是否有任务成功结束。

首先,19行通过及时返回的poll()方法,尝试取出一个已完成的任务:

1
复制代码Future<T> f = ecs.poll();

根据f的结果,分成两种情况讨论。

ExecutorCompletionService默认使用LinkedBlockingQueue作为任务队列。对LinkedBlockingQueue不熟悉的可参照源码|并发一枝花之BlockingQueue。

case1:如果有任务完成

如果有任务完成,则f不为null,进入40-49行,active–,并尝试取出任务结果:

1
2
3
4
5
6
7
8
复制代码if (f != null) {
--active;
try {
return f.get();
} catch (...) {
ee = ...;
}
}
  • 如果能够成功取出,即当前任务已成功结束,直接返回。
  • 如果抛出异常,则当前任务异常结束,使用ee记录异常。

显然,如果已完成的任务是异常结束的,invokeAny()不会退出,而是继续查看其它任务。

FutureTask#get()的用法参照源码|使用FutureTask的正确姿势。

case2:如果没有任务完成

如果没有任务完成,则f为null,进入23-39行,判断是继续提交任务、退出还是等待任务结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码if (f == null) {
if (ntasks > 0) { // check1
--ntasks;
futures.add(ecs.submit(it.next()));
++active;
}
else if (active == 0) // check2
break;
else if (timed) { // check3
f = ecs.poll(nanos, TimeUnit.NANOSECONDS);
if (f == null)
throw new TimeoutException();
nanos = deadline - System.nanoTime();
}
else // check4
f = ecs.take();
}
  • check1:如果还有剩余任务(ntasks > 0),那就继续提交,同时ntasks–,active++。
  • check2:如果没有剩余任务了,且也没有已提交未结束的任务(active == 0),则表示全部任务均已执行结束,但没有一个任务是成功的,可以退出循环。退出循环后,将在47行抛出ee记录的最后一个异常。
  • check3:如果可以没有剩余任务,但还有已提交未结束的任务,且开启了超时机制,则尝试使用超时版poll()等待任务完成。但是,如果这种情况下超时了,就表示整个invokeAny()方法超时了,所以poll()返回null的时候,要主动抛出TimeoutException。
  • check4:如果可以没有剩余任务,但还有已提交未结束的任务,且未开启超时机制,则使用无限阻塞的take()方法,等待任务完成。

这种一堆if-else的代码很丑。可修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
> 复制代码if (f == null) { // check1
> if (ntasks > 0) {
> --ntasks;
> futures.add(ecs.submit(it.next()));
> ++active;
> continue;
> }
> if (active == 0) { // check2
> assert ntasks == 0; // 防止自己改吧改吧把它这句判断挪到了前面
> break;
> }
> if (timed) { // check3
> f = ecs.poll(nanos, TimeUnit.NANOSECONDS);
> if (f == null) {
> throw new TimeoutException();
> }
> nanos = deadline - System.nanoTime();
> } else { // check4
> f = ecs.take();
> }
> }
>
>

修改依据:

  • check1、check2、check3/check4没有并列的判断关系
  • check3、check4有并列的判断关系,非此即彼
  • 结构更清爽

总结

不会写总结。。。

但是会写吐槽啊!!!

猴子现在每次写博客都经历着从“卧槽似乎很简单啊,写个毛”到“卧槽这跟想象的不一样啊!卧槽巨帅!”的心态崩塌,各位巨巨写的代码是真好看,性能还棒棒哒,羡慕崇拜打鸡血。哎,,,保持谦卑,并羡慕脸遥拜各位巨巨。


本文链接:源码|批量执行invokeAll()&&多选一invokeAny()

作者:猴子007

出处:monkeysayhi.github.io

本文基于知识共享署名-相同方式共享 4.0国际许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名及链接。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

怎么样去理解 Python 中的装饰器 怎么样去理解 Pyt

发表于 2017-12-13

怎么样去理解 Python 中的装饰器

首先,本垃圾文档工程师又来了。开始日常的水文写作。起因是看到这个问题如何理解Python装饰器?,正好不久前给人讲过这些,本垃圾于是又开始新的一轮辣鸡文章写作行为了。

预备知识

首先要理解装饰器,首先要先理解在 Python 中很重要的一个概念就是:“函数是 First Class Member” 。这句话再翻译一下,函数是一种特殊类型的变量,可以和其余变量一样,作为参数传递给函数,也可以作为返回值返回。

1
2
3
4
5
6
7
8
复制代码
def abc():
print("abc")

def abc1(func):
func()

abc1(abc)

这段代码的输出就是我们在函数 abc 中输出的 abc 字符串。过程很简单,我们将函数 abc 作为一个参数传递给 abc1 ,然后,在 abc1 中调用传入的函数

再来看一段代码

1
2
3
4
5
6
复制代码
def abc1():
def abc():
print("abc")
return abc
abc1()()

这段代码输出和之前的一样,这里我们将在 abc1 内部定义的函数 abc 作为一个变量返回,然后我们在调用 abc1 获取到返回值后,继续调用返回的函数。

好了,我们再来做一个思考题,实现一个函数 add ,达到 add(m)(n) 等价于 m+n 的效果。这题如果把之前的 First-Class Member 这一概念理清楚后,我们便能很清楚的写出来了

1
2
3
4
5
复制代码def add(m):
def temp(n):
return m+n
return temp
print(add(1)(2))

嗯,这里输出就是 3 。

正文

看了前面的预备知识后,我们便可以开始今天的主题了

先来看一个需求吧

现在我们有一个函数

1
2
3
4
5
复制代码
def range_loop(a,b):
for i in range(a,b):
temp_result=a+b
return temp_result

现在我们要给这个函数加上一些代码,来计算这个函数的运行时间。

我们大概一想,写出了这样的代码

1
2
3
4
5
6
7
复制代码import time
def range_loop(a,b):
time_flag=time.time()
for i in range(a,b):
temp_result=a+b
print(time.time()-time_flag)
return temp_result

先且不论,这样计算时间是不是准确的,现在我们要给如下很多函数加上一个时间计算的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码import time
def range_loop(a,b):
time_flag=time.time()
for i in range(a,b):
temp_result=a+b
print(time.time()-time_flag)
return temp_result
def range_loop1(a,b):
time_flag=time.time()
for i in range(a,b):
temp_result=a+b
print(time.time()-time_flag)
return temp_result
def range_loop2(a,b):
time_flag=time.time()
for i in range(a,b):
temp_result=a+b
print(time.time()-time_flag)
return temp_result

我们初略一想,嗯,Ctrl+C,Ctrl+V。emmmm 好了,现在你们不觉得这段代码特别脏么?我们想让他变得干净点怎么办?

我们想了想,按照之前说的 First-Class Member 的概念。然后写出了如下的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码import time
def time_count(func,a,b):
time_flag=time.time()
temp_result=func(a,b)
print(time.time()-time_flag)
return temp_result

def range_loop(a,b):
for i in range(a,b):
temp_result=a+b
return temp_result
def range_loop1(a,b):
for i in range(a,b):
temp_result=a+b
return temp_result
def range_loop2(a,b):
for i in range(a,b):
temp_result=a+b
return temp_result
time_count(range_loop,a,b)
time_count(range_loop1,a,b)
time_count(range_loop2,a,b)

嗯,看起来像那么回事,好了好了,我们现在新的问题又来了,我们现在是假设,我们所有函数都只有两个参数传入,那么现在如果想支持任意参数的传入怎么办?我们眉头一皱,写下了如下的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码
import time
def time_count(func,*args,**kwargs):
time_flag=time.time()
temp_result=func(*args,**kwargs)
print(time.time()-time_flag)
return temp_result

def range_loop(a,b):
for i in range(a,b):
temp_result=a+b
return temp_result
def range_loop1(a,b):
for i in range(a,b):
temp_result=a+b
return temp_result
def range_loop2(a,b):
for i in range(a,b):
temp_result=a+b
return temp_result
time_count(range_loop,a,b)
time_count(range_loop1,a,b)
time_count(range_loop2,a,b)

好了,现在看起来,有点像模像样了,但是我们再想想,这段代码实际上改变了我们的函数调用方式,比如我们直接运行 range_loop(a,b) 还是没有办法获取到函数执行时间。那么现在我们如果不想改变函数的调用方式,又想获取到函数的运行时间怎么办?

很简单嘛,替换一下不就好了

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
复制代码
import time
def time_count(func):
def wrap(*args,**kwargs):
time_flag=time.time()
temp_result=func(*args,**kwargs)
print(time.time()-time_flag)
return temp_result
return wrap

def range_loop(a,b):
for i in range(a,b):
temp_result=a+b
return temp_result
def range_loop1(a,b):
for i in range(a,b):
temp_result=a+b
return temp_result
def range_loop2(a,b):
for i in range(a,b):
temp_result=a+b
return temp_result
range_loop=time_count(range_loop)
range_loop1=time_count(range_loop1)
range_loop2=time_count(range_loop2)
range_loop(1,2)
range_loop1(1,2)
range_loop2(1,2)

emmmm,这样看起来感觉舒服多了?既没有改变原有的运行方式,也输出了函数运行时间。

但是。。。你们不觉得手动替换太恶心了么???喵喵喵???还有什么可以简化下的么??

好了,Python 知道我们是爱吃糖的孩子,给我们提供了一个新的语法糖,这也是今天的男一号,Decorator 装饰器

说说 Decorator

我们前面已经实现了,在不改变函数特性的情况下,给原有的代码新增一点功能,但是我们也觉得这样手动的替换,太恶心了,是的 Python 官方也觉得这样很恶心,所以新的语法糖来了

我们上面的代码可以写成这样了

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
复制代码
import time
def time_count(func):
def wrap(*args,**kwargs):
time_flag=time.time()
temp_result=func(*args,**kwargs)
print(time.time()-time_flag)
return temp_result
return wrap
@time_count
def range_loop(a,b):
for i in range(a,b):
temp_result=a+b
return temp_result
@time_count
def range_loop1(a,b):
for i in range(a,b):
temp_result=a+b
return temp_result
@time_count
def range_loop2(a,b):
for i in range(a,b):
temp_result=a+b
return temp_result
range_loop(1,2)
range_loop1(1,2)
range_loop2(1,2)

哇,写到这里,你是不是恍然大悟!まさか???是的,其实 @ 符号其实是一个语法糖,他将我们之前的手动替换的过程交给了环境执行。好了用人话描述下,@ 的作用是将被包裹的函数作为一个变量传递给装饰函数/类,将装饰函数/类返回的值替换原本的函数。

1
2
3
复制代码@decorator
def abc():
pass

如同前面所讲的一样,实际上是发生了一个特殊的替换过程 abc=decorator(abc) ,好了我们来做几个题来练习下吧?

1
2
3
4
5
6
7
复制代码
def decorator(func):
return 1
@decorator
def abc():
pass
abc()

这段代码会发生什么?答:会抛出异常。为啥啊?答:因为装饰的时候发生了替换,abc=decorator(abc) ,替换后 abc 的值为 1 。整数默认不能作为一个函数进行调用。

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
复制代码
def time_count(func):
def wrap(*args,**kwargs):
time_flag=time.time()
temp_result=func(*args,**kwargs)
print(time.time()-time_flag)
return temp_result
return wrap

def decorator(func):
def wrap(*args,**kwargs):
temp_result=func(*args,**kwargs)
return temp_result
return wrap

def decorator1(func):
def wrap(*args,**kwargs):
temp_result=func(*args,**kwargs)
return temp_result
return wrap

@time_count
@decorator
@decorator1
def range_loop(a,b):
for i in range(a,b):
temp_result=a+b
return temp_result

这段代码怎么替换的?答:time_count(decorator(decorator1(range_loop)))

嗯,现在是不是对装饰器什么的有了基本的了解?

扩展一下

现在,我想修改下前面写的 time_count 函数,让他支持传入一个 flag 参数,当 flag 为 True 的时候,输出函数运行时间,为 False 的时候不输出时间

我们一步步来,我们先假设新的函数叫做 time_count_plus

我们想实现的效果是这样的

1
2
3
4
5
复制代码@time_count_plus(flag=True)
def range_loop(a,b):
for i in range(a,b):
temp_result=a+b
return temp_result

嗯,我们看了下,首先我们调用了 time_count_plus(flag=True) 一次,将它返回的值作为一个装饰函数来替换 range_loop ,OK 那么我们首先 time_count_plus 要接收一个参数,返回一个函数对吧

1
2
3
4
复制代码def time_count_plus(flag=True):
def wrap1(func):
pass
return wrap1

好了,现在返回了一个函数来作为装饰函数,然后我们说了 @ 其实触发了一次替换过程,好那么我们现在的替换是不是 range_loop=time_count_plus(flag=True)(range_loop) 好了,现在大家应该很清楚了,我们在 wrap1 里面是不是还应该有一个函数并返回?

嗯,最终的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码def time_count_plus(flag=True):
def wrap1(func):
def wrap2(*args,**kwargs):
if flag:
time_flag=time.time()
temp_result=func(*args,**kwargs)
print(time.time()-time_flag)
else:
temp_result=func(*args,**kwargs)
return temp_result
return wrap2
return wrap1
@time_count_plus(flag=True)
def range_loop(a,b):
for i in range(a,b):
temp_result=a+b
return temp_result

是不是这样就清楚多啦!

扩展两下

好了,我们现在有新的需求来了

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码m=3
n=2
def add(a,b):
return a+b

def sub(a,b):
return a-b

def mul(a,b):
return a*b

def div(a,b):
return a/b

现在我们有字符串 a , a 的值可能为 +、-、*、/ 那么现在,我们想根据 a 的值来调用对应的函数怎么办?

我们煎蛋一想,嗯,逻辑判断嘛

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码
m=3
n=2
def add(a,b):
return a+b

def sub(a,b):
return a-b

def mul(a,b):
return a*b

def div(a,b):
return a/b
a=input('请输入 + - * / 中的任意一个\n')
if a=='+':
print(add(m,n))
elif a=='-':
print(sub(m-n))
elif a=='*':
print(mul(m,n))
elif a=='/':
print(div(m,n))

但是这段代码,if else 是不是太多了点?我们仔细一想,用一下 First-Class Member 的特性,然后配合 dict 实现操作符和函数之间的关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码m=3
n=2
def add(a,b):
return a+b

def sub(a,b):
return a-b

def mul(a,b):
return a*b

def div(a,b):
return a/b
func_dict={"+":add,"-":sub,"*":mul,"/":div}
a=input('请输入 + - * / 中的任意一个\n')
func_dict[a](m,n)

emmmm,看起来不错啊,但是我们注册的过程能不能再简化一点? 嗯,这个时候装饰器语法特性就能用上了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码m=3
n=2
func_dict={}
def register(operator):
def wrap(func):
func_dict[operator]=func
return func
return wrap
@register(operator="+")
def add(a,b):
return a+b
@register(operator="-")
def sub(a,b):
return a-b
@register(operator="*")
def mul(a,b):
return a*b
@register(operator="/")
def div(a,b):
return a/b

a=input('请输入 + - * / 中的任意一个\n')
func_dict[a](m,n)

嗯,还记得我们前面说的使用 @ 语法的时候,实际上是触发了一个替换的过程么?这里就是利用这一特性,在装饰器触发的时候,注册函数映射,这样我们直接根据 ‘a’ 的值来获取函数处理数据。另外请注意一点,我们这里没有必要修改原函数,所以我们没有必要写第三层的函数。

如果有熟悉 Flask 同学就知道,在调用 route 方法注册路由的时候,也是使用了这一特性 ,可以参考另外一篇很久前写的垃圾水文 菜鸟阅读 Flask 源码系列(1):Flask的router初探

总结

其实全文下来,大家应该能知道这样一点东西。Python 中的装饰器其实是 First-Class Member 概念的更进一层应用,我们将函数传递给其余函数,包裹上新的功能后再行返回。@ 其实只是将这样一个过程进行了简化而已。在 Python 中,装饰器无处不在,很多官方库中的实现也依赖于装饰器,比如很久之前写过这样一篇垃圾水文 Python 描述符入门指北。

嗯,今天就先写到这里吧!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

HTTP 代理服务器技术选型之旅 背景 代理服务器工作模型

发表于 2017-12-13

好久不写博客了,在元旦到来前水一篇文章,聊聊我在实现代理服务器的过程中遇到的一些坑,同时祝各位读者新年快乐。

背景

长期以来,贴吧开发人员多,业务耦合大,需求变化频繁,因此容易产生 bug。而我所负责的广告相关业务,和 UI 密切相关,一旦因为某种原因(甚至是被别人改了代码)产生了 bug,必然大幅度影响广告收入。

解决问题的一种方法在于频繁的测试,既然避免不了代码层面的耦合,那总是可以通过定时的检查来避免问题。所以我们维护了一组核心 case,密切关注最核心的功能。选择核心 case 实际上是在覆盖面和测试成本之间的权衡,然而多个 case 有不同的测试步骤,测试效率始终难以提高。

因此,我们的目标是建立一个代理服务器,能够在运行时把任何包(包括线上包)的数据改成我希望的样子。换句话说,这个代理服务器也可以理解为一个私服,它能够获得客户端的请求数据并作出修改,也可以获得服务端的响应数据并做修改。

代理服务器工作模型

在早期版本中,我们选择了简单的 HTTP 协议。这种选择对技术的要求最低,我们自己实现了一个代理服务器,开启 socket,监听端口,然后将客户端的请求发送给服务器,再把服务器的返回数据传回客户端。这种模式也被称为:“中间人模式”(MITM: Man In The Middle)。

虽然道理很简单,但实现起来还是有些地方要注意。首先,当 socket 接受数据后,应该新开一个进程/线程 进行处理。既然涉及到新的进程/线程,就一定要注意它的释放时机,否则会导致内存无限制增加。

其次,对于 socket 来说,它并没有等待函数,也就是说我无从得知何时有数据可读,因此这个艰巨的任务就交给了 select。我们把需要监听的 socket 对象作为参数传入其中,函数会一直阻塞,直到有可读、可写的对象,或者达到超时时间。

Keep-Alive 字段可以复用 TCP 连接,是一种常见的 HTTP 协议的优化方式,在 HTTP 1.1 中已经是默认选项。填写这个字段后,Server 返回的数据可能是分批次的,这样能够改善用户体验,但也会增加代理服务器的实现难度。所以代理服务器在作为客户端,向真正服务器请求数据时,应该删除这个字段。

由于整套流程都是自己实现,因此可以比较容易的 HOOK 住上下行数据并做修改。只有注意在接收到全部数据后再做修改即,整个流程可以用下图简单表示:

代理服务器的工作模式

当时做完这一套东西以后,我在团队内部做了一次分享, 感兴趣的读者可以去 images.bestswifter.com/Proxy 2.key 下载 PPT。

技术选型

短连接

由于长连接基于 TCP,不用每次新建连接,也省略了不必要的 HTTP 报文头部,效率明显优于 HTTP。所以各大公司基本上选择了长连接作为实际生产环境下的连接方式。然而由于不熟悉 WebSocket 协议,并且我们依然支持短连接,所以代理服务器最终选择了 HTTP 协议。

要想实现这一点, 就得在应用启动时,模拟后台向客户端发送一段控制信息,强制客户端选择 HTTP 请求。这样一来,即使是线上包也可以走代理服务器。

HTTPS

由于苹果强制要求使用 HTTPS,虽然已经延期,但也是明年的趋势。考虑到后续的使用,我们决定对之前实现的代理服务器进行升级。由于 HTTPS 涉及到请求协议的解析,以及加密解密和证书管理,上述自研方案很难 hold 住。经过一番调研,最后选择了一个比较知名的开源库 mitmproxy。

Mitmproxy

选择这个库最主要的理由是它直接支持 HTTPS,不过没有中文文档,国内的使用相对来说比较少,所以在接入的时候可能会略花一点时间。

这是一个 python 库, 首先要安装 virtualenv,如果本地没装的话输入:

1
复制代码sudo pip install virtualenv

安装好了以后,进入 mitmproxy/venv3.5/bin 文件夹输入:

1
复制代码source ./active

这样就可以启用 virtualenv 环境了。

Hook 脚本

这个库可以理解为命令行中可交互版本的 Charles,不过我并不打算用它的这个功能。因为我的需求主要是利用脚本来 Hook 请求, 所以我选择了 mitmdump 这个工具。使用它的时候可以指定脚本:

1
复制代码mitmdump -s "xxx.py"

脚本也很简单,我们可以重写 requeest 或者 receive 函数:

1
2
复制代码def request(flow):
flow.response.content = "<p>hello world</p>"

运行脚本以后,把手机的代理设为本机 ip 地址,端口号改为 8080,然后用手机浏览器打开 mitm.it/,如果一切配置顺利,你会看到证书的安装界面。

安装好证书后,用手机访问任何一个网站(包括 HTTPS),你应该都会看到一个小小的 hello world,至此所有的配置就完成了。

bug 修改

这个开源库有一个很严重的 bug,在解析 multipart 类型的数据时可能会发生。它使用了 splitline 方法来分割换行符,然而如果数据中有 \n 的话,就会因此丢失。很不幸的是,很多 protobuf 编码后的数据都有 \n,一旦丢失就会导致解析失败。

如果你不幸遇到了和我一样的坑,可以把相关代码改成我的版本:

1
2
3
4
5
6
7
8
复制代码for i in content.split(b"--" + boundary):
parts = i.split(b'\r\n\r\n', 2)
if len(parts) > 1 and parts[0][0:2] != b"--":
match = rx.search(parts[0])
if match:
key = match.group(1)
value = parts[1][0:len(parts[1])-2] # Remove last \r\n
r.append((key, value))

More

到了这一步,基本上已经成功实现支持 HTTPS 的代理服务器了。后续要处理的可能就是解析 protobuf,完善业务代码等等琐碎的事情,只要小心谨慎,基本上不会有问题。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Nodejs 中遇到含空格 URL 的神奇“Bug”——小

发表于 2017-12-13

本文首发于知乎专栏蚂蚁金服体验科技。

首先声明,我在“Bug”字眼上加了引号,自然是为了说明它并非一个真 Bug。

问题抛出

昨天有个童鞋在看后台监控的时候,突然发现了一个错误:

1
2
3
4
5
yaml复制代码[error] 000001#0: ... upstream prematurely closed connection while reading response header from upstream.
client: 10.10.10.10
server: foo.com
request: "GET /foo/bar?rmicmd,begin run clean docker images job HTTP/1.1"
upstream: "http://..."

大概意思就是说:一台服务器通过 HTTP 协议去请求另一台服务器的时候,单方面被对方服务器断开了连接——并且并没有任何返回。

开始重现

客户端 CURL 指令

其实这次请求的一些猫腻很容易就能发现——在 URL 中有空格。所以我们能简化出一条最简单的 CURL 指令:

1
sh复制代码$ curl "http://foo/bar baz" -v

注意:不带任何转义。

最小 Node.js 源码

好的,那么接下去开始写相应的最简单的 Node.js HTTP 服务端源码。

1
2
3
4
5
6
7
8
9
10
js复制代码'use strict';

const http = require('http');

const server = http.createServer(function(req, resp) {
console.log('🌚');
resp.end('hello world');
});

server.listen(5555);

大功告成,启动这段 Node.js 代码,开始试试看上面的指令吧。

如果你也正在跟着尝试这件事情的话,你就会发现 Node.js 的命令行没有输出任何信息,尤其是嘲讽的 '🌚',而在 CURL 的结果中,你将会看见:

1
2
3
4
5
6
7
8
9
10
11
12
shell复制代码$ curl 'http://127.0.0.1:5555/d d' -v
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 5555 (#0)
> GET /d d HTTP/1.1
> Host: 127.0.0.1:5555
> User-Agent: curl/7.54.0
> Accept: */*
>
* Empty reply from server
* Connection #0 to host 127.0.0.1 left intact
curl: (52) Empty reply from server

瞧,Empty reply from server。

Nginx

发现了问题之后,就有另一个问题值得思考了:就 Node.js 会出现这种情况呢,还是其它一些 HTTP 服务器也会有这种情况呢。

于是拿小白鼠 Nginx 做了个实验。我写了这么一个配置:

1
2
3
4
5
6
7
nginx复制代码server {
listen 5555;

location / {
return 200 $uri;
}
}

接着也执行一遍 CURL,得到了如下的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yaml复制代码$ curl 'http://127.0.0.1:5555/d d' -v
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 5555 (#0)
> GET /d d HTTP/1.1
> Host: 127.0.0.1:5555
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: openresty/1.11.2.1
< Date: Tue, 12 Dec 2017 09:07:56 GMT
< Content-Type: application/octet-stream
< Content-Length: 4
< Connection: keep-alive
<
* Connection #0 to host xcoder.in left intact
/d d


厉害了,我的 Nginx
于是乎,理所当然,我暂时将这个事件定性为 Node.js 的一个 Bug。

Node.js 源码排查

认定了它是个 Bug 之后,我就开始了一贯的看源码环节——由于这个 Bug 的复现条件比较明显,我暂时将其定性为“Node.js HTTP 服务端模块在接到请求后解析 HTTP 数据包的时候解析 URI 时出了问题”。

http.js -> _http_server.js -> _http_common.js

源码以 Node.js 8.9.2 为准。

这里先预留一下我们能马上想到的 node_http_parser.cc,而先讲这几个文件,是有原因的——这涉及到最后的一个应对方式。

首先看看 lib/http.js 的相应源码:

1
2
3
4
5
6
7
8
js复制代码...
const server = require('_http_server');

const { Server } = server;

function createServer(requestListener) {
return new Server(requestListener);
}

那么,马上进入 lib/_http_server.js 看吧。

首先是创建一个 HttpParser 并绑上监听获取到 HTTP 数据包后解析结果的回调函数的代码:

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
js复制代码const {
parsers,
...
} = require('_http_common');

function connectionListener(socket) {
...

var parser = parsers.alloc();
parser.reinitialize(HTTPParser.REQUEST);
parser.socket = socket;
socket.parser = parser;
parser.incoming = null;

...

state.onData = socketOnData.bind(undefined, this, socket, parser, state);
...
socket.on('data', state.onData);

...
}

function socketOnData(server, socket, parser, state, d) {
assert(!socket._paused);
debug('SERVER socketOnData %d', d.length);

var ret = parser.execute(d);
onParserExecuteCommon(server, socket, parser, state, ret, d);
}

从源码中文我们能看到,当一个 HTTP 请求过来的时候,监听函数 connectionListener() 会拿着 Socket 对象加上一个 data 事件监听——一旦有请求连接过来,就去执行 socketOnData() 函数。

而在 socketOnData() 函数中,做的主要事情就是 parser.execute(d) 来解析 HTTP 数据包,在解析完成后执行一下回调函数 onParserExecuteCommon()。

至于这个 parser,我们能看到它是从 lib/_http_common.js 中来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码var parsers = new FreeList('parsers', 1000, function() {
var parser = new HTTPParser(HTTPParser.REQUEST);

...

parser[kOnHeaders] = parserOnHeaders;
parser[kOnHeadersComplete] = parserOnHeadersComplete;
parser[kOnBody] = parserOnBody;
parser[kOnMessageComplete] = parserOnMessageComplete;
parser[kOnExecute] = null;

return parser;
});

能看出来 parsers 是 HTTPParser 的一条 Free List(效果类似于最简易的动态内存池),每个 Parser 在初始化的时候绑定上了各种回调函数。具体的一些回调函数就不细讲了,有兴趣的童鞋可自行翻阅。

这么一来,链路就比较明晰了:

请求进来的时候,Server 对象会为该次请求的 Socket 分配一个 HttpParser 对象,并调用其 execute() 函数进行解析,在解析完成后调用 onParserExecuteCommon() 函数。

node_http_parser.cc

我们在 lib/_http_common.js 中能发现,HTTPParser 的实现存在于 src/node_http_parser.cc 中:

1
2
js复制代码const binding = process.binding('http_parser');
const { methods, HTTPParser } = binding;

至于为什么 const binding = process.binding('http_parser') 就是对应到 src/node_http_parser.cc 文件,以及这一小节中下面的一些 C++ 源码相关分析,不明白且有兴趣的童鞋可自行去阅读更深一层的源码,或者网上搜索答案,或者我提前无耻硬广一下我快要上市的书《Node.js:来一打 C++ 扩展》——里面也有说明,以及我的有一场知乎 Live《深入理解 Node.js 包与模块机制》。

总而言之,我们接下去要看的就是 src/node_http_parser.cc 了。

1
2
3
4
5
6
7
8
9
cpp复制代码env->SetProtoMethod(t, "close", Parser::Close);
env->SetProtoMethod(t, "execute", Parser::Execute);
env->SetProtoMethod(t, "finish", Parser::Finish);
env->SetProtoMethod(t, "reinitialize", Parser::Reinitialize);
env->SetProtoMethod(t, "pause", Parser::Pause<true>);
env->SetProtoMethod(t, "resume", Parser::Pause<false>);
env->SetProtoMethod(t, "consume", Parser::Consume);
env->SetProtoMethod(t, "unconsume", Parser::Unconsume);
env->SetProtoMethod(t, "getCurrentBuffer", Parser::GetCurrentBuffer);

如代码片段所示,前文中 parser.execute() 所对应的函数就是 Parser::Execute() 了。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
cpp复制代码class Parser : public AsyncWrap {
...

static void Execute(const FunctionCallbackInfo<Value>& args) {
Parser* parser;
...

Local<Object> buffer_obj = args[0].As<Object>();
char* buffer_data = Buffer::Data(buffer_obj);
size_t buffer_len = Buffer::Length(buffer_obj);

...

Local<Value> ret = parser->Execute(buffer_data, buffer_len);

if (!ret.IsEmpty())
args.GetReturnValue().Set(ret);
}

Local<Value> Execute(char* data, size_t len) {
EscapableHandleScope scope(env()->isolate());

current_buffer_len_ = len;
current_buffer_data_ = data;
got_exception_ = false;

size_t nparsed =
http_parser_execute(&parser_, &settings, data, len);

Save();

// Unassign the 'buffer_' variable
current_buffer_.Clear();
current_buffer_len_ = 0;
current_buffer_data_ = nullptr;

// If there was an exception in one of the callbacks
if (got_exception_)
return scope.Escape(Local<Value>());

Local<Integer> nparsed_obj = Integer::New(env()->isolate(), nparsed);
// If there was a parse error in one of the callbacks
// TODO(bnoordhuis) What if there is an error on EOF?
if (!parser_.upgrade && nparsed != len) {
enum http_errno err = HTTP_PARSER_ERRNO(&parser_);

Local<Value> e = Exception::Error(env()->parse_error_string());
Local<Object> obj = e->ToObject(env()->isolate());
obj->Set(env()->bytes_parsed_string(), nparsed_obj);
obj->Set(env()->code_string(),
OneByteString(env()->isolate(), http_errno_name(err)));

return scope.Escape(e);
}
return scope.Escape(nparsed_obj);
}
}

首先进入 Parser 的静态 Execute() 函数,我们看到它把传进来的 Buffer 转化为 C++ 下的 char* 指针,并记录其数据长度,同时去执行当前调用的 parser 对象所对应的 Execute() 函数。

在这个 Execute() 函数中,有个最重要的代码,就是:

1
2
cpp复制代码size_t nparsed =
http_parser_execute(&parser_, &settings, data, len);

这段代码是调用真正解析 HTTP 数据包的函数,它是 Node.js 这个项目的一个自研依赖,叫 http-parser。它独立的项目地址在 github.com/nodejs/http…,我们本文中用的是 Node.js v8.9.2 中所依赖的源码,应该会有偏差。

http-parser

HTTP Request 数据包体

如果你已经对 HTTP 包体了解了,可以略过这一节。

HTTP 的 Request 数据包其实是文本格式的,在 Raw 的状态下,大概是以这样的形式存在:

1
2
3
makefile复制代码方法 URI HTTP/版本
头1: 我是头1
头2: 我是头2

简单起见,这里就写出最基础的一些内容,至于 Body 什么的大家自己找资料看吧。

上面的是什么意思呢?我们看看 CURL 的结果就知道了,实际上对应 curl ... -v 的中间输出:

1
2
3
4
makefile复制代码GET /test HTTP/1.1
Host: 127.0.0.1:5555
User-Agent: curl/7.54.0
Accept: */*

所以实际上大家平时在文章中、浏览器调试工具中看到的什么请求头啊什么的,都是以文本形式存在的,以换行符分割。

而——重点来了,导致我们本文所述“Bug”出现的请求,它的请求包如下:

1
2
3
4
makefile复制代码GET /foo bar HTTP/1.1
Host: 127.0.0.1:5555
User-Agent: curl/7.54.0
Accept: */*

重点在第一行:

1
bash复制代码GET /foo bar HTTP/1.1

源码解析

话不多少,我们之间前往 http-parser 的 http_parser.c 看 http_parser_execute () 函数中的状态机变化。

从源码中文我们能看到,http-parser 的流程是从头到尾以 O(n) 的时间复杂度对字符串逐字扫描,并且不后退也不往前跳。

那么扫描到每个字符的时候,都有属于当前的一个状态,如“正在扫描处理 uri”、“正在扫描处理 HTTP 协议并且处理到了 H”、“正在扫描处理 HTTP 协议并且处理到了 HT”、“正在扫描处理 HTTP 协议并且处理到了 HTT”、“正在扫描处理 HTTP 协议并且处理到了 HTTP”、……


憋笑,这是真的,我们看看代码就知道了:

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
c复制代码case s_req_server:
case s_req_server_with_at:
case s_req_path:
case s_req_query_string_start:
case s_req_query_string:
case s_req_fragment_start:
case s_req_fragment:
{
switch (ch) {
case ' ':
UPDATE_STATE(s_req_http_start);
CALLBACK_DATA(url);
break;
case CR:
case LF:
parser->http_major = 0;
parser->http_minor = 9;
UPDATE_STATE((ch == CR) ?
s_req_line_almost_done :
s_header_field_start);
CALLBACK_DATA(url);
break;
default:
UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch));
if (UNLIKELY(CURRENT_STATE() == s_dead)) {
SET_ERRNO(HPE_INVALID_URL);
goto error;
}
}
break;
}

在扫描的时候,如果当前状态是 URI 相关的(如 s_req_path、s_req_query_string 等),则执行一个子 switch,里面的处理如下:

  • 若当前字符是空格,则将状态改变为 s_req_http_start 并认为 URI 已经解析好了,通过宏 CALLBACK_DATA() 触发 URI 解析好的事件;
  • 若当前字符是换行符,则说明还在解析 URI 的时候就被换行了,后面就不可能跟着 HTTP 协议版本的申明了,所以设置默认的 HTTP 版本为 0.9,并修改当前状态,最后认为 URI 已经解析好了,通过宏 CALLBACK_DATA() 触发 URI 解析好的事件;
  • 其余情况(所有其它字符)下,通过调用 parse_url_char() 函数来解析一些东西并更新当前状态。(因为哪怕是在解析 URI 状态中,也还有各种不同的细分,如 s_req_path、s_req_query_string )

这里的重点还是当状态为解析 URI 的时候遇到了空格的处理,上面也解释过了,一旦遇到这种情况,则会认为 URI 已经解析好了,并且将状态修改为 s_req_http_start。也就是说,有“Bug”的那个数据包
GET /foo bar HTTP/1.1 在解析到 foo 后面的空格的时候它就将状态改为 s_req_http_start 并且认为 URI 已经解析结束了。

好的,接下来我们看看 s_req_http_start 怎么处理:

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
c复制代码case s_req_http_start:
switch (ch) {
case 'H':
UPDATE_STATE(s_req_http_H);
break;
case ' ':
break;
default:
SET_ERRNO(HPE_INVALID_CONSTANT);
goto error;
}
break;

case s_req_http_H:
STRICT_CHECK(ch != 'T');
UPDATE_STATE(s_req_http_HT);
break;

case s_req_http_HT:
...

case s_req_http_HTT:
...

case s_req_http_HTTP:
...

case s_req_first_http_major:
...

如代码所见,若当前状态为 s_req_http_start,则先判断当前字符是不是合标。因为就 HTTP 请求包体的格式来看,如果 URI 解析结束的话,理应出现类似 HTTP/1.1 的这么一个版本申明。所以这个时候 http-parser 会直接判断当前字符是否为 H。

  • 若是 H,则将状态改为 s_req_http_H 并继续扫描循环的下一位,同理在 s_req_http_H 下若合法状态就会变成 s_req_http_HT,以此类推;

+若是空格,则认为是多余的空格,那么当前状态不做任何改变,并继续下一个扫描;

  • 但如果当前字符既不是空格也不是 H,那么好了,http-parser 直接认为你的请求包不合法,将你本次的解析设置错误 HPE_INVALID_CONSTANT 并 goto 到 error 代码块。

至此,我们基本上已经明白了原因了:

http-parser 认为在 HTTP 请求包体中,第一行的 URI 解析阶段一旦出现了空格,就会认为 URI 解析完成,继而解析 HTTP 协议版本。但若此时紧跟着的不是 HTTP 协议版本的标准格式,http-parser 就会认为你这是一个 HPE_INVALID_CONSTANT 的数据包。

不过,我们还是继续看看它的 error 代码块吧:

1
2
3
4
5
6
c复制代码error:
if (HTTP_PARSER_ERRNO(parser) == HPE_OK) {
SET_ERRNO(HPE_UNKNOWN);
}

RETURN(p - data);

这段代码中首先判断一下当跳到这段代码的时候有没有设置错误,若没有设置错误则将错误设置为未知错误(HPE_UNKNOWN),然后返回已解析的数据包长度。

p 是当前解析字符指针,data 是这个数据包的起始指针,所以 p - data 就是已解析的数据长度。如果成功解析完,这个数据包理论上是等于这个数据包的完整长度,若不等则理论上说明肯定是中途出错提前返回。

回到 node_http_parser.cc

看完了 http-parser 的原理后,很多地方茅塞顿开。现在我们回到它的调用地 node_http_parser.cc 继续阅读吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cpp复制代码Local<Value> Execute(char* data, size_t len) {
...

size_t nparsed =
http_parser_execute(&parser_, &settings, data, len);

Local<Integer> nparsed_obj = Integer::New(env()->isolate(), nparsed);
if (!parser_.upgrade && nparsed != len) {
enum http_errno err = HTTP_PARSER_ERRNO(&parser_);

Local<Value> e = Exception::Error(env()->parse_error_string());
Local<Object> obj = e->ToObject(env()->isolate());
obj->Set(env()->bytes_parsed_string(), nparsed_obj);
obj->Set(env()->code_string(),
OneByteString(env()->isolate(), http_errno_name(err)));

return scope.Escape(e);
}
return scope.Escape(nparsed_obj);
}

从调用处我们能看见,在执行完 http_parser_execute() 后有一个判断,若当前请求不是 upgrade 请求(即请求头中有说明 Upgrade,通常用于 WebSocket),并且解析长度不等于原数据包长度(前文说了这种情况属于出错了)的话,那么进入中间的错误代码块。

在错误代码块中,先 HTTP_PARSER_ERRNO(&parser_) 拿到错误码,然后通过 Exception::Error() 生成错误对象,将错误信息塞进错误对象中,最后返回错误对象。

如果没错,则返回解析长度(nparsed_obj 即 nparsed)。

在这个文件中,眼尖的童鞋可能发现了,执行 Execute() 有好多处,这是因为实际上一个 HTTP 请求可能是流式的,所以有时候可能会只拿到部分数据包。所以最后有一个结束符需要被确认。这也是为什么 http-parser 在解析的时候只能逐字解析而不能跳跃或者后退了。

回到 _http_server.js

我们把 Parser::Execute() 也就是 JavaScript 代码中的 parser.execute() 给搞清楚后,我们就能回到 _http_server.js 看代码了。

前文说了,socketOnData 在解析完数据包后会执行 onParserExecuteCommon 函数,现在就来看看这个 onParserExecuteCommon() 函数。

1
2
3
4
5
6
7
8
9
10
js复制代码function onParserExecuteCommon(server, socket, parser, state, ret, d) {
resetSocketTimeout(server, socket, state);

if (ret instanceof Error) {
debug('parse error', ret);
socketOnError.call(socket, ret);
} else if (parser.incoming && parser.incoming.upgrade) {
...
}
}

长长的一个函数被我精简成这么几句话,重点很明显。ret 就是从 socketOnData 传进来已解析的数据长度,但是在 C++ 代码中我们也看到了它还有可能是一个错误对象。所以在这个函数中一开始就做了一个判断,判断解析的结果是不是一个错误对象,如果是错误对象则调用 socketOnError()。

1
2
3
4
5
6
7
8
js复制代码function socketOnError(e) {
// Ignore further errors
this.removeListener('error', socketOnError);
this.on('error', () => {});

if (!this.server.emit('clientError', e, this))
this.destroy(e);
}

我们看到,如果真的不小心走到这一步的话,HTTP Server 对象会触发一个 clientError 事件。

整个事情串联起来了:

  • 收到请求后会通过 http-parser 解析数据包;
  • GET /foo bar HTTP/1.1 会被解析出错并返回一个错误对象;
  • 错误对象会进入 if (ret instanceof Error) 条件分支并调用 socketOnError() 函数;
  • socketOnError() 函数中会对服务器触发一个 clientError 事件;(this.server.emit('clientError', e, this))
  • 至此,HTTP Server 并不会走到你的那个 function(req, resp) 中去,所以不会有任何的数据被返回就结束了,也就解答了一开始的问题——收不到任何数据就请求结束。

这就是我要逐级进来看代码,而不是直达 http-parser 的原因了——clientError 是一个关键。

处理办法

要解决这个“Bug”其实不难,直接监听 clientError 事件并做一些处理即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码'use strict';

const http = require('http');

const server = http.createServer(function(req, resp) {
console.log('🌚');
resp.end('hello world');
}).on('clientError', function(err, sock) {
console.log('🐷');
sock.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});

server.listen(5555);

**注意:**由于运行到 clientError 事件时,并没有任何 Request 和 Response 的封装,你能拿到的是一个 Node.js 中原始的 Socket 对象,所以当你要返回数据的时候需要自己按照 HTTP 返回数据包的格式来输出。

这个时候再挥起你的小手试一下 CURL 吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
perl复制代码$ curl 'http://127.0.0.1:5555/d d' -v
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 5555 (#0)
> GET /d d HTTP/1.1
> Host: 127.0.0.1:5555
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
* no chunk, no close, no size. Assume close to signal end
<
* Closing connection 0

如愿以偿地输出了 400 状态码。

引申

接下来我们要引申讨论的一个点是,为什么这货不是一个真正意义上的 Bug。

首先我们看看 Nginx 这么实现这个黑科技的吧。

Nginx 实现

打开 Nginx 源码的相应位置。

我们能看到它的状态机对于 URI 和 HTTP 协议声明中间多了一个中间状态,叫 sw_check_uri_http_09,专门处理 URI 后面的空格。

在各种 URI 解析状态中,基本上都能找到这么一句话,表示若当前状态正则解析 URI 的各种状态并且遇到空格的话,则将状态改为 sw_check_uri_http_09。

1
2
3
4
5
6
7
8
9
10
11
c复制代码case sw_check_uri:
switch (ch) {
case ' ':
r->uri_end = p;
state = sw_check_uri_http_09;
break;

...
}

...

然后在 sw_check_uri_http_09 状态时会做一些检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c复制代码case sw_check_uri_http_09:
switch (ch) {
case ' ':
break;
case CR:
r->http_minor = 9;
state = sw_almost_done;
break;
case LF:
r->http_minor = 9;
goto done;
case 'H':
r->http_protocol.data = p;
state = sw_http_H;
break;
default:
r->space_in_uri = 1;
state = sw_check_uri;
p--;
break;
}
break;

例如:

  • 遇到空格则继续保持当前状态开始扫描下一位;
  • 如果是换行符则设置默认 HTTP 版本并继续扫描;
  • 如果遇到的是 H 才修改状态为 sw_http_H 认为接下去开始 HTTP 版本扫描;
  • 如果是其它字符,则标明一下 URI 中有空格,然后将状态改回 sw_check_uri,然后倒退回一格以 sw_check_uri 继续扫描当前的空格。

在理解了这个“黑科技”后,我们很快能找到一个很好玩的点,开启你的 Nginx 并用 CURL 请求以下面的例子一下它看看吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码$ curl 'http://xcoder.in:5555/d H' -v
* Trying 103.238.225.181...
* TCP_NODELAY set
* Connected to xcoder.in (103.238.225.181) port 5555 (#0)
> GET /d H HTTP/1.1
> Host: xcoder.in:5555
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Server: openresty/1.11.2.1
< Date: Tue, 12 Dec 2017 11:18:13 GMT
< Content-Type: text/html
< Content-Length: 179
< Connection: close
<
<html>
<head><title>400 Bad Request</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<hr><center>openresty/1.11.2.1</center>
</body>
</html>
* Closing connection 0

怎么样?是不是发现结果跟之前的不一样了——它居然也返回了 400 Bad Request。

原因为何就交给童鞋们自己考虑吧。

RFC 2616 与 RFC 2396

那么,为什么即使在 Nginx 支持空格 URI 的情况下,我还说 Node.js 这个不算 Bug,并且指明 Nginx 是“黑科技”呢?

后来我去看了 HTTP 协议 RFC。

原因在于 Network Working Group 的 RFC 2616,关于 HTTP 协议的规范。

在 RFC 2616 的 3.2.1 节中做了一些说明,它说了在 HTTP 协议中关于 URI 的文法和语义参照了 RFC 2396。

URIs in HTTP can be represented in absolute form or relative to some known base URI, depending upon the context of their use. The two forms are differentiated by the fact that absolute URIs always begin with a scheme name followed by a colon. For definitive information on URL syntax and semantics, see “Uniform Resource Identifiers (URI): Generic Syntax and Semantics,” RFC 2396 (which replaces RFCs 1738 and RFC 1808). This specification adopts the definitions of “URI-reference”, “absoluteURI”, “relativeURI”, “port”, “host”,”abs_path”, “rel_path”, and “authority” from that specification.

而在 RFC 2396 中,我们同样找到了它的 2.4.3 节。里面对于 Disallow 的 US-ASCII 字符做了解释,其中有:

  • 控制符,指 ASCII 码在 0x00-0x1F 范围内以及 0x7F;

控制符通常不可见;

  • 空格,指 0x20;

空格不可控,如经由一些排版软件转录后可能会有变化,而到了 HTTP 协议这层时,反正空格不推荐使用了,所以就索性用空格作为首行分隔符了;

  • 分隔符,"<"、">"、"#"、"%"、"\""。

如 # 将用于浏览器地址栏的 Hash;而 % 则会与 URI 转义一同使用,所以不应单独出现在 URI 中。

于是乎,HTTP 请求中,包体的 URI 似乎本就不应该出现空格,而 Nginx 是一个黑魔法的姿势。

小结

嚯,写得累死了。本次的一个探索基于了一个有空格非正常的 URI 通过 CURL 或者其它一些客户端请求时,Node.js 出现的 Bug 状态。

实际上发现这个 Bug 的时候,客户端请求似乎是因为那边的开发者手抖,不小心将不应该拼接进来的内容给拼接到了 URL 中,类似于 $ rm -rf /。

一开始我以为这是 Node.js 的 Bug,在探寻之后发现是因为我们自己没用 Node.js HTTP Server 提供的 clientError 事件做正确的处理。而 Nginx 的正常请求则是它的黑科技。这些答案都能从 RFC 中寻找——再次体现了遇到问题看源码看规范的重要性。

另,我本打算给 http-parser 也加上黑魔法,后来我快写好的时候发现它是流式的,很多状态没法在现有的体系中保留下来,最后放弃了,反正这也不算 Bug。不过在以后有时间的时候,感觉还是可以好好整理一下代码,好好修改一下给提个 PR 上去,以此自勉。

最后,求 fafa。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

从JDK源码看OutputStream

发表于 2017-12-13

概况

前面已经了解了输入流《从JDK源码看InputStream》,接着看对应的输出流,JDK 给我们提供了很多实用的输出流 xxxOutputStream,而 OutputStream 是所有字节输出流的抽象。包括 ByteArrayOutputStream 、FilterOutputStream 、BufferedOutputStream 、DataOutputStream 和 PushbackOutputStream 等等。

继承结构

1
2
复制代码--java.lang.Object
--java.io.OutputStream

类定义

1
复制代码public abstract class OutputStream implements Closeable, Flushable

OutputStream 被定为 public 且 abstract 的类,实现了 Closeable 和 Flushable 接口。

Closeable 接口表示 OutputStream 可以被close,接口定义如下:

1
2
3
复制代码public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}

而 Flushable 接口表示 OutputStream 可以进行 flush 操作,接口定义如下。

1
2
3
复制代码public interface Flushable {
void flush() throws IOException;
}

主要属性

没有属性

主要方法

write方法

一共有三个 write 方法,其中有一个抽象的 write 方法,其余两个 write 方法都会调用这个抽象方法,该方法用于将一个字节写入一个输出流读。

主要看第三个 write 方法即可,它传入的三个参数,byte数组、偏移量和数组长度。该方法主要是将指定长度的字节数据写入到输出流中,而在写入前会检查数组是否为空,偏移量和长度是否满足正确的条件等等。最后才是调用抽象方法进行写操作,抽象方法一般都由子类实现具体的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码
public abstract void write(int b) throws IOException;

public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}

public void write(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
for (int i = 0 ; i < len ; i++) {
write(b[off + i]);
}
}

flush方法

对该输出流中缓冲的字节进行 flush 操作,即之前写入该输出流缓冲区的字节将被强制 flush 到目的地。其中目的地可能的情况为:

  1. 如果是一个文件,那么 flush 操作只能保证该输出流缓冲区的字节数据写入到操作系统中等待写入,而并不能保证将其写到磁盘上。
  2. 如果是套接字,那么 flush 操作只能保证写入到操作系统中等待传到其他节点,而不能保证其能立刻写入到远程节点。
  3. 如果是其他设备,那么 flush 也只是将其写入操作系统,而到达其他设备则由操作系统控制。
1
复制代码public void flush() throws IOException {}

close方法

此方法用于关闭输出流,并且释放相关资源,作为抽象类,这里关闭动作不做任何事。另外关闭了的流无法再重新打开。

1
复制代码public void close() throws IOException {}

=============广告时间===============

公众号的菜单已分为“分布式”、“机器学习”、“深度学习”、“NLP”、“Java深度”、“Java并发核心”、“JDK源码”、“Tomcat内核”等,可能有一款适合你的胃口。

鄙人的新书《Tomcat内核设计剖析》已经在京东销售了,有需要的朋友可以购买。感谢各位朋友。

为什么写《Tomcat内核设计剖析》

=========================

相关阅读:

从JDK源码看InputStream

从JDK源码看Writer

欢迎关注:

这里写图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

(001)Spring 之 IOC及其容器

发表于 2017-12-13

概述

Spring的最终模板是简化应用开发的编程模型。spring用于替代更加重量级的企业级java技术,如EJB(Enterprise JavaBean)。

为了降低Java开发的复杂度。spring采取了以下4中关键策略:

  • 基于POJO(Plain Ordinary Java Objects,实际上就是普通的java类)的轻量级和最小侵入式编程。
  • 通过依赖注入(DI, Dependency Injection)和面向接口编程实现松耦合。
  • 基于切面(AOP, Aspect Oriented Programming)和惯例(可以理解为默认配置、方式)进行声明式编程。
  • 通过切面和模板减少重复代码。

ioc容器及其使用

他山之石

  • 谈谈对Spring IOC的理解:blog.csdn.net/qq_22654611…
  • 长话短说Spring(1)之IoC控制反转:www.jianshu.com/p/dff5484f4…
  • Spring IOC原理总结:www.jianshu.com/p/6253726f2…
  • Spring IoC有什么好处呢?:www.zhihu.com/question/23…
  • Spring实现IoC的多种方式:www.cnblogs.com/best/p/5727…
  • Spring容器初始化过程:www.cnblogs.com/luyanliang/…

IOC容器存在价值

Java是一门面向对象编程语言。Java应用本质上是一个个对象及其关系的组合。举个简单的例子。

在传统的人员招聘模式中,流程一般都是这样:HR从多如海的应聘简历中挑选然后进行笔试、面试等等一系列筛选后发放offer。这一系列过程复杂而且费时,最关键的是结果还不理想,特别是针对某些特定的岗位很难通过这一模式物色到合适的人才资源。 (自己创建特定的对象,使用特定的对象)

后来逐渐出现了一些公司专门提供类似的人才寻访服务,这就是大名鼎鼎的猎头行业。猎头的兴起可以说很大程度上改变了人才招聘的模式,现在公司需要招聘某个职位的人才,只需要告诉猎头我要一个怎样的人干怎样的工作等等要求,猎头就会通过自己的渠道去物色人才,经过筛选后提供给客户,大大简化了招聘过程的繁琐,提高了招聘的质量和效率。(告诉中间人,中间人自动给你需要、合适的对象)

这其中一个很重要的变化就是公司HR将繁琐的招聘寻访人才的过程转移至了第三方,也就是猎头。相对比而言,IoC在这里充当了猎头的角色,开发者即公司HR,而对象的控制权就相当于人才寻访过程中的一系列工作。

一句话,在java中,将对象的创建与对象的使用分离开,通过依赖注入(DI)的方式达到对象控制反转(IOC)的目的。原本需要自己做创建对象、维护各个对象关系,现在统一交给统一专业的人或服务处理。

创建对象等操作就是你对对象的控制权,把控制权交给三方,这就是 控制反转(IOC) 的意思。

当需要某个对象的时候,三方将合适的对象给你,这个就是 依赖注入(DI) 的意思。

IOC具象化

关系

主要类或接口uml图

IOC容器接口设计图

ApplicationContext 扩展了BeanFactory后的结果,BeanFactory为bean对象的出生地。

BeanDefinition载入

定义一个bean对象长什么样子,在spring中叫BeanDefinition。如何初始化一个BeanDefinition,spring中常用三种方式定义:xml文件、注解扫描、java代码.

加载BeanDefinition

bean的生命周期

spring中bean生命周期.png

Bean的完整生命周期经历了各种方法调用,这些方法可以划分为以下几类:

  • Bean自身的方法:这个包括了Bean本身调用的方法和通过配置文件中的init-method和destroy-method指定的方法。
  • Bean级生命周期接口方法:这个包括了BeanNameAware、BeanFactoryAware、InitializingBean和DisposableBean这些接口的方法
  • 容器级生命周期接口方法:这个包括了InstantiationAwareBeanPostProcessor 和 BeanPostProcessor 这两个接口实现,一般称它们的实现类为“后处理器”。
  • 工厂后处理器接口方法:这个包括了AspectJWeavingEnabler, ConfigurationClassPostProcessor, CustomAutowireConfigurer等等非常有用的工厂后处理器  接口的方法。工厂后处理器也是容器级的。在应用上下文装配配置文件之后立即调用。

举例(通过注解与java代码定义bean)

  • 定义一个Animal接口。
  • 定义实现Animal接口的两个动物Cat,Dog类;
  • 定义一个接口代理AnimalProxy。
  • 定义一个spring Bean java类

实现通过代理类完成不同操作。

1.Animal接口

1
2
3
4
5
6
复制代码package bean;

public interface Animal {

public void sayName();
}

2.Cat类

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码package bean;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Primary
@Component
public class Cat implements Animal {

public void sayName() {
System.out.println("this is cat");
}
}

3.Dog类

1
2
3
4
5
6
7
8
9
10
复制代码package bean;

import org.springframework.stereotype.Component;

@Component
public class Dog implements Animal {
public void sayName() {
System.out.println("this is dog");
}
}

4.AnimalProxy类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码package bean;

import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class AnimalProxy implements Animal {

@Autowired
private Animal animal;

public void sayName(){
animal.sayName();
}
}

5. AnimalConfig(Bean定义类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码package bean;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

/**
* User: Rudy Tan
* Date: 2017/11/24
*/
@Configuration
@ComponentScan
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AnimalConfig {
}

6.测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码import bean.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AnimalConfig.class)
public class AppTest {

@Autowired
private AnimalProxy animalProxy;

@Test
public void testBeanLoad(){
animalProxy.sayName();
}
}

说明:

  • 注解Component:指明该类为bean类
  • 注解Primary:指明当多个bean对象满足获取条件时候,该对象优选获取。
  • 注解Configuration:指明该类为spring bean定义类。
  • 注解ComponentScan:开启当前目录下的bean注解扫描。
  • 注解EnableAspectJAutoProxy:启用aspectJauto代理。
  • 注解Autowired:该属性,spring自动载入。

总结,多人个人或多个类协助处理问题或实现某个功能,总得要找个领头的来管理这些关系吧。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【译】 REST API URI 设计的七准则

发表于 2017-12-13

摘要:本文属于原创,欢迎转载,转载请保留出处:github.com/jasonGeng88…

原文:blog.restcase.com/7-rules-for…

在了解 REST API URI 设计的规则之前,让我们快速过一下我们将要讨论的一些术语。

URI

REST API 使用统一资源标识符(URI)来寻址资源。在今天的网站上,URI 设计范围从可以清楚地传达API的资源模型,如:

api.example.com/louvre/leon…

到那些难以让人理解的,比如:

api.example.com/68dd0-a9d3-…

Tim Berners-Lee 在他的“Web架构公理”列表中列出了关于 URI 的不透明度的注释:

***唯一可以使用标识符的是对对象的引用。当你没有取消引用时,你不应该查看 URI 字符串的内容以获取其他信息。

  • Tim Berners-Lee***

客户端必须遵循 Web 的链接范例,将 URI 视为不透明标识符。

REST API 设计人员应该创建 URI,将 REST API 的资源模型传达给潜在的客户端开发人员。 在这篇文章中,我将尝试为 REST API URsI 引入一套设计规则。

在深入了解规则之前,先看一下在 RFC 3986 中定义的通用 URI 语法,如下所示:

URI = scheme “://“ authority “/“ path [“?” query] [“#” fragment]

规则#1:URI中不应包含尾随的斜杠(/)

这是作为 URI 路径中最后一个字符的最重要的规则之一,正斜杠(/)不会增加语义值,并可能导致混淆。 REST API 不应该期望有一个尾部的斜杠,并且不应该将它们包含在它们提供给客户端的链接中。

许多 Web 组件和框架将平等对待以下两个 URI:

api.canvas.com/shapes/

api.canvas.com/shapes

然而,URI 中的每个字符都会被计入作为资源的唯一标识。

两个不同的 URI 映射到两个不同的资源。如果 URI 不同,那么资源也会不同,反之亦然。因此,REST API 必须生成和传达清晰的 URI,并且不应容忍任何客户端尝试去对一个资源进行模糊的标识。

更多的API可能会将客户端重定向到末尾没有斜杠的 URI 上,(他们也可能会返回 301 - 用于重新定位资源的 “Moved Permanently”)。

规则#2:正斜杠分隔符(/)必须用于指示层次关系

在 URI 的路径部分的正斜杠(/),用于表示资源之间的层次关系。

例如:

api.canvas.com/shapes/poly…

规则#3:应使用连字符( - )来提高 URI 的可读性

为了使你的 URI 容易被人检索和解释,请使用连字符( - )来提高长路径段中名称的可读性。在任何你将使用英文的空格或连字号的地方,在URI中都应该使用连字符来替换。

例如:

api.example.com/blogs/guy-l…

规则#4:不得在 URI 中使用下划线(_)

文本查看器(如浏览器,编辑器等)经常在 URI 下加下划线,以提供可点击的视觉提示。 根据应用程序的字体,下划线(_)字符可能被这个下划线部分地遮蔽或完全隐藏。

为避免这种混淆,请使用连字符( - )而不是下划线

规则#5:URI 路径中首选小写字母

方便的话,URI 路径中首选小写字母,因为大写字母有时会导致问题。 RFC 3986 中将 URI 定义为区分大小写,但协议头和域名除外。

例如:

api.example.com/my-folder/m…

API.EXAMPLE.COM/my-folder/m…

在 URI 格式规范(RFC 3986)中这两个 URI 是相同的。

api.example.com/My-Folder/m…

而这个 URI 与上面的两个却是不同的。

规则#6:文件扩展名不应包含在 URI 中

在 Web 上,字符(.)通常用于分隔 URI 的文件名和扩展名。

一个 REST API 不应在 URI 中包含人造的文件扩展名,来表示消息实体的格式。 相反,他们应该通过 header 头中 Content-Type 属性的媒体类型来确定如何处理实体的内容。

api.college.com/students/32…

api.college.com/students/32…

不应使用文件扩展名来表示格式偏好。

应鼓励 REST API 客户端使用 HTTP 提供的格式选择机制,即请求 header 中的 Accept 属性。

为了实现简单的链接和调试的便捷,REST API 也可以通过查询参数来支持媒体类型的选择。

规则#7:端点名称是单数还是复数?

这里采用保持简单的原则。虽然你的语法常识会告诉你使用复数来描述资源的单个实例是错误的,但实际的答案是保持 URI 格式一致并且始终使用复数形式。

不必处理奇怪的复数(person/people, goose/geese),这使 API 消费者的生活更美好,也使 API 提供商更容易实现(因为大多数现代框架将在一个通用的 controller 中处理 /students 和 /students/3248234)。

但是你怎么处理关系呢?如果一个关系只能存在于另一个资源中,RESTful 原则可以提供有用的指导。我们来看一下这个例子。某个学生有一些课程。这些课程在逻辑上映射到端点 /students,如下所示:

api.college.com/students/32… - 检索该学生所学习的所有课程清单,学生编号为3248234。

api.college.com/students/32… - 检索该学生的物理课程,学生编号为3248234。

结论

当你设计 REST API 服务时,你必须注意资源,这些资源由 URI 定义。

你正在构建的服务中的每个资源,都将至少有一个 URI 来标识它。这个 URI 最好是有意义的,并能充分描述资源。URI 应遵循可预测的层次结构,以增强可理解性,从而提高可用性:可预测的意义在于它们是一致的,层次结构建立在数据具有结构关系的意义上。

RESTful API 是为消费者编写的。URI 的名称和结构应该向消费者传达意义。通过遵循上述规则,你将创建一个更加清晰的 REST API。 这不是一个 REST 规则或约束,而是增强了 API。

也建议你来看看这篇文章,blog.restcase.com/5-basic-res…

为你的客户设计,而不是为你的数据。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…905906907…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%