• 制作Go程序的Docker容器(以及容器和主机的网络问题)


    今天突然遇到需要将 Go 程序制作成 Docker 的需求,所以进行了一些研究。方法很简单,但是官方文档和教程有些需要注意的地方,所以写本文进行记录。

    源程序

    首先介绍一下示例程序,示例程序是一个 HTTP 服务器,会显示sin(r)/r的图像,如下:

    请添加图片描述

    新建一个目录draw-surface,然后在里面新建一个draw-surface.go文件,内容为:

    // display Animated Lissajous in a browser and can set arguments in queries.
    package main
    
    import (
    	"errors"
    	"fmt"
    	"io"
    	"log"
    	"math"
    	"net/http"
    	"strconv"
    	"sync"
    )
    
    var mu sync.Mutex
    var count int
    
    func main() {
    	http.HandleFunc("/", handler) 
    	log.Fatal(http.ListenAndServe("localhost:8000", nil))
    }
    
    func handler(w http.ResponseWriter, r *http.Request) {
    	w.Header().Set("Content-Type", "image/svg+xml")
    	func2svg(w, r)
    }
    
    var width, height int = 500, 400    
    var cells int = 200                 
    var xyrange int = 50.0              
    var xyscale int = width / 2.0 / xyrange   
    var zscale float64 = float64(height) * 0.4 
    var angle float64 = math.Pi / 9            
    
    var sin, cos float64 = math.Sin(angle), math.Cos(angle)
    
    func func2svg(out io.Writer, r *http.Request) {
    	fmt.Fprintf(out, "+"style='stroke: grey; fill: white; stroke-width: 0.7' "+"width='%d' height='%d'>", width, height)
    
    	for i := 0; i < cells; i++ {
    		for j := 0; j < cells; j++ {
    			ax, ay, error1 := corner(i+1, j)
    			bx, by, error2 := corner(i, j)
    			cx, cy, error3 := corner(i, j+1)
    			dx, dy, error4 := corner(i+1, j+1)
    			if error1 == nil || error2 == nil || error3 == nil || error4 == nil {
    				fmt.Fprintf(out, "\n",
    					ax, ay, bx, by, cx, cy, dx, dy)
    			}
    		}
    	}
    	fmt.Fprintf(out, "")
    }
    
    func corner(i, j int) (float64, float64, error) {
    	x := float64(xyrange) * (float64(i)/float64(cells) - 0.5)
    	y := float64(xyrange) * (float64(j)/float64(cells) - 0.5)
    
    	z := f(x, y)
    
    	if math.IsInf(z, 1) {
    		return math.NaN(), math.NaN(), errors.New("Result of f is non-finite")
    	} else {
    		sx := float64(width/2) + (x-y)*cos*float64(xyscale)
    		sy := float64(height/2) + (x+y)*sin*float64(xyscale) - z*zscale
    		return sx, sy, nil
    	}
    }
    
    func f(x, y float64) float64 {
    	r := math.Hypot(x, y)
    	return math.Sin(r) / r
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    然后是用以下命令初始化模块:

    $ go mod init
    
    • 1

    这个程序来自于《The Go Programming language》,本文重点看main函数即可。由于是用来演示构建映像和容器,所以删除了 URL 参数部分。

    不在容器里运行(本机)

    这个程序可以直接在本机运行,此时main函数内容如下,地址设置为了localhost:8000

    func main() {
    	http.HandleFunc("/", handler)
    	log.Fatal(http.ListenAndServe("localhost:8000", nil))
    }
    
    • 1
    • 2
    • 3
    • 4

    可以直接在终端运行:

    go run draw-surface.go
    
    • 1

    然后在浏览器中使用localhost:8000就可以看到前面的示例图的内容。

    在容器中运行

    构建Docker映像

    官方文档很奇怪,Containerize your application 中介绍说用docker init来新建所需的文件,但是 Mac 上的 Docker(20.10.23)没有这个命令:

    $ docker init
    docker: 'init' is not a docker command.
    See 'docker --help'
    
    • 1
    • 2
    • 3

    但是在介绍构建 Go 映像的教程 Build your Go image 里介绍的是手动新建。

    所以这里使用手动新建 Docker 映像所需的配置文件Dockerfile

    $ touch Dockerfile
    
    • 1

    然后以此输入下面的内容,解释请看注释:

    # syntax=docker/dockerfile:1
    # 继承自 Go 1.20 的映像,这样就可以使用Go的编译器等。把容器必做一台设备的话,就是在这个设备上安装了 Go
    FROM golang:1.20
    
    # 工作目录为/app,这里的/是容器内部的根目录
    WORKDIR /app
    #将当前目录下的go.mod复制到容器内的工作目录,也就是/app下
    COPY go.mod ./
    # 使用命令安装所需的模块
    RUN go mod download
    #将当前目录下所有的go源代码文件复制到容器内的工作目录,也就是/app下
    COPY *.go ./
    
    # 构建 Go 程序 draw-surface到根目录下
    RUN CGO_ENABLED=0 GOOS=linux go build -o /draw-surface
    
    # 将该映像当做容器启动之后,执行该程序
    CMD [ "/draw-surface" ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    然后需要修改一下 Go 源代码文件draw-surface.go中的一个地方,将地址中的localhost删除,只留下:8000。如下:

    func main() {
    	http.HandleFunc("/", handler)
    	log.Fatal(http.ListenAndServe(":8000", nil))
    }
    
    • 1
    • 2
    • 3
    • 4

    为什么要这样修改呢?要解释这个问题有点偏题和涉及后面的内容了,所以放在最后的“题外话”部分。

    接下来就可以进行构建映像了,命令如下:

    $ docker build --tag draw-surface .
    [+] Building 16.6s (15/15) FINISHED                                             
     => [internal] load build definition from Dockerfile                       0.0s
     => => transferring dockerfile: 220B                                       0.0s
     => [internal] load .dockerignore                                          0.0s
     => => transferring context: 2B                                            0.0s
     => resolve image config for docker.io/docker/dockerfile:1                 2.2s
     => CACHED docker-image://docker.io/docker/dockerfile:1@sha256:ac85f380a6  0.0s
     => [internal] load build definition from Dockerfile                       0.0s
     => [internal] load metadata for docker.io/library/golang:1.20             1.6s
     => [internal] load .dockerignore                                          0.0s
     => [1/6] FROM docker.io/library/golang:1.20@sha256:77e4e426190723821471a  0.0s
     => [internal] load build context                                          0.0s
     => => transferring context: 3.50kB                                        0.0s
     => CACHED [2/6] WORKDIR /app                                              0.0s
     => CACHED [3/6] COPY go.mod ./                                            0.0s
     => CACHED [4/6] RUN go mod download                                       0.0s
     => [5/6] COPY *.go ./                                                     0.0s
     => [6/6] RUN CGO_ENABLED=0 GOOS=linux go build -o /draw-surface          12.0s
     => exporting to image                                                     0.5s
     => => exporting layers                                                    0.5s
     => => writing image sha256:d3e0c9b2bc8fb859f189ef7a51f7a478cf61f9d8a3e0c  0.0s
     => => naming to docker.io/library/draw-surface                            0.0s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    使用映像运行容器

    然后就可以使用映像运行一个容器了,使用以下命令运行:

    docker run --publish 8000:8000 draw-surface
    
    • 1

    这里的8000:8000是容器内外端口的映射,和代码中一样即可。如果你想使用 Docker Desktop 运行容器,那么端口映射需要写到 Dockerfile 中,然后才可以在第一次运行该映像的时候设置对应端口。

    请添加图片描述

    使用的话在浏览器中输入:http://localhost:8000即可看到示例图:
    请添加图片描述

    除了http://localhost:8000,还可以使用http://HostIp的内容:8000http://0.0.0.0:8000,但是不能使用http://IPAddress:8000来获取服务器返回的图像。关于如何获取容器HostIpIPAddress,在下面的“如何获取 Docker 容器的 IP 地址”有详细说明。

    题外话

    如何获取 Docker 容器的 IP 地址和主机地址

    获取 Docker 容器的 IP 地址很简单。

    Docker Desktop

    如果使用 Docker Desktop,那么在容器部分查看详细信息,然后最下面就是。如下:

    请添加图片描述

    请添加图片描述

    Docker CLI

    如果是在终端中使用 Docker CLI,那么首先使用docker ps找到你想查询的容器的 ID,然后使用以下命令查看这个容器的详细信息:

    $ docker inspect 容器ID
    
    • 1

    这里列出了很多信息,但是本文中我们需要的是HostIp,而不是IPAddress部分。你可以使用http://HostIp:8000http://0.0.0.0:8000http://localhost:8000,唯独不能使用http://IPAddress:8000获取服务器返回的图像(强调,只是本文的情况是这样,其他的项目需要根据情况而定)。

    所以如果你想获取HostIp,那么可以将上面的命令修改成:

    $ docker inspect 容器ID | grep "HostIp"
                            "HostIp": "",
                            "HostIp": "0.0.0.0",
    
    • 1
    • 2
    • 3

    如果你想获取IPAddress,那么使用下面的命令比较方便:

    $ docker inspect 容器ID | grep "IPAddress"
                "SecondaryIPAddresses": null,
                "IPAddress": "172.17.0.2",
                        "IPAddress": "172.17.0.2",
    
    • 1
    • 2
    • 3
    • 4

    为什么使用:8000格式作为地址(使用静态IP行不行)

    Docker 是一种虚拟技术,致力于用最小系统环境模拟单台设备。如果在本地网络上的一台设备上使用了localhost:8000这样的地址,而这个端口也对外开放。那么在本地网络上的其他设备中,在浏览器中使用http://服务器IP:8000这样的 URL 也无法看到图。只能在代码设置地址的时候,使用:8000说明端口或服务器IP:8000说明网络中的地址,然后访问时使用服务器IP:8000这样的 URL 就可以看到图了。不过一般考虑到移植问题,后者使用的少。

    但是 Docker 容器虽然工作起来像这样,但不是完全符合,或者说默认情况下不是这样的。

    Docker 容器不能使用服务器IP:8000声明和访问。因为在运行映像的时候,使用的8000:8000表示的是将主机的8000端口和容器的8000端口映射,但是容器并不是在主机的网络上,而是在 Docker 网络上(也就是在主机内部,有一个守护进程为各个容器分配 IP)。也就是说,主机就是一个Host,这些容器把端口映射到Host的端口上了。

    这也解释了为什么这里无法使用容器的 IP 地址获取图像,因为主机的网络端口根本找不到他。但却可以使用HostIp的地址来获取图像,因为Host就是主机,是一台机器。

    此外,不光主机本地地址http://localhost:8000可以,你还可以通过主机的 IP 来使用,也就是某个网络接口的 IP 地址,比如无线接口的IP:http://169.252.1.4:8000

    参考阅读

    IP address, Network address, and Host address Explained

    Servers - Go

    Networking overview - Docker Docs

    Dockerfile reference - Docker Docs

    How to get a Docker container’s IP address from the host - stackoverflow

    希望能帮到有需要的人~

  • 相关阅读:
    高效电商数据分析:电商爬虫API与大数据技术的融合应用
    ElasticSearch 进阶(一)
    大数 马蹄集
    DAST 黑盒漏洞扫描器 第四篇:扫描性能
    SimpleFOCStudio安装说明,免安装版本和正常安装版本
    【工业设计】产品的第一印象是什么?
    从头开始机器学习:神经网络
    阿里云国际ECS云服务器免费试用版如何使用?
    设计师首选:最佳的5款网页设计软件
    Mysql5.7(Docker环境)实现主从复制
  • 原文地址:https://blog.csdn.net/qq_33919450/article/details/134479274