Giter Club home page Giter Club logo

wizard's People

Contributors

banggua avatar chenyanchen avatar d-or avatar dengdi0326 avatar fengyfei avatar lizebang avatar shichao1996 avatar wangriyu avatar wbofeng avatar yangchenglong11 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

wizard's Issues

docker 源码阅读 (I) - DaemonCli

docker 结构

Docker 采用 C/S 模型:

Docker Architecture

其中,dockerd 通过监听 docker client 的请求,来构建、运行、分发 docker 容器。

dockerd 启动过程

概览

dockerd 是基于命令行工具 cobra 来构建的:

cmd := &cobra.Command{
	Use:           "dockerd [OPTIONS]",
	Short:         "A self-sufficient runtime for containers.",
	SilenceUsage:  true,
	SilenceErrors: true,
	Args:          cli.NoArgs,
	RunE: func(cmd *cobra.Command, args []string) error {
		opts.flags = cmd.Flags()
		return runDaemon(opts)
	},
}

在 runDaemon 中,创建一个 DaemonCli 示例,并运行该实例的 start 方法,启动 dockerd。 DaemonCli 定义如下:

type DaemonCli struct {
	*config.Config
	configFile *string
	flags      *pflag.FlagSet

	api             *apiserver.Server // HTTP 服务
	d               *daemon.Daemon
	authzMiddleware *authorization.Middleware // 授权中间件
}

DaemonCli.start 方法

  • 设置默认配置参数,并加载配置:
opts.SetDefaultOptions(opts.flags)

if cli.Config, err = loadDaemonCliConfig(opts); err != nil {
	return err
}
cli.configFile = &opts.configFile
cli.flags = opts.flags

if cli.Config.Debug {
	debug.Enable()  // 通过设置: os.Setenv("DEBUG", "1") 开启调试
}
  • 设置 umask 并创建 pid 文件
if err := daemon.CreateDaemonRoot(cli.Config); err != nil {
	return err
}

if cli.Pidfile != "" {
	pf, err := pidfile.New(cli.Pidfile)

	defer func() {
		if err := pf.Remove(); err != nil {
			logrus.Error(err)
		}
	}()
}
  • 构建 APIServerConfig 并启动 APIServer:
cli.api = apiserver.New(serverConfig)

解析监听地址,并创建 listener:

for i := 0; i < len(cli.Config.Hosts); i++ {
	// ...
	// proto: fd | tcp | unix
        ls, err := listeners.Init(proto, addr, serverConfig.SocketGroup, serverConfig.TLSConfig)
	if err != nil {
		return err
	}
	ls = wrapListeners(proto, ls)
	// 对于 tcp 协议,需要保证监听的端口不在 container 可使用的端口区间中
	if proto == "tcp" {
		if err := allocateDaemonPort(addr); err != nil {
			return err
		}
	}
	// ...
	hosts = append(hosts, protoAddrParts[1])
	cli.api.Accept(addr, ls...)
}

在代码最后,go cli.api.Accept 创建一组 http.Server:

func (s *Server) Accept(addr string, listeners ...net.Listener) {
	for _, listener := range listeners {
		httpServer := &HTTPServer{
			srv: &http.Server{
				Addr: addr,
			},
			l: listener,
		}
		s.servers = append(s.servers, httpServer)
	}
}

要注意,在 Accept 中,只是创建了 http.Server 结构,但是并没有开始处理请求。

  • 创建 Registry 服务
registryService, err := registry.NewService(cli.Config.ServiceOptions)
  • 启动 containerd
// 根据 host 操作系统类型,初始化 containerd 配置
rOpts, err := cli.getRemoteOptions()
// 创建 containerd 实例
// 根据配置不同,在此处 containerd 有可能已经启动
containerdRemote, err := libcontainerd.New(filepath.Join(cli.Config.Root, "containerd"), filepath.Join(cli.Config.ExecRoot, "containerd"), rOpts...)
  • 插件初始化
pluginStore := plugin.NewStore()

if err := cli.initMiddlewares(cli.api, serverConfig, pluginStore); err != nil {
	logrus.Fatalf("Error creating middlewares: %v", err)
}
  • 初始化 daemon.Daemon
d, err := daemon.NewDaemon(cli.Config, registryService, containerdRemote, pluginStore)
if err != nil {
	return fmt.Errorf("Error starting daemon: %v", err)
}

d.StoreHosts(hosts)
  • cluster 初始化
watchStream := make(chan *swarmapi.WatchMessage, 32)

c, err := cluster.New(cluster.Config{
	Root:                   cli.Config.Root,
	Name:                   name,
	Backend:                d,
	ImageBackend:           d.ImageService(),
	PluginBackend:          d.PluginManager(),
	NetworkSubnetsProvider: d,
	DefaultAdvertiseAddr:   cli.Config.SwarmDefaultAdvertiseAddr,
	RuntimeRoot:            cli.getSwarmRunRoot(),
	WatchStream:            watchStream,
})
// ...
d.SetCluster(c)
err = c.Start()
  • 设置路由并启动 http.Server
routerOptions, err := newRouterOptions(cli.Config, d)
// ...
routerOptions.api = cli.api
routerOptions.cluster = c

initRouter(routerOptions)

// process cluster change notifications
watchCtx, cancel := context.WithCancel(context.Background())
defer cancel()
go d.ProcessClusterNotifications(watchCtx, watchStream)

// ...

serveAPIWait := make(chan error)
go cli.api.Wait(serveAPIWait)

至此,dockerd 启动完成。

References

GoPackages - http 源码导读

server

image

server 与 conn 等接口

type Server struct {
    Addr              string        // 要监听的 TCP 地址
    Handler           Handler       // 调用的 handler, 如果为空则用 http.DefaultServeMux
    TLSConfig         *tls.Config   // 用于 ServeTLS 和 ListenAndServeTLS
    ReadTimeout       time.Duration // 读取完整 request (包括 body) 的最大时长,可以和 ReadHeaderTimeout 同时使用
    ReadHeaderTimeout time.Duration // 读取 request headers 的最大时长
    WriteTimeout      time.Duration // 写 response 的最大时长
    IdleTimeout       time.Duration // 当 keepalive 开启时等待下个 request 的最大时长,此值为空时使用 ReadTimeout 值代替,ReadTimeout 也为空使用 ReadHeaderTimeout 代替
    MaxHeaderBytes    int           // 解析 request headers 里键值对的最大字节数(包含请求行),不限制 body. 如果为 0, 使用 DefaultMaxHeaderBytes 代替
    TLSNextProto      map[string]func(*Server, *tls.Conn, Handler) // 当'应用层协议协商(NPN/ALPN)'时发生协议升级时,TLSNextProto 需要指定可选的 function 去接管 TLS 连接
    ConnState         func(net.Conn, ConnState) // 指定一个可选的钩子函数,由 client 连接状态改变触发
    ErrorLog          *log.Logger   // 指定一个可选的 logger 接收错误日志. 如果为空则由 log 包接管
    disableKeepAlives int32         // 在 SetKeepAlivesEnabled 中设置,为 1 表示取消长连接,为 0 保持长连接(默认)
    inShutdown        int32         // 非零代表 in Shutdown
    nextProtoOnce     sync.Once     // 设置 HTTP/2
    nextProtoErr      error         // http2.ConfigureServer 的结果
    mu                sync.Mutex
    listeners         map[net.Listener]struct{}
    activeConn        map[*conn]struct{}
    doneChan          chan struct{} // doneChan 代表任务结束
    onShutdown        []func()      // 通过 RegisterOnShutdown 注册,在 Shutdown 时调用当中的钩子函数
}

// 此接口由 ResponseWriters 执行去检测连接是否已断开,此机制允许客户端断开后服务端取消一个长连接
type CloseNotifier interface {
    CloseNotify() <-chan bool
}

// conn 代表服务端的 HTTP 连接
type conn struct {
    server     *Server
    cancelCtx  context.CancelFunc   // 撤销连接层的 context,读写出错时会调用
    rwc        net.Conn             // 
    remoteAddr string               // rwc.RemoteAddr().String()
    tlsState   *tls.ConnectionState // TLS 连接状态,nil 代表非 TSL
    werr       error                // rwc 写入时的首个错误(bufw 写入时)
    r          *connReader          // 一个 *conn 使用的 io.reader 封装,存有 bufr 的读取内容
    bufr       *bufio.Reader        // 从 r 读取
    bufw       *bufio.Writer        // 要写入 checkConnErrorWriter{c} 的缓冲
    lastMethod string
    curReq     atomic.Value // 存入 *response (response 中包含 request)
    curState   atomic.Value // 存入 ConnState
    mu         sync.Mutex   // 保护 hijackedv
    hijackedv  bool         // 代表连接是否已经被 hijacke
}

// 一个 ctx 带有一个截止期限,一个取消信号,或者其他绑定值
// 其函数可以被多个 goroutines 同时使用
// 一个请求过来时可能会涉及到多个 goroutines,Ctx 可以控制关闭与之相关联和派生出的子 ctx 相关联的 goroutines
type Context interface {
	// Deadline 方法是获取设置的截止时间,第一个返回值是截止时间,到了这个时间点,Context会自动发起取消请求;
	// 第二个返回值 ok==false 时表示没有设置截止时间,如果需要取消的话,需要调用 cancel 函数进行取消,取消操作包括派生出去的子 Ctx
	Deadline() (deadline time.Time, ok bool)
	// 在 goroutine 中,如果该方法返回的 chan 可以读取,则意味着 parent context 已经发起了取消请求,
	// 我们通过 Done 方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源
    Done() <-chan struct{}
    // 如果 Done 还没关闭,Err 返回 nil
    // 如果 Done 以及关闭,返回非空 err,告知 Ctx 因何取消
    Err() error
    Value(key interface{}) interface{} // 键值对形式,与 Ctx 绑定,可以为空
}

监听函数

// Serve 接收 listener 上过来的连接,并为每个连接创建 service 线程
// 在 service 线程中会读取 request 并调用 srv.Handler 进行服务
// handler 参数一般传 nil 就行,代表使用的是 DefaultServeMux
func Serve(l net.Listener, handler Handler) error { // HTTPS: ServeTLS(l net.Listener, handler Handler, certFile, keyFile string) error
	srv := &Server{Handler: handler}
	return srv.Serve(l) // HTTPS: srv.ServeTLS(l, certFile, keyFile)
}

// func HelloServer(w http.ResponseWriter, req *http.Request) {
//     io.WriteString(w, "hello, world!\n")
// }
//
// func main() {
//     http.HandleFunc("/hello", HelloServer)
//     log.Fatal(http.ListenAndServe(":12345", nil))
// }
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

// err := http.ListenAndServeTLS(":10443", "cert.pem", "key.pem", nil)
// HTTPS 方式,可以使用 crypto/tls 中的 generate_cert.go 生成 cert.pem 和 key.pem
func ListenAndServeTLS(addr, certFile, keyFile string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServeTLS(certFile, keyFile)
}

// ListenAndServe 监听 srv.Addr 地址上的 tcp 网络,然后调用 Serve 服务连接,连接会设置 keep-alives
// 如果 srv.Addr 为空则用 ":http" 代替
// ListenAndServe 总是返回非空 err
func (srv *Server) ListenAndServe() error {
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	// HTTP: 
	return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
	
	// HTTPS 方式调用 ListenAndServeTLS(certFile, keyFile string) error
	// 与 ListenAndServe 类似,只是最后要关闭 ln 并返回 srv.ServeTLS
	// defer ln.Close() 
	// return srv.ServeTLS(tcpKeepAliveListener{ln.(*net.TCPListener)}, certFile, keyFile)
}

server 的服务函数

func (srv *Server) ServeTLS(l net.Listener, certFile, keyFile string) error {
	// 在 srv.Serve 之前尝试设置 HTTP/2
	// setupHTTP2_ServeTLS 中调用 onceSetNextProtoDefaults_Serve,只有 srv.TLSNextProto 为 nil 时才可以设置 HTTP/2
	if err := srv.setupHTTP2_ServeTLS(); err != nil {
		return err
	}

	config := cloneTLSConfig(srv.TLSConfig)
	if !strSliceContains(config.NextProtos, "http/1.1") { // strSliceContains 判断是否包含字符串
		config.NextProtos = append(config.NextProtos, "http/1.1")
	}

	configHasCert := len(config.Certificates) > 0 || config.GetCertificate != nil
	if !configHasCert || certFile != "" || keyFile != "" {
		var err error
		config.Certificates = make([]tls.Certificate, 1)
		config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) // LoadX509KeyPair 解析证书,文件中必须含有 PEM 编码数据
		// PEM (Privacy Enhancement Message),定义见 RFC1421,是一种基于 base64 的编码格式
		if err != nil {
			return err
		}
	}

	tlsListener := tls.NewListener(l, config)
	return srv.Serve(tlsListener)
}

// 若启用 HTTP/2,在调用 Serve 前需要根据 listener's TLS Config 初始化 srv.TLSConfig
// Serve 总是返回非空的 err,在 Shutdown 或 Close 后返回 ErrServerClosed
// Close 是立即关闭 Server 和与之相关的 listeners 和 connections,而 shutdown 是逐步关闭 listeners 和闲置的 connections,两者不会管已被 hijack 的连接
func (srv *Server) Serve(l net.Listener) error {
	defer l.Close()
	if fn := testHookServerServe; fn != nil { // 如果钩子函数 testHookServerServe 非空则调用
		fn(srv, l)
	}
	var tempDelay time.Duration // accept 失败时 sleep 多长时间

    // setupHTTP2_Serve 和 setupHTTP2_ServeTLS 两者都是调用 onceSetNextProtoDefaults() 去尝试设置 HTTP/2
    // 只是考虑到多并发情况下的 Serve 请求,setupHTTP2_Serve 采用了更保守的政策去设置 HTTP/2
    // setupHTTP2_Serve 先调用 shouldConfigureHTTP2ForServe 判断是否应该为 Server.Serve 设置 HTTP/2
    // shouldConfigureHTTP2ForServe 中如果 srv.TLSConfig 为 nil 或者 srv.TLSConfig.NextProtos 包含 "h2" 字样返回真,否则返回假,
	if err := srv.setupHTTP2_Serve(); err != nil {
		return err
	}

	srv.trackListener(l, true) // 将 l 添加进 server.listeners
	defer srv.trackListener(l, false) // 结束后删去 l

	baseCtx := context.Background() // baseContext 会一直存在,但没有值也没有 deadline,用于主函数或者初始化或者测试或者顶层接收请求的 context
	ctx := context.WithValue(baseCtx, ServerContextKey, srv)
	// WithValue 返回 baseCtx 的副本,副本内的值是一个键值对 ServerContextKey - srv
	// ServerContextKey = &contextKey{"http-server"} 与其绑定的 value 类型为 *Server
	for {
		rw, e := l.Accept() // 接收到连接
		if e != nil {
			select {
			case <-srv.getDoneChan(): // server 已关闭
				return ErrServerClosed
			default:
			}
			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)
		// conn.setState 根据传入的状态调用 trackConn 来设置 server.activeConn 集合,再改变当前 conn.curState
		// 如果 server 设置了 ConnState 这个钩子函数,就调用
		c.setState(c.rwc, StateNew)
		go c.serve(ctx)
	}
}

server.Serve 最后调用 conn.serve,在此函数中调用 serverHandler{c.server}.ServeHTTP(w, w.req) 转入路由模块

func (c *conn) serve(ctx context.Context) {
	c.remoteAddr = c.rwc.RemoteAddr().String()
	ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
	// LocalAddrContextKey = &contextKey{"local-addr"} 与其绑定的 value 类型是 net.Addr
	defer func() {
		if err := recover(); err != nil && err != ErrAbortHandler {
			const size = 64 << 10 // 64 KB
			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() { // 已经被 hijack 的连接不用管理,由 hijack 的调用者处理
			c.close()
			c.setState(c.rwc, StateClosed)
		}
	}()

	if tlsConn, ok := c.rwc.(*tls.Conn); ok { // HTTPS
		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() // 获取当前 TLS 连接的详细信息
		// NegotiatedProtocol 协商的协议,validNPN 判断 proto 是否属于 "", "http/1.1", "http/1.0" 之一,不属于返回真
		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
		}
	}

	// HTTP/1.x following

	ctx, cancelCtx := context.WithCancel(ctx)
	// WithCancel 返回 &c, func() { c.cancel(true, Canceled) }
	// ctx.cancel close ctx.done 取消所有 ctx 的 children,如果第一个参数为 true,则把 ctx 从其 parent 的 children 列表删去
	c.cancelCtx = cancelCtx
	defer cancelCtx() // 关闭 ctx,以及相关 goroutines

	c.r = &connReader{conn: c}
	c.bufr = newBufioReader(c.r)
	c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

	for {
		w, err := c.readRequest(ctx) // 读取 request 返回 response 和可能的 err
		if c.r.remain != c.server.initialReadLimitSize() { // remain 代表 io.reader 剩余空间,initialReadLimitSize 返回 int64(srv.MaxHeaderBytes > 0 ? srv.MaxHeaderBytes : DefaultMaxHeaderBytes) + 4096
			c.setState(c.rwc, StateActive) // StateActive 代表连接已经从 request 读到数据
		}
		if err != nil {
			const errorHeaders = "\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\n"

			if err == errTooLarge { // errors.New("http: request too large")
				const publicErr = "431 Request Header Fields Too Large"
				fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
				c.closeWriteAndWait()
				// closewrite flush 所有缓存的数据并发送一个 FIN 包(如果客户端是通过 TCP 连接的),表示我们这边已结束,然后 sleep 500 ms
				return
			}
			if isCommonNetReadError(err) {
				// err 是否是 io.EOF 或者是网络超时 (net.Error) 或者是读 request 的 net.OpError 之一
				return
			}

			publicErr := "400 Bad Request"
			if v, ok := err.(badRequestError); ok {
				publicErr = publicErr + ": " + string(v)
			}

			fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
			return
		}

		// request Header : Expect 100 Continue
		req := w.req
		if req.expectsContinue() {
			if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
				// after first '100 Continue' request, wrapper response with 'HTTP/1.1 100 Continue'
				req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
			}
		} else if req.Header.get("Expect") != "" {
			w.sendExpectationFailed() // response with status code 417 (Expectation Failed)
			return
		}

		c.curReq.Store(w)

		if requestBodyRemains(req.Body) { // 之后是否还能从 body 读取到数据,true 表示能继续读(未到 io.EOF)
			registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead) // 当 body 读到 EOF,调用传入的 startBackgroundRead 函数
		} else { // 长连接下 HTTP 管线化请求时的处理
			if w.conn.bufr.Buffered() > 0 {
				// [HTTP pipelining](https://zh.wikipedia.org/wiki/HTTP%E7%AE%A1%E7%B7%9A%E5%8C%96)
				w.conn.r.closeNotifyFromPipelinedRequest() // closeNotify()
			}
			w.conn.r.startBackgroundRead()
		}

		serverHandler{c.server}.ServeHTTP(w, w.req) // server.Handler == nil -> DefaultServeMux.ServeHTTP
		w.cancelCtx()
		if c.hijacked() {
			return
		}
		w.finishRequest()
		if !w.shouldReuseConnection() { // tcp 连接是否可以继续使用
			if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
				// requestBodyLimitHit 在 requestTooLarge 函数中设置,当此值为真,停止读取后续的 request 和输入
				// closedRequestBodyEarly 表示连接之前是否已关闭
				c.closeWriteAndWait()
			}
			return
		}
		c.setState(c.rwc, StateIdle) // StateIdle 表示此连接已处理完一个 request 并处于 keep-alive 状态,等待后续 request
		c.curReq.Store((*response)(nil))

		if !w.conn.server.doKeepAlives() { // doKeepAlives 判断是否满足 disableKeepAlives == 0 && inShutdown == 0 (处于 keep-alive 模式且不在 shutdown 状态)
			// We're in shutdown mode. We might've replied
			// to the user without "Connection: close" and
			// they might think they can send another
			// request, but such is life with HTTP/1.1.
			return
		}

		if d := c.server.idleTimeout(); d != 0 {
			c.rwc.SetReadDeadline(time.Now().Add(d))
			if _, err := c.bufr.Peek(4); err != nil {
				return
			}
		}
		// SetReadDeadline 设置后续读去调用的截止时间,如果传入零值表示不会 timeout
		c.rwc.SetReadDeadline(time.Time{})
	}
}

流程:
当一个请求 request 进来的时候,server 会依次根据 ServeMux.m 中的 string(路由表达式)来一个一个匹配,
如果找到了可以匹配的 muxEntry,就取出 muxEntry.h,这是个 handler,
调用 handler 中的 ServeHTTP(ResponseWriter, *Request)来组装 Response,并返回。


路由接口

// ResponseWriter 接口用于 HTTP handler 生成 response
// 在 Handler.ServeHTTP 返回后,ResponseWriter 不应该再被使用
type ResponseWriter interface {
    Header() Header             // Header() 返回 WriteHeader 要发送的 Header map 集合
    Write([]byte) (int, error)  // Write 写入响应的 body
    WriteHeader(statusCode int) // 这个方法发送 Response 的 Header 和传入的 HTTP 状态码
}

// Flusher 由 ResponseWriters 执行去允许 HTTP handler 将缓存中的数据推给客户端, 默认的 HTTP/1.x 和 HTTP/2 ResponseWriter 支持 Flusher,
// 但是 ResponseWriter 的封装可能会不支持,Handlers 在运行时需要测试是否支持此函数
// 即使 ResponseWriters 支持 Flush,如果客户端使用了 HTTP proxy,直到响应结束,缓存的数据也有可能到达不了客户端
type Flusher interface {
	Flush()
}

// Hijacker 接口由 ResponseWriters 执行去允许 HTTP handler 接管连接
// 默认的 ResponseWriter 支持 HTTP/1.x 连接下的 Hijacker,但是 HTTP/2 连接不支持,HTTP/2 多路复用等情况不适合使用 Hijack 。
// ResponseWriter 封装也可能不支持 Hijacker. Handlers 在运行时需要测试是否支持此函数
type Hijacker interface {
	Hijack() (net.Conn, *bufio.ReadWriter, error)
}

// ServeMux 类型是 HTTP 请求的路由规则转换器。它会将每一个接收的请求的 URL 与一个注册路由的列表进行匹配,并调用和 URL 最匹配的 handler.
// 匹配到多个时较长的模式优先于较短的模式,模式也可以主机名开始,表示只匹配该主机上的路径,指定主机的模式优先于一般的模式,
// ServeMux 还会规范化请求的 URL 路径,将任何包含"."或".."元素的请求重定向到等价的没有这两种元素的URL
type ServeMux struct {
	mu    sync.RWMutex // 读写锁
	m     map[string]muxEntry // 路由规则,一个 string 对应一个 mux 实体,这里的 string 就是注册的路由表达式
	hosts bool // 是否在任意的规则中带有 host 信息
}

type muxEntry struct {
    h        Handler // 这个路由表达式对应哪个 handler
    pattern  string  // 固定的、由根开始的路径,如 "/favicon.ico",或由根开始的子树,如 "/images/",也可以主机名开头
}

// 一个 Handler 响应一个 HTTP 请求
// ServeHTTP 应该将回复的头域和数据写入 ResponseWriter 接口然后返回。返回标志着该请求已经结束,HTTP服务端可以转移向该连接上的下一个请求。
// 在 ServeHTTP 调用结束之后或者并发执行时,使用 ResponseWriter 或者读取请求体是不可取的
// handler 应该第一时间读取请求体并作出应答,在向 ResponseWriter 写入数据后就不能读取 request body 了. 同时 handler 不应该修改传入的 request
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

// HandlerFunc(f) 是一个调用 f 的 handler
type HandlerFunc func(ResponseWriter, *Request)

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

请求 - 响应实例

这里实现了一个 404 not found 响应

func NotFound(w ResponseWriter, r *Request) { Error(w, "404 page not found", StatusNotFound) } // 定义 handler

func NotFoundHandler() Handler { return HandlerFunc(NotFound) }

server 导出的注册函数使用 DefaultServeMux 相应方法

func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	mux.Handle(pattern, HandlerFunc(handler))
}

func (mux *ServeMux) Handle(pattern string, handler Handler) {
	mux.mu.Lock()
	defer mux.mu.Unlock()

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

	if mux.m == nil {
		mux.m = make(map[string]muxEntry)
	}
	mux.m[pattern] = muxEntry{h: handler, pattern: pattern} // 注册成功

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

ServeHTTP 调用 Handler() 给 request 分派与 request URL 最匹配的 handler

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
	if r.RequestURI == "*" {
		if r.ProtoAtLeast(1, 1) { // ProtoAtLeast 判断是否大于等于协议最低标准,第一个参数是 major 版本号,第二个参数是 minor 版本号,即 http/1.1
			w.Header().Set("Connection", "close") // 小于要求则在响应头返回关闭信息
		}
		w.WriteHeader(StatusBadRequest) // 状态码 400
		return
	}
	h, _ := mux.Handler(r)
	h.ServeHTTP(w, r) // 调用对应 handler 的 ServeHTTP,即执行注册好的 handler 函数,比如 NotFound 函数
}
// Handler 通过判断 r.Method, r.Host, and r.URL.Path 返回与 request 对应的 handler
// 此函数总会返回非空的 handler. 如果 path 不符合规范形式,返回的是内部生成的重定向到规范路径的 handler
// 如果 host 包含端口,匹配 handlers 时会忽略端口。第二个参数返回已注册的与请求匹配的路由
// 如果没有已注册的 handler 与请求匹配, 则返回 ``page not found'' handler 和空的 pattern
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
	if r.Method == "CONNECT" {
		// redirectToPathSlash 判断 path 是否需要追加 "/",因为存在 "path + /"已注册但 "path"
		// 本身未注册的情况。如果需要追加 "/",则返回追加的 url 和 true
		if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
			return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
		}

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

	host := stripHostPort(r.Host) // 去掉 ":<port>"
	path := cleanPath(r.URL.Path) // 规范 path 格式,比如缺失多余'/'、存在相对路径'.'、'..'等

	if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
		return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
	}

    // 修改 request 的不规范路径 
	if path != r.URL.Path {
		_, pattern = mux.handler(host, path)
		url := *r.URL
		url.Path = path
		return RedirectHandler(url.String(), StatusMovedPermanently), pattern
	}

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

// 在 ServerMux.handler 中当匹配不到注册的路由时返回 NotFoundHandler
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
	mux.mu.RLock()
	defer mux.mu.RUnlock()

	if mux.hosts {
		h, pattern = mux.match(host + path) // match 根据完整 URL 优先匹配 handler
	}
	if h == nil {
		h, pattern = mux.match(path) // 如果 URL 匹配不到再根据路径匹配
	}
	if h == nil {
		h, pattern = NotFoundHandler(), ""
	}
	return
}

References

docker 源码阅读 (III) - Plugin

Plugin

插件系统,为 docker 提供了特定的扩展能力。Docker 的插件系统采用 HTTP 服务形式,全景图如下:

Graph Driver

Volumn 插件

以 volumn 插件为例,首先,定义 Driver 结构:

type Driver interface {
	Create(*CreateRequest) error
	List() (*ListResponse, error)
	Get(*GetRequest) (*GetResponse, error)
	Remove(*RemoveRequest) error
	Path(*PathRequest) (*PathResponse, error)
	Mount(*MountRequest) (*MountResponse, error)
	Unmount(*UnmountRequest) error
	Capabilities() *CapabilitiesResponse
}

插件实现者,只要实现该接口即可。在 go-plugins-helper 中定义的接口,与 docker 源码中的接口是完全一致的。

然后,通过 NewHandler 创建一个 Handler 结构:

func NewHandler(driver Driver) *Handler {
	h := &Handler{driver, sdk.NewHandler(manifest)}
	h.initMux()
	return h
}

type Handler struct {
	driver Driver
	sdk.Handler
}

Handler 结构通过路由调用 Driver 中方法即可:

func (h *Handler) initMux() {
	h.HandleFunc(createPath, func(w http.ResponseWriter, r *http.Request) {
		log.Println("Entering go-plugins-helpers createPath")
		req := &CreateRequest{}
		err := sdk.DecodeRequest(w, r, req)
		if err != nil {
			return
		}
		err = h.driver.Create(req)
		if err != nil {
			sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true)
			return
		}
		sdk.EncodeResponse(w, struct{}{}, false)
	})
	h.HandleFunc(removePath, func(w http.ResponseWriter, r *http.Request) {
		log.Println("Entering go-plugins-helpers removePath")
		req := &RemoveRequest{}
		err := sdk.DecodeRequest(w, r, req)
		if err != nil {
			return
		}
		err = h.driver.Remove(req)
		if err != nil {
			sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true)
			return
		}
		sdk.EncodeResponse(w, struct{}{}, false)
	})
	h.HandleFunc(mountPath, func(w http.ResponseWriter, r *http.Request) {
		log.Println("Entering go-plugins-helpers mountPath")
		req := &MountRequest{}
		err := sdk.DecodeRequest(w, r, req)
		if err != nil {
			return
		}
		res, err := h.driver.Mount(req)
		if err != nil {
			sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true)
			return
		}
		sdk.EncodeResponse(w, res, false)
	})
	h.HandleFunc(hostVirtualPath, func(w http.ResponseWriter, r *http.Request) {
		log.Println("Entering go-plugins-helpers hostVirtualPath")
		req := &PathRequest{}
		err := sdk.DecodeRequest(w, r, req)
		if err != nil {
			return
		}
		res, err := h.driver.Path(req)
		if err != nil {
			sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true)
			return
		}
		sdk.EncodeResponse(w, res, false)
	})
	h.HandleFunc(getPath, func(w http.ResponseWriter, r *http.Request) {
		log.Println("Entering go-plugins-helpers getPath")
		req := &GetRequest{}
		err := sdk.DecodeRequest(w, r, req)
		if err != nil {
			return
		}
		res, err := h.driver.Get(req)
		if err != nil {
			sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true)
			return
		}
		sdk.EncodeResponse(w, res, false)
	})
	h.HandleFunc(unmountPath, func(w http.ResponseWriter, r *http.Request) {
		log.Println("Entering go-plugins-helpers unmountPath")
		req := &UnmountRequest{}
		err := sdk.DecodeRequest(w, r, req)
		if err != nil {
			return
		}
		err = h.driver.Unmount(req)
		if err != nil {
			sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true)
			return
		}
		sdk.EncodeResponse(w, struct{}{}, false)
	})
	h.HandleFunc(listPath, func(w http.ResponseWriter, r *http.Request) {
		log.Println("Entering go-plugins-helpers listPath")
		res, err := h.driver.List()
		if err != nil {
			sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true)
			return
		}
		sdk.EncodeResponse(w, res, false)
	})

	h.HandleFunc(capabilitiesPath, func(w http.ResponseWriter, r *http.Request) {
		log.Println("Entering go-plugins-helpers capabilitiesPath")
		sdk.EncodeResponse(w, h.driver.Capabilities(), false)
	})
}

Reference

docker 源码阅读 (II) - Graph Driver

Graph Driver

概览

Graph Driver

初始化

d.graphDrivers = make(map[string]string)
layerStores := make(map[string]layer.Store)
if runtime.GOOS == "windows" {
	d.graphDrivers[runtime.GOOS] = "windowsfilter"
	if system.LCOWSupported() {
		d.graphDrivers["linux"] = "lcow"
	}
} else {
	driverName := os.Getenv("DOCKER_DRIVER")
	if driverName == "" {
		driverName = config.GraphDriver
	} else {
		logrus.Infof("Setting the storage driver from the $DOCKER_DRIVER environment variable (%s)", driverName)
	}
	d.graphDrivers[runtime.GOOS] = driverName // May still be empty. Layerstore init determines instead.
}

// ...

for operatingSystem, gd := range d.graphDrivers {
	layerStores[operatingSystem], err = layer.NewStoreFromOptions(layer.StoreOptions{
		Root: config.Root,
		MetadataStorePathTemplate: filepath.Join(config.Root, "image", "%s", "layerdb"),
		GraphDriver:               gd,
		GraphDriverOptions:        config.GraphOptions,
		IDMappings:                idMappings,
		PluginGetter:              d.PluginStore,
		ExperimentalEnabled:       config.Experimental,
		OS:                        operatingSystem,
	})
	if err != nil {
		return nil, err
	}
}

// As layerstore initialization may set the driver
for os := range d.graphDrivers {
	d.graphDrivers[os] = layerStores[os].DriverName()
}

情景分析

基本功能

  • Driver 注册
func Register(name string, initFunc InitFunc) error {
	if _, exists := drivers[name]; exists {
		return fmt.Errorf("Name already registered %s", name)
	}
	drivers[name] = initFunc // 注册 Driver 及对应的初始化函数

	return nil
}
  • 获取 Driver
func GetDriver(name string, pg plugingetter.PluginGetter, config Options) (Driver, error) {
	if initFunc, exists := drivers[name]; exists { // Driver 存在,执行初始化函数
		return initFunc(filepath.Join(config.Root, name), config.DriverOptions, config.UIDMaps, config.GIDMaps)
	}

	pluginDriver, err := lookupPlugin(name, pg, config) // Driver 不存在,查看 plugin
	if err == nil {
		return pluginDriver, nil
	}
        // 返回错误
	return nil, ErrNotSupported
}
  • 获取内建 Driver
func getBuiltinDriver(name, home string, options []string, uidMaps, gidMaps []idtools.IDMap) (Driver, error) {
	if initFunc, exists := drivers[name]; exists {
		return initFunc(filepath.Join(home, name), options, uidMaps, gidMaps)
	}
        // 不检查 plugin
	return nil, ErrNotSupported
}
  • New Driver
func New(name string, pg plugingetter.PluginGetter, config Options) (Driver, error) {
	if name != "" {
		// 指定了 Driver,直接返回
		return GetDriver(name, pg, config)
	}

	driversMap := scanPriorDrivers(config.Root) // 扫描本地已有驱动,config.Root 为系统目录
	list := strings.Split(priority, ",") // 驱动优先级列表

         // 检查 Host 是否已使用了驱动优先列表中的驱动
	for _, name := range list {
		if name == "vfs" {
			// don't use vfs even if there is state present.
			continue
		}
		if _, prior := driversMap[name]; prior {
                         // 查看 Driver 是否存在,初始化函数能否成功
			driver, err := getBuiltinDriver(name, config.Root, config.DriverOptions, config.UIDMaps, config.GIDMaps)
			if err != nil {
				return nil, err
			}

			// 本地有多个 Driver,告知用户明确选择一个
			if len(driversMap)-1 > 0 {
				var driversSlice []string
				for name := range driversMap {
					driversSlice = append(driversSlice, name)
				}

				return nil, fmt.Errorf("%s contains several valid graphdrivers: %s; Please cleanup or explicitly choose storage driver (-s <DRIVER>)", config.Root, strings.Join(driversSlice, ", "))
			}

			return driver, nil
		}
	}

	// Host 没有使用优先列表中的 Driver,按顺序选择第一个可用 Driver
	for _, name := range list {
		driver, err := getBuiltinDriver(name, config.Root, config.DriverOptions, config.UIDMaps, config.GIDMaps)
		if err != nil {
			if IsDriverNotSupported(err) {
				continue
			}
			return nil, err
		}
		return driver, nil
	}

	// 优先列表不含名为 name 的驱动,检查全部 Driver
	for name, initFunc := range drivers {
		driver, err := initFunc(filepath.Join(config.Root, name), config.DriverOptions, config.UIDMaps, config.GIDMaps)
		if err != nil {
			if IsDriverNotSupported(err) {
				continue
			}
			return nil, err
		}
		return driver, nil
	}
        // 没有找到 Driver
	return nil, fmt.Errorf("No supported storage backend found")
}

NaiveDiffDriver

NaiveDiffDriver 提供一个通用的基于文件系统的 diff 工具。其结构为:

type NaiveDiffDriver struct {
	ProtoDriver
	uidMaps []idtools.IDMap
	gidMaps []idtools.IDMap
}

由于包含一个 ProtoDriver 同时,实现了 DiffDriver,所以,NaiveDiffDriver 满足 Driver 接口。NaiveDiffDriver 可以将一个 ProtoDriver 封装为 Driver.

创建代码为:

func NewNaiveDiffDriver(driver ProtoDriver, uidMaps, gidMaps []idtools.IDMap) Driver {
	return &NaiveDiffDriver{ProtoDriver: driver,
		uidMaps: uidMaps,
		gidMaps: gidMaps}
}

DiffDriver 方法

Diff

func (gdw *NaiveDiffDriver) Diff(id, parent string) (arch io.ReadCloser, err error) {
	startTime := time.Now()
	driver := gdw.ProtoDriver

	layerRootFs, err := driver.Get(id, "") // 获取一个 ContainerFS 实例
	if err != nil {
		return nil, err
	}
	layerFs := layerRootFs.Path() // 获取文件路径

	defer func() {
		if err != nil {
			driver.Put(id) // Get,Put 要成对出现
		}
	}()

	if parent == "" { // parent 为空
		archive, err := archive.Tar(layerFs, archive.Uncompressed)
		if err != nil {
			return nil, err
		}
		return ioutils.NewReadCloserWrapper(archive, func() error {
			err := archive.Close()
			driver.Put(id)
			return err
		}), nil
	}

	parentRootFs, err := driver.Get(parent, "")
	if err != nil {
		return nil, err
	}
	defer driver.Put(parent)

	parentFs := parentRootFs.Path() // 获取 parent 路径

	changes, err := archive.ChangesDirs(layerFs, parentFs) // 对比路径差异
	if err != nil {
		return nil, err
	}

	archive, err := archive.ExportChanges(layerFs, changes, gdw.uidMaps, gdw.gidMaps)
	if err != nil {
		return nil, err
	}

	return ioutils.NewReadCloserWrapper(archive, func() error { // 导出差异
		err := archive.Close()
		driver.Put(id)

		// NaiveDiffDriver compares file metadata with parent layers. Parent layers
		// are extracted from tar's with full second precision on modified time.
		// We need this hack here to make sure calls within same second receive
		// correct result.
		time.Sleep(time.Until(startTime.Truncate(time.Second).Add(time.Second)))
		return err
	}), nil
}
  • 当没有 parent 目录时,使用了 Tar 方法,Tar 封装压缩方式后调用 TarWithOptions 方法:
func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) {
	// Fix the source path to work with long path names. This is a no-op
	// on platforms other than Windows.
	srcPath = fixVolumePathPrefix(srcPath)

        // 构建 exclude 匹配模式
	pm, err := fileutils.NewPatternMatcher(options.ExcludePatterns)
	if err != nil {
		return nil, err
	}

	pipeReader, pipeWriter := io.Pipe()
        // 通过 options.Compression(压缩方式),封装 PipeWriter,压缩后内容会写入 PipeWriter
	compressWriter, err := CompressStream(pipeWriter, options.Compression)
	if err != nil {
		return nil, err
	}

	go func() {
                // 创建 tar 编码器
		ta := newTarAppender(
			idtools.NewIDMappingsFromMaps(options.UIDMaps, options.GIDMaps),
			compressWriter,
			options.ChownOpts,
		)
                // whiteout 是 UFS 系统的一种权限
		ta.WhiteoutConverter = getWhiteoutConverter(options.WhiteoutFormat)

		defer func() {
			// Make sure to check the error on Close.
			if err := ta.TarWriter.Close(); err != nil {
				logrus.Errorf("Can't close tar writer: %s", err)
			}
			if err := compressWriter.Close(); err != nil {
				logrus.Errorf("Can't close compress writer: %s", err)
			}
			if err := pipeWriter.Close(); err != nil {
				logrus.Errorf("Can't close pipe writer: %s", err)
			}
		}()

		// ta.Buffer 是在 newTarAppender 中获取的,在外部释放
		defer pools.BufioWriter32KPool.Put(ta.Buffer)

		stat, err := os.Lstat(srcPath) // 获取文件信息
		if err != nil {
			return
		}

                // 如果当前路径不是目录,调整 srcPath
		if !stat.IsDir() {
			if len(options.IncludeFiles) > 0 {
				logrus.Warn("Tar: Can't archive a file with includes")
			}

			dir, base := SplitPathDirEntry(srcPath)
			srcPath = dir
			options.IncludeFiles = []string{base}
		}
                // IncludeFiles 默认为 "."
		if len(options.IncludeFiles) == 0 {
			options.IncludeFiles = []string{"."}
		}

		seen := make(map[string]bool)

		for _, include := range options.IncludeFiles {
			rebaseName := options.RebaseNames[include]

                        // 拼接路径形成当前要遍历的目录, srcPath/include
			walkRoot := getWalkRoot(srcPath, include)
                        // 递归遍历目录
			filepath.Walk(walkRoot, func(filePath string, f os.FileInfo, err error) error {
				if err != nil {
					logrus.Errorf("Tar: Can't stat file %s to tar: %s", srcPath, err)
					return nil
				}
                                // 获取 filePath 相对于 srcPath 的相对路径
				relFilePath, err := filepath.Rel(srcPath, filePath)
				if err != nil || (!options.IncludeSourceDir && relFilePath == "." && f.IsDir()) {
					// 错误发生或相对路径为 "."
					return nil
				}

				if options.IncludeSourceDir && include == "." && relFilePath != "." {
					relFilePath = strings.Join([]string{".", relFilePath}, string(filepath.Separator))
				}

				skip := false

                                // 只有当 include 与相对路径不匹配时,才使用 Exclude 模式匹配,来决定是否 skip
				if include != relFilePath {
					skip, err = pm.Matches(relFilePath)
					if err != nil {
						logrus.Errorf("Error matching %s: %v", relFilePath, err)
						return err
					}
				}
                                // 由于函数在 Walk 中递归调用,当需要跳过时,返回错误;不需要跳过时,返回 nil
				if skip { 
					// 不是目录,不跳过
					if !f.IsDir() {
						return nil
					}

					// 没有例外模式匹配规则,跳过
					if !pm.Exclusions() {
						return filepath.SkipDir
					}

					dirSlash := relFilePath + string(filepath.Separator)

					for _, pat := range pm.Patterns() {
						if !pat.Exclusion() {
							continue
						}
						if strings.HasPrefix(pat.String()+string(filepath.Separator), dirSlash) {
							// 找到了例外规则,不跳过
							return nil
						}
					}

					// 跳过
					return filepath.SkipDir
				}
                                // 已经遍历一次
				if seen[relFilePath] {
					return nil
				}
                                // 标示首次遍历
				seen[relFilePath] = true

				// 需要重命名
				if rebaseName != "" {
					var replacement string
					if rebaseName != string(filepath.Separator) {
						replacement = rebaseName
					}

					relFilePath = strings.Replace(relFilePath, include, replacement, 1)
				}
                                // 将重命名后文件,加入 tar 文件
				if err := ta.addTarFile(filePath, relFilePath); err != nil {
					logrus.Errorf("Can't add file %s to tar: %s", filePath, err)
					// if pipe is broken, stop writing tar stream to it
					if err == io.ErrClosedPipe {
						return err
					}
				}
				return nil
			})
		}
	}()

	return pipeReader, nil // 返回 PipeReader,读取内容由上面的 goroutine 生成
}
  • 当 parent 目录存在时,通过 ChangesDirs 方法,将不同部分导出:
func ChangesDirs(newDir, oldDir string) ([]Change, error) {
	var (
		oldRoot, newRoot *FileInfo
	)
        // ... 
	oldRoot, newRoot, err := collectFileInfoForChanges(oldDir, newDir)
        // ... 
	return newRoot.Changes(oldRoot), nil
}

代码主要分为两步调用,首先调用 collectFileInfoForChanges 获取变化信息,然后将变化信息转换为 Change 结构。Change 相关结构定义如下:

type Change struct {
	Path string
	Kind ChangeType
}

type ChangeType int

const (
	// ChangeModify represents the modify operation.
	ChangeModify = iota
	// ChangeAdd represents the add operation.
	ChangeAdd
	// ChangeDelete represents the delete operation.
	ChangeDelete
)

collectFileInfoForChanges 会构建如下图所示的目录结构:

Change

构建过程如下:

func (w *walker) walk(path string, i1, i2 os.FileInfo) (err error) {
	// 递归过程,构建 FileInfo 树
	if path != "/" {
		if err := walkchunk(path, i1, w.dir1, w.root1); err != nil {
			return err
		}
		if err := walkchunk(path, i2, w.dir2, w.root2); err != nil {
			return err
		}
	}

	is1Dir := i1 != nil && i1.IsDir()
	is2Dir := i2 != nil && i2.IsDir()

        // 目录是否在同一设备上
	sameDevice := false
	if i1 != nil && i2 != nil {
		si1 := i1.Sys().(*syscall.Stat_t)
		si2 := i2.Sys().(*syscall.Stat_t)
		if si1.Dev == si2.Dev {
			sameDevice = true
		}
	}

	// 都不是目录,递归结束
	if !is1Dir && !is2Dir {
		return nil
	}

	var names1, names2 []nameIno
	if is1Dir {
                // 获取目录下全部文件
		names1, err = readdirnames(filepath.Join(w.dir1, path))
		if err != nil {
			return err
		}
	}
	if is2Dir {
                // 获取目录下全部文件
		names2, err = readdirnames(filepath.Join(w.dir2, path)) // getdents(2): fs access
		if err != nil {
			return err
		}
	}

	// 获取 name1 与 name2 的并集,name1, name2 本身有序(字母增序);合并结果 name 有序
        // 归并排序中 merge 部分
	var names []string
	ix1 := 0
	ix2 := 0

	for {
		if ix1 >= len(names1) {
			break
		}
		if ix2 >= len(names2) {
			break
		}

		ni1 := names1[ix1]
		ni2 := names2[ix2]

		switch bytes.Compare([]byte(ni1.name), []byte(ni2.name)) {
		case -1: // ni1 < ni2 -- advance ni1
			// we will not encounter ni1 in names2
			names = append(names, ni1.name)
			ix1++
		case 0: // ni1 == ni2
			if ni1.ino != ni2.ino || !sameDevice {
				names = append(names, ni1.name)
			}
			ix1++
			ix2++
		case 1: // ni1 > ni2 -- advance ni2
			// we will not encounter ni2 in names1
			names = append(names, ni2.name)
			ix2++
		}
	}
	for ix1 < len(names1) {
		names = append(names, names1[ix1].name)
		ix1++
	}
	for ix2 < len(names2) {
		names = append(names, names2[ix2].name)
		ix2++
	}

	// 拼接文件路径,递归处理
	for _, name := range names {
		fname := filepath.Join(path, name)
		var cInfo1, cInfo2 os.FileInfo
		if is1Dir {
			cInfo1, err = os.Lstat(filepath.Join(w.dir1, fname))
			if err != nil && !os.IsNotExist(err) {
				return err
			}
		}
		if is2Dir {
			cInfo2, err = os.Lstat(filepath.Join(w.dir2, fname))
			if err != nil && !os.IsNotExist(err) {
				return err
			}
		}
		if err = w.walk(fname, cInfo1, cInfo2); err != nil {
			return err
		}
	}
	return nil
}

FileInfo 新节点创建:

func walkchunk(path string, fi os.FileInfo, dir string, root *FileInfo) error {
	// ...
        // 找到最近的父节点, eg: path 为 a/b/c/d 时,c 对应的 FileInfo 为最近父节点
	parent := root.LookUp(filepath.Dir(path))
	if parent == nil {
		return fmt.Errorf("walkchunk: Unexpectedly no parent for %s", path)
	}
	info := &FileInfo{
		name:     filepath.Base(path),
		children: make(map[string]*FileInfo),
		parent:   parent,
	}
	cpath := filepath.Join(dir, path)
        // 获取文件信息
	stat, err := system.FromStatT(fi.Sys().(*syscall.Stat_t))
	// ...
	info.stat = stat
	info.capability, _ = system.Lgetxattr(cpath, "security.capability")
	parent.children[info.name] = info
	return nil
}

构建完毕后,Change 将对遍历两侧目录树,检测到变更结果集。

Changes

func (gdw *NaiveDiffDriver) Changes(id, parent string) ([]archive.Change, error) {
        // 获取 proto driver
	driver := gdw.ProtoDriver

	layerRootFs, err := driver.Get(id, "")
	if err != nil {
		return nil, err
	}
	defer driver.Put(id)

	layerFs := layerRootFs.Path()
	parentFs := ""

        // 如果 parent 不为空,获取 parent
	if parent != "" {
		parentRootFs, err := driver.Get(parent, "")
		if err != nil {
			return nil, err
		}
		defer driver.Put(parent)
		parentFs = parentRootFs.Path()
	}

        // 返回变更
	return archive.ChangesDirs(layerFs, parentFs)
}

ApplyDiff

func (gdw *NaiveDiffDriver) ApplyDiff(id, parent string, diff io.Reader) (size int64, err error) {
	// 获取 layerFS,代码略
	if size, err = ApplyUncompressedLayer(layerFs, diff, options); err != nil {
		return
	}
	// ...

	return
}

不难看出,核心代码就是将 diff 中读取的内容(tar 文件),应用到文件系统中。通过如下代码:

cmd := reexec.Command("docker-applyLayer", dest)
cmd.Stdin = layer
cmd.Env = append(cmd.Env, fmt.Sprintf("OPT=%s", data))

将方法 docker-applyLayer 启动,并将 Stdin 指向 diff(同为 io.Reader)。最终应用代码为:

func applyLayer() {

	var (
		tmpDir  string
		err     error
		options *archive.TarOptions
	)
	runtime.LockOSThread()
	flag.Parse()

	inUserns := rsystem.RunningInUserNS()
	if err := chroot(flag.Arg(0)); err != nil {
		fatal(err)
	}

	// We need to be able to set any perms
	oldmask, err := system.Umask(0)
	defer system.Umask(oldmask)
	if err != nil {
		fatal(err)
	}

	if err := json.Unmarshal([]byte(os.Getenv("OPT")), &options); err != nil {
		fatal(err)
	}

	if inUserns {
		options.InUserNS = true
	}

	if tmpDir, err = ioutil.TempDir("/", "temp-docker-extract"); err != nil {
		fatal(err)
	}

	os.Setenv("TMPDIR", tmpDir)
	size, err := archive.UnpackLayer("/", os.Stdin, options)
	os.RemoveAll(tmpDir)
	if err != nil {
		fatal(err)
	}

	encoder := json.NewEncoder(os.Stdout)
	if err := encoder.Encode(applyLayerResponse{size}); err != nil {
		fatal(fmt.Errorf("unable to encode layerSize JSON: %s", err))
	}

	if _, err := flush(os.Stdin); err != nil {
		fatal(err)
	}

	os.Exit(0)
}

References

HTTP 扩展阅读(I) - 报文结构

Request

RFC2616 中定义的 HTTP Request 消息体结构:

Request = Request-Line             // 请求行
          *(( general-header       // 通用首部
            | request-header       // 请求首部
            | entity-header )CRLF) // 实体首部
          CRLF
          [ message-body ]

image

一个 HTTP 的 request 消息以一个请求行开始,从第二行开始是 headers (️每个键值对都以 CRLF 结尾),接下来是一个 CRLF 开头的空行,表示 header 结束,最后是消息主体。

请求行的定义如下:

Request-Line = Method SP Request-URI SP HTTP-Version CRLF

Method = "OPTIONS" | "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "TRACE" | "CONNECT" | extension-method

Request-URI = "*" | absoluteURI | abs_path | authotity(CONNECT)

请求方法(也叫请求动作)

  • GET 请求会显示请求指定的资源。一般来说 GET 方法应该只用于数据的读取,而不应当用于会产生副作用的非幂等的操作中。GET 会方法请求指定的页面信息,并返回响应主体,GET 被认为是不安全的方法,因为 GET 方法会被网络蜘蛛等任意的访问。
  • HEAD 方法与 GET 方法一样,都是向服务器发出指定资源的请求。但是,服务器在响应 HEAD 请求时不会回传资源的内容部分,即:响应主体。这样,我们可以不传输全部内容的情况下,就可以获取服务器的响应头信息。HEAD 方法常被用于客户端查看服务器的性能。
  • POST 请求会向指定资源提交数据,请求服务器进行处理,如:表单数据提交、文件上传等,请求数据会被包含在请求体中。POST 方法是非幂等的方法,因为这个请求可能会创建新的资源或/和修改现有资源。
  • PUT 请求会身向指定资源位置上传其最新内容,PUT 方法是幂等的方法。通过该方法客户端可以将指定资源的最新数据传送给服务器取代指定的资源的内容。
  • DELETE 请求用于请求服务器删除所请求 URI 所标识的资源。DELETE 请求后指定资源会被删除,DELETE方法也是幂等的。
  • CONNECT 方法是 HTTP/1.1 协议预留的,能够将连接改为管道方式的代理服务器。通常用于SSL加密服务器的链接与非加密的HTTP代理服务器的通信。
  • OPTIONS 请求与 HEAD 类似,一般也是用于客户端查看服务器的性能。 这个方法会请求服务器返回该资源所支持的所有 HTTP 请求方法,该方法会用'*'来代替资源名称,向服务器发送 OPTIONS 请求,可以测试服务器功能是否正常。JavaScript 的 XMLHttpRequest 对象进行 CORS 跨域资源共享时,就是使用 OPTIONS 方法发送嗅探请求,以判断是否有对指定资源的访问权限。
  • TRACE 请求服务器回显其收到的请求信息,该方法主要用于 HTTP 请求的测试或诊断。

请求地址

Host 会与 Request-URI 一起来作为 Request 消息的接收者判断请求资源的条件,方法如下:

  • 如果 Request-URI 是绝对地址(absoluteURI),这时请求里的主机存在于 Request-URI 里。忽略任何出现在请求里 Host 头域值

  • 假如 Request-URI 不是绝对地址(absoluteURI),并且请求包括一个 Host 头域,则主机由该 Host 头域值决定

  • 假如由规则1或规则2定义的主机是一个无效的主机,则应当以一个 400(错误请求)错误消息返回

Request-URI = "*" | absoluteURI | abs_path | authority(CONNECT)
  • "*" 代表请求不指向特定的资源,而是服务器本身,且只在所使用的方法没必要应用到资源时允许。一个例子可能是 OPTIONS * HTTP/1.1
  • absoluteURI 绝对地址,比如 GET http://www.w3.org/pub/WWW/TheProject.html HTTP/1.1
  • abs_path 绝对路径,“/”代表服务器根
  • 只有 CONNECT 方法使用 authority 形式,由域名和可选端口组成的 URL,比如 CONNECT developer.mozilla.org:80 HTTP/1.1

URI(统一资源标识符,Uniform Resource Identifier)

URI 就是由某个协议方案表示的资源的定位标识符,这个协议可以使 htpp、https、ftp等,比如:

ftp://ftp.is.co.za/rfc/rfc1808.txt
http://www.ietf.org/rfc/rfc2396.txt
ldap://[2001:db8::7]/c=GB?objectClass?one
mailto:[email protected]
news:comp.infosystems.www.servers.unix
tel:+1-816-555-1212 telnet://192.0.2.16:80/
urn:oasis:names:specification:docbook:dtd:xml:4.1.2

绝对 URI 的格式应该是这样的:

http://user:[email protected]:80/dir/index.html?uid=1#ch1

协议://登录信息@服务器地址:端口号/文件路径?查询字符串#片段标识符

与 URL(统一资源定位符,Universal Resource Locator) 和 URN(统一资源名,Uniform Resource Name)的关系:

URL 和 URN 是 URI 的两个子集,URI 唯一标识了文件资源对象(类似身份证),URN 标识资源名称,URL 标识资源地址

Response

Response = Status-Line             // 状态行
           *(( general-header      // 通用首部
            | response-header      // 响应首部
            | entity-header )CRLF) // 实体首部
           CRLF
           [ message-body ]

image

response 第一行是状态行,包含状态码 Status-Code,Reason-Phrase 是状态码的简单文本描述(比如 200 - OK、404 - Not Found)

Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF

Status-Code:

  • 1xx: 信息性——收到请求,继续处理
  • 2xx: 成功性——成功收到、理解并接受行动
  • 3xx: 重定向——必须采取进一步行动来完成请求
  • 4xx: 客户端错误——请求包含错误语法或不能完成
  • 5xx: 服务器错误——服务器没有成功完成显然有效的请求

消息体

HTTP 消息的 message-body(如果存在)用于挟带与请求或响应相关联的 entity-body.
message-body 只有在应用了 transfer-coding 时,通过 Transfer-Encoding 头部域指出,与 entity-body 不同。

message-body = entity-body | <entity-body encoded as per Transfer-Encoding>

headers

general-header = Cache-Control      // 控制缓存的行为,比如 `private, max-age=0, no-cache`
                | Connection        // 控制不再转发给代理的首部字段或者管理持久连接(Keep-Alive 或者 close)
                | Date              // 表明创建 HTTP 报文的日期和时间
                | Pragma            // 唯一字段 `no-cache`,用于兼容 HTTP/1.1 之前的版本,客户端会要求所有的中间服务器不返回缓存的资源
                | Trailer           // 事先说明在报文主体后记录了哪些首部字段。该首部字段可应用在 HTTP/1.1 版本分块传输编码时
                | Transfer-Encoding // 规定了传输报文主体时采用的编码方式,比如 `chunked`
                | Upgrade           // 检测 HTTP 协议及其他协议是否可使用更高的 版本进行通信,其参数值可以用来指定一个完全不同的通信协议,使用首部字段 Upgrade 时,还需要额外指定 Connection:Upgrade
                | Via               // 追踪客户端与服务器之间的请求和响应报文 的传输路径
                | Warning           // 从 HTTP/1.0 的响应首部(Retry-After)演变过来的。该首部通常会告知用户一些与缓存相关的问题的警告,字段格式:`[警告码][警告的主机:端口号]“[警告内容]”([日期时间])`
request-header = Accept               // 通知服务器,用户代理能够处理的媒体类型及媒体类型的相对优先级(可用权重 q=0~1 来表示相对优先级)。可使用 type/subtype 这种形式,一次指定多种媒体类型,支持通配符。例如 `text/html, text/plain, text/css, image/jpeg, video/mpeg, application/zip`等
                | Accept-Charset      // 通知服务器用户代理支持的字符集及字符集的相对优先顺序,可一次性指定多种字符集
                | Accept-Encoding     // 告知服务器用户代理支持的内容编码及内容编码的优先级顺序。可一次性指定多种内容编码,例如: `gzip, compress, deflate, identity`
                | Accept-Language     // 告知服务器用户代理能够处理的自然语言集及优先级顺序,例如: `zh-cn,zh;q=0.7,en-us,en;q=0.3` 优先返回中文版响应
                | Authorization       // 告知服务器,用户代理的认证信息(证书值),例如: `Basic dWVub3NlbjpwYXNzd29yZA==`
                | Expect              // 告知服务器,客户端期望出现的某种特定行为。因服务器无法理解客户端的期望作出回应而发生错误时,会返回状态码 417 Expectation Failed。
                | From                // 告知服务器使用用户代理的用户的电子邮件地址
                | Host                // Host 会告知服务器,请求的资源所处的互联网主机名和端口号
                | If-Match            // 只有当 If-Match 的字段值跟资源的 ETag 值匹配一致时,服务器才会接受请求,否则返回状态码 412 Precondition Failed
                | If-Modified-Since   // If-Modified-Since 字段指定的日期时间后,资源发生了更新,服务器才会接受请求,否则返回状态码 304 Not Modified
                | If-None-Match       // If-None-Match 的字段值与 ETag 值不一致时,可处理该请求。与 If-Match 首部字段的作用相反
                | If-Range            // 告知服务器若指定的 If-Range 字段值(ETag 值或者时间)和请求资源的 ETag 值或时间相一致时,则作为范围请求处理。反之,则返回全体资源。
                | If-Unmodified-Since // 与 If-Modified-Since 的作用相反
                | Max-Forwards        // 通过 TRACE 方法或 OPTIONS 方法,发送包含首部字段 Max-Forwards 的请求时,该字段以十进制整数形式指定可经过的服务器最大数目,服务器转发请求之前,Max-Forwards 的值减 1 后重新赋值。当服务器接收到 Max-Forwards 值为 0 的请求时,则不再进行转发,直接返回响应。
                | Proxy-Authorization // 接收到从代理服务器发来的认证质询时,客户端会发送包含首部字段 Proxy-Authorization 的请求,以告知服务器认证所需要的信息。
                | Range               // 对于只需获取部分资源的范围请求,包含首部字段 Range 即可告知服 务器资源的指定范围。例如: `bytes=5001-10000` 表示请求获取从第 5001 字节至第 10000 字节的资源。服务器会在处理请求之后返回 206 Partial Content 的响应。无法处理该范围请求时,则返回 200 OK 的响应及全部资源。
                | Referer             // 告知服务器请求的原始资源的 URI。
                | TE                  // 告知服务器客户端能够处理响应的传输编码方式及相对优先级。它和首部字段 Accept-Encoding 的功能很相像,但是用于传输编码。
                | User-Agent          // 将创建请求的浏览器和用户代理名称等信息传达给服务器
response-header = Accept-Ranges      // 告知客户端服务器是否能处理范围请求,以指定获取服务器端某个部分的资源。可处理范围请求时指定其为 bytes,反之则指定其为 none。
                | Age                // 告知客户端,源服务器在多久前创建了响应。字段值的单位为秒。若创建该响应的服务器是缓存服务器,Age 值是指缓存后的响应再次发起认证到认证完成的时间值。代理创建响应时必须加上首部字段 Age。
                | ETag               // 告知客户端实体标签(Entity Tag)。它是一种可将资源以字符串形式做唯一性标识的方式。服务器会为每份资源分配对应的 ETag 值。有强弱之分,弱Etag以'w/'开头,强校验的ETag匹配要求两个资源内容逐字节相同,包括所有其他实体字段(如Content-Language)不发生变化;弱校验只需要确认资源内容相同即可,忽略细微差别比如修改时间等
                | Location           // 将响应接收方引导至某个与请求 URI 位置不同的资源。该字段会配合 3xx :Redirection 的响应,提供重定向的 URI。
                | Proxy-Authenticate // 由代理服务器所要求的认证信息发送给客户端。
                | Retry-After        // 告知客户端应该在多久之后再次发送请求。主要配合状态码 503 Service Unavailable 响应,或 3xx Redirect 响应一起使用。字段值可以指定为具体的日期时间(Wed, 04 Jul 2012 06:34:24 GMT 等格式),也可以是创建响应后的秒数
                | Server             // 告知客户端当前服务器上安装的 HTTP 服务器应用程序的信息。例如: `Apache/2.2.6 (Unix) PHP/5.2.5`
                | Vary               // 可对缓存进行控制。源服务器会向代理服务器传达关于本地缓存使用方法的命令。
                | WWW-Authenticate   // WWW-Authenticate 用于 HTTP 访问认证。它会告知客户端适用于访问请求 URI 所指定资源的认证方案(Basic 或是 Digest)和带参数提示的质询(challenge)
entity-header = Allow              // 服务端通知客户端能够支持 Request-URI 指定资源的所有 HTTP 方法。当服务器接收到不支持的 HTTP 方法时,会以状态码 405 Method Not Allowed 作为响应返回。与此同时,还会把所有能支持的 HTTP 方法写入首部字段 Allow 后返回
                | Content-Encoding // 告知客户端服务器对实体的主体部分选用的内容编码方式,字段值参见 Accept-Encoding
                | Content-Language // 会告知客户端,实体主体使用的自然语言,例如: `zh-CN`
                | Content-Length   // 表明了实体主体部分的大小(单位是字节)。对实体主体进行内容编码传输时,不能再使用 Content-Length 首部字段
                | Content-Location // 给出与报文主体部分相对应的 URI。和首部字段 Location 不同,Content-Location 表示的是报文主体返回资源对应的 URI。
                | Content-MD5      // 客户端会对接收的报文主体执行相同的 MD5 算法,然后与首部字段 Content-MD5 的字段值比较,其目的在于检查报文主体在传输过程中是否保持完整,以及确认传输到达。无法检测出恶意篡改
                | Content-Range    // 能告知客户端作为响应返回的实体的哪个部分符合范围请求。字段值以字节为单位,表示当前发送部分及整个实体大小。例如: `bytes 5001-10000/10000`
                | Content-Type     // 说明了实体主体内对象的媒体类型。和首部字段 Accept 一样,字段值用 type/subtype 形式赋值。例如: `text/html; charset=UTF-8`
                | Expires          // 将资源失效的日期告知客户端。当首部字段 Cache-Control 有指定 max-age 指令时,比起首部字段 Expires,会优先处理 max-age 指令
                | Last-Modified    // 指明资源最终修改的时间
                | extension-header // 允许定义额外的 entity-header 域而不改变协议,但不能假设接收方认识这些域。接收方应该忽略未识别的头域,但透明代理必须转发它

References

GoPackages - RPC 源码导读

1. server

image

service 与 server 结构体

type service struct {
	name   string                 // 服务名
	rcvr   reflect.Value          // 服务中函数的接收者
	typ    reflect.Type           // 接收者类型
	method map[string]*methodType // 已注册的函数集
}

type Server struct {
	serviceMap sync.Map   // 服务对象集合
	reqLock    sync.Mutex // 请求锁用来保护 freeReq
	freeReq    *Request
	respLock   sync.Mutex // 响应锁保护 freeResp
	freeResp   *Response
}

rpc.Register 调用 DefaultServer.Register,主要实现在内部函数 register 中

// Register 在 server 中注册并发布 receiver 的函数集时需满足以下条件:
//   * 函数和函数的类型名是已导出的
//   * 两个参数都是导出类型(或內建类型)
//   * 第二个参数是指针
//   * 函数只有一个类型为 error 的返回类型
// 如果 receiver 不是导出的类型或者没有符合条件的函数,将会返回一个错误。Register 将会使用 log 包记录出现的 error
// 客户端使用 "Type.Method" 的格式来调用函数,比如上文例子中 Arith.Multiply,这里的 Type 是 receiver 的具体类型.
func (server *Server) register(rcvr interface{}, name string, useName bool) error {
	// 新起一个 service 服务对象
	s := new(service)
    s.typ = reflect.TypeOf(rcvr)
    s.rcvr = reflect.ValueOf(rcvr)
    sname := reflect.Indirect(s.rcvr).Type().Name() // 默认服务名是 receiver 的反射类型
    // 在 server.Register 中调用 register(rcvr, "", false)
    // 在 server.RegisterName 中调用 register(rcvr, name, true)
    // 这里使用的 name 可以指定服务对象名,客户端调用 rpc 服务时可以使用 "name.Method" 代替原来的 "Type.Method"
    if useName {
        sname = name
    }
    if sname == "" {
        s := "rpc.Register: no service name for type " + s.typ.String()
        log.Print(s)
        return errors.New(s)
    }
    if !isExported(sname) && !useName {
        s := "rpc.Register: type " + sname + " is not exported"
        log.Print(s)
        return errors.New(s)
    }
    s.name = sname

	// 判断传入的接口对象的函数集是否符合 RPC 规范
	s.method = suitableMethods(s.typ, true)

	if len(s.method) == 0 {
        str := ""

        // 如果满足条件的函数集为空,根据 s.typ 的指针地址对象是否有符合条件的函数返回错误说明
        method := suitableMethods(reflect.PtrTo(s.typ), false)
        if len(method) != 0 {
            // 此错误说明传入的 s.typ 不符合条件,应该传入 *s.typ
            str = "rpc.Register: type " + sname + " has no exported methods of suitable type (hint: pass a pointer to value of that type)"
        } else {
            str = "rpc.Register: type " + sname + " has no exported methods of suitable type"
        }
        log.Print(str)
        return errors.New(str)
    }

    // LoadOrStore 会检查 sync.Map 类型对象中是否存在传入的键名,如果存在则返回相应的值和 true
    // 反之会先存入键值对再返回值和 false
	if _, dup := server.serviceMap.LoadOrStore(sname, s); dup {
		return errors.New("rpc: service already defined: " + sname)
	}
	return nil
}

注册后监听请求

// Accept 从监听器上接收获取到的连接并服务每个连接的请求
// Accept 在监听器返回非空的错误前都处于阻塞态
// 调用者一般应使用 goroutine 启用 Accept,比如 `go server.Accept(l)`
func (server *Server) Accept(lis net.Listener) {
	for {
		conn, err := lis.Accept()
		if err != nil {
			log.Print("rpc.Serve: accept:", err.Error())
			return
		}
		go server.ServeConn(conn)
	}
}

在 Accept 中调用 ServeConn 函数进行服务

// ServeConn 在一个连接上运行 server 并服务该连接.
// ServeConn 在服务该连接到客户端挂起的期间处于阻塞态.
// 一般另起线程来调用本函数,比如 `go server.ServeConn(conn)` (Accept 函数中有调用)
// ServeConn 在该连接上使用 gob 包的有线格式 (参见 gob 包) .
// 如需使用其他备份编解码器, 可以使用 ServeCodec 函数.
func (server *Server) ServeConn(conn io.ReadWriteCloser) {
	buf := bufio.NewWriter(conn)
	srv := &gobServerCodec{
		rwc:    conn,
		dec:    gob.NewDecoder(conn),
		enc:    gob.NewEncoder(buf),
		encBuf: buf,
	}
	server.ServeCodec(srv)
}

// ServeCodec 与 ServeConn 类似,只是使用了指定的编解码器来解码 requests 和编码 responses
func (server *Server) ServeCodec(codec ServerCodec) {
	sending := new(sync.Mutex)
	wg := new(sync.WaitGroup)
	for {
		service, mtype, req, argv, replyv, keepReading, err := server.readRequest(codec) // 读取请求信息
		if err != nil {
			if debugLog && err != io.EOF {
				log.Println("rpc:", err)
			}
			if !keepReading { // 如果读请求的 header 就出错了,keepReading 为 false,跳出此循环;如果能读取 header 信息便继续
				break
			}
			// 发送一个 response 表示此请求无效
			if req != nil {
				server.sendResponse(sending, req, invalidRequest, codec, err.Error())
				server.freeRequest(req)
			}
			continue
		}
		wg.Add(1)
		go service.call(server, sending, wg, mtype, req, argv, replyv, codec)
	}
	// 没有 request 后需等待 response 发送完成再关闭 codec
	wg.Wait()
	codec.Close()
}

客户端请求某个服务后,服务端在 ServeCodec 中通过调用 service.call 调用相应服务

func (s *service) call(server *Server, sending *sync.Mutex, wg *sync.WaitGroup, mtype *methodType, req *Request, argv, replyv reflect.Value, codec ServerCodec) {
	if wg != nil {
		defer wg.Done()
	}
	mtype.Lock()
	mtype.numCalls++
	mtype.Unlock()
	function := mtype.method.Func
	// 执行函数, 返回新的值给 reply
	returnValues := function.Call([]reflect.Value{s.rcvr, argv, replyv})
	// 返回值里的错误
	errInter := returnValues[0].Interface()
	errmsg := ""
	if errInter != nil {
		errmsg = errInter.(error).Error()
	}
	// 发送响应,然后释放当前请求节点
	server.sendResponse(sending, req, replyv.Interface(), codec, errmsg)
	server.freeRequest(req)
}

HTTP 方式

// ServeHTTP 实现一个用于回应 RPC 请求的 http.Handler
func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	if req.Method != "CONNECT" {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		w.WriteHeader(http.StatusMethodNotAllowed)
		io.WriteString(w, "405 must CONNECT\n")
		return
	}
	conn, _, err := w.(http.Hijacker).Hijack() // 让调用者主动接管连接
	if err != nil {
		log.Print("rpc hijacking ", req.RemoteAddr, ": ", err.Error())
		return
	}
	io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n")
	server.ServeConn(conn)
}

// HandleHTTP 注册 server 的 RPC 信息到 rpcPath 上,注册 server 的 debug 信息到 debugPath 上
// HandleHTTP 会注册到 http.DefaultServeMux上
// 之后,仍需要调用 http.Serve(),一般会另起线程:"go http.Serve(l, nil)"
func (server *Server) HandleHTTP(rpcPath, debugPath string) {
	http.Handle(rpcPath, server)
	http.Handle(debugPath, debugHTTP{server})
}

2. client

image

结构体

// Call 代表一个活跃的 RPC.
type Call struct {
	ServiceMethod string      // 调用的服务名
	Args          interface{} // 函数传入参数 (*struct)
	Reply         interface{} // 函数返回结果 (*struct)
	Error         error       // 结束后的错误状态
	Done          chan *Call  // 非空表示一个 rpc 调用结束
}

// Client 代表一个 RPC 客户端,同一个客户端可能有多个未返回的调用,也可能被多个 go 线程同时使用
type Client struct {
	codec ClientCodec
	reqMutex sync.Mutex // 保护 request
	request  Request
	mutex    sync.Mutex // 保护 seq
	seq      uint64 // 一个序列值,request 和 response 会以此标识
	pending  map[uint64]*Call // 等待响应的 Call 集合
	closing  bool // 用户已调用 Close
	shutdown bool // 服务器已告知停止
}

// ClientCodec 接口实现了 RPC 会话的客户端一侧 RPC 请求的写入和 RPC 响应的读取。
// 客户端调用 WriteRequest 来写入请求到连接,然后成对调用 ReadRsponseHeader 和
// ReadResponseBody 以读取响应。客户端在结束该连接的事务时调用 Close 方法。
// ReadResponseBody 可以使用 nil 参数调用,以强制回复的主体被读取然后丢弃。
type ClientCodec interface {
	// WriteRequest 必须能安全的被多个go程同时使用
	WriteRequest(*Request, interface{}) error
	ReadResponseHeader(*Response) error
	ReadResponseBody(interface{}) error

	Close() error
}

客户端获取 Client 对象

// DialHTTP 通过地址连向一个 HTTP RPC server (建立 HTTP 连接)
func DialHTTP(network, address string) (*Client, error) {
	return DialHTTPPath(network, address, DefaultRPCPath)
}

// DialHTTPPath 通过地址和路径连向一个 HTTP RPC server
func DialHTTPPath(network, address, path string) (*Client, error) {
	var err error
	conn, err := net.Dial(network, address)
	if err != nil {
		return nil, err
	}
	io.WriteString(conn, "CONNECT "+path+" HTTP/1.0\n\n")

	// 在切换 RPC 协议前需要保证成功的 HTTP 响应
	resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"})
	if err == nil && resp.Status == connected {
		return NewClient(conn), nil
	}
	if err == nil {
		err = errors.New("unexpected HTTP response: " + resp.Status)
	}
	conn.Close()
	return nil, &net.OpError{
		Op:   "dial-http",
		Net:  network + " " + address,
		Addr: nil,
		Err:  err,
	}
}

// Dial 通过指定的地址连向一个 RPC server (建立 TCP 连接)
func Dial(network, address string) (*Client, error) {
	conn, err := net.Dial(network, address)
	if err != nil {
		return nil, err
	}
	return NewClient(conn), nil
}

新建 Client 对象

func NewClient(conn io.ReadWriteCloser) *Client {
	encBuf := bufio.NewWriter(conn)
	client := &gobClientCodec{conn, gob.NewDecoder(conn), gob.NewEncoder(encBuf), encBuf}
	return NewClientWithCodec(client)
}

func NewClientWithCodec(codec ClientCodec) *Client {
	client := &Client{
		codec:   codec,
		pending: make(map[uint64]*Call),
	}
	go client.input() // 另起线程接收 response
	return client
}

从连接中读取 response, 根据 seq 找到 pending 集合中对应的 Call 对象,获取响应内容,Done 结束

func (client *Client) input() {
	var err error
	var response Response
	for err == nil {
		response = Response{}
		err = client.codec.ReadResponseHeader(&response)
		if err != nil {
			break
		}
		seq := response.Seq
		client.mutex.Lock()
		call := client.pending[seq] // 等待队列中的对应当前 response 的序列号的 Call 对象
		delete(client.pending, seq)
		client.mutex.Unlock()

		switch {
		case call == nil:
			// call == nil 代表等待序列中没有对应的 Call 对象,一般意味着 WriteRequest 时失败了并且 call 已经被删去
			// 返回的 response 是读取错误 request 的错误信息
			err = client.codec.ReadResponseBody(nil)
			if err != nil {
				err = errors.New("reading error body: " + err.Error())
			}
		case response.Error != "":
			// 获取到一个错误响应. 将这个传给 Call;
			call.Error = ServerError(response.Error)
			err = client.codec.ReadResponseBody(nil)
			if err != nil {
				err = errors.New("reading error body: " + err.Error())
			}
			call.done()
		default:
			err = client.codec.ReadResponseBody(call.Reply)
			if err != nil {
				call.Error = errors.New("reading body " + err.Error())
			}
			call.done()
		}
	}
	// 关闭等待中的 calls.
	client.reqMutex.Lock()
	client.mutex.Lock()
	client.shutdown = true
	closing := client.closing
	if err == io.EOF {
		if closing {
			err = ErrShutdown
		} else {
			err = io.ErrUnexpectedEOF
		}
	}
	for _, call := range client.pending {
		call.Error = err
		call.done()
	}
	client.mutex.Unlock()
	client.reqMutex.Unlock()
	if debugLog && err != io.EOF && !closing {
		log.Println("rpc: client protocol error:", err)
	}
}

Client 使用不同函数去调取 rpc 服务,Go 可以异步执行,Call 是同步的

// Go 异步地执行函数. 本方法 Call 结构体类型指针的返回值代表该次远程调用. 
// 通道类型的参数 done 会在本次调用完成时发出信号(通过返回本次 Go 方法的返回值)
// 如果 done 为nil,Go 会申请一个新的通道(写入返回值的 Done 字段)
// 如果 done 非nil,done 必须有缓冲,否则 Go 方法会崩溃。
func (client *Client) Go(serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call {
	call := new(Call)
	call.ServiceMethod = serviceMethod
	call.Args = args
	call.Reply = reply
	if done == nil {
		done = make(chan *Call, 10) // buffered.
	} else {
		// 如果调用者传的 done != nil,则必须确保通道有足够的缓冲来给多个同步 RPCs 使用
		// 如果通道完全没有缓冲,最好不要去运行
		if cap(done) == 0 {
			log.Panic("rpc: done channel is unbuffered")
		}
	}
	call.Done = done
	client.send(call)
	return call
}

// Call 调用传入名的远程服务,并等待结束返回结果和错误状态
func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error {
	call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done
	return call.Error
}

Go 中调用的函数

func (client *Client) send(call *Call) {
	client.reqMutex.Lock()
	defer client.reqMutex.Unlock()

	// 将 call 转入等待集合
	client.mutex.Lock()
	if client.shutdown || client.closing {
		call.Error = ErrShutdown
		client.mutex.Unlock()
		call.done()
		return
	}
	seq := client.seq
	client.seq++
	client.pending[seq] = call
	client.mutex.Unlock()

	// 将 request 编码并发送
	client.request.Seq = seq
	client.request.ServiceMethod = call.ServiceMethod
	err := client.codec.WriteRequest(&client.request, call.Args)
	if err != nil {
		client.mutex.Lock()
		call = client.pending[seq]
		delete(client.pending, seq)
		client.mutex.Unlock()
		if call != nil {
			call.Error = err
			call.done()
		}
	}
}

3. jsonrpc

jsonrpc 主要将 gob 序列化工具换成 json 序列化工具,主要函数还是调用 server 里的 FuncWithCodec 函数,原理基本一致

References

dockerd 源码阅读 (IV) - Image 管理

镜像管理

概览

  • 核心数据结构

Architecture

  • ImageService 注册路由

Routes

情景分析

List 本地镜像

当执行 docker images 是,触发路由:

router.NewGetRoute("/images/json", r.getImagesJSON),

该路由处理函数为:

func (s *imageRouter) getImagesJSON(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
        // ...
	images, err := s.backend.Images(imageFilters, httputils.BoolValue(r, "all"), false)
	// ...
	return httputils.WriteJSON(w, http.StatusOK, images)
}

可以看到,核心代码为 ImageService.Images 方法:

func (i *ImageService) Images(imageFilters filters.Args, all bool, withExtraAttrs bool) ([]*types.ImageSummary, error) {
	var (
		allImages    map[image.ID]*image.Image
		err          error
		danglingOnly = false
	)

        // 验证 imageFilters 合法性
	if err := imageFilters.Validate(acceptedImageFilterTags); err != nil {
		return nil, err
	}

        // ... 

	if danglingOnly {
		allImages = i.imageStore.Heads() // 仅获取无 tag 镜像
	} else {
		allImages = i.imageStore.Map() // 获取全部镜像
	}

        // before, since 的基准镜像获取 (略)

	images := []*types.ImageSummary{}
	var imagesMap map[*image.Image]*types.ImageSummary
	var layerRefs map[layer.ChainID]int
	var allLayers map[layer.ChainID]layer.Layer
	var allContainers []*container.Container

	for id, img := range allImages {
                // before, since 过滤 (代码略)
                // label 过滤
		if imageFilters.Contains("label") {
			// ...
			if !imageFilters.MatchKVList("label", img.Config.Labels) { // 对比镜像 label
				continue
			}
		}

		// 过滤操作系统不能支持的镜像
		if !system.IsOSSupported(img.OperatingSystem()) {
			continue
		}

		layerID := img.RootFS.ChainID()
		var size int64
		if layerID != "" {
			l, err := i.layerStores[img.OperatingSystem()].Get(layerID)
			if err != nil {
				// The layer may have been deleted between the call to `Map()` or
				// `Heads()` and the call to `Get()`, so we just ignore this error
				if err == layer.ErrLayerDoesNotExist {
					continue
				}
				return nil, err
			}

			size, err = l.Size()
			layer.ReleaseAndLog(i.layerStores[img.OperatingSystem()], l)
			if err != nil {
				return nil, err
			}
		}

		newImage := newImage(img, size) // 创建 ImageSummary 记录

		for _, ref := range i.referenceStore.References(id.Digest()) {
			// reference 过滤
                         // repo 信息
			if _, ok := ref.(reference.Canonical); ok {
				newImage.RepoDigests = append(newImage.RepoDigests, reference.FamiliarString(ref))
			}
                         // tag 信息
			if _, ok := ref.(reference.NamedTagged); ok {
				newImage.RepoTags = append(newImage.RepoTags, reference.FamiliarString(ref))
			}
		}
		if newImage.RepoDigests == nil && newImage.RepoTags == nil {
			if all || len(i.imageStore.Children(id)) == 0 {

				// dangling, reference 过滤
                                 // repo, tag 默认信息
				newImage.RepoDigests = []string{"<none>@<none>"}
				newImage.RepoTags = []string{"<none>:<none>"}
			} else {
				continue
			}
		} else if danglingOnly && len(newImage.RepoTags) > 0 {
			continue
		}

		// 获取额外信息,由于传入参数为 false,此部分代码略

		images = append(images, newImage)
	}

	// 获取额外信息,由于传入参数为 false,此部分代码略

	sort.Sort(sort.Reverse(byCreated(images)))

	return images, nil
}

Pull

主要数据结构关系图:

Pull Architecture

Pull 镜像流程如下:

Pull Image

  • ImageService.PullImage
func (i *ImageService) PullImage(ctx context.Context, image, tag, os string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error {
	image = strings.TrimSuffix(image, ":")

        // 获取镜像的 Reference
	ref, err := reference.ParseNormalizedNamed(image)
	if err != nil {
		return errdefs.InvalidParameter(err)
	}

        // tag 不空,调整 Reference
	if tag != "" {
		var dgst digest.Digest
		dgst, err = digest.Parse(tag)
		if err == nil {
			ref, err = reference.WithDigest(reference.TrimNamed(ref), dgst)
		} else {
			ref, err = reference.WithTag(ref, tag)
		}
	}

	return i.pullImageWithReference(ctx, ref, os, metaHeaders, authConfig, outStream)
}
  • pullImageWithReference
func (i *ImageService) pullImageWithReference(ctx context.Context, ref reference.Named, os string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error {
	// 下载进度显示
	progressChan := make(chan progress.Progress, 100)
        // 写入结束 channel
	writesDone := make(chan struct{})

	ctx, cancelFunc := context.WithCancel(ctx)

        // 启动 goroutine 显示下载进度,下载完成、失败后,关闭 writesDone
	go func() {
		progressutils.WriteDistributionProgress(cancelFunc, outStream, progressChan)
		close(writesDone)
	}()

	// ...

	imagePullConfig := &distribution.ImagePullConfig{
		Config: distribution.Config{
			MetaHeaders:      metaHeaders,
			AuthConfig:       authConfig,
			ProgressOutput:   progress.ChanOutput(progressChan),
			RegistryService:  i.registryService,
			ImageEventLogger: i.LogImageEvent,
			MetadataStore:    i.distributionMetadataStore,
			ImageStore:       distribution.NewImageConfigStoreFromStore(i.imageStore),
			ReferenceStore:   i.referenceStore,
		},
		DownloadManager: i.downloadManager,
		Schema2Types:    distribution.ImageTypes,
		OS:              os,
	}

	err := distribution.Pull(ctx, ref, imagePullConfig)
	close(progressChan)
	<-writesDone
	return err
}
  • Pull

首先,根据传入的 ref 获取镜像下载地址:

repoInfo, err := imagePullConfig.RegistryService.ResolveRepository(ref)

通过 repoInfo 获取可用的 endpoints:

endpoints, err := imagePullConfig.RegistryService.LookupPullEndpoints(reference.Domain(repoInfo.Name))

再遍历 endpoints,执行下载操作:

for _, endpoint := range endpoints {
	// 检查 Schema 是否匹配,代码略

        // 检查 https 设置是否匹配
	if endpoint.URL.Scheme != "https" {
		if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS {
			continue
		}
	}

        // 使用当前 endpoint,创建 puller 对象
	puller, err := newPuller(endpoint, repoInfo, imagePullConfig)
	if err != nil {
		lastErr = err
		continue
	}

	// ...

        // 执行 pull 操作
	if err := puller.Pull(ctx, ref, imagePullConfig.OS); err != nil {
		fallback := false
		select {
		case <-ctx.Done():
		default:
			// 是否需要 fallback,代码略
		}
		if fallback {
			if _, ok := err.(ErrNoSupport); !ok {
				discardNoSupportErrors = true
				lastErr = err
			} else if !discardNoSupportErrors {
				lastErr = err
			}
			continue
		}
                // 不需要 fallback,直接返回错误
		return TranslatePullError(err, ref)
	}

	imagePullConfig.ImageEventLogger(reference.FamiliarString(ref), reference.FamiliarName(repoInfo.Name), "pull")
	return nil
}

我们以 API V2 为例,继续跟进:

func (p *v2Puller) Pull(ctx context.Context, ref reference.Named, os string) (err error) {
        // 创建 distribution.Repository 实例
	p.repo, p.confirmedV2, err = NewV2Repository(ctx, p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "pull")
	// ...
        // 执行 pull 操作
	if err = p.pullV2Repository(ctx, ref, os); err != nil {
		if _, ok := err.(fallbackError); ok {
			return err
		}
		if continueOnError(err, p.endpoint.Mirror) {
			return fallbackError{
				err:         err,
				confirmedV2: p.confirmedV2,
				transportOK: true,
			}
		}
	}
	return err
}

在 pullV2Repository 中,根据 tag 下载对应镜像:

for _, tag := range tags {
	tagRef, err := reference.WithTag(ref, tag)
	if err != nil {
		return err
	}
	pulledNew, err := p.pullV2Tag(ctx, tagRef, os)
	if err != nil {
		// Since this is the pull-all-tags case, don't
		// allow an error pulling a particular tag to
		// make the whole pull fall back to v1.
		if fallbackErr, ok := err.(fallbackError); ok {
			return fallbackErr.err
		}
		return err
	}
	layersDownloaded = layersDownloaded || pulledNew
}

区块链原理 - 初识区块链

区块链是一个具有共享状态的密码性安全交易的单机。

  • “密码性安全(Cryptographically secure)”是指用一个很难被解开的复杂数学机制算法来保证数字货币生产的安全性。将它想象成类似于防火墙的这种。它们使得欺骗系统近乎是一个不可能的事情(比如:构造一笔假的交易,消除一笔交易等等)。
  • “交易的单机(Transactional singleton machine)”是指只有一个权威的机器实例为系统中产生的交易负责任。换句话说,只有一个全球真相是大家所相信的。
  • “具有共享状态(With shared-state)”是指在这台机器上存储的状态是共享的,对每个人都是开放的。

上面所说的复杂数学机制算法主要是指(1)密码学哈希函数,(2)非对称加密。简单的说,密码学哈希函数就是指任意长度的字符串、甚至文件体本身经过Hash函数工厂的加工,都会输出一个固定长度的字符串;同时,输入的字符串或者文件稍微做一丢丢的改动,Hash() 函数给出的输出结果都将发生翻天覆地的改变。注意,Hash()函数是公开的,任何人都能使用。

这个算法主要用于验证信息完整性——在一个信息后面放上这个信息的哈希值,收到信息之后收信人再算一遍哈希值,对比两者就知道这条信息是否被篡改过了。如果被篡改过,哪怕只有一bit,整个哈希值也会截然不同。而根据哈希函数的性质,没有人能够伪造出另一个消息具有同样的哈希值,也就是说篡改过的数据完全不可能通过哈希校验。

非对称加密就是指任何人手里都有两把钥匙,其中一把只有自己知道,叫做“私钥”,以及一把可以公布于众,叫做“公钥”;通过私钥加密的信息,必须通过公钥才能解密,连自己的私钥也无解。公钥可以通过私钥生成多把。

非对称加密除了用于信息加密之外,还有另一个用途,就是身份验证。因为通常情况我们假设一对公私钥,公钥是公开的,而私钥只有本人有,于是一个人如果有对应的私钥,我们就可以认定他是本人。其中一个重要的应用就是数字签名——某个消息后面,发信人对这个消息做哈希运算,然后用私钥加密。接着收信人首先对消息进行哈希运算,接着用相应的公钥解密数字签名,再对比两个哈希值,如果相同,就代表这个消息是本人发出的而且没有被篡改过。

单机不是说它不联网,是指全球只有一个机器或者叫系统,它是由全球所有的节点共同组成的,不属于任何组织或个人,也就是通常所说的去中心化。对于比特币来说这个系统存储着全球所有的交易信息,所以也有人把区块链叫做分布式账本,但区块链不是只可以存储交易,它还可以存储合约,数据等其他内容。

刚刚说了,区块链是一个分布式的系统,它也要解决分布式系统中的共识问题。在区块链之前,就已经有了很多关于分布式共识的研究,发明了许多共识算法。共识算法的核心在于如何解决某个变更在网络中是一致的,是被大家都承认的,同时这个信息是被确定的,不可推翻的。在区块链中则是让所有节点对于新增区块达成共识,也就是说,所有人都要认可新增的区块。

比特币区块链考虑的是公开匿名场景下的最坏保证,引入了工作量证明(Proof of Work)策略来规避少数人恶意破坏数据,并通过概率模型保证最后大家看到的就是合法的最长链。比特币网络需要所有试图参与者(矿工)都首先要付出挖矿的代价,进行算力消耗,越想拿到新区块的决定权,意味着抵押的算力越多。一旦失败,这些算力都会被没收掉,成为沉没成本。当网络中存在众多参与者时,个体试图拿到新区块决定权要付出的算力成本是巨大的,意味着进行一次作恶付出的代价已经超过可能带来的好处。简单来说比特币共识模型就是:模型中有公认的“价值”,每个节点说话都需要一定代价,诚实节点会受到奖励,而恶意节点由于只付出代价而收不到奖励,变相受到了惩罚。

然后我们来看一下比特币区块链是如何运行的,首先我们先从最基本的概念入手—公共账本。假如某几个人之间有频繁的金钱往来,每次交易结束后用现金支付可能会有点麻烦,你们想到了使用账本系统。每次交易后将交易信息写入账本,比如它记录了某个时间 A 转给了 B 1000 元等类似的信息。那么这个账本必须是公开的,每个人都可以查阅信息,同样的每个人也可以添加记录。等到了月底大家对账本的交易记录没有异议就会合计一下,假如某个人的交易显示为负值,则需要向系统交钱补上负值,如果为正值就可以从系统中取钱。

但这样的公共开放的系统存在一个问题,如何保证上面所记录的交易信息都是真实的呢?比如说 A 瞒着 B 在上面偷偷添加了一条 B 转给了 A 1000 元记录,也就是说我们怎么相信账本中所记载的信息都是准确无误的呢?这就用到了我们刚刚说的数字签名技术,记录者在添加每条记录时需在后加上数字签名。其他人通过公钥进行验证此记录是否由他本人添加,同时也可以防止他人对记录进行篡改。但这样仍存在一个问题,某个人可以将某条记录复制多次同样会导致系统出错。所以我们需要将每一条记录编号,这样哈希后的结果就是不一样的,也就是最终的数字签名也是不一样的,这样我们就保证了账本中所记载的信息都是准确无误,也就是解决了账本系统中的记录信任问题。

但这样虽然记录是没有问题的,但月底结算时如果出现有人无法支付欠款的情况该怎么办?为了解决这个问题,我们在每次添加交易时系统需要进行简单的验证,它会计算转账人的余额是否足够用来支出,假如不够,会拒绝添加此条记录。这样就可以避免出现上面提到的情况了。我们可以发现,我们完全不需要现实中的货币,只通过这个账本就可以进行所有的交易了。

现在我们的系统好像是可以解决各种问题了,但这样的一个完美的系统仍需运行在一个中心上,我们要把它运行在哪个可以信任的中心呢?这个系统要由谁来维护呢?这样的一看,我们刚刚解决的信任问题好像又出现了。。。问题好像出在了只有一个账本的身上,那假如我们让每个人都有一份账本,当某个人添加新纪录时他就向其他人广播这条记录,其他人收到消息后就记录在自己的账本中。这样我们的系统就变成了分布式的,看起来还是挺不错的。但还是存在问题,比如说 A 收到了一条 B 支付给 A 1000 元的消息,他怎么保证别人也收到了同样的一条消息呢,否则他就无法在接下来的交易中使用这 1000 元。而且即使别人收到相同的消息,但如何保证其他人接受的顺序是一致的呢?这是个比较麻烦的事情。

当每个人都拥有一份账本时,这个系统已经成为了分布式系统。我们刚刚提出来的问题其实就是分布式系统中的共识问题。我们刚刚讲过了,比特币区块链使用的是工作量证明策略来解决这个问题的。当系统中状态发生变化也就是有新纪录产生时,系统中可能会出现多个不同版本的账本,工作量证明策略选择相信消耗最多资源的那份账本。它基本的思路如下:假如某个人给了你一份交易记录并说,我发现一个数字,将这个数字放在交易记录的后面然后经过哈希算法(这里我们假设是SHA256)之后得到的哈希值前30位为零,我们可以想下找到这样一个数字有多难,SHA256产生的结果为256位,要求前30位为零的概率大概是二的三十次方分之一,差不多是十亿分之一。。。而且这个算法是无法逆向推倒的,所以为了找到这个值,除了暴力枚举外没有什么更好的办法了。而且当我们经过简单的计算验证发现它的哈希值确实符合要求时,我们有理由相信他是付出了很多的工作量的,这就是工作量证明。更加重要的是,这份工作量证明和这份交易记录紧密相关,假如你对记录做了某个微小的改动,哈希值将会发生很大的变化,就需要重新计算才能达到要求。

再回过头来看分布式账本,我们如果想要在系统中只保留一份账本的话,假如每一条交易记录都需要付出这么多的工作量显然是有些不合适的,我们可以将账单整理成区块,区块包含了一系列的交易记录已及相对应的工作量证明,其中不同区块的工作量证明的要求可能是不一样的,这个我们待会再说怎么计算,这里我们先以 60 个零为例,同需要在每份转账记录后加入发送者的数字签名一样,这里只有拥有工作量证明的区块才是合法的区块。为了保证这些区块的顺序,每个区块都要将前一个区块的哈希值加入到当前区块的头部,当你想要改变之前某个区块的内容或者交换区块顺序时,你就要重新计算这个区块以后的所有区块的工作量证明,这样整个系统就变成了区块的一套链,所以叫区块链。

在这种体系下,世界上任何一个人都可以成为区块的建立者,他们都可以接受网络中的交易信息并把它们整理成区块,然后花大量的计算完成工作量证明,一旦找到符合要求的值后,他们就将区块广播出去,为了奖励这个建立者的付出,当他完成区块的挖掘后,系统会奖励他一定数额的资产。这样看来,对于试图建立区块也就是通常所说的矿工来说,挖矿就像是买彩票,每个人都想尽快猜出那个数字,进而挖出区块拿到奖励。对于只是想通过他来交易的人来说,他只要接受矿工的广播然后把它加到自己链的末尾就可以了。但这样仍存在一个问题,假如某一时刻,我们收到了两个不同的区块链,我们应该相信哪一个呢?这种情况是很有可能出现的,虽然全网会尽力控制在一个周期内只有一个节点能够成功挖出区块,但是不能够完全避免多个节点同时挖出区块的可能性,这样同时计算出的矿工会沿着自己计算出来的链继续计算下一个区块,所以就会出现两条链,也叫分叉。遇到这种情况时,我们倾向于选择两条链中的较长的那一条,也就是付出工作量较多的那一条。如果目前两条链相等,可以等待下一个区块的产生,这样就可以做出选择,所以即使没有中心机构维持,所有人也都自己维持自己的那份区块链,我们就达成了一个去中心化的共识。

下面我们可以看下这个系统到底有多可信,我们来尝试下在这个系统中伪造信息欺骗他人有多难。比如说 Alice 想要用一个伪造的区块来欺骗 Bob ,于是他将一个包含了 一条 Alice 转账给 Bob 1000 元的消息广播给 Bob,但他并没有将这个区块广播给其他人,这样其他人就会以为 Alice 还拥有那 1000 元,为了欺骗其他人,他需要在其他人之前找到工作量证明,这是很有可能发生的。但同时 Bob 也会收到其他人的广播, 为了让 Bob 相信 Alice ,Alice 必须伪造 Bob 接收到的那个假区块后面的所有区块,这样的话,他可能就此陷入泥潭不能自拔了。。。必须一直计算下去。但仍凭他怎么计算,他的算力如果不是达到了系统的 51%,总会在某个区块被其他人先算出区块,导致他恶意创造的链被抛弃,同时他也浪费了大量算力。所以为了防止伪造情况出现,要求在区块挖出后,当有 6 个区块在其区块后又被挖掘出来,这个区块才会被真正承认。同时为了调整区块被挖出来的频率,公作量证明的难度也是不断变化的,保证大约每 10 分钟挖出一个区块。

HTTP 扩展阅读(II) - SSL/TLS

SSL/TLS

  1. TLS 定义

SSL(Secure Sockets Layer) 安全套接层,是一种安全协议,经历了 SSL 1.0、2.0、3.0 版本后发展成了标准安全协议 - TLS(Transport Layer Security) 传输层安全性协议。TLS 有 1.0 (RFC 2246)、1.1(RFC 4346)、1.2( RFC 5246)、1.3(1.3 目前处于草案阶段)版本。

TLS 在实现上分为记录层握手层两层,其中握手层又含四个子协议: 握手协议(handshake protoco 协议(change cipher spec protocol)、应用数据协议(application data protocol)和警报协议(alert protocol)

image

  1. HTTPS = HTTP over TLS.

只需配置浏览器和服务器相关设置开启 TLS,即可实现 HTTPS,TLS 高度解耦,可装可卸,与上层高级应用层协议相互协作又相互独立。

image

  1. 加密

TLS/SSL的功能实现主要依赖于三类基本算法:散列函数 Hash、对称加密和非对称加密,其利用非对称加密实现身份认证和密钥协商,对称加密算法采用协商的密钥对数据加密,基于散列函数验证信息的完整性。

image

TLS的基本工作方式是,客户端使用非对称加密与服务器进行通信,实现身份验证并协商对称加密使用的密钥,然后对称加密算法采用协商密钥对信息以及信息摘要进行加密通信,不同的节点之间采用的对称密钥不同,从而可以保证信息只能通信双方获取。

例如,在 HTTPS 协议中,客户端发出请求,服务端会将公钥发给客户端,客户端验证过后生成一个密钥再用公钥加密后发送给服务端,成功后建立连接。通信过程中客户端将请求数据用得到的公钥加密后发送,服务端用私钥解密;服务端用客户端给的密钥加密响应报文,回复客户端,客户端再用存好的相同的密钥解密。

  1. 记录层

记录协议负责在传输连接上交换的所有底层消息,并且可以配置加密。每一条 TLS 记录以一个短标头开始。标头包含记录内容的类型(或子协议)、协议版本和长度。原始消息经过分段(或者合并)、压缩、添加认证码、加密转为 TLS 记录的数据部分。

image

  • 碎片(Fragmentation)

    记录层将信息块分割成携带 2^14 字节 (16KB) 或更小块的数据的 TLSPlaintext 记录。

    记录协议传输由其他协议层提交给它的不透明数据缓冲区。如果缓冲区超过记录的长度限制(2^14),记录协议会将其切分成更小的片段。反过来也是可能的,属于同一个子协议的小缓冲区也可以组合成一个单独的记录。

struct { 
  uint8 major, minor; 
} ProtocolVersion;

enum { 
  change_cipher_spec(20),
  alert(21),
  handshake(22), 
  application_data(23), (255) 
} ContentType; 

struct {
  ContentType type; // 用于处理封闭片段的较高级协议
  ProtocolVersion version; // 使用的安全协议版本
  uint16 length; // TLSPlaintext.fragment的长度(以字节为单位),不超过 2^14
  opaque fragment[TLSPlaintext.length]; // 透明的应用数据,被视为独立的块,由类型字段指定的较高级协议处理
} TLSPlaintext;
  • 记录压缩和解压缩(Record compression and decompression)

    压缩算法将 TLSPlaintext 结构转换为 TLSCompressed 结构。如果定义CompressionMethod 为 null 表示不压缩

struct { 
  ContentType type; // same as TLSPlaintext.type
  ProtocolVersion version; // same as TLSPlaintext.version 
  uint16 length; // TLSCompressed.fragment 的长度,不超过 2^14 + 1024
  opaque fragment[TLSCompressed.length]; 
} TLSCompressed;
  • 空或标准流加密(Null or standard stream cipher)

    流加密(BulkCipherAlgorithm)将 TLSCompressed.fragment 结构转换为流 TLSCiphertext.fragment 结构

stream-ciphered struct { 
opaque content[TLSCompressed.length]; 
opaque MAC[CipherSpec.hash_size]; 
} GenericStreamCipher;

MAC 产生方法如下:

HMAC_hash(MAC_write_secret, seq_num + TLSCompressed.type + 
TLSCompressed.version + TLSCompressed.length + 
TLSCompressed.fragment));

seq_num(记录的序列号)、hash(SecurityParameters.mac_algorithm 指定的哈希算法)

注意,MAC 是在加密之前计算的。流加密加密整个块,包括 MAC。对于不使用同步向量(例如RC4)的流加密,从一个记录结尾处的流加密状态仅用于后续数据包。如果 CipherSuite 是TLS_NULL_WITH_NULL_NULL,则加密由身份操作(数据未加密,MAC大小为零,暗示不使用MAC)组成。TLSCiphertext.length 是 TLSCompressed.length 加上 CipherSpec.hash_size。

  • CBC块加密

    块加密(如RC2或DES),将 TLSCompressed.fragment 结构转换为块 TLSCiphertext.fragment 结构

block-ciphered struct { 
  opaque content[TLSCompressed.length]; 
  opaque MAC[CipherSpec.hash_size]; 
  uint8 padding[GenericBlockCipher.padding_length]; 
  uint8 padding_length;
} GenericBlockCipher;

padding: 添加的填充将明文长度强制为块密码块长度的整数倍。填充可以是长达 255 字节的任何长度,只要满足 TLSCiphertext.length 是块长度的整数倍。长度大于需要的值可以阻止基于分析交换信息长度的协议攻击。填充数据向量中的每个 uint8 必须填入填充长度值(即 padding_length)。

padding_length: 填充长度应该使得 GenericBlockCipher 结构的总大小是加密块长度的倍数。 合法值范围从零到255(含)。该长度指定 padding_length 字段本身除外的填充字段的长度。

加密块的数据长度(TLSCiphertext.length)是 TLSCompressed.length,CipherSpec.hash_size 和 padding_length 的总和加一

示例: 如果块长度为 8 字节,压缩内容长度(TLSCompressed.length)为 61 字节,MAC 长度为 20 字节,则填充前的长度为 82 字节(padding_length 占 1 字节)。
因此,为了使总长度为块长度(8 字节)的偶数倍,模 8 的填充长度必须等于6,所以填充长度可以为 6,14,22 等。如果填充长度是需要的最小值,比如6,填充将为 6 字节,每个块都包含值 6。因此,块加密之前的 GenericBlockCipher 的最后 8 个八位字节将为xx 06 06 06 06 06 06 06,其中 xx 是 MAC 的最后一个八位字节。

XX  - 06 06 06 06 06 06 - 06
MAC -     padding[6]    - padding_length
  • 记录有效载荷保护(Record payload protection)

    加密和 MAC 功能将 TLSCompressed 结构转换为 TLSCiphertext。记录的MAC还包括序列号,以便可以检测到丢失,额外或重复的消息。

struct { 
  ContentType type; // same
  ProtocolVersion version; // same
  uint16 length; // TLSCiphertext.fragment 的长度,不超过 2^14 + 2048
  select (CipherSpec.cipher_type) { 
    case stream: GenericStreamCipher; 
    case block: GenericBlockCipher; 
  } fragment; // TLSCompressed.fragment 的加密形式,带有MAC
} TLSCiphertext;

注意
这里提到的都是先 MAC 再加密,是基于 RFC 2246 的方案 (TLS 1.0) 写的。但新的方案选择先加密再 MAC,这种替代方案中,首先对明文和填充进行加密,再将结果交给MAC算法。这可以保证主动网络攻击者不能操纵任何加密数据。

  • 密钥计算(Key calculation)

    记录协议需要一种算法,从握手协议提供的安全性参数生成密钥、IV 和 MAC secret.
    主机密信息(Master secret): 在连接中的两个对等体之间共享一个48字节的密钥
    客户随机数(client random): 由客户端提供的32字节值
    服务器随机数(server random): 由服务器提供的32字节值

  1. 握手层
  • 握手协议的职责是生成通信过程所需的共享密钥和进行身份认证。这部分使用无密码套件,为防止数据被窃听,通过公钥密码或 Diffie-Hellman 密钥交换技术通信。
  • 密码规格变更协议,用于密码切换的同步,是在握手协议之后的协议。握手协议过程中使用的协议是“不加密”这一密码套件,握手协议完成后则使用协商好的密码套件。
  • 警告协议,当发生错误时使用该协议通知通信对方,如握手过程中发生异常、消息认证码错误、数据无法解压缩等。
  • 应用数据协议,通信双方真正进行应用数据传输的协议,传送过程通过 TLS 应用数据协议和 TLS 记录协议来进行传输。

握手是TLS协议中最精密复杂的部分。在这个过程中,通信双方协商连接参数,并且完成身 份验证。根据使用的功能的不同,整个过程通常需要交换 6~10 条消息。根据配置和支持的协议扩展的不同,交换过程可能有许多变种。在使用中经常可以观察到以下三种流程:(1) 完整的握手, 对服务器进行身份验证;(2) 恢复之前的会话采用的简短握手;(3) 对客户端和服务器都进行身份验证的握手。

握手协议消息的标头信息包含消息类型(1字节)和长度(3字节),余下的信息则取决于消息类型:

struct {
  HandshakeType msg_type;
  uint24 length;
  HandshakeMessage message;
} Handshake;

5.1 完整握手

每一个 TLS 连接都会以握手开始。如果客户端此前并未与服务器建立会话,那么双方会执行一次完整的握手流程来协商 TLS 会话。握手过程中,客户端和服务器将进行以下四个主要步骤:

  • 交换各自支持的功能,对需要的连接参数达成一致
  • 验证出示的证书,或使用其他方式进行身份验证
  • 对将用于保护会话的共享主密钥达成一致
  • 验证握手消息并未被第三方团体修改

下面介绍最常见的握手规则,一种在不需要身份验证的客户端与需要身份验证的服务器之间的握手

image

5.1.1 ClientHello

这条消息将客户端的功能和首选项传送给服务器。

image

  • Version: 协议版本(protocol version)指示客户端支持的最佳协议版本
  • Random: 一个 32 字节数据,28字节是随机生成的(图中的 Random Bytes);剩余的4字节包含额外的信息,与客户端时钟有关(图中使用的是 GMT Unix Time)。在握手时,客户端和服务器都会提供随机数,客户端的暂记作 random_C (用于后续的密钥的生成)。这种随机性对每次握手都是独一无二的,在身份验证中起着举足轻重的作用。它可以防止重放攻击,并确认初始数据交换的完整性。
  • Session ID: 在第一次连接时,会话ID(session ID)字段是空的,这表示客户端并不希望恢复某个已存在的会话。典型的会话 ID 包含 32 字节随机生成的数据,一般由服务端生成通过 ServerHello 返回给客户端。
  • Cipher Suites: 密码套件(cipher suite)块是由客户端支持的所有密码套件组成的列表,该列表是按优先级顺序排列的
  • Compression: 客户端可以提交一个或多个支持压缩的方法。默认的压缩方法是null,代表没有压缩
  • Extensions: 扩展(extension)块由任意数量的扩展组成。这些扩展会携带额外数据

5.1.2 ServerHello

是将服务器选择的连接参数传送回客户端。

image

这个消息的结构与 ClientHello 类似,只是每个字段只包含一个选项,其中包含服务端的 random_S 参数(用于后续的密钥协商)。服务器无需支持客户端支持的最佳版本。如果服务器不支持与客户端相同的版本,可以提供某个其他版本以期待客户端能够接受

5.1.3 Certificate

典型的 Certificate 消息用于携带服务器 X.509 证书链。服务器必须保证它发送的证书与选择的算法套件一致。比方说,公钥算法与套件中使用的必须匹配。除此以外,一些密钥交换算法依赖嵌入证书的特定数据,而且要求证书必须以客户端支持的算法签名。所有这些都表明服务器需要配置多个证书(每个证书可能会配备不同的证书链)。

image

Certificate消息是可选的,因为并非所有套件都使用身份验证,也并非所有身份验证方法都需要证书。更进一步说,虽然消息默认使用X.509证书,但是也可以携带其他形式的标志;一些套件就依赖PGP密钥

5.1.4 ServerKeyExchange

携带密钥交换的额外数据。消息内容对于不同的协商算法套件都会存在差异。在某些场景中,服务器不需要发送任何内容,
服务器密钥交换消息仅在服务器证书消息(如果发送)不包含足够的数据以允许客户端交换预主secret(premaster secret)时才由服务器发送。
比如基于 DH 的证书,公钥不被证书中包含,需要单独发送

image

5.1.5 ServerHelloDone

表明服务器已经将所有预计的握手消息发送完毕。在此之后,服务器会等待客户端发送消息。

5.1.6 verify certificate

客户端验证证书的合法性,如果验证通过才会进行后续通信,否则根据错误情况不同做出提示和操作,合法性验证包括如下:

  • 证书链的可信性 trusted certificate path;
  • 证书是否吊销 revocation,有两类方式 - 离线 CRL 与在线 OCSP,不同的客户端行为会不同;
  • 有效期 expiry date,证书是否在有效时间范围;
  • 域名 domain,核查证书域名是否与当前的访问域名匹配;

5.1.7 ClientKeyExchange

image

合法性验证通过之后,客户端计算产生随机数字的预主密钥(Pre-master),并用证书公钥加密,发送给服务器并携带客户端为密钥交换提供的所有信息。这个消息受协商的密码套件的影响,内容随着不同的协商密码套件而不同。

此时客户端已经获取全部的计算协商密钥需要的信息:两个明文随机数 random_C 和 random_S 与自己计算产生的 Pre-master,计算得到协商密钥;

enc_key=Fuc(random_C, random_S, Pre-Master)

5.1.8 ChangeCipherSpec

通知服务器后续的通信都采用协商的通信密钥和加密算法进行加密通信

注意
ChangeCipherSpec 不属于握手消息,它是另一种协议,只有一条消息,作为它的子协议进行实现。

5.1.9 Finished (Encrypted Handshake Message)

Finished消息意味着握手已经完成。消息内容将加密,以便双方可以安全地交换验证整个握手完整性所需的数据。

这个消息包含 verify_data 字段,它的值是握手过程中所有消息的散列值。这些消息在连接两端都按照各自所见的顺序排列,并以协商得到的主密钥计算散列。这个过程是通过一个伪随机函数(pseudorandom function,PRF)来完成的,这个函数可以生成任意数量的伪随机数据。
两端的计算方法一致,但会使用不同的标签(finished_label):客户端使用 client finished,而服务器则使用 server finished。

verify_data = PRF(master_secret, finished_label, Hash(handshake_messages))

因为 Finished 消息是加密的,并且它们的完整性由协商 MAC 算法保证,所以主动网络攻击者不能改变握手消息并对 vertify_data 的值造假。在 TLS 1.2 版本中,Finished消息的长度默认是 12 字节(96位),并且允许密码套件使用更长的长度。在此之前的版本,除了 SSL 3 使用 36 字节的定长消息,其他版本都使用 12 字节的定长消息。

5.1.10 Server

服务器用私钥解密加密的 Pre-master 数据,基于之前交换的两个明文随机数 random_C 和 random_S,计算得到协商密钥: enc_key=Fuc(random_C, random_S, Pre-Master);
计算之前所有接收信息的 hash 值,然后解密客户端发送的 verify_data_C,验证数据和密钥正确性;

5.1.11 change_cipher_spec

image

服务端验证通过之后,服务器同样发送 change_cipher_spec 以告知客户端后续的通信都采用协商的密钥与算法进行加密通信(图中多了一步 New Session Ticket,此为会话票证,会在会话恢复中解释);

5.1.12 Finished (Encrypted Handshake Message)

服务器也结合所有当前的通信参数信息生成一段数据 (verify_data_S) 并采用协商密钥 session secret (enc_key) 与算法加密并发送到客户端;

5.1.13 握手结束

客户端计算所有接收信息的 hash 值,并采用协商密钥解密 verify_data_S,验证服务器发送的数据和密钥,验证通过则握手完成;

5.1.14 加密通信

开始使用协商密钥与算法进行加密通信。

image

5.2 客户端身份验证

尽管可以选择对任意一端进行身份验证,但人们几乎都启用了对服务器的身份验证。如果服 务器选择的套件不是匿名的,那么就需要在 Certificate 消息中跟上自己的证书。

image

相比之下,服务器通过发送 CertificateRequest 消息请求对客户端进行身份验证。消息中列 出所有可接受的客户端证书。作为响应,客户端发送自己的 Certificate 消息(使用与服务器发 送证书相同的格式),并附上证书。此后,客户端发送 CertificateVerify 消息,证明自己拥有对应的私钥。

只有已经过身份验证的服务器才被允许请求客户端身份验证。基于这个原因,这个选项也被称为相互身份验证(mutual authentication)。

5.2.1 CertificateRequest

在 ServerHello 的过程中发出,请求对客户端进行身份验证,并将其接受的证书的公钥 和签名算法传送给客户端。

它也可以选择发送一份自己接受的证书颁发机构列表,这些机构都用其可分辨名称来表示:

struct {
  ClientCertificateType certificate_types;
  SignatureAndHashAlgorithm supported_signature_algorithms; 
  DistinguishedName certificate_authorities;
} CertificateRequest;

5.2.2 CertificateVerify

在 ClientKeyExchange 的过程中发出,证明自己拥有的私钥与之前发送的客户端证书中的公钥匹配。消息中包含一条到这一步为止的所有握手消息的签名:

struct {
  Signature handshake_messages_signature;
} CertificateVerify;

5.3 会话恢复

最初的会话恢复机制是,在一次完整协商的连接断开时,客户端和服务器都会将会话的安全参数保存一段时间。希望使用会话恢复的服务器为会话指定唯一的标识,称为会话 ID(Session ID)。服务器在 ServerHello 消息中将会话 ID 发回客户端。

希望恢复早先会话的客户端将适当的 Session ID 放入 ClientHello 消息,然后提交。服务器如果愿意恢复会话,就将相同的 Session ID 放入 ServerHello 消息返回,接着使用之前协商的主密钥生成一套新的密钥,再切换到加密模式,发送 Finished 消息。
客户端收到会话已恢复的消息以后,也进行相同的操作。这样的结果是握手只需要一次网络往返。

Session ID 由服务器端支持,协议中的标准字段,因此基本所有服务器都支持,服务器端保存会话 ID 以及协商的通信信息,占用服务器资源较多。

image

用来替代服务器会话缓存和恢复的方案是使用会话票证(Session ticket)。使用这种方式,除了所有的状态都保存在客户端(与HTTP Cookie的原理类似)之外,其消息流与服务器会话缓存是一样的。
其**是服务器取出它的所有会话数据(状态)并进行加密(密钥只有服务器知道),再以票证的方式发回客户端。在接下来的连接中,客户端恢复会话时在 ClientHello 的扩展字段 session_ticket 中携带加密信息将票证提交回服务器,由服务器检查票证的完整性,解密其内容,再使用其中的信息恢复会话。
这种方法有可能使扩展服务器集群更为简单,因为如果不使用这种方式,就需要在服务集群的各个节点之间同步会话。
Session ticket 需要服务器和客户端都支持,属于一个扩展字段,占用服务器资源很少。

警告
会话票证破坏了 TLS 安全模型。它使用票证密钥加密的会话状态并将其暴露在线路上。有些实现中的票证密钥可能会比连接使用的密码要弱。如果票证密钥被暴露,就可以解密连接上的全部数据。因此,使用会话票证时,票证密钥需要频繁轮换。

References

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.