老木白

老木白的blog

  • 首页
  • 归档
  • 分类
  • 标签
  • 关于

使用Dockerfile构建docker镜像

Posted on 2018-10-09 | In docker

使用Dockerfile可以在基础镜像之上构建与定制新的镜像。

开发流程

  1. 在本地编写与调试Dockerfile
  2. 提交Dockerfile至dockerhub
  3. 在生产环境中拉取dockerhub中的镜像并运行

Dockerhub

dockerhub与github类似,是我们托管容器的场所。

Dockerfile指令

  • FROM
    FROM指令用来指定dockerfile构建的镜像的基础镜像。dockerfile的第一条指令必须是FROM指令。当然也可以在dockerfile中使用多个FROM来构建相对复杂的镜像。
    如果不需要从任何镜像继承,那么我们可以使用FROM scratch。就如hello-world镜像一样。
1
2
3
FROM scratch
COPY hello /
CMD ["/hello"]
  • MAINTAINER
    指定维护者信息。

  • RUN
    执行shell命令。

  • EXPOSE
    暴露容器中的端口。

  • CMD
    用来指定容器启动后默认执行的命令。如果docker run在启动时指定了其它命令,那么CMD命令会被忽略。一个dockerfile只能有一个CMD指令,如果有多条则只会执行最后一个。

  • ENTRYPOINT
    用来设置容器启动时运行的命令,使容器以应用程序或服务的形式运行。通常会编写一个shell脚本作为entrypoint。

  • VOLUME
    用来指定一个本地或其它容器的挂载点。使容器中的数据在容器被删除之后得以保留。

  • ENV
    设置容器运行时的环境变量。

  • ADD
    复制指定路径或URL下的文件至容器中。ADD指令支持压缩文件解压。

  • COPY
    与ADD类似,仅提供文件及文件夹的复制,其行为更加单纯。在使用者明确自己必须使用ADD的情况之外推荐使用COPY。

Dockerfile微实战

背景

家里的宽带在每次重启光猫之后会改变公网的ip。因此我写了一个python程序来获取公网ip,在ip发生改变时以邮件的形式通知我。

编写Dockerfile

通过使用Dockerfile可以轻松在Dockerhub中构建镜像。

1
2
3
4
5
6
7
8
9
10
FROM python
MAINTAINER grezbo <grezboo@gmail.com>
ENV EMAIL_ADDR ''
ENV EMAIL_PASSWD ''
ENV TO_EMAIL_ADDR ''
ENV SMTP_SERVER ''
ENV INTERVAL ''
COPY main.py /main.py
RUN chmod +x /main.py
ENTRYPOINT ["./main.py"]
  1. FROM python表示该镜像从python镜像继承,因此编写的python程序可以直接在容器中运行,不需要额外的环境配置。
  2. MAINTAINER指令中指明了维护者的信息。
  3. ENV指令指定了环境变量。通过配置容器的环境变量,我们可以针对自己的运行环境进行个性化的配置。
  4. COPY指令将目录中的main.py文件拷贝到镜像中/main.py去。
  5. 通过RUN指令来执行命令,为/main.py来添加可执行的权限。
  6. 使用ENTRYPOINT来运行python程序。

提交Dockerfile

首先将main.py和Dockerfile两个文件提交至github的仓库中。再到dockerhub中创建一个automated build,关联之前提交代码的github仓库。如此便能够在dockerhub中通过Dockerfile来自动构建镜像了,而不是在本地构建好之后再将镜像上传至dockerhub。这样一来可以实现自动化构建,并且所有的源码都是公开的,不会让使用者担心有恶意代码的存在。

使用golang构建web应用的背后

Posted on 2018-09-04 | In golang , web

golang提供的网络编程库对于开发者来说十分易用,使用golang能轻松写出可靠、高效的web应用。并且能够让使用者清晰地理解web应用的构成及基本原理。

什么是web应用

单从狭义的角度来看,web应用应该包含以下三大最基本的功能:

  1. 能够接收从客户端发来的HTTP请求报文。
  2. 能够对HTTP请求报文进行处理。
  3. 能够将结果通过HTTP响应报文发送回客户端。

对于这三大基本功能,一个完整的web应用应包含:多路复用器(multiplexer)与控制器(handler)。

多路复用器

当请求到来时,多路复用器会根据路径将请求送到事先绑定好的处理器,交由处理器来处理HTTP请求报文。

1
2
3
4
5
6
mux := http.NewServeMux()   //构造多路复用器mux

//绑定路径与handler
mux.Handle("/handlerfunc1", http.HandlerFunc(handlerfunc)) //使用http.HandlerFunc强转
mux.HandleFunc("/handlerfunc2", handlerfunc) //使用mux.HandleFunc隐藏强转
mux.Handle("/handler", handler) //使用handler接口的实现(实现ServeHTTP方法)

让我们来看下多路复用器的类型ServeMux。

1
2
3
4
5
6
7
8
9
10
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
hosts bool // whether any patterns contain hostnames
}

type muxEntry struct {
h Handler
pattern string
}

可以看到,ServeMux结构体里包含了三部分内容,一把读写锁mu、一个map类型m以及一个布尔值hosts。
map里存储的就是路径与处理器的映射。由于map不是线程安全的,所以需要一把读写锁来对map进行保护。hosts我们先不讨论。

多路复用器ServeMux类型实现了ServeHTTP方法,因此多路复用器其本质上也是一个处理器。

1
2
3
4
5
6
7
8
9
10
11
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}

为什么要将多路复用器与处理器分开呢?我想是因为一下两点原因:

  1. 职责划分不通:多路复用器是一个总索引,主要的职责是将路径与处理器绑定起来。而处理器主要是针对不通的业务进行处理。
  2. 代码清晰:将不通的处理器单独写,并由多路复用器汇总,这样代码逻辑清晰,便于维护。

除了net/http包中的ServeMux。还有很多各有特色的多路复用器,最常见的有mux和HttpRouter。

处理器

一个类型实现了ServeHTTP(w ResponseWriter, r *Request)函数就能成为一个简单的处理器。其中Request是客户端发来的请求,ResponseWriter则是给客户端的回复。
此外,处理器可以通过串联的方式来进行非侵入的扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type content string

func (i content) ServeHTTP(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "%s", i)
}

func logUtil(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Print("some log")
h.ServeHTTP(w, r)
})
}

func main() {
mux := http.NewServeMux()
content := content("some content")
mux.Handle("/content", logUtil(content))
server := &http.Server{
Addr: "0.0.0.0:8080",
Handler: mux,
}
server.ListenAndServe()
}

代码中logUtil就是一个用来给处理器进行扩展的函数。logUtil函数传入一个http.Handler,返回一个http.Handler。因此,可以将此类的函数进行串联,为处理器提供功能丰富的扩展。

监听

可以通过http.Server结构体类型来设置web应用的一些参数,并使用ListenAndServe()函数来启动web应用。

1
2
3
4
5
server := http.Server{
Addr: "127.0.0.1:8080",
}

server.ListenAndServe()

当然,我们仅需要关注我们感兴趣的那些参数,不必对每个参数进行设置,绝大多数参数都具有默认值。例如在http.Server结构体类型中如果不指定处理器的话,默认会使用http.DefaultServeMux来作为处理器。
同样的,我们可以为http.Server设置一个处理器来替代默认的处理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//无论遇到什么请求都只会返回hello
type hello struct {}

func (h hello) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello")
}

func main() {
h := hello{}

server := http.Server{
Addr: "127.0.0.1:8080",
Handler:h,
}
server.ListenAndServe()
}

在调用ListenAndServe()之后web应用便启动起来了,它的本质其实就是一个死循环,在不断获取监听到的请求并交由处理器处理。
在ListenAndServe()中调用了Server类型的Serve(l net.Listener)函数。web应用的死循环就是写在Serve函数中的。
在Serve函数中使用无条件的for循环获取监听的连接,将连接以goroutine的方式交给conn类型的serve(ctx context.Context)函数处理。最后通过在serve函数中调用Handler接口的ServeHTTP函数来处理具体的业务逻辑。

golang接口型函数及其应用

Posted on 2018-09-02 | In golang

在翻阅golangnet/http库的实现时,遇到了如下代码:

1
2
3
4
5
6
7
func Handle(pattern string, handler Handler) {
DefaultServeMux.Handle(pattern, handler)
}

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

第一眼看去,两个函数的样子和所实现的功能类似,区别仅在于函数的签名。其中Handle函数第二个参数为Handler接口类型,而HandleFunc中第二个参数是一个func(ResponseWriter, *Request)类型。

熟悉net/httpapi的朋友应该知道,Handler接口长这样:

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

Handler接口中ServeHTTP的签名与HandleFunc中第二个参数的签名一致,都是(ResponseWriter, *Request)

wtf1

golang这么设计的原因是什么?

带着疑问,我们来看下net/http包中的HandlerFunc类型。

1
2
3
4
5
type HandlerFunc func(ResponseWriter, *Request)

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

既然HandlerFunc类型实现了ServeHTTP函数,那么HandlerFunc肯定就是一个Handler了。同时HandlerFunc类型又是一个参数类型为(ResponseWriter, *Request)的函数。像HandlerFunc这样的函数就被称为‘接口型函数’。
那么接口型函数有什么作用呢?看下面这个例子。

假如我们有一个有个info类型,包含了一段string类型的信息。并且info实现了函数show。

1
2
3
4
5
type info string

func (i info) show(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "info: %s", i)
}

show函数的签名与ServeHTTP(w ResponseWriter, r *Request)相同,仅仅是函数名不同,因此它无法被看做是一个Handler。

这时我们可以通过HandlerFunc来强转show,使它成为一个Handler,可以将HandlerFunc看做是一个让函数来满足一个接口的adapter。

1
http.HandlerFunc(info.show)

于是我们在把路由和Handler绑定的时候可以这么写。

1
2
3
mux := http.NewServeMux()
info := info("some info")
mux.Handle("/info1", http.HandlerFunc(info.show))

这样一来,可以将一个具有(w ResponseWriter, r *Request)签名的函数适配到Handler接口上,实现完整的功能,但是代码总显得不够优雅。如果能把代码中的强转给屏蔽掉的话就更完美了。

此时,回到最初抛出的疑问。在探究Handle和HandleFunc函数的实现之后,终于领悟了为什么要这么设计。

1
2
3
4
5
6
7
8
func (mux *ServeMux) Handle(pattern string, handler Handler) {
some code ...
}

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

在HandleFunc中将HandlerFunc的强转过程给封装起来了。如果我们使用HandleFunc来替代Handle。对于使用者来说无需知道有强转这一步骤,并且不需要实现ServeHTTP(w ResponseWriter, r *Request)来满足Handler接口,而是可以使用函数签名相同的函数,使得函数名可以取的更有意义,使代码的组织更为灵活。

1
2
3
mux := http.NewServeMux()
info := info("some info")
mux.HandleFunc("/info2", info.show)

老木白

3 posts
3 categories
3 tags
© 2018 老木白
Powered by Hexo v3.7.1
|
Theme – NexT.Muse v6.4.1