Go语言atomic原子操作

atomic是最轻量级的锁,在一些场景下直接使用atomic包还是很有效的。

 

下面内容摘秒自《GO并发编程实战》—— 原子操作:

CAS操作的优势是,可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作。

这可以大大的减少同步对程序性能的损耗。

当然,CAS操作也有劣势。在被操作值被频繁变更的情况下,CAS操作并不那么容易成功。

 

原子操作共有5种,即:增或减、比较并交换、载入、存储和交换

 

 1. 增或减

被用于进行增或减的原子操作(以下简称原子增/减操作)的函数名称都以“Add”为前缀,并后跟针对的具体类型的名称。

不过,由于atomic.AddUint32函数和atomic.AddUint64函数的第二个参数的类型分别是uint32和uint64,所以我们无法通过传递一个负的数值来减小被操作值。

atomic.AddUint32(&ui32, ^uint32(-NN-1))      其中NN代表了一个负整数

 

2. 比较并交换

func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)

第一个参数的值应该是指向被操作值的指针值。该值的类型即为*int32。

后两个参数的类型都是int32类型。它们的值应该分别代表被操作值的旧值和新值

CompareAndSwapInt32函数在被调用之后会先判断参数addr指向的被操作值与参数old的值是否相等。

仅当此判断得到肯定的结果之后,该函数才会用参数new代表的新值替换掉原先的旧值。否则,后面的替换操作就会被忽略。

 

3. 载入

v := atomic.LoadInt32(&value)

函数atomic.LoadInt32接受一个*int32类型的指针值,并会返回该指针值指向的那个值

有了“原子的”这个形容词就意味着,在这里读取value的值的同时,当前计算机中的任何CPU都不会进行其它的针对此值的读或写操作。

这样的约束是受到底层硬件的支持的。

 

4. 存储

在原子的存储某个值的过程中,任何CPU都不会进行针对同一个值的读或写操作。

如果我们把所有针对此值的写操作都改为原子操作,那么就不会出现针对此值的读操作因被并发的进行而读到修改了一半的值的情况了。

原子的值存储操作总会成功,因为它并不会关心被操作值的旧值是什么。

函数atomic.StoreInt32会接受两个参数。第一个参数的类型是*int 32类型的,其含义同样是指向被操作值的指针。而第二个参数则是int32类型的,它的值应该代表欲存储的新值。其它的同类函数也会有类似的参数声明列表。

 

5. 交换

与CAS操作不同,原子交换操作不会关心被操作值的旧值。它会直接设置新值。但它又比原子载入操作多做了一步。作为交换,它会返回被操作值的旧值。此类操作比CAS操作的约束更少,同时又比原子载入操作的功能更强。

以atomic.SwapInt32函数为例。它接受两个参数。第一个参数是代表了被操作值的内存地址的*int32类型值,而第二个参数则被用来表示新值。注意,该函数是有结果值的。该值即是被新值替换掉的旧值。atomic.SwapInt32函数被调用后,会把第二个参数值置于第一个参数值所表示的内存地址上(即修改被操作值),并将之前在该地址上的那个值作为结果返回。

 

例子:

df.rmutex.Lock()

defer df.rmutex.Unlock()

return df.roffset / int64(df.dataLen)

我们现在去掉施加在上面的锁定和解锁操作,转而使用原子操作来实现它。修改后的代码如下:

offset := atomic.LoadInt64(&df.roffset)

return offset / int64(df.dataLen)

 

 

用原子操作来替换mutex锁

其主要原因是,原子操作由底层硬件支持,而锁则由操作系统提供的API实现。若实现相同的功能,前者通常会更有效率。

 

关于atomic,并发编程的作者说很细很清楚,再可以看看下面两篇好文档::

Golang 1.3 sync.Mutex 源码解析

Golang 1.3 sync.Atomic源码解析

 

MAIL: xcl_168@aliyun.com

BLOG: http://blog.csdn.net/xcl168

发表在 golang | 留下评论

Golang sync包源码剖析

http://cholerae.com/2015/11/04/Golang-sync包源码剖析/

Golang在标准库中提供了一些并发编程所需要的基本工具,包括同步机制、原子操作、对象池等,这些工具基本都包含在sync包中,本文从源码入手简单分析一下各个工具的实现机制。

原子操作

Golang的原子操作函数全部包含在了sync/atomic中。sync包中其他许多工具的实现都依赖于原子操作,所以先来看一下原子操作的实现。

原子操作的实现全部在asm_xxx.s文件中,使用汇编写成。以amd64平台的cas操作为例,它的源码如下:

1

2

3

4

5

6

7

8

9

10

11

TEXT ·CompareAndSwapUint32(SB),NOSPLIT,$0-17

MOVQ    addr+0(FP), BP

MOVL    old+8(FP), AX

MOVL    new+12(FP), CX

// 载入参数

LOCK

CMPXCHGL    CX, 0(BP)

// 使用LOCK前缀完成交换操作

SETEQ   swapped+16(FP)

//设置返回值

RET

 

可见cas操作是通过对修改命令加LOCK前缀实现的,LOCK前缀可以保证对CPU缓存的操作是排他的。

 

事实上,其他的原子操作基本也是通过LOCK前缀加一个读改写指令完成,或者是通过XCHG系列指令完成(Store系列函数),而XCHG指令本身是带有锁语义的。值得注意的是,Load系列函数由于只读不改,所以没有进行任何加锁。

atomic.Value的方法实际上就是对其他原子操作的调用,Golang中的接口类型在内存中是一个struct,包含类型和值两个域,atomic.Value的方法都是对指向这两个域的指针分别做原子操作的过程。

Mutex/RWMutex

Mutex内部使用了信号量和自旋锁,信号量在runtime/sema.go中实现,自旋锁在runtime/proc1.go中实现,再此不再赘述。

Mutex的Lock方法有三个执行分支:

首先是fastpath,使用一个简单的cas操作修改锁状态,如果修改失败进入后面的分支。实际上sync包中许多函数的实现都是类似的结构,首先用原子操作实现一个fastpath,操作失败后再使用信号量和自旋锁进行slowpath。

在第二个分支会调用自旋锁,忙等待这个Mutex的Unlock。但是自旋锁的使用是有限制的(runtime/proc1.go第3712行的判断),不可能让忙等待的goroutine占用太多资源,所以自旋一定次数或者自旋的goroutine数量超出一个限制之后runtime就会禁用自旋锁。这时就会进入第三个分支。

第三个分支比较简短,会在cas操作失败之后直接获取信号量,然后等待被唤醒,如此循环下去。

Unlock过程要简单的多,直接使用cas操作修改锁的状态,然后释放信号量。

RWMutex的实现比Mutex要简单。RWMutex的结构中有两个信号量,分别对应于读者和写者,另外两个整数记录读者和写者的数量。RWMutex的加解锁过程都是两步,首先原子操作修改读者或写者计数,然后直接获取信号量,与Mutex相比省去了fastpath和自旋锁过程。由此可见,当多读少写的场景中,使用RWMutex的效率应当会高于Mutex,因为省去了大量的忙等待过程。

Cond

Cond结构由一个等待者计数、信号量、锁组成(还有一个copychecker用来检测Cond对象是否被移动,与Cond的功能实现无关)。

Cond.Wait方法会首先原子递增等待者计数,然后获取信号量进入休眠,代码如下:

1

2

3

c.L.Unlock()

runtime_Syncsemacquire(&c.sema)

c.L.Lock()

 

所以在Wait之前应当手动为c.L上锁,Wait结束后手动解锁。为避免虚假唤醒,需要将Wait放到一个条件判断循环中。官方要求的写法如下:

1

2

3

4

5

6

c.L.Lock()

for !condition() {

c.Wait()

}

... make use of condition ...

c.L.Unlock()

 

Broadcast和Signal方法底层都是调用signalImpl这个方法,区别只在于唤醒等待在信号量上的goroutine数量不同。

WaitGroup

WaitGroup由一个计数器和一个32位整数信号量组成。这个计数器是一个[12]byte,事实上只有后64位用于计数,前面的32位是为了在32位编译环境下保证64位对齐(64位的原子操作的要求)。

WaitGroup的方法不复杂,都是使用原子操作修改计数器然后对信号量进行操作。

WaitGroup的使用要求稍微多一点,文档要求当计数器为0时,Add一个正数必须在Wait之前发生。另外,如果要复用WaitGroup,必须等上一次使用中所有的Wait均已返回才行。

Pool

sync.Pool是一个在Go1.3才加入标准库的新类型,设计目的是复用对象,减轻GC压力。

由于Pool在使用时可能会在多个goroutine之间交换对象,所以比较复杂。我们先来看一下数据结构:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

type Pool struct {

local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal

localSize uintptr        // size of the local array

 

// New optionally specifies a function to generate

// a value when Get would otherwise return nil.

// It may not be changed concurrently with calls to Get.

New func() interface{}

}

 

// Local per-P Pool appendix.

type poolLocal struct {

private interface{}   // Can be used only by the respective P.

shared  []interface{} // Can be used by any P.

Mutex                 // Protects shared.

pad     [128]byte     // Prevents false sharing.

}

 

可以看到Pool类型中保存了一个poolLocal的数组,大小即P的数量,也就是给每一个系统线程分配了一个poolLocal,而poolLocal类型中保存了真正的数据。为什么要分poolLocal要分private和shared两个域来保存对象呢?因为poolLocal中的对象可能会被其他P偷走,private域保证这个P不会被偷光,至少能保留一个对象供自己用。否则,如果这个P只剩一个对象,被偷走了,那么当它本身需要对象时又要从别的P偷回来,造成了不必要的开销。

Get操作分为三个阶段:

  1. fastpath。从自身对应的poolLocal中按private->shared的顺序获取对象。
  2. 偷取对象。挨个遍历其他P的poolLocal,一旦发现shared数组不为空,就将尾部的对象偷走并返回。
  3. 使用New生成一个新的对象。

Put操作首先会试图将新对象放到自己的poolLocal.private中,如果private已经有对象了,就会放到shared的尾部。可见Put操作不会涉及到其他的P,比较简单。

Pool的清空:

在每次GC之前,runtime会调用poolCleanup函数来将Pool所有的指针变为nil,计数变为0,这样原本Pool中储存的对象会被GC全部回收。这个特性使得Pool有自己独特的用途。首先,有状态的对象绝不能储存在Pool中,Pool不能用作连接池。其次,你不需要担心Pool会不会一直增长,因为runtime定期帮你回收Pool中的数据。但是也不能无限制地向Pool中Put新的对象,这样会拖累GC,也违背了Pool的设计初衷。官方的说法是Pool适用于储存一些会在goroutine间分享的临时对象,举的例子是fmt包中的输出缓冲区。

Once

Once由一个Mutex和一个整数类型的标志done组成,只有一个方法Do,代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

func (o *Once) Do(f func()) {

if atomic.LoadUint32(&o.done) == 1 {

return

}

// Slow-path.

o.m.Lock()

defer o.m.Unlock()

if o.done == 0 {

defer atomic.StoreUint32(&o.done, 1)

f()

}

}

 

如果这个函数已经执行过了,就会走fastpath,一个原子操作判断一下done标志然后返回。

如果没有执行过,需要加锁执行函数,然后修改标志。这里在加锁后仍然使用原子操作修改标志是因为fastpath没有加锁。

发表在 golang | 留下评论

golang flagset用法

package main

import (
	"flag"
	"fmt"
	"os"
)

type test string

//决定如何处理多个参数:-t a -t b -t c ,如果不实现Set方法,默认实现是取最后一个值
func (t *test) Set(s string) error {	fmt.Println("new value:" + s)
	*t = test(string(*t) + s)//多个参数拼接在一起
	return nil
}
func (t *test) String() string {
	return string(*t)
}
//自定义获取时的处理
func (t *test) Get() interface{} {
	tmp := "处理一下参数值:" + string(*t)
	return test(tmp)
}
var fs = flag.NewFlagSet("test", flag.ExitOnError)

func main() {
	var t test = test("default-value")
	fs.Var(&t, "t", "show info")
	fs.Parse(os.Args[1:])
	s := fs.Lookup("t").Value.String()
	fmt.Println("param:", s)
        tmp := fs.Lookup("t").Value.(flag.Getter).Get()
	fmt.Println("param:", tmp)
	a, b := fs.Lookup("t").Value.(flag.Getter).Get().(test)//Get返回值为interface{}类型
	fmt.Println("param:", a, b)

}

用法:

go run test.go -h
输出:
Usage of test:
  -t value
    	show info (default default-value)
exit status 2
go run d.go  -t aaa -t bb
输出:
new value:aaa
new value:bb
param: default-valueaaabb
param: 处理一下参数值:default-valueab
param: 处理一下参数值:default-valueab true
发表在 golang | 留下评论

golang参数解析flag、flagset

http://studygolang.com/articles/2834


//flag.String(), Bool(), Int() 返回值为指针类型
//flag.StringVar(), BoolVar(), IntVar() 解析出的参数保存在第一个参数中,第一个参数为指针
//os.Args保存所有参数,os.Args[0]为正在运行的程序
package main
import (
	"flag"
	"fmt"
	"os"	
)
var (
	 levelFlag = flag.Int("level", 0, "级别")  
	 bnFlag int  
)
func init() {
	 flag.IntVar(&bnFlag, "bn", 3, "份数")  
}
func main() {

	flag.Parse()  
    count := len(os.Args)
    fmt.Println("参数总个数:",count)

    fmt.Println("参数详情:")
    for i := 0 ; i < count ;i++{
        fmt.Println(i,":",os.Args[i])
    }
    fmt.Println("\n参数值:")
	fmt.Println("级别:", *levelFlag)
	fmt.Println("份数:", bnFlag)
}


/*
运行结果:

C:\TEMP\testflag>go run tf2.go -level 3 -bn=2
参数总个数: 4
参数详情:
0 : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\go-build158983983\command-line-arguments\_obj\exe\tf2.exe
1 : -level
2 : 3
3 : -bn=2

参数值:
级别: 3
份数: 2


*/

flagset

package main

import (
	"flag"
	"fmt"
	"os"
	"time"
)

var (
/*
	参数解析出错时错误处理方式
	switch f.errorHandling {
		case ContinueOnError:
			return err
		case ExitOnError:
			os.Exit(2)
		case PanicOnError:
			panic(err)
		} 
		*/

	//flagSet = flag.NewFlagSet(os.Args[0],flag.PanicOnError) 
	flagSet = flag.NewFlagSet(os.Args[0],flag.ExitOnError) 
	//flagSet = flag.NewFlagSet("xcl",flag.ExitOnError) 
	verFlag = flagSet.String("ver", "", "version")
	xtimeFlag  = flagSet.Duration("time", 10*time.Minute, "time Duration")

	addrFlag = StringArray{}
)

func init() {
	flagSet.Var(&addrFlag, "a", "b")
}

func main() {
	fmt.Println("os.Args[0]:", os.Args[0])
	flagSet.Parse(os.Args[1:]) //flagSet.Parse(os.Args[0:])

	fmt.Println("当前命令行参数类型个数:", flagSet.NFlag())  
	 for i := 0; i != flagSet.NArg(); i++ {  
        fmt.Printf("arg[%d]=%s\n", i, flag.Arg(i))  
    }  

    fmt.Println("\n参数值:")
	fmt.Println("ver:", *verFlag)
	fmt.Println("xtimeFlag:", *xtimeFlag)
	fmt.Println("addrFlag:",addrFlag.String())

	for i,param := range flag.Args(){
        fmt.Printf("---#%d :%s\n",i,param)
    }
}


type StringArray []string

func (s *StringArray) String() string {
	return fmt.Sprint([]string(*s))
}

func (s *StringArray) Set(value string) error {
	*s = append(*s, value)
	return nil
}


/*
运行结果:
C:\TEMP\testflag>go run tfs.go -ver 9.0 -a ba -a ca -a d2 -ver 10.0 -time 2m0s
os.Args[0]: C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\go-build341936307\command-line-arguments\_obj\exe\tfs.exe
当前命令行参数类型个数: 3

参数值:
ver: 10.0
xtimeFlag: 2m0s
addrFlag: [ba ca d2]



C:\TEMP\testflag>go run tfs.go -ver 9.0 -a ba -a ca -a d2 -ver 10.0
os.Args[0]: C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\go-build712958211\command-line-arguments\_obj\exe\tfs.exe
当前命令行参数类型个数: 2

参数值:
ver: 10.0
xtimeFlag: 10m0s
addrFlag: [ba ca d2]



-- flagSet = flag.NewFlagSet(os.Args[0],flag.PanicOnError) 结果:
C:\TEMP\testflag>go run tfs.go -ver 9.0 -a ba -a ca -a d2 -ver 10.0 -time 2m0s33
os.Args[0]: C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\go-build841833143\command-line-arguments\_obj\exe\tfs.exe
invalid value "2m0s33" for flag -time: time: missing unit in duration 2m0s33
Usage of C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\go-build841833143\command-line-arguments\_obj\exe\tfs.exe:
  -a=[]: b
  -time=10m0s: time Duration
  -ver="": version
panic: invalid value "2m0s33" for flag -time: time: missing unit in duration 2m0s33

goroutine 1 [running]:
flag.(*FlagSet).Parse(0x10b18180, 0x10b42008, 0xc, 0xc, 0x0, 0x0)
        c:/go/src/flag/flag.go:814 +0xee
main.main()
        C:/TEMP/testflag/tfs.go:41 +0x163
exit status 2


-- flagSet = flag.NewFlagSet(os.Args[0],flag.ExitOnError) 结果:
C:\TEMP\testflag>go run tfs.go -ver 9.0 -a ba -a ca -a d2 -ver 10.0 -time 2m0s33
os.Args[0]: C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\go-build501686683\command-line-arguments\_obj\exe\tfs.exe
invalid value "2m0s33" for flag -time: time: missing unit in duration 2m0s33
Usage of C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\go-build501686683\command-line-arguments\_obj\exe\tfs.exe:
  -a=[]: b
  -time=10m0s: time Duration
  -ver="": version
exit status 2


*/

flagSet类可以让参数处理更加灵活.
其中NewFlagSet:
flagSet = flag.NewFlagSet("xcl",flag.ExitOnError)
NewFlagSet的第一个参数是可以任意定的.但第二个参数,则决定了参数解析出错时错误处理方式.

如果要扩展参数定义,只要实现下面的接口:
type Value interface {
String() string
Set(string) error
}
依例子中的 StringArray 一样.就可以实现如:
-a ba -a ca -a d2
addrFlag: [ba ca d2]
这种效果.
要知道,同样的:

-ver 9.0 -ver 10.0
最后的结果是 ver: 10.0 即参数只识别最末一次的.

发表在 golang | 留下评论

golang signal用法

package main

import (
	"fmt"
	"os"
	"os/signal"
	"sync"
	"syscall"
)
//func Notify(c chan<- os.Signal, sig ...os.Signal) 
//第一个参数表示接收信号的管道 
//第二个及后面的参数表示设置要监听的信号,如果不设置表示监听所有的信号。
func main() {
	c := make(chan os.Signal)

	var wg sync.WaitGroup
	wg.Add(1)
	go func(wg sync.WaitGroup, c chan os.Signal) {
		signal.Notify(c, syscall.SIGUSR2,os.Interrupt) //监听指定信号
		fmt.Println("waiting for signal")
		s := <-c //阻塞直至有信号传入 
                if s == os.Interrupt {
                   fmt.Println("here")
                   os.Exit(1)
                }  
		fmt.Println("signal:", s)
		wg.Done()
	}(wg, c)

	fmt.Println("waiting")
	wg.Wait()
}
发表在 golang | 留下评论

深入NSQ 之旅

http://www.oschina.net/translate/day-22-a-journey-into-nsq

http://wiki.jikexueyuan.com/project/nsq-guide/

发表在 nsq | 留下评论

nsq topic和channel的区别

topic:一个可供订阅的话题。
channel:属于topic的下一级,一个topic可以有多个channel。
二者关系可以再参考下面两文章:

http://www.cnblogs.com/forrestsun/p/3892710.html

http://www.linuxeden.com/html/news/20140301/148960.html

举个例子:
topic:比做一个广播,如交通广播。打开收音机,你可以换很多频率,如果换到91.6MHZ,你就会听到交通广播,(我们这里交通广播是91.6)。相当于你订阅了“交通广播”这个topic。
一个topic下有多个channel,可以看作是广播里会有很多节目,比如路况信息、美食、用车知识等等。每一个节目可以比作一个channel。

如果你一直订阅了交通广播,那你就会收到这个下面所有channel的信息,订阅了topic,就会收到topic下所有消息。
当然,你可能只关注用车知识这个消息,那可以在这个节目的播出时间听这个广播就可以了,等节目播完,就可以不收听这个广播。相当于:nsq里可以只订阅某一个channel的信息。这样的话,一个topic下无关的channel就不会发过来。
如果一个channel有多个订阅者,NSQ会使用负载均衡的策略,给其它一个订阅者发消息。

FROM : http://www.baiyuxiong.com/?p=960

发表在 nsq | 留下评论

golang nsq教程

http://nsq.io/overview/quick_start.html

发表在 golang, nsq | 留下评论

Go学习笔记 - 使用jsonrpc进行远程访问

http://www.cnblogs.com/hangxin1940/p/3256995.html

发表在 golang, rpc | 留下评论

golang rpc 简单范例

http://studygolang.com/articles/2409

RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。

它的工作流程如下图:

golang 使用 RPC的例子如下:

服务器端代码:

这里暴露了一个RPC接口,一个HTTP接口

package main

import ( 
    "fmt" 
    "io" 
    "net" 
    "net/http" 
    "net/rpc" 
)

type Watcher int

func (w *Watcher) GetInfo(arg int, result *int) error { 
    *result = 1 
    return nil 
}

func main() {

    http.HandleFunc("/ghj1976", Ghj1976Test)

    watcher := new(Watcher) 
    rpc.Register(watcher) 
    rpc.HandleHTTP()

    l, err := net.Listen("tcp", ":1234") 
    if err != nil { 
        fmt.Println("监听失败,端口可能已经被占用") 
    } 
    fmt.Println("正在监听1234端口") 
    http.Serve(l, nil) 
}

func Ghj1976Test(w http.ResponseWriter, r *http.Request) { 
    io.WriteString(w, "ghj1976-123") 
}

客户端代码:

package main

import (
"fmt"
"net/rpc"
)

func main() {
client, err := rpc.DialHTTP("tcp", "127.0.0.1:1234")
if err != nil {
fmt.Println("链接rpc服务器失败:", err)
}
var reply int
err = client.Call("Watcher.GetInfo", 1, &reply)
if err != nil {
fmt.Println("调用远程服务失败", err)
}
fmt.Println("远程服务返回结果:", reply)
}
[liuzhantao@localhost ~/dev/g]$ go run d.go 
远程服务返回结果: 1
[liuzhantao@localhost ~/dev/g]$ 
[liuzhantao@localhost ~/dev/g]$ curl 'localhost:1234/ghj1976' 
ghj1976-123
发表在 golang, rpc | 留下评论

golang rpc

server:

package main

import (
	"log"
	"net"
	"net/http"
	"net/rpc"
	"strconv"
)

type Args struct {
	A, B int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *([]string)) error {
	a := args.A
	b := args.B
	c := a * b
	*reply = append(*reply, strconv.Itoa(c))
	return nil
}

func main() {
	arith := new(Arith)

	rpc.Register(arith)
	rpc.HandleHTTP()

	l, e := net.Listen("tcp", ":1234")
	if e != nil {
		log.Fatal("listen error:", e)
	}
	http.Serve(l, nil)
}

client:

package main

import (
	"log"
	"net/rpc"
)

type Args struct {
	A, B int
}

func main() {
	client, err := rpc.DialHTTP("tcp", "127.0.0.1"+":1234")
	if err != nil {
		log.Fatal("dialing:", err)
	}

	args := &Args{7, 8}
	reply := make([]string, 10)
	err = client.Call("Arith.Multiply", args, &reply)
	if err != nil {
		log.Fatal("arith error:", err)
	}
	log.Println(reply)
}

另一种是使用NewServer
这种是当rpc已经注册的时候就要使用了另外一种了。即一个server只能在DefaultRPC中注册一种类型。
当Server使用rpc.NewServer的时候,client也需要进行下改动了

package main
 
import (
        "net/rpc"
        //"net/http"
        "log"
        "net"
        "time"
)
 
 
type Args struct {
        A, B int
}
 
type Arith int
 
func (t *Arith) Multiply(args *Args, reply *([]string)) error {
        *reply = append(*reply, "test")
        return nil
}
 
func main() {
        newServer := rpc.NewServer()
        newServer.Register(new(Arith))
 
        l, e := net.Listen("tcp", "127.0.0.1:1234") // any available address
        if e != nil {
                log.Fatalf("net.Listen tcp :0: %v", e)
        }
 
        go newServer.Accept(l)
        newServer.HandleHTTP("/foo", "/bar")
        time.Sleep(2 * time.Second)
 
        address, err := net.ResolveTCPAddr("tcp", "127.0.0.1:1234")
        if err != nil {
                panic(err)
        }
        conn, _ := net.DialTCP("tcp", nil, address)
        defer conn.Close()
 
        client := rpc.NewClient(conn)
        defer client.Close()
 
        args := &Args{7,8}
        reply := make([]string, 10)
        err = client.Call("Arith.Multiply", args, &reply)
        if err != nil {
                log.Fatal("arith error:", err)
        }
        log.Println(reply)
}
发表在 golang, rpc | 留下评论

rpc

RPC 是什么?

RPC 的全称是 Remote Procedure Call 是一种进程间通信方式。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的,本质上编写的调用代码基本相同。

RPC 起源

RPC 这个概念术语在上世纪 80 年代由 Bruce Jay Nelson 提出。这里我们追溯下当初开发 RPC 的原动机是什么?在 Nelson 的论文 "Implementing Remote Procedure Calls" 中他提到了几点:

1. 简单:RPC 概念的语义十分清晰和简单,这样建立分布式计算就更容易。
2. 高效:过程调用看起来十分简单而且高效。
3. 通用:在单机计算中过程往往是不同算法部分间最重要的通信机制。

通俗一点说,就是一般程序员对于本地的过程调用很熟悉,那么我们把 RPC 作成和本地调用完全类似,那么就更容易被接受,使用起来毫无障碍。Nelson 的论文发表于 30 年前,其观点今天看来确实高瞻远瞩,今天我们使用的 RPC 框架基本就是按这个目标来实现的。

RPC 结构

Nelson 的论文中指出实现 RPC 的程序包括 5 个部分:

1. User
2. User-stub
3. RPCRuntime
4. Server-stub
5. Server

这 5 个部分的关系如下图所示

这里 user 就是 client 端,当 user 想发起一个远程调用时,它实际是通过本地调用 user-stub。user-stub 负责将调用的接口、方法和参数通过约定的协议规范进行编码并通过本地的 RPCRuntime 实例传输到远端的实例。远端 RPCRuntime 实例收到请求后交给 server-stub 进行解码后发起本地端调用,调用结果再返回给 user 端。

RPC 实现

Nelson 论文中给出的这个实现结构也成为后来大家参考的标准范本。大约 10 年前,我最早接触分布式计算时使用的 CORBAR 实现结构基本与此类似。CORBAR 为了解决异构平台的 RPC,使用了 IDL(Interface Definition Language)来定义远程接口,并将其映射到特定的平台语言中。后来大部分的跨语言平台 RPC 基本都采用了此类方式,比如我们熟悉的 Web Service(SOAP),近年开源的 Thrift 等。他们大部分都通过 IDL 定义,并提供工具来映射生成不同语言平台的 user-stub 和 server-stub,并通过框架库来提供 RPCRuntime 的支持。不过貌似每个不同的 RPC 框架都定义了各自不同的 IDL 格式,导致程序员的学习成本进一步上升(苦逼啊),Web Service 尝试建立业界标准,无赖标准规范复杂而效率偏低,否则 Thrift 等更高效的 RPC 框架就没必要出现了。

IDL 是为了跨平台语言实现 RPC 不得已的选择,要解决更广泛的问题自然导致了更复杂的方案。而对于同一平台内的 RPC 而言显然没必要搞个中间语言出来,例如 java 原生的 RMI,这样对于 java 程序员而言显得更直接简单,降低使用的学习成本。

发表在 rpc | 留下评论

django 日志logging的配置以及处理

http://davidbj.blog.51cto.com/4159484/1433741/

发表在 django | 留下评论

golang : net/http从启动接收到request到返回response

package main

import (
	"fmt"
	"log"
	"net/http"
	"strings"
)

func sayhelloName(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()       //解析参数,默认是不会解析的
	fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息
	fmt.Println("path", r.URL.Path)
	fmt.Println("scheme", r.URL.Scheme)
	fmt.Println(r.Form["url_long"])
	for k, v := range r.Form {
		fmt.Println("key:", k)
		fmt.Println("val:", strings.Join(v, ""))
	}
	fmt.Fprintf(w, "Hello astaxie!") //这个写入到 w 的是输出到客户端的
}
func main() {
	http.HandleFunc("/", sayhelloName)       //设置访问的路由,设置到DefaultServeMux
	err := http.ListenAndServe(":9090", nil) //设置监听的端口
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

//net/http/server.go

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

//Server结构体

type Server struct {
    Addr           string        // TCP address to listen on, ":http" if empty
    Handler        Handler       // handler to invoke, http.DefaultServeMux if nil
    ReadTimeout    time.Duration // maximum duration before timing out read of the request
    WriteTimeout   time.Duration // maximum duration before timing out write of the response
    MaxHeaderBytes int           // maximum size of request headers, DefaultMaxHeaderBytes if 0
    TLSConfig      *tls.Config   // optional TLS config, used by ListenAndServeTLS
    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
    ConnState func(net.Conn, ConnState)
    ErrorLog *log.Logger
    disableKeepAlives int32     // accessed atomically.
    nextProtoOnce     sync.Once // guards initialization of TLSNextProto in Serve
    nextProtoErr      error
}

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

//监听端口

func (srv *Server) ListenAndServe() error {
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

type tcpKeepAliveListener struct {
    *net.TCPListener
}

//net/dial.go

func Listen(net, laddr string) (Listener, error) {
    addrs, err := resolveAddrList("listen", net, laddr, noDeadline)
    if err != nil {
        return nil, &OpError{Op: "listen", Net: net, Source: nil, Addr: nil, Err: err}
    }   
    var l Listener
    switch la := addrs.first(isIPv4).(type) {
    case *TCPAddr:
        l, err = ListenTCP(net, la) 
    case *UnixAddr:
        l, err = ListenUnix(net, la) 
    default:
        return nil, &OpError{Op: "listen", Net: net, Source: nil, Addr: la, Err: &AddrError{Err: "unexpected address type", Addr: laddr}} 
    }   
    if err != nil {
        return nil, err // l is non-nil interface containing nil pointer
    }   
    return l, nil 
}
//net/http/server.go

//接受客户端连接,生成一个连接,并起一个goroutine为这个新连接服务
func (srv *Server) Serve(l net.Listener) error {
    defer l.Close()
    if fn := testHookServerServe; fn != nil {
        fn(srv, l)
    }    
    var tempDelay time.Duration // how long to sleep on accept failure
    if err := srv.setupHTTP2(); err != nil {
        return err
    }    
    for {
        rw, e := l.Accept()
        if e != nil {
            if ne, ok := e.(net.Error); ok && ne.Temporary() {
                if tempDelay == 0 {
                    tempDelay = 5 * time.Millisecond
                } else {
                    tempDelay *= 2 
                }    
                if max := 1 * time.Second; tempDelay > max {
                    tempDelay = max
                }    
                srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
                time.Sleep(tempDelay)
                continue
            }    
            return e
        }    
        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve()
    }    
}
func (srv *Server) newConn(rwc net.Conn) *conn {
    c := &conn{
        server: srv,
        rwc:    rwc,
    }
    if debugServerConnections {
        c.rwc = newLoggingConn("server", c.rwc)
    }
    return c
}

type conn struct {
    server *Server
    rwc net.Conn
    remoteAddr string
    tlsState *tls.ConnectionState
    werr error
    r *connReader
    bufr *bufio.Reader
    bufw *bufio.Writer
    lastMethod string
    // mu guards hijackedv, use of bufr, (*response).closeNotifyCh.
    mu sync.Mutex
    hijackedv bool
}

//读取请求,查找相应的handler处理
func (c *conn) serve() {
	c.remoteAddr = c.rwc.RemoteAddr().String()
	defer func() {
		if err := recover(); err != nil {
			const size = 64 << 10
			buf := make([]byte, size)
			buf = buf[:runtime.Stack(buf, false)]
			c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
		}
		if !c.hijacked() {
			c.close()
			c.setState(c.rwc, StateClosed)
		}
	}()

	if tlsConn, ok := c.rwc.(*tls.Conn); ok {
		if d := c.server.ReadTimeout; d != 0 {
			c.rwc.SetReadDeadline(time.Now().Add(d))
		}
		if d := c.server.WriteTimeout; d != 0 {
			c.rwc.SetWriteDeadline(time.Now().Add(d))
		}
		if err := tlsConn.Handshake(); err != nil {
			c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err)
			return
		}
		c.tlsState = new(tls.ConnectionState)
		*c.tlsState = tlsConn.ConnectionState()
		if proto := c.tlsState.NegotiatedProtocol; validNPN(proto) {
			if fn := c.server.TLSNextProto[proto]; fn != nil {
				h := initNPNRequest{tlsConn, serverHandler{c.server}}
				fn(c.server, tlsConn, h)
			}
			return
		}
	}

	c.r = &connReader{r: c.rwc}
	c.bufr = newBufioReader(c.r)
	c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4096)

	for {
		w, err := c.readRequest()
		if c.r.remain != c.server.initialReadLimitSize() {
			// If we read any bytes off the wire, we're active.
			c.setState(c.rwc, StateActive)
		}
		if err != nil {
			if err == errTooLarge {
				// Their HTTP client may or may not be
				// able to read this if we're
				// responding to them and hanging up
				// while they're still writing their
				// request.  Undefined behavior.
				io.WriteString(c.rwc, "HTTP/1.1 431 Request Header Fields Too Large\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n431 Request Header Fields Too Large")
				c.closeWriteAndWait()
				return
			}
			if err == io.EOF {
				return // don't reply
			}
			if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
				return // don't reply
			}
			var publicErr string
			if v, ok := err.(badRequestError); ok {
				publicErr = ": " + string(v)
			}
			io.WriteString(c.rwc, "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n400 Bad Request"+publicErr)
			return
		}

		// Expect 100 Continue support
		req := w.req
		if req.expectsContinue() {
			if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
				// Wrap the Body reader with one that replies on the connection
				req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
			}
		} else if req.Header.get("Expect") != "" {
			w.sendExpectationFailed()
			return
		}

		// HTTP cannot have multiple simultaneous active requests.[*]
		// Until the server replies to this request, it can't read another,
		// so we might as well run the handler in this goroutine.
		// [*] Not strictly true: HTTP pipelining.  We could let them all process
		// in parallel even if their responses need to be serialized.
		serverHandler{c.server}.ServeHTTP(w, w.req)
		if c.hijacked() {
			return
		}
		w.finishRequest()
		if !w.shouldReuseConnection() {
			if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
				c.closeWriteAndWait()
			}
			return
		}
		c.setState(c.rwc, StateIdle)
	}
}

type serverHandler struct {
    srv *Server
}

//为http请求服务,如果在Server中设置Handler,则使用DefaultServeMux提供服务

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    }    
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }    
    handler.ServeHTTP(rw, req) 
}


// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = NewServeMux()

// NewServeMux allocates and returns a new ServeMux.
func NewServeMux() *ServeMux { return &ServeMux{m: make(map[string]muxEntry)} }

type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry
    hosts bool // whether any patterns contain hostnames
}

type muxEntry struct {
    explicit bool
    h        Handler
    pattern  string
}

// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
//查找相应的handler,并调用handler的ServeHTTP方法
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
        if r.ProtoAtLeast(1, 1) {
            w.Header().Set("Connection", "close")
        }
        w.WriteHeader(StatusBadRequest)
        return
    }
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
    if r.Method != "CONNECT" {
        if p := cleanPath(r.URL.Path); p != r.URL.Path {
            _, pattern = mux.handler(r.Host, p)
            url := *r.URL
            url.Path = p
            return RedirectHandler(url.String(), StatusMovedPermanently), pattern
        }
    }

    return mux.handler(r.Host, r.URL.Path)
}


type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

//根据host和path查找 handler,每个handler都实现了ServeHTTP方法:通过HandlerFunc转换
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
    mux.mu.RLock()
    defer mux.mu.RUnlock()

    // Host-specific pattern takes precedence over generic ones
    if mux.hosts {
        h, pattern = mux.match(host + path)
    }
    if h == nil {
        h, pattern = mux.match(path)
    }
    if h == nil {
        h, pattern = NotFoundHandler(), ""
    }
    return
}
// Find a handler on a handler map given a path string
// Most-specific (longest) pattern wins
//查找与路径匹配的Handle
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    var n = 0
    for k, v := range mux.m {
        if !pathMatch(k, path) {
            continue
        }
        if h == nil || len(k) > n {
            n = len(k)
            h = v.h
            pattern = v.pattern
        }
    }
    return
}

// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock()
    defer mux.mu.Unlock()

    if pattern == "" {
        panic("http: invalid pattern " + pattern)
    }
    if handler == nil {
        panic("http: nil handler")
    }
    if mux.m[pattern].explicit {
        panic("http: multiple registrations for " + pattern)
    }

    mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}

    if pattern[0] != '/' {
        mux.hosts = true
    }

    // Helpful behavior:
    // If pattern is /tree/, insert an implicit permanent redirect for /tree.
    // It can be overridden by an explicit registration.
    n := len(pattern)
    if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
        // If pattern contains a host name, strip it and use remaining
        // path for redirect.
        path := pattern
        if pattern[0] != '/' {
            // In pattern, at least the last character is a '/', so
            // strings.Index can't be -1.
            path = pattern[strings.Index(pattern, "/"):]
        }
        url := &url.URL{Path: path}
        mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(url.String(), StatusMovedPermanently), pattern: pattern}
    }
}

func (c *conn) readRequest() (w *response, err error) {
	if c.hijacked() {
		return nil, ErrHijacked
	}

	if d := c.server.ReadTimeout; d != 0 {
		c.rwc.SetReadDeadline(time.Now().Add(d))
	}
	if d := c.server.WriteTimeout; d != 0 {
		defer func() {
			c.rwc.SetWriteDeadline(time.Now().Add(d))
		}()
	}

	c.r.setReadLimit(c.server.initialReadLimitSize())
	c.mu.Lock() // while using bufr
	if c.lastMethod == "POST" {
		// RFC 2616 section 4.1 tolerance for old buggy clients.
		peek, _ := c.bufr.Peek(4) // ReadRequest will get err below
		c.bufr.Discard(numLeadingCRorLF(peek))
	}
	req, err := readRequest(c.bufr, keepHostHeader)
	c.mu.Unlock()
	if err != nil {
		if c.r.hitReadLimit() {
			return nil, errTooLarge
		}
		return nil, err
	}
	c.lastMethod = req.Method
	c.r.setInfiniteReadLimit()

	hosts, haveHost := req.Header["Host"]
	if req.ProtoAtLeast(1, 1) && (!haveHost || len(hosts) == 0) {
		return nil, badRequestError("missing required Host header")
	}
	if len(hosts) > 1 {
		return nil, badRequestError("too many Host headers")
	}
	if len(hosts) == 1 && !validHostHeader(hosts[0]) {
		return nil, badRequestError("malformed Host header")
	}
	for k, vv := range req.Header {
		if !validHeaderName(k) {
			return nil, badRequestError("invalid header name")
		}
		for _, v := range vv {
			if !validHeaderValue(v) {
				return nil, badRequestError("invalid header value")
			}
		}
	}
	delete(req.Header, "Host")

	req.RemoteAddr = c.remoteAddr
	req.TLS = c.tlsState
	if body, ok := req.Body.(*body); ok {
		body.doEarlyClose = true
	}

	w = &response{
		conn:          c,
		req:           req,
		reqBody:       req.Body,
		handlerHeader: make(Header),
		contentLength: -1,
	}
	w.cw.res = w
	w.w = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize)
	return w, nil
}

发表在 golang | 留下评论

GO类型断言

(1)语法:

<目标类型的值>,<布尔参数> := <表达式>.( 目标类型 ) // 安全类型断言

<目标类型的值> := <表达式>.( 目标类型 )  //非安全类型断言

(2)类型断言的本质,跟类型转换类似,都是类型之间进行转换,不同之处在于,类型断言是在接口之间进行,相当于Java中,对于一个对象,把一种接口的引用转换成另一种。

我们先来看一个最简单的错误的类型断言:

代码如下:

func test6() {

var i interface{} = "kk"

j := i.(int)

fmt.Printf("%T->%d\n", j, j)

}

 

var i interface{} = "KK" 某种程度上相当于java中的,Object i = "KK";

 

现在把这个 i 转换成 int 类型,系统内部检测到这种不匹配,就会调用内置的panic()函数,抛出一个异常。

改一下,把 i 的定义改为:var i interface{} = 99,就没问题了。输出为:

代码如下:

int->99

 

以上是不安全的类型断言。我们来看一下安全的类型断言:

代码如下:

func test6() {

var i interface{} = "TT"

j, b := i.(int)

if b {

fmt.Printf("%T->%d\n", j, j)

} else {

fmt.Println("类型不匹配")

}

}

 

输出“类型不匹配”。

发表在 golang | 留下评论

分享一个精简高效的Golang TCP Framework

利用这个包可以快速的搭建一个高效的tcp服务器 https://github.com/gansidui/gotcp

使用 LTV协议(length + type + data)

完全异步处理

单机数十万以上的连接完全没问题。

接口简单,只需要传入回调的接口以及配置 即可。

package main
import (
"fmt"
"github.com/gansidui/gotcp"
"log"
"net"
"os"
"os/signal"
"runtime"
"syscall"
"time"
)

type ConnDelegate struct{}

func (this *ConnDelegate) OnConnect(c *gotcp.Conn) bool {
fmt.Println("OnConnect")
return true
}

func (this *ConnDelegate) OnMessage(c *gotcp.Conn, p *gotcp.Packet) bool {
fmt.Println("OnMessage:", p.GetLen(), p.GetType(), string(p.GetData()))
return true
}

func (this *ConnDelegate) OnClose(c *gotcp.Conn) {
fmt.Println("OnClose")
}

func (this *ConnDelegate) OnIOError(c *gotcp.Conn, err error) {
fmt.Println("OnIOError:", err)
}

func main() {
runtime.GOMAXPROCS(runtime.NumCPU())

tcpAddr, err := net.ResolveTCPAddr("tcp4", "127.0.0.1:8989")
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)

config := &gotcp.Config{
AcceptTimeout: 5 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
MaxPacketLength: int32(2048),
SendPacketChanLimit: int32(10),
ReceivePacketChanLimit: int32(10),
}
delegate := &ConnDelegate{}

svr := gotcp.NewServer(config, delegate)
go svr.Start(listener)

ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
log.Printf("Signal: %v\r\n", <-ch)

svr.Stop()
}

func checkError(err error) {
if err != nil {
log.Fatal(err)
}
}
FROM : http://www.golanghome.com/post/468
发表在 gotcp | 留下评论

golang sync WaitGroup

WaitGroup的用途:它能够一直等到所有的goroutine执行完成,并且阻塞主线程的执行,直到所有的goroutine执行完成。

WaitGroup总共有三个方法:Add(delta int),Done(),Wait()。简单的说一下这三个方法的作用。

Add:添加或者减少等待goroutine的数量

Done:相当于Add(-1)

Wait:执行阻塞,直到所有的WaitGroup数量变成0

例子:

 

package main

 

import (

"fmt"

"sync"

"time"

)

 

func main() {

var wg sync.WaitGroup

 

for i := 0; i < 5; i = i + 1 {

wg.Add(1)

go func(n int) {

// defer wg.Done()

defer wg.Add(-1)

EchoNumber(n)

}(i)

}

 

wg.Wait()

}

 

func EchoNumber(i int) {

time.Sleep(3e9)

fmt.Println(i)

}

 

输出结果:

 

 

0

1

2

3

4

 

程序很简单,只是将每次循环的数量过3秒钟输出。那么,这个程序如果不用WaitGroup,那么将看不见输出结果。因为goroutine还没执行完,主线程已经执行完毕。注释的defer wg.Done()和defer wg.Add(-1)作用一样。这个很好,原来执行脚本,都是使用time.Sleep,用一个估计的时间等到子线程执行完。WaitGroup很好。虽然chanel也能实现,但是觉得如果涉及不到子线程与主线程数据同步,这个感觉不错。

发表在 golang | 留下评论

Go 语言编写的缓存及缓存过滤库:groupcache

FROM : http://studygolang.com/wr?u=http%3a%2f%2fblog.csdn.net%2fsongbohr%2farticle%2fdetails%2f16349989

groupcache 是使用 Go 语言编写的缓存及缓存过滤库,作为 memcached 许多场景下的替代版本。 对比原始 memcached

 

 

GROUPCACHE简单介绍

 

 

groupcache 是使用 Go 语言编写的缓存及缓存过滤库,作为 memcached 许多场景下的替代版本。 对比原始 memcached

 

 

[plain] view plain copy

 

print?

  1. Cache Results
  2. function get_foo(foo_id)
  3.     foo = memcached_get("foo:" . foo_id)
  4.     return foo if defined foo
  5.     foo = fetch_foo_from_database(foo_id)
  6.     memcached_set("foo:" . foo_id, foo)
  7.     return foo
  8. end

 

首先,groupcache 与 memcached 的相似之处:通过 key 分片,并且通过 key 来查询响应的 peer。

 

其次,groupcache 与 memcached 的不同之处:

1. 不需要对服务器进行单独的设置,这将大幅度减少部署和配置的工作量。groupcache 既是客户端库也是服务器库,并连接到自己的 peer 上。

2. 具有缓存过滤机制。众所周知,在 memcached 出现“Sorry,cache miss(缓存丢失)”时,经常会因为不受控制用户数量的请求而导致数据库(或者其它组件)产生“惊群效应(thundering herd)”;groupcache 会协调缓存填充,只会将重复调用中的一个放于缓存,而处理结果将发送给所有相同的调用者。

3. 不支持多个版本的值。如果“foo”键对应的值是“bar”,那么键“foo”的值永远都是“bar”。这里既没有缓存的有效期,也没有明确的缓存回收机制,因此同样也没有 CAS 或者 Increment/Decrement。

4. 基于上一点的改变,groupcache 就具备了自动备份“超热”项进行多重处理,这就避免了 memcached 中对某些键值过量访问而造成所在机器 CPU 或者 NIC 过载。

01 me := "http://10.0.0.1"
02 peers := groupcache.NewHTTPPool(me)
03  
04 // Whenever peers change:
05 peers.Set("http://10.0.0.1", "http://10.0.0.2", "http://10.0.0.3")
06  
07 var thumbNails = groupcache.NewGroup("thumbnail", 64<<20, groupcache.GetterFunc(
08     func(ctx groupcache.Context, key string, dest groupcache.Sink) error {
09         fileName := key
10         dest.SetBytes(generateThumbnail(fileName))
11         return nil
12     }))
13  
14 var data []byte
15 err := thumbNails.Get(ctx, "big-file.jpg",
16     groupcache.AllocatingByteSliceSink(&data))
17 // ...
18 http.ServeContent(w, r, "big-file-thumb.jpg", modTime, bytes.NewReader(data))

 

 

GROUPCACHE简单评论

groupcache  是  Brad Fitzpatrick  最新的作品(之前最有名的两个作品是  memcached  与OpenID 1 ),目标在于取代一部分memcached的功能。

 

以官方的说明是:

groupcache is a caching and cache-filling library, intended as a replacement for memcached in many cases.

另外一篇介绍文是「Playing With Groupcache」。

跟memcached 差异最大的地方在于「没有更改与删除的功能」,一旦写进去后就不会变动。在放弃update/delete 的特性后,换来的是:

  • Cluster 的能力。
  • 处理热点的能力。

以往在memcached server 之间是没有交集的,在groupcache 则是cluster 起来。另外以前在memcached 会因为同时存取同一个key 而造成single CPU overloading 的问题,在groupcache 则透过auto-mirror 机制解决。

不过还是得想一下要怎么用,毕竟没有update/delete 功能…

 

 

 

GroupCache简单使用

 

view source

 

print

?

1 package main
2  
3 import (
4     "fmt"
5     groupcache "github.com/golang/groupcache"
6     "log"
7     "net/http"
8     "os"
9     "strings"
10 )
11  
12 func main() {
13     // Usage: ./test_groupcache port
14     me := ":" + os.Args[1]
15     peers := groupcache.NewHTTPPool("http://localhost" + me)
16     peers.Set("http://localhost:8081", "http://localhost:8082", "http://localhost:8083")
17  
18     helloworld := groupcache.NewGroup("helloworld", 1024*1024*1024*16, groupcache.GetterFunc(
19         func(ctx groupcache.Context, key string, dest groupcache.Sink) error {
20             log.Println(me)
21             dest.SetString(me)
22             return nil
23     }))
24  
25     fmt.Println("GroupName: ", helloworld.Name())
26     http.HandleFunc("/xbox/", func(w http.ResponseWriter, r *http.Request) {
27         parts := strings.SplitN(r.URL.Path[len("/xbox/"):], "/", 1)
28         if len(parts) != 1 {
29             http.Error(w, "Bad Request", http.StatusBadRequest)
30             return
31         }
32         var data []byte
33         helloworld.Get(nil, parts[0], groupcache.AllocatingByteSliceSink(&data))
34         w.Write(data)
35         log.Println("Gets: ", helloworld.Stats.Gets.String())
36         log.Println("Load: ", helloworld.Stats.Loads.String())
37         log.Println("LocalLoad: ", helloworld.Stats.LocalLoads.String())
38         log.Println("PeerError: ", helloworld.Stats.PeerErrors.String())
39         log.Println("PeerLoad: ", helloworld.Stats.PeerLoads.String())
40     })
41  
42     http.ListenAndServe(me, nil)
43 }

 

 

 

GroupCache源码分析

概述

  • GitHub: https://github.com/golang/groupcache.git
  • memcached作者Brad Fitzpatrick用Go语言实现
  • 分布式缓存库
  • 数据无版本概念, 也就是一个key对应的value是不变的,并没有update
  • 节点之间可互访,自动复制热点数据

实现

LRU

type Key interface{}

 

type entry struct {

key    Key

value interface{}

}

 

type Cache Struct {

MaxEntries int

OnEvicted func(key Key, value interface{})

ll    *list.List

cache map[interface{}]*list.Element

}

 

不懂Go语言或者对Go语言不熟悉的同学,可以和我一起来顺带学习Go语言 首先是Go语言将变量的类型放置于变量名之后上面的key变量,

它的类型是Key,然后需要说明的是interface{},在Go中它可以指向任意对象,也就任意对象都是先这个接口,你可以把它看成Java/C#中的

Object, C/C++中的void*, 然后我们来看Cache,其一个字段MaxEntires表示Cache中最大可以有的KV数,OnEvicted是一个回调函数,

在触发淘汰时调用,ll是一个链表,cache是一个map,key为interface{}, value为list.Element其指向的类型是entry类型,通过数据

结构其实我们已经能够猜出LRU的实现了,没有错实现就是最基本的,Get一次放到list头,每次Add的时候判断是否满,满则淘汰掉list尾的数据

// 创建一个Cache

func New(maxEntries int) *Cache

// 向Cache中插入一个KV

func (c *Cache) Add(key Key, value interface{})

// 从Cache中获取一个key对应的value

func (c *Cache) Get(key Key) (value interface{}, ok bool)

// 从Cache中删除一个key

func (c *Cache) Remove(key Key)

// 从Cache中删除最久未被访问的数据

func (c *Cache) RemoveOldest()

// 获取当前Cache中的元素个数

func (c *Cache) Len()

这里我们看到了Go中如何实现一个类的成员方法,在func之后加类,这种写法和Go语言很意思的一个东西有关系,在Go中接口的实现并不是和Java中那样

子,而是只要某个类只要实现了某个接口的所有方法,即可认为该类实现了该接口,类似的比如说在java中有2个接口名字不同,即使方法相同也是不一样

的,而Go里面认为这是一样的。另外Go中开头字母大写的变量名,类名,方法名表示对外可知,小写开头的表示对外不暴露。另外类实这种代码

ele.Value.(*entry).value,其中(*entry)表示将Value转成*entry类型访问

Singleflight

这个包主要实现了一个可合并操作的接口,代码也没几行

// 回调函数接口

type call struct {

// 可以将其认为这是类似java的CountdownLatch的东西

wg  sync.WaitGroup

// 回调函数

val interface{}

// error

err error

}

 

//注意这个Group是singleflight下的

type Group struct {

// 保护m的锁

mu sync.Mutex       // protects m

// key/call 映射表

m  map[string]*call // lazily initialized

}

另外有一点要说明的是Go使用开头字母大小写判断是否外部可见,大写外部可见,小写外部不可见,比如上面的Group在外部实用singleflght包

是可以访问到的,而call不能,真不理解为啥不用个export啥的关键字,唉

// 这个就是这个包的主要接口,用于向其他节点发送查询请求时,合并相同key的请求,减少热点可能带来的麻烦

// 比如说我请求key="123"的数据,在没有返回的时候又有很多相同key的请求,而此时后面的没有必要发,只要

// 等待第一次返回的结果即可

func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {

// 首先获取group锁

g.mu.Lock()

// 映射表不存在就创建1个

if g.m == nil {

g.m = make(map[string]*call)

}

// 判断要查询某个key的请求是否已经在处理了

if c, ok := g.m[key]; ok {

// 已经在处理了 就释放group锁 并wait在wg上,我们看到wg加1是在某个时间段内第一次请求时加的

// 并且在完成fn操作返回后会wg done,那时wait等待就会返回,直接返回第一次请求的结果

g.mu.Unlock()

c.wg.Wait()

return c.val, c.err

}

// 创建回调,wg加1,把回调存到m中表示已经有在请求了,释放锁

c := new(call)

c.wg.Add(1)

g.m[key] = c

g.mu.Unlock()

 

// 执行fn,释放wg

c.val, c.err = fn()

c.wg.Done()

 

// 加锁将请求从m中删除,表示请求已经做好了

g.mu.Lock()

delete(g.m, key)

g.mu.Unlock()

 

return c.val, c.err

}

ByteView

一个不可变byte视图,其实就包装了一下byte数组和string,一个为null,就用另外一个

type ByteView struct {

// If b is non-nil, b is used, else s is used.

b []byte

s string

}

提供的接口也很简单:

// 返回长度

func (v ByteView) Len() int

// 按[]byte返回一个拷贝

func (v ByteView) ByteSlice() []byte

// 按string返回一个拷贝

func (v ByteView) String() string {

// 返回第i个byte

func (v ByteView) At(i int) byte

// 返回ByteView的某个片断,不拷贝

func (v ByteView) Slice(from, to int) ByteView

// 返回ByteView的从某个位置开始的片断,不拷贝

func (v ByteView) SliceFrom(from int) ByteView

// 将ByteView按[]byte拷贝出来

func (v ByteView) Copy(dest []byte) int

// 判断2个ByteView是否相等

func (v ByteView) Equal(b2 ByteView) bool

// 判断ByteView是否和string相等

func (v ByteView) EqualString(s string) bool

// 判断ByteView是否和[]byte相等

func (v ByteView) EqualBytes(b2 []byte) bool

// 对ByteView创建一个io.ReadSeeker

func (v ByteView) Reader() io.ReadSeeker

// 读取从off开始的后面的数据,其实下面调用的SliceFrom,这是封装成了io.Reader的一个ReadAt方法的形式

func (v ByteView) ReadAt(p []byte, off int64) (n int, err error) {

Sink

不太好表达这个是较个啥,举个例子,比如说我调用一个方法返回需要是一个[]byte,而方法的实现我并不知道,可以它内部产生的是一个string,

方法也不知道调用的它的实需要是啥,于是我们就可以在调用方这边使用这边使用继承了Sink的类,而方法内部只要调用Sink的方法进行传值

type Sink interface {

// SetString sets the value to s.

SetString(s string) error

 

// SetBytes sets the value to the contents of v.

// The caller retains ownership of v.

SetBytes(v []byte) error

 

// SetProto sets the value to the encoded version of m.

// The caller retains ownership of m.

SetProto(m proto.Message) error

 

// view returns a frozen view of the bytes for caching.

view() (ByteView, error)

}

 

// 使用ByteView设置Sink,间接调用SetString,SetBytes等

func setSinkView(s Sink, v ByteView) error

这里要说明一点,Go语言比较有意思的是,类是否实现了某个接口是外挂式的,也就是不要去再类定义的时候写类似implenents之类的东西,只要类方法

中包括了所有接口的方法,就说明这个类是实现了某个接口

这里实现Sink的有5个类:

stringSink

返回值是string的Sink

type stringSink struct {

sp *string

v  ByteView

}

 

// 创建一个stringSink, 传入的是一个string的指针

func StringSink(sp *string) Sink {

return &stringSink{sp: sp}

}

byteViewSink

返回值是byteView的Sink

type byteViewSink struct {

dst *ByteView

}

 

// 创建一个byteViewSink,传入的是一个ByteView指针

func ByteViewSink(dst *ByteView) Sink {

if dst == nil {

panic("nil dst")

}

return &byteViewSink{dst: dst}

}

protoSink

返回值是一个protobuffer message

type protoSink struct {

dst proto.Message // authorative value

typ string

 

v ByteView // encoded

}

 

// 创建一个protoSink,传入的是一个proto.Messages

func ProtoSink(m proto.Message) Sink {

return &protoSink{

dst: m,

}

}

allocBytesSink

返回值是[]byte的Sink,每次Set都会重新分配内存

type allocBytesSink struct {

dst *[]byte

v   ByteView

}

 

// 创建一个allocBytesSink,传入的是一个数组分片指针

func AllocatingByteSliceSink(dst *[]byte) Sink {

return &allocBytesSink{dst: dst}

}

truncBytesSink

返回值是一个定长的[]byte的Sink,超过长度的会被截断,服用传入的空间

type truncBytesSink struct {

dst *[]byte

v   ByteView

}

 

// 创建一个truncBytesSink,传入的实一个数组分片指针

func TruncatingByteSliceSink(dst *[]byte) Sink {

return &truncBytesSink{dst: dst}

}

另外这些Sink类,都是包外不可见的,但是创建函数和Sink接口可见的,这样子在对于使用上来说,只有接口操作不会有另外的东西,对外简单清晰

HTTPPool

一个HTTPPool提供了groupcache节点之间的方式以及节点的选择的实现,但是其对于groupcache.Group是透明的,Group使用的HTTPPool

实现的1个接口PeerPicker以及httpGetter实现的接口ProtoGetter

// groupcache提供了一个节点互相访问访问的类

type httpGetter struct {

transport func(Context) http.RoundTripper

baseURL   string

}

 

// 协议为GET http://example.com/groupname/key

// response见groupcache.proto,含有2个可选项分别为[]byte和double

// 实现默认使用go自带的net/http包直接发送请求

func (h *httpGetter) Get(context Context, in *pb.GetRequest, out *pb.GetResponse) error

HttpPool实现比较简单都是直接使用Go内部自带的一些包

// 创建一个HttpPool, 只能被使用一次,主要是注册PeerPicker,以及初始化http服务

func NewHTTPPool(self string) *HTTPPool

 

// 设置groupcache集群的节点列表

func (p *HTTPPool) Set(peers ...string)

 

// 提供按key选取节点,按key作hash,但是这段代码在OS为32bit是存在bug,如果算出来的hashcode正好是-1 * 2^31时

// 会导致out of range,为啥会有这个bug看看代码你就会发现了,作者忘了-1 * 2^31 <= int32 <= 1 * 2^31 -1

func (p *HTTPPool) PickPeer(key string) (ProtoGetter, bool)

 

// http服务处理函数,主要是按http://example.com/groupname/key解析请求,调用group.Get,按协议返回请求

func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request)

Cache

Cache的实现很简单基本可以认为就是直接在LRU上面包了一层,加上了统计信息,锁,以及大小限制

type cache struct {

mu         sync.RWMutex

nbytes     int64 // of all keys and values

lru        *lru.Cache

nhit, nget int64

nevict     int64 // number of evictions

}

 

// 返回统计信息,加读锁

func (c *cache) stats() CacheStats

// 加入一个kv,如果cache的大小超过nbytes了,就淘汰

func (c *cache) add(key string, value ByteView)

// 根据key返回value

func (c *cache) get(key string) (value ByteView, ok bool)

// 删除最久没被访问的数据

func (c *cache) removeOldest()

// 获取当前cache的大小

func (c *cache) bytes() int64

// 获取当前cache中kv的个数,内部会加读锁

func (c *cache) items() int64

// 获取当前cache中kv的个数,函数名中的Locked的意思是调用方已经加锁,比如上面的stats方法

func (c *cache) itemsLocked() int64

Group

该类是GroupCache的核心类,所有的其他包都是为该类服务的

type Group struct {

// group名,可以理解为namespace的名字,group其实就是一个namespace

name       string

// 由调用方传入的回调,用于groupcache访问后端数据

// type Getter interface {

//     Get(ctx Context, key string, dest Sink) error

// }

getter     Getter

// 通过该对象可以达到只调用一次

peersOnce  sync.Once

// 用于访问groupcache中其他节点的接口,比如上面的HTTPPool实现了该接口

peers      PeerPicker

// cache的总大小

cacheBytes int64

 

// 从本节点指向向后端获取到的数据存在在cache中

mainCache cache

 

// 从groupcache中其他节点上获取到的数据存在在该cache中,因为是用其他节点获取到的,也就是说这个数据存在多份,也就是所谓的hot

hotCache cache

// 用于向groupcache中其他节点访问时合并请求

loadGroup singleflight.Group

 

// 统计信息

Stats Stats

}

由上面的结构体我们可以看出来,groupcache支持namespace概念,不同的namespace有自己的配额以及cache,不同group之间cache是独立的

也就是不能存在某个group的行为影响到另外一个namespace的情况 groupcache的每个节点的cache分为2层,由本节点直接访问后端的

存在maincache,其他存在在hotcache

// 根据name从全局变量groups(一个string/group的map)查找对于group

func GetGroup(name string) *Group

// 创建一个新的group,如果已经存在该name的group将会抛出一个异常,go里面叫panic

func NewGroup(name string, cacheBytes int64, getter Getter) *Group

// 注册一个创建新group的钩子,比如将group打印出来等,全局只能注册一次,多次注册会触发panic

func RegisterNewGroupHook(fn func(*Group))

// 注册一个服务,该服务在NewGroup开始时被调用,并且只被调用一次

func RegisterServerStart(fn func())

下面重点介绍Get方法

func (g *Group) Get(ctx Context, key string, dest Sink) error

整个groupcache核心方法就这么一个,我们来看一下该方法是怎么运作的

func (g *Group) Get(ctx Context, key string, dest Sink) error {

// 初始化peers,全局初始化一次

g.peersOnce.Do(g.initPeers)

// 统计信息gets增1

g.Stats.Gets.Add(1)

if dest == nil {

return errors.New("groupcache: nil dest Sink")

}

// 查找本地是否存在该key,先查maincache,再查hotcache

value, cacheHit := g.lookupCache(key)

 

// 如果查到九hit增1,并返回

if cacheHit {

g.Stats.CacheHits.Add(1)

return setSinkView(dest, value)

}

 

// 加载该key到本地cache中,其中如果是本地直接请求后端得到的数据,并且是同一个时间段里第一个,

// 就不需要重新setSinkView了,在load中已经设置过了,destPopulated这个参数以来底的实现

destPopulated := false

value, destPopulated, err := g.load(ctx, key, dest)

if err != nil {

return err

}

if destPopulated {

return nil

}

return setSinkView(dest, value)

}

 

func (g *Group) load(ctx Context, key string, dest Sink) (value ByteView, destPopulated bool, err error) {

// 统计loads增1

g.Stats.Loads.Add(1)

// 使用singleflight来达到合并请求

viewi, err := g.loadGroup.Do(key, func() (interface{}, error) {

// 统计信息真正发送请求的次数增1

g.Stats.LoadsDeduped.Add(1)

var value ByteView

var err error

// 选取向哪个节点发送请求,比如HTTPPool中的PickPeer实现

if peer, ok := g.peers.PickPeer(key); ok {

// 从groupcache中其他节点获取数据,并将数据存入hotcache

value, err = g.getFromPeer(ctx, peer, key)

if err == nil {

g.Stats.PeerLoads.Add(1)

return value, nil

}

g.Stats.PeerErrors.Add(1)

}

// 如果选取的节点就是本节点或从其他节点获取失败,则由本节点去获取数据,也就是调用getter接口的Get方法

// 另外我们看到这里dest已经被赋值了,所以有destPopulated来表示已经赋值过不需要再赋值

value, err = g.getLocally(ctx, key, dest)

if err != nil {

g.Stats.LocalLoadErrs.Add(1)

return nil, err

}

g.Stats.LocalLoads.Add(1)

destPopulated = true

// 将获取的数据放入maincache

g.populateCache(key, value, &g.mainCache)

return value, nil

})

 

// 如果成功则返回

if err == nil {

value = viewi.(ByteView)

}

return

}

这里需要说明的是A向groupcache中其他节点B发送请求,此时B是调用Get方法,然后如果本地不存在则也会走load,但是不同的是

PickPeer会发现是本身节点(HTTPPOOL的实现),然后就会走getLocally,会将数据在B的maincache中填充一份,也就是说如果

是向groupcache中其他节点发请求的,会一下子在groupcache集群内存2分数据,一份在B的maincache里,一份在A的hotcache中,

这也就达到了自动复制,越是热点的数据越是在集群内份数多,也就达到了解决热点数据的问题

Cache Results

 

function get_foo(foo_id)

foo = memcached_get("foo:" . foo_id)

return foo if defined foo

 

foo = fetch_foo_from_database(foo_id)

memcached_set("foo:" . foo_id, foo)

return foo

end

 

首先,groupcache 与 memcached 的相似之处:通过 key 分片,并且通过 key 来查询响应的 peer。

 

其次,groupcache 与 memcached 的不同之处:

1. 不需要对服务器进行单独的设置,这将大幅度减少部署和配置的工作量。groupcache 既是客户端库也是服务器库,并连接到自己的 peer 上。

2. 具有缓存过滤机制。众所周知,在 memcached 出现“Sorry,cache miss(缓存丢失)”时,经常会因为不受控制用户数量的请求而导致数据库(或者其它组件)产生“惊群效应(thundering herd)”;groupcache 会协调缓存填充,只会将重复调用中的一个放于缓存,而处理结果将发送给所有相同的调用者。

3. 不支持多个版本的值。如果“foo”键对应的值是“bar”,那么键“foo”的值永远都是“bar”。这里既没有缓存的有效期,也没有明确的缓存回收机制,因此同样也没有 CAS 或者 Increment/Decrement。

4. 基于上一点的改变,groupcache 就具备了自动备份“超热”项进行多重处理,这就避免了 memcached 中对某些键值过量访问而造成所在机器 CPU 或者 NIC 过载。

 

 

 

 

01

 

 

me

 := "http://10.0.0.1"              

02

 

 

peers

 := groupcache.NewHTTPPool(me)              

03

 

 

               

04

 

 

//

 Whenever peers change:              

05

 

 

peers.Set("http://10.0.0.1", "http://10.0.0.2", "http://10.0.0.3")              

06

 

 

               

07

 

 

var

thumbNails = groupcache.NewGroup("thumbnail",

 64<<20, groupcache.GetterFunc(              

08

 

 

func(ctx

 groupcache.Context, key string, dest groupcache.Sink) error {              

09

 

 

fileName

 := key              

10

 

 

        dest.SetBytes(generateThumbnail(fileName))              

11

 

 

        return nil              

12

 

 

    }))              

13

 

 

               

14

 

 

var

 data []byte              

15

 

 

err

 := thumbNails.Get(ctx, "big-file.jpg",              

16

 

 

    groupcache.AllocatingByteSliceSink(&data))              

17

 

 

//

 ...              

18

 

 

http.ServeContent(w,

r, "big-file-thumb.jpg",

 modTime, bytes.NewReader(data))      

 

GROUPCACHE简单评论

groupcache  是  Brad Fitzpatrick  最新的作品(之前最有名的两个作品是  memcached  与OpenID 1 ),目标在于取代一部分memcached的功能。

 

以官方的说明是:

groupcache is a caching and cache-filling library, intended as a replacement for memcached in many cases.

另外一篇介绍文是「Playing With Groupcache」。

跟memcached 差异最大的地方在于「没有更改与删除的功能」,一旦写进去后就不会变动。在放弃update/delete 的特性后,换来的是:

  • Cluster 的能力。
  • 处理热点的能力。

以往在memcached server 之间是没有交集的,在groupcache 则是cluster 起来。另外以前在memcached 会因为同时存取同一个key 而造成single CPU overloading 的问题,在groupcache 则透过auto-mirror 机制解决。

不过还是得想一下要怎么用,毕竟没有update/delete 功能…

 

 

 

GroupCache简单使用

 

view source

 

print

?

 

 

 

 

1

 

 

package

 main      

 

 

 

 

2

 

 

       

 

 

 

 

3

 

 

import

 (      

 

 

 

 

4

 

 

    "fmt"      

 

 

 

 

5

 

 

    groupcache "github.com/golang/groupcache"      

 

 

 

 

6

 

 

    "log"      

 

 

 

 

7

 

 

    "net/http"      

 

 

 

 

8

 

 

    "os"      

 

 

 

 

9

 

 

    "strings"      

 

 

 

 

10

 

 

)      

 

 

 

 

11

 

 

       

 

 

 

 

12

 

 

func

 main() {      

 

 

 

 

13

 

 

//

 Usage: ./test_groupcache port      

 

 

 

 

14

 

 

me

:= ":" +

 os.Args[1]      

 

 

 

 

15

 

 

peers

:= groupcache.NewHTTPPool("http://localhost" +

 me)      

 

 

 

 

16

 

 

    peers.Set("http://localhost:8081", "http://localhost:8082", "http://localhost:8083")      

 

 

 

 

17

 

 

       

 

 

 

 

18

 

 

helloworld

:= groupcache.NewGroup("helloworld",

 1024*1024*1024*16, groupcache.GetterFunc(      

 

 

 

 

19

 

 

func(ctx

 groupcache.Context, key string, dest groupcache.Sink) error {      

 

 

 

 

20

 

 

            log.Println(me)      

 

 

 

 

21

 

 

            dest.SetString(me)      

 

 

 

 

22

 

 

            return nil      

 

 

 

 

23

 

 

    }))      

 

 

 

 

24

 

 

       

 

 

 

 

25

 

 

fmt.Println("GroupName:

",

 helloworld.Name())      

 

 

 

 

26

 

 

http.HandleFunc("/xbox/",

 func(w http.ResponseWriter, r *http.Request) {      

 

 

 

 

27

 

 

parts

:= strings.SplitN(r.URL.Path[len("/xbox/"):], "/",

 1)      

 

 

 

 

28

 

 

if len(parts)

 != 1 {      

 

 

 

 

29

 

 

http.Error(w, "Bad

Request",

 http.StatusBadRequest)      

 

 

 

 

30

 

 

            return      

 

 

 

 

31

 

 

        }      

 

 

 

 

32

 

 

var

 data []byte      

 

 

 

 

33

 

 

helloworld.Get(nil,

 parts[0], groupcache.AllocatingByteSliceSink(&data))      

 

 

 

 

34

 

 

        w.Write(data)      

 

 

 

 

35

 

 

log.Println("Gets:

",

 helloworld.Stats.Gets.String())      

 

 

 

 

36

 

 

log.Println("Load:

",

 helloworld.Stats.Loads.String())      

 

 

 

 

37

 

 

log.Println("LocalLoad:

",

 helloworld.Stats.LocalLoads.String())      

 

 

 

 

38

 

 

log.Println("PeerError:

",

 helloworld.Stats.PeerErrors.String())      

 

 

 

 

39

 

 

log.Println("PeerLoad:

",

 helloworld.Stats.PeerLoads.String())      

 

 

 

 

40

 

 

    })      

 

 

 

 

41

 

 

       

 

 

 

 

42

 

 

http.ListenAndServe(me,

 nil)      

 

 

 

 

43

 

 

}      

 

 

GroupCache源码分析

概述

  • GitHub: https://github.com/golang/groupcache.git
  • memcached作者Brad Fitzpatrick用Go语言实现
  • 分布式缓存库
  • 数据无版本概念, 也就是一个key对应的value是不变的,并没有update
  • 节点之间可互访,自动复制热点数据

实现

LRU

type Key interface{}

 

type entry struct {

key    Key

value interface{}

}

 

type Cache Struct {

MaxEntries int

OnEvicted func(key Key, value interface{})

ll    *list.List

cache map[interface{}]*list.Element

}

 

不懂Go语言或者对Go语言不熟悉的同学,可以和我一起来顺带学习Go语言 首先是Go语言将变量的类型放置于变量名之后上面的key变量,

它的类型是Key,然后需要说明的是interface{},在Go中它可以指向任意对象,也就任意对象都是先这个接口,你可以把它看成Java/C#中的

Object, C/C++中的void*, 然后我们来看Cache,其一个字段MaxEntires表示Cache中最大可以有的KV数,OnEvicted是一个回调函数,

在触发淘汰时调用,ll是一个链表,cache是一个map,key为interface{}, value为list.Element其指向的类型是entry类型,通过数据

结构其实我们已经能够猜出LRU的实现了,没有错实现就是最基本的,Get一次放到list头,每次Add的时候判断是否满,满则淘汰掉list尾的数据

// 创建一个Cache

func New(maxEntries int) *Cache

// 向Cache中插入一个KV

func (c *Cache) Add(key Key, value interface{})

// 从Cache中获取一个key对应的value

func (c *Cache) Get(key Key) (value interface{}, ok bool)

// 从Cache中删除一个key

func (c *Cache) Remove(key Key)

// 从Cache中删除最久未被访问的数据

func (c *Cache) RemoveOldest()

// 获取当前Cache中的元素个数

func (c *Cache) Len()

这里我们看到了Go中如何实现一个类的成员方法,在func之后加类,这种写法和Go语言很意思的一个东西有关系,在Go中接口的实现并不是和Java中那样

子,而是只要某个类只要实现了某个接口的所有方法,即可认为该类实现了该接口,类似的比如说在java中有2个接口名字不同,即使方法相同也是不一样

的,而Go里面认为这是一样的。另外Go中开头字母大写的变量名,类名,方法名表示对外可知,小写开头的表示对外不暴露。另外类实这种代码

ele.Value.(*entry).value,其中(*entry)表示将Value转成*entry类型访问

Singleflight

这个包主要实现了一个可合并操作的接口,代码也没几行

// 回调函数接口

type call struct {

// 可以将其认为这是类似java的CountdownLatch的东西

wg  sync.WaitGroup

// 回调函数

val interface{}

// error

err error

}

 

//注意这个Group是singleflight下的

type Group struct {

// 保护m的锁

mu sync.Mutex       // protects m

// key/call 映射表

m  map[string]*call // lazily initialized

}

另外有一点要说明的是Go使用开头字母大小写判断是否外部可见,大写外部可见,小写外部不可见,比如上面的Group在外部实用singleflght包

是可以访问到的,而call不能,真不理解为啥不用个export啥的关键字,唉

// 这个就是这个包的主要接口,用于向其他节点发送查询请求时,合并相同key的请求,减少热点可能带来的麻烦

// 比如说我请求key="123"的数据,在没有返回的时候又有很多相同key的请求,而此时后面的没有必要发,只要

// 等待第一次返回的结果即可

func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {

// 首先获取group锁

g.mu.Lock()

// 映射表不存在就创建1个

if g.m == nil {

g.m = make(map[string]*call)

}

// 判断要查询某个key的请求是否已经在处理了

if c, ok := g.m[key]; ok {

// 已经在处理了 就释放group锁 并wait在wg上,我们看到wg加1是在某个时间段内第一次请求时加的

// 并且在完成fn操作返回后会wg done,那时wait等待就会返回,直接返回第一次请求的结果

g.mu.Unlock()

c.wg.Wait()

return c.val, c.err

}

// 创建回调,wg加1,把回调存到m中表示已经有在请求了,释放锁

c := new(call)

c.wg.Add(1)

g.m[key] = c

g.mu.Unlock()

 

// 执行fn,释放wg

c.val, c.err = fn()

c.wg.Done()

 

// 加锁将请求从m中删除,表示请求已经做好了

g.mu.Lock()

delete(g.m, key)

g.mu.Unlock()

 

return c.val, c.err

}

ByteView

一个不可变byte视图,其实就包装了一下byte数组和string,一个为null,就用另外一个

type ByteView struct {

// If b is non-nil, b is used, else s is used.

b []byte

s string

}

提供的接口也很简单:

// 返回长度

func (v ByteView) Len() int

// 按[]byte返回一个拷贝

func (v ByteView) ByteSlice() []byte

// 按string返回一个拷贝

func (v ByteView) String() string {

// 返回第i个byte

func (v ByteView) At(i int) byte

// 返回ByteView的某个片断,不拷贝

func (v ByteView) Slice(from, to int) ByteView

// 返回ByteView的从某个位置开始的片断,不拷贝

func (v ByteView) SliceFrom(from int) ByteView

// 将ByteView按[]byte拷贝出来

func (v ByteView) Copy(dest []byte) int

// 判断2个ByteView是否相等

func (v ByteView) Equal(b2 ByteView) bool

// 判断ByteView是否和string相等

func (v ByteView) EqualString(s string) bool

// 判断ByteView是否和[]byte相等

func (v ByteView) EqualBytes(b2 []byte) bool

// 对ByteView创建一个io.ReadSeeker

func (v ByteView) Reader() io.ReadSeeker

// 读取从off开始的后面的数据,其实下面调用的SliceFrom,这是封装成了io.Reader的一个ReadAt方法的形式

func (v ByteView) ReadAt(p []byte, off int64) (n int, err error) {

Sink

不太好表达这个是较个啥,举个例子,比如说我调用一个方法返回需要是一个[]byte,而方法的实现我并不知道,可以它内部产生的是一个string,

方法也不知道调用的它的实需要是啥,于是我们就可以在调用方这边使用这边使用继承了Sink的类,而方法内部只要调用Sink的方法进行传值

type Sink interface {

// SetString sets the value to s.

SetString(s string) error

 

// SetBytes sets the value to the contents of v.

// The caller retains ownership of v.

SetBytes(v []byte) error

 

// SetProto sets the value to the encoded version of m.

// The caller retains ownership of m.

SetProto(m proto.Message) error

 

// view returns a frozen view of the bytes for caching.

view() (ByteView, error)

}

 

// 使用ByteView设置Sink,间接调用SetString,SetBytes等

func setSinkView(s Sink, v ByteView) error

这里要说明一点,Go语言比较有意思的是,类是否实现了某个接口是外挂式的,也就是不要去再类定义的时候写类似implenents之类的东西,只要类方法

中包括了所有接口的方法,就说明这个类是实现了某个接口

这里实现Sink的有5个类:

stringSink

返回值是string的Sink

type stringSink struct {

sp *string

v  ByteView

}

 

// 创建一个stringSink, 传入的是一个string的指针

func StringSink(sp *string) Sink {

return &stringSink{sp: sp}

}

byteViewSink

返回值是byteView的Sink

type byteViewSink struct {

dst *ByteView

}

 

// 创建一个byteViewSink,传入的是一个ByteView指针

func ByteViewSink(dst *ByteView) Sink {

if dst == nil {

panic("nil dst")

}

return &byteViewSink{dst: dst}

}

protoSink

返回值是一个protobuffer message

type protoSink struct {

dst proto.Message // authorative value

typ string

 

v ByteView // encoded

}

 

// 创建一个protoSink,传入的是一个proto.Messages

func ProtoSink(m proto.Message) Sink {

return &protoSink{

dst: m,

}

}

allocBytesSink

返回值是[]byte的Sink,每次Set都会重新分配内存

type allocBytesSink struct {

dst *[]byte

v   ByteView

}

 

// 创建一个allocBytesSink,传入的是一个数组分片指针

func AllocatingByteSliceSink(dst *[]byte) Sink {

return &allocBytesSink{dst: dst}

}

truncBytesSink

返回值是一个定长的[]byte的Sink,超过长度的会被截断,服用传入的空间

type truncBytesSink struct {

dst *[]byte

v   ByteView

}

 

// 创建一个truncBytesSink,传入的实一个数组分片指针

func TruncatingByteSliceSink(dst *[]byte) Sink {

return &truncBytesSink{dst: dst}

}

另外这些Sink类,都是包外不可见的,但是创建函数和Sink接口可见的,这样子在对于使用上来说,只有接口操作不会有另外的东西,对外简单清晰

HTTPPool

一个HTTPPool提供了groupcache节点之间的方式以及节点的选择的实现,但是其对于groupcache.Group是透明的,Group使用的HTTPPool

实现的1个接口PeerPicker以及httpGetter实现的接口ProtoGetter

// groupcache提供了一个节点互相访问访问的类

type httpGetter struct {

transport func(Context) http.RoundTripper

baseURL   string

}

 

// 协议为GET http://example.com/groupname/key

// response见groupcache.proto,含有2个可选项分别为[]byte和double

// 实现默认使用go自带的net/http包直接发送请求

func (h *httpGetter) Get(context Context, in *pb.GetRequest, out *pb.GetResponse) error

HttpPool实现比较简单都是直接使用Go内部自带的一些包

// 创建一个HttpPool, 只能被使用一次,主要是注册PeerPicker,以及初始化http服务

func NewHTTPPool(self string) *HTTPPool

 

// 设置groupcache集群的节点列表

func (p *HTTPPool) Set(peers ...string)

 

// 提供按key选取节点,按key作hash,但是这段代码在OS为32bit是存在bug,如果算出来的hashcode正好是-1 * 2^31时

// 会导致out of range,为啥会有这个bug看看代码你就会发现了,作者忘了-1 * 2^31 <= int32 <= 1 * 2^31 -1

func (p *HTTPPool) PickPeer(key string) (ProtoGetter, bool)

 

// http服务处理函数,主要是按http://example.com/groupname/key解析请求,调用group.Get,按协议返回请求

func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request)

Cache

Cache的实现很简单基本可以认为就是直接在LRU上面包了一层,加上了统计信息,锁,以及大小限制

type cache struct {

mu         sync.RWMutex

nbytes     int64 // of all keys and values

lru        *lru.Cache

nhit, nget int64

nevict     int64 // number of evictions

}

 

// 返回统计信息,加读锁

func (c *cache) stats() CacheStats

// 加入一个kv,如果cache的大小超过nbytes了,就淘汰

func (c *cache) add(key string, value ByteView)

// 根据key返回value

func (c *cache) get(key string) (value ByteView, ok bool)

// 删除最久没被访问的数据

func (c *cache) removeOldest()

// 获取当前cache的大小

func (c *cache) bytes() int64

// 获取当前cache中kv的个数,内部会加读锁

func (c *cache) items() int64

// 获取当前cache中kv的个数,函数名中的Locked的意思是调用方已经加锁,比如上面的stats方法

func (c *cache) itemsLocked() int64

Group

该类是GroupCache的核心类,所有的其他包都是为该类服务的

type Group struct {

// group名,可以理解为namespace的名字,group其实就是一个namespace

name       string

// 由调用方传入的回调,用于groupcache访问后端数据

// type Getter interface {

//     Get(ctx Context, key string, dest Sink) error

// }

getter     Getter

// 通过该对象可以达到只调用一次

peersOnce  sync.Once

// 用于访问groupcache中其他节点的接口,比如上面的HTTPPool实现了该接口

peers      PeerPicker

// cache的总大小

cacheBytes int64

 

// 从本节点指向向后端获取到的数据存在在cache中

mainCache cache

 

// 从groupcache中其他节点上获取到的数据存在在该cache中,因为是用其他节点获取到的,也就是说这个数据存在多份,也就是所谓的hot

hotCache cache

// 用于向groupcache中其他节点访问时合并请求

loadGroup singleflight.Group

 

// 统计信息

Stats Stats

}

由上面的结构体我们可以看出来,groupcache支持namespace概念,不同的namespace有自己的配额以及cache,不同group之间cache是独立的

也就是不能存在某个group的行为影响到另外一个namespace的情况 groupcache的每个节点的cache分为2层,由本节点直接访问后端的

存在maincache,其他存在在hotcache

// 根据name从全局变量groups(一个string/group的map)查找对于group

func GetGroup(name string) *Group

// 创建一个新的group,如果已经存在该name的group将会抛出一个异常,go里面叫panic

func NewGroup(name string, cacheBytes int64, getter Getter) *Group

// 注册一个创建新group的钩子,比如将group打印出来等,全局只能注册一次,多次注册会触发panic

func RegisterNewGroupHook(fn func(*Group))

// 注册一个服务,该服务在NewGroup开始时被调用,并且只被调用一次

func RegisterServerStart(fn func())

下面重点介绍Get方法

func (g *Group) Get(ctx Context, key string, dest Sink) error

整个groupcache核心方法就这么一个,我们来看一下该方法是怎么运作的

func (g *Group) Get(ctx Context, key string, dest Sink) error {

// 初始化peers,全局初始化一次

g.peersOnce.Do(g.initPeers)

// 统计信息gets增1

g.Stats.Gets.Add(1)

if dest == nil {

return errors.New("groupcache: nil dest Sink")

}

// 查找本地是否存在该key,先查maincache,再查hotcache

value, cacheHit := g.lookupCache(key)

 

// 如果查到九hit增1,并返回

if cacheHit {

g.Stats.CacheHits.Add(1)

return setSinkView(dest, value)

}

 

// 加载该key到本地cache中,其中如果是本地直接请求后端得到的数据,并且是同一个时间段里第一个,

// 就不需要重新setSinkView了,在load中已经设置过了,destPopulated这个参数以来底的实现

destPopulated := false

value, destPopulated, err := g.load(ctx, key, dest)

if err != nil {

return err

}

if destPopulated {

return nil

}

return setSinkView(dest, value)

}

 

func (g *Group) load(ctx Context, key string, dest Sink) (value ByteView, destPopulated bool, err error) {

// 统计loads增1

g.Stats.Loads.Add(1)

// 使用singleflight来达到合并请求

viewi, err := g.loadGroup.Do(key, func() (interface{}, error) {

// 统计信息真正发送请求的次数增1

g.Stats.LoadsDeduped.Add(1)

var value ByteView

var err error

// 选取向哪个节点发送请求,比如HTTPPool中的PickPeer实现

if peer, ok := g.peers.PickPeer(key); ok {

// 从groupcache中其他节点获取数据,并将数据存入hotcache

value, err = g.getFromPeer(ctx, peer, key)

if err == nil {

g.Stats.PeerLoads.Add(1)

return value, nil

}

g.Stats.PeerErrors.Add(1)

}

// 如果选取的节点就是本节点或从其他节点获取失败,则由本节点去获取数据,也就是调用getter接口的Get方法

// 另外我们看到这里dest已经被赋值了,所以有destPopulated来表示已经赋值过不需要再赋值

value, err = g.getLocally(ctx, key, dest)

if err != nil {

g.Stats.LocalLoadErrs.Add(1)

return nil, err

}

g.Stats.LocalLoads.Add(1)

destPopulated = true

// 将获取的数据放入maincache

g.populateCache(key, value, &g.mainCache)

return value, nil

})

 

// 如果成功则返回

if err == nil {

value = viewi.(ByteView)

}

return

}

这里需要说明的是A向groupcache中其他节点B发送请求,此时B是调用Get方法,然后如果本地不存在则也会走load,但是不同的是

PickPeer会发现是本身节点(HTTPPOOL的实现),然后就会走getLocally,会将数据在B的maincache中填充一份,也就是说如果

是向groupcache中其他节点发请求的,会一下子在groupcache集群内存2分数据,一份在B的maincache里,一份在A的hotcache中,

这也就达到了自动复制,越是热点的数据越是在集群内份数多,也就达到了解决热点数据的问题

发表在 groupcache | 留下评论

nginx map用法

map用法:

map $var1  $var2{
   value1ForVar1 value1ForVar2;
   value1ForVar1 value1ForVar2;  
}
当变量$var1为value1ForVar1, nginx自动增加一个$var2变量,值为value1ForVar2

示例:

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                  '$status $body_bytes_sent "$http_referer" '
                  '"$http_user_agent" "$http_x_forwarded_for" server_port="$server_port" dev_user="$dev_user"';

access_log  logs/access.log  main;

map $server_port $dev_user{ 80 test1; 81 test2; }
server {
    listen       80; 
    server_name  localhost;
     location / { 
        root   html;
        index  index.html index.htm;
    }   
}

访问localhost/index.html

127.0.0.1 - - [12/Aug/2016:11:53:30 +0800] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36" "-" server_port="80" dev_user="test1"

 

发表在 nginx, openresty | 留下评论

golang模板语法简明教程

【模板标签】

模板标签用"{{"和"}}"括起来

 

【注释】

{{/* a comment */}}

使用“{{/*”和“*/}}”来包含注释内容

 

【变量】

{{.}}

此标签输出当前对象的值

{{.Admpub}}

表示输出Struct对象中字段或方法名称为“Admpub”的值。

当“Admpub”是匿名字段时,可以访问其内部字段或方法,比如“Com”:{{.Admpub.Com}} ,

如果“Com”是一个方法并返回一个Struct对象,同样也可以访问其字段或方法:{{.Admpub.Com.Field1}}

{{.Method1 "参数值1" "参数值2"}}

调用方法“Method1”,将后面的参数值依次传递给此方法,并输出其返回值。

{{$admpub}}

此标签用于输出在模板中定义的名称为“admpub”的变量。当$admpub本身是一个Struct对象时,可访问其字段:{{$admpub.Field1}}

在模板中定义变量:变量名称用字母和数字组成,并带上“$”前缀,采用符号“:=”进行赋值。

比如:{{$x := "OK"}} 或 {{$x := pipeline}}

 

【管道函数】

用法1:

{{FuncName1}}

此标签将调用名称为“FuncName1”的模板函数(等同于执行“FuncName1()”,不传递任何参数)并输出其返回值。

用法2:

{{FuncName1 "参数值1" "参数值2"}}

此标签将调用“FuncName1("参数值1", "参数值2")”,并输出其返回值

用法3:

{{.Admpub|FuncName1}}

此标签将调用名称为“FuncName1”的模板函数(等同于执行“FuncName1(this.Admpub)”,将竖线“|”左边的“.Admpub”变量值作为函数参数传送)并输出其返回值。

 

【条件判断】

用法1:

{{if pipeline}} T1 {{end}}

标签结构:{{if ...}} ... {{end}}

用法2:

{{if pipeline}} T1 {{else}} T0 {{end}}

标签结构:{{if ...}} ... {{else}} ... {{end}}

用法3:

{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}

标签结构:{{if ...}} ... {{else if ...}} ... {{end}}

其中if后面可以是一个条件表达式(包括管道函数表达式。pipeline即管道),也可以是一个字符窜变量或布尔值变量。当为字符窜变量时,如为空字符串则判断为false,否则判断为true。

 

【遍历】

用法1:

{{range $k, $v := .Var}} {{$k}} => {{$v}} {{end}}

range...end结构内部如要使用外部的变量,比如.Var2,需要这样写:$.Var2

(即:在外部变量名称前加符号“$”即可,单独的“$”意义等同于global)

用法2:

{{range .Var}} {{.}} {{end}}

用法3:

{{range pipeline}} T1 {{else}} T0 {{end}}

当没有可遍历的值时,将执行else部分。

 

【嵌入子模板】

用法1:

{{template "name"}}

嵌入名称为“name”的子模板。使用前,请确保已经用“{{define "name"}}子模板内容{{end}}”定义好了子模板内容。

用法2:

{{template "name" pipeline}}

将管道的值赋给子模板中的“.”(即“{{.}}”)

 

【子模板嵌套】

{{define "T1"}}ONE{{end}}

{{define "T2"}}TWO{{end}}

{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}

{{template "T3"}}

输出:

ONE TWO

 

【定义局部变量】

用法1:

{{with pipeline}} T1 {{end}}

管道的值将赋给该标签内部的“.”。(注:这里的“内部”一词是指被{{with pipeline}}...{{end}}包围起来的部分,即T1所在位置)

用法2:

{{with pipeline}} T1 {{else}} T0 {{end}}

如果管道的值为空,“.”不受影响并且执行T0,否则,将管道的值赋给“.”并且执行T1。

 

 

说明:{{end}}标签是if、with、range的结束标签。

 

 

 

 

 

【例子:输出字符窜】

{{"\"output\""}}

输出一个字符窜常量。

 

{{`"output"`}}

输出一个原始字符串常量

 

{{printf "%q" "output"}}

函数调用.(等同于:printf("%q", "output")。)

 

{{"output" | printf "%q"}}

竖线“|”左边的结果作为函数最后一个参数。(等同于:printf("%q", "output")。)

 

{{printf "%q" (print "out" "put")}}

圆括号中表达式的整体结果作为printf函数的参数。(等同于:printf("%q", print("out", "put"))。)

 

{{"put" | printf "%s%s" "out" | printf "%q"}}

一个更复杂的调用。(等同于:printf("%q", printf("%s%s", "out", "put"))。)

 

{{"output" | printf "%s" | printf "%q"}}

等同于:printf("%q", printf("%s", "output"))。

 

{{with "output"}}{{printf "%q" .}}{{end}}

一个使用点号“.”的with操作。(等同于:printf("%q", "output")。)

 

{{with $x := "output" | printf "%q"}}{{$x}}{{end}}

with结构,定义变量,值为执行管道函数之后的结果(等同于:$x := printf("%q", "output")。)

 

{{with $x := "output"}}{{printf "%q" $x}}{{end}}

with结构中,在其它动作中使用定义的变量

 

{{with $x := "output"}}{{$x | printf "%q"}}{{end}}

同上,但使用了管道。(等同于:printf("%q", "output")。)

 

 

===============【预定义的模板全局函数】================

【and】

{{and x y}}

表示:if x then y else x

如果x为真,返回y,否则返回x。等同于Golang中的:x && y

 

【call】

{{call .X.Y 1 2}}

表示:dot.X.Y(1, 2)

call后面的第一个参数的结果必须是一个函数(即这是一个函数类型的值),其余参数作为该函数的参数。

该函数必须返回一个或两个结果值,其中第二个结果值是error类型。

如果传递的参数与函数定义的不匹配或返回的error值不为nil,则停止执行。

 

【html】

转义文本中的html标签,如将“<”转义为“&lt;”,“>”转义为“&gt;”等

 

【index】

{{index x 1 2 3}}

返回index后面的第一个参数的某个索引对应的元素值,其余的参数为索引值

表示:x[1][2][3]

x必须是一个map、slice或数组

 

【js】

返回用JavaScript的escape处理后的文本

 

【len】

返回参数的长度值(int类型)

 

【not】

返回单一参数的布尔否定值。

 

【or】

{{or x y}}

表示:if x then x else y。等同于Golang中的:x || y

如果x为真返回x,否则返回y。

 

【print】

fmt.Sprint的别名

 

【printf】

fmt.Sprintf的别名

 

【println】

fmt.Sprintln的别名

 

【urlquery】

返回适合在URL查询中嵌入到形参中的文本转义值。(类似于PHP的urlencode)

 

 

=================【布尔函数】===============

布尔函数对于任何零值返回false,非零值返回true。

这里定义了一组二进制比较操作符函数:

 

【eq】

返回表达式“arg1 == arg2”的布尔值

 

【ne】

返回表达式“arg1 != arg2”的布尔值

 

【lt】

返回表达式“arg1 < arg2”的布尔值

 

【le】

返回表达式“arg1 <= arg2”的布尔值

 

【gt】

返回表达式“arg1 > arg2”的布尔值

 

【ge】

返回表达式“arg1 >= arg2”的布尔值

 

对于简单的多路相等测试,eq只接受两个参数进行比较,后面其它的参数将分别依次与第一个参数进行比较,

{{eq arg1 arg2 arg3 arg4}}

即只能作如下比较:

arg1==arg2 || arg1==arg3 || arg1==arg4 ...

 

来源:

http://www.admpub.com/blog/post-221.html

发表在 golang | 留下评论

golang中html template模板

模板和服务端代码耦合

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
58
package main
import (
    "html/template"
    "log"
    "os"
)
func main() {
    // 声明模板内容
    const tpl = `
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{{.Title}}</title>
    </head>
    <body>
        {{range .Items}}<div>{{ . }}</div>{{else}}<div><strong>no rows</strong></div>{{end}}
    </body>
</html>`
    check := func(err error) {
        if err != nil {
            log.Fatal(err)
        }
    }
    // 创建一个新的模板,并且载入内容
    t, err := template.New("webpage").Parse(tpl)
    check(err)
    // 定义传入到模板的数据,并在终端打印
    data := struct {
        Title string
        Items []string
    }{
        Title: "My page",
        Items: []string{
            "My photos",
            "My blog",
        },
    }
    err = t.Execute(os.Stdout, data)
    check(err)
    // 定义Items为空的数据
    noItems := struct {
        Title string
        Items []string
    }{
        Title: "My another page",
        Items: []string{},
    }
    err = t.Execute(os.Stdout, noItems)
    check(err)
}

运行

$ go run main.go
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My page</title>
</head>
<body>
<div>My photos</div><div>My blog</div>
</body>
</html>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My another page</title>
</head>
<body>
<div><strong>no rows</strong></div>
</body>
</html>

这个示例来自golang官方,但是模板内容是直接放在代码里,实际开发的时候一般都会将模板文件和golang代码区分开,从上面的代码可以初步想到将tpl的内容存放到文件里,然后通过文件操作方法将内容读取出来。这个代码我们不用自己写,template中提供了另外一个函数ParseFiles可以直接解析文件。

模板和服务端代码分离

tpl.html内容:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{{.Title}}</title>
    </head>
    <body>
    </body>
</html>
</pre>

golang代码

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
package main
import (
    "html/template"
    "log"
    "os"
)
func main() {
    t, err := template.ParseFiles("./tpl.html")
    if err != nil {
        log.Fatal(err)
    }
    data := struct {
        Title string
    }{
        Title: "golang html template demo",
    }
    err = t.Execute(os.Stdout, data)
    if err != nil {
        log.Fatal(err)
    }
}

运行

$ go run tpl.go
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>golang html template demo</title>
</head>
<body>

</body>
</html>

上面实现了单个模板的载入,在模板比较多的时候一般会考虑将公共的部分提取出来,例如提取出公共的头部(header)和底部(footer)这样。

模板内嵌入公共模板

模板目录结构:

golang html template

golang html template

index.html

1
2
3
4
5
6
7
8
<html>
{{template "header" .}}
<body>
    <h1>index</h1>
    {{template "footer"}}
</body>
</html>

在模板嵌入其它模板使用template语法,其中{{template “header” .}}最后的一个点表示将当前模板中的变量传递到header模板。

header.html

1
2
3
4
5
{{define "header"}}
     <head>
         <title>{{.Title}}</title>
     </head>
{{end}}

footer.html

1
2
3
{{define "footer"}}
    <div>footer</div>
{{end}}

golang代码:

template.ParseFiles这个函数可以接收多个文件参数

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
package main
import (
    "html/template"
    "log"
    "os"
)
func main() {
    t, err := template.ParseFiles("tpl/index.html""tpl/public/header.html""tpl/public/footer.html")
    if err != nil {
        log.Fatal(err)
    }
    data := struct {
        Title string
    }{
        Title: "load common template",
    }
    err = t.Execute(os.Stdout, data)
    if err != nil {
        log.Fatal(err)
    }
}

运行

$ go run muti.go
<html>

<head>
<title>load common template</title>
</head>

<body>
<h1>index</h1>
<div>footer</div>

</body>
</html>

到此我们可能还会问,那么模板除了tempate这样的语法,还有什么样的语法呢?比如循环数组输出主要怎么写?这点可以参考text.template包,里面有详细的说明。如果阅读html.template的源码可以发现该包也是构建在text.template基础之上的。

FROM : http://www.01happy.com/golang-html-template/?utm_source=tuicool&utm_medium=referral

发表在 golang | 留下评论

Lua: 给 Redis 用户的入门指导

FROM : http://www.oschina.net/translate/intro-to-lua-for-redis-programmers

 

可能你已经听说过Redis 中嵌入了脚本语言,但是你还没有亲自去尝试吧?  这个入门教程会让你学会在你的Redis 服务器上使用强大的lua语言。

Hello, Lua!

我们的第一个Redis Lua 脚本仅仅返回一个字符串,而不会去与redis 以任何有意义的方式交互。

local msg = "Hello, world!"
return msg

这是非常简单的,第一行代码定义了一个本地变量msg存储我们的信息, 第二行代码表示 从redis 服务端返回msg的值给客户端。 保存这个文件到hello.lua,像这样去运行:

redis-cli EVAL "$(cat hello.lua)" 0

运行这段代码会打印"Hello,world!", EVAL在第一个参数是我们的lua脚本, 这我们用cat命令从文件中读取我们的脚本内容。第二个参数是这个脚本需要访问的Redis 的键的数字号。我们简单的 “Hello Script" 不会访问任何键,所以我们使用0

lidashuang

lidashuang
翻译于 3年前

3人顶

 翻译的不错哦!

访问键和参数

假设我们要建立一个URL简写服务器。我们就要去存储每条进入的URL并返回一个唯一数值,以便以后通过这个数值访问到该URL。

我们将利用Lua脚本立即从Redis中用INCRand获取一个唯一标识ID,以这个标识ID作为URL存储于一个哈希中的键值:

local link_id = redis.call("INCR", KEY[1])
redis.call("HSET", KEYS[2], link_id, ARGV[1])
return link_id

我们将用call()函数首次访问Redis。call()的参数就是发给Redis的命令:首先INCR <key>, 然后HSET <key> <field> <value>。这两个命令将依次执行——当这个脚本执行时,Redis不会做任何事,它将非常快地运行。

wyuan

wyuan
翻译于 3年前

2人顶

 翻译的不错哦!

我们将会访问两个Lua表:KEYS和ARGV。表单是关联性数组和结构化数据的Lua唯一机制。对于我们的意图,你可以把它们看做是一个你所熟悉的任意语言对等的数组,但是提醒两个很容易困扰到新手的两个Lua定则:

  • 表是基于1的,也就是说索引以数值1开始。所以在表中的第一个元素就是mytable[1],第二个就是mytable[2]等等。
  • 表中不能有nil值。如果一个操作表中有[1, nil, 3, 4],那么结果将会是[1]——表将会在第一个nil截断。

当调用这个脚本时,我们还需要传递KEYS和ARGV表的值:

redis-cli EVAL "$(cat incr-and-stor.lua)" 2 links:counter links:urls http://malcolmgladwellbookgenerator.com/
wyuan

wyuan
翻译于 3年前

2人顶

 翻译的不错哦!

在EVAL语句中,2指出需要传入的KEY的个数,后面跟着需要传入的两个KEY,最后传入是ARGV的值。在Redis中执行Lua脚本时,Redis-cli会检查传入KEY的个数,除非传入的完全是命令。

为了解释得更清楚,下面列出替换KEY和ARGV后的脚本:

 

local link_id = redis.call("INCR", "links:counter")
redis.call("HSET", "links:urls", link_id, "http://malcolmgladwellbookgenerator.com")
return link_id

为Redis编写Lua脚本时,每个KEY都是通过KEYS表指定。ARGV表用来传递参数,这个例子中ARGV用来传入URL。

leyaya.cn

leyaya.cn
翻译于 3年前

2人顶

 翻译的不错哦!

逻辑条件:increx与hincrex

上一个例子保存链接为短网址,想要知道这个链接的点击次数,在Redis中添加一个hash计数器。当带有链接标记的用户访问时,我们检查其是否存在,如存在则需要给计数器加1:

if redis.call("HEXISTS", KEYS[1], ARGV[1]) == 1 then
return redis.call("HINCR", KEYS[1], ARGV[1])
else
return nil
end

每次有人点击短网址,我们运行这个脚本跟踪这个链接被再次分享。我们用EVAL来调用脚本,传入inlinks:visits(keys[1])和上一个脚本返回的链接标识(ARGV[1])。这段脚本将检查是否存在相同的hash,如果存在就为这个标准的Redis KEY加1。

if redis.call("EXISTS",KEYS[1]) == 1 then
return redis.call("INCR",KEYS[1])
else
return nil
end
leyaya.cn

leyaya.cn
翻译于 3年前

2人顶

 翻译的不错哦!

脚本加载与注册执行

注意,当Redis在运行Lua脚本的时候,其它的事情什么都干不了!脚本最好只是简单的扩展Redis进行较小的原子操作和简单的逻辑控制需要,Lua脚本中的bug可能引发整个Redis服务器锁—最好保持脚本的简短和易于调试。

虽然这些脚本一般都比较短小,但我们还是希望不要每次执行时都使用完整的Lua脚本,实际上可以在程序一步一步(译注:application boots翻译有难度)开发中注册Lua脚本(或者在你部署时注册),然后用注册后生成的SHA-1标识来进行调用。

redis-cli SCRIPT LOAD "return 'hello world'"
=> "5332031c6b470dc5a0dd9b4bf2030dea6d65de91"

redis-cli EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
=> "hello world"
leyaya.cn

leyaya.cn
翻译于 3年前

2人顶

 翻译的不错哦!

显示调用SCRIPT LOAD通常是不必要的,当一个程序执行EVAL时就已隐式加载了。程序会先尝试EAVALSHA,当脚本没有找到时会调用EVAL。

对于Ruby开发者,可以看一下Shopify’s Wolverine,其可以为Ruby应用简单的加载并存储Lua脚本。对于PHP开发者,Predis 支持加载Lua脚本作为普通Redis命令进行调用(译注:需要继承Predis\Command\ScriptedCommand基类,并注册命令)。如果你使用这些或者其它的工具来标准化与Lua的交互,请让我知道,我很感兴趣知道本文之外的内容。

leyaya.cn

leyaya.cn
翻译于 3年前

2人顶

 翻译的不错哦!

何时使用Lua

Redis支持WATCH/MULTI/EXEC这样的块,能进行一组操作,也能一起提交执行,看起来与Lua有重叠。应该如何进行选择?MULT块中所有操作独立,但在Lua中,后面的操作能依赖前面操作的执行结果。同时使用Lua脚本还能够避免WATCH使用后竞争条件引起客户端反应变慢的情况。

在RedisGreen(译注:国外一家专门提供Redis主机的服务商),我们看到许多应用使用Lua的同时也使用MULTI/EXEC,但两者但不是替代关系。许多成功的Lua脚本都很小,仅仅实现一个你的应用需要而Redis命令中没有单一的功能。

leyaya.cn

leyaya.cn
翻译于 3年前

2人顶

 翻译的不错哦!

访问库

Redis的Lua解释器加载七个库:base,tablestring, math, debugcjsoncmsgpack。前几个都是标准库,充许你使用任何语言进行基本的操作。后面两个可以让Redis支持JSON和MessagePack—这是非常有用的功能,同时我也很想知道为什么常常看不到这种用法。

Web应用程序常常使用JSON作为api返回数据,你也许也可以把一堆JSON数据存到Redis的key中。当想访问某些JSON数据时,首先需要保存到一个hash中,使用Redis的JSON支持将非常方便:

if redis.call("EXISTS", KEYS[1]) == 1 then
local payload = redis.call("GET", KEYS[1])
return cjson.decode(payload)[ARGV[1]]
else
return nil
end
leyaya.cn

leyaya.cn
翻译于 3年前

2人顶

 翻译的不错哦!

在这里我们检查看key是否存在,如不存在则快速返回nil。如存在则从Redis中获取JSON值,用cjson.decode()进行解析,然后返回请求内容。

redis-cli set apple '{ "color": "red", "type": "fruit" }'
=> OK

redis-cli eval "$(cat json-get.lua)" 1 apple type
=> "fruit"

加载这段脚本进你的Redis服务器,将JSON数据保存到Redis中,通常是hash。 虽然我们每次访问时都必须解析,但只要你的对象很小,这个操作实际上是非常快的。如果你的API只是在内部提供,通常需要考虑效率上的问题,MessagePack 是比采用JSON更好的选择,它更小,更快,在Redis(更多场合也是如此),MessagePack是JSON更好的替代品。

if redis.call("EXISTS", KEYS[1]) == 1 then
  local payload = redis.call("GET", KEYS[1])
  return cmsgpack.unpack(payload)[ARGV[1]]
else
  return nil
end
leyaya.cn

leyaya.cn
翻译于 3年前

2人顶

 翻译的不错哦!

数值转换

Lua和Redis各有自己的一套类型,因此,理解Redis与Lua在边界调用相互转换引起值的改变是非常重要的。一个来自Lua中number返回到Redis客户端时变成了integer—任何数字后面的小数点都被清除了:

local indiana_pi = 3.2
return indiana_pi

在你运行这段脚本时,Redis将返回一个整数3,丢失了pi中有用的片段。看起来很简单,但是一旦开始进行Redis与中间脚本交互时就需要更小心。例如:

local indiana_pi = 3.2
redis.call("SET", "pi", indiana_pi)
return redis.call("GET", "pi")
leyaya.cn

leyaya.cn
翻译于 3年前

2人顶

 翻译的不错哦!

执行的结果是一个字符串:“3.2”,这是为什么呢?在Redis中没有专有的数值类型,当我们第一次调用SET的时候,Redis就已经将它保存为字符串了,将Lua初始化时将其作为一个浮点数的类型信息给丢失了。所以当我们后面取出这个值时,它就变成了一个字符串。

在Redis中,除了INCR和DECR,其它的GET,SET操作所访问的数据都作为字符串处理。INCR与DECR是专门对数值的操作,实际上返回是整数(integer)回复(维护和存储遵守数字规则),但Redis内部保存类型实际上还是字符串值。

leyaya.cn

leyaya.cn
翻译于 3年前

2人顶

 翻译的不错哦!

总结:

下面这些都是在Redis中使用Lua时常见的错误:

  • 表是Lua中的表达式,与很多流行语言不同。KEYS中的第一个元素是KEYS[1],第二个是KEYS[2](译注:不是0开始)
  • nil是表的结束符,[1,2,nil,3]将自动变为[1,2],因此在表中不要使用nil。
  • redis.call会触发Lua中的异常,redis.pcall将自动捕获所有能检测到的错误并以表的形式返回错误内容。
  • Lua数字都将被转换为整数,发给Redis的小数点会丢失,返回前把它们转换成字符串类型。
  • 确保在Lua中使用的所有KEY都在KEY表中,否则在将来的Redis版中你的脚本都有不能被很好支持的危险。
  • Lua脚本和其它Redis操作一样,在脚本执行时,其它的一切都不能运行。考虑用脚本来护展Redis服务器能力,但要保持短小和有用。

补充读物

下面有许多关于Lua和Redis很好的在线资源,本文只是我所用到的很少一部分:

发表在 lua, redis, 架构 | 留下评论

golang : 内置排序实现

https://gobyexample.com/sorting

package main
import "fmt"
import "sort"
func main() {
//Sort methods are specific to the builtin type; here’s an example for strings.
// Note that sorting is in-place, so it changes the given slice and doesn’t return a new one.
    strs := []string{"c", "a", "b"}
    sort.Strings(strs)
    fmt.Println("Strings:", strs)
//An example of sorting ints.
    ints := []int{7, 2, 4}
    sort.Ints(ints)
    fmt.Println("Ints:   ", ints)
//We can also use sort to check if a slice is already in sorted order.
    s := sort.IntsAreSorted(ints)
    fmt.Println("Sorted: ", s)
}

https://gobyexample.com/sorting-by-functions:自定义排序

package main
import "sort"
import "fmt"
//In order to sort by a custom function in Go, we need a corresponding type. Here we’ve created a ByLength type that is just an alias for the builtin []string type.
type ByLength []string
//We implement sort.Interface - Len, Less, and Swap - on our type so we can use the sort package’s generic Sort function. Len and Swap will usually be similar across types and Less will hold the actual custom sorting logic. In our case we want to sort in order of increasing string length, so we use len(s[i]) and len(s[j]) here.
func (s ByLength) Len() int {
    return len(s)
}
func (s ByLength) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}
func (s ByLength) Less(i, j int) bool {
    return len(s[i]) < len(s[j])
}
//With all of this in place, we can now implement our custom sort by casting the original fruits slice to ByLength, and then use sort.Sort on that typed slice.
func main() {
    fruits := []string{"peach", "banana", "kiwi"}
    sort.Sort(ByLength(fruits))
    fmt.Println(fruits)
}
发表在 golang, markdown | 留下评论

golang :sync/atomic - 原子操作

https://github.com/polaris1119/The-Golang-Standard-Library-by-Example/blob/master/chapter16/16.02.md

对于并发操作而言,原子操作是个非常现实的问题。典型的就是i++的问题。
当两个CPU同时对内存中的i进行读取,然后把加一之后的值放入内存中,可能两次i++的结果,这个i只增加了一次。
如何保证多CPU对同一块内存的操作是原子的。
golang中sync/atomic就是做这个使用的。

具体的原子操作在不同的操作系统中实现是不同的。比如在Intel的CPU架构机器上,主要是使用总线锁的方式实现的。
大致的意思就是当一个CPU需要操作一个内存块的时候,向总线发送一个LOCK信号,所有CPU收到这个信号后就不对这个内存块进行操作了。
等待操作的CPU执行完操作后,发送UNLOCK信号,才结束。
在AMD的CPU架构机器上就是使用MESI一致性协议的方式来保证原子操作。
所以我们在看atomic源码的时候,我们看到它针对不同的操作系统有不同汇编语言文件。

如果我们善用原子操作,它会比锁更为高效。

CAS

原子操作中最经典的CAS(compare-and-swap)在atomic包中是Compare开头的函数。

  • func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
  • func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
  • func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
  • func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
  • func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
  • func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)

CAS的意思是判断内存中的某个值是否等于old值,如果是的话,则赋new值给这块内存。CAS是一个方法,并不局限在CPU原子操作中。
CAS比互斥锁乐观,但是也就代表CAS是有赋值不成功的时候,调用CAS的那一方就需要处理赋值不成功的后续行为了。

这一系列的函数需要比较后再进行交换,也有不需要进行比较就进行交换的原子操作。

  • func SwapInt32(addr *int32, new int32) (old int32)
  • func SwapInt64(addr *int64, new int64) (old int64)
  • func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
  • func SwapUint32(addr *uint32, new uint32) (old uint32)
  • func SwapUint64(addr *uint64, new uint64) (old uint64)
  • func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)

增加或减少

对一个数值进行增加或者减少的行为也需要保证是原子的,它对应于atomic包的函数就是

  • func AddInt32(addr *int32, delta int32) (new int32)
  • func AddInt64(addr *int64, delta int64) (new int64)
  • func AddUint32(addr *uint32, delta uint32) (new uint32)
  • func AddUint64(addr *uint64, delta uint64) (new uint64)
  • func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

读取或写入

当我们要读取一个变量的时候,很有可能这个变量正在被写入,这个时候,我们就很有可能读取到写到一半的数据。
所以读取操作是需要一个原子行为的。在atomic包中就是Load开头的函数群。

  • func LoadInt32(addr *int32) (val int32)
  • func LoadInt64(addr *int64) (val int64)
  • func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
  • func LoadUint32(addr *uint32) (val uint32)
  • func LoadUint64(addr *uint64) (val uint64)
  • func LoadUintptr(addr *uintptr) (val uintptr)

好了,读取我们是完成了原子性,那写入呢?也是同样的,如果有多个CPU往内存中一个数据块写入数据的时候,可能导致这个写入的数据不完整。
在atomic包对应的是Store开头的函数群。

  • func StoreInt32(addr *int32, val int32)
  • func StoreInt64(addr *int64, val int64)
  • func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
  • func StoreUint32(addr *uint32, val uint32)
  • func StoreUint64(addr *uint64, val uint64)
  • func StoreUintptr(addr *uintptr, val uintptr)
发表在 golang | 留下评论

golang :非阻塞channel操作

在channel上的读写一般是阻塞的。可以通过给select 增加default分支的方法实现非阻塞的读写
https://gobyexample.com/non-blocking-channel-operations
发表在 golang | 留下评论

golang : 定时器

package main

import (
	"fmt"
	"time"
)

func testTimer1() {
	go func() {
		t := time.Now().Second()
		fmt.Println("test timer1:", t)
	}()

}

func testTimer2() {
	go func() {
		t := time.Now().Second()
		fmt.Println("test timer2:", t)
	}()
}

func timer1() {
	timer1 := time.NewTicker(1 * time.Second)
	for {
		select {
		case <-timer1.C:
			testTimer1()
		}
	}
}

func timer2() {
	timer2 := time.NewTicker(2 * time.Second)
	for {
		select {
		case <-timer2.C:
			testTimer2()
		}
	}
}

func main() {
	go timer1()
	timer2()
}
发表在 golang | 留下评论

golang的select典型用法

golang 的 select 的功能和 select, poll, epoll 相似, 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作。

示例:

ch1 := make (chan int, 1)

ch2 := make (chan int, 1)

 

...

 

select {

case <-ch1:

fmt.Println("ch1 pop one element")

case <-ch2:

fmt.Println("ch2 pop one element")

}

注意到 select 的代码形式和 switch 非常相似, 不过 select 的 case 里的操作语句只能是【IO 操作】 。

此示例里面 select 会一直等待等到某个 case 语句完成, 也就是等到成功从 ch1 或者 ch2 中读到数据。 则 select 语句结束。

【使用 select 实现 timeout 机制】

如下:

timeout := make (chan bool, 1)

go func() {

time.Sleep(1e9) // sleep one second

timeout <- true

}()

ch := make (chan int)

select {

case <- ch:

case <- timeout:

fmt.Println("timeout!")

}

当超时时间到的时候,case2 会操作成功。 所以 select 语句则会退出。 而不是一直阻塞在 ch 的读取操作上。 从而实现了对 ch 读取操作的超时设置。

下面这个更有意思一点。

当 select 语句带有 default 的时候:

ch1 := make (chan int, 1)

ch2 := make (chan int, 1)

 

select {

case <-ch1:

fmt.Println("ch1 pop one element")

case <-ch2:

fmt.Println("ch2 pop one element")

default:

fmt.Println("default")

}

此时因为 ch1 和 ch2 都为空,所以 case1 和 case2 都不会读取成功。 则 select 执行 default 语句。

就是因为这个 default 特性, 我们可以使用 select 语句来检测 chan 是否已经满了。

如下:

ch := make (chan int, 1)

ch <- 1

select {

case ch <- 2:

default:

fmt.Println("channel is full !")

}

因为 ch 插入 1 的时候已经满了, 当 ch 要插入 2 的时候,发现 ch 已经满了(case1 阻塞住), 则 select 执行 default 语句。 这样就可以实现对 channel 是否已满的检测, 而不是一直等待。

比如我们有一个服务, 当请求进来的时候我们会生成一个 job 扔进 channel, 由其他协程从 channel 中获取 job 去执行。 但是我们希望当 channel 瞒了的时候, 将该 job 抛弃并回复 【服务繁忙,请稍微再试。】 就可以用 select 实现该需求。

发表在 golang | 留下评论

golang : 只读、只写的channel

package main
import "fmt"
//This ping function only accepts a channel for sending values. It would be a compile-time error to try to receive on this channel.
func ping(pings chan<- string, msg string) {
    pings <- msg
}
//The pong function accepts one channel for receives (pings) and a second for sends (pongs).
func pong(pings <-chan string, pongs chan<- string) {
    msg := <-pings
    pongs <- msg
}
func main() {
    pings := make(chan string, 1)
    pongs := make(chan string, 1)
    ping(pings, "passed message")
    pong(pings, pongs)
    fmt.Println(<-pongs)
}
发表在 golang | 留下评论

golang : slice做为参数传递给变参函数

If you already have multiple args in a slice, apply them to a variadic function using func(slice...) like this.

package main

import "fmt"

func sum(nums ...int) {
	fmt.Print(nums, " ")
	total := 0
	for _, num := range nums {
		total += num
	}
	fmt.Println(total)
}
func main() {
	sum(1, 2)
	sum(1, 2, 3)
	nums := []int{1, 2, 3, 4}
	sum(nums...)
}
发表在 golang | 留下评论

golang 全局变量和常量定义的区别

全局变量定义:

var a int
var a int = 123
var a = 234

a = 122 //wrong
a := 123 //wrong

常量定义:

const a = 123
const a int = 123

从以上可知,全局变量和常量定义的区别就是 使用var和const,其它都一样

发表在 php | 留下评论

golang struct初始化

// 先定义结构体
type Rect struct {
	width  int
	height int
}

// 再初始化结构体
rect1 := new(Rect)
rect2 := &Rect{}
rect3 := &Rect{10, 20}
rect4 := &Rect{width:10, height:20}

// 定义 + 初始化同时进行
rect5 := &struct{width int, height int}{10, 20}
发表在 golang | 留下评论

How to set timeout for http.Get() requests in Golang?

timeout := time.Duration(5 * time.Second)
client := http.Client{
    Timeout: timeout,
}
client.Get(url)
var timeout = time.Duration(2 * time.Second)

func dialTimeout(network, addr string) (net.Conn, error) {
    return net.DialTimeout(network, addr, timeout)
}

func main() {
    transport := http.Transport{
        Dial: dialTimeout,
    }

    client := http.Client{
        Transport: &transport,
    }

    resp, err := client.Get("http://some.url")
}
package httpclient

import (
    "net"
    "net/http"
    "time"
)

func TimeoutDialer(cTimeout time.Duration, rwTimeout time.Duration) func(net, addr string) (c net.Conn, err error) {
    return func(netw, addr string) (net.Conn, error) {
        conn, err := net.DialTimeout(netw, addr, cTimeout)
        if err != nil {
            return nil, err
        }
        conn.SetDeadline(time.Now().Add(rwTimeout))
        return conn, nil
    }
}

func NewTimeoutClient(connectTimeout time.Duration, readWriteTimeout time.Duration) *http.Client {

    return &http.Client{
        Transport: &http.Transport{
            Dial: TimeoutDialer(connectTimeout, readWriteTimeout),
        },
    }
}

FROM : http://stackoverflow.com/questions/16895294/how-to-set-timeout-for-http-get-requests-in-golang

发表在 golang | 留下评论

Kite: Library for writing distributed microservices

Writing web services with Go is super easy. The simple but powerful net/http package lets you write performant web services in a very quick way. However sometimes all you want is to write a RPC backend application. Basically you want to have many independent worker applications that are running separately, each with their own responsibility of doing certain tasks. They should accept requests and reply to them with a well defined response.

This is obvious, however it’s getting difficult once you go beyond simple requirements. In real world scenarios you are going to have hundreds of applications running. You want to talk with them securely (and also authenticated). In order to talk with them securely, the first thing you need is to connect to a certain application. Now unless you have very few applications, there is no way you can remember the IP or hostname of that particular application (remember you have to many applications). Just storing all host IP’s persistently is not enough, because the host IP can change (just think of EC2 instances that come and go). What you need is something you can go and ask, and get the IP for the given application, just like a DNS server.

So building a distributed system with many applications is becoming hard. The Kite library development started within Koding, but it was quickly open sourced. The main goal is to create easy, simple, and convenient to use distributed microservice applications. The Kite library itself has many detailed parts, so in this blog post I’ll try to give an overview of what a Kite is capable of.

Introducing Kite

Kite is a microservice RPC library written in Go which makes writing user friendly distributed systems easy. It aims a balance between simple/easy usage and performance. Kite is a RPC server as well as a client. It can connect to other kites and peers to communicate with each other (bidirectional). A Kite identifies itself with the following parameters (order is important):

  • Username: Owner of the Kite, example: Brian, Fatih, Damian etc..
  • Environment: Current environment such as “production”, “testing”, “staging”, etc…
  • Name: Short name identifying the type of the kite. Example: mykite, fs, terminal, etc …
  • Version: 3-digit semantic version.
  • Region: Current region, such as “Europe”, “Asia” or some other locations.
  • Hostname: Hostname of the Kite.
  • ID: Unique ID that identifies a Kite. This is generated via the Kite library, however you might change it yourself.

These identifiers are important so a Kite can be distinguish and searched by others.

Kite uses SockJS to provide a WebSocket emulation over many different transports (websocket, xhr, etc..). So that means you can connect to Kite from a browser too (see our excellent Kite.js). Kite uses a modified dnode protocol for RPC messaging. The Kite protocol adds an additional session and authentication layer, so it can be used to identifies Kites easily. Under the hood it uses JWT for authentication and session information.

A Kite can discover other kites using a service discovery mechanism called Kontrol to communicate with other kites securely and with authentication. In order to use service discovery a Kite can register itself with Kontrol. This is optional but it’s encouraged and heavily reflected in the Kite API.

Kontrol is a service discovery mechanism for kites. It controls and keeps track of kites and provides a way to authenticate kite users, so they can securely talk with each other. Kontrol uses etcd for backend storage, however it can be replaced with others too (currently there is also support for PostgreSQL). Anything that satisfies the kontrol.Storage interface can be used as backend storage, thanks to the flexibility of Go’s interfaces. Kontrol also has many ways of authenticating users. It is customizable so people can use their own way of Kontrol.

How to use a Kite

Now let’s dive in. Even more interesting is writing and using Kite. It’s fun to write a Kite and let them talk to each other. First let me show you a Kite in the most simple form (for sake of simplicity I’m ignoring errors, but please don’t do that :) )

package main

 

import "github.com/koding/kite"

 

func main() {

k := kite.New("first", "1.0.0")

k.Run()

}

Here we just created a kite with the name first and version 1.0.0. The Run() method is running a server, which is blocking (just like http.Serve). This kite is now capable of receiving requests. Because no port number is assigned the OS has picked one for us automatically.

Let us assign a port now, so we can connect to it from another kite (otherwise you need to pick the assigned URL from the logs). To change the configuration of a Kite, such as Port number, the properties (such as Environment, Region, etc… you’ll need to modify the Config fields:

package main

 

import "github.com/koding/kite"

 

func main() {

k := kite.New("first", "1.0.0")

k.Config.Port = 6000

k.Run()

}

The configuration values can be also overridden via environment variables if needed.

Let us create a second kite to talk with the first kite:

package main

 

import (

"fmt"

 

"github.com/koding/kite"

)

 

func main() {

k := kite.New("second", "1.0.0")

 

client := k.NewClient("http://localhost:6000/kite")

client.Dial()

 

response, _ := client.Tell("kite.ping")

fmt.Println(response.MustString())

}

This time we connect to a new kite directly because we know the URL already. As a RPC system you need have a concept of URL paths. Kite uses simple method names, so it can be called by others. Each method is associated with a certain handle (just like a http.Handler) The kite library has some default methods, one of them is the kite.ping method which returns a pong string as a response (it doesn’t require any authentication information). The response can be anything, in any Go type that can be serialized to and from JSON, It’s up to the sender. Kite has some predefined helper methods to convert the response to the given type. In this example the second kite just connects to our first kite and calls the first kite’s kite.ping method. We didn’t send any arguments with this method (will be explained below). So if you run, you’ll see:

$ go run second.go

pong

Adding methods to Kite

Let us add our first custom method. This simple method is going to accept a number and return a squared result. The name of the method will be square. To assign a function to a method just be sure it’s satisfies the kite.Handler interface (http://godoc.org/github.com/koding/kite#Handler):

package main

 

import "github.com/koding/kite"

 

func main() {

k := kite.New("first", "1.0.0")

k.Config.Port = 6000

k.Config.DisableAuthentication = true

 

k.HandleFunc("square", func(r *kite.Request) (interface{}, error) {

a := r.Args.One().MustFloat64()

return a * a, nil

})

 

k.Run()

}

Let’s call it via our “second” kite:

package main

 

import (

"fmt"

 

"github.com/koding/kite"

)

 

func main() {

k := kite.New("second", "1.0.0")

 

client := k.NewClient("http://localhost:6000/kite")

client.Dial()

 

response, _ := client.Tell("square", 4)

fmt.Println(response.MustFloat64())

}

As you see the only thing that has changed is the method call. When we call the “square” method we also send the number 4 with as arguments. You can send any JSON compatible Go type. Running the examples, we’ll get simply:

$ go run second.go

16

It’s that easy.

Service discovery, how to find each other

Service discovery is baked into the Kite library. As said earlier, it’s a very fundamental concept and is also heavily reflected via the Kite API. That means the Kite library forces the users to make use of service discovery. To be discovered by others they need to know your real identitiy. Basically you need to be authenticated. Authentication can be done in several ways and is defined by how Kontrol enforces it. It can disable it completely, might ask the user password (via the kite cli), could fetch a token and validate what the user provided and so on…

kitectl is a handy CLI program which can be used to manage kites easily via command line. We can use it (via kitectl register command) to authenticate our machine to Kontrol, so every kite running on our host will be authenticated by default. This command creates a kite.key file under the home directory, which is signed by kontrol itself. The content is not encrypted, however because it’s signed we can use it to securely talk to Kontrol. So therefore every request we’ll make to kontrol will be trusted by Kontrol. Our username will be stored in Kontrol, so every other person in the world can trust us (of course assuming they also using the same Kontrol server). Trusting Kontrol means we can trust everyone. So this is important, because they might be several other Kontrol servers on the planet, there could be one your Intranet or something that is public.

We are going to use the same previous example, but this time we are going to register the first kite to Kontrol and fetch the IP of it from the second kite:

package main

 

import (

"net/url"

 

"github.com/koding/kite"

)

 

func main() {

k := kite.New("first", "1.0.0")

k.Config.Port = 6000

k.HandleFunc("square", func(r *kite.Request) (interface{}, error) {

a := r.Args.One().MustFloat64()

return a * a, nil

})

 

k.Register(&url.URL{Scheme: "http", Host: "localhost:6000/kite"})

k.Run()

}

As you see we used the Register() method to register ourself to Kontrol. The only parameter we pass is our URL that others should be use to connect to us. This value will be stored in Kontrol and every other kite can fetch it from there. The Register() method is a special method that it’s automatically re-registers itself if you disconnect/connect again. To protect Kontrol we use the exponential backoff algorithm to try slowly. Because it’s also used heavily in production by Koding there are many little details and improvements like this. Also another detail here is, you don’t pass Kontrol’s URL while registration. Because you are already authenticated, Kontrol’s URL is stored in kite.key. All you need is to call Register().

Now let us search for the first kite and call it’s square method.

package main

 

import (

"fmt"

 

"github.com/koding/kite"

"github.com/koding/kite/protocol"

)

 

func main() {

k := kite.New("second", "1.0.0")

 

// search a kite that has the same username and environment as us, but the

// kite name should be "first"

kites, _ := k.GetKites(&protocol.KontrolQuery{

Username:    k.Config.Username,

Environment: k.Config.Environment,

Name:        "first",

})

 

// there might be several kites that matches our query

client := kites[0]

client.Dial()

 

response, _ := client.Tell("square", 4)

fmt.Println(response.MustFloat64())

}

First we use the GetKites() method to fetch a list of kites that matches our query. GetKites() connects to Kontrol and fetches all kites with their URL’s that matches the given query. The query needs to be in tree path form (same format as used in etcd), so Username and Environment needs to be given before you can search for a “first” kite. For this example we just assume there is one (which is) and pick up the first one, dial to it and run it. The output will be the same as with the previous one.

So the registiration and fetching kites dynamically is a huge thing. You can design your distributed system so it can tolerate certain criterias you define yourself. One example is, you could start 10 first kites each registered under your name. If the second kite fetches it from Kontrol, it will get a list that contains 10 first kites along with their URL. Now it’s all up to what the “second” kite is going to do. We can randomly pick one, we can call one by one (round-robin), we can ping all of them and select the fastest one, and so on …

So all this is left on the caller. Kontrol doesn’t have any idea of how a Kite behaves, it only knows if it’s connected (registered) or not. This simplicity allows the kite implementer to build more complexity on top of the protocol.

Conclusion

The Kite library has many other small improvements and features that we haven’t yet seen. For example there is Kite.js which can be used as a client side library on browsers. It also contains a node.js server equivalent (albeit not as finished as Go counterpart). It contains a tunnelproxy and reverseproxy out of the box, that can be used to multiplex kites behind a single port/app. It’s being used in production by Koding so it has many performance based fixes and improvements by default.

Writing Kites and using it is the most important part. Once you start to use it, you can feel the simplicity of the API. The Kite library is easy to use because it shares the same philosophy as Go. It uses some of the best open source projects written in Go (such as etcd). Go made it simple to write a stable platform as a foundation for the Kite library. Because of the nature of Go, extending and improvement the Kite library was easy too.

I hope you get the idea and intention of this library and its capabilities and limitations. We are using and maintaining it extensively. However there are many things we want to improve too (such as providing other message protocols and transport protocols). Feel free to fork the project (https://github.com/koding/kite) and play around. Contributions are welcome! Please let me know what you think of it.

FROM : https://blog.gopheracademy.com/birthday-bash-2014/kite-microservice-library/

发表在 kite | 留下评论

golang kite : 生成kite.key

FROM: http://stackoverflow.com/questions/30870404/distributed-microservices-using-kite-and-kontrol

export KONTROL_PORT=4099

export KONTROL_USERNAME="kontrol"

export KONTROL_STORAGE="etcd"

export KONTROL_KONTROLURL="http://127.0.0.1:4099/kite"

export KONTROL_PUBLICKEYFILE=~/certs/key_pub.pem

export KONTROL_PRIVATEKEYFILE=~/certs/key.pem

 

cd ~/certs/

openssl genrsa -out key.pem 2048

openssl rsa -in key.pem -pubout > key_pub.pem

发表在 kite | 留下评论

GoLang之命令行使用方法——flag package

go语言通过使用标准库里的flag包来处理命令行参数。

Package flag implements command-line flag parsing.

http://golang.org/pkg/flag/

http://golang.org/pkg/

 

几点注意事项:

1,通过flag.String(), Bool(), Int()等方式来定义命令行中需要使用的flag。

2,在定义完flag后,通过调用flag.Parse()来进行对命令行参数的解析。

3,命令行参数的格式可以是:

-flag xxx (使用空格,一个 - 符号)

--flag xxx (使用空格,两个 - 符号)

-flag=xxx (使用等号,一个 - 符号)

--flag=xxx (使用等号,两个 - 符号)

其中,布尔类型的参数防止解析时的二义性,应该使用等号的方式指定。

 

测试用例:

package main

import (
    "flag"
    "fmt"
)

var Input_pstrName = flag.String("name", "gerry", "input ur name")
var Input_piAge = flag.Int("age", 20, "input ur age")
var Input_flagvar int

func Init() {
    flag.IntVar(&Input_flagvar, "flagname", 1234, "help message for flagname")
}

func main() {
    Init()
    flag.Parse()

    // After parsing, the arguments after the flag are available as the slice flag.Args() or individually as flag.Arg(i). The arguments are indexed from 0 through flag.NArg()-1
    // Args returns the non-flag command-line arguments
    // NArg is the number of arguments remaining after flags have been processed
    fmt.Printf("args=%s, num=%d\n", flag.Args(), flag.NArg())
    for i := 0; i != flag.NArg(); i++ {
        fmt.Printf("arg[%d]=%s\n", i, flag.Arg(i))
    }

    fmt.Println("name=", *Input_pstrName)
    fmt.Println("age=", *Input_piAge)
    fmt.Println("flagname=", Input_flagvar)
}

/*
output:
mba:filter gerryyang$ ./test_flag --name "aaa" -age=123 -flagname=0x11 para1 para2 para3
args=[para1 para2 para3], num=3
arg[0]=para1
arg[1]=para2
arg[2]=para3
name= aaa
age= 123
flagname= 17
*/
发表在 golang | 留下评论

探索.git目录

http://www.cnblogs.com/zhongxinWang/p/4235448.html

发表在 git | 留下评论

git

http://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html

http://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html

http://www.ruanyifeng.com/blog/2014/06/git_remote.html

 

  • Workspace:工作区
  • Index / Stage:暂存区
  • Repository:仓库区(或本地仓库)
  • Remote:远程仓库

 

发表在 git | 留下评论

django sqlalchemy session query

http://docs.sqlalchemy.org/en/rel_1_1/orm/query.html

发表在 django, python | 留下评论

NSQ:分布式的实时消息平台

FROM : http://www.infoq.com/cn/news/2015/02/nsq-distributed-message-platform

NSQ是一个基于Go语言的分布式实时消息平台,它基于MIT开源协议发布,代码托管在GitHub,其当前最新版本是0.3.1版。NSQ可用于大规模系统中的实时消息服务,并且每天能够处理数亿级别的消息,其设计目标是为在分布式环境下运行的去中心化服务提供一个强大的基础架构。NSQ具有分布式、去中心化的拓扑结构,该结构具有无单点故障、故障容错、高可用性以及能够保证消息的可靠传递的特征。NSQ非常容易配置和部署,且具有最大的灵活性,支持众多消息协议。另外,官方还提供了拆箱即用Go和Python库。如果读者兴趣构建自己的客户端的话,还可以参考官方提供的协议规范

NSQ是由四个重要组件构成:

  • nsqd:一个负责接收、排队、转发消息到客户端的守护进程
  • nsqlookupd:管理拓扑信息并提供最终一致性的发现服务的守护进程
  • nsqadmin:一套Web用户界面,可实时查看集群的统计数据和执行各种各样的管理任务
  • utilities:常见基础功能、数据流处理工具,如nsq_stat、nsq_tail、nsq_to_file、nsq_to_http、nsq_to_nsq、to_nsq

NSQ的主要特点如下:

  • 具有分布式且无单点故障的拓扑结构 支持水平扩展,在无中断情况下能够无缝地添加集群节点
  • 低延迟的消息推送,参见官方提供的性能说明文档
  • 具有组合式的负载均衡和多播形式的消息路由
  • 既擅长处理面向流(高吞吐量)的工作负载,也擅长处理面向Job的(低吞吐量)工作负载
  • 消息数据既可以存储于内存中,也可以存储在磁盘中
  • 实现了生产者、消费者自动发现和消费者自动连接生产者,参见nsqlookupd
  • 支持安全传输层协议(TLS),从而确保了消息传递的安全性
  • 具有与数据格式无关的消息结构,支持JSON、Protocol Buffers、MsgPacek等消息格式
  • 非常易于部署(几乎没有依赖)和配置(所有参数都可以通过命令行进行配置)
  • 使用了简单的TCP协议且具有多种语言的客户端功能库
  • 具有用于信息统计、管理员操作和实现生产者等的HTTP接口
  • 为实时检测集成了统计数据收集器StatsD
  • 具有强大的集群管理界面,参见nsqadmin

为了达到高效的分布式消息服务,NSQ实现了合理、智能的权衡,从而使得其能够完全适用于生产环境中,具体内容如下:

  • 支持消息内存队列的大小设置,默认完全持久化(值为0),消息即可持久到磁盘也可以保存在内存中
  • 保证消息至少传递一次,以确保消息可以最终成功发送
  • 收到的消息是无序的, 实现了松散订购
  • 发现服务nsqlookupd具有最终一致性,消息最终能够找到所有Topic生产者

官方和第三方还为NSQ开发了众多客户端功能库,如官方提供的基于HTTP的nsqd、Go客户端go-nsq、Python客户端pynsq、基于Node.js的JavaScript客户端nsqjs、异步C客户端libnsq、Java客户端nsq-java以及基于各种语言的众多第三方客户端功能库。更多客户端功能库,请读者点击这里查看。

从NSQ的设计文档中得知,单个nsqd被设计为一次能够处理多个流数据,NSQ中的数据流模型是由stream和consumer组成。Topic是一种独特的stream,Channel是一个订阅了给定Topic的consumer逻辑分组。NSQ的数据流模型结构如下图所示:

从上图可以看出,单个nsqd可以有多个Topic,每个Topic又可以有多个Channel。Channel能够接收Topic所有消息的副本,从而实现了消息多播分发;而Channel上的每个消息被分发给它的订阅者,从而实现负载均衡,所有这些就组成了一个可以表示各种简单和复杂拓扑结构的强大框架。


NSQ最初为提供短链接服务的应用Bitly而开发。另外,还有众多著名的应用在使用NSQ,如社交新闻网站Digg、私密的社交应用Path、著名的开源的应用容器引擎Docker、支付公司Stripe、新闻聚合网站Buzzfeed、查看家人所在位置的移动应用Life360、网络工具公司SimpleReach等。

有兴趣的读者还可以为NSQ贡献代码、反馈问题或者通过官方提供的快速入门教程体验下NSQ为高吞吐量的网络服务带来的性能提升。有关NSQ的安装部署、客户端开发等相关信息,请登录其官网查看。

发表在 nsq | 留下评论

使用Metrics监控应用程序的性能

在编写应用程序的时候,通常会记录日志以便事后分析,在很多情况下是产生了问题之后,再去查看日志,是一种事后的静态分析。在很多时候,我们可能需要了解整个系统在当前,或者某一时刻运行的情况,比如当前系统中对外提供了多少次服务,这些服务的响应时间是多少,随时间变化的情况是什么样的,系统出错的频率是多少。这些动态的准实时信息对于监控整个系统的运行健康状况来说很重要。

一些应用程序,比如对外提供接口或者服务的WebService,对整个系统的实时运行情况进行监控显得尤为重要,着就像我们操作系统里面的资源管理器一样,如果能够实时或者准实时的看到整个系统耗费的CPU,内存等资源,对我们快速对系统做出响应,以及优化很重要。并且,这些实时的性能参数信息,对于一些高级应用场景,比如服务的熔断机制(需要实时统计系统出错比例和响应时间),只有做到了实时监控才能提供这些数据,才能实现这种提高系统稳健性的功能。

前几天在InfoQ的一篇采访文章中看到一句话 “Profiling特别重要。如果能有一个特别强大的Profiling系统,就知道整个系统在哪个地方,哪台机器上,花了多少CPU、内存、磁盘IO或者网络带宽等资源,才能知道优化什么地方效益最大。”

同样,对于WebService的监控,比如在那个地方,那台机器上,花了多少CPU,多少内存,每一个服务的响应时间,出错的次数频率等,这些信息记录下来之后,我们就可以看到服务在运行时的动态的表现,更加容易找出错误或者定位问题点来进行优化。

最简单的做法是,在应用系统的关键地方,或者所有程序的入口,出口进行埋点,然后将这些采样信息不断的发送到某一个消息队列或者内存DB中,然后其他系统进行读取分析和展示。

在Java中有一个开源的名为Metrics的项目,它能够捕获JVM以及应用层面的性能参数,他的作者Coda Hale介绍了什么是Mertics并且为什么Metrics在应用程序系统中很有必要,视频和演讲可以分别到YouTubeSlideShare上可以下载。在.NET中有对其进行移植的项目叫 Metrics.NET

在开始介绍Metric之前,需要先看看一些基本的度量器类型,以及常见的使用场景,才能知道如何使用。

一 度量类型

Metrics提供5种基本的度量类型:Gauges, Counters, Histograms, Meters和 Timers

Gauge

Gauge是最简单的度量类型,只有一个简单的返回值,他用来记录一些对象或者事物的瞬时值。

比如,我们类型为Gauge的计数器来记录某个服务目前开通的城市个数

Metric.Gauge("Service Cities Count", () => Cities.Count, new Unit("个"));

Counters

Counter是一个简单64位的计数器,他可以增加和减少。

比如我们可以定义两个Counter类型的计数器,用来统计所有服务请求数目,和目前正在处理的请求总数。

/// <summary>
/// keep the total count of the requests
/// </summary>
private readonly Counter totalRequestsCounter = Metric.Counter("Requests", Unit.Requests);

/// <summary>
/// count the current concurrent requests
/// </summary>
private readonly Counter concurrentRequestsCounter = Metric.Counter("SampleMetrics.ConcurrentRequests", Unit.Requests);

这样,在我们请求处理开始的时候,同时将这两个计数器自增。

this.concurrentRequestsCounter.Increment(); // increment concurrent requests counter
this.totalRequestsCounter.Increment(); // increment total requests counter

当某一个请求处理完成之后,将目前正在处理的请求减一

this.concurrentRequestsCounter.Decrement(); // decrement number of concurrent requests

这种计数器也可以用来统计诸如当前有多少人在线,或者服务器中有多少处于有效期内的session

Meters

Meter是一种只能自增的计数器,通常用来度量一系列事件发生的比率。他提供了平均速率,以及指数平滑平均速率,以及采样后的1分钟,5分钟,15分钟速率。

比如需要统计请求的速率,比如统计平均每分钟内有多少次请求进来。只需要定义一个metric

/// <summary>
/// measure the rate at which requests come in
/// </summary>
private readonly Meter meter = Metric.Meter("Requests", Unit.Requests,TimeUnit.Seconds);

在处理请求的地方,调用Mark方法即可。

this.meter.Mark(); // signal a new request to the meter

再比如,要测量服务出错的概率,比如每小时出错多少次。可以定义一个metric。

/// <summary>
/// measure the rate of service exception
/// </summary>
private readonly Meter errorMeter = Metric.Meter("Error", Unit.Errors, TimeUnit.Hours);

这样,在处理请求的时候,如果出现异常了,调用一下errorMeter的Mark方法即可。

this.errorMeter.Mark();// signal a new error to the meter

Histograms

Histrogram是用来度量流数据中Value的分布情况,Histrogram可以计算最大/小值、平均值,方差,分位数(如中位数,或者95th分位数),如75%,90%,98%,99%的数据在哪个范围内。

比如,我们想度量,所有传进来服务的请求参数的长度分布。那么,可以定义一个histogram。

/// <summary>
/// keep a histogram of the input data of our request method 
/// </summary>
private readonly Histogram histogramOfData = Metric.Histogram("ResultsExample", Unit.Items);

然后在请求的地方,调用其Update方法来更新值。

this.histogramOfData.Update(request.length, methodName); // update the histogram with the input data

Timer

Timer是Histogram跟Meter的一个组合,比如要统计当前请求的速率和处理时间。

就可以定义一个Timer:

/// <summary>
/// measure the time rate and duration of requests
/// </summary>
private readonly Timer timer = Metric.Timer("Requests", Unit.Requests);

在使用的时候,调用timer的NewContext即可。

using (this.timer.NewContext(i.ToString())) // measure until disposed
{
    ...
}

二 度量数据的输出

收集了这么多数据之后,我们需要把数据时实的动态展示或者保存起来。Metric提供了多种的数据报告接口。包括自带的Metrics.NET.FlotVisualization, 以及输出到专业的系统监控Graphite,输出到开源,分布式,时间序列的中InfluxDB,或者输出到ElasticSearch中。配置起来也非常简单。比如如果要直接在http页面上展现,只需要在初始化的时候,设置合适的EndPoint即可:

Metric.Config
    .WithHttpEndpoint("http://localhost:1234/metrics/")
    .WithAllCounters()
    .WithInternalMetrics()
    .WithReporting(config => config
        .WithConsoleReport(TimeSpan.FromSeconds(30))

然后在浏览器中输入 http://localhost:1234/metrics/,就可以看到各种采集的准实时各种度量信息:

metric

上面自带的性能DashBoard略显简陋。 通常,我们一般会将这些实时采集的数据存储到分布式时序数据库InfluxDB中,然后利用开源的图表控件Grafana来实时展现这些数据,比如,可以制作想下面这样的,动态的性能准实时监控系统:

metric_dashboard

templated_dashboard_graphite

nice_dashboard

三 总结

本文介绍了如何使用埋点和各种度量工具来实时监测应用程序的性能,介绍了.NET中Metrics度量工具的使用。与传统的记录日志的方式不同,这种实时或者准实时的对当前系统各种关键指标的采样和监控,对于应用程序的运维,性能优化,提供了一种动态的视角,能帮助我们更好的了解当前应用程序或者服务在线上的各种性能参数和表现状态。Metrics的采样应该尽量减少对原有系统的侵入性,所以一般的最好是将采样的结果存储到消息队列或者内存DB中,然后进行动态展示,另外采样频率也是需要考虑的一个重要因素。因为对于一个较大的系统来说,实时采样产生的数据量会比较大。InfluxDB 似乎只能在非Windows平台使用,所以本文没有完整演示整个Metrics+InfluxDB+Grafana 构建应用程序实时监控系统的搭建。不过相信大家对照着相关文档,应该不难实现。

希望本文对您了解如何实时监控应用程序性能,以及如何构建应用程序性能参数dashboard有所帮助。

四 参考资料

发表在 metrics | 留下评论

小议OAuth 2.0的state参数—从开发角度说《互联网最大规模帐号劫持漏洞即将引爆》

背景:正常的授权流程,用户点击授权后便不再可控,剩下的工作由第三方应用和授权服务器(资源提供方)进行交互来完成。而攻击者可以阻止授权流程的正常进行,将中间的关键URL截取下来,诱骗用户访问,成功后可以将受害人的账户绑定到攻击者的微博账户上。此后,攻击者可以使用微博的账户自由登入受害人的主站账户及浏览器账户,任意查看和修改用户的隐私数据。

有一个外国研究员在最近研究OAuth 2.0的登录过程中发现了许多程序员常犯的错误(见翻译文:http://www.freebuf.com/articles/web/5997.html ),原始地址见 http://homakov.blogspot.jp/2012/07/saferweb-most-common-oauth2.html 知道创宇测试国内各网站后发现确为如此,并在今天发表了《互联网最大规模帐号劫持漏洞即将引爆》(http://tech.ccidnet.com/art/32963/20121109/4448657_1.html )。但这篇文章本身的标题实在过于劲爆,同时微博出现了各种误传,结果成了一场个人看来并不必要的恐慌,以及口水战。这篇文章,主要从开发者的角度,来解释这个漏洞的来龙去脉,并以问答的形式给出一些个人建议和其他看法。

通俗的说一下这个漏洞的表现(虽然不全准确):你有一间房子,证明方法是你有一张房契,上面说“你和房子存在有效关系”。某一天,有个中介叫你签续租合同(带复写纸一式三份那种);你签了,结果被告知房子成了别人了。后经调查,续租合同的第二联,其实是一份转让合同。就这样,房契的关系就变了。

你也许懊恼,为什么没有好好检查这一式三份的合同是否一致。同样,在这个漏洞中,开发者就会懊恼,为什么没有好好使用并检查state参数?

但是在说明这个state参数前,有必要了解大部分程序员所写的绑定OAuth账号流程,由于绑定流程很多,这里挑最常见的“用户在第三方网站A上登录后,通过Authorization code方式绑定微博”流程(也是这个漏洞常见的场景流程):
(1)用户甲到第三方网站A登录后,到了绑定页面。此时还没绑定微博。
(2)绑定页面提供一个按钮:“绑定微博”(地址a:http://aaa.com/index.php?m=user_3rd_bind_sina)
(3)用户甲点击地址a,程序生成如下地址b(为方便大家查看,参数部分均未urlencode以【】包含显示):

https://api.weibo.com/oauth2/authorize?client_id=【9999999】&redirect_uri=【http://aaa.comindex.php?m=user_3rd_bind_sina_callback】&response_type=【code】

(4)用户甲浏览器定向到地址b,授权该应用。
(5)授权服务器根据传递的redirect_uri参数,组合认证参数code生成地址c:

http://aaa.comindex.php?m=user_3rd_bind_sina_callback&code=【809ui0asduve】

(6)用户甲浏览器返回到地址c,完成绑定。

咋看起来,好像没啥问题,毕竟code也是不可预测嘛。但是各开发者有没有想过,地址c实质上是和当前登录用户一点关系都没有的,因为地址c只能证明微博用户信息,但无法证明网站A的用户信息。所以漏洞就此产生了——假设有用户乙和丙,同时发起绑定请求,登录到不同的微博账号,然后在第5步后打住,互相交换地址c,会是什么结果?答案就是用户乙绑定了用户丙的微博,用户丙绑定了用户乙的微博了…...攻击者的目标,其实就是要获取地址c,然后诱骗已登录网站A的受害者点击,从而改变了绑定关系。

为应对这种情况,Oauth 2.0引入了state参数。state参数是什么?看看没多长时间前最终定稿的rfc6749(The OAuth 2.0 Authorization Framework, http://tools.ietf.org/html/rfc6749 ):

state: RECOMMENDED. An opaque value used by the client to maintain state between the request and callback.  The authorization server includes this value when redirecting the user-agent back to the client.  The parameter SHOULD be used for preventing cross-site request forgery as described in Section 10.12.
(推荐。一个由client使用的不透明参数,用于请求阶段和回调阶段之间的状态保持。此参数将在授权服务器重定向用户代理(如浏览器,译者注)回client时包含。该参数理应使用,以防止章节10.12中描述的CSRF。)(PS:不知咋翻译opaque value,只好翻译为“不透明参数”)

这个参数在许多开放平台上也会有提及,比如新浪微博的Oauth2/authorize(http://open.weibo.com/wiki/Oauth2/authorize ):
用于保持请求和回调的状态,在回调时,会在Query Parameter中回传该参数。开发者可以用这个参数验证请求有效性,也可以记录用户请求授权页前的位置。这个参数可用于防止跨站请求伪造(CSRF)攻击。

然而大多数开发者(包括许多官方SDK),会忽略使用这个state参数。所以,大面积的网站(不乏大站)确实存在这种漏洞,这也是知道创宇认为“互联网最大规模帐号劫持即将引爆”的根源。但是在我看来,有点危言耸听,因为利用条件实属有点苛刻:
(1)攻击者必须了解第三方网站可能的绑定特性,否则攻击极可能失败。绝大多数网站都是一个网站账号只能绑定一个OAuth提供方账号(比如微博帐号)。这种特性,导致这个漏洞在绝大多数网站根本无法快速撒网,只能定向劫持未绑定的用户到攻击者OAuth账号上,而攻击一次后,这个账号必须解绑才能用于别的攻击,导致利用难度增大不少。
(2)攻击者还必须了解被定向劫持用户的上网习惯。在这个漏洞中,绝大部分都需要受害者在第三方网站上处于登录状态,否则攻击基本失效。
(3)攻击者还必须了解第三方网站、以及用户在该第三方网站上存在的利益。现在的攻击有许多都是带有利益的,如果是电商类的话,网站本身的金钱利益驱动可能存在,但又需要判断该用户在该网站是否存在高价值,这就增加了额外的工作量;如果只是娱乐类网站,除了言论相关和用户所拥有的网站管理权,我还想不出有什么可以吸引攻击者去定向攻击。

以上各种条件造就了在攻击实施环节更像是那种一对一的淘宝或者QQ钓鱼手段、或者放入针对高价值目标的社工(或APT)一环中。就前者而言,淘宝、QQ甚至各类热门游戏的钓鱼量之大,安全研究者们应该更清楚;就后者而言,实质还有其它更有效地手段。那么,这个漏洞,还能冠以“最大规模帐号劫持”吗?我相信,地下产业者看到后只会轻蔑的笑一下,然后继续埋头干活……

对于开发者而言,要修复这个漏洞,就是必须加入state参数,这个参数既不可预测,又必须可以充分证明client和当前第三方网站的登录认证状态存在关联(如果存在过期时间更好)。见rfc6749 章节10.12:

The binding value used for CSRF protection MUST contain a non-guessable value (as described in Section 10.10), and the user-agent's authenticated state (e.g., session cookie, HTML5 local storage) MUST be kept in a location accessible only to the client and the user-agent (i.e., protected by same-origin policy).
(这个用于防御CSRF的绑定参数(即state参数,译者注)必须包含一个不可预测的数值(如同章节10.10的描述),还有用户代理(如浏览器,译者注)的认证状态必须保存在只允许client和用户代理两者访问的地方(也就是受到同源策略保护))

这听起来好复杂,其实,随机算一个字符串,然后保存在session,回调时检查state参数和session里面的值,就满足要求了。php示例如下:

构造Oauth2/authorize阶段:
$_SESSION['REQ_STATE'] = md5(uniqid(mt_rand(1, 100000), true));  //生成state参数,并存放于session
//$_SESSION['REQ_STATE'] = $_USER['uid']. '_'. $_USER['reg_ip']. '_'. $state;   //(如果还是怕随机不够,算法多考虑一些独特且不可预知的用户信息吧,reg_ip是个不错的选择)

callback回调检查:
$_state_check = !empty($_SESSION['REQ_STATE']) && ($_SESSION['REQ_STATE'] == $_GET['state']) ? true : false;   //callback回调检查

以下是问答环节:

一、普通用户问答环节

问:有微博提出,针对这个漏洞,解决方法是“微博用户立刻检查“我的应用”,暂取消所有应用授权,再根据需要重新授权”,是否正确?
答:就这个漏洞而言,完全错误!该漏洞的结果是第三方网站帐号被关联到攻击者的微博账号上,而不是自己的微博帐号被关联到攻击者的第三方网站账号。在“我的应用”中解除所有应用并不会对攻击者产生任何影响。还记得房子的例子吧?可以类比为:即使自己自杀,也不能导致房子和攻击者解除房契关系。
正确方法请看知道创宇的方案:“定期查看重要网站(比如你经常看优酷的话就检查优酷)的第三方帐号绑定页面,检查是否有陌生的其他帐号绑定到自身账户,如果发现应立即取消绑定或授权。”

问:如果有一个网站的帐号可绑定多个OAuth帐号(或反过来),我是否很容易中招?(即攻击难度是否降低成可以广撒网?)
答:明显是的。然而,从实际接触的业务来看,这种奇葩应用有理由相信存在量不会多,而且做不好绝对不只有这个漏洞,各平台也基本不允许这类应用存在。
真遇到了这类应用怎么办?如果你不是搞营销的,还是别碰这类应用稳妥,赶紧双向取消绑定吧(奇葩应用取消所有绑定,微博“我的应用”也取消)。

问:如何判断自己是否为高价值用户,容易遭受攻击?
答:简单判断方法就是,你在微博上经常晒自己购物的网站来源,还说自己中了优惠券,就有可能了;又或者你成大嘴了,就容易被盯上社工。但是实际上,偷账号的手段更多,这个漏洞对普通用户意义实在不大,该干啥就干啥吧!

二、开发者问答环节

问:这个漏洞究竟严不严重?
答:“安全无小事”。作为合格的开发者,如果你是对用户负责,又是高价值网站,那是确实挺高的。而综合目前的情况,我个人综合评价是低。原因已经在上面讲了。
而且个人觉得开发者过于关注这个漏洞,其实并不见得是好事,因为这个漏洞实质上暴露的是开发者们对OAuth协议(或者说各种第三方接入协议)的各种流程和参数了解得不足——当然这也有平台甚至协议本身的责任在,看看各wiki和sdk就知道了。历史上相关案例也不少,甚至更严重,比如《淘网址sina oauth认证登录漏洞》:WooYun: 淘网址sina oauth认证登录漏洞 。
基于以上原因,个人非常建议开发者对整个OAuth流程部分(包括登录、绑定、解绑等)都通盘检查一次;阅读各种wiki(如果可以读RFC,那就更好了),对OAuth的相关参数要有相当清晰的了解并合理地运用——但这个过程确实很长,我自己也没完全仔细看各种wiki和rfc,囧。平台的话,加强引导(尤其是SDK)还是很重要的,这点绝对要赞淘宝的@放翁_文初 (http://weibo.com/fangweng )。

问:如果设计为“在绑定状态下不允许更换,需要先解绑再绑定”,那是否意味着没有问题?
答:难说,假设攻击者可以CSRF解绑;或者你的绑定回调callback代码实际上没有检查绑定状态,一样可以遭受这个攻击。如果可以确定不存在上述问题,那还好。

问:为什么OAuth 1.0没有这个问题?
答:问这个问题的都是第三方应用的开发老手啊!想必也是和我一样被一路折磨而来,还不知道哪天到头啊!表示泪流满面!(群众:喂喂喂你干嘛这么激动,扔鸡蛋 -_-#)
其实不是没这个问题,而是OAuth 1.0时代,大多数开发者都会将REQUEST TOKEN写到session(或者和当前用户有关的地方中),攻击者获取到的REQUEST TOKEN根本无法跑到受害者的session中,并且ACCESS TOKEN的时候又必须要用REQUEST TOKEN来换,这样一来,无意间完成了client的用户登录状态维持和校验,这就救了大家一命啊!OAuth 2.0时代,没了三次握手,大多数开发者也没有留意到用户状态校验这点,所以就出事了。还是强调那句,state参数除了随机,还必须可以充分证明和当前网站的登录认证状态存在关联。

问:还是不明白state参数所说的“必须可以充分证明client和当前网站的登录认证状态存在关联”,不是随机就成了么?
答:只要state参数不能校验第三方网站的登陆认证状态,这个漏洞就生效。比如:

构造Oauth2/authorize阶段:
$state = md5(uniqid(mt_rand(1, 100000), true));  //生成state参数
set_cache($state, 1, 1800);  //$state作为cachekey存入缓存。

callback回调检查:
If(!empty($_GET['state'])){
$_state_check = get_cache($_GET['state']) == 1 ? true : false;   //这块缓存并不能证明是当前第三方网站的用户的,所以state并没有防御这个漏洞的效果
}else{
$_state_check = false;
}

解决方法还是要想方设法加入用户登录认证信息的校验,不愿想的话,session足矣。

问:如果想在参数state内传递多个参数,怎么办?
答:强烈建议在传递前进行一次base64编码!在这方面我吃过很大的亏,只是使用http_build_query而没有在最后再加一次base64_encode,导致在最后回调阶段的时候,因为php解析器自动帮我urldecode了参数state,导致数据不完整,哭死了!

摘自:http://mp.weixin.qq.com/s?__biz=MjM5NTg2NTU0Ng==&mid=207501892&idx=2&sn=738dfa2703f5b6b651b7feac1fe25ce5&scene=1&key=af154fdc40fed00394fcd25f8845f3c4ad43957405ea07b652c19b35e71716975ebf5150ebe4deaf626008ef6252daca&ascene=1&uin=MzI0MTA5NzU%3D&devicetype=Windows+7&version=61010029&pass_ticket=qY8W3lx558EdeCIh%2BEXxjz%2FJZtNaFlJttyl5WNYXNfE%3D

作者:justwinit@向东博客 专注WEB应用 构架之美 --- 构架之美,在于尽态极妍 | 应用之美,在于药到病除
地址:http://www.justwinit.cn/post/8138/
版权所有。转载时必须以链接形式注明作者和原始出处及本声明!

发表在 oauth2, webserver, 安全, 架构 | 留下评论

Python进阶之“属性(property)”详解

FROM : http://python.jobbole.com/80955/

Python中有一个被称为属性函数(property)的小概念,它可以做一些有用的事情。在这篇文章中,我们将看到如何能做以下几点:

  • 将类方法转换为只读属性
  • 重新实现一个属性的setter和getter方法

在本文中,您将学习如何以几种不同的方式来使用内置的属性函数。希望读到文章的末尾时,你能看到它是多么有用。

开始

使用属性函数的最简单的方法之一是将它作为一个方法的装饰器来使用。这可以让你将一个类方法转变成一个类属性。当我需要做某些值的合并时,我发现这很有用。其他想要获取它作为方法使用的人,发现在写转换函数时它很有用。让我们来看一个简单的例子:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

 

Python

########################################################################

class Person(object):

""""""

 

#----------------------------------------------------------------------

def __init__(self, first_name, last_name):

"""Constructor"""

self.first_name = first_name

self.last_name = last_name

 

#----------------------------------------------------------------------

@property

def full_name(self):

"""

Return the full name

"""

return "%s %s" % (self.first_name, self.last_name)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

########################################################################

class Person(object):

""""""

 

#----------------------------------------------------------------------

def __init__(self, first_name, last_name):

"""Constructor"""

self.first_name = first_name

self.last_name = last_name

 

#----------------------------------------------------------------------

@property

def full_name(self):

"""

Return the full name

"""

return "%s %s" % (self.first_name, self.last_name)

[Format Time: 0.0025 seconds]

在上面的代码中,我们创建了两个类属性:self.first_name和self.last_name。接下来,我们创建了一个full_name方法,它有一个@property装饰器。这使我们能够在Python解释器会话中有如下的交互:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

Python

>>> person = Person("Mike", "Driscoll")

>>> person.full_name

'Mike Driscoll'

>>> person.first_name

'Mike'

>>> person.full_name = "Jackalope"

Traceback (most recent call last):

File "<string>", line 1, in <fragment>

AttributeError: can't set attribute

1

2

3

4

5

6

7

8

9

>>> person = Person("Mike", "Driscoll")

>>> person.full_name

'Mike Driscoll'

>>> person.first_name

'Mike'

>>> person.full_name = "Jackalope"

Traceback (most recent call last):

File "<string>", line 1, in <fragment>

AttributeError: can't set attribute

[Format Time: 0.0020 seconds]

正如你所看到的,因为我们将方法变成了属性,我们可以使用正常的点符号访问它。但是,如果我们试图将该属性设为其他值,我们会引发一个AttributeError错误。改变full_name属性的唯一方法是间接这样做:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

Python

>>> person.first_name = "Dan"

>>> person.full_name

'Dan Driscoll'

1

2

3

>>> person.first_name = "Dan"

>>> person.full_name

'Dan Driscoll'

[Format Time: 0.0007 seconds]

这是一种限制,因此让我们来看看另一个例子,其中我们可以创建一个允许设置的属性。

使用Python property取代setter和getter方法

让我们假设我们有一些遗留代码,它们是由一些对Python理解得不够好的人写的。如果你像我一样,你之前已经看到过这类的代码:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

 

Python

from decimal import Decimal

 

########################################################################

class Fees(object):

""""""

 

#----------------------------------------------------------------------

def __init__(self):

"""Constructor"""

self._fee = None

 

#----------------------------------------------------------------------

def get_fee(self):

"""

Return the current fee

"""

return self._fee

 

#----------------------------------------------------------------------

def set_fee(self, value):

"""

Set the fee

"""

if isinstance(value, str):

self._fee = Decimal(value)

elif isinstance(value, Decimal):

self._fee = value

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

from decimal import Decimal

 

########################################################################

class Fees(object):

""""""

 

#----------------------------------------------------------------------

def __init__(self):

"""Constructor"""

self._fee = None

 

#----------------------------------------------------------------------

def get_fee(self):

"""

Return the current fee

"""

return self._fee

 

#----------------------------------------------------------------------

def set_fee(self, value):

"""

Set the fee

"""

if isinstance(value, str):

self._fee = Decimal(value)

elif isinstance(value, Decimal):

self._fee = value

[Format Time: 0.0032 seconds]

要使用这个类,我们必须要使用定义的getter和setter方法 :

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

Python

>>> f = Fees()

>>> f.set_fee("1")

>>> f.get_fee()

Decimal('1')

1

2

3

4

>>> f = Fees()

>>> f.set_fee("1")

>>> f.get_fee()

Decimal('1')

[Format Time: 0.0010 seconds]

如果你想添加可以使用正常点符号访问的属性,而不破坏所有依赖于这段代码的应用程序,你可以通过添加一个属性函数非常简单地改变它:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

 

Python

from decimal import Decimal

 

########################################################################

class Fees(object):

""""""

 

#----------------------------------------------------------------------

def __init__(self):

"""Constructor"""

self._fee = None

 

#----------------------------------------------------------------------

def get_fee(self):

"""

Return the current fee

"""

return self._fee

 

#----------------------------------------------------------------------

def set_fee(self, value):

"""

Set the fee

"""

if isinstance(value, str):

self._fee = Decimal(value)

elif isinstance(value, Decimal):

self._fee = value

 

fee = property(get_fee, set_fee)

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

from decimal import Decimal

 

########################################################################

class Fees(object):

""""""

 

#----------------------------------------------------------------------

def __init__(self):

"""Constructor"""

self._fee = None

 

#----------------------------------------------------------------------

def get_fee(self):

"""

Return the current fee

"""

return self._fee

 

#----------------------------------------------------------------------

def set_fee(self, value):

"""

Set the fee

"""

if isinstance(value, str):

self._fee = Decimal(value)

elif isinstance(value, Decimal):

self._fee = value

 

fee = property(get_fee, set_fee)

[Format Time: 0.0035 seconds]

我们在这段代码的末尾添加了一行。现在我们可以这样做:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

Python

>>> f = Fees()

>>> f.set_fee("1")

>>> f.fee

Decimal('1')

>>> f.fee = "2"

>>> f.get_fee()

Decimal('2')

1

2

3

4

5

6

7

>>> f = Fees()

>>> f.set_fee("1")

>>> f.fee

Decimal('1')

>>> f.fee = "2"

>>> f.get_fee()

Decimal('2')

[Format Time: 0.0013 seconds]

正如你所看到的,当我们以这种方式使用属性函数时,它允许fee属性设置并获取值本身而不破坏原有代码。让我们使用属性装饰器来重写这段代码,看看我们是否能得到一个允许设置的属性值。

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

 

Python

from decimal import Decimal

 

########################################################################

class Fees(object):

""""""

 

#----------------------------------------------------------------------

def __init__(self):

"""Constructor"""

self._fee = None

 

#----------------------------------------------------------------------

@property

def fee(self):

"""

The fee property - the getter

"""

return self._fee

 

#----------------------------------------------------------------------

@fee.setter

def fee(self, value):

"""

The setter of the fee property

"""

if isinstance(value, str):

self._fee = Decimal(value)

elif isinstance(value, Decimal):

self._fee = value

 

#----------------------------------------------------------------------

if __name__ == "__main__":

f = Fees()

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

from decimal import Decimal

 

########################################################################

class Fees(object):

""""""

 

#----------------------------------------------------------------------

def __init__(self):

"""Constructor"""

self._fee = None

 

#----------------------------------------------------------------------

@property

def fee(self):

"""

The fee property - the getter

"""

return self._fee

 

#----------------------------------------------------------------------

@fee.setter

def fee(self, value):

"""

The setter of the fee property

"""

if isinstance(value, str):

self._fee = Decimal(value)

elif isinstance(value, Decimal):

self._fee = value

 

#----------------------------------------------------------------------

if __name__ == "__main__":

f = Fees()

[Format Time: 0.0038 seconds]

上面的代码演示了如何为fee属性创建一个setter方法。你可以用一个名为@fee.setter的装饰器装饰第二个方法名也为fee的方法来实现这个。当你如下所做时,setter被调用:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

Python

>>> f = Fees()

>>> f.fee = "1"

1

2

>>> f = Fees()

>>> f.fee = "1"

[Format Time: 0.0007 seconds]

如果你看属性函数的说明,它有fget, fset, fdel和doc几个参数。如果你想对属性使用del命令,你可以使用@fee.deleter创建另一个装饰器来装饰相同名字的函数从而实现删除的同样效果。

结束语

现在你知道如何在你的类中使用Python的属性函数。希望你能找到更有用的方式,在你的代码中使用它们。

补充阅读:

发表在 python | 留下评论

python __init__.py和__all__

__init__.py是创建python包时用的,详见文章:http://www.cnblogs.com/tqsummer/archive/2011/01/24/1943273.html

python中的Module是比较重要的概念。常见的情况是,事先写好一个.py文 件,在另一个文件中需要import时,将事先写好的.py文件拷贝 到当前目录,或者是在sys.path中增加事先写好的.py文件所在的目录,然后import。这样的做法,对于少数文件是可行的,但如果程序数目很 多,层级很复杂,就很吃力了。

有没有办法,像Java的Package一样,将多个.py文件组织起来,以便在外部统一调用,和在内部互相调用呢?答案是有的。

主要是用到python的包的概念,python __init__.py在包里起一个比较重要的作用

要弄明白这个问题,首先要知道,python在执行import语句时,到底进行了什么操作,按照python的文档,它执行了如下操作:

第1步,创建一个新的,空的module对象(它可能包含多个module);

第2步,把这个module对象插入sys.module中

第3步,装载module的代码(如果需要,首先必须编译)

第4步,执行新的module中对应的代码。

 

在执行第3步时,首先要找到module程序所在的位置,其原理为:

如 果需要导入的module的名字是m1,则解释器必须找到m1.py,它首先在当前目录查找,然后是在环境变量PYTHONPATH中查找。 PYTHONPATH可以视为系统的PATH变量一类的东西,其中包含若干个目录。如果PYTHONPATH没有设定,或者找不到m1.py,则继续搜索 与python的安装设置相关的默认路径,在Unix下,通常是/usr/local/lib/python。

事实上,搜索的顺序是:当前路径 (以及从当前目录指定的sys.path),然后是PYTHONPATH,然后是python的安装设置相关的默认路径。正因为存在这样的顺序,如果当前 路径或PYTHONPATH中存在与标准module同样的module,则会覆盖标准module。也就是说,如果当前目录下存在xml.py,那么执 行import xml时,导入的是当前目录下的module,而不是系统标准的xml。

 

了解了这些,我们就可以先构建一个package,以普通module的方式导入,就可以直接访问此package中的各个module了。

 

Python中的package定义很简单,其层次结构与程序所在目录的层次结构相同,这一点与Java类似,唯一不同的地方在于,python中的package必须包含一个__init__.py的文件。

例如,我们可以这样组织一个package:

 

package1/

__init__.py

subPack1/

__init__.py

module_11.py

module_12.py

module_13.py

subPack2/

__init__.py

module_21.py

module_22.py

……

 

__init__.py可以为空,只要它存在,就表明此目录应被作为一个package处理。当然,__init__.py中也可以设置相应的内容,下文详细介绍。

 

好了,现在我们在module_11.py中定义一个函数:

 

def funA():

print "funcA in module_11"

return

 

在顶层目录(也就是package1所在的目录,当然也参考上面的介绍,将package1放在解释器能够搜索到的地方)运行python:

 

>>>from package1.subPack1.module_11 import funcA

>>>funcA()

funcA in module_11

 

这样,我们就按照package的层次关系,正确调用了module_11中的函数。

 

细心的用户会发现,有时在import语句中会出现通配符*,导入某个module中的所有元素,这是怎么实现的呢?

答案就在__init__.py中。我们在subPack1的__init__.py文件中写

 

__all__ = [‘module_13‘, ‘module_12‘]

 

然后进入python

 

>>>from package1.subPack1 import *

>>>module_11.funcA()

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

ImportError: No module named module_11

 

也就是说,以*导入时,package内的module是受__init__.py限制的。

 

好了,最后来看看,如何在package内部互相调用。

如果希望调用同一个package中的module,则直接import即可。也就是说,在module_12.py中,可以直接使用

 

import module_11

 

如果不在同一个package中,例如我们希望在module_21.py中调用module_11.py中的FuncA,则应该这样:

 

from module_11包名.module_11 import funcA

2、__all__

在模块(*.py)中使用意为导出__all__列表里的类、函数、变量等成员,否则将导出modualA中所有不以下划线开头(私有)的成员,在模块中使用__all__属性可避免在相互引用时的命名冲突

python __init__.py和__all__

原文:http://www.cnblogs.com/sevenguin/p/4276992.html

发表在 python | 留下评论

Python Import机制备忘-模块搜索路径(sys.path)、嵌套Import、package Import 、sys.modules

Python中所有加载到内存的模块都放在sys.modules。当import一个模块时首先会在这个列表中查找是否已经加载了此模块,如果加载了则只是将模块的名字加入到正在调用import的模块的Local名字空间中。如果没有加载则从sys.path目录中按照模块名称查找模块文件,模块文件可以是py、pyc、pyd,找到后将模块载入内存,并加入到sys.modules中,并将名称导入到当前的Local名字空间。

http://www.cnblogs.com/qq78292959/archive/2013/05/17/3083961.html

扩展知识:

Python在import其它模块时,是从sys.path中搜索的。

sys.path的初始值会受到PYTHONPATH、PYTHONHOME等环境变量的影响。

也可以在脚本运行过程中动态修改sys.path从而import自己需要的模块。

 

模块的搜索路径

模块的搜索路径都放在了sys.path列表中,如果缺省的sys.path中没有含有自己的模块或包的路径,可以动态的加入(sys.path.apend)即可。下面是sys.path在Windows平台下的添加规则。

1、sys.path第一个路径往往是主模块所在的目录。在交互环境下添加一个空项,它对应当前目录。

2、如果PYTHONPATH环境变量存在,sys.path会加载此变量指定的目录。

3、我们尝试找到Python Home,如果设置了PYTHONHOME环境变量,我们认为这就是Python Home,否则,我们使用python.exe所在目录找到lib/os.py去推断Python Home。

如果我们确实找到了Python Home,则相关的子目录(Lib、plat-win、lib-tk等)将以Python Home为基础加入到sys.path,并导入(执行)lib/site.py,将site-specific目录及其下的包加入。

如果我们没有找到Python Home,则把注册表Software/Python/PythonCore/2.5/PythonPath的项加入sys.path(HKLM和 HKCU合并后加入),但相关的子目录不会自动添加的。

4、如果我们没有找到Python Home,并且没有PYTHONPATH环境变量,并且不能在注册表中找到PythonPath,那么缺省相对路径将加入(如:./Lib;./plat-win等)。

总结如下

当在安装好的主目录中运行Python.exe时,首先推断Python Home,如果找到了PythonHome,注册表中的PythonPath将被忽略;否则将注册表的PythonPath加入。

如果PYTHONPATH环境变量存在,sys.path肯定会加载此变量指定的目录。

如果Python.exe在另外的一个目录下(不同的目录,比如通过COM嵌入到其他程序),Python Home将不推断,此时注册表的PythonPath将被使用。

如果Python.exe不能发现他的主目录(PythonHome),并且注册表也没有PythonPath,则将加入缺省的相对目录。

 

标准Import

Python中所有加载到内存的模块都放在sys.modules。当import一个模块时首先会在这个列表中查找是否已经加载了此模块,如果加载了则只是将模块的名字加入到正在调用import的模块的Local名字空间中。如果没有加载则从sys.path目录中按照模块名称查找模块文件,模块文件可以是py、pyc、pyd,找到后将模块载入内存,并加入到sys.modules中,并将名称导入到当前的Local名字空间。

可以看出了,一个模块不会重复载入。多个不同的模块都可以用import引入同一个模块到自己的Local名字空间,其实背后的PyModuleObject对象只有一个。

说一个容易忽略的问题,import只能导入模块,不能导入模块中的对象(类、函数、变量等)。如一个模块A(A.py)中有个函数getName,另一个模块不能通过import A.getName将getName导入到本模块,只能用import A。如果想只导入特定的类、函数、变量则用from A import getName即可。

嵌套Import

嵌套import,我分两种情况,一种是:本模块导入A模块(import A),而A中又有import语句,会激活另一个import动作,如import B,而B模块又可以import其他模块,一直下去。

对这种嵌套比较容易理解,注意一点就是各个模块的Local名字空间是独立的,所以上面的例子,本模块import A完了后本模块只能访问模块A,不能访问B及其他模块。虽然模块B已经加载到内存了,如果要访问还要在明确的在本模块中import B。

另外一种嵌套指,在模块A中import B,而在模块B中import A。这时会怎么样呢?这个在Python列表中由RobertChen给出了详细解释,抄录如下:

[A.py]

from B import D

class C:pass

 

[B.py]

from A import C

class D:pass

为什么执行A的时候不能加载D呢?

如果将A.py改为:import B就可以了。

这是怎么回事呢?

RobertChen:这跟Python内部import的机制是有关的,具体到from B import D,Python内部会分成几个步骤:

  1. 在sys.modules中查找符号"B"
  2. 果符号B存在,则获得符号B对应的module对象<module B>。 从<module B>的__dict__中获得符号"D"对应的对象,如果"D"不存在,则抛出异常
  3. 如果符号B不存在,则创建一个新的module对象<module B>,注意,这时,module对象的__dict__为空。
    执行B.py中的表达式,填充<module B>的__dict__ 。 从<module B>的__dict__中获得"D"对应的对象,如果"D"不存在,则抛出异常。

所以,这个例子的执行顺序如下:

1、执行A.py中的from B import D

由于是执行的python A.py,所以在sys.modules中并没有<module B>存在,首先为B.py创建一个module对象(<module B>),注意,这时创建的这个module对象是空的,里边啥也没有,在Python内部创建了这个module对象之后,就会解析执行B.py,其目的是填充<module B>这个dict。

2、执行B.py中的from A import C

在执行B.py的过程中,会碰到这一句,首先检查sys.modules这个module缓存中是否已经存在<module A>了,由于这时缓存还没有缓存<module A>,所以类似的,Python内部会为A.py创建一个module对象(<module A>),然后,同样地,执行A.py中的语句。

3、再次执行A.py中的from B import D

这时,由于在第1步时,创建的<module B>对象已经缓存在了sys.modules中,所以直接就得到了<module B>,但是,注意,从整个过程来看,我们知道,这时<module B>还是一个空的对象,里面啥也没有,所以从这个module中获得符号"D"的操作就会抛出异常。如果这里只是import B,由于"B"这个符号在sys.modules中已经存在,所以是不会抛出异常的。

上面的解释已经由Zoom.Quiet收录在啄木鸟了,里面有图,可以参考一下。

Package(包) Import

包(Package)可以看成模块的集合,只要一个文件夹下面有个__init__.py文件,那么这个文件夹就可以看做是一个包。包下面的文件夹还可以成为包(子包)。更进一步,多个较小的包可以聚合成一个较大的包,通过包这种结构,方便了类的管理和维护,也方便了用户的使用。比如SQLAlchemy等都是以包的形式发布给用户的。

包和模块其实是很类似的东西,如果查看包的类型import SQLAlchemy type(SQLAlchemy),可以看到其实也是<type 'module'>。import包的时候查找的路径也是sys.path。

包导入的过程和模块的基本一致,只是导入包的时候会执行此包目录下的__init__.py而不是模块里面的语句了。另外,如果只是单纯的导入包,而包的__init__.py中又没有明确的其他初始化操作,那么此包下面的模块是不会自动导入的。如:

PA

--__init__.py

--wave.py

--PB1

--__init__.py

--pb1_m.py

--PB2

--__init__.py

--pb2_m.py

__init__.py都为空,如果有以下程序:

 

  1. import sys
  2. import PA.wave  #1
  3. import PA.PB1   #2
  4. import PA.PB1.pb1_m as m1  #3
  5. import PA.PB2.pb2_m #4
  6. PA.wave.getName() #5
  7. m1.getName() #6
  8. PA.PB2.pb2_m.getName() #7

当执行#1后,sys.modules会同时存在PA、PA.wave两个模块,此时可以调用PA.wave的任何类或函数了。但不能调用PA.PB1(2)下的任何模块。当前Local中有了PA名字。

当执行#2后,只是将PA.PB1载入内存,sys.modules中会有PA、PA.wave、PA.PB1三个模块,但是PA.PB1下的任何模块都没有自动载入内存,此时如果直接执行PA.PB1.pb1_m.getName()则会出错,因为PA.PB1中并没有pb1_m。当前Local中还是只有PA名字,并没有PA.PB1名字。

当执行#3后,会将PA.PB1下的pb1_m载入内存,sys.modules中会有PA、PA.wave、PA.PB1、PA.PB1.pb1_m四个模块,此时可以执行PA.PB1.pb1_m.getName()了。由于使用了as,当前Local中除了PA名字,另外添加了m1作为PA.PB1.pb1_m的别名。

当执行#4后,会将PA.PB2、PA.PB2.pb2_m载入内存,sys.modules中会有PA、PA.wave、PA.PB1、PA.PB1.pb1_m、PA.PB2、PA.PB2.pb2_m六个模块。当前Local中还是只有PA、m1。

下面的#5,#6,#7都是可以正确运行的。

注意的是:如果PA.PB2.pb2_m想导入PA.PB1.pb1_m、PA.wave是可以直接成功的。最好是采用明确的导入路径,对于./..相对导入路径还是不推荐用。

发表在 python | 留下评论

在python中实现动态导入模块importlib.import_module

有时候,我们很需要在程序运行的过程中,根据变量或者配置动态的决定导入哪个模块。

假设我们需要导入的模块module_12定义在subPack1文件夹下面,内容如下。

def funcA12():

print('funcA12 in module_12')

return

import os

print  os.path.join(os.path.abspath(os.path.dirname(__file__)),('templates'))

print os.path.join(os.path.relpath(os.path.dirname(__file__)),'..templates')

在这个模块的上层需要动态导入这个模块,可以使用下面的代码。

使用importlib模块的import_module方法就可以实现动态的导入。

import importlib

mo =importlib.import_module('subPack1.module_12')

mo.funcA12()

本文出自 “突破中的IT结构师” 博客,请务必保留此出处http://virusswb.blog.51cto.com/115214/794685

发表在 python | 留下评论

django处理流程分析

FROM: http://blog.csdn.net/skyie53101517/article/details/50409478

一、 处理过程的核心概念

如下图所示django的总览图,整体上把握以下django的组成:

django整体架构图

核心在于中间件middleware,django所有的请求、返回都由中间件来完成。

 

中间件,就是处理HTTP的request和response的,类似插件,比如有Request中间件、view中间件、response中间件、exception中间件等,Middleware都需要在 “project/settings.py” 中 MIDDLEWARE_CLASSES 的定义。大致的程序流程图如下所示:

整体流程

首先,Middleware都需要在 “project/settings.py” 中 MIDDLEWARE_CLASSES 的定义, 一个HTTP请求,将被这里指定的中间件从头到尾处理一遍,暂且称这些需要挨个处理的中间件为处理链,如果链中某个处理器处理后没有返回response,就把请求传递给下一个处理器;如果链中某个处理器返回了response,直接跳出处理链由response中间件处理后返回给客户端,可以称之为短路处理。

二、 中间件中的方法

Django处理一个Request的过程是首先通过中间件,然后再通过默认的URL方式进行的。我们可以在Middleware这个地方把所有Request拦截住,用我们自己的方式完成处理以后直接返回Response。因此了解中间件的构成是非常必要的。

Initializer: __init__(self)

出于性能的考虑,每个已启用的中间件在每个服务器进程中只初始化一次。也就是说 __init__() 仅在服务进程启动的时候调用,而在针对单个request处理时并不执行。

对一个middleware而言,定义 __init__() 方法的通常原因是检查自身的必要性。如果 __init__() 抛出异常 django.core.exceptions.MiddlewareNotUsed ,则Django将从middleware栈中移出该middleware。

在中间件中定义 __init__() 方法时,除了标准的 self 参数之外,不应定义任何其它参数。

Request预处理函数: process_request(self, request)

这个方法的调用时机在Django接收到request之后,但仍未解析URL以确定应当运行的view之前。Django向它传入相应的 HttpRequest 对象,以便在方法中修改。

process_request() 应当返回 None 或 HttpResponse 对象.

如果返回 None , Django将继续处理这个request,执行后续的中间件, 然后调用相应的view.

如果返回 HttpResponse 对象,Django 将不再执行任何其它的中间件(无视其种类)以及相应的view。 Django将立即返回该 HttpResponse .

View预处理函数: process_view(self, request, callback, callback_args, callback_kwargs)

这个方法的调用时机在Django执行完request预处理函数并确定待执行的view之后,但在view函数实际执行之前。

request

HttpRequest 对象。

callback

Django将调用的处理request的Python函数. 这是实际的函数对象本身, 而不是字符串表述的函数名。

args

将传入view的位置参数列表,但不包括 request 参数(它通常是传入view的第一个参数)
kwargs

将传入view的关键字参数字典.

如同 process_request() , process_view() 应当返回 None 或 HttpResponse 对象。

如果返回 None , Django将继续处理这个 request ,执行后续的中间件, 然后调用相应的view

如果返回 HttpResponse 对象,Django 将不再执行 任何 其它的中间件(不论种类)以及相应的view。Django将立即返回

Response后处理函数: process_response(self, request, response)

这个方法的调用时机在Django执行view函数并生成response之后。

该处理器能修改response的内容;一个常见的用途是内容压缩,如gzip所请求的HTML页面。

这个方法的参数相当直观: request 是request对象,而 response 则是从view中返回的response对象。

process_response() 必须 返回 HttpResponse 对象. 这个response对象可以是传入函数的那一个原始对象(通常已被修改),也可以是全新生成的。

Exception后处理函数: process_exception(self, request, exception)

这个方法只有在request处理过程中出了问题并且view函数抛出了一个未捕获的异常时才会被调用。这个钩子可以用来发送错误通知,将现场相关信息输出到日志文件, 或者甚至尝试从错误中自动恢复。

这个函数的参数除了一贯的 request 对象之外,还包括view函数抛出的实际的异常对象 exception 。

process_exception() 应当返回 None 或 HttpResponse 对象.

如果返回 None , Django将用框架内置的异常处理机制继续处理相应request。

如果返回 HttpResponse 对象, Django 将使用该response对象,而短路框架内置的异常处理机制

以下几章会详细介绍该机制。

三、 Django目录结构

dsf

conf

主要有两个作用:1) 处理全局配置, 比如数据库、加载的应用、 MiddleWare等 2) 处理urls配置, 就是url与view的映射关系。

contrib (贡献)

由Django的开发者贡献的功能模块,不过既然都已经随版本发布, 就表示是官方的。

core

Django的核心处理库,包括url分析、处理请求、缓存等,其中处理请求是核心了,比如处理fastcgi就是由wsgi.py处理。

db

顾名思义,处理与数据库相关的,就是ORM。

dispatch (分派,派遣)

其实这不是Django原创,是pydispatch库,主要处 理消费者-工作者模式。

forms && newforms && oldforms

处理html的表单,不用多介绍。

middleware

中间件,就是处理HTTP的request和response的,类似插件。比 如默认的common中间件的一个功能:当一个页面没有找对对应的 pattern时, 会自定加上‘/’重新处理。比如访问/blog时,而定义的pattern是‘^blog/$’, 所以找不到对应的pattern,会自动再用/blog/查找,当然前提是 APPEND_SLASH=True。

template

Django的模板

templatetags

处理Application的tag的wrapper,就是将INSTALLED_APPS中 所有的templatetags目录添加到 django.templatetags目录中,则当使用 {{load blog}}记载tag时,就可以使用 import django.templatetags.blog 方式加载了。不过这有一个问题,如果其他Application目录中也有blog.py, 这会加载第一个出现blog.py的tag。其实在 Django中,有许多需要处理重名 的地方,比如template,需要格外小心,这个后续在介绍。

utils

公共库,很多公用的类都在放在这里。

views

最基本的view方法。

四、 Django 术语

在应用 Django 的时候,我们经常听到一些术语:

Project

指一个完整的Web服务,一般由多个模块组成。

Application

可以理解为模块,比如用户管理、博客管理等,包含了数据的组成和数据的显示,Applicaiton都需要在 “project/settings.py” 中 INSTALLED_APPS 的定义。

Middleware

就是处理request和response的插件, Middleware都需要在 “project/settings.py” 中 MIDDLEWARE_CLASSES 的定义。

Loader

模板加载器, 其实就是为了读取 Template 文件的类,默认的有通过文件系统加载和在 “Application/templates” 目录中加载,Loader都需要在 “project/settings.py” 中 TEMPLATE_LOADERS 的定义。

五、 处理流程

其实和其他Web框架一样,HTTP处理的流程大致相同,

Django处理一个Request的过程是首先通过中间件,然后再通过默认的URL方式进行的。我们可以在Middleware这个地方把所有Request拦截住,用我们自己的方式完成处理以后直接返回Response。

1. 加载配置

Django的配置都在 “Project/settings.py” 中定义,可以是Django的配置,也 可以是自定义的配置,并且都通过 django.conf.settings 访问,非常方便。

2. 启动

最核心动作的是通过 django.core.management.commands.runfcgi 的 Command 来启动,它运行 django.core.servers.fastcgi 中的 runfastcgi , runfastcgi 使用了 flup 的 WSGIServer 来启动 fastcgi 。而 WSGIServer 中携带了 django.core.handlers.wsgi 的 WSGIHandler 类的一个实例,通过 WSGIHandler来处理由Web服务器(比如Apache,Lighttpd等)传过来的请求,此 时才是真正进入Django的世界。

3. 处理 Request

当有HTTP请求来时, WSGIHandler 就开始工作了,它从 BaseHandler 继承而来。 WSGIHandler 为每个请求创建一个 WSGIRequest 实例,而 WSGIRequest 是从 http.HttpRequest 继承而来。接下来就开始创建 Response 了.

4. 创建Response

BaseHandler 的 get_response 方法就是根据 request 创建 response , 而 具体生成 response 的动作就是执行 urls.py 中对应的view函数了,这也是 Django可以处理“友好URL”的关键步骤,每个这样的函数都要返回一个 Response 实例。此时一般的做法是通过 loader 加载 template 并生成页面内 容,其中重要的就是通过 ORM 技术从数据库中取出数据,并渲染到 Template 中,从而生成具体的页面了

5. 处理Response

Django 返回 Response 给 flup , flup 就取出 Response 的内容返回给 Web 服务器,由后者返回给浏览器。

总之, Django 在 fastcgi 中主要做了两件事:处理 Request 和创建 Response , 而它们对应的核心就是“urls分析”、“模板技术”和“ORM技术”

12

如图所示,一个HTTP请求,首先被转化成一个HttpRequest对象,然后该对象被传递给Request中间件处理,如果该中间件返回了Response,则直接传递给Response中间件做收尾处理。否则的话Request中间件将访问URL配置,确定哪个view来处理,在确定了哪个view要执行,但是还没有执行该view的时候,系统会把request传递给View中间件处理器进行处理,如果该中间件返回了Response,那么该Response直接被传递给Response中间件进行后续处理,否则将执行确定的View函数处理并返回Response,在这个过程中如果引发了异常并抛出,会被Exception中间件处理器进行处理。

六、 详细全流程

一个 Request 到达了!

首先发生的是一些和 Django有关(前期准备)的其他事情,分别是:

1. 如果是 Apache/mod_python 提供服务,request 由 mod_python 创建的 django.core.handlers.modpython.ModPythonHandler 实例传递给 Django。

2. 如果是其他服务器,则必须兼容 WSGI,这样,服务器将创建一个 django.core.handlers.wsgi.WsgiHandler 实例。

这两个类都继承自 django.core.handlers.base.BaseHandler,它包含对任何类 型的 request 来说都需要的公共代码。

有一个处理器(Handler)了

当上面其中一个处理器实例化后,紧接着发生了一系列的事情:

1. 这个处理器(handler)导入你的 Django 配置文件。

2. 这个处理器导入 Django 的自定义异常类。

3. 这个处理器调用它自己的 load_middleware 方法,加载所有列在 MIDDLEWARE_CLASSES中的 middleware 类并且内省它们。

最后一条有点复杂,我们仔细瞧瞧。

一 个 middleware 类可以渗入处理过程的四个阶段:request,view,response 和 exception。要做到这一点,只需要定义指定的、恰当的方 法:process_request,process_view, process_response 和 process_exception。middleware 可以定义其中任何一个或所有这些方法,这取 决于你想要它提供什么样的功能。

当处理器内省 middleware 时,它查找上述名字的方法,并建立四个列表作为处理器的实例变量:

1. _request_middleware 是一个保存 process_request 方法的列表(在每一 种情况下,它们是真正的方法,可以直接调用),这些方法来自于任一个定 义了它们的 middleware 类。

1. _view_middleware 是一个保存 process_view 方法的列表,这些方法来自 于任一个定义了它们的 middleware 类。

2. _response_middleware 是一个保存 process_response 方法的列表,这些 方法来自于任一个定义了它们的 middleware 类。

3. _exception_middleware 是一个保存 process_exception 方法的列表,这 些方法来自于任一个定义了它们的 middleware 类。

绿灯:现在开始

现在处理器已经准备好真正开始处理了,因此它给调度程序发送一个信号 request_started(Django 内部的调度程序允许各种不同的组件声明它们正在干 什么,并可以写一些代码监听特定的事件。关于这一点目前还没有官方的文档, 但在 wiki上有一些注释。)。接下来它实例化一个django.http.HttpRequest 的子类。根据不同的处理器,可能是 django.core.handlers.modpython.ModPythonRequest 的一个实例,也可能是 django.core.handlers.wsgi.WSGIRequest 的一个实例。需要两个不同的类是因 为 mod_python 和 WSGI APIs 以不同的格式传入 request 信息,这个信息需要 解析为 Django 能够处理的一个单独的标准格式。

一旦一个 HttpRequest 或者类似的东西存在了,处理器就调用它自己的 get_response 方法,传入这个 HttpRequest 作为唯一的参数。这里就是几乎所 有真正的活动发生的地方。

Middleware,第一回合

get_response 做的第一件事就是遍历处理器的 _request_middleware 实例变量 并调用其中的每一个方法,传入 HttpRequest 的实例作为参数。

for middleware_method in self._request_middleware:

response = middleware_method(request)

if response:

break

这些方法可以选 择短路剩下的处理并立即让 get_response 返回,通过返回自身的一个值(如果 它们这样做,返回值必须是 django.http.HttpResponse 的一个实例,后面会讨 论到)。如果其中之一这样做了,我们会立即回到主处理器代码,get_response 不会等着看其它 middleware 类想要做什么,它直接返回,然后处理器进入 response 阶段。

然而,更一般的情况是,这里应用的 middleware 方法简单地做一些处理并决定 是否增加,删除或补充 request 的属性。

关于解析

假设没有一个作用于 request 的 middleware 直接返回 response,处理器下一 步会尝试解析请求的 URL。它在配置文件中寻找一个叫做 ROOT_URLCONF 的配 置,用这个配置加上根 URL /,作为参数来创建 django.core.urlresolvers.RegexURLResolver 的一个实例,然后调用它的 resolve 方法来解析请求的 URL 路径。

URL resolver 遵循一个相当简单的模式。对于在 URL 配置文件中根据 ROOT_URLCONF 的配置产生的每一个在 urlpatterns 列表中的条目,它会检查请 求的 URL 路径是否与这个条目的正则表达式相匹配,如果是的话,有两种选择:

1. 如果这个条目有一个可以调用的 include,resolver 截取匹配的 URL,转 到 include 指定的 URL 配置文件并开始遍历其中 urlpatterns 列表中的 每一个条目。根据你 URL 的深度和模块性,这可能重复好几次。

2. 否则,resolver 返回三个条目:匹配的条目指定的 view function;一个 从 URL 得到的未命名匹配组(被用来作为 view 的位置参数);一个关键 字参数字典,它由从 URL 得到的任意命名匹配组和从 URLConf 中得到的任 意其它关键字参数组合而成。

注意这一过程会在匹配到第一个指定了 view 的条目时停止,因此最好让你的 URL 配置从复杂的正则过渡到简单的正则,这样能确保 resolver 不会首先匹配 到简单的那一个而返回错误的 view function。

如果没有找到匹配的条目,resolver 会产生 django.core.urlresolvers.Resolver404 异常,它是 django.http.Http404 例 外的子类。后面我们会知道它是如何处理的。

Middleware,第二回合

一旦知道了所需的 view function 和相关的参数,处理器就会查看它的 _view_middleware 列表,并调用其中的方法,传入 HttpRequst,view function,针对这个 view 的位置参数列表和关键字参数字典。

# Apply view middleware

for middleware_method in self._view_middleware:

response = middleware_method(request, callback, callback_args, callback_kwargs)

if response:

break

还有,middleware 有可能介入这一阶段并强迫处理器立即返回。

进入 View

如果处理过程这时候还在继续的话,处理器会调用 view function。Django 中的 Views 不很严格因为它只需要满足几个条件:

² 必须可以被调用。

² 必须接受 django.http.HttpRequest 的实例作为第一位值参数。

² 必须能产生一个异常或返回 django.http.HttpResponse 的一个实例。

除了这些,你就可以天马行空了。尽管如此,一般来说,views 会使用 Django 的 database API 来创建,检索,更新和删除数据库的某些东西,还会加载并渲 染一个模板来呈现一些东西给最终用户。

模板

Django 的模板系统有两个部分:一部分是给设计师使用的混入少量其它东西的 HTML,另一部分是给程序员使用纯 Python。

从一个 HTML 作者的角度,Django 的模板系统非常简单,需要知道的仅有三个结构:

² 变量引用。在模板中是这样: {{ foo }}。

² 模板过滤。在上面的例子中使用过滤竖线是这样:{{ foo|bar }}。通常这 用来格式化输出(比如:运行 Textile,格式化日期等等)。

² 模板标签。是这样:{% baz %}。这是模板的“逻辑”实现的地方,你可以 {% if foo %},{% for bar in foo %},等等,if 和 for 都是模板标签。

变量引用以一种非常简单的方式工作。如果你只是要打印变量,只要 {{ foo }},模板系统就会输出它。这里唯一的复杂情况是 {{ foo.bar }},这时模板系 统按顺序尝试几件事:

1. 首先它尝试一个字典方式的查找,看看 foo[‘bar’] 是否存在。如果存在, 则它的值被输出,这个过程也随之结束。

2. 如果字典查找失败,模板系统尝试属性查找,看看 foo.bar 是否存在。同 时它还检查这个属性是否可以被调用,如果可以,调用之。

3. 如果属性查找失败,模板系统尝试把它作为列表索引进行查找。

如果所有这些都失败了,模板系统输出配置 TEMPLATE_STRING_IF_INVALID 的值,默认是空字符串。

模板过滤就是简单的 Python functions,它接受一个值和一个参数,返回一个新的值。比如,date 过滤用一个 Python datetime 对象作为它的值,一个标准的 strftime 格式化字符串作为它的参数,返回对 datetime 对象应用了格式化字符 串之后的结果。

模板标签用在事情有一点点复杂的地方,它是你了解 Django 的模板系统是如何 真正工作的地方。

Django 模板的结构

在内部,一个 Django 模板体现为一个 “nodes” 集合,它们都是从基本的 django.template.Node 类继承而来。Nodes 可以做各种处理,但有一个共同点: 每一个 Node 必须有一个叫做 render 的方法,它接受的第二个参数(第一个参 数,显然是 Node 实例)是 django.template.Context 的一个实例,这是一个类 似于字典的对象,包含所有模板可以获得的变量。Node 的 render 方法必须返回 一个字符串,但如果 Node 的工作不是输出(比如,它是要通过增加,删除或修 改传入的 Context 实例变量中的变量来修改模板上下文),可以返回空字符串。

Django 包含许多 Node 的子类来提供有用的功能。比如,每个内置的模板标签都 被一个 Node 的子类处理(比如,IfNode 实现了 if 标签,ForNode 实现了 for 标签,等等)。所有内置标签可以在 django.template.defaulttags 找到。

sdf

实际上,上面介绍的所有模板结构都是某种形式的 Nodes,纯文本也不异常。变 量查找由 VariableNode 处理,出于自然,过滤也应用在 VariableNode 上,标 签是各种类型的 Nodes,纯文本是一个 TextNode。

一般来说,一个 view 渲染一个模板要经过下面的步骤,依次是:

1. 加载需要渲染的模板。这是由 django.template.loader.get_template 完 成的,它能利用这许多方法中的任意一个来定位需要的模板文件。 get_template 函数返回一个 django.template.Template 实例,其中包含 经过解析的模板和用到的方法。

2. 实例化一个 Context 用来渲染模板。如果用的是 Context 的子类 django.template.RequestContext,那么附带的上下文处理函数就会自动添 加在 view 中没有定义的变量。Context 的构建器方法用一个键/值对的字 典(对于模板,它将变为名/值变量)作为它唯一的参数,RequestContext 则用 HttpRequest 的一个实例和一个字典。

3. 调用 Template 实例的 render 方法,Context 对象作为第一个位置参数。

Template 的 render 方法的返回值是一个字符串,它由 Template 中所有 Nodes 的 render 方法返回的值连接而成,调用顺序为它们出现在 Template 中 的顺序。

关于 Response,一点点

一旦一个模板完成渲染,或者产生了其它某些合适的输出,view 就会负责产生一 个 django.http.HttpResponse 实例,它的构建器接受两个可选的参数:

1. 一个作为 response 主体的字符串(它应该是第一位置参数,或者是关键字 参数 content)。大部分时间,这将作为渲染一个模板的输出,但不是必须 这样,在这里你可以传入任何有效的 Python 字符串。

2. 作 为 response 的 Content-Type header 的值(它应该是第二位置参数, 或者是关键字参数 mine_type)。如果没有提供这个参数,Django 将会使 用配置中 DEFAULT_MIME_TYPE 的值和 DEFAULT_CHARSET 的值,如果你没有 在 Django 的全局配置文件中更改它们的话,分别是 “text/html” 和 “utf-8”。

Middleware,第三回合:异常

如果 view 函数,或者其中的什么东西,发生了异常,那么 get_response(我知 道我们已经花了些时间深入 views 和 templates,但是一旦 view 返回或产生异常,我们仍将重拾处理器中间的 get_response 方法)将遍历它的 _exception_middleware 实例变量并调用那里的每个方法,传入 HttpResponse 和这个 exception 作为参数。如果顺利,这些方法中的一个会实例化一个 HttpResponse 并返回它。

仍然没有响应?

这时候有可能还是没有得到一个 HttpResponse,这可能有几个原因:

1. view 可能没有返回值。

2. view 可能产生了异常但没有一个 middleware 能处理它。

3. 一个 middleware 方法试图处理一个异常时自己又产生了一个新的异常。

这时候,get_response 会回到自己的异常处理机制中,它们有几个层次:

1. 如果 exception 是 Http404 并且 DEBUG 设置为 True,get_response 将 执行 view django.views.debug.technical_404_response,传入 HttpRequest 和 exception 作为参数。这个 view 会展示 URL resolver 试图匹配的模式信息。

2. 如果 DEBUG 是 False 并且异常是 Http404,get_response 会调用 URL resolver 的 resolve_404 方法。这个方法查看 URL 配置以判断哪一个 view 被指定用来处理 404 错误。默认是 django.views.defaults.page_not_found,但可以在 URL 配置中给 handler404 变量赋值来更改。

3. 对于任何其它类型的异常,如果 DEBUG 设置为 True,get_response 将执 行 view django.views.debug.technical_500_response,传入 HttpRequest 和 exception 作为参数。这个 view 提供了关于异常的详细 信息,包括 traceback,每一个层次 stack 中的本地变量,HttpRequest 对象的详细描述和所有无效配置的列表。

4. 如果 DEBUG 是 False,get_response 会调用 URL resolver 的 resolve_500 方法,它和 resolve_404 方法非常相似,这时默认的 view 是 django.views.defaults.server_error,但可以在 URL 配置中给 handler500 变量赋值来更改。

此外,对于除了 django.http.Http404 或 Python 内置的 SystemExit 之外的任 何异常,处理器会给调度者发送信号 got_request_exception,在返回之前,构 建一个关于异常的描述,把它发送给列在 Django 配置文件的 ADMINS 配置中的 每一个人。

Middleware,最后回合

现在,无论 get_response 在哪一个层次上发生错误,它都会返回一个 HttpResponse 实例,因此我们回到处理器的主要部分。一旦它获得一个 HttpResponse 它做的第一件事就是遍历它的 _response_middleware 实例变量并 应用那里的方法,传入 HttpRequest 和 HttpResponse 作为参数。

finally:

# Reset URLconf for this thread on the way out for complete

# isolation of request.urlconf

urlresolvers.set_urlconf(None)

try:

# Apply response middleware, regardless of the response

for middleware_method in self._response_middleware:

response = middleware_method(request, response)

response = self.apply_response_fixes(request, response)

注意对于任何想改变点什么的 middleware 来说,这是它们的最后机会。

The check is in the mail

是该结束的时候了。一旦 middleware 完成了最后环节,处理器将给调度者发送 信号 request_finished,对与想在当前的 request 中执行的任何东西来说,这 绝对是最后的调用。监听这个信号的处理者会清空并释放任何使用中的资源。比 如,Django 的 request_finished 监听者会关闭所有数据库连接。

这件事发生以后,处理器会构建一个合适的返回值送返给实例化它的任何东西 (现在,是一个恰当的 mod_python response 或者一个 WSGI 兼容的 response,这取决于处理器)并返回。

发表在 django | 留下评论

django 配置详解

django配置文件包含了你的django安装的所有配置信息,本节为大家详细讲解django的配置

FROM: http://www.sxt.cn/u/4678/blog/5532

基本知识

一个配置文件只是一个包含模块级别变量的的python模块,所有的配置变量都是大写的,哈哈哈,这是一个简单的例子

1
2
3
DEBUG = False
DEFAULT_FROM_EMAIL = 'webmaster@example.com'
TEMPLATE_DIRS = ('/home/templates/mike', '/home/templates/john')

 

因为python的配置文件也是一个python模块,所以也是如何python文件的特性的:

  • 不允许python语法错误
  • 可以使用python的语法动态的给配置变量赋值
  • 可以从其他模块导入变量

指定配置

当你使用django的时候,你必须指定使用哪一个配置文件。可以使用环境变量DJANGO_SETTINGS_MODULE来指定。

该环境变量的值必须是python 路径语法的,例如:mysite.settings

django-admin.py utility

当使用django-admin.py的时候,你可以一次性的指定环境变量或者每次运行的时候都显式指定配置模块:

1
2
3
4
5
6
#Example (Unix Bash shell):
export DJANGO_SETTINGS_MODULE=mysite.settings
django-admin.py runserver
#Example (Windows shell):
set DJANGO_SETTINGS_MODULE=mysite.settings
django-admin.py runserver

 

1
django-admin.py runserver --settings=mysite.settings

 

服务器环境

在你的服务器配置环境中,你应该使用os.environ告诉你的WSGI(web服务器网关接口)使用哪一个配置文件

1
2
import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings'

 

默认设置

如果没有特别需要的话,django配置文件可以不主动配置任何东西。因为每一个设置都有一个默认值,所有的默认值都在django/conf/global_settings.py文件里面。

django首先把global_settings.py的配置加载过来,然后再加载指定的配置文件,并用指定的配置文件的配置覆盖global_settings.py里面的默认配置。所以导入global_settings.py的配置是多余的。

你可以是用django-admin.py diffsettings查看你的配置和django默认配置的不同之处(不同的地方会输出)

在python代码中使用配置

同时导入django.conf.settings可以使用配置文件里面的变量

1
2
3
from django.conf import settings
if settings.DEBUG:
    # Do something

 

注意不要在运行中修改配置

安全性

因为配置文件里面都是一些很敏感的信息,比如你的数据库密码等等,所以你必须尽可能的限制别人去访问这个文件。比如修改文件的权限,特别实在那种共享主机的环境中。

自定义配置

很简单,在配置文件中可以按照下面的规范来创建配置:

  • 配置名全部大写
  • 不要和已经有的配置冲突或者重名

不使用DJANGO_SETTINGS_MODULE来配置

可以使用django.conf.settings.configure(default_settings,**settings)来自定义自己的配置文件

1
2
3
from django.conf import settings
settings.configure(DEBUG=True, TEMPLATE_DEBUG=True,
    TEMPLATE_DIRS=('/home/web-apps/myapp', '/home/web-apps/base'))

 

configure函数可以接受任意多的关键字参数,每一个关键字参数都意味着一个配置名,因此每个关键字都必须是大写的,所有要用到但没有传参进去的都将从django的默认配置里面导入

默认的第一个参数是default_settings,用于指明默认的配置文件,如果你自定义了一个配置文件,可以使用可以参数,这个参数也是位置参数,所以下面的两种自定义配置文件的方法是等价的

1
2
3
4
from django.conf import settings
from myapp import myapp_defaults
settings.configure(default_settings=myapp_defaults, DEBUG=True)
1
settings.configure(myapp_defaults, DEBUG = True)

 

configure函数和DJANGO_SETTINGS_MODULE二者必选其一

如果需要读取某些配置,却没有配置其一的时候,django会抛出ImportError的异常

如果二者重复调用的话,将会产生RuntimeError的错误

所以,二者只能选一个,两个都不选或者都选,都是错误的

发表在 django | 留下评论

Python必知模块库

发表在 python | 留下评论

Python urlparse模块解析URL下载

一、urlparse模块功能介绍

urlparse模块会将一个普通的url解析为6个部分,返回的数据类型都是元组。同时,它还可以将已经分解后的url再组合成一个url地址。

返回的6个部分,分别是:scheme(机制)、netloc(网络位置)、path(路径)、params(路径段参数)、query(查询)、fragment(片段)。

二、urlparse模块函数方法

1 )、 urlparse.urlparse(url),分解url返回元组,可以得到很多关于这个url的数据,网络协议、目录层次等。

2 )、 urlparse.urlunparse(parts),它接收一个元组类型,将元组内对应元素重新组后为一个url网址,与上面功能正好相反。

3 )、 urlparse.urlsplit(url),作用与urlparse非常相似,它不会分解url参数,对于遵循RFC2396的URL很有用处。

4 )、 urlparse.urljoin(base, url ) 功能是基于一个base url和另一个url构造一个绝对URL。

发表在 python | 留下评论

django: 中间件

在有些场合,需要对Django处理的每个request都执行某段代码。 这类代码可能是在view处理之前修改传入的request,或者记录日志信息以便于调试,等等。

 

FROM: http://djangobook.py3k.cn/2.0/chapter17/

2

 

这类功能可以用Django的中间件框架来实现,该框架由切入到Django的request/response处理过程中的钩子集合组成。 这个轻量级低层次的plug-in系统,能用于全面的修改Django的输入和输出。

1

 

每个中间件组件都用于某个特定的功能。 如果你是顺着这本书读下来的话,你应该已经多次见到“中间件”了

 

  • 第12章中所有的session和user工具都籍由一小簇中间件实现(例如,由中间件设定view中可见的request.session 和 request.user )。

     

  • 第13章讨论的站点范围cache实际上也是由一个中间件实现,一旦该中间件发现与view相应的response已在缓存中,就不再调用对应的view函数。

     

  • 第14章所介绍的 flatpages , redirects , 和 csrf 等应用也都是通过中间件组件来完成其魔法般的功能。

     

这一章将深入到中间件及其工作机制中,并阐述如何自行编写中间件。

 

什么是中间件

我们从一个简单的例子开始。

 

高流量的站点通常需要将Django部署在负载平衡proxy(参见第20章)之后。 这种方式将带来一些复杂性,其一就是每个request中的远程IP地址(request.META["REMOTE_IP"])将指向该负载平衡proxy,而不是发起这个request的实际IP。 负载平衡proxy处理这个问题的方法在特殊的 X-Forwarded-For 中设置实际发起请求的IP。

1

 

因此,需要一个小小的中间件来确保运行在proxy之后的站点也能够在 request.META["REMOTE_ADDR"] 中得到正确的IP地址:

 

class SetRemoteAddrFromForwardedFor(object):
    def process_request(self, request):
        try:
            real_ip = request.META['HTTP_X_FORWARDED_FOR']
        except KeyError:
            pass
        else:
            # HTTP_X_FORWARDED_FOR can be a comma-separated list of IPs.
            # Take just the first one.
            real_ip = real_ip.split(",")[0]
            request.META['REMOTE_ADDR'] = real_ip

(Note: Although the HTTP header is called X-Forwarded-For , Django makes it available asrequest.META['HTTP_X_FORWARDED_FOR'] . With the exception of content-length and content-type , any HTTP headers in the request are converted to request.META keys by converting all characters to uppercase, replacing any hyphens with underscores and adding an HTTP_ prefix to the name.)

2

 

一旦安装了该中间件(参见下一节),每个request中的 X-Forwarded-For 值都会被自动插入到request.META['REMOTE_ADDR'] 中。这样,Django应用就不需要关心自己是否位于负载平衡proxy之后;简单读取request.META['REMOTE_ADDR'] 的方式在是否有proxy的情形下都将正常工作。

 

实际上,为针对这个非常常见的情形,Django已将该中间件内置。 它位于 django.middleware.http 中, 下一节将给出这个中间件相关的更多细节。

 

安装中间件

如果按顺序阅读本书,应当已经看到涉及到中间件安装的多个示例,因为前面章节的许多例子都需要某些特定的中间件。 出于完整性考虑,下面介绍如何安装中间件。

 

要启用一个中间件,只需将其添加到配置模块的 MIDDLEWARE_CLASSES 元组中。 在 MIDDLEWARE_CLASSES 中,中间件组件用字符串表示: 指向中间件类名的完整Python路径。 例如,下面是 django-admin.py startproject 创建的缺省 MIDDLEWARE_CLASSES :

 

MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
)

Django项目的安装并不强制要求任何中间件,如果你愿意, MIDDLEWARE_CLASSES 可以为空。

 

这里中间件出现的顺序非常重要。 在request和view的处理阶段,Django按照 MIDDLEWARE_CLASSES 中出现的顺序来应用中间件,而在response和异常处理阶段,Django则按逆序来调用它们。 也就是说,Django将MIDDLEWARE_CLASSES 视为view函数外层的顺序包装子: 在request阶段按顺序从上到下穿过,而在response则反过来。

2

 

中间件方法

现在,我们已经知道什么是中间件和怎么安装它,下面将介绍中间件类中可以定义的所有方法。

 

Initializer: __init__(self) __init__(self)「初始化]

1

在中间件类中, __init__() 方法用于执行系统范围的设置。

 

出于性能的考虑,每个已启用的中间件在每个服务器进程中只初始化  次。 也就是说 __init__() 仅在服务进程启动的时候调用,而在针对单个request处理时并不执行。

 

对一个middleware而言,定义 __init__() 方法的通常原因是检查自身的必要性。 如果 __init__() 抛出异常django.core.exceptions.MiddlewareNotUsed ,则Django将从middleware栈中移出该middleware。 可以用这个机制来检查middleware依赖的软件是否存在、服务是否运行于调试模式、以及任何其它环境因素。

 

在中间件中定义 __init__() 方法时,除了标准的 self 参数之外,不应定义任何其它参数。

 

Request预处理函数: process_request(self, request) process_request(self, request)

2

这个方法的调用时机在Django接收到request之后,但仍未解析URL以确定应当运行的view之前。 Django向它传入相应的 HttpRequest 对象,以便在方法中修改。

 

process_request() 应当返回 None 或 HttpResponse 对象.

 

  • 如果返回 None , Django将继续处理这个request,执行后续的中间件, 然后调用相应的view.

     

  • 如果返回 HttpResponse 对象, Django 将不再执行 任何 其它的中间件(而无视其种类)以及相应的view。 Django将立即返回该 HttpResponse .

     

View预处理函数: process_view(self, request, view, args, kwargs) process_view(self, request, view, args, kwargs)

1

这个方法的调用时机在Django执行完request预处理函数并确定待执行的view之后,但在view函数实际执行之前。

 

表15-1列出了传入到这个View预处理函数的参数。

 

表 15-1. 传入process_view()的参数
参数 说明
request The HttpRequest object.
view The Python function that Django will call to handle this request. This is the actual function object itself, not the name of the function as a string.
args
将传入view的位置参数列表,但不包括
request 参数(它通常是传 入view的第一个参数)
kwargs 将传入view的关键字参数字典.

Just like process_request() , process_view() should return either None or an HttpResponse object.

1

 

  • If it returns None , Django will continue processing this request, executing any other middleware and then the appropriate view.

    4

     

  • If it returns an HttpResponse object, Django won’t bother calling any other middleware (of any type) or the appropriate view. Django will immediately return that HttpResponse .

    2

     

Response后处理函数: process_response(self, request, response) process_response(self, request, response)

1

这个方法的调用时机在Django执行view函数并生成response之后。 Here, the processor can modify the content of a response. One obvious use case is content compression, such as gzipping of the request’s HTML.

2

 

这个方法的参数相当直观: request 是request对象,而 response 则是从view中返回的response对象。 request is the request object, and response is the response object returned from the view.

2

 

不同可能返回 None 的request和view预处理函数, process_response() 必须 返回 HttpResponse 对象. 这个response对象可以是传入函数的那一个原始对象(通常已被修改),也可以是全新生成的。 That response could be the original one passed into the function (possibly modified) or a brand-new one.

1

 

Exception后处理函数: process_exception(self, request, exception) process_exception(self, request, exception)

这个方法只有在request处理过程中出了问题并且view函数抛出了一个未捕获的异常时才会被调用。 这个钩子可以用来发送错误通知,将现场相关信息输出到日志文件, 或者甚至尝试从错误中自动恢复。

 

这个函数的参数除了一贯的 request 对象之外,还包括view函数抛出的实际的异常对象 exception 。

 

process_exception() 应当返回 None 或 HttpResponse 对象.

 

  • 如果返回 None , Django将用框架内置的异常处理机制继续处理相应request。

     

  • 如果返回 HttpResponse 对象, Django 将使用该response对象,而短路框架内置的异常处理机制。

    2

     

备注

 

Django自带了相当数量的中间件类(将在随后章节介绍),它们都是相当好的范例。 阅读这些代码将使你对中间件的强大有一个很好的认识。

 

在Djangos wiki上也可以找到大量的社区贡献的中间件范例:http://code.djangoproject.com/wiki/ContributedMiddlewarehttp://code.djangoproject.com/wiki/ContributedMiddleware

 

内置的中间件

Django自带若干内置中间件以处理常见问题,将从下一节开始讨论。

 

认证支持中间件

中间件类: django.contrib.auth.middleware.AuthenticationMiddleware .django.contrib.auth.middleware.AuthenticationMiddleware .

 

这个中间件激活认证支持功能. 它在每个传入的 HttpRequest 对象中添加代表当前登录用户的 request.user 属性。 It adds the request.user attribute, representing the currently logged-in user, to every incomingHttpRequest object.

1

 

完整的细节请参见第12章。

 

通用中间件

Middleware class: django.middleware.common.CommonMiddleware .

 

这个中间件为完美主义者提供了一些便利:

 

禁止 ``DISALLOWED_USER_AGENTS`` 列表中所设置的user agent访问 :一旦提供,这一列表应当由已编译的正则表达式对象组成,这些对象用于匹配传入的request请求头中的user-agent域。 下面这个例子来自某个配置文件片段:

 

import re

DISALLOWED_USER_AGENTS = (
    re.compile(r'^OmniExplorer_Bot'),
    re.compile(r'^Googlebot')
)

请注意 import re ,因为 DISALLOWED_USER_AGENTS 要求其值为已编译的正则表达式(也就是 re.compile() 的返回值)。

1

 

依据 ``APPEND_SLASH`` 和 ``PREPEND_WWW`` 的设置执行URL重写 :如果 APPEND_SLASH 为 True , 那些尾部没有斜杠的URL将被重定向到添加了斜杠的相应URL,除非path的最末组成部分包含点号。 因此,foo.com/bar 会被重定向到 foo.com/bar/ , 但是 foo.com/bar/file.txt 将以不变形式通过。

 

如果 PREPEND_WWW 为 True , 那些缺少先导www.的URLs将会被重定向到含有先导www.的相应URL上。 will be redirected to the same URL with a leading www..

 

这两个选项都是为了规范化URL。 其后的哲学是每个URL都应且只应当存在于一处。 技术上来说,URLexample.com/bar 与 example.com/bar/ 及 www.example.com/bar/ 都互不相同。

 

依据 ``USE_ETAGS`` 的设置处理Etag : ETags 是HTTP级别上按条件缓存页面的优化机制。 如果 USE_ETAGS为 True ,Django针对每个请求以MD5算法处理页面内容,从而得到Etag, 在此基础上,Django将在适当情形下处理并返回 Not Modified 回应(译注:

 

请注意,还有一个条件化的 GET 中间件, 处理Etags并干得更多,下面马上就会提及。

 

压缩中间件

中间件类 django.middleware.gzip.GZipMiddleware .

 

这个中间件自动为能处理gzip压缩(包括所有的现代浏览器)的浏览器自动压缩返回]内容。 这将极大地减少Web服务器所耗用的带宽。 代价是压缩页面需要一些额外的处理时间。

 

相对于带宽,人们一般更青睐于速度,但是如果你的情形正好相反,尽可启用这个中间件。

 

条件化的GET中间件

Middleware class: django.middleware.http.ConditionalGetMiddleware .

 

这个中间件对条件化 GET 操作提供支持。 如果response头中包括 Last-Modified 或 ETag 域,并且request头中包含 If-None-Match 或 If-Modified-Since 域,且两者一致,则该response将被response 304(Not modified)取代。 对 ETag 的支持依赖于 USE_ETAGS 配置及事先在response头中设置 ETag 域。稍前所讨论的通用中间件可用于设置response中的 ETag 域。 As discussed above, the ETag header is set by the Common middleware.

 

此外,它也将删除处理 HEAD request时所生成的response中的任何内容,并在所有request的response头中设置Date 和 Content-Length 域。

 

反向代理支持 (X-Forwarded-For中间件)

2

Middleware class: django.middleware.http.SetRemoteAddrFromForwardedFor .

 

这是我们在 什么是中间件 这一节中所举的例子。 在 request.META['HTTP_X_FORWARDED_FOR'] 存在的前提下,它根据其值来设置 request.META['REMOTE_ADDR'] 。在站点位于某个反向代理之后的、每个request的 REMOTE_ADDR都被指向 127.0.0.1 的情形下,这一功能将非常有用。 It sets request.META['REMOTE_ADDR'] based onrequest.META['HTTP_X_FORWARDED_FOR'] , if the latter is set. This is useful if you’re sitting behind a reverse proxy that causes each request’s REMOTE_ADDR to be set to 127.0.0.1 .

 

红色警告!

 

这个middleware并  验证 HTTP_X_FORWARDED_FOR 的合法性。

 

如果站点并不位于自动设置 HTTP_X_FORWARDED_FOR 的反向代理之后,请不要使用这个中间件。 否则,因为任何人都能够伪造 HTTP_X_FORWARDED_FOR 值,而 REMOTE_ADDR 又是依据 HTTP_X_FORWARDED_FOR 来设置,这就意味着任何人都能够伪造IP地址。

 

只有当能够绝对信任 HTTP_X_FORWARDED_FOR 值得时候才能够使用这个中间件。

 

会话支持中间件

Middleware class: django.contrib.sessions.middleware.SessionMiddleware .

 

这个中间件激活会话支持功能. 细节请参见第12章。 See Chapter 14 for details.

 

站点缓存中间件

Middleware classes: django.middleware.cache.UpdateCacheMiddleware anddjango.middleware.cache.FetchFromCacheMiddleware .

 

这些中间件互相配合以缓存每个基于Django的页面。 已在第13章中详细讨论。

 

事务处理中间件

Middleware class: django.middleware.transaction.TransactionMiddleware .

 

这个中间件将数据库的 COMMIT 或 ROLLBACK 绑定到request/response处理阶段。 如果view函数成功执行,则发出 COMMIT 指令。 如果view函数抛出异常,则发出 ROLLBACK 指令。

 

这个中间件在栈中的顺序非常重要。 其外层的中间件模块运行在Django缺省的 保存-提交 行为模式下。 而其内层中间件(在栈中的其后位置出现)将置于与view函数一致的事务机制的控制下。

发表在 django, python | 留下评论

python set

介绍

python的set是一个无序不重复元素集,基本功能包括关系测试和消除重复元素. 集合对象还支持并、交、差、对称差等。

sets 支持 x in set、 len(set)、和 for x in set。作为一个无序的集合,sets不记录元素位置或者插入点。因此,sets不支持 indexing, slicing, 或其它类序列(sequence-like)的操作。

回到顶部

基本操作

 

>>> x = set("jihite")

>>> y = set(['d', 'i', 'm', 'i', 't', 'e'])

>>> x       #把字符串转化为set,去重了

set(['i', 'h', 'j', 'e', 't'])

>>> y

set(['i', 'e', 'm', 'd', 't'])

>>> x & y   #交

set(['i', 'e', 't'])

>>> x | y   #并

set(['e', 'd', 'i', 'h', 'j', 'm', 't'])

>>> x - y   #差

set(['h', 'j'])

>>> y - x

set(['m', 'd'])

>>> x ^ y   #对称差:x和y的交集减去并集

set(['d', 'h', 'j', 'm'])

 

回到顶部

函数操作

 

>>> x

set(['i', 'h', 'j', 'e', 't'])

>>> s = set("hi")

>>> s

set(['i', 'h'])

>>> len(x)                    #长度

5

>>> 'i' in x

True

>>> s.issubset(x)             #s是否为x的子集

True

>>> y

set(['i', 'e', 'm', 'd', 't'])

>>> x.union(y)                #交

set(['e', 'd', 'i', 'h', 'j', 'm', 't'])

>>> x.intersection(y)         #并

set(['i', 'e', 't'])

>>> x.difference(y)           #差

set(['h', 'j'])

>>> x.symmetric_difference(y) #对称差

set(['d', 'h', 'j', 'm'])

>>> s.update(x)               #更新s,加上x中的元素

>>> s

set(['e', 't', 'i', 'h', 'j'])

>>> s.add(1)                  #增加元素

>>> s

set([1, 'e', 't', 'i', 'h', 'j'])

>>> s.remove(1)               #删除已有元素,如果没有会返回异常

>>> s

set(['e', 't', 'i', 'h', 'j'])

>>> s.remove(2)

 

Traceback (most recent call last):

File "<pyshell#29>", line 1, in <module>

s.remove(2)

KeyError: 2

>>> s.discard(2)               #如果存在元素,就删除;没有不报异常

>>> s

set(['e', 't', 'i', 'h', 'j'])

>>> s.clear()                  #清除set

>>> s

set([])

>>> x

set(['i', 'h', 'j', 'e', 't'])

>>> x.pop()                    #随机删除一元素

'i'

>>> x

set(['h', 'j', 'e', 't'])

>>> x.pop()

'h'

发表在 python | 留下评论

Python重定向标准输入、标准输出和标准错误

FROM :http://blog.csdn.net/lanbing510/article/details/8487997

UNIX用户已经对标准输入、标准输出和标准错误的概念熟悉了。这一节是为其它不熟悉的人准备的。

标准输出和标准错误(通常缩写为 stdout 和 stderr)是建立在每个UNIX系统内的管道(pipe)。当你 print 某东西时,结果输出到 stdout 管道中;当你的程序崩溃并打印出调试信息时(象Python中的错误跟踪),结果输出到 stderr 管道中。通常这两个管道只与你正在工作的终端窗口相联,所以当一个程序打印输出时,你可以看到输出,并且当一个程序崩溃时,你可以看到调试信息。(如果你在一个基于窗口的Python IDE系统上工作,stdout 和 stderr 缺省为“交互窗口”。)

例 5.32. stdout 和 stderr 介绍

>>> for i in range(3):

...     print 'Dive in'

Dive in

Dive in

Dive in

>>> import sys

>>> for i in range(3):

...     sys.stdout.write('Dive in')

Dive inDive inDive in

>>> for i in range(3):

...     sys.stderr.write('Dive in')

Dive inDive inDive in

  正如我们在例 3.28中看到的,我们可以使用Python内置的 range 函数来创建简单的计数循环,即重复某物一定的次数。
  stdout 是一个类文件对象;调用它的 write 函数会打印出任何给出的字符串。事实上,这就是 print 函数真正所做的;它会在正打印的字符串后面加上回车换行符,并调用sys.stdout.write。
  在最简单的例子中,stdout 和 stderr 将它们的输出发送到同一个地方:Python IDE,或终端(如果你正从命令行运行Python)。象 stdout,stderr 并不为你增加回车换行符;如果需要,要自已加上。

stdout 和 stderr 都是类文件对象,就象我们在提取输入源中所讨论的一样,但它们都是只写的。它们没有 read 方法,只有 write。然而,它们的确是类文件对象,并且你可以将任意文件对象或类文件对象赋给它们来重定向输出。

例 5.33. 重定向输出

[f8dy@oliver kgp]$ python2 stdout.py

Dive in

[f8dy@oliver kgp]$ cat out.log

This message will be logged instead of displayed

如果你还没有这样做,你可以下载本书中用到的本例和其它例子

#stdout.py

import sys

 

print 'Dive in'

saveout = sys.stdout

fsock = open('out.log', 'w')

sys.stdout = fsock

print 'This message will be logged instead of displayed'

sys.stdout = saveout

fsock.close()

  这样会打印到IDE的“交互窗口”中(或终端,如果你从命令行运行这一脚本)。
  始终在重定向 stdout 之前保存它,这样你可以在后面将其设回正常。
  打开一个新文件用于写入。
  将所有后续的输出重定向到我们刚打开的新文件上。
  这样只会将输出结果“打印”到日志文件中;在IDE窗口中或在屏幕上不会看到输出结果。
  在我们将 stdout 搞乱之前,让我们把它设回原来的方式。
  关闭日志文件。

重定向 stderr 完全以相同的方式进行,用 sys.stderr 代替 sys.stdout。

例 5.34. 重定向错误信息

[f8dy@oliver kgp]$ python2 stderr.py

[f8dy@oliver kgp]$ cat error.log

Traceback (most recent line last):

File "stderr.py", line 5, in ?

raise Exception, 'this error will be logged'

Exception: this error will be logged

如果你还没有这样做,你可以下载本书中用到的本例和其它例子

#stderr.py

import sys

 

fsock = open('error.log', 'w')

sys.stderr = fsock

raise Exception, 'this error will be logged'

  打开我们想用来存储调试信息的日志文件。
  将我们新打开的日志文件的文件对象赋给 stderr 重定向标准错误。
  引发一个异常。从屏幕输出上我们可以注意到这样没有在屏幕上打印出任何东西。所以正常跟踪信息已经写进 error.log。
  还要注意我们既没有显示地关闭日志文件,也没有将 stderr 设回它的初始值。这样挺好,因为一旦程序崩溃(由于我们的异常),Python将替我们清理和关闭文件,并且 stderr 永远不恢复不会造成什么不同。因为,我提到过,一旦程序崩溃,则Python也结束。如果你希望在同一个脚本的后面去做其它的事情,恢复初始值对 stdout 更为重要。

另一方面,标准输入是只读文件对象,同时它表示从前面某个程序的数据流入这个程序。这一点可能对典型的Mac OS用户可能没什么意义,或者甚至是对Windows用户也是如此,除非你更习惯在MS-DOS命令行下工作。它的工作方式是:你可以在单个文件中构造一个命令行的链,这样一个程序的输出成为链中下一个程序的输入。第一个程序简单地输出到标准输出(本身不需要任何特别的重定义,只是执行正常的 print 什么的),同时下个程序从标准输入读入,操作系统会小心地将一个程序的输出连接到下一个程序的输入。

例 5.35. 链接命令

[f8dy@oliver kgp]$ python2 kgp.py -g binary.xml

01100111

[f8dy@oliver kgp]$ cat binary.xml

<?xml version="1.0"?>

<!DOCTYPE grammar PUBLIC "-//diveintopython.org//DTD Kant Generator Pro v1.0//EN" "kgp.dtd">

<grammar>

<ref id="bit">

<p>0</p>

<p>1</p>

</ref>

<ref id="byte">

<p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\

<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>

</ref>

</grammar>

[f8dy@oliver kgp]$ cat binary.xml | python2 kgp.py -g -

10110001

  正如我们在接触中看到的,这样会打印出8个随机比特(0 或 1)的字符串。
  这样会简单地打印出 binary.xml 的全部内容。(Windows用户应该使用 type 代替 cat。)
  这样会打印 binary.xml 的内容,但是“|”字符,叫做管道符,表示输出内容不会打印到屏幕上。相反,它们成为下个命令(在本例中调用我们的Python脚本)的标准输入。
  我们没有指定一个模块(象 binary.xml),而是指定“-”,这会让我们的脚本从标准输入而不是从磁盘上的一个特别文件中装入语法。(在下个例子中有更多关于它是如何发生的内容。)这样效果同第一个语法(我们直接指定语法文件名)是一样的,但它考虑了这里的扩展的可能性。不只是简单地执行 cat binary.xml,我们可以运行一个可以动态生成语法的脚本,然后可以将它通过管道输入到我们的脚本中。语法可以来自任何地方:数据库,或某个语法生成元脚本什么的。要点就是我们完全不必修改我们的 kgp.py 脚本就可以同任何这种功能进行合并。我们要做的只是能够从标准输入中接收语法文件,并且我们可以将所有其它的逻辑分散到另一个程序中。

那么当语法文件是“-”时我们的脚本是如何中从标准输入读入的呢?没什么神秘的,就是编码。

例 5.36. 在 kgp.py 中从标准输入读入

def openAnything(source):

if source == "-":

import sys

return sys.stdin

 

# try to open with urllib (if source is http, ftp, or file URL)

import urllib

try:

 

[... 略 ...]

  这是来自 toolbox.py 中的 openAnything 函数,我们在提取输入源中讨论过的。我们所做的全部是在函数的开始处添加三行代码,用来检查是否 source 是“-”,如果是,我们返回sys.stdin。实际上,就是这样!记住,stdin 是一个带有 read 方法的类文件对象,所以我们代码的其余部分(在 kgp.py 中,我们调用 openAnything 的地方)一点没有改变。
发表在 python | 留下评论

Python中的上下文管理器

1. 上下文管理器是什么?

举个例子,你在写Python代码的时候经常将一系列操作放在一个语句块中:

当某条件为真 – 执行这个语句块

当某条件为真 – 循环执行这个语句块

有时候我们需要在当程序在语句块中运行时保持某种状态,并且在离开语句块后结束这种状态。

所以,事实上上下文管理器的任务是 – 代码块执行前准备,代码块执行后收拾。

上下文管理器是在Python2.5加入的功能,它能够让你的代码可读性更强并且错误更少。接下来,让我们来看看该如何使用。

 

 

2. 如何使用上下文管理器?

看代码是最好的学习方式,来看看我们通常是如何打开一个文件并写入”Hello World”?

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

filename = &#039;my_file.txt&#039;

mode = &#039;w&#039; # Mode that allows to write to the file

writer = open(filename, mode)

writer.write(&#039;Hello &#039;)

writer.write(&#039;World&#039;)

writer.close()

1

2

3

4

5

6

filename = &#039;my_file.txt&#039;

mode = &#039;w&#039; # Mode that allows to write to the file

writer = open(filename, mode)

writer.write(&#039;Hello &#039;)

writer.write(&#039;World&#039;)

writer.close()

[Format Time: 0.0012 seconds]

1-2行,我们指明文件名以及打开方式(写入)。

第3行,打开文件,4-5行写入“Hello world”,第6行关闭文件。

这样不就行了,为什么还需要上下文管理器?但是我们忽略了一个很小但是很重要的细节:如果我们没有机会到达第6行关闭文件,那会怎样?

举个例子,磁盘已满,因此我们在第4行尝试写入文件时就会抛出异常,而第6行则根本没有机会执行。

当然,我们可以使用try-finally语句块来进行包装:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

writer = open(filename, mode)

try:

writer.write(&#039;Hello &#039;)

writer.write(&#039;World&#039;)

finally:

writer.close()

1

2

3

4

5

6

writer = open(filename, mode)

try:

writer.write(&#039;Hello &#039;)

writer.write(&#039;World&#039;)

finally:

writer.close()

[Format Time: 0.0009 seconds]

finally语句块中的代码无论try语句块中发生了什么都会执行。因此可以保证文件一定会关闭。这么做有什么问题么?当然没有,但当我们进行一些比写入“Hello world”更复杂的事情时,try-finally语句就会变得丑陋无比。例如我们要打开两个文件,一个读一个写,两个文件之间进行拷贝操作,那么通过with语句能够保证两者能够同时被关闭。

OK,让我们把事情分解一下:

首先,创建一个名为“writer”的文件变量。

然后,对writer执行一些操作。

最后,关闭writer。

这样是不是优雅多了?

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

with open(filename, mode) as writer:

writer.write(&#039;Hello &#039;)

writer.write(&#039;World&#039;)

1

2

3

with open(filename, mode) as writer:

writer.write(&#039;Hello &#039;)

writer.write(&#039;World&#039;)

[Format Time: 0.0008 seconds]

让我们深入一点,“with”是一个新关键词,并且总是伴随着上下文管理器出现。“open(filename, mode)”曾经在之前的代码中出现。“as”是另一个关键词,它指代了从“open”函数返回的内容,并且把它赋值给了一个新的变量。“writer”是一个新的变量名。

2-3行,缩进开启一个新的代码块。在这个代码块中,我们能够对writer做任意操作。这样我们就使用了“open”上下文管理器,它保证我们的代码既优雅又安全。它出色的完成了try-finally的任务。

open函数既能够当做一个简单的函数使用,又能够作为上下文管理器。这是因为open函数返回了一个文件类型(file type)变量,而这个文件类型实现了我们之前用到的write方法,但是想要作为上下文管理器还必须实现一些特殊的方法,我会在接下来的小节中介绍。

3. 自定义上下文管理器

让我们来写一个“open”上下文管理器。

要实现上下文管理器,必须实现两个方法 – 一个负责进入语句块的准备操作,另一个负责离开语句块的善后操作。同时,我们需要两个参数:文件名和打开方式。

Python类包含两个特殊的方法,分别名为:__enter__以及__exit__(双下划线作为前缀及后缀)。

当一个对象被用作上下文管理器时:

__enter__ 方法将在进入代码块前被调用。

__exit__ 方法则在离开代码块之后被调用(即使在代码块中遇到了异常)。

下面是上下文管理器的一个例子,它分别进入和离开代码块时进行打印。

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

class PypixContextManagerDemo:

 

def __enter__(self):

print &#039;Entering the block&#039;

 

def __exit__(self, *unused):

print &#039;Exiting the block&#039;

 

with PypixContextManagerDemo():

print &#039;In the block&#039;

 

#Output:

#Entering the block

#In the block

#Exiting the block

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

class PypixContextManagerDemo:

 

def __enter__(self):

print &#039;Entering the block&#039;

 

def __exit__(self, *unused):

print &#039;Exiting the block&#039;

 

with PypixContextManagerDemo():

print &#039;In the block&#039;

 

#Output:

#Entering the block

#In the block

#Exiting the block

[Format Time: 0.0013 seconds]

注意一些东西:

  • 没有传递任何参数。
  • 在此没有使用“as”关键词。
  • 稍后我们将讨论__exit__方法的参数设置。

我们如何给一个类传递参数?其实在任何类中,都可以使用__init__方法,在此我们将重写它以接收两个必要参数(filename, mode)。

当我们进入语句块时,将会使用open函数,正如第一个例子中那样。而当我们离开语句块时,将关闭一切在__enter__函数中打开的东西。

以下是我们的代码:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

class PypixOpen:

 

def __init__(self, filename, mode):

self.filename = filename

self.mode = mode

 

def __enter__(self):

self.openedFile = open(self.filename, self.mode)

return self.openedFile

 

def __exit__(self, *unused):

self.openedFile.close()

 

with PypixOpen(filename, mode) as writer:

writer.write(&quot;Hello World from our new Context Manager!&quot;)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

class PypixOpen:

 

def __init__(self, filename, mode):

self.filename = filename

self.mode = mode

 

def __enter__(self):

self.openedFile = open(self.filename, self.mode)

return self.openedFile

 

def __exit__(self, *unused):

self.openedFile.close()

 

with PypixOpen(filename, mode) as writer:

writer.write(&quot;Hello World from our new Context Manager!&quot;)

[Format Time: 0.0024 seconds]

来看看有哪些变化:

3-5行,通过__init__接收了两个参数。

7-9行,打开文件并返回。

12行,当离开语句块时关闭文件。

14-15行,模仿open使用我们自己的上下文管理器。

除此之外,还有一些需要强调的事情:

如何处理异常

我们完全忽视了语句块内部可能出现的问题。

如果语句块内部发生了异常,__exit__方法将被调用,而异常将会被重新抛出(re-raised)。当处理文件写入操作时,大部分时间你肯定不希望隐藏这些异常,所以这是可以的。而对于不希望重新抛出的异常,我们可以让__exit__方法简单的返回True来忽略语句块中发生的所有异常(大部分情况下这都不是明智之举)。

我们可以在异常发生时了解到更多详细的信息,完备的__exit__函数签名应该是这样的:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

def __exit__(self, exc_type, exc_val, exc_tb)

1 def __exit__(self, exc_type, exc_val, exc_tb)

[Format Time: 0.0007 seconds]

这样__exit__函数就能够拿到关于异常的所有信息(异常类型,异常值以及异常追踪信息),这些信息将帮助异常处理操作。在这里我将不会详细讨论异常处理该如何写,以下是一个示例,只负责抛出SyntaxErrors异常。

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

class RaiseOnlyIfSyntaxError:

 

def __enter__(self):

pass

 

def __exit__(self, exc_type, exc_val, exc_tb):

return SyntaxError != exc_type

1

2

3

4

5

6

7

class RaiseOnlyIfSyntaxError:

 

def __enter__(self):

pass

 

def __exit__(self, exc_type, exc_val, exc_tb):

return SyntaxError != exc_type

[Format Time: 0.0010 seconds]

 

4. 谈一些关于上下文库(contextlib)的内容

contextlib是一个Python模块,作用是提供更易用的上下文管理器。

contextlib.closing

假设我们有一个创建数据库函数,它将返回一个数据库对象,并且在使用完之后关闭相关资源(数据库连接会话等)

我们可以像以往那样处理或是通过上下文管理器:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

with contextlib.closing(CreateDatabase()) as database:

database.query()

1

2

with contextlib.closing(CreateDatabase()) as database:

database.query()

[Format Time: 0.0006 seconds]

contextlib.closing方法将在语句块结束后调用数据库的关闭方法。

contextlib.nested

另一个很cool的特性能够有效地帮助我们减少嵌套:

假设我们有两个文件,一个读一个写,需要进行拷贝。

以下是不提倡的:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

with open(&#039;toReadFile&#039;, &#039;r&#039;) as reader:

with open(&#039;toWriteFile&#039;, &#039;w&#039;) as writer:

writer.writer(reader.read())

1

2

3

with open(&#039;toReadFile&#039;, &#039;r&#039;) as reader:

with open(&#039;toWriteFile&#039;, &#039;w&#039;) as writer:

writer.writer(reader.read())

[Format Time: 0.0007 seconds]

可以通过contextlib.nested进行简化:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

 

with contextlib.nested(open(&#039;fileToRead.txt&#039;, &#039;r&#039;),

open(&#039;fileToWrite.txt&#039;, &#039;w&#039;)) as (reader, writer):

writer.write(reader.read())

1

2

3

with contextlib.nested(open(&#039;fileToRead.txt&#039;, &#039;r&#039;),

open(&#039;fileToWrite.txt&#039;, &#039;w&#039;)) as (reader, writer):

writer.write(reader.read())

[Format Time: 0.0008 seconds]

在Python2.7中这种写法被一种新语法取代:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

with open(&#039;fileToRead.txt&#039;, &#039;r&#039;) as reader, \

open(&#039;fileToWrite.txt&#039;, &#039;w&#039;) as writer:

writer.write(reader.read())

1

2

3

with open(&#039;fileToRead.txt&#039;, &#039;r&#039;) as reader, \

open(&#039;fileToWrite.txt&#039;, &#039;w&#039;) as writer:

writer.write(reader.read())

[Format Time: 0.0007 seconds]

 

contextlib.contextmanager

对于Python高级玩家来说,任何能够被yield关键词分割成两部分的函数,都能够通过装饰器装饰的上下文管理器来实现。任何在yield之前的内容都可以看做在代码块执行前的操作,而任何yield之后的操作都可以放在exit函数中。

这里我举一个线程锁的例子:

锁机制保证两段代码在同时执行时不会互相干扰。例如我们有两块并行执行的代码同时写一个文件,那我们将得到一个混合两份输入的错误文件。但如果我们能有一个锁,任何想要写文件的代码都必须首先获得这个锁,那么事情就好办了。如果你想了解更多关于并发编程的内容,请参阅相关文献。

下面是线程安全写函数的例子:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

import threading

 

lock = threading.Lock()

 

def safeWriteToFile(openedFile, content):

lock.acquire()

openedFile.write(content)

lock.release()

1

2

3

4

5

6

7

8

import threading

 

lock = threading.Lock()

 

def safeWriteToFile(openedFile, content):

lock.acquire()

openedFile.write(content)

lock.release()

[Format Time: 0.0011 seconds]

接下来,让我们用上下文管理器来实现,回想之前关于yield和contextlib的分析:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

@contextlib.contextmanager

def loudLock():

print &#039;Locking&#039;

lock.acquire()

yield

print &#039;Releasing&#039;

lock.release()

 

with loudLock():

print &#039;Lock is locked: %s&#039; % lock.locked()

print &#039;Doing something that needs locking&#039;

 

#Output:

#Locking

#Lock is locked: True

#Doing something that needs locking

#Releasing

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

@contextlib.contextmanager

def loudLock():

print &#039;Locking&#039;

lock.acquire()

yield

print &#039;Releasing&#039;

lock.release()

 

with loudLock():

print &#039;Lock is locked: %s&#039; % lock.locked()

print &#039;Doing something that needs locking&#039;

 

#Output:

#Locking

#Lock is locked: True

#Doing something that needs locking

#Releasing

[Format Time: 0.0015 seconds]

特别注意,这不是异常安全(exception safe)的写法。如果你想保证异常安全,请对yield使用try语句。幸运的是threading。lock已经是一个上下文管理器了,所以我们只需要简单地:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

@contextlib.contextmanager

def loudLock():

print &#039;Locking&#039;

with lock:

yield

print &#039;Releasing&#039;

1

2

3

4

5

6

@contextlib.contextmanager

def loudLock():

print &#039;Locking&#039;

with lock:

yield

print &#039;Releasing&#039;

[Format Time: 0.0008 seconds]

因为threading.lock在异常发生时会通过__exit__函数返回False,这将在yield被调用是被重新抛出。这种情况下锁将被释放,但对于“print ‘Releasing’”的调用则不会被执行,除非我们重写try-finally。

如果你希望在上下文管理器中使用“as”关键字,那么就用yield返回你需要的值,它将通过as关键字赋值给新的变量。

发表在 python | 留下评论

Python中使用logging模块打印log日志详解

FROM : http://www.jb51.net/article/63502.htm

log信息不同于使用打桩法打印一定的标记信息,log可以根据程序需要而分出不同的log级别,比如info、debug、warn等等级别的信息,只要实时控制log级别开关就可以为开发人员提供更好的log信息,与log4xx类似,logger,handler和日志消息的调用可以有具体的日志级别(Level),只有在日志消息的级别大于logger和handler的设定的级别,才会显示。下面我就来谈谈我在Python中使用的logging模块一些方法。

logging模块介绍

Python的logging模块提供了通用的日志系统,熟练使用logging模块可以方便开发者开发第三方模块或者是自己的Python应用。同样这个模块提供不同的日志级别,并可以采用不同的方式记录日志,比如文件,HTTP、GET/POST,SMTP,Socket等,甚至可以自己实现具体的日志记录方式。下文我将主要介绍如何使用文件方式记录log。

logging模块包括logger,handler,filter,formatter这四个基本概念。

logger:提供日志接口,供应用代码使用。logger最长用的操作有两类:配置和发送日志消息。可以通过logging.getLogger(name)获取logger对象,如果不指定name则返回root对象,多次使用相同的name调用getLogger方法返回同一个logger对象。

handler:将日志记录(log record)发送到合适的目的地(destination),比如文件,socket等。一个logger对象可以通过addHandler方法添加0到多个handler,每个handler又可以定义不同日志级别,以实现日志分级过滤显示。

filter:提供一种优雅的方式决定一个日志记录是否发送到handler。

formatter:指定日志记录输出的具体格式。formatter的构造方法需要两个参数:消息的格式字符串和日期字符串,这两个参数都是可选的。

基本使用方法

一些小型的程序我们不需要构造太复杂的log系统,可以直接使用logging模块的basicConfig函数即可,代码如下:

复制代码

代码如下:

 

'''

Created on 2012-8-12

 

@author: walfred

@module: loggingmodule.BasicLogger

'''

import logging

 

log_file = "./basic_logger.log"

 

logging.basicConfig(filename = log_file, level = logging.DEBUG)

 

logging.debug("this is a debugmsg!")

logging.info("this is a infomsg!")

logging.warn("this is a warn msg!")

logging.error("this is a error msg!")

logging.critical("this is a critical msg!")

 

运行程序时我们就会在该文件的当前目录下发现basic_logger.log文件,查看basic_logger.log内容如下:

 

复制代码

代码如下:

 

INFO:root:this is a info msg!

DEBUG:root:this is a debug msg!

WARNING:root:this is a warn msg!

ERROR:root:this is a error msg!

CRITICAL:root:this is a critical msg!

 

需要说明的是我将level设定为DEBUG级别,所以log日志中只显示了包含该级别及该级别以上的log信息。信息级别依次是:notset、debug、info、warn、error、critical。如果在多个模块中使用这个配置的话,只需在主模块中配置即可,其他模块会有相同的使用效果。

较高级版本

上述的基础使用比较简单,没有显示出logging模块的厉害,适合小程序用,现在我介绍一个较高级版本的代码,我们需要依次设置logger、handler、formatter等配置。

 

复制代码

代码如下:

 

'''

Created on 2012-8-12

 

@author: walfred

@module: loggingmodule.NomalLogger

'''

import logging

 

log_file = "./nomal_logger.log"

log_level = logging.DEBUG

 

logger = logging.getLogger("loggingmodule.NomalLogger")

handler = logging.FileHandler(log_file)

formatter = logging.Formatter("[%(levelname)s][%(funcName)s][%(asctime)s]%(message)s")

 

handler.setFormatter(formatter)

logger.addHandler(handler)

logger.setLevel(log_level)

 

#test

logger.debug("this is a debug msg!")

logger.info("this is a info msg!")

logger.warn("this is a warn msg!")

logger.error("this is a error msg!")

logger.critical("this is a critical msg!")

 

这时我们查看当前目录的nomal_logger.log日志文件,如下:

 

复制代码

代码如下:

 

[DEBUG][][2012-08-12 17:43:59,295]this is a debug msg!

[INFO][][2012-08-12 17:43:59,295]this is a info msg!

[WARNING][][2012-08-12 17:43:59,295]this is a warn msg!

[ERROR][][2012-08-12 17:43:59,295]this is a error msg!

[CRITICAL][][2012-08-12 17:43:59,295]this is a critical msg!

 

这个对照前面介绍的logging模块,不难理解,下面的最终版本将会更加完整。

完善版本

这个最终版本我用singleton设计模式来写一个Logger类,代码如下:

复制代码

代码如下:

 

'''

Created on 2012-8-12

 

@author: walfred

@module: loggingmodule.FinalLogger

'''

 

import logging.handlers

 

class FinalLogger:

 

logger = None

 

levels = {"n" : logging.NOTSET,

"d" : logging.DEBUG,

"i" : logging.INFO,

"w" : logging.WARN,

"e" : logging.ERROR,

"c" : logging.CRITICAL}

 

log_level = "d"

log_file = "final_logger.log"

log_max_byte = 10 * 1024 * 1024;

log_backup_count = 5

 

@staticmethod

def getLogger():

if FinalLogger.logger is not None:

return FinalLogger.logger

 

FinalLogger.logger = logging.Logger("oggingmodule.FinalLogger")

log_handler = logging.handlers.RotatingFileHandler(filename = FinalLogger.log_file,\

maxBytes = FinalLogger.log_max_byte,\

backupCount = FinalLogger.log_backup_count)

log_fmt = logging.Formatter("[%(levelname)s][%(funcName)s][%(asctime)s]%(message)s")

log_handler.setFormatter(log_fmt)

FinalLogger.logger.addHandler(log_handler)

FinalLogger.logger.setLevel(FinalLogger.levels.get(FinalLogger.log_level))

return FinalLogger.logger

 

if __name__ == "__main__":

logger = FinalLogger.getLogger()

logger.debug("this is a debug msg!")

logger.info("this is a info msg!")

logger.warn("this is a warn msg!")

logger.error("this is a error msg!")

logger.critical("this is a critical msg!")

 

当前目录下的 final_logger.log内容如下:

复制代码

代码如下:

 

[DEBUG][][2012-08-12 18:12:23,029]this is a debug msg!

[INFO][][2012-08-12 18:12:23,029]this is a info msg!

[WARNING][][2012-08-12 18:12:23,029]this is a warn msg!

[ERROR][][2012-08-12 18:12:23,029]this is a error msg!

[CRITICAL][][2012-08-12 18:12:23,029]this is a critical msg!

发表在 python | 留下评论

Django使用request和response对象

FROM : http://iluoxuan.iteye.com/blog/1738522

http://python.usyiyi.cn/django/ref/request-response.html

当请求一张页面时,Django把请求的metadata数据包装成一个HttpRequest对象,然后Django加载合适的view方法,把这个HttpRequest 对象作为第一个参数传给view方法。任何view方法都应该返回一个HttpResponse对象。

我们在本书中大量使用这两个对象;本附录详细解释HttpRequest和HttpResponse对象。

HttpRequest

HttpRequest代表一个来自uesr-agent的HTTP请求。

大多重要的请求信息都是作为HttpRequest 对象的属性出现(see Table H-1). 除了session外,其他所有属性都是只读的。

Table H-1. HttpRequest对象的属性

Attribute Description
path 请求页面的全路径,不包括域名—例如, "/music/bands/the_beatles/"。
method 请求中使用的HTTP方法的字符串表示。全大写表示。例如:if request.method == 'GET':do_something()

elif request.method == 'POST':

do_something_else()

GET 包含所有HTTP GET参数的类字典对象。参见QueryDict 文档。
POST 包含所有HTTP POST参数的类字典对象。参见QueryDict 文档。服务器收到空的POST请求的情况也是有可能发生的。也就是说,表单form通过HTTP POST方法提交请求,但是表单中可以没有数据。因此,不能使用语句if request.POST来判断是否使用HTTP POST方法;应该使用if request.method == "POST" (参见本表的method属性)。注意: POST不包括file-upload信息。参见FILES属性。
REQUEST 为了方便,该属性是POST和GET属性的集合体,但是有特殊性,先查找POST属性,然后再查找GET属性。借鉴PHP’s $_REQUEST。例如,如果GET = {"name": "john"} 和POST = {"age": '34'},则 REQUEST["name"] 的值是"john", REQUEST["age"]的值是"34".强烈建议使用GET and POST,因为这两个属性更加显式化,写出的代码也更易理解。
COOKIES 包含所有cookies的标准Python字典对象。Keys和values都是字符串。参见第12章,有关于cookies更详细的讲解。
FILES 包含所有上传文件的类字典对象。FILES中的每个Key都是<input type="file" name="" />标签中name属性的值. FILES中的每个value 同时也是一个标准Python字典对象,包含下面三个Keys:

  • filename: 上传文件名,用Python字符串表示
  • content-type: 上传文件的Content type
  • content: 上传文件的原始内容

注意:只有在请求方法是POST,并且请求页面中<form>有enctype="multipart/form-data"属性时FILES才拥有数据。否则,FILES 是一个空字典。

META 包含所有可用HTTP头部信息的字典。 例如:

  • CONTENT_LENGTH
  • CONTENT_TYPE
  • QUERY_STRING: 未解析的原始查询字符串
  • REMOTE_ADDR: 客户端IP地址
  • REMOTE_HOST: 客户端主机名
  • SERVER_NAME: 服务器主机名
  • SERVER_PORT: 服务器端口

META 中这些头加上前缀HTTP_最为Key, 例如:

  • HTTP_ACCEPT_ENCODING
  • HTTP_ACCEPT_LANGUAGE
  • HTTP_HOST: 客户发送的HTTP主机头信息
  • HTTP_REFERER: referring页
  • HTTP_USER_AGENT: 客户端的user-agent字符串
  • HTTP_X_BENDER: X-Bender头信息
user 是一个django.contrib.auth.models.User 对象,代表当前登录的用户。如果访问用户当前没有登录,user将被初始化为django.contrib.auth.models.AnonymousUser的实例。你可以通过user的is_authenticated()方法来辨别用户是否登录:if request.user.is_authenticated():# Do something for logged-in users.

else:

# Do something for anonymous users.

只有激活Django中的AuthenticationMiddleware时该属性才可用

关于认证和用户的更详细讲解,参见第12章。

session 唯一可读写的属性,代表当前会话的字典对象。只有激活Django中的session支持时该属性才可用。 参见第12章。
raw_post_data 原始HTTP POST数据,未解析过。 高级处理时会有用处。

Request对象也有一些有用的方法,见Table H-2:

 

Table H-2. HttpRequest Methods
Method Description
__getitem__(key) 返回GET/POST的键值,先取POST,后取GET。如果键不存在抛出 KeyError。这是我们可以使用字典语法访问HttpRequest对象。例如,request["foo"]等同于先request.POST["foo"] 然后 request.GET["foo"]的操作。
has_key() 检查request.GET or request.POST中是否包含参数指定的Key。
get_full_path() 返回包含查询字符串的请求路径。例如, "/music/bands/the_beatles/?print=true"
is_secure() 如果请求是安全的,返回True,就是说,发出的是HTTPS请求。

 

QueryDict对象

在HttpRequest对象中, GET和POST属性是django.http.QueryDict类的实例。 QueryDict类似字典的自定义类,用来处理单键对应多值的情况。因为一些HTML form元素,例如,<selectmultiple="multiple">, 就会传多个值给单个键。

QueryDict对象是immutable(不可更改的),除非创建它们的copy()。这意味着我们不能直接改变request.POST and request.GET的属性。

QueryDict实现所有标准的字典方法。还包括一些特有的方法,见Table H-3.

 

Table H-3. How QueryDicts Differ from Standard Dictionaries.
Method Differences from Standard dict Implementation
__getitem__ 和标准字典的处理有一点不同,就是,如果Key对应多个Value,__getitem__()返回最后一个value。
__setitem__ 设置参数指定key的value列表(一个Python list)。注意:它只能在一个mutable QueryDict 对象上被调用(就是通过copy()产生的一个QueryDict对象的拷贝).
get() 如果key对应多个value,get()返回最后一个value。
update() 参数可以是QueryDict,也可以是标准字典。和标准字典的update方法不同,该方法添加字典 items,而不是替换它们:>>> q = QueryDict('a=1') 

>>> q = q.copy() # to make it mutable

 

>>> q.update({'a': '2'})

 

>>> q.getlist('a')

['1', '2']

 

>>> q['a'] # returns the last

 

['2']

items() 和标准字典的items()方法有一点不同,该方法使用单值逻辑的__getitem__():>>> q = QueryDict('a=1&a=2&a=3') 

>>> q.items()

 

[('a', '3')]

values() 和标准字典的values()方法有一点不同,该方法使用单值逻辑的__getitem__():

 

此外, QueryDict也有一些方法,见Table H-4.

 

H-4. 额外的 (非字典的) QueryDict 方法
Method Description
copy() 返回对象的拷贝,内部实现是用Python标准库的copy.deepcopy()。该拷贝是mutable(可更改的) — 就是说,可以更改该拷贝的值。
getlist(key) 返回和参数key对应的所有值,作为一个Python list返回。如果key不存在,则返回空list。 It’s guaranteed to return a list of some sort..
setlist(key,list_) 设置key的值为list_ (unlike __setitem__()).
appendlist(key,item) 添加item到和key关联的内部list.
setlistdefault(key,list) 和setdefault有一点不同,它接受list而不是单个value作为参数。
lists() 和items()有一点不同, 它会返回key的所有值,作为一个list, 例如:>>> q = QueryDict('a=1&a=2&a=3') 

>>> q.lists()

 

[('a', ['1', '2', '3'])]

urlencode() 返回一个以查询字符串格式进行格式化后的字符串(e.g., "a=2&b=3&b=5").

 

A Complete Example

例如, 下面是一个HTML form:

<form action="/foo/bar/" method="post">

<input type="text" name="your_name" />

<select multiple="multiple" name="bands">

<option value="beatles">The Beatles</option>

<option value="who">The Who</option>

<option value="zombies">The Zombies</option>

</select>

<input type="submit" />

</form>

如果用户在your_name域中输入"JohnSmith",同时在多选框中选择了“The Beatles”和“The Zombies”,下面是Django请求对象的内容:

>>> request.GET{}

>>> request.POST

{'your_name': ['John Smith'], 'bands': ['beatles', 'zombies']}

>>> request.POST['your_name']

'John Smith'

>>> request.POST['bands']

'zombies'

>>> request.POST.getlist('bands')

['beatles', 'zombies']

>>> request.POST.get('your_name', 'Adrian')

'John Smith'

>>> request.POST.get('nonexistent_field', 'Nowhere Man')

'Nowhere Man'

HttpResponse

对于HttpRequest 对象来说,是由Django自动创建, 但是,HttpResponse对象就必须我们自己创建。每个View方法必须返回一个HttpResponse对象。

HttpResponse类在django.http.HttpResponse。

构造HttpResponses

一般地, 你可以通过给HttpResponse的构造函数传递字符串表示的页面内容来构造HttpResponse对象:

>>> response = HttpResponse("Here's the text of the Web page.")

>>> response = HttpResponse("Text only, please.", mimetype="text/plain")

但是如果想要增量添加内容, 你可以把response当作filelike对象使用:

>>> response = HttpResponse()

>>> response.write("<p>Here's the text of the Web page.</p>")

>>> response.write("<p>Here's another paragraph.</p>")

也可以给HttpResponse传递一个iterator作为参数,而不用传递硬编码字符串。 如果你使用这种技术, 下面是需要注意的一些事项:

·         iterator应该返回字符串.

·         如果HttpResponse使用iterator进行初始化,就不能把HttpResponse实例作为filelike 对象使用。这样做将会抛出异常。

最后,再说明一下,HttpResponse实现了write()方法, 可以在任何需要filelike对象的地方使用HttpResponse对象。参见第11章,有一些例子。

设置Headers

你可以使用字典语法添加,删除headers:

>>> response = HttpResponse()

>>> response['X-DJANGO'] = "It's the best."

>>> del response['X-PHP']

>>> response['X-DJANGO']

"It's the best."

你也可以使用has_header(header)方法检测某个header是否存在。

不要手动设置Cookie headers;具体的做法,参见第12章,有关于怎样在Django中处理cookies的详细讲解。

HttpResponse子类

Django包含很多HttpResponse子类,用来处理不同的HTTP响应类型(见Table H-5). 和HttpResponse一样, 这些子类在django.http中.

 

Table H-5. HttpResponse Subclasses
Class Description
HttpResponseRedirect 构造函数接受单个参数:重定向到的URL。可以是全URL (e.g., 'http://search.yahoo.com/')或者相对URL(e.g., '/search/'). 注意:这将返回HTTP状态码302。
HttpResponsePermanentRedirect 同HttpResponseRedirect一样,但是返回永久重定向(HTTP 状态码 301)。
HttpResponseNotModified 构造函数不需要参数。Use this to designate that a page hasn’t been modified since the user’s last request.
HttpResponseBadRequest 返回400 status code。
HttpResponseNotFound 返回404 status code.
HttpResponseForbidden 返回403 status code.
HttpResponseNotAllowed 返回405 status code. 它需要一个必须的参数:一个允许的方法的list (e.g., ['GET','POST']).
HttpResponseGone 返回410 status code.
HttpResponseServerError 返回500 status code.

 

当然,你也可以自己定义不包含在上表中的HttpResponse子类。

返回Errors

在Django中返回HTTP错误码是很容易的。上面介绍了HttpResponseNotFound, HttpResponseForbidden, HttpResponseServerError等一些子类。View方法中返回这些子类的实例就OK了,例如:

def my_view(request):

#  ...

if foo:

return HttpResponseNotFound('<h1>Page not found</h1>')

else:

return HttpResponse('<h1>Page was found</h1>')

当返回HttpResponseNotFound时,你需要定义错误页面的HTML:

return HttpResponseNotFound('<h1>Page not found</h1>')

因为404错误是最常使用的HTTP错误, 有一个更方便的方法处理它。

为了方便,而且整个站点有一致的404错误页面也是友好的,Django提供一个Http404异常类。如果在一个View方法中抛出Http404,Django将会捕获它并且返回标准错误页面,同时返回错误码404。

from django.http import Http404

 

def detail(request, poll_id):

try:

p = Poll.objects.get(pk=poll_id)

except Poll.DoesNotExist:

raise Http404

return render_to_response('polls/detail.html', {'poll': p})

为了使用Http404异常, 你需要创建一个模板文件。当异常抛出时,就会显示该模板文件。该模板文件的文件名是404.html,在模板根目录下创建。

自定义404 (Not Found) View方法

当抛出Http404异常时, Django会加载一个特殊的view方法处理404错误。默认地, 它是django.views.defaults.page_not_found,负责加载和渲染404.html模板文件。

这意味着我们必须在模板根目录定义404.html模板文件,该模板文件应用于所有的404异常。

该page_not_found view方法应该可以应对几乎99%的Web App,但是如果想要重载该view方法时, 你可以在URLConf文件中指定handler404为自定义的404 errpr view方法, 像这样:

from django.conf.urls.defaults import *

 

urlpatterns = patterns('',

...

)

 

handler404 = 'mysite.views.my_custom_404_view'

Django就是通过在URLConf文件中查找handler404来决定404 view方法的。默认地, URLconfs包含下面一行代码:

from django.conf.urls.defaults import *

在 django/conf/urls/defaults.py模块中, handler404赋值为 'django.views.defaults.page_not_found'。

注意:

·         如果请求URL没有和Django的URLConf文件中的任何一个正则表示式匹配,404 view方法就会被调用。

·         如果没有定义自己的404 view — 使用默认地404 view,你仍然有一个工作要做:在模板根目录创建404.html模板文件。默认地404 view将使用该模板处理所有404错误。

·         如果 DEBUG设置为True (在setting模块中),404 view方法不会被使用,取而代之的是,traceback信息。

自定义500 (Server Error) View方法

同样地,当view代码发生运行时错误时,Django也会产生特殊行为。如果view方法产生异常,Django将会调用默认地view方法django.views.defaults.server_error, 该方法加载渲染500.html模板文件。

这意味着我们必须在模板根目录定义500.html模板文件,该模板文件应用于所有的服务器错误。

该server_error view方法应该可以应对几乎99%的Web App,但是如果想要重载该view方法时, 你可以在URLConf文件中指定handler500为自定义的server error view方法, 像这样:

from django.conf.urls.defaults import *

urlpatterns = patterns('',

...

)

 

handler500 = 'mysite.views.my_custom_error_view'

发表在 python | 留下评论

Python pickle模块

pickle提供了一个简单的持久化功能。可以将对象以文件的形式存放在磁盘上。

pickle.dump(obj, file[, protocol])

序列化对象,并将结果数据流写入到文件对象中。参数protocol是序列化模式,默认值为0,表示以文本的形式序列化。protocol的值还可以是1或2,表示以二进制的形式序列化。

pickle.load(file)

反序列化对象。将文件中的数据解析为一个Python对象。

其中要注意的是,在load(file)的时候,要让python能够找到类的定义,否则会报错:

比如下面的例子

import pickle
class Person:
  def __init__(self,n,a):
    self.name=n
    self.age=a
  def show(self):
    print self.name+"_"+str(self.age)
aa = Person("JGood", 2)
aa.show()
f=open('d:\\p.txt','w')
pickle.dump(aa,f,0)
f.close()
#del Person
f=open('d:\\p.txt','r')
bb=pickle.load(f)
f.close()
bb.show()

如果不注释掉del Person的话,那么会报错如下:

>>> 
JGood_2

Traceback (most recent call last):
 File "C:/py/test.py", line 15, in <module>
  bb=pickle.load(f)
 File "C:\Python27\lib\pickle.py", line 1378, in load
  return Unpickler(file).load()
 File "C:\Python27\lib\pickle.py", line 858, in load
  dispatch[key](self)
 File "C:\Python27\lib\pickle.py", line 1069, in load_inst
  klass = self.find_class(module, name)
 File "C:\Python27\lib\pickle.py", line 1126, in find_class
  klass = getattr(mod, name)
AttributeError: 'module' object has no attribute 'Person'

意思就是当前模块找不到类的定义了。

clear_memo()

清空pickler的“备忘”。使用Pickler实例在序列化对象的时候,它会“记住”已经被序列化的对象引用,所以对同一对象多次调用dump(obj),pickler不会“傻傻”的去多次序列化。

看下面的例子:

import StringIO
import pickle
class Person:
  def __init__(self,n,a):
    self.name=n
    self.age=a
  def show(self):
    print self.name+"_"+str(self.age)
aa = Person("JGood", 2)
aa.show()
fle = StringIO.StringIO()
pick = pickle.Pickler(fle)
pick.dump(aa)
val1=fle.getvalue()
print len(val1)
pick.clear_memo()
pick.dump(aa)
val2=fle.getvalue()
print len(val2)
fle.close()

上面的代码运行如下:

>>> 
JGood_2
66
132
>>>

此时再注释掉pick.clear_memo()后,运行结果如下:

>>> 
JGood_2
66
70
>>>

主要是因为,python的pickle如果不clear_memo,则不会多次去序列化对象。

发表在 python | 留下评论

探究functools模块wraps装饰器的用途

FROM : http://www.cnblogs.com/myd7349/p/how_to_use_wraps_of_functools.html?utm_source=tuicool&utm_medium=referral

《A Byte of Python》17.8 节讲decorator的时候,用到了functools模块中的一个装饰器:wraps。因为之前没有接触过这个装饰器,所以特地研究了一下。

何谓“装饰器”?

《A Byte of Python》中这样讲:

“Decorators are a shortcut to applying wrapper functions. This is helpful to “wrap” functionality with the same code over and over again.”

《Python参考手册(第4版)》 6.5节描述如下:

“装饰器是一个函数,其主要用途是包装另一个函数或类。这种包装的首要目的是透明地修改或增强被包装对象的行为。”

Python官方文档中这样定义:

“A function returning another function, usually applied as a function transformation using the @wrapper syntax. Common examples for decorators are classmethod() and staticmethod().”

让我们来看一下《Python参考手册》上6.5节的一个例子(有些许改动):

# coding: utf-8

# Filename: decorator_wraps_test.py

# 2014-07-05 18:58

import sys

 

debug_log = sys.stderr

 

def trace(func):

if debug_log:

def callf(*args, **kwargs):

"""A wrapper function."""

debug_log.write('Calling function: {}\n'.format(func.__name__))

res = func(*args, **kwargs)

debug_log.write('Return value: {}\n'.format(res))

return res

return callf

else:

return func

 

@trace

def square(x):

"""Calculate the square of the given number."""

return x * x

 

if __name__ == '__main__':

print(square(3))

输出:

Calling function: square

Return value: 9

9

这个例子中,我们定义了一个装饰器trace,用于追踪函数的调用过程及函数调用的返回值。如果不用装饰器语法,我们也可以这样写:

def _square(x):

return x * x

 

square = trace(_square)

上面两段代码,使用装饰器语法的版本和不用装饰器语法的版本实际上是等效的。只是当我们使用装饰器时,我们不必再手动调用装饰器函数。

嗯。trace装饰器看起来棒极了!假设我们把如上代码提供给其他程序员使用,他可能会想看一下square函数的帮助文档:

>>> from decorator_wraps_test import square

>>> help(square) # print(square.__doc__)

Help on function callf in module decorator_wraps_test:

 

callf(*args, **kwargs)

A wrapper function.

看到这样的结果,使用decorator_wraps_test.py模块的程序员一定会感到困惑。他可能会带着疑问敲入如下代码:

>>> print(square.__name__)

callf

这下,他可能会想看一看decorator_wraps_test.py的源码,找一找问题究竟出现在了哪里。我们知道,Python中所有对象都是“第 一类”的。比如,函数(对象),我们可以把它当作普通的数据对待:我们可以把它存储到容器中,或者作为另一个函数的返回值。上面的程序中,在 debug_log为真的情况下,trace会返回一个函数对象callf。这个函数对象就是一个“ 闭包 ”,因为当我们通过:

def _square(x): return x * x

square = trace(_square)

把trace返回的callf存储到square时,我们得到的不仅仅是callf函数执行语句,还有其上下文环境:

>>> print('debug_log' in square.__globals__)

True

>>> print('sys' in square.__globals__)

True

因此,使用装饰器修饰过的函数square,实际上是一个trace函数返回的“闭包”对象callf,这就揭示了上面help(square)以及print(square.__name__)的输出结果了。

那么,怎样才能在使用装饰器的基础上,还能让help(square)及print(square.__name__)得到我们期待的结果呢?这就是functools模块的wraps装饰器的作用了。

让我们先看一看效果:

# coding: utf-8

# Filename: decorator_wraps_test.py

# 2014-07-05 18:58

import functools

import sys

 

debug_log = sys.stderr

 

def trace(func):

if debug_log:

@functools.wraps(func)

def callf(*args, **kwargs):

"""A wrapper function."""

debug_log.write('Calling function: {}\n'.format(func.__name__))

res = func(*args, **kwargs)

debug_log.write('Return value: {}\n'.format(res))

return res

return callf

else:

return func

 

@trace

def square(x):

"""Calculate the square of the given number."""

return x * x

 

if __name__ == '__main__':

print(square(3))

print(square.__doc__)

print(square.__name__)

输出:

Calling function: square

Return value: 9

9

Calculate the square of the given number.

square

很完美!哈哈。这里,我们使用了一个带参数的wraps装饰器“装饰”了嵌套函数callf,得到了预期的效果。那么,wraps的原理是什么呢?

首先,简要介绍一下带参数的装饰器:

>>> def trace(log_level):

def impl_f(func):

print(log_level, 'Implementing function: "{}"'.format(func.__name__))

return func

return impl_f

 

>>> @trace('[INFO]')

def print_msg(msg): print(msg)

 

[INFO] Implementing function: "print_msg"

>>> @trace('[DEBUG]')

def assert_(expr): assert expr

 

[DEBUG] Implementing function: "assert_"

>>> print_msg('Hello, world!')

Hello, world!

这段代码定义了一个带参数的trace装饰器函数。因此:

@trace('[INFO]')

def print_msg(msg): print(msg)

等价于:

temp = trace('[INFO]')

def _print_msg(msg): print(msg)

print_msg = temp(_print_msg)

相信这样类比一下,带参数的装饰器就很好理解了。(当然,这个例子举得并不好。《Python参考手册》上有一个关于带参数的装饰器的更好的例子,感兴趣的童鞋可以自己看看 。)

接下来,让我们看看wraps这个装饰器的代码吧!

让我们先找到functools模块文件的路径:

>>> import functools

>>> functools.__file__

'D:\\Program Files\\Python34\\lib\\functools.py'

下面,把wraps相关的代码摘录出来:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',

'__annotations__')

WRAPPER_UPDATES = ('__dict__',)

def update_wrapper(wrapper,

wrapped,

assigned = WRAPPER_ASSIGNMENTS,

updated = WRAPPER_UPDATES):

"""Update a wrapper function to look like the wrapped function

 

wrapper is the function to be updated

wrapped is the original function

assigned is a tuple naming the attributes assigned directly

from the wrapped function to the wrapper function (defaults to

functools.WRAPPER_ASSIGNMENTS)

updated is a tuple naming the attributes of the wrapper that

are updated with the corresponding attribute from the wrapped

function (defaults to functools.WRAPPER_UPDATES)

"""

for attr in assigned:

try:

value = getattr(wrapped, attr)

except AttributeError:

pass

else:

setattr(wrapper, attr, value)

for attr in updated:

getattr(wrapper, attr).update(getattr(wrapped, attr, {}))

# Issue #17482: set __wrapped__ last so we don't inadvertently copy it

# from the wrapped function when updating __dict__

wrapper.__wrapped__ = wrapped

# Return the wrapper so this can be used as a decorator via partial()

return wrapper

 

def wraps(wrapped,

assigned = WRAPPER_ASSIGNMENTS,

updated = WRAPPER_UPDATES):

"""Decorator factory to apply update_wrapper() to a wrapper function

 

Returns a decorator that invokes update_wrapper() with the decorated

function as the wrapper argument and the arguments to wraps() as the

remaining arguments. Default arguments are as for update_wrapper().

This is a convenience function to simplify applying partial() to

update_wrapper().

"""

return partial(update_wrapper, wrapped=wrapped,

assigned=assigned, updated=updated)

从代码中可以看到,wraps是通过functools模块中另外两个函数:partial和update_wrapper来实现其功能的。让我们看一看这两个函数:

1. partial函数

partial函数实现对函数参数进行部分求值(《Python参考手册》中4.9有这么一句:函数参数的部分求值与叫做 柯里化 (currying)的过程关系十分密切。虽然不是太明白,但感觉很厉害的样子!):

>>> from functools import partial

>>> def foo(x, y, z):

print(locals())

>>> foo(1, 2, 3)

{'z': 3, 'y': 2, 'x': 1}

>>> foo_without_z

functools.partial(<function foo at 0x00000000033FC6A8>, z=100)

>>> foo_without_z is foo

False

>>> foo_without_z(10, 20)

{'z': 100, 'y': 20, 'x': 10}

这里,我们通过partial为foo提供参数z的值,得到了一个新的“函数对象”(这里之所以加个引号是因为foo_without_z和一般的函数对 象有些差别。比如,foo_without_z没有__name__属性。)foo_without_z。因此,本例中:

foo_without_z(10, 20)

等价于:

foo(10, 20, z = 100)

(比较有趣的一点是,foo_without_z没有__name__属性,而其文档字符串__doc__也和partial的文档字符串很相像。此外, 我认为,这里的partial和C++标准库中的bind1st、bind2nd这些parameter binders有异曲同工之妙。这里没有把partial函数的实现代码摘录出来,有兴趣的童鞋可以自己研究一下它的工作原理。)

因此,wraps函数中:

return partial(update_wrapper, wrapped=wrapped,

assigned=assigned, updated=updated)

实际上是返回一个对update_wrapper进行部分求值的“函数对象”。因此,上例中使用了wraps装饰器的decorator_wraps_test.py的等价版本如下:

def trace(func):

if debug_log:

def _callf(*args, **kwargs):

"""A wrapper function."""

debug_log.write('Calling function: {}\n'.format(func.__name__))

res = func(*args, **kwargs)

debug_log.write('Return value: {}\n'.format(res))

return res

 

_temp = functools.wraps(func)

callf = _temp(_callf)

return callf

else:

return func

对wraps也进行展开:

def trace(func):

if debug_log:

def _callf(*args, **kwargs):

"""A wrapper function."""

debug_log.write('Calling function: {}\n'.format(func.__name__))

res = func(*args, **kwargs)

debug_log.write('Return value: {}\n'.format(res))

return res

 

_temp = functools.partial(functools.update_wrapper,

wrapped = func,

assigned = functools.WRAPPER_ASSIGNMENTS,

updated = functools.WRAPPER_UPDATES)

callf = _temp(_callf)

return callf

else:

return func

最后,对partial的调用也进行展开:

def trace(func):

if debug_log:

def _callf(*args, **kwargs):

"""A wrapper function."""

debug_log.write('Calling function: {}\n'.format(func.__name__))

res = func(*args, **kwargs)

debug_log.write('Return value: {}\n'.format(res))

return res

 

callf = functools.update_wrapper(_callf,

wrapped = func,

assigned = functools.WRAPPER_ASSIGNMENTS,

updated = functools.WRAPPER_UPDATES)

 

return callf

else:

return func

这次,我们看到的是很直观的函数调用:用_callf和func作为参数调用update_wrapper函数。

2. update_wrapper函数

update_wrapper做的工作很简单,就是用参数wrapped表示的函数对象(例如:square)的一些属性(如:__name__、 __doc__)覆盖参数wrapper表示的函数对象(例如:callf,这里callf只是简单地调用square函数,因此可以说callf是 square的一个wrapper function)的这些相应属性。

因此,本例中使用wraps装饰器“装饰”过callf后,callf的__doc__、__name__等属性和trace要“装饰”的函数square的这些属性完全一样。

经过上面的分析,相信你也了解了functools.wraps的作用了吧。

最后,《A Byte of Python》一书讲装饰器的时候提到了一篇博客: DRY Principles through Python Decorators 。有兴趣的童鞋可以去阅读以下。

发表在 python | 留下评论

Python: locals 和globals

Python两个内置函数——locals 和globals

 

 

这两个函数主要提供,基于字典的访问局部和全局变量的方式。

在理解这两个函数时,首先来理解一下python中的名字空间概念。Python使用叫做名字空间的

东西来记录变量的轨迹。名字空间只是一个字典,它的键字就是变量名,字典的值就是那些变

量的值。实际上,名字空间可以象Python的字典一样进行访问

 

 

每个函数都有着自已的名字空间,叫做局部名字空间,它记录了函数的变量,包括函数的参数

和局部定义的变量。每个模块拥有它自已的名字空间,叫做全局名字空间,它记录了模块的变

量,包括函数、类、其它导入的模块、模块级的变量和常量。还有就是内置名字空间,任何模

块均可访问它,它存放着内置的函数和异常。

 

 

当一行代码要使用变量 x 的值时,Python会到所有可用的名字空间去查找变量,按照如下顺序:

 

 

1.局部名字空间 - 特指当前函数或类的方法。如果函数定义了一个局部变量 x,Python将使用

这个变量,然后停止搜索。

2.全局名字空间 - 特指当前的模块。如果模块定义了一个名为 x 的变量,函数或类,Python

将使用这个变量然后停止搜索。

3.内置名字空间 - 对每个模块都是全局的。作为最后的尝试,Python将假设 x 是内置函数或变量。

 

 

如果Python在这些名字空间找不到 x,它将放弃查找并引发一个 NameError 的异常,同时传递

There is no variable named 'x' 这样一条信息。

 

 

#局部变量函数locals例子(locals 返回一个名字/值对的字典。):

[python] view plain copy

 

print?

  1. def foo(arg, a):
  2.     x = 1
  3.     y = 'xxxxxx'
  4.     for i in range(10):
  5.         j = 1
  6.         k = i
  7.     print locals()
  8. #调用函数的打印结果
  9. foo(1,2)
  10. #{'a': 2, 'i': 9, 'k': 9, 'j': 1, 'arg': 1, 'y': 'xxxxxx', 'x': 1}

 

 

 

 

from module import 和 import module之间的不同。使用 import module,模块自身被导入,

但是它保持着自已的名字空间,这就是为什么你需要使用模块名来访问它的函数或属性(module.function)

的原因。但是使用 from module import,实际上是从另一个模块中将指定的函数和属性导入到你自己的名字

空间,这就是为什么你可以直接访问它们却不需要引用它们所来源的模块的原因。

 

 

locals 是只读的,globals 不是

发表在 python | 留下评论

python 装饰器及标准库functools中的wraps

FROM : http://my.oschina.net/chinesezhx/blog/495293

这里有一篇比较好的讲解装饰器的书写的 Python装饰器学习(九步入门) .

这里不单独记录装饰器的书写格式了,重点是工作流程.

首先常见的 装饰器 格式就是通过@语法糖,简便的写法,让流程有些不太清楚.

 

装饰器不带参数的情况下:

def deco(func):

def _deco():

print("before myfunc() called.")

func()

print("  after myfunc() called.")

return _deco

 

@deco

def myfunc():

print(" myfunc() called.")

 

myfunc()

 

运行结果:

before myfunc() called.

myfunc() called.

after myfunc() called.

myfunc() called.

 

这个@语法糖的作用是:

def myfunc():

print(" myfunc() called.")

myfunc = deco(myfunc)

 

也就是现在的myfunc不再是一开始定义的那个了,而变成了

def _deco():

print("before myfunc() called.")

func()

print("  after myfunc() called.")

 

这一点可以通过

print myfunc.__name__

 

而复杂一点的,装饰器带参数的,如:

def deco(arg="haha"):

def _deco(func):

def __deco():

print("before %s called [%s]." % (func.__name__, arg))

func()

print("  after %s called [%s]." % (func.__name__, arg))

return __deco

return _deco

@deco()#注意有括号

def myfunc():

print(" myfunc() called.")

@deco("haha1")

def myfunc1():

print(" myfunc() called.")

 

myfunc()

myfunc1()

 

实际的操作是,先把装饰进行了运算,即函数deco先被调用

等效于:

def _deco(func):

def __deco():

print("before %s called [%s]." % (func.__name__, arg))

func()

print("  after %s called [%s]." % (func.__name__, arg))

return __deco

@d_deco#注意没有括号,第一处

def myfunc():

print(" myfunc() called.")

@_deco#这也没括号,第二处

def myfunc1():

print(" myfunc1() called.")

 

myfunc()

myfunc1()

 

而参数arg在第一处,使用的是默认的"haha",第二处使用的是"haha1",

更直观的表达方式就是:

def deco(arg="haha"):

def _deco(func):

def __deco():

print("before %s called [%s]." % (func.__name__, arg))

func()

print("  after %s called [%s]." % (func.__name__, arg))

return __deco

return _deco

def myfunc():

print(" myfunc() called.")

def myfunc1():

print(" myfunc() called.")

 

myfunc = deco()(myfunc)

myfunc1 = deco("haha1")(myfunc1)

 

这时再来看标准库functools中的wraps的使用,比如官网例子:

from functools import wraps

def my_decorator(f):

@wraps(f)

def wrapper(*args, **kwds):

print 'Calling decorated function'

return f(*args, **kwds)

return wrapper

@my_decorator

def example():

"""Docstring"""

print 'Called example function'

example()

print example.__name__

print example.__doc__

 

过程就是

def my_decorator(f):

def wrapper(*args, **kwds):

print 'Calling decorated function'

return f(*args, **kwds)

wrapper.__name__ = f.__name__

wrapper.__doc__  = f.__doc__

return wrapper

example = my_decorator(example)

 

这样就保留了原函数名称属性和doc,

标准库中函数wraps,可以这样理解:

def wraps(f):

def _f(*args,**kwargs):

f(*args,**kwargs)

_f.__name__ = f.__name

_f.__doc__  = f.__doc__

return _f

 

上面的wraps流程可以看出,如果直接使用wraps简直就是f = f(其实不能直接使用),所以一般都是如实例这样包藏在一个装饰器函数内部.

注:示例代码来自:Python装饰器学习(九步入门)python标准库functools之wraps

发表在 python | 留下评论

Python装饰器学习(九步入门)

FRO M: http://www.cnblogs.com/rhcad/archive/2011/12/21/2295507.html

第一步:最简单的函数,准备附加额外功能

12

3

4

5

6

7

8

# -*- coding:gbk -*-'''示例1: 最简单的函数,表示调用了两次'''

 

def myfunc():

print("myfunc() called.")

 

myfunc()

myfunc()

 

第二步:使用装饰函数在函数执行前和执行后分别附加额外功能

12

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

# -*- coding:gbk -*-'''示例2: 替换函数(装饰)

装饰函数的参数是被装饰的函数对象,返回原函数对象

装饰的实质语句: myfunc = deco(myfunc)'''

 

def deco(func):

print("before myfunc() called.")

func()

print("  after myfunc() called.")

return func

 

def myfunc():

print(" myfunc() called.")

 

myfunc = deco(myfunc)

 

myfunc()

myfunc()

 

第三步:使用语法糖@来装饰函数

12

3

4

5

6

7

8

9

10

11

12

13

14

15

16

# -*- coding:gbk -*-'''示例3: 使用语法糖@来装饰函数,相当于“myfunc = deco(myfunc)”

但发现新函数只在第一次被调用,且原函数多调用了一次'''

 

def deco(func):

print("before myfunc() called.")

func()

print("  after myfunc() called.")

return func

 

@deco

def myfunc():

print(" myfunc() called.")

 

myfunc()

myfunc()

 

第四步:使用内嵌包装函数来确保每次新函数都被调用

12

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

# -*- coding:gbk -*-'''示例4: 使用内嵌包装函数来确保每次新函数都被调用,

内嵌包装函数的形参和返回值与原函数相同,装饰函数返回内嵌包装函数对象'''

 

def deco(func):

def _deco():

print("before myfunc() called.")

func()

print("  after myfunc() called.")

# 不需要返回func,实际上应返回原函数的返回值

return _deco

 

@deco

def myfunc():

print(" myfunc() called.")

return 'ok'

 

myfunc()

myfunc()

 

第五步:对带参数的函数进行装饰

12

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

# -*- coding:gbk -*-'''示例5: 对带参数的函数进行装饰,

内嵌包装函数的形参和返回值与原函数相同,装饰函数返回内嵌包装函数对象'''

 

def deco(func):

def _deco(a, b):

print("before myfunc() called.")

ret = func(a, b)

print("  after myfunc() called. result: %s" % ret)

return ret

return _deco

 

@deco

def myfunc(a, b):

print(" myfunc(%s,%s) called." % (a, b))

return a + b

 

myfunc(1, 2)

myfunc(3, 4)

 

第六步:对参数数量不确定的函数进行装饰

12

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

# -*- coding:gbk -*-'''示例6: 对参数数量不确定的函数进行装饰,

参数用(*args, **kwargs),自动适应变参和命名参数'''

 

def deco(func):

def _deco(*args, **kwargs):

print("before %s called." % func.__name__)

ret = func(*args, **kwargs)

print("  after %s called. result: %s" % (func.__name__, ret))

return ret

return _deco

 

@deco

def myfunc(a, b):

print(" myfunc(%s,%s) called." % (a, b))

return a+b

 

@deco

def myfunc2(a, b, c):

print(" myfunc2(%s,%s,%s) called." % (a, b, c))

return a+b+c

 

myfunc(1, 2)

myfunc(3, 4)

myfunc2(1, 2, 3)

myfunc2(3, 4, 5)

 

第七步:让装饰器带参数

12

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

# -*- coding:gbk -*-'''示例7: 在示例4的基础上,让装饰器带参数,

和上一示例相比在外层多了一层包装。

装饰函数名实际上应更有意义些'''

 

def deco(arg):

def _deco(func):

def __deco():

print("before %s called [%s]." % (func.__name__, arg))

func()

print("  after %s called [%s]." % (func.__name__, arg))

return __deco

return _deco

 

@deco("mymodule")

def myfunc():

print(" myfunc() called.")

 

@deco("module2")

def myfunc2():

print(" myfunc2() called.")

 

myfunc()

myfunc2()

 

第八步:让装饰器带 类 参数

12

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

# -*- coding:gbk -*-'''示例8: 装饰器带类参数'''

 

class locker:

def __init__(self):

print("locker.__init__() should be not called.")

@staticmethod

def acquire():

print("locker.acquire() called.(这是静态方法)")

@staticmethod

def release():

print("  locker.release() called.(不需要对象实例)")

 

def deco(cls):

'''cls 必须实现acquire和release静态方法'''

def _deco(func):

def __deco():

print("before %s called [%s]." % (func.__name__, cls))

cls.acquire()

try:

return func()

finally:

cls.release()

return __deco

return _deco

 

@deco(locker)

def myfunc():

print(" myfunc() called.")

 

myfunc()

myfunc()

 

第九步:装饰器带类参数,并分拆公共类到其他py文件中,同时演示了对一个函数应用多个装饰器

12

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

# -*- coding:gbk -*-'''mylocker.py: 公共类 for 示例9.py'''

 

class mylocker:

def __init__(self):

print("mylocker.__init__() called.")

@staticmethod

def acquire():

print("mylocker.acquire() called.")

@staticmethod

def unlock():

print("  mylocker.unlock() called.")

 

class lockerex(mylocker):

@staticmethod

def acquire():

print("lockerex.acquire() called.")

@staticmethod

def unlock():

print("  lockerex.unlock() called.")

 

def lockhelper(cls):

'''cls 必须实现acquire和release静态方法'''

def _deco(func):

def __deco(*args, **kwargs):

print("before %s called." % func.__name__)

cls.acquire()

try:

return func(*args, **kwargs)

finally:

cls.unlock()

return __deco

return _deco

 

12

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

# -*- coding:gbk -*-'''示例9: 装饰器带类参数,并分拆公共类到其他py文件中

同时演示了对一个函数应用多个装饰器'''

 

from mylocker import *

 

class example:

@lockhelper(mylocker)

def myfunc(self):

print(" myfunc() called.")

 

@lockhelper(mylocker)

@lockhelper(lockerex)

def myfunc2(self, a, b):

print(" myfunc2() called.")

return a + b

 

if __name__=="__main__":

a = example()

a.myfunc()

print(a.myfunc())

print(a.myfunc2(1, 2))

print(a.myfunc2(3, 4))

 

下面是参考资料,当初有不少地方没看明白,真正练习后才明白些:

1. Python装饰器学习 http://blog.csdn.net/thy38/article/details/4471421

2. Python装饰器与面向切面编程 http://www.cnblogs.com/huxi/archive/2011/03/01/1967600.html

3. Python装饰器的理解 http://apps.hi.baidu.com/share/detail/17572338

发表在 python | 留下评论

Python 的 Magic Methods 指南

FROM : http://blog.csdn.net/zajin/article/details/17911311

什么是魔术方法呢?它们是面向对象Python语言中的一切。它们是你可以自定义并添加“魔法”到类中的特殊方法。它们被双下划线环绕(比如__init__或__lt__)。它们的文档也不像它所需要的那么齐备。Python的所有魔术方法都在Python文档的同一区域,但它们的使用分散,组织松散。而且文档的这部分区域中几乎没有一个示例(这很有可能是设计好的,因为在语法参考里它们都很详尽,但伴随的是枯燥的语法描述等等)。

构造与初始化

我们每个知道的最基本的“魔法”方法是__init__。一种让我们在初始化一个类时定义一些行为。然而当我执行 x = SomeClass(), __init__ 不是第一个被执行的。事实上,第一被执行的的方法是__new__,它会创建一个实例,然后在构造器创建时传递一些参数。在一个object的生命周期的另一端的方法是__del__。让我们仔细看看这3个“魔法”方法:

  • __new__(cls, [...)
  • __new__ 是一个类的初始化过程中第一个被执行的方法。它创建了类,然后把一些参数传递给__init__。__new__ 很少被使用,特别是当我们用一些不可变类型的子类时(像tuple ,string),我不想关心__new__的太多的细节,因为那是没有用的。但它有它存在的意义。更多详细的请看 in the Python docs.
  • __init__(self, [...)
  • 类的构造器,当初始构造方法被执行(例如,我们执行 x = SomeClass(10,'foo')),__init__ 就会获得 10 和 ‘foo’ 作为参数。__init__ 在python类的定义中经常被使用
  • __del__(self)
  • 若果 __new__ 和 __init__ 形成一个类的构造函数,__del__ 是就是析构函数。它不实现语句 del x 的行为(这样代码就不会转换为 x.__del__())。它定义了一个被垃圾回收的行为。它在类消除的时后需要做一些额外的行为时是非常有用的,就像 sockets 和 file 类。注意,当编译器还在运行,如果类还存活着,这里不能确保__del__一定会被执行。所以__del__ 不能替代一些良好的编程习惯(比如连接用完了将其关掉),事实上__del__很少被使用,因为它的调用是非常不稳定的;请谨慎使用!

把他们合起来后,这里就是一个 __init__ 和 __del__ 使用的例子:

01 from os.path import joinclass FileObject:
02     '''Wrapper for file objects to make sure the file gets closed on deletion.'''
03  
04     def __init__(self, filepath='~', filename='sample.txt'):
05         # open a file filename in filepath in read and write mode
06         self.file = open(join(filepath, filename), 'r+')
07  
08     def __del__(self):
09         self.file.close()
10         del self.file

 

定义自己的类中的操作

我们使用Python的“魔法”方法最大得优势之一是它提供了一种简单的方法去定义类的行为,比如 built-in 类型。这就意味着你可以避免丑陋的,违反直觉的,非标准化的基本操作方法。在一些语言中,他们通常这样写:

1 if instance.equals(other_instance):
2     # do something

 

当让Python中也可以这么做,但是这增加了混乱和不必要的冗余。不同的类库中的相同的方法可能会用不同名字,使得使用者做了太多不必要的操作。相比之下“魔法”方法是强大的,我们可以使用它定义一个方法代替上面的例子(__eq__ , 在这个例子中):

1 if instance == other_instance:
2     #do something

 

这是“魔法”方法强大用途的一部分。他们绝大部分让我们定义操作的意义,以至于我们可以使用他们在我们自己的类中就像使用built in 类型。

魔法比较方法

python拥有大量用于实现对象与对象之间比较的魔法方法,这些对象使用运算符进行直观比较而不是难看的方法调用。同时它也提供了一种方法去重载python默认的对象比较行为(比较引用)。这里有一个这些方法和它们做了什么事情的列表:

  • __cmp__(self, other)
  • __cmp__ 是比较方法里面最基本的的魔法方法。实际上它实现了所有的比较运算符(如<, ==, !=)的行为,但也许不是你想要的行为(例如,一个实例是否和另一个实例相等应该由某个条件来决定,一个实例是否大于另一个实例应该由其他的条件来决定)。当self < other时__cmp__应该返回一个负整数,当self == other时返回0,self > other时返回正整数。通常来说最好是定义每个你需要的比较方法而不是一次性定义所有的比较方法,但是__cmp__是一个消除重复性的良途,并且当你有很多比较方法需要用相类似的条件去实现的时候这能让代码变得清晰。
  • __eq__(self, other)
  • 定义相等符号的行为,==
  • __ne__(self,other)
  • 定义不等符号的行为,!=
  • __lt__(self,other)
  • 定义小于符号的行为,<
  • __gt__(self,other)
  • 定义大于符号的行为,>
  • __le__(self,other)
  • 定义小于等于符号的行为,<=
  • __ge__(self,other)
  • 定义大于等于符号的行为,>=

 

例如,假设一个类是一个单词模型。我们可能要按字典比较单词(按字母),这是比较字符串默认行为,但我们也可能需要基于其他一些标准来做比较,比如按长度、或字节数量等。在下面的例子中,我们将比较长度。下面是实现:

class Word(str):

'''Class for words, defining comparison based on word length.'''

 

def __new__(cls, word):

# Note that we have to use __new__. This is because str is an immutable

# type, so we have to initialize it early (at creation)

if ' ' in word:

print "Value contains spaces. Truncating to first space."

word = word[:word.index(' ')] # Word is now all chars before first space

return str.__new__(cls, word)

 

def __gt__(self, other):

return len(self) > len(other)

def __lt__(self, other):

return len(self) < len(other)

def __ge__(self, other):

return len(self) >= len(other)

def __le__(self, other):

return len(self) <= len(other)

 

现在,我们可以创建两个单词(通过使用Word('foo')和Word('bar'))然后依据长度比较它们。但要注意,我们没有定义__eq__和__ne__,因为这样会导致其他一些奇怪的行为(尤其是Word('foo')==Word('bar')会判定为true),它不是基于长度相等意义上的测量,所以我们回归到字符平等意义上的实现。

现在需要留心啦——为达到预期的比较效果你不需要为每个比较定义魔术方法。如果你只定义__eq__以及其他的(比如__gt__,__lt__等),标准库已经在functools模块里为我们提供了一个类修饰器,它可以定义所有的富特性比较方法。这个特性只有在Python 2.7中才是可用的,但如果你碰巧的话这可以节省大量的时间和精力。你可以通过将@total_ordering放置在类定义前面来使用它。

数值魔术方法

就如同你可以通过定义比较操作来比较你自己的类实例一样,你也可以自己定义数学运算符号的行为。好吧,先系紧你的裤腰带,深呼吸......,这些操作可多着呢。由于文章组织需要,我把这些数学“魔术方法”分为5类:单目运算操作,一般数学运算操作,满足交换律的数学运算(后面会有更多介绍),参数赋值操作和类型转换操作:

单目运算符操作与函数:

单目运算符或单目运算函数只有一个操作数: 比如取负(-2),绝对值操作等。

  • __pos__(self)
  • 实现一个取正数的操作(比如 +some_object ,python调用__pos__函数)
  • __neg__(self)
  • 实现一个取负数的操作(比如 -some_object )
  • __abs__(self)
  • 实现一个内建的abs()函数的行为
  • __invert__(self)
  • 实现一个取反操作符(~操作符)的行为。想要了解这个操作的解释,参考the Wikipedia article on bitwise operations.
  • __round__(self, n)
  • 实现一个内建的round()函数的行为。 n 是待取整的十进制数.
  • __floor__(self)
  • 实现math.floor()的函数行为,比如, 把数字下取整到最近的整数.
  • __ceil__(self)
  • 实现math.ceil()的函数行为,比如, 把数字上取整到最近的整数.
  • __trunc__(self)
  • 实现math.trunc()的函数行为,比如, 把数字截断而得到整数.
  • 一般算数运算
    好吧,现在我们开始介绍双目运算操作或函数,比如 +, -, * 等等. 这些很容易自解释.

    • __add__(self, other)
    • 实现一个加法.
    • __sub__(self, other)
    • 实现一个减法.
    • __mul__(self, other)
    • 实现一个乘法.
    • __floordiv__(self, other)
    • 实现一个“//”操作符产生的整除操作()
    • __div__(self, other)
    • 实现一个“/”操作符代表的除法操作.
    • __truediv__(self, other)
    • 实现真实除法,注意,只有当你from __future__ import division时才会有效
    • __mod__(self, other)  实现一个“%”操作符代表的取模操作.
    • __divmod__(self, other)
    • 实现一个内建函数divmod()
    • __pow__
    • 实现一个指数操作(“**”操作符)的行为
    • __lshift__(self, other)
    • 实现一个位左移操作(<<)的功能
    • __rshift__(self, other)
    • 实现一个位右移操作(>>)的功能.
    • __and__(self, other)
    • 实现一个按位进行与操作(&)的行为.
    • __or__(self, other)  实现一个按位进行或操作(|)的行为.
    • __xor__(self, other)
    • 实现一个异或操作(^)的行为
反射算术运算符

你相信我说我能用一位来表示反射运算吗?可能有人会认为表示一个反射运算是大的吓人的“外国概念”,反射实际上它是非常简单的。看下面的例子:

some_object + other

 

这是一个正常的加法。除了可以交换操作数以外,反射运算和加法是一样的:

other + some_object

 

除了执行那种 other对像作为第一个操作数,而它自身作为第二个操作数的运算以外,所有的魔法方法做的事情与正常运算表示的意义是等价的。在大部分情况下反射运算结果和它正常的运算是等价的,所以你可以不定义__radd__,而是调用__add__等等。注意,对像(本例中的other)在运算符左边的时候,必须保证该对像没有定义(或者返回NotImplemented的)它的非反射运算符。例如,在这个例子中,some_object.__radd__  只有在 other没有定义__add__的时候才会被调用。

  • __radd__(self, other)
  • 反射加法
  • __rsub__(self, other)
  • 反射减法的
  • __rmul__(self, other)
  • 反射除法
  • __rfloordiv__(self, other)
  • 反射地板除,使用//运算符的
  • __rdiv__(self, other)
  • 反射除法,使用/运算符的.
  • __rtruediv__(self, other)
  • 反射真除.注意只有from __future__ import division 的时候它才有效
  • __rmod__(self, other)
  • 反射取模运算,使用%运算符.
  • __rdivmod__(self, other)
  • 长除法,使用divmod()内置函数,当divmod(other,self)时被调用.
  • __rpow__
  • 反射乘方,使用**运算符的
  • __rlshift__(self, other)
  • 反射左移,使用<<操作符.
  • __rrshift__(self, other)
  • 反射右移,使用>>操作符.
  • __rand__(self, other)
  • 反射位与,使用&操作符.
  • __ror__(self, other)
  • 反射位或,使用|操作符.
  • __rxor__(self, other)
  • 反射异或,使用^操作符.
增量运算

Python 还有很多种魔法方法,允许一些习惯行为被定义成增量运算。你很可能已经熟悉了增量运算,增量运算是算术运算和赋值运算的结合。如果你还不知道我在说什么,就看一下下面的例子:

x = 5x += 1 # in other words x = x + 1

 

每一个方法的返回值都会被赋给左边的变量。(比如,对于a += b, __iadd__ 可能会返回a + b, a + b会赋给变量a。) 下面是清单:

  • __iadd__(self, other)
  • 加法赋值
  • __isub__(self, other)
  • 减法赋值.
  • __imul__(self, other)
  • 乘法赋值
  • __ifloordiv__(self, other)
  • 整除赋值,地板除,相当于 //= 运算符.
  • __idiv__(self, other)
  • 除法赋值,相当于 /= 运算符.
  • __itruediv__(self, other)
  • 真除赋值,注意只有你 whenfrom __future__ import divisionis,才有效.
  • __imod_(self, other)
  • 模赋值,相当于 %= 运算符.
  • __ipow__
  • 乘方赋值,相当于 **= 运算符.
  • __ilshift__(self, other)
  • 左移赋值,相当于 <<= 运算符.
  • __irshift__(self, other)
  • 左移赋值,相当于 >>= 运算符.
  • __iand__(self, other)
  • 与赋值,相当于 &= 运算符.
  • __ior__(self, other)
  • 或赋值,相当于 |= 运算符.
  • __ixor__(self, other)
  • 异或运算符,相当于 ^= 运算符.

类型转换魔法

Python 同样有一系列的魔法方法旨在实现内置类型的转换,比如float() 函数。它们是:

  • __int__(self)
  • 转换成整型.
  • __long__(self)
  • 转换成长整型.
  • __float__(self)
  • 转换成浮点型.
  • __complex__(self)
  • 转换成 复数型.
  • __oct__(self)
  • 转换成八进制.
  • __hex__(self)
  • 转换成十六进制.
  • __index__(self)
  • 当对象被切片时转换成int型。如果你定义了一个可能被用来做切片操作的数值型,你就应该定义__index__.
  • __trunc__(self)
  • 当 math.trunc(self) 使用时被调用.__trunc__返回自身类型的整型截取 (通常是一个长整型).
  • __coerce__(self, other)
  • 执行混合类型的运算,如果转换不能完成,应该返回None;否则,要返回一对两个元数的元组self和other, 被操作成同类型。
表示你的类

用一个字符串来表示一个类往往会非常有用。在Python中,有很多你可以在类定义中实施的方法来自定义内置函数的返回值以表示出你所写出的类的某些行为。

 

  • __str__(self)
  • 定义当 str() 被你的一个类的实例调用时所要产生的行为。

 

  • __repr__(self)
  • 定义 当 repr()  被你的一个类的实例调用时所要产生的行为。 str() 和 repr() 的主要区别是其目标群体。 repr() 返回的是机器可读的输出,而 str() 返回的是人类可读的。

 

 

  • __unicode__(self)
  • 定义当 unicode() 被你的一个类的实例调用时所要产生的行为。 unicode() 和 str() 很相似,但是返回的是unicode字符串。注意,如果对你的类调用 str() 然而你只定义了 __unicode__() ,那么其将不会工作。你应该定义 __str__() 来确保调用时能返回正确的值,并不是每个人都有心情去使用unicode。
  • __format__(self, formatstr)
  • 定义当你的一个类的实例被用来用新式的格式化字符串方法进行格式化时所要产生的行为。例如, "Hello, {0:abc}!".format(a) 将会导致调用 a.__format__("abc") 。这对定义你自己的数值或字符串类型是十分有意义的,你可能会给出一些特殊的格式化选项。

 

  • __hash__(self)
  • 定义当 hash()被你的一个类的实例调用时所要产生的行为。它返回一个整数,用来在字典中进行快速比较。请注意,这通常也承担着实现__eq__。有下面这样的规则:a == b 暗示着 hash(a) == hash(b) 。

 

 

  • __nonzero__(self)
  • 定义当 bool() 被你的一个类的实例调用时所要产生的行为。本方法应该返回True或者False,取决于你想让它返回的值。

 

  • __dir__(self)
  • 定义当 dir() 被你的一个类的实例调用时所要产生的行为。该方法应该返回一个属性的列表给用户,一般而言,实现 __dir__ 是不必要的,但是,如果你重新定义了__getattr__或__getattribute__(你将在下一节中看到)或者其它的动态生成属性,那么它对你的类的交互使用是至关重要的。
  • __sizeof__(self)
  • 定义当 sys.getsizeof() 被你的一个类的实例调用时所要产生的行为。该方法应该以字节为单位,返回你的对象的大小。这通常对于以C扩展的形式实现的Python类更加有意义,其有助于理解这些扩展。

我们几乎完成了对这些枯燥的魔法方法(并且没有实例)的指导。现在,我们已经提及到了一些较基本的魔法方法,到了该转移到更高级内容的时候了。

 

属性访问控制

很多用过其它语言的人抱怨Python缺乏对类真正的封装(比如没办法定义private属性和public的getter和settter)。但这不是真的啊:真相是Python通过“魔法”实现了大量的封装,而不是使用明确的方法或字段修饰符。看一下吧:

  • __getattr__(self, name)
  • 你可以定义如何处理用户试图访问一个不存在(不存在或还没创建)属性的行为。这对于捕获或者重定向一般的拼写错误非常有用,给出访问了不能访问的属性的警告(如果你愿意,你还可以推断并返回那个属性。),或者巧妙地处理一个AttributeError异常。它只有在一个不存在的属性被访问的情况下才被调用,然而,这并不是一个真正封装的方案。
  • __setattr__(self, name, value)
  • 与__getattr__不同,__setattr__是一个真正的封装方案。它允许你定义当给一个存在或不存在的属性赋值时的行为,意味着对任何属性值的改变你都可以定义一个规则。可是,你得小心使用__setattr__,在这个清单结尾的例子会向你说明。
  • __delattr__
  • 它与__setattr__非常像, 只不过是用来删除而不是设置属性。 __detattr__需要预防措施,就像setattr一样,当被调用时可能会引起无限递归(当__delattr__已经实现时,调用 del self.name 就会引起无限的递归)。
  • __getattribute__(self, name)
  •  __getattribute__相当适合它的同伴__setattr__和__delattr__.但我却不建议你使用它。__getattribute__只有在新风格的类中才会被使用(所有的新风格类在Python最新的版本中,在老版本中,你可以子类化object来获得一个新风格类。它允许你定义一条规则来处理无论什么时候属性值被访问时的行为。比如类似于由于其它的伙伴犯错而引起的无限递归(这时你就可以调用基类的__getattribute__方法来阻止它)。它也避免了对__getattr__的依赖,当__getattribute__方法已经实现的时候,__getattr__只有在__getattribute__被明确的调用或抛出一个AttributeError异常的时候才会被调用。这个方法能被使用(毕竟,这是你的选择),但是我不推荐它,因为它很少使用并且运行的时候很难保证没有BUG。
如果定义了任何属性访问控制方法,容易产生错误。思考下面这个例子:

def __setattr__(self, name, value):

self.name = value

# since every time an attribute is assigned, __setattr__() is called, this

# is recursion.

# so this really means self.__setattr__('name', value). Since the method

# keeps calling itself, the recursion goes on forever causing a crashdef __setattr__(self, name, value):

self.__dict__[name] = value # assigning to the dict of names in the class

# define custom behavior here

 

再次证明了Python的魔法方法是难以置信的强大,但强大的力量也需要强大的责任。如果你不想运行时中断你的代码,那了解如何适当地使用魔法方法就非常重要啦。

我们从Python中定制的属性访问中学到了什么?它们不是被轻易使用的。事实上,它有点过分强大并且违反直觉。但它们存在的原因是用来止痒的:Python不阻止你制造遭糕东西,但可能会让它变的困难。自由是最重要的东西,所以你可做任何你想做的事情。这里有一个例子,展示了一些特殊的属性访问控制行为。(注意我们使用super,因为不是所有的类都有__dict__属性):

class AccessCounter(object):

'''A class that contains a value and implements an access counter.    The counter increments each time the value is changed.'''

 

def __init__(self, val):

super(AccessCounter, self).__setattr__('counter', 0)

super(AccessCounter, self).__setattr__('value', val)

 

def __setattr__(self, name, value):

if name == 'value':

super(AccessCounter, self).__setattr__('counter', self.counter + 1)

# Make this unconditional.

# If you want to prevent other attributes to be set, raise AttributeError(name)

super(AccessCounter, self).__setattr__(name, value)

 

def __delattr__(self, name):

if name == 'value':

super(AccessCounter, self).__setattr__('counter', self.counter + 1)

super(AccessCounter, self).__delattr__(name)]

 

自定义序列

有很多办法能让你的Python类使用起来就像内置的序列(dict,tuple,list,string等)。Python里有一些目前我最喜欢的办法,因为它们给你的控制到了荒谬的程度并且神奇地使得大量的全局函数优雅地工作在你类的实例当中。但是在深入讲这些好东西之前,我们先介绍下需求。

需求

在讨论在Python中创建你自己的序列也是时候谈谈协议了。在其他语言中协议有点类似于接口,因为你必须实现一系列的方法。然而,在Python中协议是完全不正式的,不需要显式的声明去实现它,它更像是一种指导原则。

为什么我们要谈论协议呢?因为在Python中实现自定义容器类型涉及到这些协议的使用。首先,有一些协议用于定义不变容器:为了实现一个不变窗口,你只需定义__len__和__getitem__方法(接下来会细说)。不变容器的协议要求所有的类加上一个 __setitem__和__delitem__方法。最后,如果你想让你的容器支持遍历,你必须定义__iter__方法,它返回一个iterator。这个iterator必须遵守iterator的协议,它要求iterator类里面有__iter__方法(返回自身)和next方法。

容器后的魔法

不需要再等待了,这里就是容器所使用的一些魔法方法。

  • __len__(self)
  • 返回容器的长度。对于可变和不可变容器的协议,这都是其中的一部分。
  • __getitem__(self, key)
  • 定义当某一项被访问时,使用self[key]所产生的行为。这也是不可变容器和可变容器协议的一部分。如果键的类型错误将产生TypeError;如果key没有合适的值则产生KeyError。
  • __setitem__(self, key, value)
  • 定义当一个条目被赋值时,使用self[nkey] = value所产生的行为。这也是协议的一部分。而且,在相应的情形下也会产生KeyError和TypeError。
  • __delitem__(self, key)
  • 定义当某一项被删除时所产生的行为。(例如del self[key])。这只是可变容器协议的一部分。当你使用一个无效的键时必须抛出适当的异常。
  • __iter__(self)
  • 返回一个容器迭代器,很多情况下会返回迭代器,尤其是当内置的iter()方法被调用的时候,以及当使用for x in Container:方式循环的时候。迭代器是它们本身的对象,它们必须定义返回self的__iter__方法。
  • __reversed__(self)
  • 实现当reversed()被调用时的行为。应该返回序列反转后的版本。仅当序列可以是有序的时候实现它,例如对于列表或者元组。
  • __contains__(self, item)
  • 定义了调用in和not in来测试成员是否存在的时候所产生的行为。你可能会问为什么这个不是序列协议的一部分?因为当__contains__没有被定义的时候,Python会迭代这个序列,并且当找到需要的值时会返回True。
  • __missing__(self, key)
  • 其在dict的子类中被使用。它定义了当一个不存在字典中的键被访问时所产生的行为。(例如,如果我有一个字典d,当"george"不是字典中的key时,使用了d["george"],此时d["george"]将会被调用)。
一个例子

对于我们的例子, 让我们看看一个列表,它实现了一些功能结构,你可能在其他在其他程序中用到 (例如Haskell).

class FunctionalList:

'''A class wrapping a list with some extra functional magic, like head,    tail, init, last, drop, and take.'''

 

def __init__(self, values=None):

if values is None:

self.values = []

else:

self.values = values

 

def __len__(self):

return len(self.values)

 

def __getitem__(self, key):

# if key is of invalid type or value, the list values will raise the error

return self.values[key]

 

def __setitem__(self, key, value):

self.values[key] = value

 

def __delitem__(self, key):

del self.values[key]

 

def __iter__(self):

return iter(self.values)

 

def __reversed__(self):

return FunctionalList(reversed(self.values))

 

def append(self, value):

self.values.append(value)

def head(self):

# get the first element

return self.values[0]

def tail(self):

# get all elements after the first

return self.values[1:]

def init(self):

# get elements up to the last

return self.values[:-1]

def last(self):

# get last element

return self.values[-1]

def drop(self, n):

# get all elements except first n

return self.values[n:]

def take(self, n):

# get first n elements

return self.values[:n]

 

这样你拥有了它,如何实现自己的序列的,有点用的例子。当然,也有更有用的应用程序的自定义序列,但在标准库中,已经有相当多的实现(包括电池,对吧?),像Counter,OrderedDict,和NamedTuple。

反射

你也可以控制怎么使用内置在函数sisinstance()和issubclass()方法 反射定义魔法方法. 这个魔法方法是:

  • __instancecheck__(self, instance)
  • 检查对象是否是您定义的类的一个实例(例.isinstance(instance, class).
  • __subclasscheck__(self, subclass)
  • 检查类是否是你定义类的子类 (例.issubclass(subclass, class)).

这些魔法方法的用例看起来很小, 并且确实非常实用. 我不想花太多时间在反射魔法方法上,因为它们不是非常重要, 但是它们反应了关于面向对象程序上一些重要的东西在Python上,并且总的来说Python: 总是一个简单的方法去找某些事情, 即使是没有必要的. 这些魔法方法可能看起来不是很有用, 但是一旦你需要它们,你会感到庆幸它们的存在 (并且为自己阅读了本指南高兴!).

可调用对象

你也许已经知道,在Python中,方法是最高级的对象。这意味着他们也可以被传递到方法中,就像其他对象一样。这是一个非常惊人的特性。

在Python中,一个特殊的魔法方法可以让类的实例的行为表现的像函数一样,你可以调用它们,将一个函数当做一个参数传到另外一个函数中等等。这是一个非常强大的特性,其让Python编程更加舒适甜美。

  • __call__(self, [args...])
  • 允许一个类的实例像函数一样被调用。实质上说,这意味着 x() 与 x.__call__() 是相同的。注意 __call__ 的参数可变。这意味着你可以定义 __call__ 为其他你想要的函数,无论有多少个参数。

__call__ 在那些类的实例经常改变状态的时候会非常有效。“调用”这个实例是一种改变这个对象状态的直接和优雅的做法。比如这样一个例子,一个类表示了一个实体在飞机上的位置:

class Entity:

'''表示一个实体的类。调用该类以更新实体的位置。'''

 

def __init__(self, size, x, y):

self.x, self.y = x, y

self.size = size

 

def __call__(self, x, y):

'''Change the position of the entity.'''

self.x, self.y = x, y

 

# snip...

 

会话管理器

在Python 2.5中,为了代码重用而新定义了一个关键字with,其也就带来了一种with语句。会话管理在Python中并不罕见(之前是作为库的一部分而实现的),不过直到PEP 343被接受后,其就作为了一种一级语言结构。你也许在之前看到过这样的语句:

with open('foo.txt') as bar:

# 执行一些针对bar的操作

会话管理器通过包装一个with语句来设置和清理相应对象的行为。会话管理器的行为通过两个魔方方法来决定:

  • __enter__(self)
  • 定义了当使用with语句的时候,会话管理器在块被初始创建事要产生的行为。请注意,__enter__的返回值与with语句的目标或者as后的名字绑定。
  • __exit__(self, exception_type, exception_value, traceback)
  • 定义了当一个代码块被执行或者终止后,会话管理器应该做什么。它可以被用来处理异常、执行清理工作或做一些代码块执行完毕之后的日常工作。如果代码块执行成功,exception_type,exception_value,和traceback将会为None。否则,你可以选择处理这个异常或者是直接交给用户处理。如果你想处理这个异常的话,请确保__exit__在所有语句结束之后返回True。如果你想让异常被会话管理器处理的话,那么就让其产生该异常。

__enter__和__exit__对于那些定义良好以及有普通的启动和清理行为的类是很有意义的。你也可以使用这些方法来创建一般的可以包装其它对象的会话管理器。下面是一个例子:

class Closer:

'''通过with语句和一个close方法来关闭一个对象的会话管理器。'''

 

def __init__(self, obj):

self.obj = obj

 

def __enter__(self):

return self.obj # bound to target

 

def __exit__(self, exception_type, exception_val, trace):

try:

self.obj.close()

except AttributeError: # obj isn't closable

print 'Not closable.'

return True # exception handled successfully

下面是一个实际使用Closer的例子,使用一个FTP连接来证明(一个可关闭的套接字):

01 >>> from magicmethods import Closer
02 >>> from ftplib import FTP
03 >>> with Closer(FTP('ftp.somesite.com')) as conn:
04 ...     conn.dir()
05 ...
06 >>> conn.dir()
07 >>> with Closer(int(5)) as i:
08 ...     i += 1
09 ...
10 Not closable.
11 >>> i
12 6

看到我们的包装器如何友好地处理恰当和不不恰当的行为了吗?这是会话管理器和魔法方法的强大功能。请注意,Python标准库包括了一个叫作 contextlib 的模块,其包含了一个会话管理器,contextlib.closing()完成了类似的功能(当一个对象没有close()方法时则没有任何处理)。

抽象基类

见http://docs.python.org/2/library/abc.html。

创建描述器对象

描述器是通过获取、设置以及删除的时候被访问的类。当然也可以改变其它的对象。描述器并不是独立的。相反,它意味着被一个所有者类持有。当创建面向对象的数据库或者类,里面含有相互依赖的属相时,描述器将会非常有用。一种典型的使用方法是用不同的单位表示同一个数值,或者表示某个数据的附加属性(比如坐标系上某个点包含了这个点到原点的距离信息)。

为了成为一个描述器,一个类必须至少有__get__,__set__,__delete__方法被实现,让我们看看这些魔法方法:

  • __get__(self, instance, owner)
  • 定义了当描述器的值被取得的时候的行为。instance是拥有该描述器对象的一个实例。owner是拥有者本身。
  • __set__(self, instance, value)
  • 定义了当描述器的值被改变的时候的行为。instance是拥有该描述器类的一个实例。value是要设置的值。

 

  • __delete__(self, instance)
  • 定义了当描述器的值被删除的时候的行为。instance是拥有该描述器对象的一个实例。

 

下面是一个描述器的实例:单位转换。

class Meter(object):

'''对于”米“的描述器。'''

 

def __init__(self, value=0.0):

self.value = float(value)

def __get__(self, instance, owner):

return self.value

def __set__(self, instance, value):

self.value = float(value)class Foot(object):

'''对于”英尺“的描述器。'''

 

def __get__(self, instance, owner):

return instance.meter * 3.2808

def __set__(self, instance, value):

instance.meter = float(value) / 3.2808class Distance(object):

'''用米和英寸来表示两个描述器之间的距离。'''

meter = Meter()

foot = Foot()

复制

有时候,尤其是当你在处理可变对象时,你可能想要复制一个对象,然后对其做出一些改变而不希望影响原来的对象。这就是Python的copy所发挥作用的地方。然而(幸运的是),Python的模块并不是“感性”的,所以我们没必要担心一个基于Linux的机器会突然开始工作,但是我们确实需要告诉Python如何高效地复制一些东西。

  • __copy__(self)
  • 定义了当对你的类的实例调用copy.copy()时所产生的行为。copy.copy()返回了你的对象的一个浅拷贝——这意味着,当实例本身是一个新实例时,它的所有数据都被引用了——例如,当一个对象本身被复制了,它的数据仍然是被引用的(因此,对于浅拷贝中数据的更改仍然可能导致数据在原始对象的中的改变)。
  • __deepcopy__(self, memodict={})
  • 定义了当对你的类的实例调用copy.deepcopy()时所产生的行为。copy.deepcopy()返回了你的对象的一个深拷贝——对象和其数据都被拷贝了。memodict是对之前被拷贝的对象的一个缓存——这优化了拷贝过程并且阻止了对递归数据结构拷贝时的无限递归。当你想要进行对一个单独的属性进行深拷贝时,调用copy.deepcopy(),并以memodict为第一个参数。

这些魔法方法的使用例子都是什么?答案和以往一样,当你需要进行和默认行为相比,更细粒度的控制时使用这些方法。例如,你想要复制一个对象,其中以字典的形式(其可能会很大)存储了一个缓存,那么对缓存进行复制可能是没有意义的——如果当该缓存可以在内存中被多个实例共享,那么对其进行复制就确实是没意义的。

Pickling 序列化你的对象

如果你打算与其他python发烧友交换数据,那你一定应该听说过pickling。Pickling是一个将Python数据结构进行序列化的工具,它对于存储、重新取回一个对象这类工作来说真是难以置信的有用。但它也是一些担心和误解的源头。

Pickling是如此的重要,以至于它不仅仅拥有自己的模块(pickling),而且还有自己的协议和“魔术”方法。但首先,我们先简单地介绍一下Pickling如何序列化已存在的类型(如果你已经知道这些了,那么请自行飘过)。

Pickling: 赶快到盐水中泡泡

(译者注:pickle是用来直接保存Python对象的模块,在英文中有“腌制”的意思)

让我们深入挖掘pickling方法。假设你想保存一个字典并在之后检索它:你可以把它写入一个文件中,小心确保其有正确的语法,之后用exec()或者读取文件来检索它。但这很有可能是相当危险的:如果你将重要数据保存在纯文本中,它可能会损坏或者发生各种各样的改变,有些会让你的程序崩溃,有些甚至会在你的电脑上运行恶意代码。因此,我们应该使用 pickle方法:

import pickle

data = {'foo': [1, 2, 3],

'bar': ('Hello', 'world!'),

'baz': True}

jar = open('data.pkl', 'wb')

pickle.dump(data, jar) # write the pickled data to the file jar

jar.close()

几个小时之后,我们希望找回这些数据,现在我们只需unpickle它:

import pickle

pkl_file = open('data.pkl', 'rb') # connect to the pickled data

data = pickle.load(pkl_file) # load it into a variable

print data

pkl_file.close()

发生了什么?正如你所想的那样,我们现在找回了data。

现在,我们要注意一点:pickle并不完美。被pickle序列化的文件很容易被意外或是有意损坏。pickle模块可能比一般的纯文本文件要来的安全,但它仍然可能会被利用去运行恶意代码。而且它在各个Python版本之间是不兼容的,所以不要传送pkl文件并妄想其他人可以打开它。但是,pickle确实是处理缓存和其他序列化任务的强有力工具。

用Pickle序列化你的对象

pickle模块不仅可以用于内建类型,它还可以以用于序列化任何遵循pickle协议的类。pickle协议为Python对象定义了四个可选的方法,你可以重载这些方法来定义它们的行为(这和C扩展有些不同,但这不在我们的讨论范围之内):

  • __getinitargs__(self)
  • 如果你想在你的类被unpickle的时候执行__init__方法,你可以重载__getinitargs__方法,它会返回一个元组,包含你想传给__init__方法的参数。注意,这种方法只适用于旧式的Python类型(译者注:区别于2.2中引入的新式类)。
  • __getnewargs__(self)
  • 对于新式类,在unpickle的时候你可以决定传给__new__方法的参数。以上方法可以返回一个包含你想传给__new__方法的参数元组。
  • __getstate__(self)
  • 除了储存__dict__中的原来的那些变量,你可以自定义使用pickle序列化对象时想要储存的额外属性。这些属性将在你unpickle文件时被__setstate__方法使用。
  • __setstate__(self, state)
  • 当文件被unpickle时,其中保存的对象属性不会直接被写入对象的__dict中,而是会被传入这个方法。这个方法和__getstate__是配套的:当他们都被定义了的时候,你可以任意定义对象被序列化存储时的状态。
  • __reduce__(self)
  • 当你定义扩展类(使用C语言实现的Python扩展类)时,可以通过实现__reduce__函数来控制pickle的数据。如果__reduce__()方法被定义了,在一个对象被pickle时它将被调用。如果它返回一个字符串,那么pickle在将在全局空间中搜索对应名字的对象进行pickle;它还可以返回一个元组,包含2-5个元素: 一个可以用来重建该对象的可调用对象,一个包含有传给该可调用对象参数的元组,传给__setstate__方法的参数(可选),一个用于待pickle对象列表的迭代器(译者注:这些对象会被append到原来对象的后面)(可选)调用对象,一个包含有传给该可调用对象参数的元组,传给__setstate__方法的参数(可选),一个用于待pickle对象列表的迭代器(译者注:这些对象会被append到原来对象的后面)(可选),一个用于待pickle的字典的迭代器(可选)。
  • __reduce_ex__(self)
  • __reduce_ex__是为兼容性而设计的。如果它被实现了,__reduce_ex__将会取代__reduce__在pickle时被执行。__reduce__可以同时被实现以支持那些不支持__reduce_ex__的老版本pickling API。

(译者注:这段说的不是非常清楚,感兴趣可以去看文档,一般来说只要使用上一节中的方法就足够了,注意在反序列化之前要先有对象的定义,否则会出错)

一个例子

我们以Slate为例,这一段记录一个值以及这个值是何时被写入的程序,但是,这个Slate有一点特殊的地方,就是当前值不会被保存。

import time

 

class Slate:

'''Class to store a string and a changelog, and forget its value when    pickled.'''

 

def __init__(self, value):

self.value = value

self.last_change = time.asctime()

self.history = {}

 

def change(self, new_value):

# Change the value. Commit last value to history

self.history[self.last_change] = self.value

self.value = new_value

self.last_change = time.asctime()

 

def print_changes(self):

print 'Changelog for Slate object:'

for k, v in self.history.items():

print '%s\t %s' % (k, v)

 

def __getstate__(self):

# Deliberately do not return self.value or self.last_change.

# We want to have a "blank slate" when we unpickle.

return self.history

 

def __setstate__(self, state):

# Make self.history = state and last_change and value undefined

self.history = state

self.value, self.last_change = None, None

 

总结

这份指南的目的是希望为所有人带来一些知识,即使你是Python大牛或者精通面向对象开发。如果你是一个Python初学者,阅读这篇文章之后,你已经获得了编写丰富,优雅,灵活的类的知识基础了。如果你是一个有一些经验的Python程序员,你可能会发现一些能让你写的代码更简洁的方法。如果你是一个曾经使用过Python的程序员,该文可能会帮助你知晓一些新的概念和方法以及帮助你减少编写代码量的方式。如果你是一个Python专家,该文会帮助你想起来一些你已经遗忘的只是,或者一些你还没听说过的新功能。不惯你现在有多少经验,我希望这次对于Python特殊方法的旅程是真正的一次神奇之旅。(双关语的感觉真是棒!)

附录 1: 如何调用Magic Method

一些magic method已经映射到自带的方法(built-in functions);这种情况下如何调用他们是显而易见的。然而,在其他情况下,调用它们就不那么容易了。本附录致力于展示能够调用magic method的一些不被察觉的语法。

Magic Method 何时被调用(例子) Explanation
__new__(cls [,...]) instance = MyClass(arg1, arg2)  __new__ is called on instance creation
__init__(self [,...]) instance = MyClass(arg1, arg2) __init__ is called on instance creation
__cmp__(self, other) self == other, self > other, etc. Called for any comparison
__pos__(self) +self Unary plus sign
__neg__(self) -self Unary minus sign
__invert__(self) ~self Bitwise inversion
__index__(self) x[self] Conversion when object is used as index
__nonzero__(self) bool(self) Boolean value of the object
__getattr__(self, name) self.name # name doesn't exist Accessing nonexistent attribute
__setattr__(self, name, val) self.name = val Assigning to an attribute
__delattr__(self, name) del self.name Deleting an attribute
__getattribute__(self, name) self.name Accessing any attribute
__getitem__(self, key) self[key] Accessing an item using an index
__setitem__(self, key, val) self[key] = val Assigning to an item using an index
__delitem__(self, key) del self[key] Deleting an item using an index
__iter__(self) for x in self Iteration
__contains__(self, value) value in self,value not in self Membership tests using in
__call__(self [,...]) self(args) "Calling" an instance
__enter__(self) with self as x: with statement context managers
__exit__(self, exc, val, trace) with self as x: with statement context managers
__getstate__(self) pickle.dump(pkl_file, self) Pickling
__setstate__(self) data = pickle.load(pkl_file) Pickling

希望这个表能够解决你可能会遇到的哪个语法调用哪个magic method的问题。

附录 2: Python 3中的改动 

这里我们列举出一些Python 3与2.x在对象模型上主要的的不同之处。

  • 因为Python 3中string和unicode直接已经没有差别,__unicode__已经不存在了,并且__bytes__(它的行为与__str__和__unicode__类似)成为新的自带方法来构造byte数组。
  • 因为Python 3里面的division默认变成了true division,__div__在Python3中不存在了。
  • __coerce__被去除掉了是因为它与其他magic method冗余并且造成了行为混淆。
  • __cmp__被去除掉了是因为它与其他magic method冗余。
  • __nonzero__被重命名为__bool__
发表在 python | 留下评论

Python运算符重载

 

FROM : http://www.jb51.net/article/67064.htm

在Python语言中提供了类似于C++的运算符重在功能:

一下为Python运算符重在调用的方法如下:

Method         Overloads         Call for

__init__        构造函数         X=Class()

__del__         析构函数         对象销毁

__add__         +                 X+Y,X+=Y

__or__         |                 X|Y,X|=Y

__repr__        打印转换         print X,repr(X)

__str__         打印转换         print X,str(X)

__call__        调用函数         X()

__getattr_    限制             X.undefine

__setattr__     取值             X.any=value

__getitem__     索引             X[key],

__len__         长度             len(X)

__cmp__         比较             X==Y,X<Y

__lt__         小于             X<Y

__eq__         等于             X=Y

__radd__        Right-Side +         +X

__iadd__        +=                 X+=Y

__iter__        迭代             For In

1. 减法重载

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

class Number:

def __init__(self, start):

self.data = start

def __sub__(self, other): #minus method

return Number(self.data - other)

number = Number(20)

y = number – 10 # invoke __sub__ method

class Number:

def __init__(self, start):

self.data = start

def __sub__(self, other): #minus method

return Number(self.data - other)

number = Number(20)

y = number – 10 # invoke __sub__ method

2. 迭代重载

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

class indexer:

def __getitem__(self, index): #iter override

return index ** 2

X = indexer()

X[2]

for i in range(5):

print X[i]

class indexer:

def __getitem__(self, index): #iter override

return index ** 2

X = indexer()

X[2]

for i in range(5):

print X[i]

3. 索引重载

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

class stepper:

def __getitem__(self, i):

return self.data[i]

X = stepper()

X.data = 'Spam'

X[1] #call __getitem__

for item in X: #call __getitem__

print item

class stepper:

def __getitem__(self, i):

return self.data[i]

X = stepper()

X.data = 'Spam'

X[1] #call __getitem__

for item in X: #call __getitem__

print item

4. getAttr/setAttr重载

?

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

class empty:

def __getattr__(self,attrname):

if attrname == 'age':

return 40

else:

raise AttributeError,attrname

X = empty()

print X.age #call__getattr__

class accesscontrol:

def __setattr__(self, attr, value):

if attr == 'age':

# Self.attrname = value loops!

self.__dict__[attr] = value

else:

print attr

raise AttributeError, attr + 'not allowed'

X = accesscontrol()

X.age = 40   #call __setattr__

X.name = 'wang' #raise exception

class empty:

def __getattr__(self,attrname):

if attrname == 'age':

return 40

else:

raise AttributeError,attrname

X = empty()

print X.age #call__getattr__

class accesscontrol:

def __setattr__(self, attr, value):

if attr == 'age':

# Self.attrname = value loops!

self.__dict__[attr] = value

else:

print attr

raise AttributeError, attr + 'not allowed'

X = accesscontrol()

X.age = 40   #call __setattr__

X.name = 'wang' #raise exception

5. 打印重载

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

class adder:

def __init__(self, value=0):

self.data = value

def __add__(self, other):

self.data += other

class addrepr(adder):

def __repr__(self):

return 'addrepr(%s)' % self.data

x = addrepr(2) #run __init__

x + 1    #run __add__

print x   #run __repr__

class adder:

def __init__(self, value=0):

self.data = value

def __add__(self, other):

self.data += other

class addrepr(adder):

def __repr__(self):

return 'addrepr(%s)' % self.data

x = addrepr(2) #run __init__

x + 1    #run __add__

print x   #run __repr__

6. Call调用函数重载

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

class Prod:

def __init__(self, value):

self.value = value

def __call__(self, other):

return self.value * other

p = Prod(2) #call __init__

print p(1) #call __call__

print p(2)

class Prod:

def __init__(self, value):

self.value = value

def __call__(self, other):

return self.value * other

p = Prod(2) #call __init__

print p(1) #call __call__

print p(2)

7. 析构函数重载

?

1

2

3

4

5

6

7

8

class Life:

def __init__(self, name='name'):

print 'Hello', name

self.name = name

def __del__(self):

print 'Goodby', self.name

brain = Life('Brain') #call __init__

brain = 'loretta'  # call __del__

发表在 python | 留下评论

python修饰符

修饰符基础——闭包

什么是闭包呢?标准的概念大家可以看wikipedia上的解释点击打开链接   举个例子:

[python] view plain copy

 

  1. def do_add(base):
  2.     def add(increase):
  3.         return base + increase
  4.     return add

 

do_add函数里嵌套了一个内层函数add,这个内层函数就是一个闭包,其实可以也不用管这个“闭包”的概念,先来看下这种模式解决些什么问题.

 

调用do_add函数:a = do_add(23),由于内层的函数add里的逻辑用到了do_add函数的入参,而这时这个入参base绑定了值23,由于do_add函数返回的是函数add,所以这时的a其实就是内部的add绑定了23的函数add;同理可知,b = do_add(44),这里的b就是内部add绑定了44的函数add,a和b这两个add函数是不相同的,因为内部add绑定的值不同,只是两个函数的模板相同罢了,这时我们执行a(100)得到结果是123,b(100)得到结果是144。这样做有什么用呢?其实可以这样看:我们可以把a = do_add(23)和b = do_add(44)看成是配置过程,23和44是配置信息,a(100)和b(100)根据不同的配置获得不同的结果,这样我们就可以解决开发中“根据配置信息不同获得不同结果”的问题

 

而修饰符实际上就是闭包一种形式,只是配置过程参数是所修饰的一个函数,并且用@符号代替了a=do_add(23)这样的配置方式,下面看一下修饰符的用法

 

修饰符用法

 

修饰符无参数,原函数无参数

[python] view plain copy

 

  1. import time
  2. def timeit(func):
  3.     def wrapper():
  4.         start = time.clock()
  5.         func()
  6.         end =time.clock()
  7.         print 'used:', end - start
  8.     return wrapper
  9. @timeit
  10. def foo():
  11.     print 'in foo()'
  12. foo()

 

如上代码:对所修饰函数运行时间的进行统计,最后修饰过的 foo()  等价于 foo=timeit(foo) 而timeit返回wrapper,归根到底真正执行的是wrapper

在实际应用中,函数很少没有参数,所以我们看看foo有参数的情况下,修饰符怎么用

 

 

修饰符无参数,原函数有参数

[python] view plain copy

 

  1. import time
  2. def timeit(func):
  3.     def wrapper(args):
  4.         start = time.clock()
  5.         func(args)
  6.         end =time.clock()
  7.         print 'used:', end - start
  8.     return wrapper
  9. @timeit
  10. def foo(arg):
  11.     print 'in foo(),arg is' + arg
  12.     foo("aaaaa")

 

 

上述过程可以简化如下:

[decorated] foo(‘aaaaa’)   =>   timeit(foo)(‘aaaaa’)  =>  wrapper(‘aaaaa’)  =>  [real] foo(‘aaaaa’)

如果修饰符函数也有参数,又怎么用呢?

 

修饰符有参数,原函数有参数

[python] view plain copy

 

  1. import time
  2. def timeit(s):
  3.   def wrapper1(func)
  4.     def wrapper2(args):
  5.         print "the decorator's arg is"+s
  6.         start = time.clock()
  7.         func(args)
  8.         end =time.clock()
  9.         print 'used:', end - start
  10.     return wrapper2
  11.   return wrapper1
  12. @timeit(s="hello")
  13. def foo(arg):
  14.     print 'in foo(),arg is' + arg
  15. foo("aaaaa")
  16. 同理,就是多加了一层闭包。

 

应用多个修饰符

这个记住一个结论就好了,就是修饰符从离原函数最近的开始包裹,最外层的修饰符最后包裹

 

应用举例——Fibonacci数列

 

 

[python] view plain copy

 

  1. def memoize(f):
  2.     cache = {}
  3. def helper(x):
  4. if x notin cache:
  5.             cache[x] = f(x)
  6. return cache[x]
  7. return helper
  8. @memoize()
  9. def fib(n):
  10. if n in (0, 1):
  11. return n
  12. else:
  13. return fib(n - 1) + fib(n - 2)

FROM : http://blog.csdn.net/shangliuyan/article/details/8555991

发表在 python | 留下评论

python 类

http://www.cnblogs.com/wilber2013/p/4677412.html

http://www.cnblogs.com/wilber2013/p/4682046.html

http://www.cnblogs.com/wilber2013/p/4690681.html

发表在 python | 留下评论

Python Requests

发送请求

使用Requests发送网络请求非常简单。

一开始要导入Requests模块:

>>> import requests

然后,尝试获取某个网页。本例子中,我们来获取Github的公共时间线

>>> r = requests.get('https://github.com/timeline.json')

现在,我们有一个名为 r 的 Response 对象。可以从这个对象中获取所有我们想要的信息。

Requests简便的API意味着所有HTTP请求类型都是显而易见的。例如,你可以这样发送一个HTTP POST请求:

>>> r = requests.post("http://httpbin.org/post")

漂亮,对吧?那么其他HTTP请求类型:PUT, DELETE, HEAD以及OPTIONS又是如何的呢?都是一样的简单:

>>> r = requests.put("http://httpbin.org/put")

>>> r = requests.delete("http://httpbin.org/delete")

>>> r = requests.head("http://httpbin.org/get")

>>> r = requests.options("http://httpbin.org/get")

都很不错吧,但这也仅是Requests的冰山一角呢。

为URL传递参数

你也许经常想为URL的查询字符串(query string)传递某种数据。如果你是手工构建URL,那么数据会以键/值 对的形式置于URL中,跟在一个问号的后面。例如,httpbin.org/get?key=val 。 Requests允许你使用 params 关键字参数,以一个字典来提供这些参数。举例来说,如果你想传递 key1=value1 和 key2=value2 到 httpbin.org/get ,那么你可以使用如下代码:

>>> payload = {'key1': 'value1', 'key2': 'value2'}

>>> r = requests.get("http://httpbin.org/get", params=payload)

通过打印输出该URL,你能看到URL已被正确编码:

>>> print r.url

u'http://httpbin.org/get?key2=value2&key1=value1'

响应内容

我们能读取服务器响应的内容。再次以Github时间线为例:

>>> import requests

>>> r = requests.get('https://github.com/timeline.json')

>>> r.text

'[{"repository":{"open_issues":0,"url":"https://github.com/...

Requests会自动解码来自服务器的内容。大多数unicode字符集都能被无缝地解码。

请求发出后,Requests会基于HTTP头部对响应的编码作出有根据的推测。当你访问r.text 之时,Requests会使用其推测的文本编码。你可以找出Requests使用了什么编码,并且能够使用 r.encoding 属性来改变它:

>>> r.encoding

'utf-8'

>>> r.encoding = 'ISO-8859-1'

如果你改变了编码,每当你访问 r.text ,Request都将会使用 r.encoding 的新值。

在你需要的情况下,Requests也可以使用定制的编码。如果你创建了自己的编码,并使用codecs 模块进行注册,你就可以轻松地使用这个解码器名称作为 r.encoding 的值, 然后由Requests来为你处理编码。

二进制响应内容

你也能以字节的方式访问请求响应体,对于非文本请求:

>>> r.content

b'[{"repository":{"open_issues":0,"url":"https://github.com/...

Requests会自动为你解码 gzip 和 deflate 传输编码的响应数据。

例如,以请求返回的二进制数据创建一张图片,你可以使用如下代码:

>>> from PIL import Image

>>> from StringIO import StringIO

>>> i = Image.open(StringIO(r.content))

JSON响应内容

Requests中也有一个内置的JSON解码器,助你处理JSON数据:

>>> import requests

>>> r = requests.get('https://github.com/timeline.json')

>>> r.json()

[{u'repository': {u'open_issues': 0, u'url': 'https://github.com/...

如果JSON解码失败, r.json 就会抛出一个异常。

原始响应内容

在罕见的情况下你可能想获取来自服务器的原始套接字响应,那么你可以访问 r.raw 。 如果你确实想这么干,那请你确保在初始请求中设置了 stream=True 。具体的你可以这么做:

>>> r = requests.get('https://github.com/timeline.json', stream=True)

>>> r.raw

<requests.packages.urllib3.response.HTTPResponse object at 0x101194810>

>>> r.raw.read(10)

'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03'

定制请求头

如果你想为请求添加HTTP头部,只要简单地传递一个 dict 给 headers 参数就可以了。

例如,在前一个示例中我们没有指定content-type:

>>> import json

>>> url = 'https://api.github.com/some/endpoint'

>>> payload = {'some': 'data'}

>>> headers = {'content-type': 'application/json'}

 

>>> r = requests.post(url, data=json.dumps(payload), headers=headers)

更加复杂的POST请求

通常,你想要发送一些编码为表单形式的数据—非常像一个HTML表单。 要实现这个,只需简单地传递一个字典给 data 参数。你的数据字典 在发出请求时会自动编码为表单形式:

>>> payload = {'key1': 'value1', 'key2': 'value2'}

>>> r = requests.post("http://httpbin.org/post", data=payload)

>>> print r.text

{

...

"form": {

"key2": "value2",

"key1": "value1"

},

...

}

很多时候你想要发送的数据并非编码为表单形式的。如果你传递一个 string 而不是一个dict ,那么数据会被直接发布出去。

例如,Github API v3接受编码为JSON的POST/PATCH数据:

>>> import json

>>> url = 'https://api.github.com/some/endpoint'

>>> payload = {'some': 'data'}

 

>>> r = requests.post(url, data=json.dumps(payload))

POST一个多部分编码(Multipart-Encoded)的文件

Requests使得上传多部分编码文件变得很简单:

>>> url = 'http://httpbin.org/post'

>>> files = {'file': open('report.xls', 'rb')}

 

>>> r = requests.post(url, files=files)

>>> r.text

{

...

"files": {

"file": "<censored...binary...data>"

},

...

}

你可以显式地设置文件名:

>>> url = 'http://httpbin.org/post'

>>> files = {'file': ('report.xls', open('report.xls', 'rb'))}

 

>>> r = requests.post(url, files=files)

>>> r.text

{

...

"files": {

"file": "<censored...binary...data>"

},

...

}

如果你想,你也可以发送作为文件来接收的字符串:

>>> url = 'http://httpbin.org/post'

>>> files = {'file': ('report.csv', 'some,data,to,send\nanother,row,to,send\n')}

 

>>> r = requests.post(url, files=files)

>>> r.text

{

...

"files": {

"file": "some,data,to,send\\nanother,row,to,send\\n"

},

...

}

响应状态码

我们可以检测响应状态码:

>>> r = requests.get('http://httpbin.org/get')

>>> r.status_code

200

为方便引用,Requests还附带了一个内置的状态码查询对象:

>>> r.status_code == requests.codes.ok

True

如果发送了一个失败请求(非200响应),我们可以通过 Response.raise_for_status() 来抛出异常:

>>> bad_r = requests.get('http://httpbin.org/status/404')

>>> bad_r.status_code

404

 

>>> bad_r.raise_for_status()

Traceback (most recent call last):

File "requests/models.py", line 832, in raise_for_status

raise http_error

requests.exceptions.HTTPError: 404 Client Error

但是,由于我们的例子中 r 的 status_code 是 200 ,当我们调用 raise_for_status() 时,得到的是:

>>> r.raise_for_status()

None

一切都挺和谐哈。

响应头

我们可以查看以一个Python字典形式展示的服务器响应头:

>>> r.headers

{

'status': '200 OK',

'content-encoding': 'gzip',

'transfer-encoding': 'chunked',

'connection': 'close',

'server': 'nginx/1.0.4',

'x-runtime': '148ms',

'etag': '"e1ca502697e5c9317743dc078f67693f"',

'content-type': 'application/json; charset=utf-8'

}

但是这个字典比较特殊:它是仅为HTTP头部而生的。根据 RFC 2616 , HTTP头部是大小写不敏感的。

因此,我们可以使用任意大写形式来访问这些响应头字段:

>>> r.headers['Content-Type']

'application/json; charset=utf-8'

 

>>> r.headers.get('content-type')

'application/json; charset=utf-8'

如果某个响应头字段不存在,那么它的默认值为 None

>>> r.headers['X-Random']

None

Cookies

如果某个响应中包含一些Cookie,你可以快速访问它们:

>>> url = 'http://example.com/some/cookie/setting/url'

>>> r = requests.get(url)

 

>>> r.cookies['example_cookie_name']

'example_cookie_value'

要想发送你的cookies到服务器,可以使用 cookies 参数:

>>> url = 'http://httpbin.org/cookies'

>>> cookies = dict(cookies_are='working')

 

>>> r = requests.get(url, cookies=cookies)

>>> r.text

'{"cookies": {"cookies_are": "working"}}'

重定向与请求历史

使用GET或OPTIONS时,Requests会自动处理位置重定向。

Github将所有的HTTP请求重定向到HTTPS。可以使用响应对象的 history 方法来追踪重定向。 我们来看看Github做了什么:

>>> r = requests.get('http://github.com')

>>> r.url

'https://github.com/'

>>> r.status_code

200

>>> r.history

[<Response [301]>]

Response.history 是一个:class:Request 对象的列表,为了完成请求而创建了这些对象。这个对象列表按照从最老到最近的请求进行排序。

如果你使用的是GET或OPTIONS,那么你可以通过 allow_redirects 参数禁用重定向处理:

>>> r = requests.get('http://github.com', allow_redirects=False)

>>> r.status_code

301

>>> r.history

[]

如果你使用的是POST,PUT,PATCH,DELETE或HEAD,你也可以启用重定向:

>>> r = requests.post('http://github.com', allow_redirects=True)

>>> r.url

'https://github.com/'

>>> r.history

[<Response [301]>]

超时

你可以告诉requests在经过以 timeout 参数设定的秒数时间之后停止等待响应:

>>> requests.get('http://github.com', timeout=0.001)

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

requests.exceptions.Timeout: HTTPConnectionPool(host='github.com', port=80): Request timed out. (timeout=0.001)

注:

timeout 仅对连接过程有效,与响应体的下载无关。

错误与异常

遇到网络问题(如:DNS查询失败、拒绝连接等)时,Requests会抛出一个ConnectionError 异常。

遇到罕见的无效HTTP响应时,Requests则会抛出一个 HTTPError 异常。

若请求超时,则抛出一个 Timeout 异常。

若请求超过了设定的最大重定向次数,则会抛出一个 TooManyRedirects 异常。

所有Requests显式抛出的异常都继承自 requests.exceptions.RequestException 。

 

FROM : http://blog.csdn.net/iloveyin/article/details/21444613

发表在 python | 留下评论

django自定义404、500等错误模板

在目录/srv/django/下通过django-admin startproject testProject创建项目

生成/srv/django/testProject 、 /srv/django/testProject/testProject目录

在/srv/django/testProject 下使用django-admin startapp testApp生成/srv/django/testProject/testApp目录

自定义404错误:

在/srv/django/testProject/testProject/urls.py中增加handler404:

from django.conf.urls import url 
from django.contrib import admin
from django.conf.urls import include

urlpatterns = [ 
    url(r'^admin/', admin.site.urls),
    url(r'^testApp/', include('testApp.urls')),
]

handler404 = 'testApp.views.nof'

/srv/django/testProject/testApp/urls.py

from django.conf.urls import url
#from testApp import views
from . import views

urlpatterns = [
    url(r'^test/$',views.test,name='test'),
]

/srv/django/testProject/testApp/views.py内容:

def test(request):
    raise Http404('test 404 page')

def nof(request, exception):     
    return render_to_response('404.html',{'title':'not found'})

在/srv/django/testProject/testApp/templates中增加404.html

/srv/django/testProject/testProject/settings.py

DEBUG = FALSE

访问 xxxx/testApp/test/即可看到效果。

发表在 python | 留下评论

python str和repr的区别

尽管str(),repr()和``运算在特性和功能方面都非常相似,事实上repr()和``做的是完全一样的事情,它们返回的是一个对象的“官方”字符串表示,也就是说绝大多数情况下可以通过求值运算(使用内建函数eval())重新得到该对象。

但str()则有所不同,str()致力于生成一个对象的可读性好的字符串表示,它的返回结果通常无法用于eval()求值,但很适合用于print语句输出。需要再次提醒的是,并不是所有repr()返回的字符串都能够用 eval()内建函数得到原来的对象。 也就是说 repr() 输出对 Python比较友好,而str()的输出对用户比较友好。

虽然如此,很多情况下这三者的输出仍然都是完全一样的。 大家可以看下下面的代码,来进行对比

>>> s = 'Hello, world.'
>>> str(s)
'Hello, world.'
>>> repr(s)
"'Hello, world.'"
>>> str(0.1)
'0.1'
>>> repr(0.1)
'0.10000000000000001'
>>> x = 10 * 3.25
>>> y = 200 * 200
>>> s = 'The value of x is ' + repr(x) + ', and y is ' + repr(y) + '...'
>>> print s
The value of x is 32.5, and y is 40000...
>>> # The repr() of a string adds string quotes and backslashes:
... hello = 'hello, world\n'
>>> hellos = repr(hello)
>>> print hellos
'hello, world\n'
>>> # The argument to repr() may be any Python object:
... repr((x, y, ('spam', 'eggs')))
"(32.5, 40000, ('spam', 'eggs'))"
发表在 python | 留下评论

python unicode str

FROM :http://blog.chinaunix.net/uid-10597892-id-2946918.html

字符串在Python内部的表示是unicode编码,因此,在做编码转换时,通常需要以unicode作为中间编码,即先将其他编码的字符串解码(decode)成unicode,再从unicode编码(encode)成另一种编码。

decode的作用是将其他编码的字符串转换成unicode编码,如str1.decode('gb2312'),表示将gb2312编码的字符串str1转换成unicode编码。

encode的作用是将unicode编码转换成其他编码的字符串,如str2.encode('gb2312'),表示将unicode编码的字符串str2转换成gb2312编码。

因此,转码的时候一定要先搞明白,字符串str是什么编码,然后decode成unicode,然后再encode成其他编码

 

UTF-8 是一种编码方式, 它主要是用来解决序号到二进制数据之间的转化问题。这里的序号怎么理解呢? 比如那 ascii 编码为例, 数字 0 在 ascii 码表中的序号是 60,那么 ascii编码要解决的问题就是将 60 转为为二进制,而后进行存储和传输。因为 ascii 所表示的字符集非常有限, 不到 256 个, 如果需要用序号来标识一个字符的话,0 - 255 就够用了,将小于 256 的数字转化为二进制最节约的方式就是采用一个 byte, 所以 ascii 编码方式下每个字符最后存储所占用的大小就是 1 byte。 但是对于中文 ascii 就无能为力了, 因为中文的字数远远超过 256 个, 导致序号不够用了,所以我们要引入更多的序号,这就诞生了例如 GBK ,UTF-8 等编码方式。最大的序号也就决定了编码方式中一个字符所占用的最小空间, 当然可以采用变长编码的方式节约空间。
Unicode 是字符集, unicode 的引入是为了囊括世界上所有语言的字符,如 Unicode 字符列表。Unicode 字符集引入了字符到序号的映射关系, 有了序号我们也就知道它代表哪个字符了。 有了序号之后我们可以采用我们需要的编码方式进行编码,例如 UTF-8,GBK,ascii 等。
总结起来就是 UTF-8 等编码方式解决的是二进制到序号之间的对应问题, Unicode 等字符集解决的是字符到序号之间的对应问题。

 

代码中字符串的默认编码与代码文件本身的编码一致。

如:s='中文'

如果是在utf8的文件中,该字符串就是utf8编码,如果是在gb2312的文件中,则其编码为gb2312。这种情况下,要进行编码转换,都需 要先用decode方法将其转换成unicode编码,再使用encode方法将其转换成其他编码。通常,在没有指定特定的编码方式时,都是使用的系统默 认编码创建的代码文件。

如果字符串是这样定义:s=u'中文'

则该字符串的编码就被指定为unicode了,即python的内部编码,而与代码文件本身的编码无关。因此,对于这种情况做编码转换,只需要直接使用encode方法将其转换成指定编码即可。

如果一个字符串已经是unicode了,再进行解码则将出错,因此通常要对其编码方式是否为unicode进行判断:

isinstance(s, unicode) #用来判断是否为unicode

用非unicode编码形式的str来encode会报错

如何获得系统的默认编码?

#!/usr/bin/env python
#coding=utf-8
import sys
print sys.getdefaultencoding()

该段程序在英文WindowsXP上输出为:ascii

在某些IDE中,字符串的输出总是出现乱码,甚至错误,其实是由于IDE的结果输出控制台自身不能显示字符串的编码,而不是程序本身的问题。

如在UliPad中运行如下代码:

s=u"中文"
print s

会提示:UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)。这是因为UliPad在英文WindowsXP上的控制台信息输出窗口是按照ascii编码输出的(英文系统的默认编码是 ascii),而上面代码中的字符串是Unicode编码的,所以输出时产生了错误。

将最后一句改为:print s.encode('gb2312')

则能正确输出“中文”两个字。

若最后一句改为:print s.encode('utf8')

则输出:\xe4\xb8\xad\xe6\x96\x87,这是控制台信息输出窗口按照ascii编码输出utf8编码的字符串的结果。

unicode(str,'gb2312')与str.decode('gb2312')是一样的,都是将gb2312编码的str转为unicode编码

使用str.__class__可以查看str的编码形式


>>>>>
groups.google.com/group/python-cn/browse_thread/thread/be4e4e0d4c3272dd
-----
python是个容易出现编码问题的语言。所以,我按照我的理解写下下面这些文字。
=首先,要了解几个概念。=
*字节:计算机数据的表示。8位二进制。可以表示无符号整数:0-255。下文,用“字节流”表示“字节”组成的串。
*字符:英文字符“abc”,或者中文字符“你我他”。字符本身不知道如何在计算机中保存。下文中,会避免使用“字符串”这个词,而用“文本”来表
示“字符”组成的串。
*编码(动词):按照某种规则(这个规则称为:编码(名词))将“文本”转换为“字节流”。(在python中:unicode变成str)
*解码(动词):将“字节流”按照某种规则转换成“文本”。(在python中:str变成unicode)
**实际上,任何东西在计算机中表示,都需要编码。例如,视频要编码然后保存在文件中,播放的时候需要解码才能观看。
unicode:unicode定义了,一个“字符”和一个“数字”的对应,但是并没有规定这个“数字”在计算机中怎么保存。(就像在C中,一个整数既
可以是int,也可以是short。unicode没有规定用int还是用short来表示一个“字符”)
utf8:unicode实现。它使用unicode定义的“字符”“数字”映射,进而规定了,如何在计算机中保存这个数字。其它的utf16等都是
unicode实现。
gbk:类似utf8这样的“编码”。但是它没有使用unicode定义的“字符”“数字”映射,而是使用了另一套的映射方法。而且,它还定义了如何在
计算机中保存。

=python中的encode,decode方法=
首先,要知道encode是 unicode转换成str。decode是str转换成unicode。
下文中,u代表unicode类型的变量,s代表str类型的变量。
u.encode('...')基本上总是能成功的,只要你填写了正确的编码。就像任何文件都可以压缩成zip文件。
s.decode('...')经常是会出错的,因为str是什么“编码”取决于上下文,当你解码的时候需要确保s是用什么编码的。就像,打开zip文
件的时候,你要确保它确实是zip文件,而不仅仅是伪造了扩展名的zip文件。
u.decode(),s.encode()不建议使用,s.encode相当于s.decode().encode()首先用默认编码(一般是
ascii)转换成unicode在进行encode。

=关于#coding=utf8=
当你在py文件的第一行中,写了这句话,并确实按照这个编码保存了文本的话,那么这句话有以下几个功能。
1.使得词法分析器能正常运作,对于注释中的中文不报错了。
2.对于u"中文"这样literal string能知道两个引号中的内容是utf8编码的,然后能正确转换成unicode
3."中文"对于这样的literal string你会知道,这中间的内容是utf8编码,然后就可以正确转换成其它编码或unicode了。

没有写完,先码那么多字,以后再来补充,这里不是wiki,太麻烦了。

>>>>>
>>>>>
=Python编码和Windows控制台=
我发现,很多初学者出错的地方都在print语句,这牵涉到控制台的输出。我不了解linux,所以只说控制台的。
首先,Windows的控制台确实是unicode(utf16_le编码)的,或者更准确的说使用字符为单位输出文本的。
但是,程序的执行是可以被重定向到文件的,而文件的单位是“字节”。
所以,对于C运行时的函数printf之类的,输出必须有一个编码,把文本转换成字节。可能是为了兼容95,98,
没有使用unicode的编码,而是mbcs(不是gbk之类的)。
windows的mbcs,也就是ansi,它会在不同语言的windows中使用不同的编码,在中文的windows中就是gb系列的编码。
这造成了同一个文本,在不同语言的windows中是不兼容的。
现在我们知道了,如果你要在windows的控制台中输出文本,它的编码一定要是“mbcs”。
对于python的unicode变量,使用print输出的话,会使用sys.getfilesystemencoding()返回的编码,把它变成str。
如果是一个utf8编码str变量,那么就需要 print s.decode('utf8').encode('mbcs')
最后,对于str变量,file文件读取的内容,urllib得到的网络上的内容,都是以“字节”形式的。
它们如果确实是一段“文本”,比如你想print出来看看。那么你必须知道它们的编码。然后decode成unicode。
如何知道它们的编码:
1.事先约定。(比如这个文本文件就是你自己用utf8编码保存的)
2.协议。(python文件第一行的#coding=utf8,html中的等)
2.猜。

>>>>>

> 这个非常好,但还不是很明白
> 将“文本”转换为“字节流”。(在python中:unicode变成str)
 

"最后,对于str变量,file文件读取的内容,urllib得到的网络上的内容,都是以“字节”形式的。"
虽然文件或者网页是文本的,但是在保存或者传输时已经被编码成bytes了,所以用"rb"打开的file和从socket读取的流是基于字节的.
"它们如果确实是一段“文本”,比如你想print出来看看。那么你必须知道它们的编码。然后decode成unicode。"
这里的加引号的"文本",其实还是字节流(bytes),而不是真正的文本(unicode),只是说明我们知道他是可以解码成文本的.
在解码的时候,如果是基于约定的,那就可以直接从指定地方读取如BOM或者python文件的指定coding或者网页的meta,就可以正确解码,
但是现在很多文件/网页虽然指定了编码,但是文件格式实际却使用了其他的编码(比如py文件指定了coding=utf8,但是你还是可以保存成ansi--记事本的默认编码),这种情况下真实的编码就需要去猜了
解码了的文本只存在运行环境中,如果你需要打印/保存/输出给数据库/网络传递,就又需要一次编码过程,这个编码与上面的编码没有关系,只是依赖于你的选择,但是这个编码也不是可以随便选择的,因为编码后的bytes如果又需要传递给其他人/环境,那么如果你的编码也不遵循约定,又给下一个人/环境造成了困扰,于是递归之~~~~ 

>>>>>

> 主要有一条非常容易误解:
> 一般人会认为Unicode(广义)统一了编码,其实不然。Unicode不是唯一的编码,而一大堆编码的统称。但是Windows下Unicode
> (狭义)一般特指UCS2,也就是UTF-16/LE
 

unicode作为字符集(ucs)是唯一的,编码方案(utf)才是有很多种

>>>>>
将字符与字节的概念区分开来是很重要的。Java 一直就是这样,Python也开始这么做了,Ruby 貌似还在混乱当中。
>>>>>
>>>>>
我也说两句。我对编码的研究相对比较深一些。因为工作中也经常遇到乱码,于是在05年,对编码专门做过研究,并在公司刊物上发过文章,最后形成了一个教材,每年在公司给新员工都讲一遍。于是项目中遇到乱码的问题就能很快的定位并解决了。
理论上,从一个字符到具体的编码,会经过以下几个概念。
字符集(Abstract character repertoire)
编码字符集(Coded character set)
字符编码方式(Character encoding form)
字符编码方案(Character encoding scheme )
字符集:就算一堆抽象的字符,如所有中文。字符集的定义是抽象的,与计算机无关。
编码字符集:是一个从整数集子集到字符集抽象元素的映射。即给抽象的字符编上数字。如gb2312中的定义的字符,每个字符都有个整数和它对应。一个整数只对应着一个字符。反过来,则不一定是。这里所说的映射关系,是数学意义上的映射关系。编码字符集也是与计算机无关的。unicode字符集也在这一层。
字符编码方式:这个开始与计算机有关了。编码字符集的编码点在计算机里的具体表现形式。通俗的说,意思就是怎么样才能将字符所对应的整数的放进计算机内存,或文件、或网络中。于是,不同人有不同的实现方式,所谓的万码奔腾,就是指这个。gb2312,utf-8,utf-16,utf-32等都在这一层。
字符编码方案:这个更加与计算机密切相关。具体是与操作系统密切相关。主要是解决大小字节序的问题。对于UTF-16和UTF-32
编码,Unicode都支持big-endian 和 little-endian两种编码方案。
一般来说,我们所说的编码,都在第三层完成。具体到一个软件系统中,则很复杂。
浏览器-apache-tomcat(包括tomcat内部的jsp编码、编译,文件读取)-数据库之间,只要存在数据交互,就有可能发生编码不一致,如果在读取数据时,没有正确的decode和encode,出现乱码就是家常便饭了。

发表在 python | 留下评论

Python bytes

Python中的字节码用b'xxx'的形式表示。x可以用字符表示,也可以用ASCII编码形式\xnn表示,nn从00-ff(十六进制)共256种字符。

一、基本操作

下面列举一下字节的基本操作,可以看出来它和字符串还是非常相近的:

In[40]: b = b"abcd\x64"
In[41]: b
Out[41]: b'abcdd'
In[42]: type(b)
Out[42]: bytes
In[43]: len(b)
Out[43]: 5
In[44]: b[4]
Out[44]: 100 # 100用十六进制表示就是\x64

如果想要修改一个字节串中的某个字节,不能够直接修改,需要将其转化为bytearray后再进行修改:

In[46]: barr = bytearray(b)
In[47]: type(barr)
Out[47]: bytearray
In[48]: barr[0] = 110
In[49]: barr
Out[49]: bytearray(b'nbcdd')

二、字节与字符的关系

上面也提到字节跟字符很相近,其实它们是可以相互转化的。字节通过某种编码形式就可以转化为相应的字符。字节通过encode()方法传入编码方式就可以转化为字符,而字符通过decode()方法就可以转化为字节:

In[50]: s = "人生苦短,我用Python"
In[51]: b = s.encode('utf-8')
In[52]: b
Out[52]: b'\xe4\xba\xba\xe7\x94\x9f\xe8\x8b\xa6\xe7\x9f\xad\xef\xbc\x8c\xe6\x88\x91\xe7\x94\xa8Python'
In[53]: c = s.encode('gb18030')
In[54]: c
Out[54]: b'\xc8\xcb\xc9\xfa\xbf\xe0\xb6\xcc\xa3\xac\xce\xd2\xd3\xc3Python'
In[55]: b.decode('utf-8')
Out[55]: '人生苦短,我用Python'
In[56]: c.decode('gb18030')
Out[56]: '人生苦短,我用Python'
In[57]: c.decode('utf-8')
Traceback (most recent call last):
 exec(code_obj, self.user_global_ns, self.user_ns)
 File "<ipython-input-57-8b50aa70bce9>", line 1, in <module>
 c.decode('utf-8')
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc8 in position 0: invalid continuation byte
In[58]: b.decode('gb18030')
Out[58]: '浜虹敓鑻︾煭锛屾垜鐢≒ython'

我们可以看到用不同的编码方式解析出来的字符和字节的方式是完全不同,如果编码和解码用了不同的编码方式,就会产生乱码,甚至转换失败。因为每种编码方式包含的字节种类数目不同,如上例中的\xc8就超出了utf-8的最大字符。

三、应用

举个最简单的例子,我要爬取一个网页的内容,现在来爬取用百度搜索Python时返回的页面,百度用的是utf-8编码格式,如果不对返回结果解码,那它就是一个超级长的字节串。而进行正确解码后就可以显示一个正常的html页面。

import urllib.request

url = "http://www.baidu.com/s?ie=utf-8&wd=python"
page = urllib.request.urlopen(url)
mybytes = page.read()
encoding = "utf-8"
print(mybytes.decode(encoding))
page.close()
发表在 python | 留下评论

python 中 unicode,str 以及编码总结

  • python 中 u"哈哈" == "哈哈" 的返回结果是什么? 如果你的答案是 True 推荐看看下面的内容, 如果你的答案是 False 也推荐看看下面的内容。 (smile)

>>> u"哈哈".encode('utf8') == "哈哈"

True

>>> u"哈哈" == "哈哈"

False

>>> u"哈哈" == "哈哈".decode('utf8')

True

u"哈哈"是unicode,"哈哈"是utf8,将u"哈哈" encode为utf8或 将 "哈哈"decode 为unicode后,两者相等。

s.decode(defaultencoding) , 将s以 defaultencoding 方式解码为unicode

1. 什么是 unicode?

unicode: 字符集。比如英文字母,汉字都属于 unicode 字符集表达的范畴。unicode 字符集出现的目的就是为了兼容全世界所有的文字符号。

经常会混淆的两个概念, utf-8 和 unicode 是什么关系? whats-the-difference-between-unicode-and-utf8

UTF-8 and Unicode cannot be compared. UTF-8 is an encoding used to translate numbers into binary data. Unicode is a character set used to translate characters into numbers.


UTF-8 是一种编码方式, 它主要是用来解决序号到二进制数据之间的转化问题。这里的序号怎么理解呢? 比如那 ascii 编码为例, 数字 0 在 ascii 码表中的序号是 60,那么 ascii编码要解决的问题就是将 60 转为为二进制,而后进行存储和传输。因为 ascii 所表示的字符集非常有限, 不到 256 个, 如果需要用序号来标识一个字符的话,0 - 255 就够用了,将小于 256 的数字转化为二进制最节约的方式就是采用一个 byte, 所以 ascii 编码方式下每个字符最后存储所占用的大小就是 1 byte。 但是对于中文 ascii 就无能为力了, 因为中文的字数远远超过 256 个, 导致序号不够用了,所以我们要引入更多的序号,这就诞生了例如 GBK ,UTF-8 等编码方式。最大的序号也就决定了编码方式中一个字符所占用的最小空间, 当然可以采用变长编码的方式节约空间。

Unicode 是字符集, unicode 的引入是为了囊括世界上所有语言的字符,如 Unicode 字符列表。Unicode 字符集引入了字符到序号的映射关系, 有了序号我们也就知道它代表哪个字符了。 有了序号之后我们可以采用我们需要的编码方式进行编码,例如 UTF-8,GBK,ascii 等。

总结起来就是 UTF-8 等编码方式解决的是二进制到序号之间的对应问题, Unicode 等字符集解决的是字符到序号之间的对应问题。

上面都是略为抽象的概念, 下面来看几个实际的例子和详细分析。

2. python unicode 和编码实例分析

>>> u"哈"
u'\u54c8'
>>> u"哈".encode("utf8")
'\xe5\x93\x88'
>>> u"哈".encode("gbk")
'\xb9\xfe'

 

“哈” 是 string, 默认采用了 utf-8 的方式进行了编码,  所以输出二进制数据 '\xe5\x93\x88', 关于 str 和 unicode 的区别后文会详述。

u"哈" 是 unicode,其 numbers 是 ox54c8,采用 utf-8 编码这个序号的二进制数据为  '\xe5\x93\x88', 也可以采用 gbk 的方式进行编码,编码后二进制数据为 '\xb9\xfe'。 可以看出 “哈” 对应的序号采用 UTF-8 编码为三个字节, 采用 GBK 编码则为两个字节。

 

>>> u"哈".encode("utf8")
'\xe5\x93\x88'
>>> u"哈".encode("utf8").decode("gbk")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'gbk' codec can't decode byte 0x88 in position 2: incomplete multibyte sequence
>>> u"哈哈".encode("utf8").decode("gbk")
u'\u935d\u581d\u6431'
>>> print u"哈哈".encode("utf8").decode("gbk")
鍝堝搱
  • u"哈".encode("utf8"): unicode 字符采用 utf8 编码,结果为 \xe5\x93\x88, 占用三个字节。
  • u"哈".encode("utf8").decode("gbk"): 报错了,报错信息为 gbk 无法解码 0x88,0x88 是 utf8 编码后的第三个字节,
    由于 GBK 的编码都是两个字节的, 所以一个字节 GBK 就不认识了,出现了解码错误。
  • u"哈哈".encode("utf8").decode("gbk"):如果上一个报错是因为字节的数目不对,那么"哈哈"就是 6 个字节了,
    GBK 能解析出三个字符序号,从结果看果然输出了三个汉字(没有一个认识的,得多读书了)。
    
    

4. python unicode vs string

string 可以理解为 byte[],  "哈" 即为一个 string, 如下例:

>>> "哈"
'\xe5\x93\x88'
>>> u"哈"
u'\u54c8'
>>> type("哈")
<type 'str'>
>>> type(u"哈")
<type 'unicode'>
>>> len("哈")
3
>>> len(u"哈")
1
>>> u"哈".encode("utf8")
'\xe5\x93\x88'
>>> "哈".decode("utf8") == u"哈"
True
>>> "哈" == u"哈".encode("utf8")
True

 

  • "哈" :类型为 str,长度为 3,'\xe5\x93\x88' 为 UTF-8 编码之后的字节数组。
  • u"哈":类型为 unicode,长度为 1, u'\u54c8', 代表在 unicode 字符集的序号。
  • "哈".decode("utf8") == u"哈":  str "哈" '\xe5\x93\x88' ,采用 utf8 解码之后即为 unicode 字符 "哈"。
  • "哈" == u"哈".encode("utf8"):  u"哈" 采用 utf8 编码之后即为字节数组 '\xe5\x93\x88'。

5. u"哈哈" 等于 "哈哈" 么?

>>> u"abc" == "abc"
True
>>> u"哈" == "哈"
__main__:1: UnicodeWarning: Unicode equal comparison failed to convert both arguments to Unicode - interpreting them as being unequal
False

 

u"abc" == "abc" 返回了 True,但是 u"哈" == "哈" 返回了 False, 这是为什么呢?

因为 u"哈" 和 "哈" 类型不同,从 Waring 的信息可以看出在比较的时候 python 会试图进行转换将 "哈" 转化为 Unicode,但是为什么转换会失败呢? 我们在看看下面的例子:

 

>>> import sys
>>> reload(sys)
<module 'sys' (built-in)>
>>> sys.getdefaultencoding()
'ascii'
>>> sys.setdefaultencoding("utf8")
>>> u"哈" == "哈"
True
  • sys.getdefaultencoding(): 获取默认编码, 可以看到默认编码为 ascii。
  • sys.setdefaultencoding("utf8"): 设置默认编码为 UTF-8, 可以看到这个时候 u"哈" 又等于 "哈" 了。

从上面的例子可以看出 u"哈" == "哈" 其实可以等价为: u"哈" == "哈".encode(sys.getdefaultencoding()), 直接比较 str 和 unicode 是很危险, 因为它的结果并不固定!

6. python 统一使用 unicode

首先说一下为什么会有这篇总结?因为在写 python 程序的时候踩到了坑,具体情况是这样的,程序从数据库里面读取了一个城市名称集合 cities,然后采用 Thrift 框架调用了一个远程服务返回当前所在的城市 city,然后判断获取的城市是否在集合中,程序从数据库中取回集合中的数据为 unicode 类型, 但是 Thrift 请求回来的字符串为 str 类型,这个时候 city in cities 永远都返回 False(均为中文的情况),导致程序逻辑错误。

另一个值得注意的地方:在 Thrift 的 protocol 设置为 thrift.protocol.TBinaryProtocol.TBinaryProtocolAccelerated 的时候,程序的返回均为 str 类型,但是将 protocal 设置为 thrift.protocol.TBinaryProtocol.TBinaryProtocol 的时候程序返回即为 unicode 类型。

我们在 python 程序中的数据来源于一般有如下几个方面: 数据库数据, RPC调用返回数据,用户输入数据, 程序硬编码。 在 python 程序中处理这些数据的时候,如果我们想远离编码和类型等问题带来的麻烦,那么请将 python 中的字符串采用 unicode 形式进行处理

发表在 python | 留下评论

Python中的str与unicode处理方法

FROM : http://python.jobbole.com/81244/

str与字节码

首先,我们完全不谈unicode。

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

Python

s = "人生苦短"

1 s = "人生苦短"

[Format Time: 0.0004 seconds]

s是个字符串,它本身存储的就是字节码。那么这个字节码是什么格式的?

如果这段代码是在解释器上输入的,那么这个s的格式就是解释器的编码格式,对于windows的cmd而言,就是gbk。

如果将段代码是保存后才执行的,比如存储为utf-8,那么在解释器载入这段程序的时候,就会将s初始化为utf-8编码。

unicode与str

我们知道unicode是一种编码标准,具体的实现标准可能是utf-8,utf-16,gbk ……

python 在内部使用两个字节来存储一个unicode,使用unicode对象而不是str的好处,就是unicode方便于跨平台。

你可以用如下两种方式定义一个unicode:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

Python

s1 = u"人生苦短"

s2 = unicode("人生苦短", "utf-8")

1

2

s1 = u"人生苦短"

s2 = unicode("人生苦短", "utf-8")

[Format Time: 0.0006 seconds]

 

encode与decode

在python中的编码解码是这样的:

 

所以我们可以写这样的代码:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

Python

# -*- coding:utf-8 -*-

su = "人生苦短"

# : su是一个utf-8格式的字节串

u  = s.decode("utf-8")

# : s被解码为unicode对象,赋给u

sg = u.encode("gbk")

# : u被编码为gbk格式的字节串,赋给sg

print sg

# 打印sg

1

2

3

4

5

6

7

8

9

# -*- coding:utf-8 -*-

su = "人生苦短"

# : su是一个utf-8格式的字节串

u  = s.decode("utf-8")

# : s被解码为unicode对象,赋给u

sg = u.encode("gbk")

# : u被编码为gbk格式的字节串,赋给sg

print sg

# 打印sg

[Format Time: 0.0010 seconds]

但是事实情况要比这个复杂,比如看如下代码:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

Python

s = "人生苦短"

s.encode('gbk')

1

2

s = "人生苦短"

s.encode('gbk')

[Format Time: 0.0005 seconds]

看!str也能编码,(事实上unicode对象也能解码,但是意义不大)

这样为什么可以?看上图的编码流程的箭头,你就能想到原理,当对str进行编码时,会先用默认编码将自己解码为unicode,然后在将unicode编码为你指定编码。

这就引出了python2.x中在处理中文时,大多数出现错误的原因所在:python的默认编码,defaultencoding是ascii

看这个例子:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

Python

# -*- coding: utf-8 -*-

s = "人生苦短"

s.encode('gbk')

1

2

3

# -*- coding: utf-8 -*-

s = "人生苦短"

s.encode('gbk')

[Format Time: 0.0005 seconds]

上面的代码会报错,错误信息:UnicodeDecodeError: ‘ascii’ codec can’t decode byte ……

因为你没有指定defaultencoding,所以它其实在做这样的事情:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

Python

# -*- coding: utf-8 -*-

s = "人生苦短"

s.decode('ascii').encode('gbk')

1

2

3

# -*- coding: utf-8 -*-

s = "人生苦短"

s.decode('ascii').encode('gbk')

[Format Time: 0.0007 seconds]

 

设置defaultencoding

设置defaultencoding的代码如下:

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

Python

reload(sys)

sys.setdefaultencoding('utf-8')

1

2

reload(sys)

sys.setdefaultencoding('utf-8')

[Format Time: 0.0005 seconds]

如果你在python中进行编码和解码的时候,不指定编码方式,那么python就会使用defaultencoding。

比如上一节例子中将str编码为另一种格式,就会使用defaultencoding。

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

Python

s.encode("utf-8") 等价于 s.decode(defaultencoding).encode("utf-8")

1 s.encode("utf-8") 等价于 s.decode(defaultencoding).encode("utf-8")

[Format Time: 0.0008 seconds]

再比如你使用str创建unicode对象时,如果不说明这个str的编码格式,那么程序也会使用defaultencoding。

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

Python

u = unicode("人生苦短") 等价于 u = unicode("人生苦短",defaultencoding)

1 u = unicode("人生苦短") 等价于 u = unicode("人生苦短",defaultencoding)

[Format Time: 0.0008 seconds]

默认的defaultcoding:ascii是许多错误的原因,所以早早的设置defaultencoding是一个好习惯。

文件头声明编码的作用。

这要感谢这篇博客关于python文件头部分知识的讲解。

顶部的:# -*- coding: utf-8 -*-目前看来有三个作用。

  1. 如果代码中有中文注释,就需要此声明
  2. 比较高级的编辑器(比如我的emacs),会根据头部声明,将此作为代码文件的格式。
  3. 程序会通过头部声明,解码初始化 u”人生苦短”,这样的unicode对象,(所以头部声明和代码的存储格式要一致)

关于requests库

requests是一个很实用的Python HTTP客户端库,编写爬虫和测试服务器响应数据时经常会用到。

其中的Request对象在访问服务器后会返回一个Response对象,这个对象将返回的Http响应字节码保存到content属性中。

但是如果你访问另一个属性text时,会返回一个unicode对象,乱码问题就会常常发成在这里。

因为Response对象会通过另一个属性encoding来将字节码编码成unicode,而这个encoding属性居然是responses自己猜出来的。

官方文档:

text

Content of the response, in unicode.

If Response.encoding is None, encoding will be guessed using chardet.

The encoding of the response content is determined based solely on HTTP headers, following RFC 2616 to the letter. If you can take advantage of non-HTTP knowledge to make a better guess at the encoding, you should set r.encoding appropriately before accessing this property.

所以要么你直接使用content(字节码),要么记得把encoding设置正确,比如我获取了一段gbk编码的网页,就需要以下方法才能得到正确的unicode。

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

Python

import requests

url = "http://xxx.xxx.xxx"

response = requests.get(url)

response.encoding = 'gbk'

 

print response.text

1

2

3

4

5

6

import requests

url = "http://xxx.xxx.xxx"

response = requests.get(url)

response.encoding = 'gbk'

 

print response.text

[Format Time: 0.0011 seconds]

 

不仅仅要原理,更要使用方法!

如果是早期的我写博客,那么我一定会写这样的例子:

如果现在的文件编码为gbk,然后文件头为:# -*- coding: utf-8 -*-,再将默认编码设置为xxx,那么如下程序的结果会是……

这就类似于,当年学c的时候,用各种优先级,结合性,指针来展示自己水平的代码。

实际上这些根本就不实用,谁会在真正的工作中写这样的代码呢?我在这里想谈谈实用的处理中文的python方法。

基本设置

主动设置defaultencoding。(默认的是ascii)

代码文件的保存格式要与文件头部的# coding:xxx一致

如果是中文,程序内部尽量使用unicode,而不用str

关于打印

你在打印str的时候,实际就是直接将字节流发送给shell。如果你的字节流编码格式与shell的编码格式不相同,就会乱码。

而你在打印unicode的时候,系统自动将其编码为shell的编码格式,是不会出现乱码的。

程序内外要统一

如果说程序内部要保证只用unicode,那么在从外部读如字节流的时候,一定要将这些字节流转化为unicode,在后面的代码中去处理unicode,而不是str。

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

Python

with open("test") as f:

for i in f:

# 将读入的utf-8字节流进行解码

u = i.decode('utf-8')

....

1

2

3

4

5

with open("test") as f:

for i in f:

# 将读入的utf-8字节流进行解码

u = i.decode('utf-8')

....

[Format Time: 0.0012 seconds]

如果把连接程序内外的这段数据流比喻成通道的的话,那么与其将通道开为字节流,读入后进行解码,不如直接将通道开为unicode的。

Crayon Syntax Highlighter v2.7.1.1

 

 

 

 

 

 

Python

# 使用codecs直接开unicode通道

file = codecs.open("test", "r", "utf-8")

for i in file:

print type(i)

# i的类型是unicode的

1

2

3

4

5

# 使用codecs直接开unicode通道

file = codecs.open("test", "r", "utf-8")

for i in file:

print type(i)

# i的类型是unicode的

[Format Time: 0.0010 seconds]

所以python处理中文编码问题的关键是你要清晰的明白,自己在干什么,打算读入什么格式的编码,声明的的这些字节是什么格式的,str到unicode是如何转换的,str的一种编码到另一种编码又是如何进行的。 还有,你不能把问题变得混乱,要自己主动去维护一种统一。

发表在 python | 留下评论

Python中列表、字典、元组、集合数据结构整理

列表:

代码如下:

shoplist = ['apple', 'mango', 'carrot', 'banana']

字典:

代码如下:
di = {'a':123,'b':'something'}

集合:

代码如下:
jihe = {'apple','pear','apple'}

元组:

代码如下:
t = 123,456,'hello'

 

1.列表

空列表:a=[]

函数方法:

代码如下:
          a.append(3)       >>>[3]
a.extend([3,4,5])       >>>[3,3,4,5]    #添加一个列表序列
a.insert(1,'hello')        >>>[3,'hello',3,4,5]
a.remove(3)             >>>['hello',3,4,5] #删除第一个出现的3,没有3则报错
a.pop()              >>>['hello',3,4]
a.pop(0)              >>>[3,4]
a.index(4)          >>>1    #返回出现的第一个4的下标
a.count(3)          >>>1    #列表中元素3的个数
a.sort        >>>[3,4]    #排序
a.reverse()        >>>[4,3]    #反序

 

删除元素的方法:

代码如下:
        a.remove(3)    #通过值删除元素,删除第一个为参数值得元素
a.pop()       #通过下标删除元素,默认删除列表最后一个值,带参数则删除下标为参数值的元素
del a[0]       #通过下标删除元素,
del a[2:4] #删除a表下标为2,3的元素
del a[:]   #删除a列表所有元素
del a       #删除列表

 

列表推导式:

代码如下:
        vec = [2,4,6]
[3*x for x in vec if x<6]    >>>[6,12]    3*2,3*4
vec2 = [1,2,3]
[x*y for x in vec for y in vec2]    >>>[2,4,6,4,8,12,6,12,18]

 

嵌套列表推导式:

代码如下:
        mat = [
[1,2,3],
[4,5,6],
[7,8,9]
]
print ([[row[i] for row in mat] for i in [0,1,2]])
>>>[[1, 4, 7], [2, 5, 8], [3, 6, 9]]

 

思考:list (zip(mat)) 和 list (zip(*mat))结果会有什么不同

2.元组

空元组:t = ()
元组赋值: t = (123,345)
t[0]         >>>123
3.字典

代码如下:
    d = {'Jack':'jack@mail.com','Tom':'Tom@main.com'}
d['Jack']            >>>'jack@mail.com
d['Jim'] = 'Jim@sin.com'    >>>{'Jim': 'Jim@sin.com', 'Jack': 'jack@mail.com', 'Tom': 'Tom@main.com'}del d['Jim']    >>>{'Jack': 'jack@mail.com', 'Tom': 'Tom@main.com'}
list(d.keys())    #将返回一个字典中所有关键字组成的无序列表
sorted(d.keys()) #将返回一个字典中所有关键字组成的排序列表
dict()    #构造函数可以直接从key-value对中创建字典
dict([('Tim',123),('Tiny',234)])    >>>{'Tiny': 234, 'Tim': 123}

 

推导式创建字典:

代码如下:
        {d2:d2+'@main.com' for d2 in list(d.keys())}
>>>{'Jack': 'Jack@main.com', 'Tom': 'Tom@main.com'}

练习:循环输出字典中的键值对:

代码如下:
        for name,email in d.items():
print(name,email)

4.集合

 

空集合:A = set() ※想要创建空集合,必须使用set()

演示:

代码如下:
    basket = {'apple','orange','apple'}    >>>{'orange', 'apple'}    #注意重复的元素只显示一个
'apple' in basket              >>>True
'pear' in basket            >>>False

 

集合的数学运算:

代码如下:
        a = set('ababcdabca')        >>>{'c', 'b', 'a', 'd'}
b = {'a','b','m'}            >>>{'b', 'a', 'm'}
a - b        >>>{'c', 'd'}
b - a        >>>{'m'}
a | b        >>>{'c', 'd', 'b', 'a', 'm'}
a & b        >>>{'a','b'}
a ^ b        >>>{'c','d','m'}

 

集合推导式:

代码如下:
       {x for x in a if x not in 'ab'}    >>>{'c','d'}
发表在 python | 留下评论

PYTHON数据结构之列表、元组及字典

元组和列表的主要区别在于:

列表可以修改,元组则不能。

也就是说,如果根据需求来添加元素,那么列表可能会更加好用,而出于某些原因,序列不能修改的时候,使用元组则更加合适。

 创建元组的方法很简单,只需用逗号分隔开一些值,那么你就自动创建了元组。

列表

列表是处理一组有序项目的数据结构,即你可以在一个列表中存储一个 序列 的项目。

1.list函数

因为字符串不能像列表一样被修改,所以有时候根据字符串创建列表就很有用了,list可以实现这个操作:

1 >>>list("Hello")
2 ['H','e'.'l'.'l'.'o']
PS:可以用一下方法将一个由字符组成的列表转换为字符串:

1 ''.join(list)
list为要转换的列表名。

2.基本的列表操作

改变列表
1 >>>x = [1,1,1]
2 >>>x[1] = 2
3 >>>x
4 [1,2,1]
删除元素
>>>names = ['Void','Alice','Jack']
>>>del names[2]
>>>names
['Void','Jack']
分片赋值
>>>name = list('Perl')
>>>name
['P','e','r','l']
>>>name[2:] = list('ar')
>>>name
['P','e','a','r']
3.列表方法

append
append方法用于在列表末尾追加新的对象:

1 >>>list = [1,2,3]
2 >>>list.append(4)
3 >>>list
4 [1,2,3.4]
count
count用于统计某个元素在列表出现的次数:

1 >>>x = [[1,2],1,1]
2 >>>x.count(1)
3 2
extend
extend方法可以在列表的末尾一次性追加另一个序列中的多个值。换句话说,可以用新列表扩展原有的列表:

1 >>>a = [1,2,3]
2 >>>b = [4,5]
3 >>>a.extend(b)
4 >>>a
5 [1,2,3,4,5]
index
index方法用于从列表中找出某个值第一个匹配项的索引位置:

1 >>>a = ['Love','for','good']
2 >>>a.index('good')
3 3
insert
insert方法用于将对象插入到列表中:

1 >>>a = [1,2,3]
2 >>>a.insert(1,'four')
3 >>>a
4 [1,'four',2,3]
pop
pop方法会移除列表中的一个元素(默认是最后一个),并且返回该元素的值:

1 >>>x = [1,2,3]
2 >>>x.pop()
3 3
4 >>>x
5 [1,2]
remove
remove用于移除列表中某个值的第一个匹配项:

1 >>>x = ['to','be','or','not','to','be']
2 >>>x.remove('be')
3 >>>x
4 ['to','or','not','to','be']
reverse
reverse方法将列表中的元素反向存放:

1 >>>x = [1,2.3]
2 >>>x.reverse()
3 >>>x
4 [3,2,1]
sort
sort方法用于在原位置对列表进行排序:

1 >>>x = [4,6,2,1,7,9]
2 >>>x.sort()
3 >>>x
4 [1,2,4,6,7,9]

元组

元组和列表的主要区别在于:列表可以修改,元组则不能。也就是说,如果根据需求来添加元素,那么列表可能会更加好用,而出于某些原因,序列不能修改的时候,使用元组则更加合适。

创建元组的方法很简单,只需用逗号分隔开一些值,那么你就自动创建了元组。

1 >>>1,2,3
2 (1,2,3)
PS:那么你想知道如何实现一个值的元组么?你可以猜一下,方法很奇特-必须有一个逗号,哪怕只有一个值:

>>>43,
(43)
>>>43
43
1.tuple函数

tuple函数的功能与list基本上是一样的:以一个序列作为参数并把它转换为元组

>>>tuple([1,2,3])
(1,2,3)
2.基本元组操作

元组的操作其实并不复杂,除了创建和访问之外也没有太多的其他操作。

字典

列表这种数据结构适合于将值组织到一个结构中,并且通过编号对其进行引用。而字典这种数据结构是通过名字来引用值的数据结构,这种数据结构称为映射(mapping)。字典是Python中唯一内建的映射类型,字典中的值并没有特殊的顺序,但是都存储在一个特定的键下,键可以是数字、字符串、甚至元组。

1.创建和使用字典

1 >>>phone = {'Void':'123','Allen':'321'}
2 >>>phone['Void']
3 '123'
2.基本字典操作

len(d)返回d中的项(键-值对)的数量
d[k]返回关联到键k上的值
d[k]=v将值v关联到键k上
del d[k]删除键为k的项
k in d 检查d中是否含有键为k的项

发表在 python | 留下评论

Django + nginx + uwsgi 部署

原理

the web client <-> the web server <-> the socket <-> uwsgi <-> Django

 

http://uwsgi-docs.readthedocs.io/en/latest/tutorials/Django_and_nginx.html

发表在 django | 留下评论

vagrant中css,img不生效的问题 | Vagrant下共享目录静态文件(js/jpg/png等)“缓存”问题

https://blog.smdcn.net/article/1325.html

用vagrant搭建了一个共享开发平台,修改css文件后,页面看不到效果,通过查看源文件地址,发现新修改的Css文件并没有加载进来,加载的还是旧的文件地址。一开始以为是浏览器有缓存,清空了各种浏览器缓存后,依然还是不生效。后来,将css文件重命名,第一次加载就可以,再修改,查看源文件后,又没有加载,也尝试在css?version=1.0,加入一个版本号,结果依然不行,各种虐,各种郁闷,各种查找原因,最后发现是nginx的缓存问题;

修改nginx.conf

http {
log_format main ‘$remote_addr - $remote_user [$time_local] "$request" ‘
‘$status $body_bytes_sent "$http_referer" ‘
‘"$http_user_agent" "$http_x_forwarded_for"‘;

access_log /var/log/nginx/access.log main;

sendfile off;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;

 

}

将以上的   sendfile on  修改成  sendfile off ,重启nginx,问题解决。

发表在 nginx, vagrant, 杂项, 架构 | 留下评论

openresty 协程

Lua中没有线程和异步编程编程的概念,对于并发执行提供了协程的概念,个人认为协程是在A运行中发现自己忙则把CPU使用权让出来给B使用,最后A能从中断位置继续执行,本地还是单线程,CPU独占的;因此如果写网络程序需要配合非阻塞I/O来实现。

 

ngx_lua 模块对协程做了封装,我们可以直接调用ngx.thread API使用,虽然称其为“轻量级线程”,但其本质还是Lua协程。该API必须配合该ngx_lua模块提供的非阻塞I/O API一起使用,比如我们之前使用的ngx.location.capture_multi和lua-resty-redis、lua-resty-mysql等基于cosocket实现的都是支持的。

 

通过Lua协程我们可以并发的调用多个接口,然后谁先执行成功谁先返回,类似于BigPipe模型。

 

1、依赖的API

Java代码
  1. location /api1 {
  2.     echo_sleep 3;
  3.     echo api1 : $arg_a;
  4. }
  5. location /api2 {
  6.     echo_sleep 3;
  7.     echo api2 : $arg_a;
  8. }

我们使用echo_sleep等待3秒。

 

2、串行实现

Java代码
  1. location /serial {
  2.     content_by_lua '
  3.         local t1 = ngx.now()
  4.         local res1 = ngx.location.capture("/api1", {args = ngx.req.get_uri_args()})
  5.         local res2 = ngx.location.capture("/api2", {args = ngx.req.get_uri_args()})
  6.         local t2 = ngx.now()
  7.         ngx.print(res1.body, "<br/>", res2.body, "<br/>", tostring(t2-t1))
  8.     ';
  9. }

即一个个的调用,总的执行时间在6秒以上,比如访问http://192.168.1.2/serial?a=22

Java代码
  1. api1 : 22
  2. api2 : 22
  3. 6.0040001869202

 

3、ngx.location.capture_multi实现

Java代码
  1. location /concurrency1 {
  2.     content_by_lua '
  3.         local t1 = ngx.now()
  4.         local res1,res2 = ngx.location.capture_multi({
  5.               {"/api1", {args = ngx.req.get_uri_args()}},
  6.               {"/api2", {args = ngx.req.get_uri_args()}}
  7.         })
  8.         local t2 = ngx.now()
  9.         ngx.print(res1.body, "<br/>", res2.body, "<br/>", tostring(t2-t1))
  10.     ';
  11. }

直接使用ngx.location.capture_multi来实现,比如访问http://192.168.1.2/concurrency1?a=22

Java代码
  1. api1 : 22
  2. api2 : 22
  3. 3.0020000934601

4、协程API实现 

Java代码
  1. location /concurrency2 {
  2.     content_by_lua '
  3.         local t1 = ngx.now()
  4.         local function capture(uri, args)
  5.            return ngx.location.capture(uri, args)
  6.         end
  7.         local thread1 = ngx.thread.spawn(capture, "/api1", {args = ngx.req.get_uri_args()})
  8.         local thread2 = ngx.thread.spawn(capture, "/api2", {args = ngx.req.get_uri_args()})
  9.         local ok1, res1 = ngx.thread.wait(thread1)
  10.         local ok2, res2 = ngx.thread.wait(thread2)
  11.         local t2 = ngx.now()
  12.         ngx.print(res1.body, "<br/>", res2.body, "<br/>", tostring(t2-t1))
  13.     ';
  14. }

使用ngx.thread.spawn创建一个轻量级线程,然后使用ngx.thread.wait等待该线程的执行成功。比如访问http://192.168.1.2/concurrency2?a=22

Java代码
  1. api1 : 22
  2. api2 : 22
  3. 3.0030000209808

其有点类似于Java中的线程池执行模型,但不同于线程池,其每次只执行一个函数,遇到IO等待则让出CPU让下一个执行。我们可以通过下面的方式实现任意一个成功即返回,之前的是等待所有执行成功才返回。

Java代码
  1. local  ok, res = ngx.thread.wait(thread1, thread2)

FROM: http://jinnianshilongnian.iteye.com/blog/2190343

发表在 lua, openresty | 留下评论

第八章 流量复制/AB测试/协程

FROM: http://jinnianshilongnian.iteye.com/blog/2190343

流量复制

在实际开发中经常涉及到项目的升级,而该升级不能简单的上线就完事了,需要验证该升级是否兼容老的上线,因此可能需要并行运行两个项目一段时间进行数据比对和校验,待没问题后再进行上线。这其实就需要进行流量复制,把流量复制到其他服务器上,一种方式是使用如tcpcopy引流;另外我们还可以使用nginx的HttpLuaModule模块中的ngx.location.capture_multi进行并发执行来模拟复制。

 

构造两个服务

Java代码
  1. location /test1 {
  2.     keepalive_timeout 60s;
  3.     keepalive_requests 1000;
  4.     content_by_lua '
  5.         ngx.print("test1 : ", ngx.req.get_uri_args()["a"])
  6.         ngx.log(ngx.ERR, "request test1")
  7.     ';
  8. }
  9. location /test2 {
  10.     keepalive_timeout 60s;
  11.     keepalive_requests 1000;
  12.     content_by_lua '
  13.         ngx.print("test2 : ", ngx.req.get_uri_args()["a"])
  14.         ngx.log(ngx.ERR, "request test2")
  15.     ';
  16. }

通过ngx.location.capture_multi调用

Java代码
  1. location /test {
  2.      lua_socket_connect_timeout 3s;
  3.      lua_socket_send_timeout 3s;
  4.      lua_socket_read_timeout 3s;
  5.      lua_socket_pool_size 100;
  6.      lua_socket_keepalive_timeout 60s;
  7.      lua_socket_buffer_size 8k;
  8.      content_by_lua '
  9.          local res1, res2 = ngx.location.capture_multi{
  10.                { "/test1", { args = ngx.req.get_uri_args() } },
  11.                { "/test2", { args = ngx.req.get_uri_args()} },
  12.          }
  13.          if res1.status == ngx.HTTP_OK then
  14.              ngx.print(res1.body)
  15.          end
  16.          if res2.status ~= ngx.HTTP_OK then
  17.             --记录错误
  18.          end
  19.      ';
  20. }

此处可以根据需求设置相应的超时时间和长连接连接池等;ngx.location.capture底层通过cosocket实现,而其支持Lua中的协程,通过它可以以同步的方式写非阻塞的代码实现。

 

此处要考虑记录失败的情况,对失败的数据进行重放还是放弃根据自己业务做处理。

 

AB测试

AB测试即多版本测试,有时候我们开发了新版本需要灰度测试,即让一部分人看到新版,一部分人看到老版,然后通过访问数据决定是否切换到新版。比如可以通过根据区域、用户等信息进行切版本。

 

比如京东商城有一个cookie叫做__jda,该cookie是在用户访问网站时种下的,因此我们可以拿到这个cookie,根据这个cookie进行版本选择。

 

比如两次清空cookie访问发现第二个数字串是变化的,即我们可以根据第二个数字串进行判断。

__jda=122270672.1059377902.1425691107.1425691107.1425699059.1

__jda=122270672.556927616.1425699216.1425699216.1425699216.1。

 

判断规则可以比较多的选择,比如通过尾号;要切30%的流量到新版,可以通过选择尾号为1,3,5的切到新版,其余的还停留在老版。

 

1、使用map选择版本

Java代码
  1. map $cookie___jda $ab_key {
  2.     default                                       "0";
  3.     ~^\d+\.\d+(?P<k>(1|3|5))\.                    "1";
  4. }

使用map映射规则,即如果是到新版则等于"1",到老版等于“0”; 然后我们就可以通过ngx.var.ab_key获取到该数据。

Java代码
  1. location /abtest1 {
  2.     if ($ab_key = "1") {
  3.         echo_location /test1 ngx.var.args;
  4.     }
  5.     if ($ab_key = "0") {
  6.         echo_location /test2 ngx.var.args;
  7.     }
  8. }

此处也可以使用proxy_pass到不同版本的服务器上

Java代码
  1. location /abtest2 {
  2.     if ($ab_key = "1") {
  3.         rewrite ^ /test1 break;
  4.         proxy_pass http://backend1;
  5.     }
  6.     rewrite ^ /test2 break;
  7.     proxy_pass http://backend2;
  8. }

 

2、直接在Lua中使用lua-resty-cookie获取该Cookie进行解析

首先下载lua-resty-cookie

Java代码
  1. cd /usr/example/lualib/resty/
  2. wget https://raw.githubusercontent.com/cloudflare/lua-resty-cookie/master/lib/resty/cookie.lua

 

Java代码
  1. location /abtest3 {
  2.     content_by_lua '
  3.          local ck = require("resty.cookie")
  4.          local cookie = ck:new()
  5.          local ab_key = "0"
  6.          local jda = cookie:get("__jda")
  7.          if jda then
  8.              local v = ngx.re.match(jda, [[^\d+\.\d+(1|3|5)\.]])
  9.              if v then
  10.                 ab_key = "1"
  11.              end
  12.          end
  13.          if ab_key == "1" then
  14.              ngx.exec("/test1", ngx.var.args)
  15.          else
  16.              ngx.print(ngx.location.capture("/test2", {args = ngx.req.get_uri_args()}).body)
  17.          end
  18.     ';
  19. }

首先使用lua-resty-cookie获取cookie,然后使用ngx.re.match进行规则的匹配,最后使用ngx.exec或者ngx.location.capture进行处理。此处同时使用ngx.exec和ngx.location.capture目的是为了演示,此外没有对ngx.location.capture进行异常处理。

 

协程

Lua中没有线程和异步编程编程的概念,对于并发执行提供了协程的概念,个人认为协程是在A运行中发现自己忙则把CPU使用权让出来给B使用,最后A能从中断位置继续执行,本地还是单线程,CPU独占的;因此如果写网络程序需要配合非阻塞I/O来实现。

 

ngx_lua 模块对协程做了封装,我们可以直接调用ngx.thread API使用,虽然称其为“轻量级线程”,但其本质还是Lua协程。该API必须配合该ngx_lua模块提供的非阻塞I/O API一起使用,比如我们之前使用的ngx.location.capture_multi和lua-resty-redis、lua-resty-mysql等基于cosocket实现的都是支持的。

 

通过Lua协程我们可以并发的调用多个接口,然后谁先执行成功谁先返回,类似于BigPipe模型。

 

1、依赖的API

Java代码
  1. location /api1 {
  2.     echo_sleep 3;
  3.     echo api1 : $arg_a;
  4. }
  5. location /api2 {
  6.     echo_sleep 3;
  7.     echo api2 : $arg_a;
  8. }

我们使用echo_sleep等待3秒。

 

2、串行实现

Java代码
  1. location /serial {
  2.     content_by_lua '
  3.         local t1 = ngx.now()
  4.         local res1 = ngx.location.capture("/api1", {args = ngx.req.get_uri_args()})
  5.         local res2 = ngx.location.capture("/api2", {args = ngx.req.get_uri_args()})
  6.         local t2 = ngx.now()
  7.         ngx.print(res1.body, "<br/>", res2.body, "<br/>", tostring(t2-t1))
  8.     ';
  9. }

即一个个的调用,总的执行时间在6秒以上,比如访问http://192.168.1.2/serial?a=22

Java代码
  1. api1 : 22
  2. api2 : 22
  3. 6.0040001869202

 

3、ngx.location.capture_multi实现

Java代码
  1. location /concurrency1 {
  2.     content_by_lua '
  3.         local t1 = ngx.now()
  4.         local res1,res2 = ngx.location.capture_multi({
  5.               {"/api1", {args = ngx.req.get_uri_args()}},
  6.               {"/api2", {args = ngx.req.get_uri_args()}}
  7.         })
  8.         local t2 = ngx.now()
  9.         ngx.print(res1.body, "<br/>", res2.body, "<br/>", tostring(t2-t1))
  10.     ';
  11. }

直接使用ngx.location.capture_multi来实现,比如访问http://192.168.1.2/concurrency1?a=22

Java代码
  1. api1 : 22
  2. api2 : 22
  3. 3.0020000934601

4、协程API实现 

Java代码
  1. location /concurrency2 {
  2.     content_by_lua '
  3.         local t1 = ngx.now()
  4.         local function capture(uri, args)
  5.            return ngx.location.capture(uri, args)
  6.         end
  7.         local thread1 = ngx.thread.spawn(capture, "/api1", {args = ngx.req.get_uri_args()})
  8.         local thread2 = ngx.thread.spawn(capture, "/api2", {args = ngx.req.get_uri_args()})
  9.         local ok1, res1 = ngx.thread.wait(thread1)
  10.         local ok2, res2 = ngx.thread.wait(thread2)
  11.         local t2 = ngx.now()
  12.         ngx.print(res1.body, "<br/>", res2.body, "<br/>", tostring(t2-t1))
  13.     ';
  14. }

使用ngx.thread.spawn创建一个轻量级线程,然后使用ngx.thread.wait等待该线程的执行成功。比如访问http://192.168.1.2/concurrency2?a=22

Java代码
  1. api1 : 22
  2. api2 : 22
  3. 3.0030000209808

其有点类似于Java中的线程池执行模型,但不同于线程池,其每次只执行一个函数,遇到IO等待则让出CPU让下一个执行。我们可以通过下面的方式实现任意一个成功即返回,之前的是等待所有执行成功才返回。

Java代码
  1. local  ok, res = ngx.thread.wait(thread1, thread2)

 

Lua协程参考资料

《Programming in Lua》

http://timyang.net/lua/lua-coroutine-vs-java-wait-notify/

https://github.com/andycai/luaprimer/blob/master/05.md

http://my.oschina.net/wangxuanyihaha/blog/186401

http://manual.luaer.cn/2.11.html

发表在 lua, openresty | 留下评论

lua pairs和ipairs区别

在对lua tbl做循环处理的时候,经常会用的pairs和ipairs,两者的具体区别mark下

ipairs:

for i,v in ipairs(t) do body end
will iterate over the pairs (1,t[1]), (2,t[2]), ···, up to the first integer key absent from the table.
即 ipairs 是从t[1]开始,不断获取t[2],t[3]....直到第一个非连续整形key出现即停止。

local aa = {
[2] = "111",
[3] = "333",
[4] = "555",
}
for k,v in ipairs(aa) do
print (k,v)
end

这个执行结果是

>lua -e "io.stdout:setvbuf 'no'" "test.lua"
>Exit code: 0

因为aa[1] 为 nil直接结束循环

local aa = {
[1] = "111",
[3] = "333",
[4] = "555",
}
for k,v in ipairs(aa) do
print (k,v)
end

这个返回的结果为

>lua -e "io.stdout:setvbuf 'no'" "test.lua"
1 111
>Exit code: 0

这个aa[1]存在,aa[2]不存在即结束了循环

而对于pairs就简单多了

for k,v in pairs(t) do body end
will iterate over all key–value pairs of table t.
即 pairs(t) 返回所有的 key-value对,示例:

do
local aa = {
[1] = "111",
[3] = "333",
[4] = "555",
}
for k,v in pairs(aa) do
print (k,v)
end
end

结果如下:

>lua -e "io.stdout:setvbuf 'no'" "test.lua"
1 111
4 555
3 333
>Exit code: 0

所以,一般情况下,还是用pairs比较好些!

发表在 lua, openresty | 留下评论

luajit: 什么是 JIT ?

https://moonbingbing.gitbooks.io/openresty-best-practices/content/lua/what_jit.html

尽量使用支持可以被 JIT 编译的 API。到底有哪些 API 是可以被 JIT 编译呢?我们的参考来来源是哪里呢?LuaJIT 的官方地址:http://wiki.luajit.org/NYI

https://groups.google.com/forum/#!topic/openresty/o67EjkImXyE

发表在 lua, openresty | 留下评论

lua table.new

FROM :https://groups.google.com/forum/#!topic/openresty/dxbBP84-hr0

table.new(narray, nhash) 两个参数分别代表table里是array还是hash的
table.new(10, 0) 或者 table.new(0, 10) 这样的,后者是 hash 性质的 table

Lua table 可以同时拥有数组部分和哈希部分。在物理上,数组部分和哈希部分也是在 table 内部分开存储的。比如
table.new(100, 200) 也是完全合法的。

table.new(narr, nrec) 接口是标准 Lua C API 函数 lua_createtable() 的 Lua 版本:

http://www.lua.org/manual/5.1/manual.html#lua_createtable

另外,不同于使用了 lua_createtable() 的 lua_CFunction,table.new() 是可以被 JIT 编译的。

table.new() 一般用于给 lua table 预分配空间。否则 table
会在插入新元素时自增长,而自增长是高代价操作(因为需要重新分配空间、重新 hash,以及拷贝数据)。 

发表在 lua, openresty | 留下评论

Apache Cassandra架构理解

基本流程: 点对点分布式系统,集群中各节点平等,数据分布于集群中各节点,各节点间每秒交换一次信息。
每个节点的commit log提交日志捕获写操作来确保数据持久性。
数据先被写入MemTable(内存中的数据结构),待MemTable满后数据被写入SSTable(硬盘的数据文件)。
所有的写内容被自动在集群中partition分区replicate复制

 

库表结构: Cassandra数据库面向行。用户可连接至集群的任意节点,通过类似SQL的CQL查询数据。
集群中,一个应用一般包含一个keyspace,一个keyspace中包含多个表。

 

读写请求: 客户端连接到某一节点发起读或写请求时,该节点充当客户端应用拥有相应数据的节点间的
coordinator协调者以根据集群配置确定环(ring)中的哪个节点应当获取这个请求。

CQL是客户端,Driver也是一种客户端. 使用CQL连接指定的-h节点就是协调节点.

图解: 协调节点(10)负责和客户端的交互.真正的数据在节点1,4,6上,分别表示数据的三个副本,最终只要节点1上的数据返回即可.


关键词


节点间通信gossip

Cassandra使用点对点通讯协议gossip在集群中的节点间交换位置和状态信息。
gossip进程每秒运行一次,与至多3个其他节点交换信息,这样所有节点可很快了解集群中的其他节点信息

 

gossip协议的具体表现形式就是配置文件中的seeds种子节点. 一个注意点是同一个集群的所有节点的种子节点应该一致.
否则如果种子节点不一致, 有时候会出现集群分裂, 即会出现两个集群. 一般先启动种子节点,尽早发现集群中的其他节点.

每个节点都和其他节点交换信息, 由于随机和概率,一定会穷举出集群的所有节点.同时每个节点都会保存集群中的所有其他节点.
这样随便连到哪一个节点,都能知道集群中的所有其他节点. 比如cql随便连接集群的一个节点,都能获取集群所有节点的状态.
也就是说任何一个节点关于集群中的节点信息的状态都应该是一致的!


失败检测与恢复

  • ●gossip可检测其他节点是否正常以避免将请求路由至不可达或者性能差的节点(后者需配置为dynamic snitch方可)。
  •  
  • ●可通过配置phi_convict_threshold来调整失败检测的敏感度。
  •  
  • ●对于失败的节点,其他节点会通过gossip定期与之联系以查看是否恢复而非简单将之移除。若需强制添加或移除集群中节点需使用nodetool工具。
  •  
  • ●一旦某节点被标记为失败,其错过的写操作会有其他replicas存储一段时间. (需开启hinted handoff,若节点失败的时间超过了max_hint_window_in_ms,错过的写不再被存储). Down掉的节点经过一段时间恢复后需执行repair操作,一般在所有节点运行nodetool repair以确保数据一致。
  •  

dynamic snitch特性: 查询请求路由到某个节点,如果这个节点当掉或者响应慢,则应该能够查询其他节点上的副本
删除节点: 节点失败后,仍然在集群中,通过removenode可以将节点从集群中下线.区别就是status如果不存在就说明下线了. DN则仍然在集群中.
失败节点数据: 数据无法正常存储到失败的节点,所以会由其他节点暂时保存,等它恢复之后,再将错过的写补充上去.


一致性哈希DHT

 

图解: key的范围是0到2^32形成一个环, 叫做hash空间环, 即hash的值空间. 对集群的服务器(比如ip地址)进行hash,都能确定其在环空间上的位置.
定位数据访问到相应服务器的算法:将数据key使用相同的函数H计算出哈希值h,通根据h确定此数据在环上的位置,
从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器

图解: 由于一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题,所以引入了虚拟节点:
把每台server分成v个虚拟节点,再把所有虚拟节点(n*v)随机分配到一致性哈希的圆环上,
这样所有的用户从自己圆环上的位置顺时针往下取到第一个vnode就是自己所属节点. 当此节点存在故障时,再顺时针取下一个作为替代节点.

图解: key经过hash会定位到hash环上的一个位置, 找到下一个vnode为数据的第一份存储节点. 接下来的两个vnode为另外两个副本.


hash值空间&token

上面在计算key存在在哪个节点上是使用往前游走的方式找到环上的第一个节点. 游走是一个计算的过程.
如果能够事先计算好集群中的节点(vnodes)在整个hash环的值空间, 这样对key进行hash后,可以看它是落在哪个hash值空间上,
而值空间和节点的关系已经知道了,所以可以直接定位到key落在哪个节点上了. 这就是token的作用.

 

C表中每行数据由primary key标识,C为每个primarykey分配一个hash值,集群中每个节点(vnode)拥有一个或多个hash值区间
这样便可根据primary key对应的hash值该条数据放在包含该hash值hash值区间对应的节点(vnode)中


虚拟节点

 

图解: 没有使用虚拟节点, Ring环的tokens数量=集群的机器数量. 比如上面一共有6个节点,所以token数=6.
因为副本因子=3,一条记录要在集群中的三个节点存在. 简单地方式是计算rowkey的hash值,落在环中的哪个token上,
第一份数据就在那个节点上, 剩余两个副本落在这个节点在token环上的后两个节点.
图中的A,B,C,D,E,F是key的范围,真实的值是hash环空间,比如0~2^32区间分成10份.每一段是2^32的1/10.
节点1包含A,F,E表示key范围在A,F,E的数据会存储到节点1上.以此类推.

 

若不使用虚拟节点则需手工为集群中每个节点计算和分配一个token
每个token决定了节点在环中的位置以及节点应当承担的一段连续的数据hash值的范围
如上图上半部分,每个节点分配了一个单独的token代表环中的一个位置,每个节点存储将row key映射为hash值之后
落在该节点应当承担的唯一的一段连续的hash值范围内的数据。每个节点也包含来自其他节点的row的副本。

 

而使用虚拟节点允许每个节点拥有多个较小的不连续的hash值范围。
如上图中下半部分,集群中的节点使用了虚拟节点,虚拟节点随机选择且不连续。
数据的存放位置也由row key映射而得的hash值确定,但是是落在更小的分区范围内

使用虚拟节点的好处

  • ●无需为每个节点计算、分配token
  • ●添加移除节点后无需重新平衡集群负载
  • ●重建死掉的节点更快
  • ●改善了在同一集群使用异种机器


数据复制

当前有两种可用的复制策略:

  • ●SimpleStrategy:仅用于单数据中心,将第一个replica放在由partitioner确定的节点中,其余的replicas放在上述节点顺时针方向的后续节点中。
  • ●NetworkTopologyStrategy:可用于较复杂的多数据中心。可以指定在每个数据中心分别存储多少份replicas。

 

复制策略在创建keyspace时指定,如

CREATE KEYSPACE Excelsior WITH REPLICATION = { 'class' : 'SimpleStrategy','replication_factor' : 3 };

CREATE KEYSPACE Excalibur WITH REPLICATION = {'class' :'NetworkTopologyStrategy', 'dc1' : 3, 'dc2' : 2};

 

其中dc1、dc2这些数据中心名称要与snitch中配置的名称一致.上面的拓扑策略表示在dc1配置3个副本,在dc2配置2个副本


Partitioners

在Cassandra中,table的每行由唯一的primarykey标识,partitioner实际上为一hash函数用以计算primary key的token。Cassandra依据这个token值在集群中放置对应的行
注意:若使用虚拟节点(vnodes)则无需手工计算tokens。若不使用虚拟节点则必须手工计算tokens将所得的值指派给cassandra.ymal主配置文件中的initial_token参数


客户端请求


client连接至节点并发出read/write请求时,该节点充当client端应用与包含请求数据的节点(或replica)之间的协调者,
它利用配置的partitioner和replicaplacement策略确定那个节点当获取请求。


写请求

  • ●协调者(coordinator)将write请求发送到拥有对应row的所有replica节点,只要节点可用便获取并执行写请求。
  •  
  • ●写一致性级别(write consistency level)确定要有多少个replica节点必须返回成功的确认信息。成功意味着数据被正确写入了commit log和memtable。
  •  
  • ●上例为单数据中心,11个节点(不是有12个吗?),复制因子为3,写一致性等级为ONE的写情况(红色的箭头表示只要一个节点成功写入,便可立即返回给客户端)
  •  

写请求是如何保证一致性的?

读请求

  • [1] 直接读请求(Direct Read)
  • [2] 后台读修复请求(RR:Read Repair)

与直接读请求[1]联系的replica数目由一致性级别确定(上图中请求了R1和R3两个节点).
后台读修复请求[2]被发送到没有收到直接读请求的replica(R2),以确保请求的row在所有replica上一致.

  • ●协调者首先与一致性级别确定的所有replica联系,被联系的节点返回请求的数据,
  • ●若多个节点被联系,则来自各replica的row会在内存中作比较,若不一致,则协调者使用含最新数据的replica向client返回结果。
  • ●协调者在后台联系和比较来自其余拥有对应row的replica的数据,若不一致,会向过时的replica发写请求用最新的数据进行更新:read repair。
  • ●上例为单数据中心,11个节点,复制因子为3,一致性级别为QUORUM(3/2+1=2)的读情况(请求了两个节点,但最终返回给客户端的是最新的数据)
  •  

读请求是如何保证数据一致性的? 直接读请求将查询请求发送到了2个副本所在的节点(1,7). 因为有两个副本,所以会比较这两个副本哪个是最新的.
比较操作是在协调节点,还是在各个副本节点? 当然应该是在协调节点上,因为在各个副本节点上是没办法知道其他节点的副本的.
那么比较操作是不是把这两个副本的数据都传送到协调节点. 不是的,只需要传递时间撮就可以,因为要比较的只是哪个副本数据是最新的.
怎么判断两个副本的数据不一致? 实际上是使用md5判断值不一样,说明两个副本的数据是不一样的.
因为没有必要在比较的时候就把两个副本的全部查询结果都传送给协调节点,所以在确定哪个是最新的后,那个副本需要把查询结果传送给协调节点
再由协调节点将数据返回给客户端. 即图中红色的部分为结果数据的流程. 而黑色的往返箭头只是传递时间撮用来比较哪个是最新数据.


协调节点

问题:客户端连接的那个节点是怎么指定的? 是addContactPoint指定的节点吗? 但是ContactPoint一般设置为种子节点中的一个.

 

如果是CQL客户端连接C集群,则CQL连接的那个节点就是协调节点.
但是如果使用Driver API. 指定的ContactPoint并不是协调节点!

 

读写流程


写流程

图解: 上图表示写请求分别到MemTable和CommitLog, 并且MemTable的数据会刷写到磁盘上. 除了写数据,还有索引也会保存到磁盘上.

 

先将数据写进内存中的数据结构memtable,同时追加到磁盘中的commitlog中。
memtable内容超出指定容量后会被放进将被刷入磁盘的队列(memtable_flush_queue_size配置队列长度)。
若将被刷入磁盘的数据超出了队列长度,C会锁定写,并将内存数据刷进磁盘中的SSTable,之后commit log被清空。


读流程

首先检查BloomFilter①,每个SSTable都有一个Bloomfilter,用以在任何磁盘IO前检查请求PK对应的数据在SSTable中是否存在
BF可能误判不会漏判:判断存在,但实际上可能不存在, 判断不存在,则一定不存在,则流程不会访问这个SSTable(红色).
若数据很可能存在②,则检查PartitionKey cache(索引的缓存),之后根据索引条目是否在cache中找到而执行不同步骤:


在索引缓存中找到

  • ●③从compression offset map中查找拥有对应数据的压缩快。
  • ●④从磁盘取出压缩的数据,返回结果集。


没有在索引缓存中

  • ●⑤搜索Partition summary(partition index的样本集)确定index条目在磁盘中的近似位置。
  • ●⑥从磁盘中SSTable内取出index条目。
  • ●⑦从compression offset map中查找拥有对应数据的压缩快。
  • 从磁盘取出压缩的数据,返回结果集。


示例

第一个SSTable文件是insert(左), 第二个SSTable是update的数据(右).

由insert/update过程可知,read请求到达某一节点后,必须结合所有包含请求的row中的column的SSTable以及memtable来产生请求的数据

例如,要更新包含用户数据的某个row中的email 列,cassandra并不重写整个row到新的数据文件,
而仅仅将新的email写进新的数据文件,username等仍处于旧的数据文件中。
上图中红线表示Cassandra需要整合的row的片段用以产生用户请求的结果。
为节省CPU和磁盘I/O,Cassandra会缓存合并后的结果,且可直接在该cache中更新row而不用重新合并

 

参考文档


一致性哈希算法及其在分布式系统中的应用: http://blog.codinglabs.org/articles/consistent-hashing.html

Riak Clusters: http://docs.basho.com/riak/latest/theory/concepts/Clusters/

http://docs.datastax.com/en/cassandra/2.0/cassandra/architecture/architectureTOC.html

http://yikebocai.com/2014/06/cassandra-principle/

Cassandra研究报告: http://blog.csdn.net/zyz511919766/article/details/38683219
Cassandra 分布式数据库详解,第 1 部分:配置、启动与集群 https://www.ibm.com/developerworks/cn/opensource/os-cn-cassandraxu1/
Cassandra 分布式数据库详解,第 2 部分:数据结构与数据读写 https://www.ibm.com/developerworks/cn/opensource/os-cn-cassandraxu2/

 

 

来源:zqhxuyuan.github.io

原文:http://zqhxuyuan.github.io/2015/08/25/2015-08-25-Cassandra-Architecture/#

发表在 cassandra | 留下评论

ngx_lua获取、设置cookie

获取所有cookie

ngx.var.http_cookie 
如:nickname=user-1465294791; token=22413d5a1187e48af9ddf19b0f800c6e; uid=1465294791

设置cookie

ngx.header['Set-Cookie'] = {"visit_version=123;expires=Wed, 27-Mar-2014 16:39:44 GMT"};

获取cookie: ngx.var.cookie_cookieName

ngx.var.cookie_visit_version
发表在 lua, openresty | 留下评论

用Nginx+Lua实现高性能、高可靠、安全的登陆验证

FROM: http://blog.csdn.net/langeldep/article/details/8629906

对于一个中型或大型网站,有n个子项目在不同的服务器甚至不同的IDC部署和运行,SSO(单点登录)和无SESSION已经是必备的功能。在这种情况下用户登陆后的身份验证就会是一个问题。一种简单的解决办法就是登陆时将用户的身份写入cookie,为了安全再写入一个cookie的校验,防止cookie篡改。

1 <?php
2 $secretkey = '1234567890abcdefghi';
3 // ……   取出数据到 $data 的代码略去
4 if( md5( sha1( $password ) ) == $data['passowrd'] ) { // 登陆成功
5     $_COOKIE['uid'] = 1234;
6     $_COOKIE['nickname'] = 'soga';
7     $_COOKIE['token'] = md5( "uid:1234&nickname:soga&secretkey:1234567890abcdefghi" );
8 }
9 ?>

验证登陆合法性:

01 <?php
02 $secretkey = '1234567890abcdefghi';
03
04 if( $_COOKIE['token'] == md5( "uid:{$_COOKIE['uid']}&nickname:{$_COOKIE['nickname']}&secretkey:{$secretkey}" ) ) {
05     $login = true;
06 }
07 else {
08     $login = false;
09 }
10 ?>

这样实现有一些问题存在,密钥$secretkey会暴露在所有的程序内,存在安全隐患。所有产品的程序中都需要内置token校验的算法。

如果能在webserver层进行校验,直接告诉应用层校验结果,就可以避免上面的问题。找一种webserver上安全、稳定、高性能的实现,并且开发成本低的方案。我选择的是Nginx + ngx_lua。Nginx的稳定性、高性能搭配Lua的高性能、低成本开发,简直绝配。

Nginx+ngx_lua的编译安装就不在这里讲了。

Nginx 配置:

1 access_by_lua_file '/dir/test.lua';

test.lua 代码:

01 local secretkey='1234567890abcdefghi'
02 if ngx.var.cookie_uid == nil or ngx.var.cookie_nickname == nil or ngx.var.cookie_token ==nil then
03 ngx.req.set_header("Check-Login", "NULL")
04 return
05 end
06
07 local ctoken = ngx.md5('uid:' .. ngx.var.cookie_uid .. '&nickname:' ..ngx.var.cookie_nickname .. '&secretkey:' .. secretkey)
08
09 if ctoken == ngx.var.cookie_token then
10 ngx.req.set_header("Check-Login", "Yes")
11 else
12 ngx.req.set_header("Check-Login", "No")
13 end
14
15 return

然后就可以测试一下了:

无cookie登陆测试

01 [root@localhost soft]# curl -v "http://yuenshui.com:88/test.php"
02 * About to connect() to yuenshui.com port 88
03 *   Trying 122.0.66.162… connected
04 * Connected to yuenshui.com (122.0.66.162) port 88
05 > GET /test.php HTTP/1.1
06 > User-Agent: curl/7.15.5 (x86_64-redhat-linux-gnu) libcurl/7.15.5 OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.5
07 > Host: yuenshui.com:88
08 > Accept: */*
09 >
10 < HTTP/1.1 200 OK
11 < Server: nginx
12 < Date: Sun, 24 Jun 2012 10:38:09 GMT
13 < Content-Type: application/octet-stream
14 < Transfer-Encoding: chunked
15 < Connection: keep-alive
16 nil
17 <pre>$_SERVER:
18 Array
19 (
20     [TMP] => /tmp
21     [TMPDIR] => /tmp
22     [TEMP] => /tmp
23     [OSTYPE] =>
24     [MACHTYPE] =>
25     [MALLOC_CHECK_] => 2
26     [USER] => www
27     [HOME] => /home/www
28     [FCGI_ROLE] => RESPONDER
29     [SERVER_SOFTWARE] => nginx
30     [QUERY_STRING] =>
31     [REQUEST_METHOD] => GET
32     [CONTENT_TYPE] =>
33     [CONTENT_LENGTH] =>
34     [SCRIPT_NAME] => /test.php
35     [REQUEST_URI] => /test.php
36     [DOCUMENT_URI] => /test.php
37     [SERVER_PROTOCOL] => HTTP/1.1
38     [REMOTE_ADDR] => 114.93.76.130
39     [REMOTE_PORT] => 1037
40     [SERVER_ADDR] => 122.0.66.162
41     [SERVER_PORT] => 88
42     [SERVER_NAME] => yuenshui.com
43     [REDIRECT_STATUS] => 200
44     [HTTP_USER_AGENT] => curl/7.15.5 (x86_64-redhat-linux-gnu) libcurl/7.15.5OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.5
45     [HTTP_HOST] => yuenshui.com:88
46     [HTTP_ACCEPT] => */*
47     [HTTP_CHECK_LOGIN] => NULL
48     [PHP_SELF] => /test.php
49     [REQUEST_TIME] => 1340534289
50     [argv] => Array
51         (
52         )
53
54     [argc] => 0
55 )
56
57 $_COOKIE:
58 Array
59 (
60 )

token错误的测试:

01 [root@localhost soft]# curl -b "uid=12345;nickname=soga;token=aa6f21ec0fcf008aa5250904985a817b"  "http://yuenshui.com:88/test.php"
02 <pre>$_SERVER:
03 Array
04 (
05     [TMP] => /tmp
06     [TMPDIR] => /tmp
07     [TEMP] => /tmp
08     [OSTYPE] =>
09     [MACHTYPE] =>
10     [MALLOC_CHECK_] => 2
11     [USER] => www
12     [HOME] => /home/www
13     [FCGI_ROLE] => RESPONDER
14     [SERVER_SOFTWARE] => nginx
15     [QUERY_STRING] =>
16     [REQUEST_METHOD] => GET
17     [CONTENT_TYPE] =>
18     [CONTENT_LENGTH] =>
19     [SCRIPT_NAME] => /test.php
20     [REQUEST_URI] => /test.php
21     [DOCUMENT_URI] => /test.php
22     [SERVER_PROTOCOL] => HTTP/1.1
23     [REMOTE_ADDR] => 114.93.76.130
24     [REMOTE_PORT] => 1059
25     [SERVER_ADDR] => 122.0.66.162
26     [SERVER_PORT] => 88
27     [SERVER_NAME] => yuenshui.com
28     [REDIRECT_STATUS] => 200
29     [HTTP_USER_AGENT] => curl/7.15.5 (x86_64-redhat-linux-gnu) libcurl/7.15.5OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.5
30     [HTTP_HOST] => yuenshui.com:88
31     [HTTP_ACCEPT] => */*
32     [HTTP_COOKIE] => uid=12345;nickname=soga;token=aa6f21ec0fcf008aa5250904985a817b
33     [HTTP_CHECK_LOGIN] => No
34     [PHP_SELF] => /test.php
35     [REQUEST_TIME] => 1340537366
36     [argv] => Array
37         (
38         )
39
40     [argc] => 0
41 )
42
43 $_COOKIE:
44 Array
45 (
46     [uid] => 12345
47     [nickname] => soga
48     [token] => aa6f21ec0fcf008aa5250904985a817b
49 )

token正确的测试:

01 [root@localhost soft]# curl -b "uid=1234;nickname=soga;token=aa6f21ec0fcf008aa5250904985a817b"  "http://yuenshui.com:88/test.php"
02 <pre>$_SERVER:
03 Array
04 (
05     [TMP] => /tmp
06     [TMPDIR] => /tmp
07     [TEMP] => /tmp
08     [OSTYPE] =>
09     [MACHTYPE] =>
10     [MALLOC_CHECK_] => 2
11     [USER] => www
12     [HOME] => /home/www
13     [FCGI_ROLE] => RESPONDER
14     [SERVER_SOFTWARE] => nginx
15     [QUERY_STRING] =>
16     [REQUEST_METHOD] => GET
17     [CONTENT_TYPE] =>
18     [CONTENT_LENGTH] =>
19     [SCRIPT_NAME] => /test.php
20     [REQUEST_URI] => /test.php
21     [DOCUMENT_URI] => /test.php
22     [SERVER_PROTOCOL] => HTTP/1.1
23     [REMOTE_ADDR] => 114.93.76.130
24     [REMOTE_PORT] => 1059
25     [SERVER_ADDR] => 122.0.66.162
26     [SERVER_PORT] => 88
27     [SERVER_NAME] => yuenshui.com
28     [REDIRECT_STATUS] => 200
29     [HTTP_USER_AGENT] => curl/7.15.5 (x86_64-redhat-linux-gnu) libcurl/7.15.5OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.5
30     [HTTP_HOST] => yuenshui.com:88
31     [HTTP_ACCEPT] => */*
32     [HTTP_COOKIE] => uid=1234;nickname=soga;token=aa6f21ec0fcf008aa5250904985a817b
33     [HTTP_CHECK_LOGIN] => Yes
34     [PHP_SELF] => /test.php
35     [REQUEST_TIME] => 1340537463
36     [argv] => Array
37         (
38         )
39
40     [argc] => 0
41 )
42
43 $_COOKIE:
44 Array
45 (
46     [uid] => 12345
47     [nickname] => soga
48     [token] => aa6f21ec0fcf008aa5250904985a817b
49 )

http://yuenshui.com:88/test.php   打印信息的PHP代码:

01 <pre><?php
02 echo "\$_SERVER:\r\n";
03 unset($_SERVER['SCRIPT_FILENAME']);
04 unset($_SERVER['DOCUMENT_ROOT']);
05 unset($_SERVER['PATH']);
06 unset($_SERVER['HOSTNAME']);
07 unset($_SERVER['GATEWAY_INTERFACE']);
08 print_r($_SERVER);
09 echo "\r\n\$_COOKIE:\r\n";
10 print_r($_COOKIE);
11 ?>

上面是所有的测试程序。大家也可以通过curl进行测试http://yuenshui.com:88/test.php

发表在 lua, openresty | 留下评论

图解Javascript上下文与作用域

本文尝试阐述Javascript中的上下文与作用域背后的机制,主要涉及到执行上下文(execution context)、作用域链(scope chain)、闭包(closure)、this等概念。

Execution context

执行上下文(简称上下文)决定了Js执行过程中可以获取哪些变量、函数、数据,一段程序可能被分割成许多不同的上下文,每一个上下文都会绑定一个变量对象(variable object),它就像一个容器,用来存储当前上下文中所有已定义或可获取的变量、函数等。位于最顶端或最外层的上下文称为全局上下文(global context),全局上下文取决于执行环境,如Node中的global和Browser中的window

js-global-context

 

需要注意的是,上下文与作用域(scope)是不同的概念。Js本身是单线程的,每当有function被执行时,就会产生一个新的上下文,这一上下文会被压入Js的上下文堆栈(context stack)中,function执行结束后则被弹出,因此Js解释器总是在栈顶上下文中执行。在生成新的上下文时,首先会绑定该上下文的变量对象,其中包括arguments和该函数中定义的变量;之后会创建属于该上下文的作用域链(scope chain),最后将this赋予这一function所属的Object,这一过程可以通过下图表示:

js-context-stack

 

this

上文提到this被赋予function所属的Object,具体来说,当function是定义在global对中时,this指向global;当function作为Object的方法时,this指向该Object:

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var x = 1;
var f = function(){
  console.log(this.x);
}
f();  // -> 1
var ff = function(){
  this.x = 2;
  console.log(this.x);
}
ff(); // -> 2
x     // -> 2
var o = {x: "o's x", f: f};
o.f(); // "o's x"

 

Scope chain

上文提到,在function被执行时生成新的上下文时会先绑定当前上下文的变量对象,再创建作用域链。我们知道function的定义是可以嵌套在其他function所创建的上下文中,也可以并列地定义在同一个上下文中(如global)。作用域链实际上就是自下而上地将所有嵌套定义的上下文所绑定的变量对象串接到一起,使嵌套的function可以“继承”上层上下文的变量,而并列的function之间互不干扰:

js-scope-chain

 

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var x = 'global';
function a(){
  var x = "a's x";
  function b(){
    var y = "b's y";
    console.log(x);
  };
  b();
}
function c(){
  var x = "c's x";
  function d(){
    console.log(y);
  };
  d();
}
a();  // -> "a's x"
c();  // -> ReferenceError: y is not defined
x     // -> "global"
y     // -> ReferenceError: y is not defined

 

Closure

如果理解了上文中提到的上下文与作用域链的机制,再来看闭包的概念就很清楚了。每个function在调用时会创建新的上下文及作用域链,而作用域链就是将外层(上层)上下文所绑定的变量对象逐一串连起来,使当前function可以获取外层上下文的变量、数据等。如果我们在function中定义新的function,同时将内层function作为值返回,那么内层function所包含的作用域链将会一起返回,即使内层function在其他上下文中执行,其内部的作用域链仍然保持着原有的数据,而当前的上下文可能无法获取原先外层function中的数据,使得function内部的作用域链被保护起来,从而形成“闭包”。看下面的例子:

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var x = 100;
var inc = function(){
  var x = 0;
  return function(){
    console.log(x++);
  };
};
var inc1 = inc();
var inc2 = inc();
inc1();  // -> 0
inc1();  // -> 1
inc2();  // -> 0
inc1();  // -> 2
inc2();  // -> 1
x;       // -> 100

执行过程如下图所示,inc内部返回的匿名function在创建时生成的作用域链包括了inc中的x,即使后来赋值给inc1inc2之后,直接在global context下调用,它们的作用域链仍然是由定义中所处的上下文环境决定,而且由于x是在function inc中定义的,无法被外层的global context所改变,从而实现了闭包的效果:

js-closure

 

this in closure

我们已经反复提到执行上下文和作用域实际上是通过function创建、分割的,而function中的this与作用域链不同,它是由执行该function时当前所处的Object环境所决定的,这也是this最容易被混淆用错的一点。一般情况下的例子如下:

JavaScript

1
2
3
4
5
6
7
8
var name = "global";
var o = {
  name: "o",
  getName: function(){
    return this.name
  }
};
o.getName();  // -> "o"

由于执行o.getName()getName所绑定的this是调用它的o,所以此时this == o;更容易搞混的是在closure条件下:

JavaScript

1
2
3
4
5
6
7
8
9
10
var name = "global";
var oo = {
  name: "oo",
  getNameFunc: function(){
    return function(){
      return this.name;
    };
  }
}
oo.getNameFunc()();  // -> "global"

此时闭包函数被return后调用相当于:

JavaScript

1
2
getName = oo.getNameFunc();
getName();  // -> "global"

换一个更明显的例子:

JavaScript

1
2
3
4
5
var ooo = {
  name: "ooo",
  getName: oo.getNameFunc() // 此时闭包函数的this被绑定到新的Object
};
ooo.getName();  // -> "ooo"

当然,有时候为了避免闭包中的this在执行时被替换,可以采取下面的方法:

JavaScript

1
2
3
4
5
6
7
8
9
10
11
var name = "global";
var oooo = {
  name: "ox4",
  getNameFunc: function(){
    var self = this;
    return function(){
       return self.name;
    };
  }
};
oooo.getNameFunc()(); // -> "ox4"

或者是在调用时强行定义执行的Object:

JavaScript

1
2
3
4
5
6
7
8
9
10
11
var name = "global";
var oo = {
  name: "oo",
  getNameFunc: function(){
    return function(){
      return this.name;
    };
  }
}
oo.getNameFunc()();  // -> "global"
oo.getNameFunc().bind(oo)(); // -> "oo"

 

总结

Js是一门很有趣的语言,由于它的很多特性是针对HTML中DOM的操作,因而显得随意而略失严谨,但随着前端的不断繁荣发展和Node的兴起,Js已经不再是”toy language”或是jQuery时代的”CSS扩展”,本文提到的这些概念无论是对新手还是从传统Web开发中过度过来的Js开发人员来说,都很容易被混淆或误解,希望本文可以有所帮助。

写这篇总结的原因是我在Github上分享的Learn javascript in one picture,刚开始有人质疑这只能算是一张语法表(syntax cheat sheet),根本不会涉及更深层的闭包、作用域等内容,但是出乎意料的是这个项目竟然获得3000多个star,所以不能虎头蛇尾,以上。

References

  1. Understanding Scope and Context in JavaScript
  2. this – JavaScript | MDN
  3. 闭包 – JavaScript | MDN

FROM: http://web.jobbole.com/83031/

发表在 js | 留下评论

图解Javascript原型(prototype)链

FROM: http://www.techug.com/prototype-chain-in-javascript

本文尝试阐述Js中原型(prototype)、原型链(prototype chain)等概念及其作用机制。上一篇文章(图解Javascript上下文与作用域)介绍了Js中变量作用域的相关概念,实际上关注的一个核心问题是:“在执行当前这行代码时Js解释器可以获取哪些变量”,而原型与原型链实际上还是关于这一问题。

我们知道,在Js中一切皆为对象(Object),但是Js中并没有类(class);Js是基于原型(prototype-based)来实现的面向对象(OOP)的编程范式的,但并不是所有的对象都拥有prototype这一属性:

var a = {};  
console.log(a.prototype);  //=> undefined

var b = function(){};  
console.log(b.prototype);  //=> {}

var c = 'Hello';  
console.log(c.prototype);  //=> undefined

prototype是每个function定义时自带的属性,但是Js中function本身也是对象,我们先来看一下下面几个概念的差别:

1. functionFunctionObject{}

function是Js的一个关键词,用于定义函数类型的变量,有两种语法形式:

function f1(){  
  console.log('This is function f1!');
}
typeof(f1);  //=> 'function'

var f2 = function(){  
  console.log('This is function f2!');
}
typeof(f2);  //=> 'function'

如果用更加面向对象的方法来定义函数,可以用Function

var f3 = new Function("console.log('This is function f3!');");  
f3();        //=> 'This is function f3!'  
typeof(f3);  //=> 'function'

typeof(Function); //=> 'function'

实际上Function就是一个用于构造函数类型变量的类,或者说是函数类型实例的构造函数(constructor);与之相似有的ObjectStringNumber等,都是Js内置类型实例的构造函数。比较特殊的是Object,它用于生成对象类型,其简写形式为{}

var o1 = new Object();  
typeof(o1);      //=> 'object'

var o2 = {};  
typeof(o2);     //=> 'object'

typeof(Object); //=> 'function'

2. prototype VS __proto__

清楚了上面的概念之后再来看prototype

Each function has two properties: length and prototype

prototypelength是每一个函数类型自带的两个属性,而其它非函数类型并没有(开头的例子已经说明),这一点之所以比较容易被忽略或误解,是因为所有类型的构造函数本身也是函数,所以它们自带了prototype属性:

// Node
console.log(Object.prototype);  //=> {}  
console.log(Function.prototype);//=> [Function: Empty]  
console.log(String.prototype);  //=> [String: '']

除了prototype之外,Js中的所有对象(undefinednull等特殊情况除外)都有一个内置的[[Prototype]]属性,指向它“父类”的prototype,这个内置属性在ECMA标准中并没有给出明确的获取方式,但是许多Js的实现(如Node、大部分浏览器等)都提供了一个__proto__属性来指代这一[[Prototype]],我们通过下面的例子来说明实例中的__proto__是如何指向构造函数的prototype的:

var Person = function(){};  
Person.prototype.type = 'Person';  
Person.prototype.maxAge = 100;

var p = new Person();  
console.log(p.maxAge);  
p.name = 'rainy';

Person.prototype.constructor === Person;  //=> true  
p.__proto__ === Person.prototype;         //=> true  
console.log(p.prototype);                 //=> undefined

上面的代码示例可以用下图解释:

js __proto__

Person是一个函数类型的变量,因此自带了prototype属性,prototype属性中的constructor又指向Person本身;通过new关键字生成的Person类的实例p1,通过__proto__属性指向了Person的原型。这里的__proto__只是为了说明实例p1在内部实现的时候与父类之间存在的关联(指向父类的原型),在实际操作过程中实例可以直接通过.获取父类原型中的属性,从而实现了继承的功能。

3. 原型链

清楚了prototype__proto__的概念与关系之后我们会对“Js中一切皆为对象”这句话有更加深刻的理解。进而我们会想到,既然__proto__是(几乎)所有对象都内置的属性,而且指向父类的原型,那是不是意味着我们可以“逆流而上”一直找到源头呢?我们来看下面的例子:

// Node
var Obj = function(){};  
var o = new Obj();  
o.__proto__ === Obj.prototype;  //=> true  
o.__proto__.constructor === Obj; //=> true

Obj.__proto__ === Function.prototype; //=> true  
Obj.__proto__.constructor === Function; //=> true

Function.__proto__ === Function.prototype; //=> true  
Object.__proto__ === Object.prototype;     //=> false  
Object.__proto__ === Function.prototype;   //=> true

Function.__proto__.constructor === Function;//=> true  
Function.__proto__.__proto__;               //=> {}  
Function.__proto__.__proto__ === o.__proto__.__proto__; //=> true  
o.__proto__.__proto__.__proto__ === null;   //=> true

js prototype chain

从上面的例子和图解可以看出,prototype对象也有__proto__属性,向上追溯一直到null

new关键词的作用就是完成上图所示实例与父类原型之间关系的串接,并创建一个新的对象;instanceof关键词的作用也可以从上图中看出,实际上就是判断__proto__(以及__proto__.__proto__…)所指向是否父类的原型:

var Obj = function(){};  
var o = new Obj();

o instanceof Obj; //=> true  
o instanceof Object; //=> true  
o instanceof Function; //=> false

o.__proto__ === Obj.prototype; //=> true  
o.__proto__.__proto__ === Object.prototype; //=> true  
o.__proto__.__proto__ === Function;  //=> false

参考

  1. JavaScript constructors, prototypes, and the new keyword
  2. Javascript 面向对象编程
  3. Professional JavaScript for Web Developers

– END –

发表在 js | 留下评论

openresty+websocket+redis simple chat浅析

FROM: http://www.2cto.com/kf/201603/493726.html

openresty 很早就支持websocket了,但是早期的版本cosocket是单工的,处理起来比较麻烦,后来的版本cosocket是双全工的,就可以按照这个讨论的方案来实现基于websocket的聊天,或者是push程序了,但是网络上没有找到一个具体一点的例子,于是自己写了个simple的例子。

1

client的websocket连接到openresty之后,使用ngx.thread.spawn启动两个 轻线程,一个用来接收客户端提交的数据往redis的channel写,另一个用来订阅channel,读取redis的数据写给客户端。channel相当于一个chat room,多个client一起订阅,有人发聊天信息(pub),所有人都能得到信息(sub)。代码比较简陋,简单的思路的实现。

2 服务端代码

依赖:

openresty redis lua-resty-redis lua-resty-websocket 只支持RFC 6455

nginx的配置全贴了,就是两个location,一个是页面地址,一个是websocket地址。

配置片段

 location = /sredis { content_by_lua_file conf/lua/ws_redis.lua; } location ~ /ws/(.*) { alias conf/html/$1.html; }

lua代码

-- simple chat with redis local server = require "resty.websocket.server" local redis = require "resty.redis" local channel_name = "chat" local msg_id = 0 --create connection local wb, err = server:new{ timeout = 10000, max_payload_len = 65535 } --create success if not wb then ngx.log(ngx.ERR, "failed to new websocket: ", err) return ngx.exit(444) end local push = function() -- --create redis local red = redis:new() red:set_timeout(5000) -- 1 sec local ok, err = red:connect("127.0.0.1", 6379) if not ok then ngx.log(ngx.ERR, "failed to connect redis: ", err) wb:send_close() return end --sub local res, err = red:subscribe(channel_name) if not res then ngx.log(ngx.ERR, "failed to sub redis: ", err) wb:send_close() return end -- loop : read from redis while true do local res, err = red:read_reply() if res then local item = res[3] local bytes, err = wb:send_text(tostring(msg_id).." "..item) if not bytes then -- better error handling ngx.log(ngx.ERR, "failed to send text: ", err) return ngx.exit(444) end msg_id = msg_id + 1 end end end local co = ngx.thread.spawn(push) --main loop while true do -- 获取数据 local data, typ, err = wb:recv_frame() -- 如果连接损坏 退出 if wb.fatal then ngx.log(ngx.ERR, "failed to receive frame: ", err) return ngx.exit(444) end if not data then local bytes, err = wb:send_ping() if not bytes then ngx.log(ngx.ERR, "failed to send ping: ", err) return ngx.exit(444) end ngx.log(ngx.ERR, "send ping: ", data) elseif typ == "close" then break elseif typ == "ping" then local bytes, err = wb:send_pong() if not bytes then ngx.log(ngx.ERR, "failed to send pong: ", err) return ngx.exit(444) end elseif typ == "pong" then ngx.log(ngx.ERR, "client ponged") elseif typ == "text" then --send to redis local red2 = redis:new() red2:set_timeout(1000) -- 1 sec local ok, err = red2:connect("127.0.0.1", 6379) if not ok then ngx.log(ngx.ERR, "failed to connect redis: ", err) break end local res, err = red2:publish(channel_name, data) if not res then ngx.log(ngx.ERR, "failed to publish redis: ", err) end end end wb:send_close() ngx.thread.wait(co)

3 页面代码


<script type="text/javascript"> var ws = null; function WebSocketConn() { if (ws != null && ws.readyState == 1) { log("已经在线"); return } if ("WebSocket" in window) { // Let us open a web socket ws = new WebSocket("ws://localhost:8008/sredis"); ws.onopen = function() { log('成功进入聊天室'); }; ws.onmessage = function(event) { log(event.data) }; ws.onclose = function() { // websocket is closed. log("已经和服务器断开"); }; ws.onerror = function(event) { console.log("error " + event.data); }; } else { // The browser doesn't support WebSocket alert("WebSocket NOT supported by your Browser!"); } } function SendMsg() { if (ws != null && ws.readyState == 1) { var msg = document.getElementById('msgtext').value; ws.send(msg); } else { log('请先进入聊天室'); } } function WebSocketClose() { if (ws != null && ws.readyState == 1) { ws.close(); log("发送断开服务器请求"); } else { log("当前没有连接服务器") } } function log(text) { var li = document.createElement('li'); li.appendChild(document.createTextNode(text)); document.getElementById('log').appendChild(li); return false; } </script>

4 效果

用iphone试了试,不好使,可能是websocket版本实现的问题。pc端测试可以正常使用。

这里写图片描述

发表在 websocket, 架构 | 留下评论

使用mysql实现ID生成器

flickr巧妙地使用了MySQL的自增ID,及replace into语法,十分简洁地实现了分片ID生成功能。

首先,创建一个表:

 

[sql] view plain copy

在CODE上查看代码片派生到我的代码片

  1. CREATE TABLE `Tickets64` (
  2.   `id` bigint(20) unsigned NOT NULL auto_increment,
  3.   `stub` char(1) NOT NULL default '',
  4.   PRIMARY KEY  (`id`),
  5.   UNIQUE KEY `stub` (`stub`)
  6. ) ENGINE=MyISAM

使用上面的sql可以得到一个ID:

 在CODE上查看代码片派生到我的代码片
  1. REPLACE INTO Tickets64 (stub) VALUES ('a');
  2. SELECT LAST_INSERT_ID();

因为使用了replace into的语法,实际上,Tickets64这个表里的数据永远都是这样的:

在CODE上查看代码片派生到我的代码片

  1. +-------------------+------+
  2. | id                | stub |
  3. +-------------------+------+
  4. | 72157623227190423 |    a |
  5. +-------------------+------+

那么如何解决单点故障呢?

 

很简单,利用mysql的自增ID即可。比如有两台ID生成服务器,设置成下面即可:

  1. TicketServer1:
  2. auto-increment-increment = 2
  3. auto-increment-offset = 1
  4. TicketServer2:
  5. auto-increment-increment = 2
  6. auto-increment-offset = 2

优点:

 

简单可靠。

缺点:

ID只是一个ID,没有带入时间,shardingId等信息。

发表在 架构 | 留下评论

分片(Sharding)的全局ID生成

FROM: http://blog.csdn.net/hengyunabc/article/details/19025973

前言

数据在分片时,典型的是分库分表,就有一个全局ID生成的问题。单纯的生成全局ID并不是什么难题,但是生成的ID通常要满足分片的一些要求:

 

  • 不能有单点故障。
  • 以时间为序,或者ID里包含时间。这样一是可以少一个索引,二是冷热数据容易分离。
  • 可以控制ShardingId。比如某一个用户的文章要放在同一个分片内,这样查询效率高,修改也容易。
  • 不要太长,最好64bit。使用long比较好操作,如果是96bit,那就要各种移位相当的不方便,还有可能有些组件不能支持这么大的ID。

 

先来看看老外的做法,以时间顺序:

flickr

flickr巧妙地使用了MySQL的自增ID,及replace into语法,十分简洁地实现了分片ID生成功能。

首先,创建一个表:

 

[sql] view plain copy

在CODE上查看代码片派生到我的代码片

  1. CREATE TABLE `Tickets64` (
  2.   `id` bigint(20) unsigned NOT NULL auto_increment,
  3.   `stub` char(1) NOT NULL default '',
  4.   PRIMARY KEY  (`id`),
  5.   UNIQUE KEY `stub` (`stub`)
  6. ) ENGINE=MyISAM

使用上面的sql可以得到一个ID:

 

 

[sql] view plain copy

在CODE上查看代码片派生到我的代码片

  1. REPLACE INTO Tickets64 (stub) VALUES ('a');
  2. SELECT LAST_INSERT_ID();

因为使用了replace into的语法,实际上,Tickets64这个表里的数据永远都是这样的:

 

 

[plain] view plain copy

在CODE上查看代码片派生到我的代码片

  1. +-------------------+------+
  2. | id                | stub |
  3. +-------------------+------+
  4. | 72157623227190423 |    a |
  5. +-------------------+------+

那么如何解决单点故障呢?

 

很简单,利用mysql的自增ID即可。比如有两台ID生成服务器,设置成下面即可:

 

[plain] view plain copy

在CODE上查看代码片派生到我的代码片

  1. TicketServer1:
  2. auto-increment-increment = 2
  3. auto-increment-offset = 1
  4. TicketServer2:
  5. auto-increment-increment = 2
  6. auto-increment-offset = 2

优点:

 

简单可靠。

缺点:

ID只是一个ID,没有带入时间,shardingId等信息。

twitter

twitter利用zookeeper实现了一个全局ID生成的服务snowflake,https://github.com/twitter/snowflake,可以生成全局唯一的64bit ID。

生成的ID的构成:

 

[plain] view plain copy

在CODE上查看代码片派生到我的代码片

  1. 时间--用前面41 bit来表示时间,精确到毫秒,可以表示69年的数据
  2. 机器ID--用10 bit来表示,也就是说可以部署1024台机器
  3. 序列数--用12 bit来表示,意味着每台机器,每毫秒最多可以生成4096个ID

优点:

 

充分把信息保存到ID里。

缺点:

结构略复杂,要依赖zookeeper。

分片ID不能灵活生成。

instagram

instagram参考了flickr的方案,再结合twitter的经验,利用Postgres数据库的特性,实现了一个更简单可靠的ID生成服务。

instagram是这样设计它们的ID的:

 

[plain] view plain copy

在CODE上查看代码片派生到我的代码片

  1. 使用41 bit来存放时间,精确到毫秒,可以使用41年。
  2. 使用13 bit来存放逻辑分片ID。
  3. 使用10 bit来存放自增长ID,意味着每台机器,每毫秒最多可以生成1024个ID

以instagram举的例子为说明:
假定时间是September 9th, 2011, at 5:00pm,则毫秒数是1387263000(直接使用系统得到的从1970年开始的毫秒数)。那么先把时间数据放到ID里:
id = 1387263000 << (64-41)
再把分片ID放到时间里,假定用户ID是31341,有2000个逻辑分片,则分片ID是31341 % 2000 -> 1341:
id |= 1341 << (64-41-13)
最后,把自增序列放ID里,假定前一个序列是5000,则新的序列是5001:
id |= (5001 % 1024)
这样就得到了一个全局的分片ID。

 

下面列出instagram使用的Postgres schema的sql:

 

[sql] view plain copy

在CODE上查看代码片派生到我的代码片

  1. REATE OR REPLACE FUNCTION insta5.next_id(OUT result bigint) AS $$
  2. DECLARE
  3.     our_epoch bigint := 1314220021721;
  4.     seq_id bigint;
  5.     now_millis bigint;
  6.     shard_id int := 5;
  7. BEGIN
  8.     SELECT nextval('insta5.table_id_seq') %% 1024 INTO seq_id;
  9.     SELECT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000) INTO now_millis;
  10.     result := (now_millis - our_epoch) << 23;
  11.     result := result | (shard_id << 10);
  12.     result := result | (seq_id);
  13. END;
  14. $$ LANGUAGE PLPGSQL;

则在插入新数据时,直接用类似下面的SQL即可(连请求生成ID的步骤都省略了!):

 

 

[sql] view plain copy

在CODE上查看代码片派生到我的代码片

  1. CREATE TABLE insta5.our_table (
  2.     "id" bigint NOT NULL DEFAULT insta5.next_id(),
  3.     ...rest of table schema...
  4. )

即使是不懂Postgres数据库,也能从上面的SQL看出个大概。把这个移植到mysql上应该也不是什么难事。

 

缺点:

貌似真的没啥缺点。

优点:

充分把信息保存到ID里。

充分利用数据库自身的机制,程序完全不用额外处理,直接插入到对应的分片的表即可。

使用redis的方案

站在前人的肩膀上,我想到了一个利用redis + lua的方案。

首先,lua内置的时间函数不能精确到毫秒,因此先要修改下redis的代码,增加currentMiliseconds函数,我偷懒,直接加到math模块里了。

修改redis代码下的scripting.c文件,加入下面的内容:

 

[cpp] view plain copy

在CODE上查看代码片派生到我的代码片

  1. #include <sys/time.h>
  2. int redis_math_currentMiliseconds (lua_State *L);
  3. void scriptingInit(void) {
  4.     ...
  5.     lua_pushstring(lua,"currentMiliseconds");
  6.     lua_pushcfunction(lua,redis_math_currentMiliseconds);
  7.     lua_settable(lua,-3);
  8.     lua_setglobal(lua,"math");
  9.     ...
  10. }
  11. int redis_math_currentMiliseconds(lua_State *L) {
  12.     struct timeval now;
  13.     gettimeofday(&now, NULL);
  14.     lua_pushnumber(L, now.tv_sec*1000 + now.tv_usec/1000);
  15.     return 1;
  16. }

这个方案直接返回三元组(时间,分片ID,增长序列),当然Lua脚本是非常灵活的,可以自己随意修改。

[plain] view plain copy

在CODE上查看代码片派生到我的代码片

  1. 时间:redis服务器上的毫秒数
  2. 分片ID:由传递进来的参数KEYS[1]%1024得到。
  3. 增长序列:由redis上"idgenerator_next_" 为前缀,接分片ID的Key用incrby命令得到。

 

 

例如,用户发一个文章,要生成一个文章ID,假定用户ID是14532,则

 

[plain] view plain copy

在CODE上查看代码片派生到我的代码片

  1. time <-- math.currentMiliseconds();
  2. shardindId  <-- 14532 % 1024;     //即196
  3. articleId <-- incrby idgenerator_next_196 1  //1是增长的步长

用lua脚本表示是:

 

 

[plain] view plain copy

在CODE上查看代码片派生到我的代码片

  1. local step = redis.call('GET', 'idgenerator_step');
  2. local shardId = KEYS[1] % 1024;
  3. local next = redis.call('INCRBY', 'idgenerator_next_' .. shardId, step);
  4. return {math.currentMiliseconds(), shardId, next};

“idgenerator_step"这个key用来存放增长的步长。

 

客户端用eval执行上面的脚本,得到三元组之后,可以自由组合成64bit的全局ID。

上面只是一个服务器,那么如何解决单点问题呢?

上面的“idgenerator_step"的作用就体现出来了。

比如,要部署三台redis做为ID生成服务器,分别是A,B,C。那么在启动时设置redis-A下面的键值:

 

[plain] view plain copy

在CODE上查看代码片派生到我的代码片

  1. idgenerator_step = 3
  2. idgenerator_next_1, idgenerator_next_2, idgenerator_next_3 ... idgenerator_next_1024 = 1

 

设置redis-B下面的键值:

 

 

[plain] view plain copy

在CODE上查看代码片派生到我的代码片

  1. idgenerator_step = 3
  2. idgenerator_next_1, idgenerator_next_2, idgenerator_next_3 ... idgenerator_next_1024 = 2

 

设置redis-C下面的键值:

 

 

[plain] view plain copy

在CODE上查看代码片派生到我的代码片

  1. idgenerator_step = 3
  2. idgenerator_next_1, idgenerator_next_2, idgenerator_next_3 ... idgenerator_next_1024 = 3

 

那么上面三台ID生成服务器之间就是完全独立的,而且平等关系的。任意一台服务器挂掉都不影响,客户端只要随机选择一台去用eval命令得到三元组即可。

我测试了下单台的redis服务器每秒可以生成3万个ID。那么部署三台ID服务器足以支持任何的应用了。

测试程序见这里:

https://gist.github.com/hengyunabc/9032295

缺点:

如果不熟悉lua脚本,可能定制自己的ID规则等比较麻烦。

注意机器时间不能设置为自动同步的,否则可能会因为时间同步,而导致ID重复了。

优点:

非常的快,而且可以线性部署。

可以随意定制自己的Lua脚本,生成各种业务的ID。

其它的东东:

MongoDB的Objectid,这个实在是太长了要12个字节:

 

[plain] view plain copy

在CODE上查看代码片派生到我的代码片

  1. ObjectId is a 12-byte BSON type, constructed using:
  2. a 4-byte value representing the seconds since the Unix epoch,
  3. a 3-byte machine identifier,
  4. a 2-byte process id, and
  5. a 3-byte counter, starting with a random value.

 

总结:

生成全局ID并不很难实现的东东,不过从各个网络的做法,及演进还是可以学到很多东东。有时候一些简单现成的组件就可以解决问题,只是缺少思路而已。

参考:

 

http://code.flickr.net/2010/02/08/ticket-servers-distributed-unique-primary-keys-on-the-cheap/

http://instagram-engineering.tumblr.com/post/10853187575/sharding-ids-at-instagram

https://github.com/twitter/snowflake/

http://docs.mongodb.org/manual/reference/object-id/

发表在 架构 | 留下评论

基于redis的分布式ID生成器

FROM:http://blog.csdn.net/hengyunabc/article/details/44244951

项目地址

https://github.com/hengyunabc/redis-id-generator

基于redis的分布式ID生成器。

准备

首先,要知道redis的EVAL,EVALSHA命令:

http://redis.readthedocs.org/en/latest/script/eval.html

http://redis.readthedocs.org/en/latest/script/evalsha.html

原理

利用redis的lua脚本执行功能,在每个节点上通过lua脚本生成唯一ID。
生成的ID是64位的:

  • 使用41 bit来存放时间,精确到毫秒,可以使用41年。
  • 使用12 bit来存放逻辑分片ID,最大分片ID是4095
  • 使用10 bit来存放自增长ID,意味着每个节点,每毫秒最多可以生成1024个ID

比如GTM时间 Fri Mar 13 10:00:00 CST 2015 ,它的距1970年的毫秒数是 1426212000000,假定分片ID是53,自增长序列是4,则生成的ID是:

5981966696448054276 = 1426212000000 << 22 + 53 << 10 + 4
  • 1
  • 1

redis提供了TIME命令,可以取得redis服务器上的秒数和微秒数。因些lua脚本返回的是一个四元组。

second, microSecond, partition, seq
  • 1
  • 1

客户端要自己处理,生成最终ID。

((second * 1000 + microSecond / 1000) << (12 + 10)) + (shardId << 10) + seq;
  • 1
  • 1

集群实现原理

假定集群里有3个节点,则节点1返回的seq是:

0, 3, 6, 9, 12 ...
  • 1
  • 1

节点2返回的seq是

1, 4, 7, 10, 13 ...
  • 1
  • 1

节点3返回的seq是

2, 5, 8, 11, 14 ...
  • 1
  • 1

这样每个节点返回的数据都是唯一的。

单个节点部署

下载redis-script-node1.lua,并把它load到redis上。

cd redis-directory/ wget https://raw.githubusercontent.com/hengyunabc/redis-id-generator/master/redis-script-node1.lua ./redis-cli script load "$(cat redis-script-node1.lua)" 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

获取lua脚本的sha1值,可能是:

fce3758b2e0af6cbf8fea4d42b379cd0dc374418
  • 1
  • 1

在代码里,通过EVALSHA命令,传递这个sha1值,就可以得到生成的ID。

比如,通过命令行执行:

./redis-cli EVALSHA fce3758b2e0af6cbf8fea4d42b379cd0dc374418 2 test 123456789
  • 1
  • 1

结果可能是:

1) (integer) 1426238286 2) (integer) 130532 3) (integer) 277 4) (integer) 4
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

集群部署

假定集群是3个节点,则分别对三个节点执行:

./redis-cli -host node1 -p 6379 script load "$(cat redis-script-node1.lua)" ./redis-cli -host node2 -p 7379 script load "$(cat redis-script-node2.lua)" ./redis-cli -host node3 -p 8379 script load "$(cat redis-script-node3.lua)" 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

性能

redis默认配置。

单节点,单线程: time:0:00:00.959 speed:10427.52867570386 单节点,20线程: time:0:00:06.710 speed:29806.259314456034
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

结论:
- 单节点,qps约3w
- 可以线性扩展,3个结点足以满足绝大部分的应用

java客户端封装

在redis-id-generator-java目录下,有example和benchmark代码。

在调用时,要传入两个参数
- tag,即为哪一类服务生成ID
- shardId,即分片由哪个ID生成,比如一个用户的订单,则分片ID应该由userId来生成

public class Example { public static void main(String[] args) { String tab = "order"; long userId = 123456789; IdGenerator idGenerator = IdGenerator.builder() .addHost("127.0.0.1", 6379, "fce3758b2e0af6cbf8fea4d42b379cd0dc374418") // .addHost("127.0.0.1", 7379, "1abc55928f37176cb934fc7a65069bf32282d817") // .addHost("127.0.0.1", 8379, "b056d20feb3f89483b10c81027440cbf6920f74f") .build(); long id = idGenerator.next(tab, userId); System.out.println("id:" + id); List<Long> result = IdGenerator.parseId(id); System.out.println("miliSeconds:" + result.get(0) + ", partition:" + result.get(1) + ", seq:" + result.get(2)); } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

多语言客户端

只要支持redis evalsha命令就可以了。

其它

之前写的一个blog:分片(Sharding)的全局ID生成

发表在 架构 | 留下评论

php图片裁剪扩展tclip

FROM: http://blog.jiunile.com/php%E5%9B%BE%E7%89%87%E8%A3%81%E5%89%AA%E6%89%A9%E5%B1%95tclip.html

用于图片裁剪。有以下特点:

1.能进行人脸识别。图片中有人脸,将自动视为人脸区域为重要区域,将不会被裁剪掉。

2.自动识别其它重要区域。如果图片中未识别出人脸,则会根据特征分布计算出重区域。

总而言之,自动识别图片中的重要区域,并且在图片裁剪时保留重要区域。

目前已经用于一淘玩客

效果演示:

原图:

常规从中间截取为 400 * 225 大小大图片。效果如下:

使用tclip裁剪图片效果如下:

安装步骤

源码下载

opencv2 下载地址  http://www.opencv.org.cn/index.php/Download

安装opencv2

此扩展依赖于opencv2.0 之上版本。因此安装前先安装opencv。opencv的安装步骤如下

01
02
03
04
05
06
07
08
09
10
11
12
13
yum install gtk+ gtk+-devel pkgconfig libpng zlib libjpeg libtiff cmake
wget http://downloads.sourceforge.net/project/opencvlibrary/opencv-unix/2.4.4/OpenCV-2.4.4a.tar.bz2?r=http%3A%2F%2Fwww.opencv.org.cn%2Findex.php%2FDownload&amp;ts=1375240385&amp;use_mirror=ncu
tar jxvf OpenCV-2.4.4a.tar.bz2
cd opencv-2.4.4/
cmake CMakeLists.txt
make && make install
vim /etc/profile
#在 unset i 前增加
export PKG_CONFIG_PATH=/usr/lib/pkgconfig/:/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH
#保持退出后,执行如下命令
source /etc/profile
echo "/usr/local/lib/" > /etc/ld.so.conf.d/opencv.conf
ldconfig

安装tclip扩展

下载源代码:http://code.taobao.org/svn/tclip/trunk

01
02
03
04
05
06
07
08
09
10
11
#cd 到源代码目录中的php_ext文件夹
/usr/local/php5/bin/phpize
./configure --with-php-config=/usr/local/php5/bin/php-config
make && make install
#修改php.ini。加入 extension=tclip.so
#重启fpm即可
#查看是否加载成功
php -i | grep tclip
tclip
tclip support => enabled
tclip.face_config_path => no value => no value

安装命令行

1
2
3
#cd 进入源码中的soft文件夹内
chmod +x ./tclip.sh
./tclip.sh

使用方法说明

第一种:在php中使用格式:

tclip(文件原路径,裁剪后的图片保存路径,裁剪后的图片宽度,裁剪后的图片高度)

示例:

1
2
3
4
5
$source_file = "/tmp/a.jpg";
$dest_file = "/www/a_dest.jpg";
$width = 400;
$height = 200;
tclip($source_file, $dest_file, $width, $height);

第二种:命令行

1
2
3
4
5
6
参数说明:
-s 原图路径
-d 裁剪后的图片保存路径
-w 裁剪后的图片宽度
-h 裁剪后的图片高度
./tclip -s a.jpg -d a_dest.jpg -w 400 -h 200
发表在 php | 留下评论

consul kv存储

key值中可以带/, 可以看做是不同的目录结构。

value的值经过了base64_encode,获取到数据后base64_decode才能获取到原始值。数据不能大于512Kb

不同数据中心的kv存储系统是独立的,使用dc=?参数指定。

/v1/kv?keys :返回所有key

/v1/kv/pre?recurse : 返回所有符合给定前缀的key 、value

 

 

 

The KV endpoints are used to access Consul's simple key/value store, useful for storing service configuration or other metadata.

The following endpoints are supported:

  • /v1/kv/<key>: Manages updates of individual keys, deletes of individual keys or key prefixes, and fetches of individual keys or key prefixes
  • /v1/txn: Manages updates or fetches of multiple keys inside a single, atomic transaction

/v1/kv/<key>

This endpoint manages updates of individual keys, deletes of individual keys or key prefixes, and fetches of individual keys or key prefixes. The GETPUT and DELETEmethods are all supported.

By default, the datacenter of the agent is queried; however, the dc can be provided using the "?dc=" query parameter. It is important to note that each datacenter has its own KV store, and there is no built-in replication between datacenters. If you are interested in replication between datacenters, look at the Consul Replicate project.

The KV endpoint supports the use of ACL tokens using the "?token=" query parameter.

GET Method

When using the GET method, Consul will return the specified key. If the "?recurse" query parameter is provided, it will return all keys with the given prefix.

This endpoint supports blocking queries and all consistency modes.

Each object will look like:

[ { "CreateIndex": 100, "ModifyIndex": 200, "LockIndex": 200, "Key": "zip", "Flags": 0, "Value": "dGVzdA==", "Session": "adf4238a-882b-9ddc-4a9d-5b6758e4159e" } ] 

CreateIndex is the internal index value that represents when the entry was created.

ModifyIndex is the last index that modified this key. This index corresponds to theX-Consul-Index header value that is returned in responses, and it can be used to establish blocking queries by setting the "?index" query parameter. You can even perform blocking queries against entire subtrees of the KV store: if "?recurse" is provided, the returned X-Consul-Index corresponds to the latest ModifyIndexwithin the prefix, and a blocking query using that "?index" will wait until any key within that prefix is updated.

LockIndex is the number of times this key has successfully been acquired in a lock. If the lock is held, the Session key provides the session that owns the lock.

Key is simply the full path of the entry.

Flags is an opaque unsigned integer that can be attached to each entry. Clients can choose to use this however makes sense for their application.

Value is a Base64-encoded blob of data. Note that values cannot be larger than 512kB.

It is possible to list just keys without their values by using the "?keys" query parameter. This will return a list of the keys under the given prefix. The optional "?separator=" can be used to list only up to a given separator.

For example, listing "/web/" with a "/" separator may return:

[ "/web/bar", "/web/foo", "/web/subdir/" ] 

Using the key listing method may be suitable when you do not need the values or flags or want to implement a key-space explorer.

If the "?raw" query parameter is used with a non-recursive GET, the response is just the raw value of the key, without any encoding.

If no entries are found, a 404 code is returned.

PUT method

When using the PUT method, Consul expects the request body to be the value corresponding to the key. There are a number of query parameters that can be used with a PUT request:

  • ?flags=<num> : This can be used to specify an unsigned value between 0 and (264)-1. Clients can choose to use this however makes sense for their application.
  • ?cas=<index> : This flag is used to turn the PUT into a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches theModifyIndex of that key.
  • ?acquire=<session> : This flag is used to turn the PUT into a lock acquisition operation. This is useful as it allows leader election to be built on top of Consul. If the lock is not held and the session is valid, this increments the LockIndexand sets the Session value of the key in addition to updating the key contents. A key does not need to exist to be acquired. If the lock is already held by the given session, then the LockIndex is not incremented but the key contents are updated. This lets the current lock holder update the key contents without having to give up the lock and reacquire it.
  • ?release=<session> : This flag is used to turn the PUT into a lock release operation. This is useful when paired with "?acquire=" as it allows clients to yield a lock. This will leave the LockIndex unmodified but will clear the associated Session of the key. The key must be held by this session to be unlocked.

The return value is either true or false. If false is returned, the update has not taken place.

DELETE method

The DELETE method can be used to delete a single key or all keys sharing a prefix. There are a few query parameters that can be used with a DELETE request:

  • ?recurse : This is used to delete all keys which have the specified prefix. Without this, only a key with an exact match will be deleted.
  • ?cas=<index> : This flag is used to turn the DELETE into a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. Unlike PUT, the index must be greater than 0 for Consul to take any action: a 0 index will not delete the key. If the index is non-zero, the key is only deleted if the index matches the ModifyIndex of that key.

/v1/txn

Available in Consul 0.7 and later, this endpoint manages updates or fetches of multiple keys inside a single, atomic transaction. Only the PUT method is supported.

By default, the datacenter of the agent receives the transaction; however, the dc can be provided using the "?dc=" query parameter. It is important to note that each datacenter has its own KV store, and there is no built-in replication between datacenters. If you are interested in replication between datacenters, look at theConsul Replicate project.

The transaction endpoint supports the use of ACL tokens using the "?token=" query parameter.

PUT Method

The PUT method lets you submit a list of operations to apply to the key/value store inside a transaction. If any operation fails, the transaction will be rolled back and none of the changes will be applied.

If the transaction doesn't contain any write operations then it will be fast-pathed internally to an endpoint that works like other reads, except that blocking queries are not currently supported. In this mode, you may supply the "?stale" or "?consistent" query parameters with the request to control consistency. To support bounding the acceptable staleness of data, read-only transaction responses provide the X-Consul-LastContact header containing the time in milliseconds that a server was last contacted by the leader node. The X-Consul-KnownLeader header also indicates if there is a known leader. These won't be present if the transaction contains any write operations, and any consistency query parameters will be ignored, since writes are always managed by the leader via the Raft consensus protocol.

The body of the request should be a list of operations to perform inside the atomic transaction. Up to 64 operations may be present in a single transaction. Operations look like this:

[ { "KV": { "Verb": "<verb>", "Key": "<key>", "Value": "<Base64-encoded blob of data>", "Flags": <flags>, "Index": <index>, "Session": "<session id>" } }, ... ] 

KV is the only available operation type, though other types of operations may be added in future versions of Consul to be mixed with key/value operations. The following fields are available:

  • Verb is the type of operation to perform. Please see the table below for available verbs.
  • Key is simply the full path of the entry.
  • Value is a Base64-encoded blob of data. Note that values cannot be larger than 512kB.
  • Flags is an opaque unsigned integer that can be attached to each entry. Clients can choose to use this however makes sense for their application.
  • Index and Session are used for locking, unlocking, and check-and-set operations. Please see the table below for details on how they are used.

The following table summarizes the available verbs and the fields that apply to that operation ("X" means a field is required and "O" means it is optional):

 

 

Verb Operation Key Value Flags Index Session
set Sets the Key to the given Value. X X O
cas Sets the Key to the given Value with check-and-set semantics. The Key will only be set if its current modify index matches the supplied Index. X X O X
lock Locks the Key with the given Session. The Keywill only obtain the lock if the Session is valid, and no other session has it locked. X X O X
unlock Unlocks the Key with the given Session. TheKey will only release the lock if the Session is valid and currently has it locked. X X O X
get Gets the Key during the transaction. This fails the transaction if the Key doesn't exist. The key may not be present in the results if ACLs do not permit it to be read. X
get-tree Gets all keys with a prefix of Key during the transaction. This does not fail the transaction if theKey doesn't exist. Not all keys may be present in the results if ACLs do not permit them to be read. X
check-index Fails the transaction if Key does not have a modify index equal to Index. X X
check-session Fails the transaction if Key is not currently locked by Session. X X
delete Deletes the Key. X
delete-tree Deletes all keys with a prefix ofKey. X
delete-cas Deletes the Key with check-and-set semantics. The Key will only be deleted if its current modify index matches the supplied Index. X X

If the transaction can be processed, a status code of 200 will be returned if it was successfully applied, or a status code of 409 will be returned if it was rolled back. If either of these status codes are returned, the response will look like this:

{ "Results": [ { "KV": { "LockIndex": <lock index>, "Key": "<key>", "Flags": <flags>, "Value": "<Base64-encoded blob of data, or null>", "CreateIndex": <index>, "ModifyIndex": <index> } }, ... ], "Errors": [ { "OpIndex": <index of failed operation>, "What": "<error message for failed operation>" }, ... ] } 

Results has entries for some operations if the transaction was successful. To save space, the Value will be null for any Verb other than "get" or "get-tree". Like the/v1/kv/<key> endpoint, Value will be Base64-encoded if it is present. Also, no result entries will be added for verbs that delete keys.

Errors has entries describing which operations failed if the transaction was rolled back. The OpIndex gives the index of the failed operation in the transaction, andWhat is a string with an error message about why that operation failed.

If any other status code is returned, such as 400 or 500, then the body of the response will simply be an unstructured error message about what happened.

发表在 Consul, 服务注册和发现 | 留下评论

consul agent配制参数

The agent has various configuration options that can be specified via the command-line or via configuration files. All of the configuration options are completely optional. Defaults are specified with their descriptions.

When loading configuration, Consul loads the configuration from files and directories in lexical order. For example, configuration file basic_config.json will be processed before extra_config.json. Configuration specified later will be merged into configuration specified earlier. In most cases, "merge" means that the later version will override the earlier. In some cases, such as event handlers, merging appends the handlers to the existing configuration. The exact merging behavior is specified for each option below.

Consul also supports reloading configuration when it receives the SIGHUP signal. Not all changes are respected, but those that are are documented below in theReloadable Configuration section. The reload command can also be used to trigger a configuration reload.

Command-line Options

The options below are all specified on the command-line.

  • -advertise - The advertise address is used to change the address that we advertise to other nodes in the cluster. By default, the -bind address is advertised. However, in some cases, there may be a routable address that cannot be bound. This flag enables gossiping a different address to support this. If this address is not routable, the node will be in a constant flapping state as other nodes will treat the non-routability as a failure.
  • -advertise-wan - The advertise WAN address is used to change the address that we advertise to server nodes joining through the WAN. This can also be set on client agents when used in combination with the translate_wan_addrsconfiguration option. By default, the -advertise address is advertised. However, in some cases all members of all datacenters cannot be on the same physical or virtual network, especially on hybrid setups mixing cloud and private datacenters. This flag enables server nodes gossiping through the public network for the WAN while using private VLANs for gossiping to each other and their client agents, and it allows client agents to be reached at this address when being accessed from a remote datacenter if the remote datacenter is configured with translate_wan_addrs.
  • -atlas - This flag enables Atlas integration. It is used to provide the Atlas infrastructure name and the SCADA connection. The format of this isusername/environment. This enables Atlas features such as the Monitoring UI and node auto joining.
  • -atlas-join - When set, enables auto-join via Atlas. Atlas will track the most recent members to join the infrastructure named by -atlas and automatically join them on start. For servers, the LAN and WAN pool are both joined.
  • -atlas-token - Provides the Atlas API authentication token. This can also be provided using the ATLAS_TOKEN environment variable. Required for use with Atlas.
  • -atlas-endpoint - The endpoint address used for Atlas integration. Used only if the -atlas and -atlas-token options are specified. This is optional, and defaults to the public Atlas endpoints. This can also be specified using theSCADA_ENDPOINT environment variable. The CLI option takes precedence, followed by the configuration file directive, and lastly, the environment variable.
  • -bootstrap - This flag is used to control if a server is in "bootstrap" mode. It is important that no more than one server per datacenter be running in this mode. Technically, a server in bootstrap mode is allowed to self-elect as the Raft leader. It is important that only a single node is in this mode; otherwise, consistency cannot be guaranteed as multiple nodes are able to self-elect. It is not recommended to use this flag after a cluster has been bootstrapped.
  • -bootstrap-expect - This flag provides the number of expected servers in the datacenter. Either this value should not be provided or the value must agree with other servers in the cluster. When provided, Consul waits until the specified number of servers are available and then bootstraps the cluster. This allows an initial leader to be elected automatically. This cannot be used in conjunction with the legacy -bootstrap flag.
  • -bind - The address that should be bound to for internal cluster communications. This is an IP address that should be reachable by all other nodes in the cluster. By default, this is "0.0.0.0", meaning Consul will use the first available private IPv4 address. If you specify "[::]", Consul will use the first available public IPv6 address. Consul uses both TCP and UDP and the same port for both. If you have any firewalls, be sure to allow both protocols.
  • -client - The address to which Consul will bind client interfaces, including the HTTP, DNS, and RPC servers. By default, this is "127.0.0.1", allowing only loopback connections. The RPC address is used by other Consul commands, such as consul members, in order to query a running Consul agent.
  • -config-file - A configuration file to load. For more information on the format of this file, read the Configuration Files section. This option can be specified multiple times to load multiple configuration files. If it is specified multiple times, configuration files loaded later will merge with configuration files loaded earlier. During a config merge, single-value keys (string, int, bool) will simply have their values replaced while list types will be appended together.
  • -config-dir - A directory of configuration files to load. Consul will load all files in this directory with the suffix ".json". The load order is alphabetical, and the the same merge routine is used as with the config-file option above. This option can be specified multiple times to load multiple directories. Sub-directories of the config directory are not loaded. For more information on the format of the configuration files, see the Configuration Files section.
  • -data-dir - This flag provides a data directory for the agent to store state. This is required for all agents. The directory should be durable across reboots. This is especially critical for agents that are running in server mode as they must be able to persist cluster state. Additionally, the directory must support the use of filesystem locking, meaning some types of mounted folders (e.g. VirtualBox shared folders) may not be suitable.
  • -dev - Enable development server mode. This is useful for quickly starting a Consul agent with all persistence options turned off, enabling an in-memory server which can be used for rapid prototyping or developing against the API. This mode is not intended for production use as it does not write any data to disk.
  • -dc - This flag controls the datacenter in which the agent is running. If not provided, it defaults to "dc1". Consul has first-class support for multiple datacenters, but it relies on proper configuration. Nodes in the same datacenter should be on a single LAN.
  • -domain - By default, Consul responds to DNS queries in the "consul." domain. This flag can be used to change that domain. All queries in this domain are assumed to be handled by Consul and will not be recursively resolved.
  • -encrypt - Specifies the secret key to use for encryption of Consul network traffic. This key must be 16-bytes that are Base64-encoded. The easiest way to create an encryption key is to use consul keygen. All nodes within a cluster must share the same encryption key to communicate. The provided key is automatically persisted to the data directory and loaded automatically whenever the agent is restarted. This means that to encrypt Consul's gossip protocol, this option only needs to be provided once on each agent's initial startup sequence. If it is provided after Consul has been initialized with an encryption key, then the provided key is ignored and a warning will be displayed.
  • -http-port - the HTTP API port to listen on. This overrides the default port 8500. This option is very useful when deploying Consul to an environment which communicates the HTTP port through the environment e.g. PaaS like CloudFoundry, allowing you to set the port directly via a Procfile.
  • -join - Address of another agent to join upon starting up. This can be specified multiple times to specify multiple agents to join. If Consul is unable to join with any of the specified addresses, agent startup will fail. By default, the agent won't join any nodes when it starts up.
  • -retry-join - Similar to -join but allows retrying a join if the first attempt fails. This is useful for cases where we know the address will become available eventually.
  • -retry-interval - Time to wait between join attempts. Defaults to 30s.
  • -retry-max - The maximum number of -join attempts to be made before exiting with return code 1. By default, this is set to 0 which is interpreted as infinite retries.
  • -join-wan - Address of another wan agent to join upon starting up. This can be specified multiple times to specify multiple WAN agents to join. If Consul is unable to join with any of the specified addresses, agent startup will fail. By default, the agent won't -join-wan any nodes when it starts up.
  • -retry-join-wan - Similar to retry-join but allows retrying a wan join if the first attempt fails. This is useful for cases where we know the address will become available eventually.
  • -retry-interval-wan - Time to wait between -join-wan attempts. Defaults to 30s.
  • -retry-max-wan - The maximum number of -join-wan attempts to be made before exiting with return code 1. By default, this is set to 0 which is interpreted as infinite retries.
  • -log-level - The level of logging to show after the Consul agent has started. This defaults to "info". The available log levels are "trace", "debug", "info", "warn", and "err". Note that you can always connect to an agent via consul monitor and use any log level. Also, the log level can be changed during a config reload.
  • -node - The name of this node in the cluster. This must be unique within the cluster. By default this is the hostname of the machine.
  • -pid-file - This flag provides the file path for the agent to store its PID. This is useful for sending signals (for example, SIGINT to close the agent or SIGHUPto update check definite
  • -protocol - The Consul protocol version to use. This defaults to the latest version. This should be set only when upgrading. You can view the protocol versions supported by Consul by running consul -v.
  • -recursor - Specifies the address of an upstream DNS server. This option may be provided multiple times, and is functionally equivalent to the recursorsconfiguration option.
  • -rejoin - When provided, Consul will ignore a previous leave and attempt to rejoin the cluster when starting. By default, Consul treats leave as a permanent intent and does not attempt to join the cluster again when starting. This flag allows the previous state to be used to rejoin the cluster.
  • -server - This flag is used to control if an agent is in server or client mode. When provided, an agent will act as a Consul server. Each Consul cluster must have at least one server and ideally no more than 5 per datacenter. All servers participate in the Raft consensus algorithm to ensure that transactions occur in a consistent, linearizable manner. Transactions modify cluster state, which is maintained on all server nodes to ensure availability in the case of node failure. Server nodes also participate in a WAN gossip pool with server nodes in other datacenters. Servers act as gateways to other datacenters and forward traffic as appropriate.
  • -syslog - This flag enables logging to syslog. This is only supported on Linux and OSX. It will result in an error if provided on Windows.
  • -ui - Enables the built-in web UI server and the required HTTP routes. This eliminates the need to maintain the Consul web UI files separately from the binary.
  • -ui-dir - This flag provides the directory containing the Web UI resources for Consul. This will automatically enable the Web UI. The directory must be readable to the agent.

Configuration Files

In addition to the command-line options, configuration can be put into files. This may be easier in certain situations, for example when Consul is being configured using a configuration management system.

The configuration files are JSON formatted, making them easily readable and editable by both humans and computers. The configuration is formatted as a single JSON object with configuration within it.

Configuration files are used for more than just setting up the agent, they are also used to provide check and service definitions. These are used to announce the availability of system servers to the rest of the cluster. They are documented separately under check configuration and service configuration respectively. The service and check definitions support being updated during a reload.

Example Configuration File

{ "datacenter": "east-aws", "data_dir": "/opt/consul", "log_level": "INFO", "node_name": "foobar", "server": true, "watches": [ { "type": "checks", "handler": "/usr/bin/health-check-handler.sh" } ], "telemetry": { "statsite_address": "127.0.0.1:2180" } } 

Example Configuration File, with TLS

{ "datacenter": "east-aws", "data_dir": "/opt/consul", "log_level": "INFO", "node_name": "foobar", "server": true, "addresses": { "https": "0.0.0.0" }, "ports": { "https": 8080 }, "key_file": "/etc/pki/tls/private/my.key", "cert_file": "/etc/pki/tls/certs/my.crt", "ca_file": "/etc/pki/tls/certs/ca-bundle.crt" } 

Note that the use of port:

"ports": { "https": 8080 } 

Consul will not enable TLS for the HTTP API unless the https port has been assigned a port number > 0.

Configuration Key Reference

  • acl_datacenter - Only used by servers. This designates the datacenter which is authoritative for ACL information. It must be provided to enable ACLs. All servers and datacenters must agree on the ACL datacenter. Setting it on the servers is all you need for enforcement, but for the APIs to forward properly from the clients, it must be set on them too. Future changes may move enforcement to the edges, so it's best to just set acl_datacenter on all nodes.
  • acl_default_policy - Either "allow" or "deny"; defaults to "allow". The default policy controls the behavior of a token when there is no matching rule. In "allow" mode, ACLs are a blacklist: any operation not specifically prohibited is allowed. In "deny" mode, ACLs are a whitelist: any operation not specifically allowed is blocked.
  • acl_down_policy - Either "allow", "deny" or "extend-cache"; "extend-cache" is the default. In the case that the policy for a token cannot be read from theacl_datacenter or leader node, the down policy is applied. In "allow" mode, all actions are permitted, "deny" restricts all operations, and "extend-cache" allows any cached ACLs to be used, ignoring their TTL values. If a non-cached ACL is used, "extend-cache" acts like "deny".
  • acl_master_token - Only used for servers in the acl_datacenter. This token will be created with management-level permissions if it does not exist. It allows operators to bootstrap the ACL system with a token ID that is well-known.Note that the acl_master_token is only installed when a server acquires cluster leadership. If you would like to install or change theacl_master_token, set the new value for acl_master_token in the configuration for all servers. Once this is done, restart the current leader to force a leader election. If the acl_master_token is not supplied, then the servers do not create a master token. When you provide a value, it can be any string value. Using a UUID would ensure that it looks the same as the other tokens, but isn't strictly necessary.
  • acl_token - When provided, the agent will use this token when making requests to the Consul servers. Clients can override this token on a per-request basis by providing the "?token" query parameter. When not provided, the empty token, which maps to the 'anonymous' ACL policy, is used.
  • acl_ttl - Used to control Time-To-Live caching of ACLs. By default, this is 30 seconds. This setting has a major performance impact: reducing it will cause more frequent refreshes while increasing it reduces the number of caches. However, because the caches are not actively invalidated, ACL policy may be stale up to the TTL value.
  • addresses - This is a nested object that allows setting bind addresses.Both rpc and http support binding to Unix domain sockets. A socket can be specified in the form unix:///path/to/socket. A new domain socket will be created at the given path. If the specified file path already exists, Consul will attempt to clear the file and create the domain socket in its place. The permissions of the socket file are tunable via the unix_sockets config construct.

    When running Consul agent commands against Unix socket interfaces, use the -rpc-addr or -http-addr arguments to specify the path to the socket. You can also place the desired values in CONSUL_RPC_ADDR andCONSUL_HTTP_ADDR environment variables.

    For TCP addresses, these should simply be an IP address without the port. For example: 10.0.0.1, not 10.0.0.1:8500. Ports are set separately in theports structure.

    The following keys are valid:

    • dns - The DNS server. Defaults to client_addr
    • http - The HTTP API. Defaults to client_addr
    • https - The HTTPS API. Defaults to client_addr
    • rpc - The CLI RPC endpoint. Defaults to client_addr
  • advertise_addr Equivalent to the -advertise command-line flag.
  • advertise_addrs Allows to set the advertised addresses for SerfLan, SerfWan and RPC together with the port. This gives you more control than -advertiseor -advertise-wan while it serves the same purpose. These settings might override -advertise or -advertise-wanThis is a nested setting that allows the following keys:
    • serf_lan - The SerfLan address. Accepts values in the form of "host:port" like "10.23.31.101:8301".
    • serf_wan - The SerfWan address. Accepts values in the form of "host:port" like "10.23.31.101:8302".
    • rpc - The server RPC address. Accepts values in the form of "host:port" like "10.23.31.101:8300".
  • advertise_addr_wan Equivalent to the -advertise-wan command-line flag.
  • atlas_acl_token When provided, any requests made by Atlas will use this ACL token unless explicitly overridden. When not provided the acl_token is used. This can be set to 'anonymous' to reduce permission below that ofacl_token.
  • atlas_infrastructure Equivalent to the -atlas command-line flag.
  • atlas_join Equivalent to the -atlas-join command-line flag.
  • atlas_token Equivalent to the -atlas-token command-line flag.
  • atlas_endpoint Equivalent to the -atlas-endpoint command-line flag.
  • bootstrap Equivalent to the -bootstrap command-line flag.
  • bootstrap_expect Equivalent to the -bootstrap-expect command-line flag.
  • bind_addr Equivalent to the -bind command-line flag.
  • ca_file This provides a file path to a PEM-encoded certificate authority. The certificate authority is used to check the authenticity of client and server connections with the appropriate verify_incoming or verify_outgoingflags.
  • cert_file This provides a file path to a PEM-encoded certificate. The certificate is provided to clients or servers to verify the agent's authenticity. It must be provided along with key_file.
  • check_update_interval This interval controls how often check output from checks in a steady state is synchronized with the server. By default, this is set to 5 minutes ("5m"). Many checks which are in a steady state produce slightly different output per run (timestamps, etc) which cause constant writes. This configuration allows deferring the sync of check output for a given interval to reduce write pressure. If a check ever changes state, the new state and associated output is synchronized immediately. To disable this behavior, set the value to "0s".
  • client_addr Equivalent to the -client command-line flag.
  • datacenter Equivalent to the -dc command-line flag.
  • data_dir Equivalent to the -data-dir command-line flag.
  •  disable_anonymous_signature Disables providing an anonymous signature for de-duplication with the update check. Seedisable_update_check.
  • disable_remote_exec Disables support for remote execution. When set to true, the agent will ignore any incoming remote exec requests.
  • disable_update_check Disables automatic checking for security bulletins and new version releases.
  • dns_config This object allows a number of sub-keys to be set which can tune how DNS queries are serviced. See this guide on DNS caching for more detail.The following sub-keys are available:
    • allow_stale - Enables a stale query for DNS information. This allows any Consul server, rather than only the leader, to service the request. The advantage of this is you get linear read scalability with Consul servers. By default, this is false, meaning all requests are serviced by the leader, providing stronger consistency but less throughput and higher latency.
    • max_stale When allow_stale is specified, this is used to limit how stale results are allowed to be. By default, this is set to "5s": if a Consul server is more than 5 seconds behind the leader, the query will be re-evaluated on the leader to get more up-to-date results.
    • node_ttl By default, this is "0s", so all node lookups are served with a 0 TTL value. DNS caching for node lookups can be enabled by setting this value. This should be specified with the "s" suffix for second or "m" for minute.
    • service_ttl This is a sub-object which allows for setting a TTL on service lookups with a per-service policy. The "*" wildcard service can be used when there is no specific policy available for a service. By default, all services are served with a 0 TTL value. DNS caching for service lookups can be enabled by setting this value.
    • enable_truncate If set to true, a UDP DNS query that would return more than 3 records, or more than would fit into a valid UDP response, will set the truncated flag, indicating to clients that they should re-query using TCP to get the full set of records.
    • only_passing If set to true, any nodes whose health checks are warning or critical will be excluded from DNS results. If false, the default, only nodes whose healthchecks are failing as critical will be excluded. For service lookups, the health checks of the node itself, as well as the service-specific checks are considered. For example, if a node has a health check that is critical then all services on that node will be excluded because they are also considered critical.
    • udp_answer_limit - Limit the number of resource records contained in the answer section of a UDP-based DNS response. When answering a question, Consul will use the complete list of matching hosts, shuffle the list randomly, and then limit the number of answers toudp_answer_limit (default 3). In environments where RFC 3484 Section 6 Rule 9 is implemented and enforced (i.e. DNS answers are always sorted and therefore never random), clients may need to set this value to 1to preserve the expected randomized distribution behavior (note: RFC 3484has been obsoleted by RFC 6724 and as a result it should be increasingly uncommon to need to change this value with modern resolvers).
  • domain Equivalent to the -domain command-line flag.
  • enable_debug When set, enables some additional debugging features. Currently, this is only used to set the runtime profiling HTTP endpoints.
  • enable_syslog Equivalent to the -syslog command-line flag.
  • encrypt Equivalent to the -encrypt command-line flag.
  • key_file This provides a the file path to a PEM-encoded private key. The key is used with the certificate to verify the agent's authenticity. This must be provided along with cert_file.
  • http_api_response_headers This object allows adding headers to the HTTP API responses. For example, the following config can be used to enableCORS on the HTTP API endpoints:
     { "http_api_response_headers": { "Access-Control-Allow-Origin": "*" } } 
  • leave_on_terminate If enabled, when the agent receives a TERM signal, it will send a Leave message to the rest of the cluster and gracefully leave. Defaults to false.
  • log_level Equivalent to the -log-level command-line flag.
  • node_name Equivalent to the -node command-line flag.
  • ports This is a nested object that allows setting the bind ports for the following keys:
    • dns - The DNS server, -1 to disable. Default 8600.
    • http - The HTTP API, -1 to disable. Default 8500.
    • https - The HTTPS API, -1 to disable. Default -1 (disabled).
    • rpc - The CLI RPC endpoint. Default 8400.
    • serf_lan - The Serf LAN port. Default 8301.
    • serf_wan - The Serf WAN port. Default 8302.
    • server - Server RPC address. Default 8300.
  • protocol Equivalent to the -protocol command-line flag.
  • reap This controls Consul's automatic reaping of child processes, which is useful if Consul is running as PID 1 in a Docker container. If this isn't specified, then Consul will automatically reap child processes if it detects it is running as PID 1. If this is set to true or false, then it controls reaping regardless of Consul's PID (forces reaping on or off, respectively).
  • reconnect_timeout This controls how long it takes for a failed node to be completely removed from the cluster. This defaults to 72 hours and it is recommended that this is set to at least double the maximum expected recoverable outage time for a node or network partition. WARNING: Setting this time too low could cause Consul servers to be removed from quorum during an extended node failure or partition, which could complicate recovery of the cluster. The value is a time with a unit suffix, which can be "s", "m", "h" for seconds, minutes, or hours. The value must be >= 8 hours.
  • reconnect_timeout_wan This is the WAN equivalent of thereconnect_timeout parameter, which controls how long it takes for a failed server to be completely removed from the WAN pool. This also defaults to 72 hours, and must be >= 8 hours.
  • recursor Provides a single recursor address. This has been deprecated, and the value is appended to the recursors list for backwards compatibility.
  • recursors This flag provides addresses of upstream DNS servers that are used to recursively resolve queries if they are not inside the service domain for consul. For example, a node can use Consul directly as a DNS server, and if the record is outside of the "consul." domain, the query will be resolved upstream.
  • rejoin_after_leave Equivalent to the -rejoin command-line flag.
  • retry_join Equivalent to the -retry-join command-line flag. Takes a list of addresses to attempt joining every retry_interval until at least one -join works.
  • retry_interval Equivalent to the -retry-interval command-line flag.
  • retry_join_wan Equivalent to the -retry-join-wan command-line flag. Takes a list of addresses to attempt joining to WAN everyretry_interval_wan until at least one -join-wan works.
  • retry_interval_wan Equivalent to the -retry-interval-wan command-line flag.
  • server Equivalent to the -server command-line flag.
  • server_name When provided, this overrides the node_name for the TLS certificate. It can be used to ensure that the certificate name matches the hostname we declare.
  • session_ttl_min The minimum allowed session TTL. This ensures sessions are not created with TTL's shorter than the specified limit. It is recommended to keep this limit at or above the default to encourage clients to send infrequent heartbeats. Defaults to 10s.
  • skip_leave_on_interrupt This is similar to leave_on_terminate but only affects interrupt handling. When Consul receives an interrupt signal (such as hitting Control-C in a terminal), Consul will gracefully leave the cluster. Setting this to true disables that behavior. The default behavior for this feature varies based on whether or not the agent is running as a client or a server (prior to Consul 0.7 the default value was unconditionally set to false). On agents in client-mode, this defaults to false and for agents in server-mode, this defaults to true (i.e. Ctrl-C on a server will keep the server in the cluster and therefore quorum, and Ctrl-C on a client will gracefully leave).
  • start_join An array of strings specifying addresses of nodes to -join upon startup.
  • start_join_wan An array of strings specifying addresses of WAN nodes to -join-wan upon startup.
  • telemetry This is a nested object that configures where Consul sends its runtime telemetry, and contains the following keys:
    • statsd_address This provides the address of a statsd instance in the format host:port. If provided, Consul will send various telemetry information to that instance for aggregation. This can be used to capture runtime information. This sends UDP packets only and can be used with statsd or statsite.
    • statsite_address This provides the address of a statsite instance in the format host:port. If provided, Consul will stream various telemetry information to that instance for aggregation. This can be used to capture runtime information. This streams via TCP and can only be used with statsite.
    • statsite_prefix The prefix used while writing all telemetry data to statsite. By default, this is set to "consul".
    • dogstatsd_addr This provides the address of a DogStatsD instance in the format host:port. DogStatsD is a protocol-compatible flavor of statsd, with the added ability to decorate metrics with tags and event information. If provided, Consul will send various telemetry information to that instance for aggregation. This can be used to capture runtime information.
    • dogstatsd_tags This provides a list of global tags that will be added to all telemetry packets sent to DogStatsD. It is a list of strings, where each string looks like "my_tag_name:my_tag_value".
    • disable_hostname This controls whether or not to prepend runtime telemetry with the machine's hostname, defaults to false.
  • statsd_addr Deprecated, see the telemetry structure
  • statsite_addr Deprecated, see the telemetry structure
  • statsite_prefix Deprecated, see the telemetry structure
  • dogstatsd_addr Deprecated, see he telemetry structure
  • dogstatsd_tags Deprecated, see the telemetry structure
  • syslog_facility When enable_syslog is provided, this controls to which facility messages are sent. By default, LOCAL0 will be used.
  • translate_wan_addrs If set to true, Consul will prefer a node's configuredWAN address when servicing DNS requests for a node in a remote datacenter. This allows the node to be reached within its own datacenter using its local address, and reached from other datacenters using its WAN address, which is useful in hybrid setups with mixed networks. This is disabled by default.
  • ui - Equivalent to the -ui command-line flag.
  • ui_dir - Equivalent to the -ui-dir command-line flag.
  • unix_sockets - This allows tuning the ownership and permissions of the Unix domain socket files created by Consul. Domain sockets are only used if the HTTP or RPC addresses are configured with the unix:// prefix. The following options are valid within this construct and apply globally to all sockets created by Consul:
    • user - The name or ID of the user who will own the socket file.
    • group - The group ID ownership of the socket file. Note that this option currently only supports numeric IDs.
    • mode - The permission bits to set on the file.
      It is important to note that this option may have different effects on different operating systems. Linux generally observes socket file permissions while many BSD variants ignore permissions on the socket file itself. It is important to test this feature on your specific distribution. This feature is currently not functional on Windows hosts.
  • verify_incoming - If set to true, Consul requires that all incoming connections make use of TLS and that the client provides a certificate signed by the Certificate Authority from the ca_file. By default, this is false, and Consul will not enforce the use of TLS or verify a client's authenticity. This applies to both server RPC and to the HTTPS API. Note: to enable the HTTPS API, you must define an HTTPS port via the ports configuration. By default, HTTPS is disabled.
  • verify_outgoing - If set to true, Consul requires that all outgoing connections make use of TLS and that the server provides a certificate that is signed by the Certificate Authority from the ca_file. By default, this is false, and Consul will not make use of TLS for outgoing connections. This applies to clients and servers as both will make outgoing connections.
  • verify_server_hostname - If set to true, Consul verifies for all outgoing connections that the TLS certificate presented by the servers matches "server.." hostname. This implies verify_outgoing. By default, this is false, and Consul does not verify the hostname of the certificate, only that it is signed by a trusted CA. This setting is important to prevent a compromised client from being restarted as a server, and thus being able to perform a MITM attack or to be added as a Raft peer. This is new in 0.5.1.
  • watches - Watches is a list of watch specifications which allow an external process to be automatically invoked when a particular data view is updated. See the watch documentation for more detail. Watches can be modified when the configuration is reloaded.

Ports Used

Consul requires up to 5 different ports to work properly, some on TCP, UDP, or both protocols. Below we document the requirements for each port.

  • Server RPC (Default 8300). This is used by servers to handle incoming requests from other agents. TCP only.
  • Serf LAN (Default 8301). This is used to handle gossip in the LAN. Required by all agents. TCP and UDP.
  • Serf WAN (Default 8302). This is used by servers to gossip over the WAN to other servers. TCP and UDP.
  • CLI RPC (Default 8400). This is used by all agents to handle RPC from the CLI. TCP only.
  • HTTP API (Default 8500). This is used by clients to talk to the HTTP API. TCP only.
  • DNS Interface (Default 8600). Used to resolve DNS queries. TCP and UDP.

Consul will also make an outgoing connection to HashiCorp's servers for Atlas-related features and to check for the availability of newer versions of Consul. This will be a TLS-secured TCP connection to scada.hashicorp.com:7223.

Reloadable Configuration

Reloading configuration does not reload all configuration items. The items which are reloaded include:

  • Log level
  • Checks
  • Services
  • Watches
  • HTTP Client Address
  • Atlas Token
  • Atlas Infrastructure
  • Atlas Endpoint
发表在 Consul | 留下评论

consul 服务定义

https://www.consul.io/docs/agent/services.html

One of the main goals of service discovery is to provide a catalog of available services. To that end, the agent provides a simple service definition format to declare the availability of a service and to potentially associate it with a health check. A health check is considered to be application level if it associated with a service. A service is defined in a configuration file or added at runtime over the HTTP interface.

Service Definition

A service definition that is a script looks like:

{ "service": { "name": "redis", "tags": ["master"], "address": "127.0.0.1", "port": 8000, "enableTagOverride": false, "checks": [ { "script": "/usr/local/bin/check_redis.py", "interval": "10s" } ] } } 

A service definition must include a name and may optionally provide an idtags,addressportcheck, and enableTagOverride. The id is set to the name if not provided. It is required that all services have a unique ID per node, so if names might conflict then unique IDs should be provided.

The tags property is a list of values that are opaque to Consul but can be used to distinguish between "master" or "slave" nodes, different versions, or any other service level labels.

The address field can be used to specify a service-specific IP address. By default, the IP address of the agent is used, and this does not need to be provided. The portfield can be used as well to make a service-oriented architecture simpler to configure; this way, the address and port of a service can be discovered.

Services may also contain a token field to provide an ACL token. This token is used for any interaction with the catalog for the service, including anti-entropy syncs and deregistration.

A service can have an associated health check. This is a powerful feature as it allows a web balancer to gracefully remove failing nodes, a database to replace a failed slave, etc. The health check is strongly integrated in the DNS interface as well. If a service is failing its health check or a node has any failing system-level check, the DNS interface will omit that node from any service query.

The check must be of the script, HTTP, TCP or TTL type. If it is a script type, scriptand interval must be provided. If it is a HTTP type, http and interval must be provided. If it is a TCP type, tcp and interval must be provided. If it is a TTL type, then only ttl must be provided. The check name is automatically generated asservice:<service-id>. If there are multiple service checks registered, the ID will be generated as service:<service-id>:<num> where <num> is an incrementing number starting from 1.

Note: there is more information about checks here.

The enableTagOverride can optionally be specified to disable the anti-entropy feature for this service. If enableTagOverride is set to TRUE then external agents can update this service in the catalog and modify the tags. Subsequent local sync operations by this agent will ignore the updated tags. For instance: If an external agent modified both the tags and the port for this service and enableTagOverridewas set to TRUE then after the next sync cycle the service's port would revert to the original value but the tags would maintain the updated value. As a counter example: If an external agent modified both the tags and port for this service andenableTagOverride was set to FALSE then after the next sync cycle the service's port AND the tags would revert to the original value and all modifications would be lost. It's important to note that this applies only to the locally registered service. If you have multiple nodes all registering the same service their enableTagOverrideconfiguration and all other service configuration items are independent of one another. Updating the tags for the service registered on one node is independent of the same service (by name) registered on another node. If enableTagOverride is not specified the default value is false. See anti-entropy syncs for more info.

To configure a service, either provide it as a -config-file option to the agent or place it inside the -config-dir of the agent. The file must end in the ".json" extension to be loaded by Consul. Check definitions can also be updated by sending a SIGHUP to the agent. Alternatively, the service can be registered dynamically using the HTTP API.

Multiple Service Definitions

Multiple services definitions can be provided at once using the services (plural) key in your configuration file.

{ "services": [ { "id": "red0", "name": "redis", "tags": [ "master" ], "address": "127.0.0.1", "port": 6000, "checks": [ { "script": "/bin/check_redis -p 6000", "interval": "5s", "ttl": "20s" } ] }, { "id": "red1", "name": "redis", "tags": [ "delayed", "slave" ], "address": "127.0.0.1", "port": 7000, "checks": [ { "script": "/bin/check_redis -p 7000", "interval": "30s", "ttl": "60s" } ] }, ... ] } 

Service and Tag Names with DNS

Consul exposes service definitions and tags over the DNS interface. DNS queries have a strict set of allowed characters and a well-defined format that Consul cannot override. While it is possible to register services or tags with names that don't match the conventions, those services and tags will not be discoverable via the DNS interface. It is recommended to always use DNS-compliant service and tag names.

DNS-compliant service and tag names may contain any alpha-numeric characters, as well as dashes. Dots are not supported because Consul internally uses them to delimit service tags.

发表在 Consul, 服务注册和发现 | 留下评论

服务发现系统consul--配置

agent有各种各样的配置项可以在命令行或者配置文件进行定义,所有的配置项都是可选择的,当加载配置文件的时候,consul从配置文件或者配置目录加载配置。后面定义的配置会合并前面定义的配置,但是大多数情况下,合并的意思是后面定义的配置会覆盖前面定义的配置,但是有些情况,例如event句柄,合并仅仅是添加到前面定义的句柄后面。consul重新加载配置文件也支持以信号的方式接收update信号。

下面看看命令行参数:

-advertise:通知展现地址用来改变我们给集群中的其他节点展现的地址,一般情况下-bind地址就是展现地址
-bootstrap:用来控制一个server是否在bootstrap模式,在一个datacenter中只能有一个server处于bootstrap模式,当一个server处于bootstrap模式时,可以自己选举为raft leader。
-bootstrap-expect:在一个datacenter中期望提供的server节点数目,当该值提供的时候,consul一直等到达到指定sever数目的时候才会引导整个集群,该标记不能和bootstrap公用
-bind:该地址用来在集群内部的通讯,集群内的所有节点到地址都必须是可达的,默认是0.0.0.0
-client:consul绑定在哪个client地址上,这个地址提供HTTP、DNS、RPC等服务,默认是127.0.0.1
-config-file:明确的指定要加载哪个配置文件
-config-dir:配置文件目录,里面所有以.json结尾的文件都会被加载
-data-dir:提供一个目录用来存放agent的状态,所有的agent允许都需要该目录,该目录必须是稳定的,系统重启后都继续存在
-dc:该标记控制agent允许的datacenter的名称,默认是dc1
-encrypt:指定secret key,使consul在通讯时进行加密,key可以通过consul keygen生成,同一个集群中的节点必须使用相同的key
-join:加入一个已经启动的agent的ip地址,可以多次指定多个agent的地址。如果consul不能加入任何指定的地址中,则agent会启动失败,默认agent启动时不会加入任何节点。
-retry-join:和join类似,但是允许你在第一次失败后进行尝试。
-retry-interval:两次join之间的时间间隔,默认是30s
-retry-max:尝试重复join的次数,默认是0,也就是无限次尝试
-log-level:consul agent启动后显示的日志信息级别。默认是info,可选:trace、debug、info、warn、err。
-node:节点在集群中的名称,在一个集群中必须是唯一的,默认是该节点的主机名
-protocol:consul使用的协议版本
-rejoin:使consul忽略先前的离开,在再次启动后仍旧尝试加入集群中。
-server:定义agent运行在server模式,每个集群至少有一个server,建议每个集群的server不要超过5个
-syslog:开启系统日志功能,只在linux/osx上生效
-ui-dir:提供存放web ui资源的路径,该目录必须是可读的
-pid-file:提供一个路径来存放pid文件,可以使用该文件进行SIGINT/SIGHUP(关闭/更新)agent

除了命令行参数外,配置也可以写入文件中,在某些情况下配置文件会更简单一些,例如:利用consul被用来管理系统。配置文件是json格式的,很容易编写。配置文件不仅被用来设置agent的启动,也可以用来提供健康检测和服务发现的定义。配置文件的一般样例如下:

{
  "datacenter": "east-aws",
  "data_dir": "/opt/consul",
  "log_level": "INFO",
  "node_name": "foobar",
  "server": true,
  "watches": [
    {
        "type": "checks",
        "handler": "/usr/bin/health-check-handler.sh"
    }
  ]
}

下面看看详细的配置文件参数:

acl_datacenter:只用于server,指定的datacenter的权威ACL信息,所有的servers和datacenter必须同意ACL datacenter
acl_default_policy:默认是allow
acl_down_policy:
acl_master_token:
acl_token:agent会使用这个token和consul server进行请求
acl_ttl:控制TTL的cache,默认是30s
addresses:一个嵌套对象,可以设置以下key:dns、http、rpc
advertise_addr:等同于-advertise
bootstrap:等同于-bootstrap
bootstrap_expect:等同于-bootstrap-expect
bind_addr:等同于-bind
ca_file:提供CA文件路径,用来检查客户端或者服务端的链接
cert_file:必须和key_file一起
check_update_interval:
client_addr:等同于-client
datacenter:等同于-dc
data_dir:等同于-data-dir
disable_anonymous_signature:在进行更新检查时禁止匿名签名
disable_remote_exec:禁止支持远程执行,设置为true,agent会忽视所有进入的远程执行请求
disable_update_check:禁止自动检查安全公告和新版本信息
dns_config:是一个嵌套对象,可以设置以下参数:allow_stale、max_stale、node_ttl 、service_ttl、enable_truncate
domain:默认情况下consul在进行DNS查询时,查询的是consul域,可以通过该参数进行修改
enable_debug:开启debug模式
enable_syslog:等同于-syslog
encrypt:等同于-encrypt
key_file:提供私钥的路径
leave_on_terminate:默认是false,如果为true,当agent收到一个TERM信号的时候,它会发送leave信息到集群中的其他节点上。
log_level:等同于-log-level
node_name:等同于-node
ports:这是一个嵌套对象,可以设置以下key:dns(dns地址:8600)、http(http api地址:8500)、rpc(rpc:8400)、serf_lan(lan port:8301)、serf_wan(wan port:8302)、server(server rpc:8300)
protocol:等同于-protocol
recursor:
rejoin_after_leave:等同于-rejoin
retry_join:等同于-retry-join
retry_interval:等同于-retry-interval 
server:等同于-server
server_name:会覆盖TLS CA的node_name,可以用来确认CA name和hostname相匹配
skip_leave_on_interrupt:和leave_on_terminate比较类似,不过只影响当前句柄
start_join:一个字符数组提供的节点地址会在启动时被加入
statsd_addr:
statsite_addr:
syslog_facility:当enable_syslog被提供后,该参数控制哪个级别的信息被发送,默认Local0
ui_dir:等同于-ui-dir
verify_incoming:默认false,如果为true,则所有进入链接都需要使用TLS,需要客户端使用ca_file提供ca文件,只用于consul server端,因为client从来没有进入的链接
verify_outgoing:默认false,如果为true,则所有出去链接都需要使用TLS,需要服务端使用ca_file提供ca文件,consul server和client都需要使用,因为两者都有出去的链接
watches:watch一个详细名单

consul agent支持所有的网络通讯进行加密,关于加密的具体信息 这里有描述 ,有两个分开的系统,一个是gossip流量,一个是RPC。

开启gossip加密,只需要你在启动consul agent的时候设置encryption key,可以在配置文件中通过encrypt参数进行设置,key必须是16bytes长度的base64加密,也可以通过consul keygen直接产生一个。注意,在同一个集群中的所有节点必须使用同一个key。

使用TLS为RPC加密,主要是上面介绍的verify_incoming和verify_outgoing参数来设置。

发表在 Consul, 服务注册和发现 | 留下评论

Splunk

Splunk 是一款顶级的日志分析软件,如果你经常用 grep、awk、sed、sort、uniq、tail、head 来分析日志,那么你需要 Splunk。能处理常规的日志格式,比如 apache、squid、系统日志、mail.log 这些。对所有日志先进行 index,然后可以交叉查询,支持复杂的查询语句。然后通过直观的方式表现出来。日志可以通过文件方式传倒 Splunk 服务器,也可以通过网络实时传输过去。或者是分布式的日志收集。总之支持多种日志收集方法。

发表在 Splunk | 留下评论

Ganglia

Ganglia是UC Berkeley发起的一个开源集群监视项目,设计用于测量数以千计的节点。Ganglia的核心包含gmond、gmetad以及一个Web前端。主要是用来监控系统性能,如:cpu 、mem、硬盘利用率, I/O负载、网络流量情况等,通过曲线很容易见到每个节点的工作状态,对合理调整、分配系统资源,提高系统整体性能起到重要作用。
每台计算机都运行一个收集和发送度量数据的名为 gmond 的守护进程。接收所有度量数据的主机可以显示这些数据并且可以将这些数据的精简表单传递到层次结构中。正因为有这种层次结构模式,才使得 Ganglia 可以实现良好的扩展。gmond 带来的系统负载非常少,这使得它成为在集群中各台计算机上运行的一段代码,而不会影响用户性能。所有这些数据多次收集会影响节点性能。网络中的 “抖动”发生在大量小消息同时出现时,可以通过将节点时钟保持一致,来避免这个问题。
gmetad可以部署在集群内任一台节点或者通过网络连接到集群的独立主机,它通过单播路由的方式与gmond通信,收集区域内节点的状态信息,并以XML数据的形式,保存在数据库中。
由RRDTool工具处理数据,并生成相应的的图形显示,以Web方式直观的提供给客户端。
Ganglia包括如下几个程序,他们之间通过XDR(xml的压缩格式)或者XML格式传递监控数据,达到监控效果。集群内的节点,通过运行gmond收集发布节点状态信息,然后gmetad周期性的轮询gmond收集到的信息,然后存入rrd数据库,通过web服务器可以对其进行查询展示。

ganglia工作原理

ganglia主要有两个角色,gmond(ganglia monitor daemons)和gmetad(ganglia metadata daemons)。gmond是agent,需要在被监控的每台机器上部署,负责采集所在机器的系统状态,信息都是存储在内存里面的。

ganglia有一种工作模式是组播,顾名思义,以组播的形式发出自己采集到的信息。这时候集群内所有配置成组播的都可以接收数据,也就是说在组播的情形下,集群内的数据都是共享并且一致的(和路由协议很像),gmetad的功能就是从采集集群内所有系统状态信息,在组播的工作模式下,gmetad可以从任一台gmond上采集集群信息。但是组播的局限性就是在于集群要在一个网段内,并且网络负载提高。

ganglia还有一种工作模式是单播,每个agent上的gmond采集好各自的信息,然后通过udp汇总到一台gmond上,然后这台gmond汇总所有来自其他gmond的信息并且联合本机信息也发送给ganglia,单播的模式就是push,gmetad等待从gmond中心节点上过来的信息。

gmetad会把从gmond收集到的信息写入rrdtool里面,rrdtool是一个环形数据库,用来存储集群信息,然后在ganglia-web可以去读取rrdtool,并且绘图呈现给前端。

ganglia安装配置

在gmetad上得安装gmond和gmetad

for package in ganglia ganglia-devel ganglia-gmetad ganglia-gmond
do
  sudo yum -y install $package
done

配置gmetad
$ vim /etc/ganglia/gmetad.conf

data_source "Hadoop" gmond_ip
这里设置的gmetad去哪里取数据
rrd_rootdir "/var/lib/ganglia/rrds"

$ 配置gmond
$ vim /etc/ganglia/gmond.conf

...
globals {
  daemonize = yes
  setuid = yes
  user = nobody
  debug_level = 0
  max_udp_msg_len = 1472
  mute = no //设置本节点将不会再广播任何自己收集到的数据到网络上
  deaf = no //本节点将不再接收任何其他节点广播的数据包
  host_dmax = 0 /*secs */
  cleanup_threshold = 300 /*secs */
  gexec = no
  send_metadata_interval = 30
}
...
cluster {
  name = "Hadoop"
  owner = "unspecified"
  latlong = "unspecified"
  url = "unspecified"
}
...
udp_send_channel {
  port = 8649
  ttl = 1
  host = gmond_host_ip ==>> 因为是单播,需要指定IP,这个IP就是gmond中心的IP,gmetad就是从这里拉取数据
}
...

rrdtool安装配置

rrdtool用来存取gmetad收集到的所有gmond的数据

for package in rrdtool rrdtool-devel
do
  sudo yum -y install $package
done

mkdir -p /var/lib/ganglia/rrds		==>> 数据就会存在这下面
chown nobody:nobody -R  /var/lib/ganglia

ganglia-web安装配置

for package in httpd php php-gd php-common php-cli php-devel
do
  yum install $package
done

vim /etc/http/conf/httpd.conf
DocumentRoot "/var/www/"

$ wget http://jaist.dl.sourceforge.net/project/ganglia/ganglia-web/3.5.10/ganglia-web-3.5.10.tar.gz
$ tar -zxvf ganglia-web-3.5.10.tar.gz
$ mkdir -p /var/www/ganglia-web
$ cp -raf ganglia-web-3.5.10/* /var/www/ganglia-web
$ cd /var/www/ganglia-web
$ vim conf_default.php
$conf['gweb_root'] = dirname(__FILE__);
$conf['gweb_confdir'] = "/var/www/ganglia-web";
...
$conf['gmetad_root'] = "/var/lib/ganglia";
$conf['rrds'] = "/var/lib/ganglia/rrds";

启动ganglia

$ service httpd start
$ service gmond start
$ service gmetad start

* 访问 http://ip/ganglia-web

安装过程中碰到几个问题

One
呈现ganglia-web的时候出现

RRDs directory '/var/lib/ganglia/rrds' is not readable

解决方案

1.chown nobody:nobody -R RRDs /var/lib/ganglia
2.chmod 777 -R /var/lib/ganglia
3.vim /etc/php.ini
  safe_mode = Off

Two
呈现web的时候部分jss和css加载失败
* ganglia web使用graph.php来生成rrd图,调用的函数为passthru,而默认的php.ini限制了passthru的执行。
解决方案

vim /etc/php.ini
disable_functions = exec,passthru,popen,proc_open
把这个函数去了就行。
发表在 Ganglia, 监控 | 留下评论

案例剖析:从0开始搭建一个微服务的持续交付系统

本文介绍了如何利用开源软件快速搭建一套微服务的持续交付系统。本文假设的环境是

Linux操作系统,用到的软件包括Git、Jenkins、Salt、ZooKeeper、Apache等。开始之前,我先简单介绍下持续交付和微服务的概念,以便大家更好的理解本文的精华。

什么是持续交付?我们先举个物流的例子,现在各大电商都非常重视物流的自动化建设,在实现包括运输、装卸、包装、分拣、识别等作业过程的设备和设施自动化的同时,更在研究无人机和自动驾驶汽车送货,达到物流的全自动。

那么软件开发呢,从开发人员check in代码到代码仓库,到代码的构建、部署、测试、发布,我们可以形象地把这个过程称为“软件物流”,现实世界的物流实现了相当的自动化,“软件物流”也应如是,实现从开发人员check in代码(客户下单)到生产系统上线(送货上门)的自动化。

说到这里,我们可以给持续交付下一个“非专业”的定义,持续交付就是实现“软件物流”的自动化。

图1.持续交付流水线

图1摘自《持续交付:发布可靠软件的系统方法》,展示了持续交付具体包括的内容。本文重点讨论如何实现微服务的持续交付流程,所以会忽略掉整个流程的一些细节(如代码分析、单元测试等等)。

那什么是微服务呢?微服务的概念最初由Martin Fowler与James Lewis于2014年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言书写,以及不同数据存储技术,并保持最低限度的集中式管理。目前微服务的主流实现方式有两种:RESTful API和消息队列。


图2 RESTful微服务

图3 message queue微服务

图2、图3是两种典型微服务架构的简略图。当然现实中的系统会复杂的多,比如会有微服务聚合,多级缓存,注册中心等。

微服务相对单体式应用来说有明显的好处:

  1. 解决了单体式应用的复杂性问题,单个微服务很容易开发、理解和维护。
  2. 每个微服务都可以由独立的团队来开发,可以自由选择开发语言。
  3. 每个微服务可以独立部署,系统可以快速演进。
  4. 可以对每个微服务进行独立扩展,极大的提高系统伸缩性及资源利用率。

但在一个单体式应用拆分成数十个乃至上百个微服务,由于服务数量的增加,以及微服务支持多种编程语言的特性,对软件的构建,部署,测试,监控都带来了全新的挑战。本文将讨论如何通过持续交付来降低微服务构建,部署的复杂度。

 

微服务的持续交付:统一方法

 

由于微服务的特性,微服务的持续交付会比单体式应用的持续交付复杂的多。本节列出了为了降低微服务持续交付的复杂度,我们遵循的一些原则:

  1. 统一方法。这里有两个层面的含义,第一是流程的统一,有很多公司对运维自动化非常重视,但在开发,测试阶段没有采用自动化的方法。随着DevOPS运动的兴起,大家逐渐意识到需要在开发,测试阶段采用与生产环境相同的交付方法,这样在系统部署到生产环境的时候,这一交付流程已经经过多次的检验,出错的概率大大降低了。第二层含义与微服务相关,各个微服务可能用不同的语言实现,如Java、Python、C++、Golang、纯前端(JavaScript),我们要对采用不同语言实现的微服务使用统一的交付方法。
  2. 在版本控制系统中,每个微服务应该对应一个独立的仓库。以Git为例,一个Project下面,每个微服务对应一个独立Repository。这样各个微服务可以独立check in代码,而不会在持续构建的时候互相影响。
  3. 设计持续交付系统时要考虑实现软件交付的全自动化,虽然在现实中,会存在提交测试,生产变更审核等人工环节。但在理想情况下,开发人员check in 代码之后,能够自动触发构建,多套环境的部署及测试。
  4. 支持单个微服务升降级,这要求持续交付系统,对每个可部署的单元(微服务)要有独立的版本号。
  5. 程序与配置分离。要支持一套程序(可执行包+配置文件包)多处部署,这里强调了一套程序,是指在开发人员check in代码后,构建系统只生成一份程序(可执行包+配置文件包)。不管是部署到开发环境,测试环境,还是生产环境我们要用同一套程序,而不是对每个环境单独打包。我们知道Java war包会要求把配置文件包含在里面,这会造成不同的环境要求提供不同的war包,这就违反了我们说的这个原则,后面我们会讨论如何处理这个问题。
  6. 在应用程序部署时,不得依赖外网资源。我们把部署过程独立为两个阶段:环境准备阶段和应用程序部署阶段。环境准备包括操作系统,JDK或其他语言运行时系统级依赖库的安装,得益于IaaS的相对成熟,我们把这一阶段独立出来。而应用的部署需要定制化,也是本文讨论的部分。在部署应用时,要求所有的资源从内网获得,这样可以保证应用部署过程的快速、稳定、可重复。

 

快速搭建微服务的持续交付:持续构建

 

下面我们结合一个虚构的项目来介绍持续交付的实现细节,假设我们有一个项目BetaCat,由ms1、ms2…msN,n个微服务构成。下面我们重点介绍ms1微服务如何实现持续交付,其它微服务可以类推。

本节讨论下如何实现持续构建,下一节会探讨持续部署。

图4 Jenkins处理仓库代码流程

如图4所示,开发人员check in 代码到Git仓库后,Jenkins会自动地进行构建工作,并把打好的包上传到Repo server上。

图5 配置文件示例

作为统一方法的一部分,我们在每个微服务仓库上创建了CI目录,用于配置文件的打包,在CI目录里,只放入需要参数化的配置文件,执行脚本等,并会严格遵循原有系统的目录结构,如图5所示,我们要求有start.sh、stop.sh及service(用于Linux的init启停该微服务)。

图5中配置文件参数化内容,参数部分用”{{“与”}}”包围起来,在持续部署的时候会根据传入的参数替换为特定的值。

我们还定义了持续构建的统一输出,对每个微服务采用tgz的打包格式,微服务ms1持续构建的输出文件示例如下:

  • ms1-1.0.7.tgz (可执行包)
  • ms1_config-1.0.7.tgz(配置文件包)

在可执行包里面要求把所有的依赖库(除了系统lib库)都包含在里面,对不同编程语言的微服务的构建工具没有强制要求,统一由Jenkins调用。C/C++我们推荐使用CMake,Java一般用Maven,Python直接打包。

配置文件包就是前面GIT仓库的CI目录直接打包而成。

图6 Bundle示例

同时为了在部署时不用具体指定每个微服务的版本号,我们引入了bundle的概念,如图6。在任何一个微服务构建之后,会触发bundle,sha512校验文件生成,并上传到Repo Server。

最后让我们看下持续交付上传到Repo Server的目录结构:


图7 目录结构

这样持续构建的工作就完成了,接下来就需要进行持续部署了。

 

快速搭建微服务的持续交付:持续部署

 

在开始持续部署的讨论之前,我们先描述一下软件运行注入配置的三个时点:


图8 配置注入的三个时间点

打包时点,典型的是Java的war包,会把配置文件打包在一起。部署时点,在部署的时候利用专门的部署工具更新配置文件,这也是我们采用的方法;运行时点,程序运行时通过环境变量或注册中心/配置中心获得配置信息,如用Docker部署微服务时就要考虑通过这种方法来获得所需要的配置信息。


图9 采用salt进行部署

图9显示了我们对不同的环境统一采用salt进行部署。由于我们支持用户只输入bundle的版本信息来实现部署,这就要求在持续部署的时候,部署系统能自动获取每个微服务的版本号,为此我们对salt/foreman做了一点小改动,修改后返回的pillar格式包含各个微服务的版本,同时下载并解压对应的配置文件包到salt master的相应目录,以及关闭salt master file_list缓存:fileserver_list_cache_time: 0。

图10 foreman web界面以及Salt格式

图10左边表示我们在foreman web界面上设置的参数,右边表示通过salt pillar.items取得的格式,可以看到多了每个微服务的版本号信息。

下面我们按照部署三部曲(安装、配置注入、服务运行)来介绍部署规则文件(saltstate、sls文件)的编写:

1、betacat_ms1.sls 第一部分:安装


在这一部分,检查并创建安装目录,下载需要的可执行包,并解压到正确的位置,可执行包直接从Repo Server获取,并通过sha512验证文件的完整性。

2、betacat_ms1.sls 第二部分:配置注入

配置注入部分,读取配置文件包,通过salt master转换后下发给目标机。这里用红框标出了设计的核心。通过salt的file.recurse和之前持续部署中打好的配置程序包,并把所有的配置项传入。可以做到不用对多个配置文件单独编写部署逻辑,完全参数化。

3、betacat_ms1.sls 第三部分:服务运行


在这一部分,确保微服务在运行状态,并在必要的时候重启。这里需要特别指出的一点,在整个sls文件中,对不同的微服务来说,只有3个元参数:项目名称(BeatCat)、微服务名称(ms1)以及sig(ms1, 微服务进程的唯一识别字符串)。那么我们可以通过简单的脚本来自动生成sls文件,而不需要手工编写。大大降低持续部署的开发维护成本。

 

快速搭建微服务的持续交付:全自动化

 

为了支持持续交付流程的全自动化,我们引入了ZooKeeper,如图14。

图14 引入ZooKeeper后的流程

  1. 代码check in 到Git后,触发构建,Jenkins会把打好的包上传到Repository Server,并更新ZooKeeper的本次及latest包版本信息。
  2. 侦听到ZooKeeper的latest包版本信息变动后,会触发saltstack的部署命令向各个环境部署最新的程序。
  3. 部署完毕,会更新ZooKeeper上的目标机部署版本信息。
  4. 侦听到ZooKeeper上的目标机部署版本信息变动后,会触发一套或多套自动化测试脚本的运行。
  5. 自动化测试通过后,会更新ZooKeeper上的包版本的测试信息。
  6. 通过测试的包,可以自动上传到生产环境的repo server,并更新生产环境ZooKeeper的包版本信息。
  7. 生产环境,侦听到ZooKeeper的包版本信息变动后,会触发生产环境的部署。
  8. 生产环境部署完毕,会更新ZooKeeper上的目标机部署版本信息。

 

总结

 

在前面几节我们结合一个虚构的项目betacat介绍了如何实现持续交付。下面让我们来做个简单的回顾:

  1. 我们提到了统一方法,我们通过在持续构建阶段对不同语言的微服务代码输出统一的tgz包格式,并引入了bundle文件,简化了后续的持续部署的实现;我们在开发环境、测试环境、生产环境都salt/foreman来进行统一部署,实现了部署方式的统一。
  2. 我们提到了每个微服务应该对应一个独立的仓库,在实践中,也会碰到一份代码需要生成多份微服务的例子(工具类的微服务),这个需要在持续构建的时候多做点工作。也会碰到一个仓库中包含了多个需要独立部署的微服务,一般建议把它们独立出来,当然也可以在构建这些微服务时共有一个仓库,但每次代码check in都会触发多个微服务的构建。
  3. 实现软件交付的全自动化,为此我们引入了ZooKeeper,并通过各个脚本作为简单的粘合剂来实现各个环节的自动触发,配合完成整个交付的全自动化。
  4. 支持单个微服务的升降级,我们对每个微服务都有版本号,并为了简化部署,我们还引入了bundle,用户在一般情况下只要指定一个bundle的版本,就可以自动把需要的多个微服务部署到目标机上,特殊情况下,也可以具体指定每个微服务的版本号。
  5. 程序与配置分离,在开发环境与测试环境,我们用的同一个Repo server,实现多套环境的部署,生产环境的Repo也只是原始的Repo的一个简单子集。Java war包要求把配置文件打包在里面,其实在原有war包带上版本号的基础上,再配合一个配置文件包,应用统一方法更新即可。
  6. 我们提到在应用程序部署时,不得依赖外网资源。把所有的外部依赖要么在环境准备阶段准备好,要么把依赖打包在可执行包内。在持续构建时候,可以把C/C++、Java lib库包括在可执行包里面;前端比较流行的bower_components等相关文件也可以在持续构建阶段打包进可执行包。 

 

优点回顾

 

我们提出了一套利用开源软件快速搭建微服务的持续交付的方法,下面分析一下它的优点。

  1. 低侵入性在改造已有微服务项目时,我们通过在每个微服务代码仓库添加CI目录来实现配置项的参数化,最大程度上降低了对既有项目的侵入性。而在项目切换到持续交付流程时,只要保持CI目录下的配置为最新。
  2. 低维护成本用户在微服务添加配置项或配置文件时,只要往CI目录check in文件(如果是全新的配置项需要在foreman里设置),就可以实现配置文件的自动更新,不需要修改持续构建,持续部署系统本身。
  3. 低耦合得益于系统的低耦合,我们可以自由地替换各个部分,Git也可以用SVN,现有流行的持续构建工具对主流的版本控制系统都有很好的支持;Jenkins也可以替换为Teamcity或其他持续构建工具,只要保持输出到Repo的包保持一致;Salt也可以用其他持续部署工具来替换(puppet的file部署目录时source与recurse有冲突,实现salt “file.recurse”的类似效果可能会有障碍);ZooKeeper也可以用etcd或消息队列来轻松替换。
  4. 可以快速切换到Docker部署。

 

局限性反思

 

1、基于部署目标机来填写配置信息,配置过程为:

选择机器->选择要部署的微服务集合->设置配置信息。

这需要对每台机器都要配置一遍配置信息,虽然我们之前实现了配置信息的去重。更合理的方式应该是:

选择要部署的微服务集合->选择配置信息-->选择要部署的机器

微服务集合类似于k8s的pod的概念,配置信息可以多个版本并存,这样在实现配置的集中管理的同时,可以方便地实现蓝绿发布或金丝雀发布。更进一步,目标机简单可抛弃,实现不可变基础设施(Immutable Infrastructure)。

2、暂未实现数据库schema的自动变更。

 

作者介绍

 

祝小华,森浦资讯DevOPS技术负责人,十多年软件开发工作经验,先后就职于Alcatel-Lucent wireless,IBM DS8000部门从事软件开发工作。关注存储,微服务架构,容器技术及数据分析。

发表在 ci/cd | 留下评论

yii2.0框架的错误和异常处理机制

FROM : http://tech.lubanr.com/2015/12/12/yii2-0框架的错误和异常处理机制/

在应用开发中,错误和异常处理机制是一块比较重要的模块。yii框架有专门的模块来进行错误和异常处理,本文尝试从yii2.0的源码出发,对yii框架的错误和异常处理机制做一个说明。

yii2.0中,错误和异常处理最先接触到的就是 frontend/config/main.php 中的 component中的一项配置
'errorHandler' => ['errorAction'=>'site/error']

我们先记下这个配置,然后来看看yii框架的启动过程以及错误和异常处理在yii框架中的注册过程。

yii框架的入口页面只有一个: frontend/web/index.php, 所有的访问都会经过nginx重写到这个脚本上(静态文件除外)。

该文件的内容如下:

 

defined('YII_DEBUG') or define('YII_DEBUG', true);

defined('YII_ENV') or define('YII_ENV', 'dev');

require(__DIR__ . '/../../vendor/autoload.php');

require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');

require(__DIR__ . '/../../common/config/bootstrap.php');

require(__DIR__ . '/../config/bootstrap.php');

$config = yii\helpers\ArrayHelper::merge(

require(__DIR__ . '/../../common/config/main.php'),

require(__DIR__ . '/../../common/config/main-local.php'),

require(__DIR__ . '/../config/main.php'),

require(__DIR__ . '/../config/main-local.php')

);

$application = new yii\web\Application($config);

$application->run();

 

可以看出,$application是核心所在,我们来看看$application都干了些什么

(yii\base\Application 195行)

yii\web\Application在初始化的时候,会调用基类 yii\base\Application的init函数,在init中,有这样一行代码:(第204行)

$this->registerErrorHandler($config);

改行的作用是在application中注册error和exception处理。
protected function registerErrorHandler(&$config)
{
if (YII_ENABLE_ERROR_HANDLER) {
if (!isset($config['components']['errorHandler']['class'])) {
echo "Error: no errorHandler component is configured.\n";
exit(1);
}
$this->set('errorHandler', $config['components']['errorHandler']);
unset($config['components']['errorHandler']);
$this->getErrorHandler()->register();
}
}

registerErrorHandler方法的定义如上。$config中的components[‘errorHandler’][‘class’]在web\Application中的coreComponents变量定义并在preInit()函数中被merge到config中来,此处$config[‘components’][‘errorHandler’][‘class’] = yii\web\ErrorHandler,

因此registerErrorHandler($config) 最终的结果是执行了ErrorHandler类中的register()方法。

yii\web\ErrorHandler的register方法代码:(在yii\base\ErrorHandler中定义)
public function register()
{
ini_set('display_errors', false);
set_exception_handler([$this, 'handleException']);
if (defined('HHVM_VERSION')) {
set_error_handler([$this, 'handleHhvmError']);
} else {
set_error_handler([$this, 'handleError']);
}
if ($this->memoryReserveSize > 0) {
$this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
}
register_shutdown_function([$this, 'handleFatalError']);
}

该方法主要做了两个动作 set_exception_handler和set_error_handler.

我们先值考虑异常处理:异常处理由ErrorHandler实例的HandleException方法处理。
public function handleException($exception)
{
if ($exception instanceof ExitException) {
return;
}

$this->exception = $exception;

// disable error capturing to avoid recursive errors while handling exceptions

$this->unregister();

// set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent

// HTTP exceptions will override this value in renderException()

if (PHP_SAPI !== 'cli') {

http_response_code(500);

}

try {

$this->logException($exception);

if ($this->discardExistingOutput) {

$this->clearOutput();

}

$this->renderException($exception);

if (!YII_ENV_TEST) {

\Yii::getLogger()->flush(true);

if (defined('HHVM_VERSION')) {

flush();

}

exit(1);

}

} catch (\Exception $e) {

// an other exception could be thrown while displaying the exception

$msg = "An Error occurred while handling another error:\n";

$msg .= (string) $e;

$msg .= "\nPrevious exception:\n";

$msg .= (string) $exception;

if (YII_DEBUG) {

if (PHP_SAPI === 'cli') {

echo $msg . "\n";

} else {

echo '

' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset) . '

';

}

} else {

echo 'An internal server error occurred.';

}

$msg .= "\n\$_SERVER = " . VarDumper::export($_SERVER);

error_log($msg);

if (defined('HHVM_VERSION')) {

flush();

}

exit(1);

}

$this->exception = null;
}

该方法处理完之后,程序退出,可以看到 renderException决定了最终异常的展现形式。renderException是一个纯虚函数,yii\web\ErrorHandler里面有默认实现:
protected function renderException($exception)
{
if (Yii::$app->has('response')) {
$response = Yii::$app->getResponse();
// reset parameters of response to avoid interference with partially created response data
// in case the error occurred while sending the response.
$response->isSent = false;
$response->stream = null;
$response->data = null;
$response->content = null;
} else {
$response = new Response();
}
$useErrorView = $response->format === Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException);

if ($useErrorView && $this->errorAction !== null) {

$result = Yii::$app->runAction($this->errorAction);

if ($result instanceof Response) {

$response = $result;

} else {

$response->data = $result;

}

} elseif ($response->format === Response::FORMAT_HTML) {

if (YII_ENV_TEST || isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {

// AJAX request

$response->data = '

' . $this->htmlEncode($this->convertExceptionToString($exception)) . '

';

} else {

// if there is an error during error rendering it's useful to

// display PHP error in debug mode instead of a blank screen

if (YII_DEBUG) {

ini_set('display_errors', 1);

}

$file = $useErrorView ? $this->errorView : $this->exceptionView;

$response->data = $this->renderFile($file, [

'exception' => $exception,

]);

}

} elseif ($response->format === Response::FORMAT_RAW) {

$response->data = $exception;

} else {

$response->data = $this->convertExceptionToArray($exception);

}

if ($exception instanceof HttpException) {

$response->setStatusCode($exception->statusCode);

} else {

$response->setStatusCode(500);

}

$response->send();
}

已经不需要再解释什么了,这个就是大家经常看到的异常出错页面的渲染处理过程,如果我们需要定制自己的异常处理方式,需要做的就是继承yii\base\ErrorHandler,写一个定制的renderException,最后在$config中定制自己的errorHandler, 如:

‘errorHandler’=>[‘action’=>’site/error’, ‘class’=>’common\web\ErrorHandler’]

发表在 yii | 留下评论

容器和Kubernetes的应用与开发

容器就是新的进程

让我们从计算机开聊。 当计算机启动时,它会运行一个叫init的程序,然后init会启动其他所需的程序:服务器、终端、窗口管理器等。 Init能做几件有趣的事情, 例如让一个程序开机启动, 隔一段时间运行一个程序, 还有确保一个程序没有失败或者crash,如果有就重启它。 正在运行的程序可以看到这台机器上的所有东西: 其它在运行的程序,所有的文件,以及网络。

1.jpg
 

多个进程同时跑在一台计算机上。所有的进程可以自由的互相之间交互,或者与常规的资源交互。

通过将进程进行划分, 程序员可以有一个更加简单的模型来方便理解, 所以创建命名空间(namespace)的工具也被开发出来了。 程序或者进程只能看到运行在同一个命名空间下的其他进程。 如果它们寻找文件,那么只能看见硬盘上分配到这个命名空间的那一部分。 从安全的角度而言,一个命名空间里面的某个进程被黑掉了影响的仅仅也只是这个命名空间而已。

类似于Docker和Rkt这样的工具被开发出来以后使得我们能系统化地使用这些特性。 这些工具提供了打包的功能,将一个命名空间打包成一个容器,使得我们可以很方便的将它搬到另一台机器上运行,不出意外的它会跟之前完全一致的方式继续运行,因为它本身的隔离特性。 事实上,通常可以很容易的将容器想象为可以完全独立的运行的小计算机. 因为这些新的工具非常易用,它们渐渐成为一种流行的构建软件方式。

容器就是新的进程。

2.jpg
 

容器中的进程。 在这里,一个进程仅仅能够与所在同一个容器里面的其他进程和资源交互。

扩展: 一个好“难题”

一台计算机的资源是有限的,而且同时仅能处理有限的数据和运行有限的进程。 当面临增长的负载时(比如更多用户,更大的数据集)一个简单的应对方式是垂直扩展,也即是增加更多的处理能力和内存给到这台计算机,但是很快这个代价就会非常昂贵,而且本身扩展的空间也相当有限。 另一种方式就是通过增加更多的计算机来水平扩展。 这些计算机一起就组成了集群。

为了能跑在集群上,应用也需要以不同的方式架构。 例如,如果我们确认同一个程序的两份拷贝可以不需要访问对方的数据就能运行,那么我们就能放心的将它的多份拷贝放到不同的计算机上运行。

3.jpg
 

水平扩展:在这里集群里,三台计算机每台运行两个容器。 一共有两个app server的实例来处理大的负载。

虽然容器本身并没有给我们任何其他的工具来构建分布式应用,但是考虑一下这个级别上的抽象能让构建集群的应用方便一些。容器模型所鼓励的假设情形是:

可以有多份拷贝同时运行(架构要考虑并发性)。

容器可以在集群中的任意一台机器上动态启动和停止(最好是无状态或者临时的)。

计算机或者进程可能会在任意的时间点失败或者不可用但是整个系统仍然保持工作(架构要考虑失败和恢复)。

由于在集群里面有这么多的计算机要管理,我们面临一些额外挑战:

首先,我们需要管理计算机上的资源,比如处理能力和存储。这意味着我们不得不有效地分发和调度进程到不同的计算机上去执行。

我们也需要“亲和性”和方法将相关的进程放在一起跑,以便高效利用共享存储;而同时“反亲和性”的要求又需要保证对同一个资源有竞争性的进程不能运行在同一台机器上。例如,如果我们想要将应用服务器的进程跑两份来服务两倍的请求,我们可能希望他们跑在集群里两台不同的服务器上。

当许多的进程跑在不同的地方时,我们需要一种方式让他们互相发现和沟通。我们只需要某个进程运行所在的机器ip就可以与这个进程通信。

在只有一台计算机的时候,只有一个ip地址就可以了。 在有多个计算机之后,我们需要维护一个进程到ip的映射,例如像etcd这样的分布式数据库。 当一个进程在一台机器上启动时,这个信息就被加入到数据库中。 如果进程挂掉或者机器宕机,也需要将这个条目从数据库中删除。

程序员对于开发跑在一台计算机上的应用很得心应手了。 理想状态下,我们想要的是有一个工具能将集群里面所有的计算机管理起来,而展现给程序员的就像一台“巨型”的计算机。

这个方向上的一个进展是CoreOS的Fleet项目,它的基本思想就是像一台计算机上的init进程那样延伸做整个集群的init。

Google 贡献的Kubernetes项目则让我们更加接近我们想要一台”巨型”计算机的模型。

Kubernetes:pod就是新的计算机

Kubernetes做的第一件事情就是拿走你的所有计算机,然后还回给你一个”巨型”计算机--一个Kubernetes的集群。

一个Kubernetes的pod指定一组需要运行Docker或者rkt容器。

之前我们描述的是一个集群里面不同计算机上跑着不同进程,现在我们看到的是Kubernetes集群里面的不同pod里跑着不同进程。

4.jpg
 

一个Kubernetes集群围绕着pod也就是容器组构建了一个模型. 这些pod基于资源和”亲和度”的约束被动态分配到底层节点上。

之前,我们考虑的是什么进程需要在一台机器上一起运行。 现在,我们考虑将哪些进程组构造成什么pod;pod已经成为一种优美的方式来对一个应用的一个功能单元构造模型。我们甚至可以直接使用社区构造的pod,直接将他们跑起来,例如日志和监控。

一个pod里面的所有进程跑在同一台机器上,这样解决了类似挂载磁盘这样的资源共享的问题。 背后是Kubernetes将pod分配到不同的计算节点也就是kubernetes node上,我们可以给pod或者node设置发生的条件例如资源约束、亲和性等。

计算机就是资源的集合:计算能力、内存、磁盘和网络接口。与之类似,一个pod可以从底层的资源池中分配一定量的资源. 它也会有自己的网卡和pod所在的虚拟网络的ip。所以,pod就是新的计算机。

如果我们需要某个特定功能进行扩展,我们只需要在集群中多跑几个这个pod的拷贝。 当硬件不足,我们就往集群里面增加更多的计算和存储。 通过将资源与它所承载的功能解耦,调度器可以保证所有的可用资源会被尽可能高效利用。

Kubernetes复制控制器用来保证任意时间某个pod的一定数量的拷贝在运行。 就像一个分布式的init,如果一个pod挂了: 起因可能是里面的一个进程失败了,或者pod 的依赖挂了,或者它所在的节点down了; kubernetes会探测到并在另一个可用的节点上启动一个新的拷贝。

一个Kubernetes的service会跟踪集群里某种特定type的pod的所有实例。 例如,我们有一个ap server service,它会跟踪cluster里面所有的app server的pod。service是一个非常简便的抽象;我们的应用可以非常快的找到某种类型服务的所有功能单元然后将工作分发给他们。

7.jpg
 

一个完整的Kubernetes集群图

Pod被动态分配到节点上。 每一种pod对应的服务都有服务发现和负载均衡,同时也描绘了pod和服务的虚拟网络。

Kubernetes既是一个在集群里面管理和调度进程的框架,也是一种构建应用的新的思维模型,基于的是pod里面的进程分组和service所提供的服务发现。

整个生态以及未来发展

管理一台计算机已经是一个难题了。 管理一大群互相通讯的机器更是复杂得多. 感谢发明了像Docker、Kubernetes这样非凡工具的好心人,我们现在有了容器这样的简单模型,也有工具将集群管理起来就像一台计算机。 构建可扩展的应用也从没像现在这样如此简单。

容器和集群管理软件业也影响了人们构建应用的方式。 他们创造了新的模式和抽象,很多的可能性仍在探索中, 例如, 使用容器来构建可重用的应用组件或者库可能也会很有意思。 在Hasura,我们正为数据库、搜索、用户管理、文件管理等等创建组件,构建应用就只需将它们快速组装起来。

总的来说,在追求创造更简模型的道路上我们已经前进了一大步。 当今的所有软件本质就是运行代码,执行功能。 从这个角度,我们做的所有的事情仅仅是管理这些功能:将它们分组,运行它们的多份拷贝,找到并与它们交互,然后处理失败的情况。 由此推出一个逻辑结论, 或许某一天我们会有这样一个系统,我们只需要描述我们需要的功能,余下的交给系统按照描述完成即可。 那确实是求之不得啊!

FROM : http://www.chinacloud.cn/show.aspx?id=22762&cid=16

发表在 Kubernetes | 留下评论

Cgroups控制cpu,内存,io示例

FROM: http://www.cnblogs.com/yanghuahui/p/3751826.html

百度私有PaaS云就是使用轻量的cgoups做的应用之间的隔离,以下是关于百度架构师许立强,对于虚拟机VM,应用沙盒,cgroups技术选型的理解

 

 

本文用脚本运行示例进程,来验证Cgroups关于cpu、内存、io这三部分的隔离效果。

测试机器:CentOS release 6.4 (Final)

启动Cgroups

service cgconfig start   #开启cgroups服务
chkconfig cgconfig on   #开启启动

在/cgroup,有如下文件夹,默认是多挂载点的形式,即各个子系统的配置在不同的子文件夹下

[root@localhost /]# ls /cgroup/
blkio  cpu  cpuacct  cpuset  devices  freezer  memory  net_cls

 

cgroups管理进程cpu资源

跑一个耗cpu的脚本

x=0
while [ True ];do
    x=$x+1
done;

top可以看到这个脚本基本占了100%的cpu资源

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND          
30142 root      20   0  104m 2520 1024 R 99.7  0.1  14:38.97 sh

下面用cgroups控制这个进程的cpu资源

mkdir -p /cgroup/cpu/foo/   #新建一个控制组foo
echo 50000 > /cgroup/cpu/foo/cpu.cfs_quota_us  #将cpu.cfs_quota_us设为50000,相对于cpu.cfs_period_us的100000是50%
echo 30142 > /cgroup/cpu/foo/tasks

然后top的实时统计数据如下,cpu占用率将近50%,看来cgroups关于cpu的控制起了效果

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                         30142 root      20   0  105m 2884 1024 R 49.4  0.2  23:32.53 sh

cpu控制组foo下面还有其他的控制,还可以做更多其他的关于cpu的控制

[root@localhost ~]# ls /cgroup/cpu/foo/
cgroup.event_control  cgroup.procs  cpu.cfs_period_us  cpu.cfs_quota_us  cpu.rt_period_us  cpu.rt_runtime_us  cpu.shares  cpu.stat  notify_on_release  tasks

 

 

cgroups管理进程内存资源

跑一个耗内存的脚本,内存不断增长

x="a"
while [ True ];do
    x=$x$x
done;

top看内存占用稳步上升

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                         30215 root      20   0  871m 501m 1036 R 99.8 26.7   0:38.69 sh  
30215 root      20   0 1639m 721m 1036 R 98.7 38.4   1:03.99 sh 
30215 root      20   0 1639m 929m 1036 R 98.6 49.5   1:13.73 sh

下面用cgroups控制这个进程的内存资源

mkdir -p /cgroup/memory/foo
echo 1048576 >  /cgroup/memory/foo/memory.limit_in_bytes   #分配1MB的内存给这个控制组
echo 30215 > /cgroup/memory/foo/tasks

发现之前的脚本被kill掉

[root@localhost ~]# sh /home/test.sh 
已杀死

因为这是强硬的限制内存,当进程试图占用的内存超过了cgroups的限制,会触发out of memory,导致进程被kill掉。

实际情况中对进程的内存使用会有一个预估,然后会给这个进程的限制超配50%比如,除非发生内存泄露等异常情况,才会因为cgroups的限制被kill掉。

也可以通过配置关掉cgroups oom kill进程,通过memory.oom_control来实现(oom_kill_disable 1),但是尽管进程不会被直接杀死,但进程也进入了休眠状态,无法继续执行,仍让无法服务。

关于内存的控制,还有以下配置文件,关于虚拟内存的控制,以及权值比重式的内存控制等

复制代码
[root@localhost /]# ls /cgroup/memory/foo/
cgroup.event_control  memory.force_empty         memory.memsw.failcnt             
memory.memsw.usage_in_bytes      memory.soft_limit_in_bytes  memory.usage_in_bytes  tasks
cgroup.procs          memory.limit_in_bytes      memory.memsw.limit_in_bytes      
memory.move_charge_at_immigrate  memory.stat                 memory.use_hierarchy
memory.failcnt        memory.max_usage_in_bytes  memory.memsw.max_usage_in_bytes  
memory.oom_control               memory.swappiness           notify_on_release
复制代码

 

 

cgroups管理进程io资源

跑一个耗io的脚本

 dd if=/dev/sda of=/dev/null &

通过iotop看io占用情况,磁盘速度到了284M/s

30252 be/4 root      284.71 M/s    0.00 B/s  0.00 %  0.00 % dd if=/dev/sda of=/dev/null

下面用cgroups控制这个进程的io资源

mkdir -p /cgroup/blkio/foo

echo '8:0   1048576' >  /cgroup/blkio/foo/blkio.throttle.read_bps_device
#8:0对应主设备号和副设备号,可以通过ls -l /dev/sda查看
echo 30252 > /cgroup/blkio/foo/tasks

再通过iotop看,确实将读速度降到了1M/s

30252 be/4 root      993.36 K/s    0.00 B/s  0.00 %  0.00 % dd if=/dev/sda of=/dev/null

对于io还有很多其他可以控制层面和方式,如下

复制代码
[root@localhost ~]# ls /cgroup/blkio/foo/
blkio.io_merged         blkio.io_serviced      blkio.reset_stats                
blkio.throttle.io_serviced       blkio.throttle.write_bps_device   blkio.weight          cgroup.procs
blkio.io_queued         blkio.io_service_time  blkio.sectors                    
blkio.throttle.read_bps_device   blkio.throttle.write_iops_device  blkio.weight_device   notify_on_release
blkio.io_service_bytes  blkio.io_wait_time     blkio.throttle.io_service_bytes  
blkio.throttle.read_iops_device  blkio.time                        cgroup.event_control  tasks
复制代码

 

参考博文:

Cgroup与LXC简介

用cgroups管理cpu资源

百度社区私有云经验分享

发表在 docker, linux, os, 计算机基础 | 留下评论