zoukankan      html  css  js  c++  java
  • socket网络编程快速上手(二)——细节问题(5)(完结篇)

    6.Connect的使用方式

      前面提到,connect发生EINTR错误时,是不能重新启动的。那怎么办呢,是关闭套接字还是直接退出进程呢?如果EINTR前,三次握手已经发起,我们当然希望链路就此已经建立完成,不要再重新走流程了。这个时候我们就需要为connect量身定做一个使用方案。代码如下:

     1 STATUS connectWithTimeout(int sock, struct sockaddr*addrs, int adrsLen,struct timeval* tm)
     2 {
     3     int err = 0;
     4     int len = sizeof(int);
     5     int flag;
     6     int ret;
     7     fd_set set;
     8     struct timeval mytm;
     9     
    10     if(tm!=NULL){
    11         memcpy(&mytm, tm, sizeof(struct timeval));
    12     }
    13  
    14     flag = 1;
    15     ioctl(sock,FIONBIO,&flag);
    16 
    17     ret = connect(sock, addrs, adrsLen);
    18     if ( ret == -1 )
    19     {
    20         if ( EINPROGRESS == errno )
    21         {
    22             FD_ZERO(&set);
    23             FD_SET(sock,&set);
    24             if ( select(sock+1,NULL,&set,NULL,tm) > 0 )
    25             {
    26                 getsockopt(sock,SOL_SOCKET,SO_ERROR,&err,(socklen_t*)&len);
    27                 if ( 0 == err )
    28                     ret = OK;
    29                 else
    30                     ret = ERROR;
    31             }
    32             else
    33             {
    34                 ret = ERROR;
    35             }
    36         }
    37     }
    38 
    39     flag = 0;
    40     ioctl(sock,FIONBIO,&flag);
    41 
    42     if(tm!=NULL){
    43         memcpy(tm, &mytm, sizeof(struct timeval));
    44     }
    45 
    46     return ret;
    47 }
    connectWithTimeout

      这部分代码是从原有工程里抠出来的,在学习完前面知识后,我觉得有些地方不是很完善,按照下面的代码做了修改。此处将老代码贴出只是为了防止自己的理解有误,做了画蛇添足的事情。新代码如下:

     1 STATUS connectWithTimeout(int sock, struct sockaddr* addrs, int adrsLen,struct timeval* tm)
     2 {
     3     int err = 0;
     4     int len = sizeof(int);
     5     int flag;
     6     int ret = -1;
     7     int retselect = -1;
     8     fd_set set;
     9     struct timeval mytm;
    10     
    11     if (tm != NULL){
    12         memcpy(&mytm, tm, sizeof(struct timeval));
    13     }
    14  
    15     flag = 1;
    16     ioctl(sock,FIONBIO,&flag);
    17 
    18     ret = connect(sock, addrs, adrsLen);
    19     if (-1 == ret)
    20     {
    21         if (EINPROGRESS == errno)
    22         {
    23 reselect:
    24             FD_ZERO(&set);
    25             FD_SET(sock,&set);
    26             if ((retselect = select(sock+1, NULL, &set, NULL, tm)) > 0)
    27             {
    28                 if (FD_ISSET(sock, &set))
    29                 {
    30                     getsockopt(sock, SOL_SOCKET, SO_ERROR, &err, (socklen_t*)&len);
    31                     if (0 == err)
    32                         ret = 0;
    33                     else
    34                         ret = -1;
    35                 }
    36                     
    37             }
    38             else if (retselect < 0)
    39             {
    40                 if (EINTR == errno)
    41                 {
    42                     printf("error! errno = %s:%d
    ", strerror(errno), errno);
    43                     goto reselect;
    44                 }
    45             }
    46         }
    47     }
    48     else if (0 == ret)
    49     {
    50         ret = 0;  //OK
    51     }
    52 
    53     flag = 0;
    54     ioctl(sock, FIONBIO, &flag);
    55 
    56     if (tm != NULL){
    57         memcpy(tm, &mytm, sizeof(struct timeval));
    58     }
    59 
    60     return ret;
    61 }
    connectWithTimeoutNew.c

      这是一个非阻塞式connect的模型,更为详细的介绍可以看《UNIX网络编程》(我真不是个做广告的)。但此处模型个人认为逻辑上还是完善的。

    15-16行:设置套接口为非阻塞方式,这个步骤需要视使用环境而定,因此这里存在一个移植性问题。

    18-21行:非阻塞模式下的connect,调用后立即返回,如果已经errno为EINPROGRESS,说明已经发起了三次握手。否则为异常。

    23-45行:使用select去监测套接口状态。实现规定(那些乱七八糟的不同实现,我一直搞不清楚,哎,接触的不多!大家可以自己去查查):(1)当连接建立成功时,描述字变为可写,(2)当连接建立遇到错误时,描述字变为既可读又可写。我是个比较投机的人,归纳一下,描述字变为可写时说明连接有结果了。30行使用getsockopt获取描述字状态(使用SO_ERROR选项)。建立成功err为0,否则为-1。对于一个商业软件的“贡献者”,我们不能放过任何出错处理,但这里不对getsockopt的返回值进行出错处理是有原因的。在异常情况下,有些实现该调用返回0,有些则返回-1,因此我们不能根据它的返回值来判断连接是否异常。38-45行,前面已经提到,select这个阻塞的家伙很有可能发生EINTR错误,我们也必须兼容这种错误。注意reselect的位置,自己使用时位置放错了效果可是截然不同的。

    48-51行:connect有时反应很快啊,一经调用就成功建立连接了,这种情况是存在的。所以我们也要兼容这种情况。

    后面行:恢复调用前状态。

      又到了热血澎湃的总结时刻:以前,看到有些地方使用非阻塞的connect,真的很费解,为什么简单的connect不直接使用,还要搞那么多花样?现在其实我们应该可以想明白了,作为一个完美程序的追求者,着眼于代码长期地维护(自己太高尚了),阻塞的connect确实会存在很多的问题。那是否存在一个稳健的阻塞的connect版本,说实话,我没仔细想过,粗略地想想那应该是个很复杂的东西,比如:connect返回时是否已经开始三次握手等等?因此,阻塞的或者非阻塞的connect目前并非再是二者选其一、根据喜好选择使用的问题了,而是必须使用非阻塞的connect。这个结论或许很武断,但本人通过目前了解的知识得出了这样的结论,还是希望有大神突然出现,指点一二。

    7.Accept返回前连接夭折

      这是《UNIX网络编程》上的原装问题,新发现的,按照书上的说法,貌似很严重,我要先测试一下。在我给出一个比较稳健的程序之前不允许存在已知的类似问题。又要牵涉到SO_LINGER选项了,真是有点无法自圆自说的感觉。就当初探SO_LINGER选项吧,现在它只是配角,后面有机会详细研究一下它。

      服务器代码,这个代码同样可以用来测试select发生的EINTR的问题:

      1 #include <stdio.h>
      2 #include <stdlib.h>
      3 #include <string.h>
      4 #include <unistd.h>
      5 #include <sys/types.h>
      6 #include <sys/socket.h>
      7 #include <netinet/in.h>
      8 #include <arpa/inet.h>
      9 #include <signal.h>
     10 #include <errno.h>
     11 
     12 #define  PORT         1234
     13 #define  BACKLOG      5
     14 #define  MAXDATASIZE  1000
     15 
     16 void sig_chld(int signo)
     17 {
     18    pid_t pid;
     19    int stat = 0;
     20 
     21    pid = wait(&stat);
     22    printf("child %d terminated(stat:%d)
    ", pid, stat);
     23    
     24    return;
     25 }
     26 
     27 void signal_ex(int signo, void* func)
     28 {
     29     struct sigaction act, oact;
     30     
     31     act.sa_handler = func;
     32     sigemptyset(&act.sa_mask); //清空此信号集
     33     act.sa_flags = 0;
     34     
     35     if (sigaction(signo, &act, &oact) < 0)
     36     {
     37         printf("sig err!
    ");
     38     }
     39 
     40     //sigaction(SIGINT, &oact, NULL); //恢复成原始状态
     41     return;
     42 }
     43 
     44 int main()
     45 {
     46     int  listenfd, connectfd;
     47     struct  sockaddr_in server;
     48     struct  sockaddr_in client;
     49     socklen_t  addrlen;
     50     char    szbuf[MAXDATASIZE + 1] = {0};
     51     int     num = 0;
     52     pid_t   pid_child;
     53     int ret;
     54     fd_set set;
     55     struct timeval mytm;
     56     
     57     if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
     58     {
     59         perror("Creating  socket failed.");
     60         exit(1);
     61     }
     62     
     63     int opt = SO_REUSEADDR;
     64     setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
     65     
     66     bzero(&server, sizeof(server));
     67     server.sin_family = AF_INET;
     68     server.sin_port = htons(PORT);
     69     server.sin_addr.s_addr = htonl(INADDR_ANY);
     70     if (bind(listenfd, (struct sockaddr *)&server, sizeof(server)) == -1) 
     71     {
     72         perror("Bind()error.");
     73         exit(1);
     74     }   
     75     if (listen(listenfd, BACKLOG) == -1)
     76     {
     77         perror("listen()error
    ");
     78         exit(1);
     79     }
     80 
     81     signal_ex(SIGCHLD, sig_chld);
     82     while (1)
     83     {
     84         addrlen = sizeof(client);
     85         sleep(10);
     86         printf("start accept!
    ");
     87         if ((connectfd = accept(listenfd, (struct sockaddr*)&client, &addrlen)) == -1) 
     88         {
     89             #if 1
     90             if (EINTR == errno)
     91             {
     92                 printf("EINTR!
    ");
     93                 continue;
     94             }
     95             #endif
     96             
     97             perror("accept()error
    ");
     98             exit(1);
     99         }
    100         printf("You got a connection from cient's ip is %s, prot is %d
    ", inet_ntoa(client.sin_addr), htons(client.sin_port));
    101 
    102         if (0 == (pid_child = fork()))
    103         {
    104             close(connectfd);
    105             close(listenfd);
    106             printf("child a ha!
    ");
    107             sleep(5);
    108             exit(0);
    109         }
    110 
    111         mytm.tv_sec  = 15;
    112         mytm.tv_usec = 0;
    113 reselect:
    114         FD_ZERO(&set);
    115         FD_SET(connectfd, &set);
    116         if ((ret = select(connectfd + 1, &set, NULL, NULL, &mytm)) > 0)
    117         {
    118             if(FD_ISSET(connectfd, &set))
    119             {
    120                 printf("connectfd can be readn!
    ");
    121             }
    122         }
    123         else if (0 == ret)
    124         {
    125             printf("timeout!
    ");
    126         }
    127         else if (ret < 0)
    128         {
    129             //perror("error! ");
    130             if (EINTR == errno)
    131             {
    132                 printf("error! errno = %s:%d
    ", strerror(errno), errno);
    133                 goto reselect;
    134             }
    135         }
    136 
    137         close(connectfd);
    138         connectfd = -1;
    139     }
    140     
    141     close(listenfd);
    142     
    143     return 0;
    144 }
    server_select.c

       客户端代码:

     1 #include <sys/types.h>
     2 #include <sys/socket.h>
     3 #include <netinet/in.h>
     4 #include <netdb.h>
     5 #include <signal.h>
     6 
     7 #define  PORT        1234
     8 #define  MAXDATASIZE 1000
     9 
    10 int main(int argc, char *argv[])
    11 {
    12     int  sockfd = -1;
    13     struct sockaddr_in server;
    14     struct linger ling;
    15     
    16     if (argc != 2) 
    17     {
    18         printf("Usage:%s <IP Address>
    ", argv[0]);
    19         exit(1);
    20     }
    21 
    22     signal(SIGPIPE, SIG_IGN);
    23     
    24     if ((sockfd=socket(AF_INET, SOCK_STREAM, 0)) == -1)
    25     {
    26         printf("socket()error
    ");
    27         exit(1);
    28     }
    29     bzero(&server, sizeof(server));
    30     server.sin_family = AF_INET;
    31     server.sin_port = htons(PORT);
    32     server.sin_addr.s_addr = inet_addr(argv[1]);
    33     if (connect(sockfd, (struct sockaddr *)&server, sizeof(server)) == -1)
    34     {
    35         printf("connect()error
    ");
    36         exit(1);
    37     }
    38 
    39     ling.l_onoff = 1;
    40     ling.l_linger = 0;
    41     setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
    42     
    43     close(sockfd);
    44     
    45     return 0;
    46 }
    client_select.c

      书上提到,有些实现内核会抛一个错误给应用进程,应用进程在accept时会返回错误。而有些实现内核完全能够兼容这种问题,不对应用进程产生任何影响。强大的linux显然是后者,客户端发送RST后,服务器accept没有返回任何错误,运行正常。

      因此,该问题暂且略过,实际使用时还是应当注意这种极端的情况。

    8.最终的代码

       写到这,赶紧要为这个标题画句号了。一边写一边学,再这么下去就成无底洞了。而UDP的相关知识自己本身用得不多,在这不敢下笔,等哪一天心中有物再来详细整理一下。因此,这边要很不负责任地给出一个最终版本的TCP代码了。

      个人认为,网络编程还有很多知识,但是该代码已经可以应付一个新手解决很多实际的问题了。之所以叫“快速上手”,干得也就是这种事,离精通还是很远的。之后,有什么问题再以专题的形式给出吧。

      先是服务器代码:

      1 #include <stdio.h>
      2 #include <stdlib.h>
      3 #include <string.h>
      4 #include <unistd.h>
      5 #include <sys/types.h>
      6 #include <sys/socket.h>
      7 #include <netinet/in.h>
      8 #include <arpa/inet.h>
      9 #include <signal.h>
     10 #include <errno.h>
     11 
     12 #define  PORT         1234
     13 #define  BACKLOG      5
     14 #define  MAXDATASIZE  1000
     15 #define  TEST_STRING  "HELLO, WORLD!"
     16 #define  TEST_STRING_LEN strlen(TEST_STRING)
     17 
     18 int readn(int connfd, void *vptr, int n)
     19 {
     20     int    nleft;
     21     int    nread;
     22     char *ptr;
     23     int ret = -1;
     24     struct timeval     select_timeout;
     25     fd_set rset;
     26 
     27     ptr = (char*)vptr;
     28     nleft = n;
     29 
     30     while (nleft > 0)
     31     {
     32         FD_ZERO(&rset);
     33         FD_SET(connfd, &rset);
     34         select_timeout.tv_sec = 5;
     35         select_timeout.tv_usec = 0;
     36         if ((ret = select(connfd+1, &rset, NULL, NULL, &select_timeout)) < 0)
     37         {
     38             if (errno == EINTR)
     39             {
     40                 continue;
     41             }
     42             else
     43             {
     44                 return -1;
     45             }
     46         }
     47         else if (0 == ret)
     48         {
     49             return -1;
     50         }
     51         if ((nread = recv(connfd, ptr, nleft, 0)) < 0)
     52         {
     53             if(errno == EINTR)
     54             {
     55                 nread = 0;
     56             }
     57             else
     58             {
     59                 return -1;
     60             }
     61         }
     62         else if (nread == 0)
     63         {
     64             break;
     65         }
     66         nleft -= nread;
     67         ptr   += nread;
     68     }
     69     
     70     return(n - nleft);
     71 }
     72 
     73 void sig_chld(int signo)
     74 {
     75    pid_t pid;
     76    int stat = 0;
     77 
     78    pid = wait(&stat);
     79    
     80    return;
     81 }
     82 
     83 void signal_ex(int signo, void* func)
     84 {
     85     struct sigaction act, oact;
     86     
     87     act.sa_handler = func;
     88     sigemptyset(&act.sa_mask); //清空此信号集
     89     act.sa_flags = 0;
     90     
     91     if (sigaction(signo, &act, &oact) < 0)
     92     {
     93         printf("sig err!
    ");
     94     }
     95 
     96     //sigaction(SIGINT, &oact, NULL); //恢复成原始状态
     97     return;
     98 }
     99 
    100 int main()
    101 {
    102     int  listenfd, connectfd;
    103     struct  sockaddr_in server;
    104     struct  sockaddr_in client;
    105     socklen_t  addrlen;
    106     char    szbuf[MAXDATASIZE + 1] = {0};
    107     int     num = 0;
    108     pid_t   pid_child;
    109 
    110     signal(SIGPIPE, SIG_IGN);
    111     
    112     if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    113     {
    114         perror("Creating  socket failed.");
    115         exit(1);
    116     }
    117     
    118     int opt = SO_REUSEADDR;
    119     setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    120     
    121     bzero(&server, sizeof(server));
    122     server.sin_family = AF_INET;
    123     server.sin_port = htons(PORT);
    124     server.sin_addr.s_addr = htonl(INADDR_ANY);
    125     if (bind(listenfd, (struct sockaddr *)&server, sizeof(server)) == -1) 
    126     {
    127         perror("Bind()error.");
    128         exit(1);
    129     }   
    130     if (listen(listenfd, BACKLOG) == -1)
    131     {
    132         perror("listen()error
    ");
    133         exit(1);
    134     }
    135 
    136     signal_ex(SIGCHLD, sig_chld);
    137     while (1)
    138     {
    139         addrlen = sizeof(client);
    140         printf("start accept!
    ");
    141         if ((connectfd = accept(listenfd, (struct sockaddr*)&client, &addrlen)) == -1) 
    142         {
    143             if (EINTR == errno)
    144             {
    145                 printf("EINTR!
    ");
    146                 continue;
    147             }
    148             
    149             perror("accept()error
    ");
    150             exit(1);
    151         }
    152         printf("You got a connection from cient's ip is %s, prot is %d
    ", inet_ntoa(client.sin_addr), htons(client.sin_port));
    153 
    154         if (0 == (pid_child = fork()))
    155         {
    156             while (1)
    157             {
    158                 num = readn(connectfd, szbuf, TEST_STRING_LEN);
    159                 if (num < 0)
    160                 {
    161                     printf("read error!
    ");
    162                     break;
    163                 }
    164                 else if (0 == num)
    165                 {
    166                     printf("read over!
    ");
    167                     break;
    168                 }
    169                 else
    170                 {
    171                     printf("recv: %s
    ", szbuf);
    172                 }
    173             }
    174             close(connectfd);
    175             close(listenfd);
    176             sleep(5);
    177             exit(0);
    178         }
    179         
    180         close(connectfd);
    181         connectfd = -1;
    182     }
    183     
    184     close(listenfd);
    185     
    186     return 0;
    187 }
    server_last.c

      客户端:

      1 #include <stdio.h>
      2 #include <stdlib.h>
      3 #include <unistd.h>
      4 #include <string.h>
      5 #include <sys/types.h>
      6 #include <sys/socket.h>
      7 #include <netinet/in.h>
      8 #include <netdb.h>
      9 #include <signal.h>
     10 #include <errno.h>
     11 #include <fcntl.h>
     12 
     13 #define  PORT        1234
     14 #define  MAXDATASIZE 1000
     15 #define  TEST_STRING  "HELLO, WORLD!"
     16 #define  TEST_STRING_LEN strlen(TEST_STRING)
     17 
     18 int writen(int connfd, void *vptr, size_t n)
     19 {
     20     int nleft, nwritten;
     21      char    *ptr;
     22 
     23     ptr = (char*)vptr;
     24     nleft = n;
     25 
     26     while (nleft > 0)
     27     {
     28         if ((nwritten = send(connfd, ptr, nleft, MSG_NOSIGNAL)) == -1)
     29         {
     30             if (errno == EINTR)
     31             {
     32                 nwritten = 0;
     33             }
     34             else 
     35             {
     36                 return -1;
     37             }
     38         }
     39         nleft -= nwritten;
     40         ptr   += nwritten;
     41     }
     42 
     43     return(n);
     44 }
     45 
     46 int connectWithTimeout(int sock, struct sockaddr* addrs, int adrsLen,struct timeval* tm)
     47 {
     48     int err = 0;
     49     int len = sizeof(int);
     50     int flag;
     51     int ret = -1;
     52     int retselect = -1;
     53     fd_set set;
     54     struct timeval mytm;
     55     
     56     if (tm != NULL){
     57         memcpy(&mytm, tm, sizeof(struct timeval));
     58     }
     59 
     60      flag = fcntl(sock, F_GETFL, 0);
     61     fcntl(sock, F_SETFL, flag | O_NONBLOCK);  //linux用这个
     62   //  flag = 1;
     63   //  ioctl(sock,FIONBIO,&flag);
     64 
     65     ret = connect(sock, addrs, adrsLen);
     66     if (-1 == ret)
     67     {
     68         if (EINPROGRESS == errno)
     69         {
     70 reselect:
     71             printf("start check!
    ");
     72             FD_ZERO(&set);
     73             FD_SET(sock,&set);
     74             if ((retselect = select(sock+1, NULL, &set, NULL, tm)) > 0)
     75             {
     76                 if (FD_ISSET(sock, &set))
     77                 {
     78                     getsockopt(sock, SOL_SOCKET, SO_ERROR, &err, (socklen_t*)&len);
     79                     if (0 == err)
     80                         ret = 0;
     81                     else
     82                         ret = -1;
     83                 }
     84                     
     85             }
     86             else if (retselect < 0)
     87             {
     88                 if (EINTR == errno)
     89                 {
     90                     printf("error! errno = %s:%d
    ", strerror(errno), errno);
     91                     goto reselect;
     92                 }
     93             }
     94         }
     95     }
     96     else if (0 == ret)
     97     {
     98         printf("OK at right!
    ");
     99         ret = 0;  //OK
    100     }
    101 
    102     fcntl(sock, F_SETFL, flag);
    103   //  flag = 0;
    104   //  ioctl(sock, FIONBIO, &flag);
    105 
    106     if (tm != NULL){
    107         memcpy(tm, &mytm, sizeof(struct timeval));
    108     }
    109 
    110     return ret;
    111 }
    112 
    113 int main(int argc, char *argv[])
    114 {
    115     int  sockfd, num;
    116     char  szbuf[MAXDATASIZE] = {0};
    117     struct sockaddr_in server;
    118     struct timeval timeOut;
    119     int  ret = -1;
    120     int  iSendTime = 0;
    121     
    122     if (argc != 2) 
    123     {
    124         printf("Usage:%s <IP Address>
    ", argv[0]);
    125         exit(1);
    126     }
    127 
    128     signal(SIGPIPE, SIG_IGN);
    129     
    130     if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    131     {
    132         printf("socket()error
    ");
    133         exit(1);
    134     }
    135     
    136     bzero(&server, sizeof(server));
    137     server.sin_family = AF_INET;
    138     server.sin_port = htons(PORT);
    139     server.sin_addr.s_addr = inet_addr(argv[1]);
    140 
    141     timeOut.tv_sec  = 5;
    142     timeOut.tv_usec = 0;
    143     ret = connectWithTimeout(sockfd, (struct sockaddr *)&server, sizeof(server), &timeOut);
    144     if (-1 == ret)
    145     {
    146         printf("connect()error
    ");
    147         exit(1);
    148     }
    149 
    150     memset(szbuf, 0, sizeof(szbuf));
    151     strcpy(szbuf, TEST_STRING);
    152     
    153     while (iSendTime < 5)
    154     {
    155         ret = writen(sockfd, szbuf, TEST_STRING_LEN);
    156         if (TEST_STRING_LEN != ret)
    157         {
    158             break;
    159         }
    160         else
    161         {
    162             printf("%dth send success!
    ", iSendTime);
    163             iSendTime++;
    164         }
    165     }
    166     
    167     close(sockfd);
    168     
    169     return 0;
    170 }
    client_last.c

      总结的时候,有很多知识都得到了更新,最终代码对部分函数进行了完善。当然还有一些东西没有考虑进去,比如说对端掉电、路由器损坏等问题,以上程序都无法很好的适应这些问题。后续再慢慢改进吧。

      最终的例子功能很简单,最主要还是在socket编程的各个细节的处理上。

      在此要说OVER了!

  • 相关阅读:
    openCV使用
    Object-C知识点 (二) 控件的实用属性
    Linux 配置JDK + MyEclipse
    虚拟机+linux+大杂烩
    THREE.js代码备份——webgl
    THREE.js代码备份——webgl
    THREE.js代码备份——canvas_ascii_effect(以AscII码显示图形)
    THREE.js代码备份——canvas
    THREE.js代码备份——canvas_lines(随机点、画线)
    THREE.js代码备份——线框cube、按键、鼠标控制
  • 原文地址:https://www.cnblogs.com/wxyy/p/3328101.html
Copyright © 2011-2022 走看看