[转]Go Context

比较好的一篇理解go Context包的文章,适合初学者

https://www.flysnow.org/2017/05/12/go-in-action-go-context.html

什么是WaitGroup

WaitGroup以前我们在并发的时候介绍过,它是一种控制并发的方式,它的这种方式是控制多个goroutine同时完成。

func main() {
	var wg sync.WaitGroup

	wg.Add(2)
	go func() {
		time.Sleep(2*time.Second)
		fmt.Println("1号完成")
		wg.Done()
	}()
	go func() {
		time.Sleep(2*time.Second)
		fmt.Println("2号完成")
		wg.Done()
	}()
	wg.Wait()
	fmt.Println("好了,大家都干完了,放工")
}

一个很简单的例子,一定要例子中的2个goroutine同时做完,才算是完成,先做好的就要等着其他未完成的,所有的goroutine要都全部完成才可以。

这是一种控制并发的方式,这种尤其适用于,好多个goroutine协同做一件事情的时候,因为每个goroutine做的都是这件事情的一部分,只有全部的goroutine都完成,这件事情才算是完成,这是等待的方式。

在实际的业务种,我们可能会有这么一种场景:需要我们主动的通知某一个goroutine结束。比如我们开启一个后台goroutine一直做事情,比如监控,现在不需要了,就需要通知这个监控goroutine结束,不然它会一直跑,就泄漏了。

chan通知

我们都知道一个goroutine启动后,我们是无法控制他的,大部分情况是等待它自己结束,那么如果这个goroutine是一个不会自己结束的后台goroutine呢?比如监控等,会一直运行的。

这种情况化,一直傻瓜式的办法是全局变量,其他地方通过修改这个变量完成结束通知,然后后台goroutine不停的检查这个变量,如果发现被通知关闭了,就自我结束。

这种方式也可以,但是首先我们要保证这个变量在多线程下的安全,基于此,有一种更好的方式:chan + select 。

func main() {
	stop := make(chan bool)

	go func() {
		for {
			select {
			case <-stop:
				fmt.Println("监控退出,停止了...")
				return
			default:
				fmt.Println("goroutine监控中...")
				time.Sleep(2 * time.Second)
			}
		}
	}()

	time.Sleep(10 * time.Second)
	fmt.Println("可以了,通知监控停止")
	stop<- true
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)

}

例子中我们定义一个stop的chan,通知他结束后台goroutine。实现也非常简单,在后台goroutine中,使用select判断stop是否可以接收到值,如果可以接收到,就表示可以退出停止了;如果没有接收到,就会执行default里的监控逻辑,继续监控,只到收到stop的通知。

有了以上的逻辑,我们就可以在其他goroutine种,给stop chan发送值了,例子中是在main goroutine中发送的,控制让这个监控的goroutine结束。

发送了stop<- true结束的指令后,我这里使用time.Sleep(5 * time.Second)故意停顿5秒来检测我们结束监控goroutine是否成功。如果成功的话,不会再有goroutine监控中...的输出了;如果没有成功,监控goroutine就会继续打印goroutine监控中...输出。

这种chan+select的方式,是比较优雅的结束一个goroutine的方式,不过这种方式也有局限性,如果有很多goroutine都需要控制结束怎么办呢?如果这些goroutine又衍生了其他更多的goroutine怎么办呢?如果一层层的无穷尽的goroutine呢?这就非常复杂了,即使我们定义很多chan也很难解决这个问题,因为goroutine的关系链就导致了这种场景非常复杂。

初识Context

上面说的这种场景是存在的,比如一个网络请求Request,每个Request都需要开启一个goroutine做一些事情,这些goroutine又可能会开启其他的goroutine。所以我们需要一种可以跟踪goroutine的方案,才可以达到控制他们的目的,这就是Go语言为我们提供的Context,称之为上下文非常贴切,它就是goroutine的上下文。

下面我们就使用Go Context重写上面的示例。

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	go func(ctx context.Context) {
		for {
			select {
			case <-ctx.Done():
				fmt.Println("监控退出,停止了...")
				return
			default:
				fmt.Println("goroutine监控中...")
				time.Sleep(2 * time.Second)
			}
		}
	}(ctx)

	time.Sleep(10 * time.Second)
	fmt.Println("可以了,通知监控停止")
	cancel()
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)

}

重写比较简单,就是把原来的chan stop 换成Context,使用Context跟踪goroutine,以便进行控制,比如结束等。

context.Background() 返回一个空的Context,这个空的Context一般用于整个Context树的根节点。然后我们使用context.WithCancel(parent)函数,创建一个可取消的子Context,然后当作参数传给goroutine使用,这样就可以使用这个子Context跟踪这个goroutine。

在goroutine中,使用select调用<-ctx.Done()判断是否要结束,如果接受到值的话,就可以返回结束goroutine了;如果接收不到,就会继续进行监控。

那么是如何发送结束指令的呢?这就是示例中的cancel函数啦,它是我们调用context.WithCancel(parent)函数生成子Context的时候返回的,第二个返回值就是这个取消函数,它是CancelFunc类型的。我们调用它就可以发出取消指令,然后我们的监控goroutine就会收到信号,就会返回结束。

Context控制多个goroutine

使用Context控制一个goroutine的例子如上,非常简单,下面我们看看控制多个goroutine的例子,其实也比较简单。

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	go watch(ctx,"【监控1】")
	go watch(ctx,"【监控2】")
	go watch(ctx,"【监控3】")

	time.Sleep(10 * time.Second)
	fmt.Println("可以了,通知监控停止")
	cancel()
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)
}

func watch(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println(name,"监控退出,停止了...")
			return
		default:
			fmt.Println(name,"goroutine监控中...")
			time.Sleep(2 * time.Second)
		}
	}
}

示例中启动了3个监控goroutine进行不断的监控,每一个都使用了Context进行跟踪,当我们使用cancel函数通知取消时,这3个goroutine都会被结束。这就是Context的控制能力,它就像一个控制器一样,按下开关后,所有基于这个Context或者衍生的子Context都会收到通知,这时就可以进行清理操作了,最终释放goroutine,这就优雅的解决了goroutine启动后不可控的问题。

《Go语言实战》读书笔记,未完待续,欢迎扫码关注公众号flysnow_org或者网站http://www.flysnow.org/,第一时间看后续笔记。觉得有帮助的话,顺手分享到朋友圈吧,感谢支持。

Context接口

Context的接口定义的比较简洁,我们看下这个接口的方法。

type Context interface {
	Deadline() (deadline time.Time, ok bool)

	Done() <-chan struct{}

	Err() error

	Value(key interface{}) interface{}
}

这个接口共有4个方法,了解这些方法的意思非常重要,这样我们才可以更好的使用他们。

Deadline方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。

Done方法返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。

Err方法返回取消的错误原因,因为什么Context被取消。

Value方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。

以上四个方法中常用的就是Done了,如果Context取消的时候,我们就可以得到一个关闭的chan,关闭的chan是可以读取的,所以只要可以读取的时候,就意味着收到Context取消的信号了,以下是这个方法的经典用法。

  func Stream(ctx context.Context, out chan<- Value) error {
  	for {
  		v, err := DoSomething(ctx)
  		if err != nil {
  			return err
  		}
  		select {
  		case <-ctx.Done():
  			return ctx.Err()
  		case out <- v:
  		}
  	}
  }

Context接口并不需要我们实现,Go内置已经帮我们实现了2个,我们代码中最开始都是以这两个内置的作为最顶层的partent context,衍生出更多的子Context。

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}

一个是Background,主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。

一个是TODO,它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。

他们两个本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。

type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
	return
}

func (*emptyCtx) Done() <-chan struct{} {
	return nil
}

func (*emptyCtx) Err() error {
	return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
	return nil
}

这就是emptyCtx实现Context接口的方法,可以看到,这些方法什么都没做,返回的都是nil或者零值。

Context的继承衍生

有了如上的根Context,那么是如何衍生更多的子Context的呢?这就要靠context包为我们提供的With系列的函数了。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

这四个With函数,接收的都有一个partent参数,就是父Context,我们要基于这个父Context创建出子Context的意思,这种方式可以理解为子Context对父Context的继承,也可以理解为基于父Context的衍生。

通过这些函数,就创建了一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个。

WithCancel函数,传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context。WithDeadline函数,和WithCancel差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。

WithTimeoutWithDeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思。

WithValue函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过Context.Value方法访问到,后面我们会专门讲。

大家可能留意到,前三个函数都返回一个取消函数CancelFunc,这是一个函数类型,它的定义非常简单。

type CancelFunc func()

这就是取消函数的类型,该函数可以取消一个Context,以及这个节点Context下所有的所有的Context,不管有多少层级。

WithValue传递元数据

通过Context我们也可以传递一些必须的元数据,这些数据会附加在Context上以供使用。

var key string="name"

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	//附加值
	valueCtx:=context.WithValue(ctx,key,"【监控1】")
	go watch(valueCtx)
	time.Sleep(10 * time.Second)
	fmt.Println("可以了,通知监控停止")
	cancel()
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)
}

func watch(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			//取出值
			fmt.Println(ctx.Value(key),"监控退出,停止了...")
			return
		default:
			//取出值
			fmt.Println(ctx.Value(key),"goroutine监控中...")
			time.Sleep(2 * time.Second)
		}
	}
}

在前面的例子,我们通过传递参数的方式,把name的值传递给监控函数。在这个例子里,我们实现一样的效果,但是通过的是Context的Value的方式。

我们可以使用context.WithValue方法附加一对K-V的键值对,这里Key必须是等价性的,也就是具有可比性;Value值要是线程安全的。

这样我们就生成了一个新的Context,这个新的Context带有这个键值对,在使用的时候,可以通过Value方法读取ctx.Value(key)

记住,使用WithValue传值,一般是必须的值,不要什么值都传递。

Context 使用原则

  1. 不要把Context放在结构体中,要以参数的方式传递
  2. 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
  3. 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
  4. Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
  5. Context是线程安全的,可以放心的在多个goroutine中传递

[转]Go net/http包

原文地址:https://studygolang.com/articles/9467

Go net/http包

这一篇就基本了解了go 的http包

Go Http客户端

get请求可以直接http.Get方法

package main

import (
	"fmt"
	"net/http"
	"log"
	"reflect"
	"bytes"
)

func main() {

	resp, err := http.Get("http://www.baidu.com")
	if err != nil {
		// handle error
		log.Println(err)
		return
	}

	defer resp.Body.Close()

	headers := resp.Header

	for k, v := range headers {
		fmt.Printf("k=%v, v=%v\n", k, v)
	}

	fmt.Printf("resp status %s,statusCode %d\n", resp.Status, resp.StatusCode)

	fmt.Printf("resp Proto %s\n", resp.Proto)

	fmt.Printf("resp content length %d\n", resp.ContentLength)

	fmt.Printf("resp transfer encoding %v\n", resp.TransferEncoding)

	fmt.Printf("resp Uncompressed %t\n", resp.Uncompressed)

	fmt.Println(reflect.TypeOf(resp.Body)) // *http.gzipReader

	buf := bytes.NewBuffer(make([]byte, 0, 512))

	length, _ := buf.ReadFrom(resp.Body)

	fmt.Println(len(buf.Bytes()))
	fmt.Println(length)
	fmt.Println(string(buf.Bytes()))
}

有时需要在请求的时候设置头参数、cookie之类的数据,就可以使用http.Do方法。

package main

import (
	"net/http"
	"strings"
	"fmt"
	"io/ioutil"
	"log"
	"encoding/json"
)

func main() {
	client := &http.Client{}

	req, err := http.NewRequest("POST", "http://www.maimaiche.com/loginRegister/login.do",
		strings.NewReader("mobile=xxxxxxxxx&isRemberPwd=1"))
	if err != nil {
		log.Println(err)
		return
	}

	req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")

	resp, err := client.Do(req)

	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Println(err)
		return
	}

	fmt.Println(resp.Header.Get("Content-Type")) //application/json;charset=UTF-8

	type Result struct {
		Msg    string
		Status string
		Obj    string
	}

	result := &Result{}
	json.Unmarshal(body, result) //解析json字符串

	if result.Status == "1" {
		fmt.Println(result.Msg)
	} else {
		fmt.Println("login error")
	}
	fmt.Println(result)
}

如果使用http POST方法可以直接使用http.Post 或 http.PostForm,

package main

import (
	"net/http"
	"strings"
	"fmt"
	"io/ioutil"
)

func main() {
	resp, err := http.Post("http://www.maimaiche.com/loginRegister/login.do",
		"application/x-www-form-urlencoded",
		strings.NewReader("mobile=xxxxxxxxxx&isRemberPwd=1"))
	if err != nil {
		fmt.Println(err)
		return
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(body))
}

http.PostForm方法,

package main

import (
	"net/http"
	"fmt"
	"io/ioutil"
	"net/url"
)

func main() {

	postParam := url.Values{
		"mobile":      {"xxxxxx"},
		"isRemberPwd": {"1"},
	}

	resp, err := http.PostForm("http://www.maimaiche.com/loginRegister/login.do", postParam)
	if err != nil {
		fmt.Println(err)
		return
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(body))
}

 

Go Http服务器端

一切的基础:ServeMux 和 Handler

Go 语言中处理 HTTP 请求主要跟两个东西相关:ServeMux 和 Handler。

ServrMux 本质上是一个 HTTP 请求路由器(或者叫多路复用器,Multiplexor)。它把收到的请求与一组预先定义的 URL 路径列表做对比,然后在匹配到路径的时候调用关联的处理器(Handler)。

处理器(Handler)负责输出HTTP响应的头和正文。任何满足了http.Handler接口的对象都可作为一个处理器。通俗的说,对象只要有个如下签名的ServeHTTP方法即可:

ServeHTTP(http.ResponseWriter, *http.Request)

Go 语言的 HTTP 包自带了几个函数用作常用处理器,比如FileServer,NotFoundHandler 和 RedirectHandler。我们从一个简单具体的例子开始:

package main

import (
	"log"
	"net/http"
)

func main() {
	mux := http.NewServeMux()

	rh := http.RedirectHandler("http://www.baidu.com", 307)
	mux.Handle("/foo", rh)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

快速地过一下代码:

  1. 在 main 函数中我们只用了 http.NewServeMux 函数来创建一个空的 ServeMux。
  2. 然后我们使用 http.RedirectHandler 函数创建了一个新的处理器,这个处理器会对收到的所有请求,都执行307重定向操作到 http://www.baidu.com。
  3. 接下来我们使用 ServeMux.Handle 函数将处理器注册到新创建的 ServeMux,所以它在 URL 路径/foo 上收到所有的请求都交给这个处理器。
  4. 最后我们创建了一个新的服务器,并通过 http.ListenAndServe 函数监听所有进入的请求,通过传递刚才创建的 ServeMux来为请求去匹配对应处理器。

 

然后在浏览器中访问 http://localhost:3000/foo,你应该能发现请求已经成功的重定向了。

明察秋毫的你应该能注意到一些有意思的事情:ListenAndServer 的函数签名是 ListenAndServe(addr string, handler Handler) ,但是第二个参数我们传递的是个 ServeMux。

我们之所以能这么做,是因为 ServeMux 也有 ServeHTTP 方法,因此它也是个合法的 Handler。

对我来说,将 ServerMux 用作一个特殊的Handler是一种简化。它不是自己输出响应而是将请求传递给注册到它的其他 Handler。这乍一听起来不是什么明显的飞跃 – 但在 Go 中将 Handler 链在一起是非常普遍的用法。

 

自定义处理器(Custom Handlers)

让我们创建一个自定义的处理器,功能是将以特定格式输出当前的本地时间:

type timeHandler struct {
	format string
}

func (th *timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	tm := time.Now().Format(th.format)
	w.Write([]byte("The time is: " + tm))
}

这个例子里代码本身并不是重点。

真正的重点是我们有一个对象(本例中就是个timerHandler结构体,但是也可以是一个字符串、一个函数或者任意的东西),我们在这个对象上实现了一个 ServeHTTP(http.ResponseWriter, *http.Request) 签名的方法,这就是我们创建一个处理器所需的全部东西。

我们把这个集成到具体的示例里:

//File: main.go

package main

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

type timeHandler struct {
	format string
}

func (th *timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	tm := time.Now().Format(th.format)
	w.Write([]byte("The time is: " + tm))
}

func main() {
	mux := http.NewServeMux()

	th := &timeHandler{format: time.RFC1123}
	mux.Handle("/time", th)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

main函数中,我们像初始化一个常规的结构体一样,初始化了timeHandler,用 & 符号获得了其地址。随后,像之前的例子一样,我们使用 mux.Handle 函数来将其注册到 ServerMux。

现在当我们运行这个应用,ServerMux 将会将任何对 /time的请求直接交给 timeHandler.ServeHTTP 方法处理。

访问一下这个地址看一下效果:http://localhost:3000/time 。

注意我们可以在多个路由中轻松的复用 timeHandler:

//File: main.go

package main

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

type timeHandler struct {
	format string
}

func (th *timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	tm := time.Now().Format(th.format)
	w.Write([]byte("The time is: " + tm))
}

func main() {
	mux := http.NewServeMux()

	th1123 := &timeHandler{format: time.RFC1123}
	mux.Handle("/time/rfc1123", th1123)

	th3339 := &timeHandler{format: time.RFC3339}
	mux.Handle("/time/rfc3339", th3339)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

 

将函数作为处理器

对于简单的情况(比如上面的例子),定义个新的有 ServerHTTP 方法的自定义类型有些累赘。我们看一下另外一种方式,我们借助 http.HandlerFunc 类型来让一个常规函数满足作为一个 Handler 接口的条件。

任何有 func(http.ResponseWriter, *http.Request) 签名的函数都能转化为一个 HandlerFunc 类型。这很有用,因为 HandlerFunc 对象内置了 ServeHTTP 方法,后者可以聪明又方便的调用我们最初提供的函数内容。

让我们使用这个技术重新实现一遍timeHandler应用:

package main

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

func timeHandler(w http.ResponseWriter, r *http.Request) {
	tm := time.Now().Format(time.RFC1123)
	w.Write([]byte("The time is: " + tm))
}

func main() {
	mux := http.NewServeMux()

	// Convert the timeHandler function to a HandlerFunc type
	th := http.HandlerFunc(timeHandler)
	// And add it to the ServeMux
	mux.Handle("/time", th)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

实际上,将一个函数转换成 HandlerFunc 后注册到 ServeMux 是很普遍的用法,所以 Go 语言为此提供了个便捷方式:ServerMux.HandlerFunc 方法。

我们使用便捷方式重写 main() 函数看起来是这样的:

package main

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

func timeHandler(w http.ResponseWriter, r *http.Request) {
	tm := time.Now().Format(time.RFC1123)
	w.Write([]byte("The time is: " + tm))
}

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("/time", timeHandler)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

绝大多数情况下这种用函数当处理器的方式工作的很好。但是当事情开始变得更复杂的时候,就会有些产生一些限制了。

你可能已经注意到了,跟之前的方式不同,我们不得不将时间格式硬编码到 timeHandler 的方法中。如果我们想从 main() 函数中传递一些信息或者变量给处理器该怎么办?

一个优雅的方式是将我们处理器放到一个闭包中,将我们要使用的变量带进去:

//File: main.go
package main

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

func timeHandler(format string) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		tm := time.Now().Format(format)
		w.Write([]byte("The time is: " + tm))
	}
	return http.HandlerFunc(fn)
}

func main() {
	mux := http.NewServeMux()

	th := timeHandler(time.RFC1123)
	mux.Handle("/time", th)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

timeHandler 函数现在有了个更巧妙的身份。除了把一个函数封装成 Handler(像我们之前做到那样),我们现在使用它来返回一个处理器。这种机制有两个关键点:

首先是创建了一个fn,这是个匿名函数,将 format 变量封装到一个闭包里。闭包的本质让处理器在任何情况下,都可以在本地范围内访问到 format 变量。

其次我们的闭包函数满足 func(http.ResponseWriter, *http.Request) 签名。如果你记得之前我们说的,这意味我们可以将它转换成一个HandlerFunc类型(满足了http.Handler接口)。我们的timeHandler 函数随后转换后的 HandlerFunc 返回。

 

在上面的例子中我们已经可以传递一个简单的字符串给处理器。但是在实际的应用中可以使用这种方法传递数据库连接、模板组,或者其他应用级的上下文。使用全局变量也是个不错的选择,还能得到额外的好处就是编写更优雅的自包含的处理器以便测试。

 

你也可能见过相同的写法,像这样:

func timeHandler(format string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		tm := time.Now().Format(format)
		w.Write([]byte("The time is: " + tm))
	})
}

或者在返回时,使用一个到 HandlerFunc 类型的隐式转换:

func timeHandler(format string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tm := time.Now().Format(format)
		w.Write([]byte("The time is: " + tm))
	}
}

 

更便利的 DefaultServeMux

你可能已经在很多地方看到过 DefaultServeMux, 从最简单的 Hello World 例子,到 go 语言的源代码中。

我花了很长时间才意识到 DefaultServerMux 并没有什么的特殊的地方。DefaultServerMux 就是我们之前用到的 ServerMux,只是它随着 net/httpp 包初始化的时候被自动初始化了而已。Go 源代码中的相关行如下:

var DefaultServeMux = NewServeMux()

net/http 包提供了一组快捷方式来配合 DefaultServeMux:http.Handle 和 http.HandleFunc。这些函数与我们之前看过的类似的名称的函数功能一样,唯一的不同是他们将处理器注册到 DefaultServerMux ,而之前我们是注册到自己创建的 ServeMux。

此外,ListenAndServe在没有提供其他的处理器的情况下(也就是第二个参数设成了 nil),内部会使用 DefaultServeMux。

因此,作为最后一个步骤,我们使用 DefaultServeMux 来改写我们的 timeHandler应用:

//File: main.go
package main

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

func timeHandler(format string) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		tm := time.Now().Format(format)
		w.Write([]byte("The time is: " + tm))
	}
	return http.HandlerFunc(fn)
}

func main() {
	// Note that we skip creating the ServeMux...

	var format string = time.RFC1123
	th := timeHandler(format)

	// We use http.Handle instead of mux.Handle...
	http.Handle("/time", th)

	log.Println("Listening...")
	// And pass nil as the handler to ListenAndServe.
	http.ListenAndServe(":3000", nil)
}

======END======

[go]json处理的一些总结

https://colobu.com/2017/06/21/json-tricks-in-Go/

本文是基于 Go 官方和 https://eager.io/blog/go-and-json/ 进行翻译整理的

JSON 是一种轻量级的数据交换格式,常用作前后端数据交换,Go 在 encoding/json 包中提供了对 JSON 的支持。

序列化

把 Go struct 序列化成 JSON 对象,Go 提供了 Marshal 方法,正如其含义所示表示编排序列化,函数签名如下:

1
func Marshal(v interface{}) ([]byte, error)

举例来说,比如下面的 Go struct:

1
2
3
4
5
type Message struct {
    Name string
    Body string
    Time int64
}

使用 Marshal 序列化:

1
2
3
m := Message{"Alice", "Hello", 1294706395881547000}
b, err := json.Marshal(m) 
fmt.Println(b) //{"Name":"Alice","Body":"Hello","Time":1294706395881547000}

在 Go 中并不是所有的类型都能进行序列化:

  • JSON object key 只支持 string
  • Channel、complex、function 等 type 无法进行序列化
  • 数据中如果存在循环引用,则不能进行序列化,因为序列化时会进行递归
  • Pointer 序列化之后是其指向的值或者是 nil

还需要注意的是:只有 struct 中支持导出的 field 才能被 JSON package 序列化,即首字母大写的 field

反序列化

反序列化函数是 Unmarshal ,其函数签名如下:

1
func Unmarshal(data []byte, v interface{}) error

如果要进行反序列化,我们首先需要创建一个可以接受序列化数据的 Go struct:

1
2
var m Message
err := json.Unmarshal(b, &m)

JSON 对象一般都是小写表示,Marshal 之后 JSON 对象的首字母依然是大写,如果序列化之后名称想要改变如何实现,答案就是 struct tags

Struct Tag

Struct tag 可以决定 Marshal 和 Unmarshal 函数如何序列化和反序列化数据。

指定 JSON filed name

JSON object 中的 name 一般都是小写,我们可以通过 struct tag 来实现:

1
2
3
type MyStruct struct {
    SomeField string `json:"some_field"`
}

SomeField 序列化之后会变成 some_field。

指定 field 是 empty 时的行为

使用 omitempty 可以告诉 Marshal 函数如果 field 的值是对应类型的 zero-value,那么序列化之后的 JSON object 中不包含此 field:

1
2
3
4
5
6
type MyStruct struct {
    SomeField string `json:"some_field,omitempty"`
}

m := MyStruct{}
b, err := json.Marshal(m) //{}

如果 SomeField == “” ,序列化之后的对象就是 {}

跳过 field

Struct tag “-” 表示跳过指定的 filed:

1
2
3
4
5
6
type MyStruct struct {
    SomeField string `json:"some_field"`
    Passwd string `json:"-"`
}
m := MyStruct{}
b, err := json.Marshal(m) //{"some_feild":""}

即序列化的时候不输出,这样可以有效保护需要保护的字段不被序列化。

反序列化任意 JSON 数据

默认的 JSON 只支持以下几种 Go 类型:

  • bool for JSON booleans
  • float64 for JSON numbers
  • string for JSON strings
  • nil for JSON null

在序列化之前如果不知道 JSON 数据格式,我们使用 interface{} 来存储。interface {} 的作用详见本博的其他文章。

有如下的数据格式:

1
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

如果我们序列化之前不知道其数据格式,我们可以使用 interface{} 来存储我们的 decode 之后的数据:

1
2
var f interface{}
err := json.Unmarshal(b, &f)

反序列化之后 f 应该是像下面这样:

1
2
3
4
5
6
7
8
f = map[string]interface{}{
    "Name": "Wednesday",
    "Age":  6,
    "Parents": []interface{}{
        "Gomez",
        "Morticia",
    },
}

key 是 string,value 是存储在 interface{} 内的。想要获得 f 中的数据,我们首先需要进行 type assertion,然后通过 range 迭代获得 f 中所有的 key :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
m := f.(map[string]interface{})
for k, v := range m {
    switch vv := v.(type) {
    case string:
        fmt.Println(k, "is string", vv)
    case float64:
        fmt.Println(k, "is float64", vv)
    case []interface{}:
        fmt.Println(k, "is an array:")
        for i, u := range vv {
            fmt.Println(i, u)
        }
    default:
        fmt.Println(k, "is of a type I don't know how to handle")
    }
}

反序列化对 slice、map、pointer 的处理

我们定义一个 struct 继续对上面例子中的 b 进行反序列化:

1
2
3
4
5
6
7
8
type FamilyMember struct {
    Name    string
    Age     int
    Parents []string
}

var m FamilyMember
err := json.Unmarshal(b, &m)

这个例子是能够正常工作的,你一定也注意到了,struct 中包含一个 slice Parents ,slice 默认是 nil,之所以反序列化可以正常进行就是因为 Unmarshal 在序列化时进行了对 slice Parents 做了初始化,同理,对 map 和 pointer 都会做类似的工作,比如序列化如果 Pointer 不是 nil 首先进行 dereference 获得其指向的值,然后再进行序列化,反序列化时首先对 nil pointer 进行初始化

Stream JSON

除了 marshal 和 unmarshal 函数,Go 还提供了 Decoder 和 Encoder 对 stream JSON 进行处理,常见 request 中的 Body、文件等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
jsonFile, err := os.Open("post.json")
if err != nil {
    fmt.Println("Error opening json file:", err)
    return
}

defer jsonFile.Close()
decoder := json.NewDecoder(jsonFile)
for {
    var post Post
    err := decoder.Decode(&post)
    if err == io.EOF {
        break
    }

    if err != nil {
        fmt.Println("error decoding json:", err)
        return
    }

    fmt.Println(post)
}

嵌入式 struct 的序列化

Go 支持对 nested struct 进行序列化和反序列化:

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
type App struct {
	Id string `json:"id"`
}

type Org struct {
	Name string `json:"name"`
}

type AppWithOrg struct {
	App
	Org
}

func main() {
	data := []byte(`
        {
            "id": "k34rAT4",
            "name": "My Awesome Org"
        }
    `)

	var b AppWithOrg

	json.Unmarshal(data, &b)
	fmt.Printf("%#v", b)

	a := AppWithOrg{
		App: App{
			Id: "k34rAT4",
		},
		Org: Org{
			Name: "My Awesome Org",
		},
	}
	data, _ = json.Marshal(a)
	fmt.Println(string(data))
}

Nested struct 虽然看起来有点怪异,有些时候它将非常有用。

自定义序列化函数

Go JSON package 中定了两个 Interface Marshaler 和 Unmarshaler ,实现这两个 Interface 可以让你定义的 type 支持序列化操作。

错误处理

总是记得检查序列或反序列化的错误,可以让你的程序更健壮,而不是在出错之后带着错误继续执行下去。

参考资料

  1. 临时忽略struct空字段
  2. 临时添加额外的字段
  3. 临时粘合两个struct
  4. 一个json切分成两个struct
  5. 临时改名struct的字段
  6. 用字符串传递数字
  7. 容忍字符串和数字互转
  8. 容忍空数组作为对象
  9. 使用 MarshalJSON支持time.Time
  10. 使用 RegisterTypeEncoder支持time.Time
  11. 使用 MarshalText支持非字符串作为key的map
  12. 使用 json.RawMessage
  13. 使用 json.Number
  14. 统一更改字段的命名风格
  15. 使用私有的字段
  16. 忽略掉一些字段
  17. 忽略掉一些字段2

taowenjson-iterator的作者。 序列化和反序列化需要处理JSON和struct的关系,其中会用到一些技巧。 原文 Golang 中使用 JSON 的小技巧是他的经验之谈,介绍了一些struct解析成json的技巧,以及 json-iterator 库的一些便利的处理。

有的时候上游传过来的字段是string类型的,但是我们却想用变成数字来使用。 本来用一个json:”,string” 就可以支持了,如果不知道golang的这些小技巧,就要大费周章了。

参考文章:http://attilaolah.eu/2014/09/10/json-and-struct-composition-in-go/

临时忽略struct空字段

1
2
3
4
5
type User struct {
Email string `json:”email”`
Password string `json:”password”`
// many more fields…
}

如果想临时忽略掉空Password字段,可以用omitempty:

1
2
3
4
5
6
json.Marshal(struct {
*User
Password bool `json:”password,omitempty”`
}{
User: user,
})

临时添加额外的字段

1
2
3
4
5
type User struct {
Email string `json:”email”`
Password string `json:”password”`
// many more fields…
}

临时忽略掉空Password字段,并且添加token字段

1
2
3
4
5
6
7
8
json.Marshal(struct {
*User
Token string `json:”token”`
Password bool `json:”password,omitempty”`
}{
User: user,
Token: token,
})

临时粘合两个struct

通过嵌入struct的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type BlogPost struct {
URL string `json:”url”`
Title string `json:”title”`
}
type Analytics struct {
Visitors int `json:”visitors”`
PageViews int `json:”page_views”`
}
json.Marshal(struct{
*BlogPost
*Analytics
}{post, analytics})

一个json切分成两个struct

1
2
3
4
5
6
7
8
9
json.Unmarshal([]byte(`{
“url”: “attila@attilaolah.eu”,
“title”: “Attila’s Blog”,
“visitors”: 6,
“page_views”: 14
}`), &struct {
*BlogPost
*Analytics
}{&post, &analytics})

临时改名struct的字段

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
type CacheItem struct {
Key string `json:”key”`
MaxAge int `json:”cacheAge”`
Value Value `json:”cacheValue”`
}
json.Marshal(struct{
*CacheItem
// Omit bad keys
OmitMaxAge omit `json:”cacheAge,omitempty”`
OmitValue omit `json:”cacheValue,omitempty”`
// Add nice keys
MaxAge int `json:”max_age”`
Value *Value `json:”value”`
}{
CacheItem: item,
// Set the int by value:
MaxAge: item.MaxAge,
// Set the nested struct by reference, avoid making a copy:
Value: &item.Value,
})

用字符串传递数字

1
2
3
type TestObject struct {
Field1 int `json:”,string”`
}

这个对应的json是 {"Field1": "100"}

如果json是 {"Field1": 100} 则会报错

容忍字符串和数字互转

如果你使用的是jsoniter,可以启动模糊模式来支持 PHP 传递过来的 JSON。

1
2
3
import “github.com/json-iterator/go/extra”
extra.RegisterFuzzyDecoders()

这样就可以处理字符串和数字类型不对的问题了。比如

1
2
var val string
jsoniter.UnmarshalFromString(`100`, &val)

又比如

1
2
var val float32
jsoniter.UnmarshalFromString(`”1.23″`, &val)

容忍空数组作为对象

PHP另外一个令人崩溃的地方是,如果 PHP array是空的时候,序列化出来是[]。但是不为空的时候,序列化出来的是{"key":"value"}。 我们需要把 [] 当成 {} 处理。

如果你使用的是jsoniter,可以启动模糊模式来支持 PHP 传递过来的 JSON。

1
2
3
import “github.com/json-iterator/go/extra”
extra.RegisterFuzzyDecoders()

这样就可以支持了

1
2
var val map[string]interface{}
jsoniter.UnmarshalFromString(`[]`, &val)

使用 MarshalJSON支持time.Time

golang 默认会把 time.Time 用字符串方式序列化。如果我们想用其他方式表示 time.Time,需要自定义类型并定义 MarshalJSON

1
2
3
4
5
6
type timeImplementedMarshaler time.Time
func (obj timeImplementedMarshaler) MarshalJSON() ([]byte, error) {
seconds := time.Time(obj).Unix()
return []byte(strconv.FormatInt(seconds, 10)), nil
}

序列化的时候会调用 MarshalJSON

1
2
3
4
5
6
7
8
9
type TestObject struct {
Field timeImplementedMarshaler
}
should := require.New(t)
val := timeImplementedMarshaler(time.Unix(123, 0))
obj := TestObject{val}
bytes, err := jsoniter.Marshal(obj)
should.Nil(err)
should.Equal(`{“Field”:123}`, string(bytes))

使用 RegisterTypeEncoder支持time.Time

jsoniter 能够对不是你定义的type自定义JSON编解码方式。比如对于 time.Time 可以用 epoch int64 来序列化

1
2
3
4
5
import “github.com/json-iterator/go/extra”
extra.RegisterTimeAsInt64Codec(time.Microsecond)
output, err := jsoniter.Marshal(time.Unix(1, 1002))
should.Equal(“1000001”, string(output))

如果要自定义的话,参见 RegisterTimeAsInt64Codec 的实现代码

使用 MarshalText支持非字符串作为key的map

虽然 JSON 标准里只支持 string 作为 key 的 map。但是 golang 通过 MarshalText() 接口,使得其他类型也可以作为 map 的 key。例如

1
2
3
4
f, _, _ := big.ParseFloat(“1”, 10, 64, big.ToZero)
val := map[*big.Float]string{f: “2”}
str, err := MarshalToString(val)
should.Equal(`{“1″:”2”}`, str)

其中 big.Float 就实现了 MarshalText()

使用 json.RawMessage

如果部分json文档没有标准格式,我们可以把原始的信息用[]byte保存下来。

1
2
3
4
5
6
7
type TestObject struct {
Field1 string
Field2 json.RawMessage
}
var data TestObject
json.Unmarshal([]byte(`{“field1”: “hello”, “field2”: [1,2,3]}`), &data)
should.Equal(` [1,2,3]`, string(data.Field2))

使用 json.Number

默认情况下,如果是 interface{} 对应数字的情况会是 float64 类型的。如果输入的数字比较大,这个表示会有损精度。所以可以 UseNumber() 启用 json.Number 来用字符串表示数字。

1
2
3
4
5
decoder1 := json.NewDecoder(bytes.NewBufferString(`123`))
decoder1.UseNumber()
var obj1 interface{}
decoder1.Decode(&obj1)
should.Equal(json.Number(“123”), obj1)

jsoniter 支持标准库的这个用法。同时,扩展了行为使得 Unmarshal 也可以支持 UseNumber 了。

1
2
3
4
json := Config{UseNumber:true}.Froze()
var obj interface{}
json.UnmarshalFromString(“123”, &obj)
should.Equal(json.Number(“123”), obj)

统一更改字段的命名风格

经常 JSON 里的字段名 Go 里的字段名是不一样的。我们可以用 field tag 来修改。

1
2
3
4
5
6
7
8
output, err := jsoniter.Marshal(struct {
UserName string `json:”user_name”`
FirstLanguage string `json:”first_language”`
}{
UserName: “taowen”,
FirstLanguage: “Chinese”,
})
should.Equal(`{“user_name”:”taowen”,”first_language”:”Chinese”}`, string(output))

但是一个个字段来设置,太麻烦了。如果使用 jsoniter,我们可以统一设置命名风格。

1
2
3
4
5
6
7
8
9
10
11
12
import “github.com/json-iterator/go/extra”
extra.SetNamingStrategy(LowerCaseWithUnderscores)
output, err := jsoniter.Marshal(struct {
UserName string
FirstLanguage string
}{
UserName: “taowen”,
FirstLanguage: “Chinese”,
})
should.Nil(err)
should.Equal(`{“user_name”:”taowen”,”first_language”:”Chinese”}`, string(output))

使用私有的字段

Go 的标准库只支持 public 的 field。jsoniter 额外支持了 private 的 field。需要使用 SupportPrivateFields() 来开启开关。

1
2
3
4
5
6
7
8
9
import “github.com/json-iterator/go/extra”
extra.SupportPrivateFields()
type TestObject struct {
field1 string
}
obj := TestObject{}
jsoniter.UnmarshalFromString(`{“field1″:”Hello”}`, &obj)
should.Equal(“Hello”, obj.field1)

下面是我补充的内容

忽略掉一些字段

原文中第一节有个错误,我更正过来了。omitempty不会忽略某个字段,而是忽略空的字段,当字段的值为空值的时候,它不会出现在JSON数据中。

如果想忽略某个字段,需要使用 json:"-"格式。

1
2
3
4
5
type User struct {
Email string `json:”email”`
Password string `json:”password”`
// many more fields…
}

如果想临时忽略掉空Password字段,可以用-:

1
2
3
4
5
6
json.Marshal(struct {
*User
Password bool `json:”-“`
}{
User: user,
})

忽略掉一些字段2

如果不想更改原struct,还可以使用下面的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type User struct {
Email string `json:”email”`
Password string `json:”password”`
// many more fields…
}
type omit *struct{}
type PublicUser struct {
*User
Password omit `json:”-“`
}
json.Marshal(PublicUser{
User: user,
})

python抓取的一些总结

python作为爬虫的最常见语言,很有自己的优势。这里举一些常见的用法。

1,使用scrapy框架。

https://scrapy-chs.readthedocs.io/zh_CN/0.24/intro/tutorial.html

2, 纯脚本

lib库

from concurrent.futures import ThreadPoolExecutor

from bs4 import BeautifulSoup

import Queue, time, random

import requests

提供一个比较粗糙的代码,使用到了python代理,queue,多线程,BeautifulSoup。最终print方法应该用线程锁避免打印错误。


#coding=utf-8
import requests
from concurrent.futures import ThreadPoolExecutor
import Queue, time, random
#import pymysql
from bs4 import BeautifulSoup
import re
import urllib
import urllib2
import gzip
import cStringIO
import datetime
import json
from StringIO import StringIO
import threading

import base64

index_url = 'https://www.juzimi.com/alltags'
page_url = 'https://www.juzimi.com/alltags?page=%s'

task_queue = Queue.Queue()

has_words = []
has_words_value = {}

lock=threading.Lock()

ip = ""
def getIp() :
global ip

url = 'http://s.zdaye.com/?api=201903221353043521&count=1&px=2'
ipret = curl(url, '', False, False)
time.sleep(5)
print "get ip:" + str(ipret)
ip = str(ipret)
def curl(url, data = '', isCompress = False, use_ip = True):

global ip
if(data):
data = urllib.urlencode(data)
else:
data = None
#headers = {"method":"GET","user-agent":self.ua,"Referer":self.refer, "Cookie":self.cookie, "Upgrade-Insecure-Requests": 1,"Accept-Encoding": "gzip, deflate, br"}
headers = {"method":"GET","Accept-Encoding": "gzip, deflate, br", "user-agent" : "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36"}

try :

#代理代码
if(use_ip) :
opener = urllib2.build_opener(urllib2.ProxyHandler({"https" : ip}))
urllib2.install_opener(opener)

request = urllib2.Request(url, data, headers)
response = urllib2.urlopen(request, timeout=10)

#response = opener.open(request)
if(isCompress) :
buf = StringIO(response.read())
data = gzip.GzipFile(fileobj=buf)
return data.read()
return response.read()

except :
exit()
getIp()
print "get ip retry"
self.curl(url, data, isCompress, use_ip)

#exit()

def setTaskR() :
task_queue.put({'url':'https://www.juzimi.com/tags/%E6%96%B0%E8%AF%97', 'title':'诗词'})

def setTask() :
#for i in range(0, 12) :
for i in range(0, 12) :
url = page_url % (i)
content = curl(url, '', True)
#print url
soup = BeautifulSoup(content, "html.parser")
span = soup.findAll('div',{'class':'views-field-name'})
for tmp in span :
data = {}
href = tmp.find('a').get('href')
title = tmp.find('a').get('title')
data = {"url":"https://www.juzimi.com" + href, "title" : title}
print data
task_queue.put(data)
#print tmp.get('title')
time.sleep(1)

def getFile() :

global has_words
global has_words_value

for line in open('word.log') :
if(line.find('juzimi.com') != -1) :
continue
line = line.split(":", 1)
if(len(line) > 1 and line[1] == 'test') :
continue

#print line[0]
if(not line[0] in has_words) :
has_words.append(line[0])
has_words_value[line[0]] = 1
#print line[0]
else :
has_words_value[line[0]] = has_words_value[line[0]] + 1
has_words = []
for k in has_words_value:
if(has_words_value[k] > 100) :
has_words.append(k)
for line in open('word.url') :
lines = eval(line)
url = lines['url']
title = lines['title'].encode('utf-8')
if(title in has_words) :
continue
task_queue.put(lines)

#runTask()
sleep_time = random.randint(30,60)
#time.sleep(sleep_time)
#time.sleep(60)

def runTask() :
while(not task_queue.empty()) :
data = task_queue.get()
printinfo =[]
hotword = data['title']
url = data['url']
hotword = hotword.encode('utf-8')
#print url
lastIndex = 0
content = curl(url, '', True)
#content = re.sub(r'\n|&nbsp|\xa0|\\xa0|\u3000|\\u3000|\\u0020|\u0020', '', str(content))
content = content.replace('<br/>', '')
content = content.replace('\r', ' ')
soup = BeautifulSoup(content, "html.parser")
last = soup.find('li', {'class', 'pager-last'})
if (not last) :

last = soup.findAll('li', {'class' : 'pager-item'})
if(not last) :
print "get empty:" + url
continue
for tmp in last :
if(int(tmp.text) > lastIndex) :
#print int(tmp.text)
lastIndex = int(tmp.text)
else :
lastIndex = last.text

span = soup.findAll('div',{'class', 'views-field-phpcode-1'})

if(not span) :
print "get empty:" + url
continue

#print url
for tmp in span :
words = tmp.findAll('a')
for word in words :
#printinfo.append({'hotword' : hotword, 'content' : word.text.encode('utf-8')})
print hotword + ":" + word.text.encode('utf-8')
#time.sleep(3)
sleep_time = random.randint(10,20)
#time.sleep(sleep_time)
for i in range(1, int(lastIndex)) :

url = "https://www.juzimi.com/tags/" +hotword + "?page=" + str(i)
#ret = getContent(url, hotword)

t = threading.Thread(target=getContent, args=(url, hotword))
t.start()
"""
for tmp in ret:
printinfo.append(tmp)
"""

"""
for tmp in printinfo :
print tmp['hotword'] + ":" + tmp['content']
"""

def getContent(url, hotword) :
printinfo =[]
#print url
content = curl(url, '', True)
#content = re.sub(r'\n|&nbsp|\xa0|\\xa0|\u3000|\\u3000|\\u0020|\u0020', '', str(content))
content = content.replace('<br/>', '')
content = content.replace('\r', ' ')
soup = BeautifulSoup(content, "html.parser")
last = soup.find('li', {'class', 'pager-last'})
span = soup.findAll('div',{'class', 'views-field-phpcode-1'})
for tmp in span :
words = tmp.findAll('a')
for word in words :
#printinfo.append({'hotword' : hotword, 'content' : word.text.encode('utf-8')})
print hotword + ":" + word.text.encode('utf-8')
sleep_time = random.randint(20,30)
#time.sleep(sleep_time)
#return printinfo

getIp()

getFile()

"""

#setTaskR()
#runTask()
#exit()
"""
executor = ThreadPoolExecutor(max_workers = 50)
#executor.submit(runTask)
#exit()
for i in range(0, 20) :
executor.submit(runTask)

&nbsp;

 

[转]python中str字符串和unicode对象字符串的拼接问题

一个很基本的问题,遇到了,这里记录下

str字符串

s = ‘中文’ # s: <type ‘str’>
s是个str对象,中文字符串。存储方式是字节码。字节码是怎么存的:

如果这行代码在python解释器中输入&运行,那么s的格式就是解释器的编码格式;

如果这行代码是在源码文件中写入、保存然后执行,那么解释器载入代码时就将s初始化为文件指定编码(比如py文件开头那行的utf-8);

unicode对象字符串

unicode是一种编码标准,具体的实现可能是utf-8,utf-16,gbk等等,这就是中文字符串和unicode有密切关系的原因。

python内部使用两个字节存储一个unicode对象(unicode对象并不只能是字符串,这两个字节还可以存其他内容),为什么要用unicode而不用str呢,因为中文转码的缘故,因为unicode的优点是便于跨平台。

s1 = u’中文’ # s1: <type ‘unicode’>
s2 = unicode(‘中文’, ‘utf-8’) # utf8是在指定解码方式, s2: <type ‘unicode’>
str字符串和unicode字符串拼接

只要注意正确的decode、encode方式,统一编码后就能顺利地拼接了。

# -*- coding: utf-8 -*-

s1 = ‘中文’
s2 = u’你好’
print s1 + unicode(s2, ‘utf-8’) # 中文你好
print s1 + s2.decode(‘utf-8’) # 中文你好
print s1.encode(‘utf-8’) + s2 # 中文你好

print type(s1) # <type ‘str’>
print type(s2) # <type ‘unicode’>
print type(s1.decode(‘utf-8’)) # <type ‘unicode’>
print type(s2.encode(‘utf-8’)) # <type ‘str’>
对于str要注意当前环境编码方式,也许是控制台那种设定好了的,也许是你自己在代码中指定的。(看你的代码是在哪里敲的了)
对于unicode对象,一般都是decode得到的,像直接【u’你好’】这种其实不是很常见,所以要注意字符串来源是什么编码,比如从gbk文件或utf8文件中读入的。
———————
作者:Joy_Shen
来源:CSDN
原文:https://blog.csdn.net/index20001/article/details/78974814
版权声明:本文为博主原创文章,转载请附上博文链接!

一款跨平台的Web目录扫描工具

工具是由安全盒子王松编写 是用Python编写的  其中包含脚本识别 后台地址 以及乌云抓的常见漏洞url

这对于Ubuntu系统的用户 特别方便  Windows上也有很多的扫描目录的好工具 比如御剑 wwwscan 等等很多

我自己加入了一点点字典

亲测

从github上下载即可

[AppleScript] 纯文本查看 复制代码
1
2
3
4
5
git clone [url]https://github.com/Strikersb/webdirscan[/url]
cd webdirscan
python webdirscan.py  url

报错的话 安装的第三方Python模块成功解决

[AppleScript] 纯文本查看 复制代码
1
pip install requests

运行图如下

[AppleScript] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
root@xaiSec:/home/hacker# cd webdirscan
root@xaiSec:/home/hacker/webdirscan# python webdirscan.py -h
usage: webdirscan.py [-h] [-d SCANDICT] [-o SCANOUTPUT] [-t THREADNUM]
                     scanSite
positional arguments:
  scanSite              The website to be scanned
optional arguments:
  -h, --help            show this help message and exit
  -d SCANDICT, --dict SCANDICT
                        Dictionary for scanning
  -o SCANOUTPUT, --output SCANOUTPUT
                        Results saved files
  -t THREADNUM, --thread THREADNUM
                        Number of threads running the program
root@xaiSec:/home/hacker/webdirscan#

翻译后

[AppleScript] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
用法:webdirscan.py [-h] [-d SCANDICT] [-o SCANOUTPUT] [-t THREADNUM]
                      scanSite
位置参数:
   scanSite要扫描的网站
可选参数:
   -h,--help显示此帮助消息并退出
   -d SCANDICT,--dict SCANDICT
                         扫描词典
   -o SCANOUTPUT,--output SCANOUTPUT
                         结果保存的文件
   -t THREADNUM, - thread THREADNUM
                         运行程序的线程数

首先我们先来扫描一个网站 比如我们来扫描下百度= =  嘿嘿
输出结果为以下

[AppleScript] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
root@xaiSec:/home/hacker/webdirscan# python webdirscan.py [url]www.baidu.com[/url]
Dirscan is running!
Scan target: [url]http://www.baidu.com[/url]
Total Dictionary: 85025
[200]http://www.baidu.com/robots.txt
[200]http://www.baidu.com/index.htm
[200]http://www.baidu.com/index.php
[200]http://www.baidu.com/index.html
[200]http://www.baidu.com/index.html
[200]http://www.baidu.com/cache/
[200]http://www.baidu.com/index.htm
[200]http://www.baidu.com/robots.txt

 

whistle代理调试工具

首先看官方文档:http://wproxy.org/whistle/

whistle(读音[ˈwɪsəl],拼音[wēisǒu])基于Node实现的跨平台web调试代理工具,类似的工具有Windows平台上的Fiddler,主要用于查看、修改HTTP、HTTPS、Websocket的请求、响应,也可以作为HTTP代理服务器使用,不同于Fiddler通过断点修改请求响应的方式,whistle采用的是类似配置系统hosts的方式,一切操作都可以通过配置实现,支持域名、路径、正则表达式、通配符、通配路径等多种匹配方式,且可以通过Node模块扩展功能

这里简单介绍下安装

安装启动

安装启动whistle,需要以下四个步骤: 安装Node安装whistle启动whistle配置代理

1. 安装Node

whistle支持v0.10.0以上版本的Node,为获取更好的性能,推荐安装最新版本的Node。

如果你的系统已经安装了v0.10.0以上版本的Node,可以忽略此步骤,直接进入安装whistle的步骤,否则:

  1. Windows或Mac系统,访问https://nodejs.org/,安装LTS版本的Node,默认安装即可。
  2. Linux下推荐使用源码安装: 从Node官网下载最新版的Source Code(或者用wget命令下载),解压文件(tar -xzvf node-vx.y.z.tar.gz)后进入解压后的根目录(node-vx.y.z),依次执行./configure./make./make install

安装完Node后,执行下面命令,查看当前Node版本

$ node -v
v4.4.0

如果能正常输出Node的版本号,表示Node已安装成功(Windows系统可能需要重新打开cmd)。

2. 安装whistle

Node安装成功后,执行如下npm命令安装whistle (Mac或Linux的非root用户需要在命令行前面加sudo,如:sudo npm install -g whistle

$ npm install -g whistle

npm默认镜像是在国外,有时候安装速度很慢或者出现安装不了的情况,如果无法安装或者安装很慢,可以使用taobao的镜像安装:

$ npm install cnpm -g --registry=https://registry.npm.taobao.org
$ cnpm install -g whistle

或者直接指定镜像安装:
$ npm install whistle -g --registry=https://registry.npm.taobao.org

whistle安装完成后,执行命令 whistle help 或 w2 help,查看whistle的帮助信息

$ w2 help

 Usage: whistle <command> [options]


  Commands:

    status              Show the running status of whistle
    use/add [filepath]  Set rules from a specified js file (.whistle.js by default)
    run                 Start a front service
    start               Start a background service
    stop                Stop current background service
    restart             Restart current background service
    help                Display help information

  Options:

    -h, --help                                      output usage information
    -D, --baseDir [baseDir]                         set the configured storage root path
    -z, --certDir [directory]                       set custom certificate store directory
    -l, --localUIHost [hostname]                    set the domain for the web ui of whistle (local.whistlejs.com by default)
    -L, --pluginHost [hostname]                     set the domain for the web ui of plugin  (as: "script=a.b.com&vase=x.y.com")
    -n, --username [username]                       set the username to access the web ui of whistle
    -w, --password [password]                       set the password to access the web ui of whistle
    -N, --guestName [username]                      set the the guest name to access the web ui of whistle (can only view the data)
    -W, --guestPassword [password]                  set the guest password to access the web ui of whistle (can only view the data)
    -s, --sockets [number]                          set the max number of cached long connection on each domain (60 by default)
    -S, --storage [newStorageDir]                   set the configured storage directory
    -C, --copy [storageDir]                         copy the configuration of the specified directory to a new directory
    -c, --dnsCache [time]                           set the cache time of DNS (30000ms by default)
    -H, --host [host]                               set the listening host (INADDR_ANY by default)
    -p, --port [port]                               set the listening port (8899 by default)
    -P, --uiport [uiport]                           set the listening port of whistle (8900 by default)
    -m, --middlewares [script path or module name]  set the express middlewares loaded at startup (as: xx,yy/zz.js)
    -M, --mode [mode]                               set the way of starting the whistle mode (as: pureProxy|debug|multiEnv)
    -u, --uipath [script path]                      set the path of custom web ui
    -t, --timeout [ms]                              set the request timeout (66000ms by default)
    -e, --extra [extraData]                         set the extra parameters for plugin
    -f, --secureFilter [secureFilter]               set the path of secure filter
    -R, --reqCacheSize [reqCacheSize]               set the cache size of request data (600 by default)
    -F, --frameCacheSize [frameCacheSize]           set the cache size of webSocket and socket's frames (512 by default)
    -V, --version                                   output the version number

如果能正常输出whistle的帮助信息,表示whistle已安装成功。

3. 启动whistle

最新版本的whistle支持三种等价的命令whistlew2wproxy

启动whistle:

$ w2 start

Note: 如果要防止其他人访问配置页面,可以在启动时加上登录用户名和密码 -n yourusername -w yourpassword

重启whsitle:

$ w2 restart

停止whistle:

$ w2 stop

调试模式启动whistle(主要用于查看whistle的异常及插件开发):

$ w2 run

启动完whistle后,最后一步需要配置代理。

4. 配置代理

配置信息
  1. 代理服务器:127.0.0.1 (如果部署在远程服务器或虚拟机上,改成对应服务器或虚拟机的ip即可)
  2. 默认端口:8899 (如果端口被占用,可以在启动是时通过 -p 来指定新的端口,更多信息可以通过执行命令行 w2 help (v0.7.0及以上版本也可以使用w2 help) 查看)

勾选上 对所有协议均使用相同的代理服务器

代理配置方式(把上面配置信息配置上即可)
  1. 全局代理:直接配置系统代理:
    • Windows
    • Mac: System Preferences > Network > Advanced > Proxies > HTTP or HTTPS
       

      * Linux: Settings > Network > VPN > Network Proxy > Manual

       
  2. 浏览器代理:安装浏览器代理插件 (推荐)
    • 安装Chrome代理插件:推荐安装SwitchyOmega

      * Firefox: 地址栏输入访问 `about:preferences`,找到 `Network Proxy`,选择 `手动代理配置(Manual proxy configuration)`,输入代理服务器地址、端口,保存

       
  3. 移动端需要在设置中配置当前Wi-Fi的代理,以 iOS 为例:

PS: 如果配置完代理,手机无法访问,可能是whistle所在的电脑防火墙限制了远程访问whistle的端口,关闭防火墙或者设置白名单:http://jingyan.baidu.com/article/870c6fc317cae7b03ee4be48.html

访问配置页面

启动whistle及配置完代理后,用Chrome浏览器(由于css兼容性问题界面只支持Chrome浏览器)访问配置页面,如果能正常打开页面,whistle安装启动完毕,可以开始使用。

可以通过以下两种方式来访问配置页面:

[转]百度的春晚战事(猜猜哪个是博主)

百度的春晚战事

原创: 史中 浅黑科技 浅黑科技 今天

编辑

请点击输入图片描述

浅友们大家好~我是史中,我的日常生活是开撩五湖四海的科技大牛,我会尝试各种姿势,把他们的无边脑洞和温情故事讲给你听。如果你特别想听到谁的故事,不妨加微信(微信号:shizhongpro)告诉我。

百度的春晚战事

文 | 史中

我们对春晚一无所知。

罗振宇曾在跨年演讲上如是说。

无论悲喜,反正每个中国人都为春晚辟出了一块“专属记忆”。而从2015年开始,中国人的春晚记忆里被点上了一颗“红痣”。那就是——总有一家顶尖互联网公司面带羞赧地走上舞台,给十几亿人发红包。

“一无所知”的形容,可谓精妙。春晚时,你只知道自己在对着电视刷红包,但从空中俯瞰,十多亿人同时拿起手机,将会汇聚起怎样一种数据海啸,即使《2012》《后天》里的那种惊天排浪,也难以企及十分之一。

据说,美国“超级碗”直播中间插播广告的时候,电视机前的几亿观众会集体上厕所冲马桶,导致美国各大城市的市政供水出现崩溃。超级碗直播的全球观众有1.3亿。而我们的春晚直播,全球观众有13亿。

2015年,微信曾经上春晚发过红包,在全国观众的冲击下一度跪倒长达一小时,俯首拜年。

 

2018年,淘宝为春晚准备了三倍于“双11”的服务器资源。而就在主持人口播活动开始的一瞬间,服务器瞬间超过负荷。事实证明,春晚观众肉身涌进淘宝服务器的瞬时流量是当年“双11”的15倍。

精明的腾讯阿里,都是提前三个月准备春晚,尚且如此狼狈。

而2019年,央视春晚红包招标时间很晚,距离除夕只有一个多月的时间。巨头们都觉得凶险异常,百度却高高举手:我来!我来!

所有吃瓜群众都侧目,这种“情商低”的状态,还真是百度的风格。。。

接下来中哥就告诉你,2019年2月4日除夕晚上,这片土地上究竟发生了什么。

编辑

请点击输入图片描述

(零)百度的一封信、一部引擎和一场战役 

故事,要从一封信说起。

2018年12月18日,李彦宏突然发布了一封内部信,宣布了一个神秘的“1218改革”。其中一条如下:

所有部门的基础技术整合到“TG”(基础技术体系),数据中心、基础架构、运维这些百度核心技术和技术大牛全部兵合一处将打一家。

在一般人眼里,这好像是个路人甲的架构调整;但技术人看后却内心一惊,百度正在把所有部门的核心发动机拆开,重新组装成了一个硕大无朋的“擎天柱”。

用破釜沉舟来形容,丝毫不过分。

而“擎天柱”的负责人,是十五年的老百度人,百度云的创立者之一:侯震宇。震宇隐隐然觉得“天将降大任”,但他又实在猜不透,自己手上这个变形金刚将会用什么姿势书写历史。

2019年1月4日,刚刚划归震宇部门的大牛架构师汪瑫从上海来到北京汇报。工作聊罢,汪瑫轻描淡写地告诉震宇一个“One more thing”。

“听说业务部门刚刚拿下了今年的春晚红包,你知道吗?”汪瑫说。

“什么??!!”震宇当时如同全身过电,血往上涌。就在前后脚,百度 App 部门联系震宇,确认了春晚红包的项目。

编辑

请点击输入图片描述

侯震宇

那一刻震宇意识到,“擎天柱”的第一战已经来了——在履职的第16天,组织架构都来不及调整,新任务也来不及制定,甚至连人都没认全的情况下,要负责保障一个“全中国人都对其威力一无所知”的春晚。

这还真是一个故事的好开头啊。

就在此时此刻,穿越几道门,坐在办公室里的“厂长”李彦宏一如既往地气定神闲,没人能看出他内心究竟是平静如水还是波涛汹涌。

过去几年,百度过得并不轻松。

公司的一些商业决策失误被人诟病,正在艰难地走出泥泞;而在百度文化里如同“定盘星”一样闪耀的技术人,也一边忍受着旁人的侧目,一边度过艰难的日子。

商业上的是非博弈,永远有回旋的余地。但技术人的心一旦散了,百度的未来将会被彻底改写。

李彦宏比任何人都清楚,对士兵最大的慷慨,就是为他们准备一个盛大的战场。

百度人渴望一场大战,洗刷十年风尘。

编辑

请点击输入图片描述

李彦宏

(一)生死战 

不用多想,你都能把百度的春晚发红包的姿势猜得八九不离十:

1、时间:春晚期间,分几轮发红包;

2、地点:打开“百度 App”;

3、人物:全体中国人;

4、动作:点点点点点。

万万想不到,就是这么简单的四步,引发了接下来整整一个月惊天动地血雨腥风的故事。

说回当时。

远在三亚休假的百度信息流主任架构师吴永巍也同时接到这个消息,一刻不停地赶回他所在的上海研发中心,又赶最早的飞机降落北京。

编辑

请点击输入图片描述

吴永巍

震宇代表基础技术保障团队,吴永巍代表百度 App 的技术团队组成了联合作战组。他们一秒都不敢耽搁,当天就开始筹备组建春晚红包技术团队。

贺峰,十一年百度人,运维负责人,他责无旁贷地成为“百度春晚红包”稳定性计划总制定者。

 

陈曦洋,十一年百度人,系统性能优化大神,他的能力是像庖丁解牛一样把一个 App 拆解成细碎的零件,为每一个零件做细致的优化,使得 App 所需的系统网络资源降低到理论最低极限。

 

汪瑫,七年百度人,不仅是技术大牛,还和百度登录帐号团队非常熟悉。

 

宋磊,十年百度人,系统部网络组专家,主要工作是维护百度网络的稳定运行。

 

张家军,八年百度人,系统部供应链负责人,百度平日所需的新增服务器,都由他来搞定。

 

等等等等,后面的名单还有长长一串。

所有的技术人,听到“春晚任务”,第一反应都是懵逼。因为他们的很多朋友都在阿里、腾讯,这些人曾经哭诉春晚有多么残忍的场景还历历在目。风水轮流转,这下轮到自己了。。。

但是,几乎只有一秒钟的迟疑,他们又变得像孩子一样兴奋。

外界不总是说,百度在移动时代不行了么?但是,行不行别人说了可不算。十年了,我们终于不用再等了。厂长既然给我们这次机会,我倒要证明给所有人看,我们到底是行还是不行!

贺峰对所有人说。

编辑

请点击输入图片描述

贺峰

很快,经过全部专家组讨论,贺峰把最终的计划表拿出来了。(原任务表极其复杂,中哥用自己的话解释给你。)

任务一:陈曦洋“拆解”百度 App 的每一个零件,精确计算出当天每一个 App 将会发出多少流量,进而计算出春晚时百度所需准备的总资源数量。(时间:第一周。)

任务二:贺峰亲自操刀,制定“春晚剧本”。春晚当天,百度集团所有系统资源全部切换档位,其他系统让位给红包系统。红包抢完,要在最短时间内再把系统资源还给其他部门。为此,需要一个极其精密无缝切换的操作预案。(时间:第一周+第二周。)

任务三:由于百度内部所能协调出来的系统资源不够,张家军要向全国厂商发出数万台服务器的紧急采购,确保打仗所需的全部“粮草”及时到位。(时间:第二周+第三周。)

任务四:宋磊向运营商全面采购带宽等软资源,建设足够的 IDC 网络  CDN 网络。(时间:第二周-第四周。)

任务五:陈曦洋协助百度 App 团队,百度 App 每秒数据传输量降到最少,然后打包成最新版百度 App 下发,确保春晚当天每个人手上的百度 App 都是性能最佳的最新版。(时间:第二周+第三周。)

任务六:陈曦洋协助小程序团队,一起开发春晚当天红包的小程序,确保这个小程序也占用最少的资源。(时间:第四周)

任务七:汪瑫负责协助百度帐号登录体系(Passport)进行全方位加固,应对春晚涌来的登录请求。(时间:第一周直到春晚。)

任务八:通知各大第三方应用市场,告诉他们春晚的时候,可能会有大量用户去下载百度 App,让他们也做好准备。

任务九:所有资源+调优全部到位,联合百度 App 的业务部门进行四轮“全链路联合测试”。(时间:第四周。)

任务十:春晚当天,所有人全力以赴,抗住十三亿人的流量海啸

十大任务列阵于此,旌旗猎猎。

编辑

请点击输入图片描述

彼时,即使是计划制定者贺峰也难以想象,在未来四周时间里,百度、三大运营商、全球硬件供应链、中国数家服务器厂商、外包建设团队、机房运维团队、全世界总计数万人和他们的家人将会为此付出怎样艰苦卓绝的努力。

但此时此刻,已经没人能阻挡这一切发生了。

这将是我职业生涯的生死战。

贺峰说。

(二)太空变轨 

计算结果出来了!我预计春晚的流量,每秒峰值将会达到5000万次!每分钟的峰值将会达到10亿次!

陈曦洋一夜没睡,拿着最新出炉的报表对贺峰说。

编辑

请点击输入图片描述

为了测算流量,陈曦洋研究了不少腾讯、阿里红包相关新闻。

经过计算,支撑这些流量的云计算系统,需要由10万台服务器组成。

这意味着什么呢?

2018年全年,全中国960万平方公里销售的全部服务器是300万台。而百度需要在一个月内,毫无准备的情况下,搞定去年全国销量的三十分之一,并且完成采购、生产、调试、接入百度云的全过程。

贺峰知道,如果全靠临时采购,这件事不可能完成。百度内部必须让出至少5万台服务器来支持春晚红包计划。

“凤巢广告系统、原生广告系统、网盟变现系统,统统要在春晚过程中熄火,把资源让给红包系统。”贺峰的计划白纸黑字。

要知道,凤巢系统是百度的广告收入核心。凤巢暂停四小时,意味着百度这艘火箭要在万米高空强行熄火,失速四小时。

编辑

请点击输入图片描述

而这不仅是真金白银的损失,还是一个极其危险的操作——万一熄火之后点不着,凤巢系统将面临对数百万客户的巨额赔偿。

当贺峰把计划拿给凤巢负责人王岳的时候,内心是很打鼓的,他知道对方有一万个理由拒绝他的计划。但是,王岳仅仅花十分钟看完了方案,说了三个字:没问题!

“没问题”,意味着一部包含5万台服务器的超级引擎将会在春晚四小时交由贺峰团队驾驶,这一下就满足了计划所需的一半。

剩下的5万台,交给采购部门,这个等下再说。回来继续看陈曦洋。

10万台服务器已经是团队的极限了。但是陈曦洋知道,把服务器总数控制在10万台,这还是有前提的。前提就是每个用户手机上的百度 App 还需要进行大量优化。

当陈曦洋带着三个兄弟完成对百度 App 的解析之后,他的眼泪都快流下来了。

百度 App 在启动时,会对自家服务器发送100多个连接。这些连接来自于百度不同的业务团队。

 

陈曦洋一下就懂了。由于百度 App 是目前百度装机量最高的超级 App,是“全村人的希望”。所以很多业务和技术同学都把展示自己团队成果的模块挤进百度 App,只是希望自己的努力能够被更多的用户感受到。

这些年逆风行船,百度的技术产品人其实一直在用力。

编辑

请点击输入图片描述

但是对于此刻的陈曦洋来说,这意味着灾难。在春晚当天,每一秒钟,每一个连接都要乘以几千万人这么一个倍数,每个 App 100 个连接,每秒就是几十亿次连接,BAT 加在一起都必跪无疑。

只见陈曦洋,此时已经红了眼,变成了“操刀鬼”——对百度 App 所有连接大砍大杀。

1月10日,陈曦洋和指挥部的领导开会,大家一致认定,百度 App 对外连接的数量,要从100个砍到3个。

由于 AppStore 和各大安卓商店都需要有审核时间,百度 App 必须在春晚前两周发布最新版本,所以很多连接来不及修改背后的整体逻辑,只能用暂时抑制的方式来处理。

编辑

请点击输入图片描述

花开两朵,各表一枝。

那边“操刀鬼”陈曦洋正在为百度 App 瘦身,这边“拼命三郎”汪瑫已经代表项目组进驻了百度帐号体系团队。

多说一句,汪瑫本来属于上海团队,和春晚项目关系不大。但是由于他技术非常牛,又和帐号团队比较熟悉,被震宇和吴永巍“强行”拉来参与春晚计划。接到任务时他二话没说,第二天就飞到北京,开始了长达一个月的加班。

编辑

请点击输入图片描述

汪瑫

业内八卦显示:2018年,淘宝春晚发红包,最大的问题就出在了登录系统。

讲真,由于搜索业务天然不必须登陆操作。这么多年来,百度的帐号体系建设是弱于电商阿里和社交腾讯的。

祸不单行,就在接到春晚任务之前,帐号团队的技术负责人李盈沉浸在苦恼中,因为他团队的一位优秀的同学提出了离职。但是,就在接到任务的当天,那位原本想离职的同学找到李盈,说自己不走了。

“我想打仗。”他说。

百度 App 登录状态的比例比较低。但是抢红包的时候,技术上必须要登录才能知道给谁“结账”。所以汪瑫预计,春晚当天百度 App 会遭受一波剧烈的登录冲击。有多剧烈呢?预计会是平常登录峰值的 2500 倍。

这意味着将会有很多人请求短信验证码。

编辑

请点击输入图片描述

时间有限,汪瑫一边连夜协助帐号团队做各种优化,一边让团队向各大运营商和第三方服务商购买短信发送的服务。一时间,所有省市的三大运营商的短信发送能力,被百度一家预定一空。

但即使是这样,还是不能满足春晚当天的预计值,汪瑫紧急派出百度最好的工程师进驻第三方服务商,帮助他们优化代码,提高短信发送的能力。终于达到了预期数值。

所有工程师的这些操作都在短短两周内叠加在一起。对于百度来说,这是一场史诗级的“太空变轨”。

软件这边忙得热火朝天,转回头来看,还有5万台服务器的硬件缺口。所有人的目光都落在张家军身上。

编辑

请点击输入图片描述

张家军

(三)那个疯狂的夜晚 

贺峰看着张家军说:“不要有压力,我们这次肯定‘一战成名’。”

“此话怎讲?”张家军问。

“就是说,成功也会出名,失败也会出名。”

“。。。”

张家军盘点了一下手上的“数据中心地图”,他发现自己简直是太幸运了:

北京数据中心曾经预定了4万台服务器,约定春节前交货。此时已经有2万多台交付完毕,还剩1.8万台未交付。

于是,张家军的计划很快出炉:

1、催促北京数据中心在一周内交付1.8万台服务器。

 

2、拼尽全力,就是抢,也要在两周内抢来1万台新的服务器,放到南京数据中心。

这些服务器都会并入百度云统一调配。张家军发现,虽然百度云平时默不作声,但是这么多年对于技术的极致追求,越是在艰难的时刻越能体现出闪光的价值:

百度设计的服务器,大多是以整体机柜的方式制造的。也就是说,在服务器厂家出厂的时候,就已经是一台大机柜里面固定好30台服务器的形态了。

这意味着,百度云不需要像其他云计算厂商那样,一台服务器一台服务器地在现场安装,而是把整个机柜直接推进去就可以进行测试安装了。

编辑

请点击输入图片描述

这就是整体机柜在安装时的场景

在接下来的一周里,百度的合作伙伴浪潮也体现出了国际顶尖的专业精神,一队队卡车整齐地并入高速公路,直接开赴北京。

你可能不相信,这两个大厂,共同创造了8小时安装1万台服务器的世界纪录。

编辑

请点击输入图片描述

北京如期搞定。

但最让张家军头疼的是,如何凭空变出来南京机房的1万台服务器。

他给我算了一笔账:

电子行业的硬件备货周期,通常就是8-12周。也就是说,你要至少提前两三个月向服务器生产商和零部件供应商下订单。

 

即使是服务器厂商完全停掉手中其他的活,产能也是有限的。从接到订单到生产出来,一般也要一周时间。

 

从卡车从服务器厂商的车间开出来,到百度数据中心,一般需要3-5天时间。

 

从服务器检测安装,到并网调试,一般需要1-2天。

“我接到任务的时间是1月6日,我的任务是:1月21日早晨8点,1万台服务器要一台不少齐装满员站在南京机房里。”张家军看着我,一字一顿地说。

“对不起,我们的生产能力不够,不能耽误百度春晚这么大的事儿,这个单子我们不敢接。”这几乎是所有服务器生产商对于张家军的标准回复。

只有几家大型服务器生产厂商接受订单,但是,限于原料储备不足,他们未来两周的生产能力上限是——4000台。

这下张家军的任务变成了:帮助服务器厂商协调全球供应链,从全世界找到另外几千台服务器的所有配件。

如果换成别人,任务到此已经可以宣告失败了。

但是张家军不准备认输。

过去几年,他和同事们拼命了解产业链的运作方式,和各大厂商建立联系,一点一点构建百度云的基础设施。他们赌上了自己的青春和年华,却依然能听到外界很多讥讽的声音。这么多年都扛过来了,就是因为他们相信,有朝一日,所有的证明都会如数归来,那些不该他们承受的东西,终会像雾霾一样退散。

此时此刻,他怎么舍得认输。

他和团队给全球的供应商一个一个打电话,买最近的一班飞机飞到国外,到每一个工厂里查看零件配给数量。一天,两天,三天,这些原本计划分配给美国、欧洲的零部件,从以色列、美国、东南亚调转航向,一起向中国飞来。

这是一场全球供应链的胜利。

服务器厂商开足马力,所有工人放弃了提前回家的计划,回到岗位三班倒,服务器被源源不断地生产出来。卡车等在工厂门口,放弃编队,装满一辆车就出发一辆,在通往南京的高速公路上,每隔一百公里就有一辆满载服务器的卡车在飞驰。

南京数据中心里,百度的工作人员、机房运维人员、建设外包队伍已经严阵以待。

张家军在北京总部协调,不能脱身,于是他和南京24小时通着电话,时刻指挥进展。服务器进驻机房, 还要有调试的过程,但凡有哪一步出现差错,就会导致满盘皆输。所以,张家军团队为每一种想到的意外都做了周密的预案。

1月20日夜里,最大一波服务器抵达南京,眼看大功告成。此时,一个最不可能发生的意外,却真的发生了。

机房的货梯,由于承受不住一吨多的机柜上下折磨,毫无预警地罢工了。他们马上转战客梯,客梯很快也出现故障。

编辑

请点击输入图片描述

有人拍下了当时紧急修复电梯的场景

临近春节,电梯检修人员大多回家过年,人手非常紧张,要天亮才能赶到。但是百度的同学们知道,仅仅这一夜,他们也是等不起的。

漆黑的夜里,所有现场的人员,用双手和双肩扛起来一百多斤的服务器,一步一步地从楼梯往上爬。

当时一位同学用视频记录下了现场的场景:

人肉抬服务器上楼

有同学的手被划破了,血珠渗出来。同事要把它换下来,他只是摇头,说没事。有人看到了他眼角的泪水。

现场还有很多一吨多的整机柜服务器,靠人力根本搬不上楼。现场指挥部的同学,甚至叫来了一辆吊车,要砸开机房的窗户运进去。

张家军在视频里看到现场吊车的钩子在风中摇晃,惊出一身冷汗。他怕同学受伤,言辞拒绝了这个方案。但是现场的负责人反复恳求:你就让我们试试吧!服务器要上楼啊!!

终于,现场维修团队想到了一个办法,紧急把隔壁楼的电梯配件拆过来,货梯终于缓缓启动,那时,已经是半夜两点了。

2019年1月21日早晨八点,负责服务器软件调试的同学如期赶到,所有的服务器静静地站在机房里。它们就那样沉默着,仿佛昨夜什么都没发生。

(四)核弹头就位 

1月17日,百度召开总监会。

贺峰回忆,李彦宏的表情“非常淡定”。他只是笑眯眯地看着大伙儿,说:“你们一定能搞成的。”这次总监会只是比平常多了一个小环节,晚上大家一起吃了个饭,李彦宏挨桌给大伙敬了酒。

实际上,那几天正是所有团队最焦头烂额的时候。

震宇告诉我,就在这一个月的时间,百度的 IDC 新增带宽资源超过了过去20年的历史总和,CDN 资源新增了2018年的一半。

根据宋磊回忆,两周左右的时间里,69位工程师飞了7万多公里。北上广的剩余带宽资源几乎都被百度拿空了。

编辑

请点击输入图片描述

宋磊

和他的朋友可乐君

网络团队有两位工程师是夫妻,一个负责网络建设,一个负责网络测试。一个人刚建设好,回家看孩子,换另一个人来测试。有时候建设工程师要陪着测试工程师一起工作,两个人只好把孩子扔给老人,一夜都不回家。

而宋磊本人,从1月6日计划启动到大年初一,睡得最长的一晚是五个小时,最短只有一个小时。

编辑

请点击输入图片描述

一位同学躺在桌子上睡觉,被无情偷拍。

时间一刻不停,已经是1月26日。距离春晚还有8天。

这一边,张家军的“粮草”已经全部到位,工程师也把所有的服务器完整无误地接入百度云,10万台机器全副武装准备就绪。宋磊新建了相当于支撑全澳洲人口同时观看视频的 CDN 和 IDC 网络,并且连续几个通宵完成了压力测试。

另一边,陈曦洋成功地把百度 App 对外连接数从100个砍到3个,汪瑫把百度 App 的登陆能力从1500人次每秒疯狂提升到15万人次每秒。

百度开启了“春晚红包计划”全链路测试。

贺峰为这四个半小时制定了几百页的“剧本”。剧本分为两部分:

1、主持人播报抢红包开始的每一个时间点,百度系统分别提前多少秒做好什么准备。就像火箭发射一般精密。

 

2、一旦遇到意外情况,哪个子系统要做出怎样的调整,根据意外程度的不同,做出的调整力度也不同,这套预案中,涉及到了上千条意外情况。一旦条件触发,指挥部的同学只要点击一个按钮,就能启动相应的更改。

编辑

请点击输入图片描述

这就是“剧本”

根据经验,运维人员真正遇到问题的时候,心理会承受巨大的冲击,很容易慌乱。为了让同学们临危不乱,贺峰还专门编写了《作战守则》,上面写着“指令要清晰,行动听指挥”等等要求贴在墙上,发给每一个同学提前学习。

编辑

请点击输入图片描述

对于百度这群工程师来说,这几百页作战计划里的每一条预案,都不是凭空想出来的。他们在写每一个字的时候,都可以回忆起十多年来自己在百度的运维经验,这厚厚一本,哪里是作战计划,分明是一个百度工程师的技术人生。

百度科技园 K2 大楼的整整一个大厅被改成作战指挥室,中心一个核心指挥室,旁边20个小屋是包括百度 App、大搜、摇一摇、帐号系统、BFE中台、网络、系统监控、IT团队、红包核心系统等等在内的分组作战室。加上地下一层本来就有的中控室,组成了联合指挥作战系统。

编辑

请点击输入图片描述

核心指挥室

这里要稍微插一句。

2019年百度春晚的红包设计和之前阿里腾讯的稍有不同。他们把百度 App 日常的功能和红包相结合。比如你要在百度搜索框里语音说出“幸福快乐年”,或者手动刷一下百度 App 首页的新闻瀑布流查看“拜年视频”后才能进入,这就造成了用户行为的不可预测性加大。

这就形成了一个开放空间:难免有人说出方言,或者刷新瀑布流搞错了方向,从而对后台的人工智能系统带来不可预知的巨大压力。

这会导致测试的时候,非常难以模拟春晚真实场景。

百度人的“实在”在这里体现得淋漓尽致。反正我就是要实现这样的效果,技术上的问题,一点点搞就好了。

编辑

请点击输入图片描述

必不可少的步骤:拜杨超越

演习过程中,专门有一队“蓝军”,负责为春晚系统制造各种麻烦,例如掐断某个机房的数据通路,让某一个模块停止响应,甚至直接对百度系统发起攻击。而在另一边,指挥团队严格按照剧本对所有问题瞬间应对。百度安全团队也加入了护航编队,对夹杂在正常访问之间的进攻进行拦截。

虽然中间几经波折,但是在倒数两次联合测试中,整个百度春晚红包系统都经受住每秒5000万次访问的考验。所有人悬了一个月的心,这才稍稍放下一些。一向严谨的吴永巍对团队成员说,我现在的信心指数是85-90分!

就在大家紧锣密鼓忙活的时候,发生了两件怪事:

宋磊在那几周新增了一个习惯:每天半夜两点把自己加班的情况拍照,晒到朋友圈。当时大家还纳闷,为什么老宋那么低调的一个人,却要天天晒加班呢?

 

从上海过来支援的汪瑫,1月24日神秘消失了一天。他究竟去哪了呢?

这些小八卦,当时谁也没空探秘。因为春晚已经近在眼前了。

编辑

请点击输入图片描述

(五)除夕 

2月4日,除夕。

从当天零点开始,已经有同学在作战部值班。早晨八点,全员就绪,大战一触即发。

所有百度的同学里,有两位是最为特殊的。他们当天晚上会进驻到央视直播现场。人们开玩笑说,他们是百度押在央视的“人质”。这两位同学在去之前还满怀激动地打听,我们去了要做什么呢?其他人冷冷地说,根据腾讯和阿里的经验,你们去了只有一个任务:我们这里如果砸了,你们两个负责“挨骂”。

临走时,这两个同学用幽怨的眼神看了一眼百度大部队,决绝地赶赴央视。嘴里唱着“风萧萧兮易水寒。。。”

根据设计,在除夕当天上午11点,百度会向用户推送一个小红包活动作为预热,让真实的用户来参与,从而对系统进行一波实打实的终极测试。

11:00,预热活动开始,后台数据直线上升。百度 App 瞬间访问峰值达到88万次每秒。这个数值已经是百度 App 历史最大峰值的几十倍。但是贺峰知道,这还仅仅是毛毛雨,他们为春晚设计了5000万次每秒的能力。

直到这时,陈曦洋所负责的重要任务——掐断百度 App 的多余回连数据突然有所抖动。陈曦洋和百度 App 的技术同学各个满头大汗,直到直播前一个小时,才把问题解决妥当。

晚上八点,春晚准时开播。

凤巢系统缓缓熄火,红包系统接管引擎驾驶。所有系统齿轮咬合,像起跑线前的赛车一样低吼着冲出去。

编辑

请点击输入图片描述

作战室的同学仿佛进入了另一个世界。整个大厦,掉一根针在地上都能听到。

按照央视彩排的时间表,第一次摇红包应该发生在晚上八点半。但是,就在八点十八分的时候,主持人突然提前预告了一下:“观众朋友们可以下载百度 App 参与今年的春晚摇红包活动。”

这之后一分钟,指挥部的舆情监控群里,突然有人甩进来一张图片:

编辑

请点击输入图片描述

苹果的 AppStore 被网友挤垮,已经打不开了。

陈曦洋赶紧拿出手机测试,哪里是 AppStore,小米、华为等等 AppStore 全部躺尸。他这才明白,虽然当时自己派人和各大应用商店提前打过预防针,但事实证明,他们对春晚“一无所知”。。。

作战组马上给出数据,预计全国有200万-300万人无法下载百度 App,这将带来不小的损失。而另一组数据显示,无法下载 App 的人们又涌向了百度搜索,在手机浏览器里用关键词搜索的方式尝试下载百度 App。

贺峰评估了一下百度 CDN 的占用量,马上下令,把链接指向百度自家的下载接口,让大家不用通过第三方市场,而是直接从百度家下载百度 App。就这样,下载高峰直接冲击百度自家网络,一点一点,几百万人都安装了百度 App。

编辑

请点击输入图片描述

这一切被汪瑫看在眼里。

他的第一反应是:我厂真牛逼。他的第二反应是:这300万人下载了 App 之后,肯定是要登录的啊,我负责的登录系统看来马上就要挂了。。。。

还好,有惊无险,大家分散下载了 App,也就分散登录 App,并没有对登录系统带来致命的打击。

随着春晚的进行,访问百度 App 的流量一轮比一轮大,逼近预计中的峰值。

百度20年积累下来的遍布全国的数据通路,顺利扛过了前三轮红包的数据洪峰,只剩零点钟声敲响前的最后一次。根据预测,这将是最大的一波浪潮。

在最后一次红包到来之前23分钟,贺峰突然接到驻场春晚那两个同学的消息:根据测算,春晚比预计延迟了4分钟。

编辑

请点击输入图片描述

贺峰心里咯噔一下。

这会造成百度 App 红包开抢的时间早于主持人播报的时间。也就是说,听到主持人播报才进来的用户,很可能发现红包已经被准点动手的用户抢完了。。。

贺峰要做一个决定:是保持原计划放开红包,还是要按照春晚的进度延后 App 上红包开抢的时间。

把这么大规模的调整部署到10万台服务器上,起码需要五分钟。

所有人都看着贺峰。贺峰盯着屏幕,两手一压,对大伙说:再等等。

就在红包开启前十分钟,贺峰判断央视应该是无法抢回时间了,下令马上对10万台服务器发出指令,延后4分钟开启红包系统。

上天眷顾,最后百度 App 红包开启的时间,和主持人宣布红包开抢的时间严丝合缝。

一瞬间,上亿人手机屏幕上显示着百度 App 的红包界面,巨大的数据浪潮涌向北京和南京两地的数据机房。那一刻,百度大厦里1000多位同事,百度散落在各地机房的100位同事,带着备用零件守候在机房的100多位服务器厂商的工程师,三大运营商为了保护网络通畅而留守在各地机房的1000多位同事,那些中国通信行业和互联网行业的梦想者,用自己的付出搭建出了人类历史上最宽的信息通路。

暗夜无声,大地上烟花四起。

这个古老的民族,迈入了新的一年。

(六)那些小事 

燃烧了四个半小时的红包系统渐渐熄火。

间断了四个半小时的凤巢系统缓缓启动。

伴随着10万台服务器的嘶嘶声,百度这架火箭完成了太空中的第二次惊险变轨。

指挥部里,欢声雷动。

编辑

请点击输入图片描述

直到这个时候,吴永巍、震宇、贺峰、陈曦洋他们才敢确定,这个曾经击垮了阿里巴巴和腾讯的春晚,并没有击垮百度。他们用了三十个日夜,证明了自己是当之无愧的“老司机”;证明了自己对春晚并不是一无所知。

可叹的是,如此宏大的工程,调动了全球的供应链体系,调动了全中国的网络带宽,调动了全百度的技术资源。中间如果百度系统有任何一个技术环节卡住,百度云的服务器有任何一个螺丝钉松动,百度工程师、商务采购团队有任何一个人掉链子,全中国人都会面对一个完全不同的结局。

百度无疑是幸运的,但这世界上,只有勇者才有资格谈运气。

我听到很多百度同学回忆起那个瞬间,他们描绘的,正如《少林足球》里那个场景:

历经磨难,少林师兄师弟的武功全部觉醒,周星驰跪在地上,说:欢迎各位师兄弟归位!

编辑

请点击输入图片描述

直到正月末,我来到百度科技园来采访春晚红包故事的时候,我依然能够感觉到,在大厦里洋溢着的喜悦气氛。

哦对了,还有一些小事忘记交代。

那两个被“质押”在央视的小伙伴,期待中的指责并没有来,反而被冲进来的央视同事们握着手说:百度真是太牛了,你们是第一个没有出现问题的合作伙伴。你们不是有 AI 技术吗?你们不是有视频技术吗?合作起来呀~

编辑

请点击输入图片描述

而好奇的人们,也终于打听到了前几天神秘事件的原因:

宋磊羞涩地表示,之所以每天凌晨两点发朋友圈,是因为他怕老婆想多了,晒朋友圈以证清白。。。

而汪瑫消失的那一天,是飞回上海给儿子过两岁生日。因为去年儿子一周岁的生日时,他就在外地加班,没能陪他。这次儿子喊着要爸爸,他才连夜飞回去一天。第二天给儿子过完生日,晚上八点他陪儿子上床睡觉,确定儿子睡熟了,才赶紧跑出来,赶十点的飞机飞回北京。

而且,除夕那天其实是吴永巍的生日。但直到整个春晚战役结束,第二天凌晨他才告诉几个小伙伴这件事儿。“我和兄弟们过了最有意义的一个生日。”他说。

离别之前,我颇为郑重地问陈曦洋:“为百度付出了这么多,你觉得值得吗?”

“我只是想证明百度。”陈曦洋说完,沉默良久。

十一年前,我一毕业就加入百度,我的所有技术积累都是百度给我的。我把百度当成家。之前,百度遇到很多问题,我比外面的人更难受,就像对自己的孩子一样,恨铁不成钢。

 

我记得,在2016年百度最困难的时候,我们团队去杭州团建,秋天的杭州特别美。有一位家就在杭州附近的同学,回来之后就辞职去了杭州。

 

我知道,大家都有权做出自己的选择。从2016年到现在,几乎每周都有猎头给我打电话,但我从来都是直接挂掉。我觉得,总有人要让百度变得更好,而我应该是那些人中的一员。

他就这么安静地说着。

听到这里,我突然明白,在春晚红包成功的那一刻,那些百度技术人眼里的泪花,告慰的不是过去一个月的艰苦卓绝,而是十年前的自己,那个不解风情却无问西东的少年。

编辑

请点击输入图片描述

陈曦洋

他们在漫长的岁月里安静地忍受着孤独,这一次却像疯子一样拼尽全力,押上全部韶华和热血。他们也许并没想赢,他们,只是不想输。

有人说,生活是一场战役,结局或是千秋标榜,或是万古遗憾。

但我更相信,岁月是一张长长的考题,没人会逼你交卷。

你尽可以用一生的时间,慢慢给出自己的答案。

编辑

请点击输入图片描述

再自我介绍一下吧。我是史中,是一个倾心故事的科技记者。我的日常是和各路大神聊天。如果想和我做朋友,

可以搜索微信:shizhongpro

或者关注微博:@史中方枪枪 @浅黑科技

不想走丢的话,你也可以关注我的公众号“浅黑科技”。(记得给浅黑加星标哦)

编辑

请点击输入图片描述

岁月

是一张长长的考卷

编辑

请点击输入图片描述

阅读原文

阿里云CentOS 7上安装配置Docker

Docker 是一个开源工具,它可以让创建和管理 Linux 容器变得简单。容器就像是轻量级的虚拟机,并且可以以毫秒级的速度来启动或停止。Docker 帮助系统管理员和程序员在容器中开发应用程序,并且可以扩展到成千上万的节点。

1

这是一只鲸鱼,它托着许多集装箱。我们可以把宿主机可当做这只鲸鱼,把相互隔离的容器可看成集装箱,每个集装箱中都包含自己的应用程序。

Docker与传统虚拟区别

传统虚拟化技术的体系架构:

2

Docker技术的体系架构:

3

容器和 VM(虚拟机)的主要区别是:

  • 容器提供了基于进程的隔离,而虚拟机提供了资源的完全隔离。
  • 虚拟机可能需要一分钟来启动,而容器只需要一秒钟或更短。
  • 容器使用宿主操作系统的内核,而虚拟机使用独立的内核。

Doker 平台的基本构成

4

Docker 平台基本上由三部分组成:

  • 客户端:用户使用 Docker 提供的工具(CLI 以及 API 等)来构建,上传镜像并发布命令来创建和启动容器
  • Docker 主机:从 Docker registry 上下载镜像并启动容器
  • Docker registry:Docker 镜像仓库,用于保存镜像,并提供镜像上传和下载
  • 后面的文章会具体分析。

Docker 容器的状态机

5

一个容器在某个时刻可能处于以下几种状态之一:

  • created:已经被创建 (使用 docker ps -a 命令可以列出)但是还没有被启动 (使用 docker ps 命令还无法列出)
  • running:运行中
  • paused:容器的进程被暂停了
  • restarting:容器的进程正在重启过程中
  • exited:上图中的 stopped 状态,表示容器之前运行过但是现在处于停止状态(要区别于 created 状态,它是指一个新创出的尚未运行过的容器)。可以通过 start 命令使其重新进入 running 状态
  • destroyed:容器被删除了,再也不存在了

Docker 的安装

RedHat/CentOS必须要6.6版本以上,或者7.x才能安装docker,建议在RedHat/CentOS 7上使用docker,因为RedHat/CentOS 7的内核升级到了kernel 3.10,对lxc容器支持更好。

查看Linux内核版本(内核版本必须是3.10或者以上):

cat /proc/version

uname -a

lsb_release -a

##无法执行命令安装
yum install -y redhat-lsb

更新YUM源:

yum update

安装:

yum  install docker -y

检查版本:

docker -v

安装完成后,使用下面的命令来启动 docker 服务,并将其设置为开机启动:

service docker start
chkconfig docker on

下载官方的 CentOS 镜像:

docker pull centos

检查CentOS 镜像是否被获取:

docker images

下载完成后,你应该会看到:

[root@iZ2ze74fkxrls31tr2ia2fZ ~]# docker images centos
REPOSITORY       TAG        IMAGE ID     CREATED         SIZE
docker.io/centos latest    3fa822599e10    3weeks ago   203.5 MB

如果看到以上输出,说明你可以使用“docker.io/centos”这个镜像了,或将其称为仓库(Repository),该镜像有一个名为“latest”的标签(Tag),此外还有一个名为“3fa822599e10 ”的镜像 ID(可能您所看到的镜像 ID 与此处的不一致,那是正常现象,因为这个数字是随机生成的)。此外,我们可以看到该镜像只有 203.5 MB,非常小巧,而不像虚拟机的镜像文件那样庞大。

启动容器:

docker run -i -t -v /root/software/:/mnt/software/ 3fa822599e10 /bin/bash

docker run -ti ubuntu:14.04 /bin/bash

命令参数说明:
docker run <相关参数> <镜像 ID> <初始命令>

  • -i:表示以“交互模式”运行容器
  • -t:表示容器启动后会进入其命令行
  • -v:表示需要将本地哪个目录挂载到容器中,格式:-v <宿主机目录>:<容器目录>

Docker命令

我们可以把Docker 的命令大概地分类如下:

镜像操作:
    build     Build an image from a Dockerfile
    commit    Create a new image from a container's changes
    images    List images
    load      Load an image from a tar archive or STDIN
    pull      Pull an image or a repository from a registry
    push      Push an image or a repository to a registry
    rmi       Remove one or more images
    search    Search the Docker Hub for images
    tag       Tag an image into a repository
    save      Save one or more images to a tar archive 
    history   显示某镜像的历史
    inspect   获取镜像的详细信息

    容器及其中应用的生命周期操作:
    create    创建一个容器
    kill      Kill one or more running containers
    inspect   Return low-level information on a container, image or task
    pause     Pause all processes within one or more containers
    ps        List containers
    rm        删除一个或者多个容器
    rename    Rename a container
    restart   Restart a container
    run       创建并启动一个容器
    start     启动一个处于停止状态的容器
    stats     显示容器实时的资源消耗信息
    stop      停止一个处于运行状态的容器
    top       Display the running processes of a container
    unpause   Unpause all processes within one or more containers
    update    Update configuration of one or more containers
    wait      Block until a container stops, then print its exit code
    attach    Attach to a running container
    exec      Run a command in a running container
    port      List port mappings or a specific mapping for the container
    logs      获取容器的日志

    容器文件系统操作:
    cp        Copy files/folders between a container and the local filesystem
    diff      Inspect changes on a container's filesystem
    export    Export a container's filesystem as a tar archive
    import    Import the contents from a tarball to create a filesystem image

    Docker registry 操作:
    login     Log in to a Docker registry.
    logout    Log out from a Docker registry.

    Volume 操作
    volume    Manage Docker volumes

    网络操作
    network   Manage Docker networks

    Swarm 相关操作
    swarm     Manage Docker Swarm
    service   Manage Docker services
    node      Manage Docker Swarm nodes

    系统操作:
    version   Show the Docker version information
    events    持续返回docker 事件
    info      显示Docker 主机系统范围内的信息
# 查看运行中的容器
docker ps

# 查看所有容器
docker ps -a

# 退出容器
按Ctrl+D 即可退出当前容器【但退出后会停止容器】

# 退出不停止容器:
组合键:Ctrl+P+Q

# 启动容器
docker start 容器名或ID

# 进入容器
docker attach 容器名或ID

# 停止容器
docker stop 容器名或ID

# 暂停容器
docker pause 容器名或ID

#继续容器
docker unpause 容器名或ID

# 删除容器
docker rm 容器名或ID

# 删除全部容器--慎用
docker stop $(docker ps -q) & docker rm $(docker ps -aq)

#保存容器,生成镜像
docker commit 容器ID 镜像名称

#从 host 拷贝文件到 container 里面
docker cp /home/soft centos:/webapp

docker run与start的区别

docker run 只在第一次运行时使用,将镜像放到容器中,以后再次启动这个容器时,只需要使用命令docker start 即可。

docker run相当于执行了两步操作:将镜像放入容器中(docker create),然后将容器启动,使之变成运行时容器(docker start)。

6

而docker start的作用是,重新启动已存在的镜像。也就是说,如果使用这个命令,我们必须事先知道这个容器的ID,或者这个容器的名字,我们可以使用docker ps找到这个容器的信息。

7

因为容器的ID是随机码,而容器的名字又是看似无意义的命名,我们可以使用命令:

docker rename jovial_cori  centos

给这个容器命名。这样以后,我们再次启动或停止容器时,就可以直接使用这个名字:

docker [stop] [start]  new_name

而要显示出所有容器,包括没有启动的,可以使用命令:

docker ps -a

Docker配置

更改存储目录:

#复制docker存储目录
rsync -aXS /var/lib/docker/. /home/docker

#更改 docker 存储文件目录
ln -s  /home/docker  /var/lib/docker

获取IP:

docker inspect <container id>

要获取所有容器名称及其IP地址只需一个命令:

docker inspect -f '{{.Name}} - {{.NetworkSettings.IPAddress }}' $(docker ps -aq)

docker inspect --format='{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker ps -aq)

Docker 镜像加速器

注册个帐号

https://dev.aliyun.com/search.html

阿里云会自动为用户分配一个镜像加速器的地址,登录后进入”管理中心”–>”加速器”,里面有分配给你的镜像加速器的地址以及各个环境的使用说明。

镜像加速器地址:https://xxxxx.mirror.aliyuncs.com

如何配置镜像加速器

针对Docker客户端版本大于1.10.0的用户
您可以通过修改daemon配置文件/etc/docker/daemon.json来使用加速器:

{
    "registry-mirrors": ["<your accelerate address>"]
}

重启Docker Daemon:

sudo systemctl daemon-reload
sudo systemctl restart docker

后续:
1,查看镜像
docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1fef6edc3f23 docker.io/centos "bin/bash" About a minute ago Exited (0) 8 seconds ago affectionate_khorana
66f6e4ff1098 docker.io/centos "bin/bash" About a minute ago Exited (0) About a minute ago elastic_khorana
a44b68ea8265 75835a67d134 "/bin/bash" 7 minutes ago Exited (0) 2 minutes ago angry_meitner
d0c8cbacb8ce 75835a67d134 "/bin/bash" 25 minutes ago Exited (0) 13 minutes ago mystifying_perlman
2.更新镜像
docker commit #第一列容器ID centos:update
3.保存镜像
docker save centos:update > centos.tar
4.加载镜像
docker load < centos.tar



常见命令:

1,后台启动docker并且端口映射

docker run -itd -p 3390<物理机端口>:3390<容器端口> cd3c0fd5029a /bin/bash

2,进入docker

docker attach <容器ID>

3,退出docker,不关闭container

ctrl+p ctrl+q

4.更新镜像

docker commit <容器ID> centos:update<自己定义的名字:标签>

5.保存镜像

docker save centos:update > centos.tar

6.加载镜像

docker load < centos.tar

nginx proxy_cache 缓存配置

前言:
由于本人工作原因,涉及到网络直播领域,其中视频的回放下载,涉及到了一些视频下载方面的技术。针对于一个完整视频的下载,目前市面上的主流做法是,先将整个视频流切片,存储到文件服务器中,在用户需要观看回放视频时。通过一个视频回源服务器,去文件服务器中逐个请求切片,返回给用户播放。
今天着重探讨的是关于回源服务器缓存的配置以及合理的缓存策略。
通过给回源服务器配置缓存的案例,详细讲解一整套缓存配置机制,并且可沿用到其他任何缓存配置场景中。

今天的讲解分为四点:
回源服务器的工作是啥
为啥需要给回源服务器加缓存
如何配置缓存
如何针对业务场景配置完备的缓存机制

回源服务器的工作:
回源服务器在下面叙述中简称:源站
如图所示,在文件下载的过程中,横跨在cdn与文件服务器之间,作为下载枢纽。

源站架构:源站是nginx+php的webserver架构,如图所示:

但如果源站只是简单的收到请求,然后下载资源,再返回,势必会存在以下几点不够优化的问题:
1、cdn可能存在多次回源现象
2、源站对同一资源的多次下载,存在网络流量带宽浪费,以及不必要的耗时。
所以为了优化这些问题,需要给源站做一层缓存。缓存策略采用nginx自带的proxy_cache模块。

proxy_cache原理:
proxy_cache模块的工作原理如图所示:
如何配置proxy_cache模块
在nginx.conf文件中添加如下代码:
http{
……
proxy_cache_path/data/nginx/tmp-test levels=1:2 keys_zone=tmp-test:100m inactive=7d max_size=1000g;
}
代码说明:
proxy_cache_path 缓存文件路径

levels 设置缓存文件目录层次;levels=1:2 表示两级目录

keys_zone 设置缓存名字和共享内存大小

inactive 在指定时间内没人访问则被删除

max_size 最大缓存空间,如果缓存空间满,默认覆盖掉缓存时间最长的资源。

当配置好之后,重启nginx,如果不报错,则配置的proxy_cache会生效

查看  proxy_cache_path /data/nginx/目录,
会发现生成了tmp-test文件夹。

如何使用proxy_cache
在你对应的nginx vhost server配置文件中添加如下代码:
location /tmp-test/ {
proxy_cache tmp-test;
proxy_cache_valid 200 206 304 301 302 10d;
proxy_cache_key $uri;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_passhttp://127.0.0.1:8081/media_store.php/tmp-test/;
}
配置项介绍:
Proxy_cache tmp-test 使用名为tmp-test的对应缓存配置
proxy_cache_valid  200 206 304 301 302 10d; 对httpcode为200…的缓存10天

proxy_cache_key $uri  定义缓存唯一key,通过唯一key来进行hash存取

proxy_set_header  自定义http header头,用于发送给后端真实服务器。

proxy_pass  指代理后转发的路径,注意是否需要最后的/

到这里,最基本的proxy_cache功能就配置成功了。当uri成功匹配到该location,则proxy_cache就会生效。

添加proxy_cache之后,请求过程的变化:
1、第一次访问:
第一次访问,proxy_cache并没有找到对应的缓存文件(未命中缓存MISS),所以当第一次请求完成的同时,proxy_cache会保持缓存:
2、保存缓存,如图所示:

3、同一个url第二次访问,当同一个文件再次到达源站,proxy_cache就会找到其对应的缓存文件(命中缓存HIT)直接返回给请求端,无需再执行php程序,如图所示:
提出疑问:
到此,就完成了最基本的proxy_cache配置和访问过程介绍,但是最基本的配置,往往无法满足我们的业务需求,我们往往会提出以下几点疑问和需求:
需要主动清理缓存文件
写入路径为一块磁盘,如果磁盘打满该怎么解决?
如何让源站支持断点续传,以及断点续传的缓存策略
如果请求端 range 请求(分片下载)一个大资源,同样的uri,如何区别请求?
还需要告诉请求端,资源的过期时间
日志统计,如何配置命中与不命中字段,如何做统计?
面对以上疑问,我们一个一个解决。

问题一:主动清理缓存
采用:nginx  proxy_cache_purge 模块 ,该模块与proxy_cache成对出现,功能正好相反。
设计方法:在nginx中,另启一个server,当需要清理响应资源的缓存时,在本机访问这个server。
例如:
访问 127.0.0.1:8083/tmp-test/TL39ef7ea6d8e8d48e87a30c43b8f75e30.txt 即可清理该资源的缓存文件。
配置方法:
location /tmp-test/ {
allow 127.0.0.1; //只允许本机访问
deny all; //禁止其他所有ip
proxy_cache_purge tmp-test $uri; //清理缓存
}
proxy_cache_purge:缓存清理模块
tmp-test:指定的key_zone
$uri:指定的生成key的参数
proxy_cache_purge缓存清理过程,如图所示:
问题二:缓存文件强磁盘打满该怎么办?
由于写入路径为一个单一目录,只能写入一块磁盘。一块磁盘很快就会被打满,解决该问题有如下两种方法:
1、将多块磁盘做磁盘阵列? 缺点是:减小了实际的存储空间。
2、巧妙得运用proxy_cache_path的目录结构,由于levels=1:2,这导致缓存文件的目录结构为两层,每层目录名,都是由hash函数生成。如图所示:

总共含有16*16*16=4096个文件目录。对该一级目录进行软连接,分别将0-f软连接到你所需要的指定磁盘目录上,如图所示:

通过软链的方法,实现:将不同盘下的目录作为真正存放数据的路径,解决了多盘利用,单盘被打满的问题。

问题三:支持range(断点续传)
添加上缓存代理之后,客户端发起的range请求将会失效,如下图所示:

导致range参数无法传递到下一级的原因如下:
当缓存代理转发http请求到后端服务器时,http header会改变,header中的部分参数,会被取消掉。其中range参数被取消,导致,后端nginx服务器没有收到range参数,最终导致这个分片下载不成功。所以需要对代理转发的header进行配置。
例如:
location /tmp-test/ {
proxy_cache tmp-test;
proxy_cache_valid 200 206 304 301 302 10d;
proxy_cache_key $uri;
<span style=”color:#ff0000;”>proxy_set_header Range $http_range;</span>
proxy_pass http://127.0.0.1:8081/media_store.php/tmp-test/;
}
红色部分的含义:将http请求中的range值($http_range)放到代理转发的http请求头中作为参数range的值。

问题四,当支持range加载后,proxy_cache_key,则需要重新配置:
如果请求端 Range请求(分片下载)一个大资源,同样的uri,proxy cache如何识别资源对应的key。
由于nginx配置为:proxy_cache_key $uri,用uri作为key
所以当请求为普通请求和range请求时,都是同样的uri作为key。proxy_cache将有可能导致错误返回。如下图所示:

解决方法如下:
修改proxy_cache_key ,配置proxy_cache_key $http_range$uri;
这样就能解决:key唯一性。可以避免不管是正常请求还是不同的range请求,第一次获取的内容和之后获取的缓存内容都不会出现异常。

问题五:如何配置-返回过期时间
需要通过返回过期时间来指定请求端,哪些资源需要缓存,哪些资源不缓存,
参数 正常请求 range请求
返回过期时间 返回 不返回
为了防止请求端将分片资源当做完整资源缓存起来,我们需要对正常请求,返回过期时间;对range请求, 不返回过期时间。
解决该问题,通过对nginx配置即可解决:
location /media_store.php {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index media_store.php;
fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
include fastcgi_params;
if ( $http_range = ”){
expires 2592000s;
}
}
在proxy_pass代理之后的location中加入对$http_range的判断,expires 表示过期时间。 2592000s指缓存过期时间。

问题七:缓存命中情况如何在http头中体现,以及在nginx日志中查看
解决方法:
利用nginx $upstream_cache_status变量:该变量代表缓存命中的状态,
如果命中,为HIT;如果未命中,为MISS
在返回nginx server配置中添加:
add_header  Nginx-Cache “$upstream_cache_status”;
在nginxlog中添加:
log_format       combinedio  …$upstream_cache_status;
http返回head截图:
nginx log日志截图:
总结:
整个一套完备的缓存策略就介绍到此,这套方案中不仅实现了基本的缓存配置,还解决了实际场景应用中会遇到的,磁盘扩展,缓存清理,断点续传,缓存过期时间,缓存命中提示等问题,只要将这套方案灵活运用,不管是再复杂的场景,基本都能满足需求。以上都是我在工作中爬过的坑,不断完善总结出的结果,希望对读者能有帮助。