IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> Linux C 网络编程(socket 套接字) -> 正文阅读

[网络协议]Linux C 网络编程(socket 套接字)

功能

前面我们学习了系统编程,通过管道等我们可以将一个系统中的资源和信息调用起来,但这是远远不够的,我们还可以通过网络编程和其他的的系统来交互信息。
为了相互通信,国际制定了TCP/IP协议,通过IP地址和端口好进行通信。每一部设备都有自己的IP地址。
其实网络编程就是将一个数据包发送出去的过程,如何理解这个过程我推荐下面这篇博客,用通俗的语言简单讲解了一下,让你先有一个概念。
一台计算机是如何把数据发送给另一台计算机的

套接字

套接字地址结构

#include <socket.h>
struct sockaddr{   //该结构体一般不用
	unsigned short   sa_family;     //地址类型
	char             sa_data[14]    //14字节的协议地址
};

其中,成员sa_ family 表示套接字的协议族类型,对应于TCP/IP协议该值为AF INET;成员sa _data存储具体的协议地址。sa data 之所以被定义成14个字节,因为有的协议族使用较长的地址格式。一般在编程中并不对该结构体进行操作,而是使用另一个 与它等价的数据结构:

struct sockaddr_in{
	unsigned short   sin_family;   //地址类型
	unsigned short   sin_port;     //端口号
	struct in_addr   sin_addr;     //IP地址
	unsigned char    sin_zero[8];   //填充字节,一般为0
};

其中,成员sin_family表示地址类型,对于使用TCP/IP协议进行的网络编程,该值只能是AF_INET。sin_port是端口号,sin_addr 用来存储32位的IP地址,数组sin_zero为填充字段,一般赋值为 0。
sockaddr_in信息设置例子:

struct sockaddr_in sock;
sock.sin_family = AF_INET;
sock.sin_port   = htons(80);   //设置端口号80
sock.sin_addr.s_addr = inet_addr("127.0.0.1")     //设置地址
memset(sock.sin_zero,0,sizeof(sock.sin_zero));     //将数组sin_zero清0

套接字的创建

socket函数用来创建一个套接字, 函数原型为:

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

参数domain用于指定套接字所使用的协议族,定义在linux/socket.h中定义。常用的协议族如下:

  • AF_UNIX:创建只在本机内进行通信的套接字。
  • AF_INET:使用IPv4 TCP/IP协议。
  • AF_INET6:使用IPv6 TCP/IP协议。
    参数type指定套接字的类型,可以取如下值。
  • SOCK_STREAM:创建TCP流套接字。
  • SOCK_DGRAM:创建UDP数据报套接字。
  • SOCK_RAW:创建原始套接字。
    参数protocol通常设置为0,表示通过参数domain指定的协议族和参数type指定的套接字类型来确定使用的协议。
    创建成功后返回一个新创建的套接字;若有错误返回-1,错误代码存入errno中。

建立联系

函数connect用来在一个指定的套接字上创建一个连接,函数原型为:

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

参数sockfd是一个由函数socket创建的套接字。如果该套接字的类型SOCK_STREAM, 则connect函数用于向服务器发出连接请求,服务器的IP地址和端口号由参数serv_addr 指定。如果套接字的类型是SOCK_DGRAM,则conneet函数并不建立真正的连接,它只是告诉内核与该套接字进行通信的目的地址(由第二个参数指定),只有该目的地址发来的数据才会被该socket接收。对于SOCK DGRAM类型的套接字,调用connect函数的好处是不必在每次发送和接收数据时都指定目的地址。
通常一个面向连接的套接字(如TCP套接字)只能调用- -次connect函数。而对于无连接的套接字(如UDP套接字)则可以多次调用connect函数以改变与目的地址的绑定。将参数serv_ addr中的sa_ family 设置为AF UNSPEC可以取消绑定。
addrlen为参数serv. addr的长度。
执行成功返回0,有错误发生则返回-1,错误代码存入errmo 中。

绑定套接字

函数bind用来将一个套接字和某个端口绑定在一起,函数原型为:

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

socket函数只是创建一个套接字,这个套接字将工作在那个端口上,程序并没有指定。前面提到,在客户机服务器模型中,服务器端的IP地址和端口号一般是固定的,因此在服务器端的程序中,使用bind函数将一个套接字和某个端口绑定在一起。该函数一般只有服务器端的程序调用。参数my_addr 指定了sockfd将绑定到的本地地址,可以将参数my_addr 的sin_addr设置为INADDR_ANY而不是某个确定的IP地址就可以绑定到任何网络接口。对于只有一个IP地址的计算机,INADDR_ANY对应的就是它的IP地址:对于多宿主主机(拥有多块网卡), INADDR ANY表示本服务器程序将处理来自所有网络接口上相应端口的连接请求。
函数执行成功返回0,当有错误发生时则返回-1,错误代码存入errmo中。

在套接字上监听

函数listen把套接字转换为被动监听, 函数原型为:

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

由函数socket创建的套接字是主动套接字,这种套接字可以用来主动请求连接到某个服务器(通过函数conet)但是作为服务器端的程序,通常在某个端口上监听等待来自客户端的连接请求。在服务器端,般是先调用函数 socket创建一个主动套接字, 然后调用函数bind将该套接字绑定到某个端口上,接着再调用函数listen将该套接字转化为监听套接字,等待来自于客户端的连接请求。一般多 个客户端连接到一个服务器, 服务器向这些客户端提供某种服务。服务器端设置一个连接队列,记录已经建立的连接,参数backlog指定了该连接队列的最大长度。如果连接队列已经达到最大,之后的连接请求将被服务器拒绝。
执行成功返回0,当有错误发生时则返回-1,错误代码存入ermo中。
注意:函数listen只是将套接字设置为监听模式以等待连接请求,它并不能接收连接请求,真正接收客户端连接请求的是后面介绍的accept函数。

接受连接

函数accept用来接受一个连接,函数原型为:

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

参数s是由函数socket创建,经函数bind绑定到本地某一端口上,然后通过函数listen转化而来的监听套接字。

  • 参数 addr用来保存发起连接请求的主机的地址和端口。
  • 参数addrlen是addr所指向的结构体的大小。

执行成功返回一个新的代表客户端的套接字,出错则返回-1,错误代码存入errno 中。只能对面向连接的套接字使用accept函数。accept 执行成功时,将创建-一个新的套接字, 并且为这个新的套接字分配-一个套接字描述符,并返回这个新的套接字描述符。这个新的套接字描述符与打开文件时返回的文件描述符类似,进程可以利用这个新的套接字描述符与客户端交换数据,参数s所指定的套接字继续等待客户端的连接请求。

如果参数s所指定的套接字被设置为阳塞方式(Linux下的默认方式),且连接请求队列为空,则acept0将被阻塞直到有连接请求到达为止;如果参数s所指定的套接字被设置为非阳塞方式,则如果队列为空,accept 将立即返回-1, ernno 被设置为EAGAIN。

TCP套接字的数据传输

发送数据

发送数据可以使用send函数(write函数也可以发送数据),函数原型为:

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int s, const void *msg, size_t len, int flags);

函数send只能对处于连接状态的套接字使用。参数s为已建好的套接字描述符,即accept函数的返回值。参msg指向存放待发送数据的缓冲区,参数len为待发送数据的长度。
参数flags为控制选项,一般设置为0或以下值:

  • MSG_OOB:在指定的套接字上发送带外数据,该类型的套接字必须支持带外数据。
  • MSG_DONTROUTE:通过最直接的路径发送数据,而忽略下层协议的路由设置。

如果要发送的数据太长而不能发送就会出现错误,errno设置为EMSGSIZE;如果要发送的数据长度大于该套接字的缓冲区剩余空间大小时,send()一般会被阻塞,如果设置的非阻塞方式,则此时立即返回-1,并将errno设为EAGAIN。
函数执行成功返回实际发送字节数,出错则返回-1,错误代码存入errno。
注意:执行成功只说明数据写入套接字的缓冲区中,并不代表数据已经成功的通过网络发送到目的地。

接受数据

函数recv用来在TCP套接字上接收数据(read函数也可以接收数据),函数原型为:

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int s, void *buf, size_t len, int flags);

函数recv从参数s所指定的套接字描述符上接收数据并保存到参数buf所指定的缓冲区中,参数len为缓冲区长度。
参数flags为控制选项,一般设置为0或以下值:

  • MSG_OOB :请求接收带外数据;
  • MSG_PEEK:只查看数据而不读出;
  • MSG_WAITALL:只在接收缓冲区满时才返回;

如果一个数据包太长以至于缓冲不能完全放下时,剩余部分的数据将可能被丢弃。如果指定套接字上无数据到达时,recv()将被阻塞,如果该套接字被设置为非阻塞方式,则立即返回-并将errno设置为EAGAIN。函数recv接收到数据就返回了,并不会等待接收到参数len指定的长度数据才返回。
执行成功返回接收的字节数,出错返回-1,错误代码存入errno。

关闭套接字

函数close

函数close用来关闭一个套接字描述符,它与关闭文件描述符类似。函数原型为:

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

参数fd为一个套接字描述符。该函数关闭一个套接字
执行成功返回0,出错则返回-1,错误代码存入errno中。

函数shutdown

函数shutdown也用于关闭一个套接字描述符,函数原型为:

#include <sys/socket.h>
int shutdown(int s, int how);

函数shutdowm功能与函数close类似,但是shutdown()功能更加强大,可以对套接字的关闭进行一些更细致的控制,它允许对套接字进行单向关闭或全面禁止。参数s为待关闭套接字的描述符参数how指定了关闭的方式,具体取值如下。

  • SHUT_RD:将连接上的读通道关闭,此后进程将不能再接收任何数据,接收缓冲区中还未被读取的数据也将被丢失,但仍然可以在套接字上发送数据。
  • SHUT_WR:将连接上的写通道关闭,此后进程将不能再发送任何数据,发送缓冲区中还为被发送的数据也将被丢弃,但仍然可以在该套接字上接收数据。
  • SHUT_RFWR:将读写通道都将被关闭。

执行成功后返回0, 出错则返回-1,错误码存入errno中。

简单应用

在这里插入图片描述

文件 my_client.c my_recv.c my_server.c my_recv.h四个文件

文件my_client.c:

/******************************************************
 * 先运行my_server.c服务器程序,等待client发送请求
 * 再运行my_client.c客户端程序
 * 本机地址127.0.0.1
 * 本机IP地址192.168.**.***
 * ***************************************************/
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "my_recv.h"

#define INVALID_USERINFO           'n'     //用户信息无效
#define VALID_USERINFO             'y'     //用户信息有效

//获取用户输入存入到buf,buf的长度为len,用户输入数据以'\n'为结束标志
int get_userinfo(char *buf, int len)
{
    int i;
    int c;

    if(buf == NULL)
    {
        return -1;
    }

    i = 0;
    while(((c = getchar()) != '\n') && (c != EOF) && (i < len-2))
    {
        buf[i++] = c;
    }
    buf[i++] = '\n';
    buf[i++] = '\0';

    return 0;
}

//输入用户名,然后通过fd发送出去
void input_userinfo(int conn_fd, const char *string)
{
    char     input_buf[32];
    char     recv_buf[BUFSIZE];
    int      flag_userinfo;

    //输入用户信息直到正确为止
    do{
        printf("%s:",string);
        if(get_userinfo(input_buf, 32) < 0)
        {
            printf("error return from get_userinfo\n");
            exit(1);
        }
        if(send(conn_fd, input_buf, strlen(input_buf), 0) < 0)           //向服务器端发送信息
        {
            my_err("send", __LINE__);
        }
        //从连接套接字上读取一次数据
        if(my_recv(conn_fd, recv_buf, sizeof(recv_buf)) < 0)             //读取服务器端发来的信息
        {
            printf("date is too long\n");
            exit(1);
        }
        if(recv_buf[0] == VALID_USERINFO)
        {
            flag_userinfo = VALID_USERINFO;
        }
        else
        {
            printf("%s error, input again,",string);
            flag_userinfo = INVALID_USERINFO;
        }
    }while(flag_userinfo == INVALID_USERINFO);
}

int main(int argc, char **argv)
{
    int                i;
    int                ret;
    int                conn_fd;
    int                serv_port;
    struct sockaddr_in serv_addr;
    char               recv_buf[BUFSIZ];
    //检查参数个数
    if(argc != 5)
    {
        printf("Usage: [-p] [serv_port] [-a] [serv_address]\n");
        exit(1);
    }
    
    
    //初始化服务器端地址结构
    memset(&serv_addr, 0, sizeof(struct sockaddr_in));
    serv_addr.sin_family = AF_INET;
    //从命令行获取服务器端的端口与地址
    for ( i = 0; i < argc; i++)
    {
        if(strcmp("-p", argv[i]) == 0)
        {
            serv_port = atoi(argv[i+1]);
            if(serv_port < 0 || serv_port > 65535)
            {
                printf("invalid serv_addr.sin_port\n");
                exit(1);
            }
            else
            {
                serv_addr.sin_port = htons(serv_port);
            }
            continue;
        }
        if(strcmp("-a", argv[i]) == 0)
        {
            if(inet_aton(argv[i+1], &serv_addr.sin_addr) == 0)
            {
                printf("invalid server ip address\n");
                exit(1);
            }
            continue;
        }
    }
    //检测是否少输入了某项参数
    if(serv_addr.sin_port == 0 || serv_addr.sin_addr.s_addr == 0)
    {
        printf("Usage: [-p] [serv_addr.sin_port] [-a][serv_address]\n");
        exit(1);
    }
    //创建一个TCP套接字
    conn_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(conn_fd < 0)
    {
        my_err("socket", __LINE__);
    }
    //向服务器端发送连接请求
    if(connect(conn_fd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr)) < 0)
    {
        my_err("connect", __LINE__);
    }
    //输入用户名和密码
    input_userinfo(conn_fd,"username");
    input_userinfo(conn_fd, "password");

    //读取欢迎信息并打印出来
    if((ret = my_recv(conn_fd, recv_buf, sizeof(recv_buf))) < 0)
    {
        printf("date is too long\n");
        exit(1);
    }
    for(i = 0; i< ret; i++)
    {
        printf("%c",recv_buf[i]);
    }
    printf("\n");

    close(conn_fd);
    
    return 0;
}

文件my_recv.c

/*********************************
 * 该程序中的函数在库my_recv.h中
 * ******************************/
#define MY_RECV_C

#include <sys/socket.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include "my_recv.h"

#define BUFSIZE    1024

//自定义的错误处理函数
void my_err(const char *err_string, int line)
{
    fprintf(stderr, "line:%d", line);
    perror(err_string);
    exit(1);
}

/*********************************
 * 从套接字上读取一次数据(以\n结束)
 * 从conn_fd连接套接字上接收数据 
 * 读取到的数据保存到date_buf缓冲中
 * len为date_buf所指的空间长度
 * 错误返回-1,服务器已关闭连接返回0,成功返回读取的字节数
 * *****************************/
int my_recv(int conn_fd, char *date_buf, int len)
{
    static char recv_buf[BUFSIZE];           //自定义缓冲区,//BUFSIZE定义在my_recv.h中
    static char *pread;                      //指向下一次读取数据位置
    static int  len_remain = 0;              //自定义缓冲区中剩余字节数
    int         i;

    //如果缓冲区没有数据,则从套接字读取数据
    if(len_remain <= 0)
    {
        if((len_remain = recv(conn_fd, recv_buf, sizeof(recv_buf), 0)) < 0)
        {
            my_err("recv", __LINE__);
        }
        else if(len_remain == 0)         //目的计算机端的socket关闭
        {
            return 0;
        }
        pread = recv_buf;          //重新初始化pread指针
    }

    //从自定义缓冲区读取一次数据
    for(i = 0; *pread != '\n'; i++)
    {
        if(i > len)         //防止指针越界
        {
            return -1;
        }
        date_buf[i] = *pread++;
        len_remain--;
    }
    //去除结束标志
    len_remain--;
    pread++;

    return i;       //读取成功
}

文件my_recv.h

#ifndef    __MY_RECV_H
#define    __MY_RECV_H
    #define   BUFSIZE 1024
    void my_err(const char *err_string,int line);
    int my_recv(int conn_fd, char *date_buf, int line);
#endif

文件my_server.c

/******************************************************
 * 先运行my_server.c服务器程序,等待client发送请求
 * 再运行my_client.c客户端程序
 * 本机地址127.0.0.1
 * 本机IP地址192.168.**.***
 * ***************************************************/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include "my_recv.h"

#define SERV_PORT         4507        //服务器端口
#define LISTENQ           12          //连接请求队列的最大长度

#define INVALID_USERINFO  'n'         //用户信息无效
#define VALID_USERINFO    'y'         //用户信息有效

#define USERNAME          0           //接收到的是用户名
#define PASSWORD          1           //接收到的是密码

struct userinfo{                      //保存用户名和密码的结构体
    char username[32];
    char password[32];
};

struct userinfo users[] = {
    {"linux","unix"},
    {"4507","4508"},
    {"clh","clh"},
    {"xl","xl"},
    {" "," "}           //以只含一个空格的字符串作为数组的结束标志
};

//查找用户名是否存在
int find_name(const char *name)
{
    int i;

    if(name == NULL)
    {
        printf("in find_name, NULL pointer");
        return -2;
    }

    for(i = 0; users[i].username[0] != ' '; i++)
    {
        if(strcmp(users[i].username, name) == 0)
        {
            return i;
        }
    }

    return -1;
}

//发送数据
void send_date(int con_fd, const char *string)
{
    if(send(con_fd, string, strlen(string), 0) < 0)
    {
        my_err("send", __LINE__);
    }
}

int main()
{
    int                   sock_fd, conn_fd;
    int                   optval;
    int                   flag_recv = USERNAME;
    int                   ret;
    int                   name_num;
    pid_t                 pid;
    socklen_t             cli_len;
    struct sockaddr_in    cli_addr,serv_addr;
    char                  recv_buf[128];

    //创建一个TCP套接字
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(sock_fd < 0)
    {
        my_err("socket", __LINE__);
    }

    //设置该套接字使之可以重新绑定端口
    optval = 1;
    if(setsockopt(sock_fd,SOL_SOCKET, SO_REUSEADDR,(void *)&optval,sizeof(int)) < 0)
    {
        my_err("setsockopt", __LINE__);
    }

    //初始化服务器端地址结构
    memset(&serv_addr,0,sizeof(struct sockaddr_in));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port   = htons(SERV_PORT);
    serv_addr.sin_addr.s_addr  = htons(INADDR_ANY);

    //将套接字绑定到本地端口
    if(bind(sock_fd,(struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in)) < 0)
    {
        my_err("bind", __LINE__);
    }

    //把套接字转化为监听套接字
    if(listen(sock_fd, LISTENQ) < 0)
    {
        my_err("listen", __LINE__);
    }

    cli_len = sizeof(struct sockaddr_in);
    while(1)
    {
        //通过accept接收客户端的连接请求,并返回连接套接字用于收发数据
        conn_fd = accept(sock_fd,(struct sockaddr *)&cli_addr,&cli_len);
        if(conn_fd < 0)
        {
            my_err("accept", __LINE__);
        }

        printf("accept a new client, ip: %d\n",inet_ntoa(cli_addr.sin_addr));
        //创建一个子进程处理刚刚接收的连接请求
        if((pid = fork()) == 0)
        {
            while(1)
            {
                //接收数据到buf
                if((ret = recv(conn_fd, recv_buf, sizeof(recv_buf), 0)) < 0)
                {
                    perror("recv");
                    exit(1);
                }
                recv_buf[ret-1] = '\0';     //将数据结束标志'\n'替换成字符串结束符

                if(flag_recv == USERNAME)
                {
                    name_num = find_name(recv_buf);
                    switch(name_num)
                    {
                        case -1:
                            send_date(conn_fd, "n\n");     //无效
                            break;
                        case -2:
                            exit(1);
                            break;
                        default:
                            send_date(conn_fd, "y\n");     //有效
                            flag_recv = PASSWORD;
                            break;
                    }
                }
                else if(flag_recv == PASSWORD)
                {
                    if(strcmp(users[name_num].password, recv_buf) == 0)
                    {
                        send_date(conn_fd, "y\n");
                        send_date(conn_fd, "Welcome login my tcp server\n");
                        printf("%s login\n", users[name_num].username);
                        break;
                    }
                    else
                    {
                        send_date(conn_fd, "n\n");     //无效
                    }
                }
            }

            close(sock_fd);
            close(conn_fd);
            exit(0);  //结束子进程
        }
        else
        {        //父进程关闭刚刚接收到的请求,执行accept等待其他连接请求
            close(conn_fd);
        }
    }

    return 0;
}

运行结果:
服务器:
gcc my_server.c my_recv.c
./a.out

客户端:
gcc my_client.c my_recv.c
./a.out -p 4507 -a 127.0.0.1

数据:

usernamepassword
linuxunix
45074508
clhclh
  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-07-30 13:06:15  更:2021-07-30 13:08:17 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 18:30:39-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码