zoukankan      html  css  js  c++  java
  • 网络IPC:套接字

    网络进程间通信:socket API简介

    不同计算机(通过网络相连)上运行的进程相互通信机制称为网络进程间通信(network IPC)。

    在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)构成套接字,就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

    套接字是通信端口的抽象!通过套接字网络IPC接口,进程能够使用该接口和其他进程通信。

    几个定义:

    1. IP地址:即依照TCP/IP协议分配给本地主机的网络地址,两个进程要通讯,任一进程首先要知道通讯对方的位置,即对方的IP。
    2. 端口号:用来辨别本地通讯进程,一个本地的进程在通讯时均会占用一个端口号,不同的进程端口号不同,因此在通讯前必须要分配一个没有被访问的端口号。
    3. 连接:指两个进程间的通讯链路。
    4. 半相关:网络中用一个三元组可以在全局唯一标志一个进程:(协议,本地地址,本地端口号)这样一个三元组,叫做一个半相关,它指定连接的每半部分。
    5. 全相关:一个完整的网间进程通信需要由两个进程组成,并且只能使用同一种高层协议。也就是说,不可能通信的一端用TCP协议,而另一端用UDP协议。因此一个完整的网间通信需要一个五元组来标识:(协议,本地地址,本地端口号,远地地址,远地端口号),这样一个五元组,叫做一个相关(association),即两个协议相同的半相关才能组合成一个合适的相关,或完全指定组成一连接。

    套接字描述符

    套接字是端点的抽象。与应用进程要使用文件描述符访问文件一样,访问套接字也需要用套接字描述符。套接字描述符在UNIX系统中是用文件描述符实现的。

    要创建一个套接字,可以调用socket函数。

    #include<sys/socket.h>
    int socket(int domain, int type, int protocol);
    

      参数:

    作用:socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。

    网络字节序

    网络协议指定了字节序,因此异构计算机系统能够交换协议信息而不会混淆字节序。TCP/IP协议栈采用大端字节序。应用进程交换格式化数据时,字节序问题就会出现。对于TCP/IP,地址用网络字节序来表示,所以应用进程有时需要在处理器的字节序与网络字节序之间转换。

    #include<arpa/inet.h>
    uint32_t htonl(uint32_t hostlong);
    uint16_t htons(uint16_t hostshort);
    uint32_t ntohl(uint32_t netlong);
    uint16_t ntohs(uint16_t netshort);
    

    这些函数名很好记,h表示host,n表示network, l表示32位长整数,s表示16位短整数

    在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket!

    将套接字与地址绑定

    与客户端的套接字关联的地址意义不大,可以让系统选择一个默认的地址。然而,对于服务器,需要给一个接收客户端请求的套接字绑定一个众所周知的地址。客户端应有一种方法用以连接服务器的地址,最简单的方法就是为服务器保留一个地址并且在/etc/services或某个名字服务(name service)中注册。

      可以用bind函数来搞定这个问题:

    #include <sys/types.h>        
    #include <sys/socket.h>
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

    参数:

    第一个参数:bind()函数把一个地址族中的特定地址赋给该sockfd(套接字描述字)。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。 

    第二个参数:struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同:

    地址格式

    地址标识了特定通信域中的套接字端点,地址格式与特定的通信域相关。为使不同格式地址能够被传入到套接字函数,地址需被强转为通用的地址结构sockaddr表示。

    //头文件
    #include<netinet/in.h>

    struct sockaddr 是一个通用地址结构,该结构定义如下: 

    struct sockaddr
    {
       sa_family_t sa_family;
       char        sa_data[14];
    }

    IPV4因特网域:

    //ipv4对应的是: 
    /* 网络地址 */
    struct in_addr 
    {
        uint32_t       s_addr;     /* address in network byte order */
    };
    
    struct sockaddr_in {
        sa_family_t    sin_family;    /* address family: AF_INET */
        in_port_t      sin_port;      /* port in network byte order */
        struct in_addr sin_addr;      /* internet address */
    };

    IPv6因特网域:

    //ipv6对应的是: 
    struct in6_addr 
    { 
        unsigned char   s6_addr[16];   /* IPv6 address */ 
    };
    
    struct sockaddr_in6 
    { 
        sa_family_t     sin6_family;   /* AF_INET6 */ 
        in_port_t       sin6_port;     /* port number */ 
        uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
        struct in6_addr sin6_addr;     /* IPv6 address */ 
        uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
    };
    

      Unix域对应的是: 

    #define UNIX_PATH_MAX    108
    
    struct sockaddr_un 
    { 
        sa_family_t sun_family;               /* AF_UNIX */ 
        char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
    };

    第三个参数:addrlen 对应的是地址的长度

    返回值:成功返回0,出错返回-1

    作用:将套接字与端口号绑定,即把一个ip地址和端口号组合赋给socket


    点分十进制IP与网络字节序IP之间的转换

    有时需要打印出能被人而不是计算机所理解的地址格式。我们可以利用函数来进行二进制地址格式与点分十进制格式的相互转换。但是这些函数仅支持IPv4地址。

     #include <sys/socket.h>
     #include <netinet/in.h>
     #include <arpa/inet.h>
     //点分十进制IP转换网络字节序IP
     int inet_aton(const char *cp, struct in_addr *inp);
     //点分十进制IP转换网络字节序IP
     in_addr_t inet_addr(const char *cp);
     //网络字节序IP 转化点分十进制IP
     char *inet_ntoa(struct in_addr in);

    其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void* 类型!

    #include <arpa/inet.h>
    //网络字节序IP 转化点分十进制IP
    const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
    //点分十进制IP转换网络字节序IP
    int inet_pton(int af, const char *src, void *dst);
    

    监听

    如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

    服务器调用 listen 来宣告可以接收连接请求!

    #include <sys/types.h>    
    #include <sys/socket.h>
     int listen(int sockfd, int backlog);
    

    参数:sockfd为要监听的socket描述字,backlog为相应socket可以排队的最大连接个数  

    返回值:成功返回0,出错返回-1

    作用:socket函数创建一个套接字时,默认是一个主动套接字,listen函数把一个未调用connect的未连接的套接字转换成一个被动套接字,指示内核应接收指向该套接字的连接请求。(主动/客户 -> 被动/服务器)


    连接

    如果是面向连接的网络服务,在开始交换数据前,都要在请求服务的进程套接字(客户端)和提供服务的进程套接字(服务器)之间建立一个连接,使用connect函数:

    #include <sys/types.h>        
    #include <sys/socket.h>
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    

    参数:第一个参数sockfd为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。 

    返回值:成功返回0,出错返回-1

    作用:客户端通过调用connect函数来建立与TCP服务器的连接

    注意:在connect中所指定的地址是想与之通信的服务器地址。如果sockfd没有绑定到一个地址,connect会给调用者绑定一个默认地址!


    使用accept函数获得连接请求并建立连接

    #include <sys/types.h>          
    #include <sys/socket.h>
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    

    参 数 :第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度 

    返回值:如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,该描述符连接到调用connect的客户端。这个新的套接字描述符和原始的套接字描述符具有相同的套接字类型和地址族。

    注 意:传给accept的原始套接字没有关联到这个连接,而是继续保持可用状态并接受其它连接请求!

    通俗点来说,accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。


    数据传输

    既然套接字端点表示文件描述符,那么只要建立连接,就可以使用write和read来通过套接字通信了。

    #include <unistd.h>
    ssize_t write(int fd, const void *buf, size_t count);
    ssize_t read(int fd, void *buf, size_t count);
    

    write()会把指针buf所指的内存写入count个字节到参数fd所指的文件内(文件读写位置也会随之移动),如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中!

    read()会把参数fd所指的文件传送nbyte个字节到buf指针所指的内存中,成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0 。

    如果想指定多个选项、从多个客户端接收数据包或发送带外数据,需要采用6个传递数据的套接字函数中的一个。

    三个函数用来发送数据:

    #include <sys/types.h>
    #include <sys/socket.h>
    ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
    ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
    

     sendto()适用于已连接的数据报或流式套接口发送数据。

    参数:

    •   sockfd:一个标识套接口的描述字。
    •   buf:包含待发送数据的缓冲区。
    •   len:buf缓冲区中数据的长度。
    •   flags:调用方式标志位。
    •   dest_addr:(可选)指针,指向目的套接口的地址。
    •   addrlen:所指地址的长度。

    三个函数用来接收数据:

    #include <sys/types.h>
    #include <sys/socket.h>
    ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
    ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
    
    recvfrom()函数用于从已连接的套接口上接收数据,并捕获数据发送源的地址。
    参数:
    • sockfd:用来标识一个已连接套接口的描述字;
    • buf:接收数据缓冲区;
    • len:缓冲区长度;
    • flags:调用操作方式,一般情况下为0;
    • src_addr:指向装有源地址缓冲区的指针;

    关闭套接字描述符

     close函数用来关闭文件描述符:

    #include <unistd.h>
    int close(int fd);
    

      注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

     地址“重用”

    缺省条件下,一个套接字不能与一个已在使用中的本地地址捆绑。但有时会需要“重用”地址。因为每一个连接都由本地地址和远端地址的组合唯一确定,所以只要远端地址不同,两个套接口与一个地址捆绑并无大碍。为了通知套接口实现不要因为一个地址已被一个套接口使用就不让它与另一个套接口捆绑,应用程序可在bind()调用前先设置SO_REUSEADDR选项。请注意仅在bind()调用时该选项才被解释;故此无需(但也无害)将一个不会共用地址的套接字设置该选项,或者在bind()对这个或其他套接口无影响情况下设置或清除这一选项。

    解决这个问题的方法是使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。 在server代码的socket()和bind()调用之间插入如下代码:

    int opt=1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    基于TCP的socket通信基本流程:

    1. TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。
    2. TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。
    3. TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。
    4. 之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

    建立一个基于TCP的socket API  :

    服务器:

    /*************************************************************************
     > File Name: server.c
     > Author:Lynn-Zhang 
     > Mail: iynu17@yeah.net
     > Created Time: Fri 29 Jul 2016 12:15:28 PM CST
     ************************************************************************/
    
    #include<stdio.h>
    #include<netinet/in.h>
    #include<sys/types.h>
    #include<sys/socket.h>
    #include<string.h>
    #include<stdlib.h>
    #include<arpa/inet.h>
    #include<pthread.h>
    
    static void usage(const char* proc)
    {
        printf("Usage: %s [ip] [port]
    ",proc);
    }
    
    void *thread_run(void *arg)
    {
        printf("create a new thread
    ");
        int fd=(int)arg;
        char buf[1024];
        while(1)
        {
            //服务器端将套接字描述符中到数据读到buf并打印,再将自己的回复写入套接字描述符
            memset(buf,'',sizeof(buf));
            ssize_t _s=read(fd,buf,sizeof(buf)-1);
            if(_s>0)
            {
                buf[_s]='';
                printf("client:# %s",buf);
                printf("server:$ ");
                fflush(stdout);
                
                //服务器将回复写入fd
                memset(buf,'',sizeof(buf));
                ssize_t _in=read(0,buf,sizeof(buf)-1);
                if(_in>=0)
                {
                  buf[_in-1]='';
                  write(fd,buf,strlen(buf));
                }
                printf("please wait ...
    ");
            }
            else if(_s==0)
            {
                printf("client close...
    ");
                break;
            }
            else
            {
                printf("read error ...
    ");
                break;
            }
        }
        return (void*)0;
    }
    
    int main(int argc,char *argv[])
    {
        //参数必须能构成完整的socket
        if(argc!=3)
        {
            usage(argv[0]);
            exit(1);
        }
        //建立服务器端socket
        int listen_sock=socket(AF_INET,SOCK_STREAM,0);
        if(listen_sock<0)
        {
            perror("socket");
            return 1;
        }
    
        struct sockaddr_in local;
        local.sin_family=AF_INET;
        local.sin_port=htons(atoi(argv[2]));
        local.sin_addr.s_addr=inet_addr(argv[1]);
        
        int opt=1;
        if(setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))<0)
        {
            perror("setsockopet error
    ");
            return -1;
        }
    
        //将套接字绑定到服务器端的ip地址和端口号绑定
        if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0)
        {
            perror("bind");
            return 2;
        }
        //建立监听队列,等待套接字的连接请求
        listen(listen_sock,5);
    
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        while(1)
        {
            //获得连接请求并建立连接
            int client_sock=accept(listen_sock,(struct sockaddr*)&peer,&len);
            if(client_sock<0)
            {
                perror("accept faild ...
    ");
                return 3;
            }
            printf("get a new link,socket -> %s:%d
    ",inet_ntoa(peer.sin_addr));
    
            pthread_t id;
            pthread_create(&id,NULL,thread_run,(void*)client_sock);
    
            pthread_detach(id);
    
    //        pid_t id=fork();
    //        if(id==0)
    //        {//child
    //            char buf[1024];
    //            while(1)
    //            {
    //                 //将监听到的套接子描述符指定文件描述中的数据读到buf中
    //                memset(buf,'',sizeof(buf));
    //                ssize_t _s=read(client_sock,buf,sizeof(buf)-1);
    //                if(_s>0)
    //                {
    //                    buf[_s-1]=''
    //                    printf("client:# %s
    ",buf); 
    //                    printf("server:$ ");
    //                    fflush(stdout);
    //                    memset(buf,'',sizeof(buf));
    //                    ssize_t _s=read(0,buf,sizeof(buf)-1);
    //                    if(_s>0)
    //                    {                    
    //                         buf[_s-1]='';
    //                         write(client_sock,buf,strlen(buf));
    //                    }
    //                    else
    //                    {
    //                        printf("Fail !
    ");
    //                    }
    //                }
    //                else
    //                {
    //                      printf("read done...
    ");
    //                    break;
    //                }
    //            }
    //
    //        }
    //        else
    //        {//father
    //            waitpid(-1,NULL,WNOHANG);
    //        }
    //
        }
        close(listen_sock);
        return 0;
    }
    

      客户端:

    /*************************************************************************
     > File Name: client.c
     > Author:Lynn-Zhang 
     > Mail: iynu17@yeah.net
     > Created Time: Fri 29 Jul 2016 09:00:01 AM CST
     ************************************************************************/
    
    #include<stdio.h>
    #include<netinet/in.h>
    #include<sys/types.h>
    #include<sys/socket.h>
    #include<string.h>
    #include<stdlib.h>
    #include<arpa/inet.h>
    #include<errno.h>
    #include<pthread.h>
    
    static usage(const char* proc)
    {
        printf("Usage: %s [ip] [port]
    ",proc);
    }
    
    int main(int argc,char* argv[])
    {
        //传入的参数是一个完整的socket(ip地址+端口号)
        if(argc!=3)
        {
            usage(argv[0]);
            exit(1);
        }
        //建立一个套接字描述符
        int sock=socket(AF_INET,SOCK_STREAM,0);
        if(sock<0)
        {
            perror("socket");
            return 2;
        }
        //IPv4因特网域(AF_INET)中,套接字地址用sockaddr_in表示
        struct sockaddr_in remote;
        remote.sin_family=AF_INET;   //socket通信域
        remote.sin_port=htons(atoi(argv[2]));   //端口号
        remote.sin_addr.s_addr=inet_addr(argv[1]);  //ip地址
        //请求连接
        int ret=connect(sock,(struct sockaddr*)&remote,sizeof(remote));
        if(ret<0)
        {
            printf("connect failed ... ,errno is :%d,errstring is: %s
    ",errno,strerror(errno));
            return 3;
        }
        printf("connect success ...
    ");
        char buf[1024];
        while(1)
        {
            //从标准输入将数据读入buf中,再写入sock中
            memset(buf,'',sizeof(buf));
            printf("client:# ");
            fflush(stdout);
            ssize_t _s=read(0,buf,sizeof(buf)-1);
            fflush(stdin);
            if(_s<0)
            {
                perror("read
    ");
                break;
            }
            buf[_s]='';
            write(sock,buf,strlen(buf));
            if(strcmp(buf,"quit")==0)
            {
                printf("quit!
    ");
                break;
            }
    
            _s=read(sock,buf,sizeof(buf));
            if(_s>0)
            {
                buf[_s]='';
                printf("server:$ %s
    ",buf);
            }
        }
        close(sock);
        printf("sock close");
        return 0;
    }
    

      服务器: 

    客户端:



     基于UDP协议的Socket编程

    注意:

    UDP没有建立连接的过程!

    创建一个基于udp协议的套接字,使用socket函数时第二个参数不能传递SOCK_STREAM,而是传递SOCK_DGRAM 

    如创建一个基于IPv4地址族的UDP套接字: socket(AF_INET,SOCK_DGRAM, 0);

    通常用于基于UDP协议的I/O一般使用 recvfrom 和 sendto 两个函数进行数据收发!

     服务器:

    /*************************************************************************
     > File Name: server.c
     > Author:Lynn-Zhang 
     > Mail: iynu17@yeah.net
     > Created Time: Wed 03 Aug 2016 01:14:30 PM CST
     ************************************************************************/
    
    #include<stdio.h>
    #include<sys/types.h>
    #include<sys/socket.h>
    #include<netinet/in.h>
    #include<arpa/inet.h>
    #include<string.h>
    
    void usage(const char* proc)
    {
        printf("Usage: %s [ip] [port]
    ",proc);
    }
    int main(int argc,char* argv[])
    {
        //要求输出配套到套接字
        if(argc!=3)
        {
            usage(argv[0]);
            return 1;
        }
        //建立套接字描述符
        int sock=socket(AF_INET,SOCK_DGRAM,0);
        if(sock<0)
        {
            perror("socket");
            return 2;
        }
        
        struct sockaddr_in local;
        local.sin_family=AF_INET;
        local.sin_port=htons(atoi(argv[2]));
        local.sin_addr.s_addr=inet_addr(argv[1]);
        //将套接字与地址绑定 
        int ret=bind(sock,(struct sockaddr*)&local,sizeof(local));
        if(ret<0)
        {
            perror("bind");
            return 3;
        }
        int done=0;
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        char buf[1024];
    
        while(!done)
        {
            memset(buf,'',sizeof(buf));
            recvfrom(sock,buf,sizeof(buf),0,(struct sockaddr*)&peer,&len);
            printf("##########################
    ");
            printf("get a client , socket:%s:%d
    ",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));
            printf("client:%s,echo client!
    ",buf);
            printf("##########################
    ");
    printf("server:");
            fflush(stdout);
            memset(buf,'',sizeof(buf));
            ssize_t _s= read(0,buf,sizeof(buf)-1);
            buf[_s]='';
            sendto(sock,buf,sizeof(buf),0,(struct sockaddr*)&peer,len);
        }
        return 0;
    }
    

      客户端:

    /*************************************************************************
     > File Name: client.c
     > Author:Lynn-Zhang 
     > Mail: iynu17@yeah.net
     > Created Time: Wed 03 Aug 2016 03:48:21 PM CST
     ************************************************************************/
    
    #include<stdio.h>
    #include<sys/types.h>
    #include<sys/socket.h>
    #include<netinet/in.h>
    #include<arpa/inet.h>
    #include<string.h>
    
    static void usage(const char* proc)
    {
        printf("Usage:%s [remote_ip] [remote_port]
    ",proc);
    }
    
    int main(int argc,char* argv[])
    {
        if(argc!=3)
        {
            usage(argv[0]);
            return 1;
        }
    
        int sock=socket(AF_INET,SOCK_DGRAM,0);
        if(sock<0)
        {
            perror("socket");
            return 2;
        }
    
        struct sockaddr_in remote;
        remote.sin_family=AF_INET;
        remote.sin_port=htons(atoi(argv[2]));
        remote.sin_addr.s_addr=inet_addr(argv[1]);
    
        int done=0;
        char buf[1024];
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        while(!done)
        {
            printf("Please Enter: ");
            fflush(stdout);
            ssize_t _s=read(0,buf,sizeof(buf)-1);
            if(_s>0)
            {
                buf[_s-1]='';
                sendto(sock,buf,sizeof(buf),0,(struct sockaddr*)&remote,sizeof(remote));
                
                memset(buf,'',sizeof(buf));
                recvfrom(sock,buf,sizeof(buf),0,(struct sockaddr*)&peer,&len);
                printf("server echo %s
    socket: %s:%d
    ",buf,inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));
            }
        }
        return 0;
    }
    

     运行结果:

    服务器:

    客户端:

    部分参考:

    吴秦   http://www.cnblogs.com/skynet/

    《Unix 环境高级编程》

  • 相关阅读:
    BeanFactory not initialized or already closed
    点击程序不弹出界面,但有后台服务
    python获取一年所有的日期
    keepalived实现高可用
    解决docker镜像pull超时问题
    docker容器的操作
    docker小结
    docker概述
    docker镜像操作
    python批量下载
  • 原文地址:https://www.cnblogs.com/Lynn-Zhang/p/5716078.html
Copyright © 2011-2022 走看看