mac下开启docker API远程调用

·  阅读 1877
原文链接: cloud.tencent.com

docker容器技术至今已有五年的发展,作为一个工具,已经像Linux命令一样融入我们开发的生活。现在大多开发都使用Mac作为开发机,大都会装一个Docker for mac这个Mac下的docker工具。本文将从以下几个话题进行展开,说明Mac下docker的使用原理。

Docker for mac介绍

docker for mac 是docker在Mac机器上的一个docker工具集。集成了Kitematic, Docker图形用户界面,现在又集成了kubernetes。对于开发人员是很方便使用的,安装也很简单,教程也很多。

docker是基于Linux 容器技术开发出来的,Mac OS是unix系统,按照该理论,Mac下是不能运行容器的。之前自己的Mac下跑一个namespace隔离的demo实验,跑完后,一直无法得到理论中的结果。突然才意识到是操作系统环境的问题,Mac OS下也许不支持这种namespace隔离导致的,随后,就倒腾了一台安装ubuntu18.04的虚拟机,跑出了希望的结果。

通过这个例子说明,Mac OS下运行的docker不是原生的docker。容器的daemon应该运行在一个Linux环境虚拟机中。这里只做一个猜想,因为还没找到资料证明认为的这个结论。我们知道,在单机环境中,docker的客户端和daemon的通信是默认方式是socket文件的方式。在Linux环境中,我们要使用docker server端提供的API,就需要设置DOCKER_OPTS= -H tcp://0.0.0.0:2375 server端监听2375端口,我们才可以通过docker 提供的API进行接口调用。

那么Mac OS环境的文件系统找了好久,也没找到docker server端提供的参数配置文件。然后Google后找到了点思路。

Docker for Mac提供的UI界面可以配置该参数。

尝试添加tcp监听端口,结果如下所示。那么就需探索其它方法。

在stackoverflow中看到一个回答,进入docker daemon所在的虚拟机中

screen ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/tty
linuxkit-025000000001:~# ps -ef | grep docker
 1003 root      0:03 containerd-shim -namespace services.linuxkit -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/services.linuxkit/docker-ce -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd
 1025 root      0:00 /usr/local/bin/docker-init /usr/bin/entrypoint.sh
 1176 root      0:00 {start-docker.sh} /bin/sh /usr/bin/start-docker.sh
 1184 root      0:00 /usr/sbin/sntpc -v -i 30 gateway.docker.internal
 1226 root      1:12 kubelet --kubeconfig=/etc/kubernetes/kubelet.conf --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true --cluster-dns=10.96.0.10 --cluster-domain=cluster.local --cgroups-per-qos=false --enforce-node-allocatable= --network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin --cadvisor-port=0 --kube-reserved-cgroup=podruntime --system-reserved-cgroup=systemreserved --cgroup-root=kubepods --hostname-override=docker-for-desktop --fail-swap-on=false
 1275 root      0:24 /usr/local/bin/dockerd -H unix:///var/run/docker.sock --config-file /run/config/docker/daemon.json --swarm-default-advertise-addr=eth0 --userland-proxy-path /usr/bin/vpnkit-expose-port
 1318 root      0:00 /usr/bin/trim-after-delete -- /sbin/fstrim /var/lib/docker
 1422 root      0:00 /vsudd -inport 2376:unix:/var/run/docker.sock
 1474 root      0:07 docker-containerd --config /var/run/docker/containerd/containerd.toml
linuxkit-025000000001:~# more /run/config/docker/daemon.json
{
  "debug" : true,
  "experimental" : true
}复制代码

说明UI管理的daemon的配置数据是从这里获得。通过以上的实验,我们可以知道,在验证文章开头的那个猜想是正确的,Mac OS下的docker是通过Linux虚拟机的方式运行的。这里看到了linuxkit,这又是一项什么技术,先收一收吧,担心自己还没法驾驭的了。

Docker for mac 的知识点展开就先到这里了。我们的问题是怎么解决Mac OS下docker API的使用问题,目前还没有解决。还需要探索新的东西来解决该问题。

socket通信和http协议在进程间通信

在Linuxkit下的命令行中,我们看到了dockerd -H unix:///var/run/docker.sock,这是docker daemon的默认通信方式,那么我们是否可以将tcp协议转化为socket协议,翻墙软件大都基于这样的一种协议转化。在此,还不是十分的清楚明白这种sock文件通信的机制。

来补一补这一节理论知识。

"unix://"这种通信协议被称为UNIX域协议,它主要用于同一主机下,不同进程间的通信,比tcp协议的通信效率高很多。通socket编程又有不同。

unix域提供两类套接字:字节流套接字(类似TCP)和数据报套接字(类似UDP)。使用unix域协议有如下的优势: (1)unix域套接字往往比通信两端位于同一个主机的TCP套接字快出一倍。 (2)unix域套接字可用于在同一个主机上的不同进程之间传递描述符。 (3)unix域套接字较新的实现把客户的凭证(用户ID和组ID)提供给服务器,从而能够提供额外的安全检查措施。

unix域中用于标识客户和服务器的协议地址是普通文件系统中的路径名

来看一个go实现的demo

服务端:server.go

package main
 
import (
	"encoding/binary"
	"bytes"
	"io"
	"os"
	"fmt"
	"net"
	"time"
)
 
const (
	UNIX_SOCK_PIPE_PATH = "/var/run/unixsock_test.sock" // socket file path
)
 
func main() {
	// Remove socket file
	os.Remove(UNIX_SOCK_PIPE_PATH)
	// Get unix socket address based on file path
	uaddr, err := net.ResolveUnixAddr("unix", UNIX_SOCK_PIPE_PATH)
	if err != nil {
		fmt.Println(err)
		return
	}
 
	// Listen on the address
	unixListener, err := net.ListenUnix("unix", uaddr)
	if err != nil {
		fmt.Println(err)
		return
	}
 
	// Close listener when close this function, you can also emit it because this function will not terminate gracefully
	defer unixListener.Close()
 
	fmt.Println("Waiting for asking questions ...")
	// Monitor request and process
	for {
		uconn, err := unixListener.AcceptUnix()
		if err != nil {
			fmt.Println(err)
			continue
		}
 
		// Handle request
		go handleConnection(uconn)
	}
}
 
/*******************************************************
* Handle connection and request
* conn: conn handler
*******************************************************/
func handleConnection(conn *net.UnixConn) {
	// Close connection when finish handling
	defer func() {
		conn.Close()
	}()
 
	// Read data and return response
	data, err := parseRequest(conn)
	if err != nil {
		fmt.Println(err)
		return
	}
 
	fmt.Printf("%+v\tReceived from client: %s\n", time.Now(), string(data))
	time.Sleep(time.Duration(1) * time.Second) // sleep to simulate request process
 
	// Send back response
	sendResponse(conn, []byte(time.Now().String()))
}
 
/*******************************************************
* Parse request of unix socket
* conn: conn handler
*******************************************************/
func parseRequest(conn *net.UnixConn) ([]byte, error) {
	var reqLen uint32
	lenBytes := make([]byte, 4)
	if _, err := io.ReadFull(conn, lenBytes); err != nil {
		return nil, err
	}
 
	lenBuf := bytes.NewBuffer(lenBytes)
	if err := binary.Read(lenBuf, binary.BigEndian, &reqLen); err != nil {
		return nil, err
	}
 
	reqBytes := make([]byte, reqLen)
	_, err := io.ReadFull(conn, reqBytes)
 
	if err != nil {
		return nil, err
	}
 
	return reqBytes, nil
}
 
/*******************************************************
* Send response to client
* conn: conn handler
*******************************************************/
func sendResponse(conn *net.UnixConn, data []byte) {
	buf := new(bytes.Buffer)
	msglen := uint32(len(data))
 
	binary.Write(buf, binary.BigEndian, &msglen)
	data = append(buf.Bytes(), data...)
 
	conn.Write(data)
}
复制代码

客户端:client.go

package main
 
import (
	"time"
	"io"
	"encoding/binary"
	"bytes"
	"fmt"
	"net"
)
 
const (
	UNIX_SOCK_PIPE_PATH = "/var/run/unixsock_test.sock" // socket file path
)
 
var (
	exitSemaphore chan bool
)
 
func main() {
	// Get unix socket address based on file path
	uaddr, err := net.ResolveUnixAddr("unix", UNIX_SOCK_PIPE_PATH)
	if err != nil {
		fmt.Println(err)
		return
	}
 
	// Connect server with unix socket
	uconn, err := net.DialUnix("unix", nil, uaddr)
	if err != nil {
		fmt.Println(err)
		return
	}
 
	// Close unix socket when exit this function
	defer uconn.Close()
	
	// Wait to receive response
	go onMessageReceived(uconn)
 
	// Send a request to server
	// you can define your own rules
	msg := "tell me current time\n"
	sendRequest(uconn, []byte(msg))
 
	// Wait server response
	// change this duration bigger than server sleep time to get correct response
	exitSemaphore = make(chan bool)
	select {
	case <- time.After(time.Duration(2) * time.Second):
		fmt.Println("Wait response timeout")
	case <-exitSemaphore:
		fmt.Println("Get response correctly")
	}
 
	close(exitSemaphore)
}
 
/*******************************************************
* Send request to server, you can define your own proxy
* conn: conn handler
*******************************************************/
func sendRequest(conn *net.UnixConn, data []byte) {
	buf := new(bytes.Buffer)
	msglen := uint32(len(data))
 
	binary.Write(buf, binary.BigEndian, &msglen)
	data = append(buf.Bytes(), data...)
 
	conn.Write(data)
}
 
/*******************************************************
* Handle connection and response
* conn: conn handler
*******************************************************/
func onMessageReceived(conn *net.UnixConn) {
	//for { // io Read will wait here, we don't need for loop to check
		// Read information from response
		data, err := parseResponse(conn)
		if err != nil {
			fmt.Println(err)
		} else {
			fmt.Printf("%v\tReceived from server: %s\n", time.Now(), string(data))
		}
 
		// Exit when receive data from server
		exitSemaphore <- true
	//}
}
 
/*******************************************************
* Parse request of unix socket
* conn: conn handler
*******************************************************/
func parseResponse(conn *net.UnixConn) ([]byte, error) {
	var reqLen uint32
	lenBytes := make([]byte, 4)
	if _, err := io.ReadFull(conn, lenBytes); err != nil {
		return nil, err
	}
 
	lenBuf := bytes.NewBuffer(lenBytes)
	if err := binary.Read(lenBuf, binary.BigEndian, &reqLen); err != nil {
		return nil, err
	}
 
	reqBytes := make([]byte, reqLen)
	_, err := io.ReadFull(conn, reqBytes)
 
	if err != nil {
		return nil, err
	}
 
	return reqBytes, nil
}
复制代码

备注:代码参考 blog.csdn.net/lwc5411117/…

通过这个例子,大概能说明unix域协议了吧。我们要实现的目标就是将tcp转化为unix域协议,这里有一个号称网络界瑞士军刀socat可以实现我们的想法。

mac下docker API调用实现

现在问题解决的思路很清晰了,闲话不说,直接上成熟的解决方案。

通过容器的方式来解决

```

docker run -d -v /var/run/docker.sock:/var/run/docker.sock -p 2376:2375 \

bobrik/socat TCP4-LISTEN:2375,fork,reuseaddr UNIX-CONNECT:/var/run/docker.sock

```

对于以上命令的意思,在此放一个外链socat入门。

测试

 $curl -XGET http://127.0.0.1:2376/version | python -mjson.tool
 {
    "ApiVersion": "1.38",
    "Arch": "amd64",
    "BuildTime": "2018-07-18T19:13:46.000000000+00:00",
    "Components": [
        {
            "Details": {
                "ApiVersion": "1.38",
                "Arch": "amd64",
                "BuildTime": "2018-07-18T19:13:46.000000000+00:00",
                "Experimental": "true",
                "GitCommit": "0ffa825",
                "GoVersion": "go1.10.3",
                "KernelVersion": "4.9.93-linuxkit-aufs",
                "MinAPIVersion": "1.12",
                "Os": "linux"
            },
            "Name": "Engine",
            "Version": "18.06.0-ce"
        }
    ],
    "Experimental": true,
    "GitCommit": "0ffa825",
    "GoVersion": "go1.10.3",
    "KernelVersion": "4.9.93-linuxkit-aufs",
    "MinAPIVersion": "1.12",
    "Os": "linux",
    "Platform": {
        "Name": ""
    },
    "Version": "18.06.0-ce"
}复制代码

完美的实现了docker API的使用。

curl 这个工具也是可以支持unix域协议

$curl -XGET --unix-socket /var/run/docker.sock http://127.0.0.1:2375/version | python -mjson.tool
{
    "ApiVersion": "1.38",
    "Arch": "amd64",
    "BuildTime": "2018-07-18T19:13:46.000000000+00:00",
    "Components": [
        {
            "Details": {
                "ApiVersion": "1.38",
                "Arch": "amd64",
                "BuildTime": "2018-07-18T19:13:46.000000000+00:00",
                "Experimental": "true",
                "GitCommit": "0ffa825",
                "GoVersion": "go1.10.3",
                "KernelVersion": "4.9.93-linuxkit-aufs",
                "MinAPIVersion": "1.12",
                "Os": "linux"
            },
            "Name": "Engine",
            "Version": "18.06.0-ce"
        }
    ],
    "Experimental": true,
    "GitCommit": "0ffa825",
    "GoVersion": "go1.10.3",
    "KernelVersion": "4.9.93-linuxkit-aufs",
    "MinAPIVersion": "1.12",
    "Os": "linux",
    "Platform": {
        "Name": ""
    },
    "Version": "18.06.0-ce"
}复制代码

总结

到此,我们的问题已经完美的解决。对Mac 的docker原理展开做了一个说明,同时对unix域协议通过go语言实现了一个demo,最后通过socat这个强大的网络工具通过docker安装的方式解决了docker API访问的问题。

分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改