[mydocker]---一步步實現(xiàn)使用AUFS包裝busybox

1. 準(zhǔn)備工作

1.1 準(zhǔn)備環(huán)境

root@nicktming:~/go/src/github.com/nicktming/mydocker# git clone https://github.com/nicktming/mydocker.git
root@nicktming:~/go/src/github.com/nicktming/mydocker# git checkout code-4.1
root@nicktming:~/go/src/github.com/nicktming/mydocker# git checkout -b dev-4.2

1.2 本文最終效果

// 準(zhǔn)備busybox鏡像
-----------------------------terminal 01----------------------------------
root@nicktming:/nicktming# pwd
/nicktming
root@nicktming:/nicktming# ls
busybox.tar

// 根據(jù)busybox鏡像啟動容器
-----------------------------terminal 02----------------------------------
root@nicktming:~/go/src/github.com/nicktming/mydocker# git clone https://github.com/nicktming/mydocker.git
root@nicktming:~/go/src/github.com/nicktming/mydocker# git checkout code-4.2
root@nicktming:~/go/src/github.com/nicktming/mydocker# go build .
root@nicktming:~/go/src/github.com/nicktming/mydocker# ./mydocker run -it -r /nicktming /bin/sh
2019/04/07 16:31:33 rootPath:/nicktming
2019/04/07 16:31:33 current path: /nicktming/mnt.
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ # ps -l
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    5 root      0:00 ps -l
/ # mkdir nicktming && echo "test" > nicktming/test.txt
/ # ls
bin        dev        etc        home       nicktming  proc       root       sys        tmp        usr        var
/ # cat nicktming/test.txt 
test

// 查看宿主機(jī)中的變化
-----------------------------terminal 01----------------------------------
root@nicktming:/nicktming# ls
busybox  busybox.tar  mnt  writerLayer
root@nicktming:/nicktming# df -h
Filesystem      Size  Used Avail Use% Mounted on
...
none             50G  2.7G   45G   6% /nicktming/mnt
root@nicktming:/nicktming# 
root@nicktming:/nicktming# cat mnt/nicktming/test.txt 
test
root@nicktming:/nicktming# cat writerLayer/nicktming/test.txt 
test

// 退出容器
-----------------------------terminal 02----------------------------------
/ # exit
root@nicktming:~/go/src/github.com/nicktming/mydocker# 

// 再次查看宿主機(jī)內(nèi)容
-----------------------------terminal 01----------------------------------
oot@nicktming:/nicktming# ls
busybox  busybox.tar
root@nicktming:/nicktming# df -h
Filesystem      Size  Used Avail Use% Mounted on
...

2. 存在的問題

利用busybox創(chuàng)建的容器, 創(chuàng)建文件夾并且創(chuàng)建文件.

root@nicktming:~/go/src/github.com/nicktming/mydocker# git status
On branch dev-4.2
nothing to commit, working directory clean
root@nicktming:~/go/src/github.com/nicktming/mydocker# go build .
root@nicktming:~/go/src/github.com/nicktming/mydocker# ./mydocker run -it /bin/sh
2019/04/06 22:52:43 rootPath:
2019/04/06 22:52:43 set cmd.Dir by default: /root/busybox
2019/04/06 22:52:43 current path: /root/busybox.
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ # mkdir nicktming && echo "for testing." > nicktming/test.txt
/ # ls
bin        dev        etc        home       nicktming  proc       root       sys        tmp        usr        var
/ # cat nicktming/test.txt 
for testing.
/ # exit
root@nicktming:~/go/src/github.com/nicktming/mydocker# 

退出容器后, 查看宿主機(jī)的內(nèi)容.

root@nicktming:~/busybox# pwd
/root/busybox
root@nicktming:~/busybox# ls
bin  dev  etc  home  nicktming  proc  root  sys  tmp  usr  var
root@nicktming:~/busybox# cat nicktming/test.txt 
for testing.
root@nicktming:~/busybox# 

發(fā)現(xiàn)內(nèi)容在宿主機(jī)中也存在, 這樣會有一個問題, 其實busybox就是容器的鏡像層, 如果多個容器共享該鏡像層, 那就會造成容器之間互相看到對方文件, 并且文件覆蓋等等問題.

----------------------------terminal01-------------------------------------
root@nicktming:~/go/src/github.com/nicktming/mydocker# ./mydocker run -it /bin/sh
2019/04/06 23:28:23 rootPath:
2019/04/06 23:28:23 set cmd.Dir by default: /root/busybox
2019/04/06 23:28:23 current path: /root/busybox.
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ # mkdir nicktming01 && echo "testing 01." > nicktming01/test01.txt
/ # cat nicktming01/test01.txt 
testing 01.
// 此時打開terminal02創(chuàng)建另外一個文件夾和文件
----------------------------terminal02-------------------------------------
root@nicktming:~/go/src/github.com/nicktming/mydocker# ./mydocker run -it /bin/sh
2019/04/06 23:30:38 rootPath:
2019/04/06 23:30:38 set cmd.Dir by default: /root/busybox
2019/04/06 23:30:38 current path: /root/busybox.
/ # ls
bin          dev          etc          home         nicktming01  proc         root         sys          tmp          usr          var
/ # mkdir nicktming02 && echo "testing 02." > nicktming02/test02.txt
/ # cat nicktming02/test02.txt 
testing 02.
/ # ls
bin          dev          etc          home         nicktming01  nicktming02  proc         root         sys          tmp          usr          var
/ # exit
root@nicktming:~/go/src/github.com/nicktming/mydocker# 
// 回到terminal01中可以看到另外一個容器創(chuàng)建的文件夾
----------------------------terminal01-------------------------------------
/ # ls
bin          dev          etc          home         nicktming01  nicktming02  proc         root         sys          tmp          usr          var
/ # exit
root@nicktming:~/go/src/github.com/nicktming/mydocker# 

3. 利用AUFS解決此問題

正如[mydocker]---一步步實現(xiàn)使用busybox創(chuàng)建容器 所示, 可以利用AUFS解決該問題.

aufs.png

正如上圖所示, busy為鏡像層, 創(chuàng)建一個文件夾writerLayer為容器層, 所有容器內(nèi)容的操作(增刪改文件)都會在此處, 創(chuàng)建一個mnt文件當(dāng)作掛載點.

3.1 根據(jù)busybox鏡像生成容器

此問題已經(jīng)在[mydocker]---一步步實現(xiàn)使用busybox創(chuàng)建容器實現(xiàn). 這里只是做個小改動, 這次使用busybox.tar也就是根據(jù)某個鏡像來啟動容器, 在這里做個鏡像就是busybox:latest. busybox.tar 就是由鏡像busybox:latest導(dǎo)出的.

command/run.go中加入如下兩個函數(shù):

  1. PathExists方法判斷文件是否存在.
  2. getRootPath方法是根據(jù)命令行-m提供的目錄返回執(zhí)行init程序的目錄. 比如用戶輸入./mydocker run -it -r /nicktming /bin/sh此時rootPath=/nicktming并且需要/nicktming目錄已經(jīng)準(zhǔn)備好了busybox.tar文件, 此時該程序會解壓busybox.tar/nicktming/busybox并且把/nicktming/busybox設(shè)置為執(zhí)行init程序的工作目錄.
func getRootPath(rootPath string) string {
    log.Printf("rootPath:%s\n", rootPath)
    defaultPath := "/root"
    if rootPath == "" {
        log.Printf("rootPath is empaty, set cmd.Dir by default: /root/busybox\n")
        return defaultPath
    }
    imageTar := rootPath + "/busybox.tar"
    exist, _ := PathExists(imageTar)
    if !exist {
        log.Printf("%s does not exist, set cmd.Dir by default: /root/busybox\n", imageTar)
        return defaultPath
    }
    imagePath := rootPath + "/busybox"
    exist, _ = PathExists(imageTar)
    if exist {
        os.RemoveAll(imagePath)
    }
    if err := os.Mkdir(imagePath, 0777); err != nil {
        log.Printf("mkdir %s err:%v, set cmd.Dir by default: /root/busybox\n", imagePath, err)
        return defaultPath
    }
    if _, err := exec.Command("tar", "-xvf", imageTar, "-C", imagePath).CombinedOutput(); err != nil {
        log.Printf("tar -xvf %s -C %s, err:%v, set cmd.Dir by default: /root/busybox\n", imageTar, imagePath, err)
        return defaultPath
    }
    return rootPath
}

func PathExists(path string) (bool, error) {
    _, err := os.Stat(path)
    if err == nil {
        return true, nil
    }
    if os.IsNotExist(err) {
        return false, nil
    }
    return false, err
}

command/run.go中的run方法中加入cmd.Dir = getRootPath(rootPath).

func Run(command string, tty bool, cg *cgroups.CroupManger, rootPath string)  {
...
    newRootPath := getRootPath(rootPath)
    cmd.Dir = newRootPath + "/busybox"
    cmd.ExtraFiles = []*os.File{reader}
    sendInitCommand(command, writer)
...
}

執(zhí)行結(jié)果如下:

------------------------------terminal 02-----------------------------------
root@nicktming:/nicktming# pwd
/nicktming
root@nicktming:/nicktming# ls
busybox.tar
------------------------------terminal 02-----------------------------------
root@nicktming:~/go/src/github.com/nicktming/mydocker# pwd
/root/go/src/github.com/nicktming/mydocker
root@nicktming:~/go/src/github.com/nicktming/mydocker# go build .
root@nicktming:~/go/src/github.com/nicktming/mydocker# ./mydocker run -it -r /nicktming /bin/sh
2019/04/07 01:04:11 rootPath:/nicktming
2019/04/07 01:04:11 current path: /nicktming/busybox.
/ # ls -l
total 44
drwxr-xr-x    2 root     root         12288 Feb 14 18:58 bin
drwxr-xr-x    4 root     root          4096 Mar 17 16:05 dev
drwxr-xr-x    3 root     root          4096 Mar 17 16:05 etc
drwxr-xr-x    2 nobody   nogroup       4096 Feb 14 18:58 home
dr-xr-xr-x  106 root     root             0 Apr  6 17:04 proc
drwx------    2 root     root          4096 Apr  6 17:04 root
drwxr-xr-x    2 root     root          4096 Mar 17 16:05 sys
drwxrwxrwt    2 root     root          4096 Feb 14 18:58 tmp
drwxr-xr-x    3 root     root          4096 Feb 14 18:58 usr
drwxr-xr-x    4 root     root          4096 Feb 14 18:58 var
/ # ps -ef
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    5 root      0:00 ps -ef
/ # exit
root@nicktming:~/go/src/github.com/nicktming/mydocker#

3.2 創(chuàng)建掛載點

上面已經(jīng)實現(xiàn)了利用某個鏡像來創(chuàng)建容器, 并且可以自定義設(shè)置Init程序的工作目錄.

按照上圖, 可以創(chuàng)建一個掛載點mnt, 創(chuàng)建一個容器可寫層writerLayer, 鏡像層為busybox. 假設(shè)在/nicktming目錄下執(zhí)行.

1. mkdir -p /nicktming/busybox
2. tar -xvf /nicktming/busybox.tar -C /nicktming/busybox
3. mkdir -p /nicktming/mnt && mkdir -p /nicktming/writerLayer
4. mount -t aufs -o dirs=/nicktming/writerLayer:/nicktming/busybox none /nicktming/mnt
5. Init程序工作目錄為:/nicktming/mnt

上述步驟對應(yīng)如下command/run.go中增加如下方法.

// 將默認(rèn)路徑設(shè)置為/nicktming
const (
    DEFAULTPATH = "/nicktming"
)
// 創(chuàng)建 rootPath/busybox  (比如:mkdir -p /nicktming/busybox)
// tar -xvf rootPath/busybox.tar -C rootPath/busybox (比如: tar -xvf /nicktming/busybox.tar -C /nicktming/busybox)
func getRootPath(rootPath string) string {
    log.Printf("rootPath:%s\n", rootPath)
    defaultPath := DEFAULTPATH
    if rootPath == "" {
        log.Printf("rootPath is empaty, set cmd.Dir by default: /%s/busybox\n", defaultPath)
        rootPath = defaultPath
    }
    imageTar := rootPath + "/busybox.tar"
    exist, _ := PathExists(imageTar)
    if !exist {
        log.Printf("%s does not exist, set cmd.Dir by default: /%s/busybox\n", defaultPath)
        return defaultPath
    }
    imagePath := rootPath + "/busybox"
    exist, _ = PathExists(imageTar)
    if exist {
        os.RemoveAll(imagePath)
    }
    if err := os.Mkdir(imagePath, 0777); err != nil {
        log.Printf("mkdir %s err:%v, set cmd.Dir by default: /%s/busybox\n", imagePath, err, defaultPath)
        return defaultPath
    }
    if _, err := exec.Command("tar", "-xvf", imageTar, "-C", imagePath).CombinedOutput(); err != nil {
        log.Printf("tar -xvf %s -C %s, err:%v, set cmd.Dir by default: /%s/busybox\n", imageTar, imagePath, err, defaultPath)
        return defaultPath
    }
    return rootPath
}
// 創(chuàng)建Init程序工作目錄
func NewWorkDir(rootPath string) error {
    if err := CreateContainerLayer(rootPath); err != nil {
        return fmt.Errorf("CreateContainerLayer(%s) error: %v.\n", rootPath, err)
    }
    if err := CreateMntPoint(rootPath); err != nil {
        return fmt.Errorf("CreateContainerLayer(%s) error: %v.\n", rootPath, err)
    }
    if err := SetMountPoint(rootPath); err != nil {
        return fmt.Errorf("CreateContainerLayer(%s) error: %v.\n", rootPath, err)
    }
    return nil
}
// 生成 rootPath/writerLayer文件夾 (比如:mkdir -p /nicktming/writerLayer)
func CreateContainerLayer(rootPath string) error {
    writerLayer := rootPath + "/writerLayer"
    if err := os.Mkdir(writerLayer, 0777); err != nil {
        log.Printf("mkdir %s err:%v\n", writerLayer, err)
        return fmt.Errorf("mkdir %s err:%v\n", writerLayer, err)
    }
    return nil 
}
// 生成 rootPath/mnt文件夾 (比如:mkdir -p /nicktming/mnt)
func CreateMntPoint(rootPath string) error {
    mnt := rootPath + "/mnt"
    if err := os.Mkdir(mnt, 0777); err != nil {
        log.Printf("mkdir %s err:%v\n", mnt, err)
        return fmt.Errorf("mkdir %s err:%v\n", mnt, err)
    }
    return nil
}
// 掛載 (比如:mount -t aufs -o dirs=/nicktming/writerLayer:/nicktming/busybox none /nicktming/mnt)
func SetMountPoint(rootPath string) error {
    dirs := "dirs=" + rootPath + "/writerLayer:" + rootPath + "/busybox"
    mnt := rootPath + "/mnt"
    if _, err := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", mnt).CombinedOutput(); err != nil {
        log.Printf("mount -t aufs -o %s none %s, err:%v\n", dirs, mnt, err)
        return fmt.Errorf("mount -t aufs -o %s none %s, err:%v\n", dirs, mnt, err)
    }
    return nil
}

完成上面方法后, 需要給Init程序設(shè)置工作目錄. 修改command/run.go中的run方法.

func Run(command string, tty bool, cg *cgroups.CroupManger, rootPath string)  {
...
  newRootPath := getRootPath(rootPath)
    cmd.Dir = newRootPath + "/busybox"
    if err := NewWorkDir(newRootPath); err == nil {
        cmd.Dir = newRootPath + "/mnt"
    }
...
}

執(zhí)行結(jié)果如下:

// 原始狀態(tài)
-------------------------------terminal 01----------------------------------
root@nicktming:/nicktming# pwd
/nicktming
root@nicktming:/nicktming# ls
busybox.tar
root@nicktming:/nicktming# 

// 創(chuàng)建容器并且寫入文件
-------------------------------terminal 02----------------------------------
root@nicktming:~/go/src/github.com/nicktming/mydocker# go build .
root@nicktming:~/go/src/github.com/nicktming/mydocker# ./mydocker run -it /bin/sh
2019/04/07 14:23:53 rootPath:
2019/04/07 14:23:53 rootPath is empaty, set cmd.Dir by default: //nicktming/busybox
2019/04/07 14:23:53 current path: /nicktming/mnt.
/ # ps -l
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    4 root      0:00 ps -l
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ # mkdir nicktming01 && echo "testing01" > nicktming01/test01.txt
/ # ls
bin          dev          etc          home         nicktming01  proc         root         sys          tmp          usr          var
/ # cat nicktming01/test01.txt 
testing01

// 回到宿主機(jī)中可以看到對應(yīng)生成了mnt writerLayer busybox, 并且內(nèi)容在可寫層和mnt中
-------------------------------terminal 01----------------------------------
root@nicktming:/nicktming# ls
busybox  busybox.tar  mnt  writerLayer
root@nicktming:/nicktming# ls mnt/
bin  dev  etc  home  nicktming01  proc  root  sys  tmp  usr  var
root@nicktming:/nicktming# cat mnt/nicktming01/test01.txt 
testing01
root@nicktming:/nicktming# ls writerLayer/
nicktming01  root
root@nicktming:/nicktming# cat writerLayer/nicktming01/test01.txt 
testing01

// 退出容器
-------------------------------terminal 02----------------------------------
/ # exit
root@nicktming:~/go/src/github.com/nicktming/mydocker# 

// 退出容器后busybox中的內(nèi)容都沒有做任何改變 容器層寫入的東西保留在了mnt 和 writerLayer中, 并且掛載點依然存在
-------------------------------terminal 01----------------------------------
root@nicktming:/nicktming# ls
busybox  busybox.tar  mnt  writerLayer
root@nicktming:/nicktming# ls busybox
bin  dev  etc  home  proc  root  sys  tmp  usr  var
root@nicktming:/nicktming# ls mnt/
bin  dev  etc  home  nicktming01  proc  root  sys  tmp  usr  var
root@nicktming:/nicktming# ls writerLayer/
nicktming01  root
root@nicktming:/nicktming# df -h
Filesystem      Size  Used Avail Use% Mounted on
...
none             50G  2.7G   45G   6% /nicktming/mnt

3.3 清理工作

3.2 創(chuàng)建掛載點中已經(jīng)基本上達(dá)到了預(yù)期的效果, 只是在容器退出的時候掛載點和容器層依然存在, 需要將其卸載并清除.

清理工作分如下幾步: 假設(shè) rootPath=/nicktming

1. umount /nicktming/mnt
2. rmdir /nicktming/mnt
3. rmdir /nicktming/writerLayer

對應(yīng)的方法如下, 在command/run.go中加入如下方法.

func ClearWorkDir(rootPath string)  {
    ClearMountPoint(rootPath)
    ClearWriterLayer(rootPath)
}

func ClearMountPoint(rootPath string)  {
    mnt := rootPath + "/mnt"
    if _, err := exec.Command("umount", "-f", mnt).CombinedOutput(); err != nil {
        log.Printf("mount -f %s, err:%v\n", mnt, err)
    }
    if err := os.RemoveAll(mnt); err != nil {
        log.Printf("remove %s, err:%v\n", mnt, err)
    }
}

func ClearWriterLayer(rootPath string) {
    writerLayer := rootPath + "/writerLayer"
    if err := os.RemoveAll(writerLayer); err != nil {
        log.Printf("remove %s, err:%v\n", writerLayer, err)
    }
}

command/run.go中的run方法中修改如下:

func Run(command string, tty bool, cg *cgroups.CroupManger, rootPath string)  {
...
    newRootPath := getRootPath(rootPath)
    cmd.Dir = newRootPath + "/busybox"
    if err := NewWorkDir(newRootPath); err == nil {
        cmd.Dir = newRootPath + "/mnt"
    }
    defer ClearWorkDir(newRootPath)
...
}

執(zhí)行結(jié)果如下:

// 準(zhǔn)備鏡像busybox.tar
-------------------------------terminal 01----------------------------------
root@nicktming:/nicktming# pwd
/nicktming
root@nicktming:/nicktming# ls
busybox.tar

// 創(chuàng)建容器
-------------------------------terminal 02----------------------------------
root@nicktming:~/go/src/github.com/nicktming/mydocker# pwd
/root/go/src/github.com/nicktming/mydocker
root@nicktming:~/go/src/github.com/nicktming/mydocker# go build .
root@nicktming:~/go/src/github.com/nicktming/mydocker# ./mydocker run -it /bin/sh
2019/04/07 15:37:30 rootPath:
2019/04/07 15:37:30 rootPath is empaty, set cmd.Dir by default: /nicktming/busybox
2019/04/07 15:37:30 current path: /nicktming/mnt.
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ # mkdir nicktming01 && echo "testing01\n" > nicktming01/test01.txt
/ # ls
bin          dev          etc          home         nicktming01  proc         root         sys          tmp          usr          var
/ # cat nicktming01/test01.txt 
testing01\n


// 查看宿主機(jī)內(nèi)容
-------------------------------terminal 01----------------------------------
root@nicktming:/nicktming# ls
busybox  busybox.tar  mnt  writerLayer
root@nicktming:/nicktming# cat mnt/nicktming01/test01.txt 
testing01\n
root@nicktming:/nicktming# cat writerLayer/nicktming01/test01.txt 
testing01\n
root@nicktming:/nicktming# df -h
Filesystem      Size  Used Avail Use% Mounted on
...
none             50G  2.7G   45G   6% /nicktming/mnt


// 退出容器
-------------------------------terminal 02----------------------------------
/ # exit
root@nicktming:~/go/src/github.com/nicktming/mydocker# 

// 查看宿主機(jī)內(nèi)容 
-------------------------------terminal 01----------------------------------
root@nicktming:/nicktming# ls
busybox  busybox.tar
root@nicktming:/nicktming# df -h
Filesystem      Size  Used Avail Use% Mounted on
...

/nicktming/mnt掛載點已經(jīng)沒有, 并且中間文件夾mnt, writerLayer已經(jīng)被刪除了.

4. 時序圖

4-2.png

5. 參考

1. 自己動手寫docker.(基本參考此書,加入一些自己的理解,加深對docker的理解)

6. 全部內(nèi)容

mydocker.png

1. [mydocker]---環(huán)境說明
2. [mydocker]---urfave cli 理解
3. [mydocker]---Linux Namespace
4. [mydocker]---Linux Cgroup
5. [mydocker]---構(gòu)造容器01-實現(xiàn)run命令
6. [mydocker]---構(gòu)造容器02-實現(xiàn)資源限制01
7. [mydocker]---構(gòu)造容器02-實現(xiàn)資源限制02
8. [mydocker]---構(gòu)造容器03-實現(xiàn)增加管道
9. [mydocker]---通過例子理解存儲驅(qū)動AUFS
10. [mydocker]---通過例子理解chroot 和 pivot_root
11. [mydocker]---一步步實現(xiàn)使用busybox創(chuàng)建容器
12. [mydocker]---一步步實現(xiàn)使用AUFS包裝busybox
13. [mydocker]---一步步實現(xiàn)volume操作
14. [mydocker]---實現(xiàn)保存鏡像
15. [mydocker]---實現(xiàn)容器的后臺運行
16. [mydocker]---實現(xiàn)查看運行中容器
17. [mydocker]---實現(xiàn)查看容器日志
18. [mydocker]---實現(xiàn)進(jìn)入容器Namespace
19. [mydocker]---實現(xiàn)停止容器
20. [mydocker]---實現(xiàn)刪除容器
21. [mydocker]---實現(xiàn)容器層隔離
22. [mydocker]---實現(xiàn)通過容器制作鏡像
23. [mydocker]---實現(xiàn)cp操作
24. [mydocker]---實現(xiàn)容器指定環(huán)境變量
25. [mydocker]---網(wǎng)際協(xié)議IP
26. [mydocker]---網(wǎng)絡(luò)虛擬設(shè)備veth bridge iptables
27. [mydocker]---docker的四種網(wǎng)絡(luò)模型與原理實現(xiàn)(1)
28. [mydocker]---docker的四種網(wǎng)絡(luò)模型與原理實現(xiàn)(2)
29. [mydocker]---容器地址分配
30. [mydocker]---網(wǎng)絡(luò)net/netlink api 使用解析
31. [mydocker]---網(wǎng)絡(luò)實現(xiàn)
32. [mydocker]---網(wǎng)絡(luò)實現(xiàn)測試

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容