zoukankan      html  css  js  c++  java
  • HTTP服务器

    1、项目介绍

      HTTP协议是应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。协议的详细内容,前面一篇HTTP协议详解已经详细介绍了,这里不再赘述。

       项目总体描述:HTTP支持客户端/服务器模式,终端用户可通过浏览器或网络爬虫与服务器建立连接,所以首先需要自主实现服务器Server端,具体由头文件httpd.h、main函数文件httpd.c、模块功能函数文件httpd.c组成,主要实现客户端与服务器通过socket建立通信机制。首先由用户主动发起一个到服务器上指定端口(默认端口为80)的请求,服务器则在那个端口监听客户端发送过来的请求。服务器一行一行读取请求,通过请求信息判断用户请求资源的方法和路径,若方法和路径没有问题,则方法和路径通过CGI模式或非CGI向用户提供不同的HTML网页信息。处理完请求客户端向用户发送响应,包括状态行如:“HTTP/1.1 200 OK”、响应报头、消息正文,消息体即为服务器上的资源。

    实现功能一:静态首页展示(图片、文字文字信息);

    实现二:支持表单提交,可以借助浏览器或telnet工具使用GET、POST方法访问服务器,实现数据的简单计算功能;

    实现三:引入MYSQL,用户可通过页面表单进行数据操作,服务器拿到客户提交的数据后,会把数据存入到远端数据库,客户端也可请求查看数据库信息。

    整个项目的文件目录:

    目录:
    conf:配置文件,存放需要绑定的服务器的ip和port ;
    log:shell的日志文件以及http错误处理的日志文件 ;
    sql_client:mysql部分的API及CGI实现;
    thread_pool:线程池实现;
    wwwroot:web服务器工作的根目录,包含各种资源页面(例如默认的index.html页面,差错处理的404页面),以及执行cgi的可执行程序。下面还有一个 cgi-bin目录,是存放CGI脚本的地方。这些脚本使WWW服务器和浏览器能运行外部程序,而无需启动另一个程序。它是运行在Web服务器上的一个程序,并由来自于浏览者的输入触发。

    整个项目的框架图:

     

    2、各模块功能介绍

    头文件httpd.h,包含该项目代码所使用的全部函数的头文件以及宏定义,和函数声明;

     1 #ifndef _HTTPD_
     2 #define _HTTPD_
     3 
     4 #include <stdio.h>
     5 #include <stdlib.h>
     6 #include <sys/socket.h>
     7 #include <sys/types.h>
     8 #include <netinet/in.h>
     9 #include <arpa/inet.h>
    10 #include <fcntl.h>
    11 #include <errno.h>
    12 #include <string.h>
    13 #include <unistd.h>
    14 #include <sys/stat.h>
    15 #include <sys/wait.h>
    16 
    17 #define SUCCESS 0 
    18 #define NOTICE  1
    19 #define WARNING 2
    20 #define ERROR   3
    21 #define FATAL   4
    22 
    23 #define SIZE 1024
    24 
    25 void print_log(char *msg, int level); //打印日志
    26 int startup(const char *ip, int  port); //创建监听套接字
    27 void *handler_request(void *arg);  //处理请求
    28 
    29 #endif

    main函数文件main.c实现主要通信逻辑,通过socket建立连接的,监听和接受套接字,然后创建新线程处理请求。

     1 #include <pthread.h>
     2 #include "httpd.h"
     3 
     4 static void usage(const char *proc)
     5 {
     6     printf("Usage: %s [local_ip] [local_port]
    ", proc);
     7 }
     8 
     9 int main(int argc, char *argv[])
    10 {
    11     if(argc != 3){
    12         usage(argv[0]);
    13         return 1;
    14     }
    15 
    16     int listen_sock = startup(argv[1], atoi(argv[2]));//监听套接字
    17     //daemon(0, 0);
    18     while(1){
    19         struct sockaddr_in client;
    20         socklen_t len = sizeof(client);
    21         int new_sock = accept(listen_sock, (struct sockaddr*)&client, &len);//接收套接字
    22         if(new_sock < 0){
    23             print_log(strerror(errno), NOTICE);
    24             continue;
    25         }
    26 
    27         printf("get client [%s:%d]
    ",
    28                 inet_ntoa(client.sin_addr),
    29                 ntohs(client.sin_port)); //链接到一个客户端之后打印其IP及端口号
    30 
    31         pthread_t id;
    32         int ret = pthread_create(&id, NULL, //创建新线程
    33                 handler_request, (void *)new_sock);
    34         if(ret != 0){
    35             print_log(strerror(errno), WARNING);
    36             close(new_sock);
    37         }else{
    38             pthread_detach(id); //将子线程分离,该线程结束后会自动释放所有资源
    39         }
    40     }
    41     close(listen_sock);
    42     return 0;
    43 }

    模块功能函数在httpd.c文件

      1 #include "httpd.h"
      2 
      3 void print_log(char *msg, int level)
      4 {
      5 #ifdef _STDOUT_
      6     const char * const level_msg[]={
      7         "SUCCESS",
      8         "NOTICE",
      9         "WARNING",
     10         "ERROR",
     11         "FATAL",
     12     };
     13     printf("[%s][%s]
    ", msg, level_msg[level%5]);
     14 #endif
     15 }
     16 
     17 int startup(const char *ip, int  port)  //
     18 {
     19     int sock = socket(AF_INET, SOCK_STREAM, 0);  //创建套接字
     20     if(sock < 0){
     21         print_log(strerror(errno), FATAL); //strerror()将错误码转换为对应的错误码描述
     22         exit(2);
     23     }
     24 
     25     int opt = 1;
     26     setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));  //将该套接字设置为地址复用状态,若服务器挂掉可实现立即重启
     27 
     28     struct sockaddr_in local;
     29     local.sin_family = AF_INET;
     30     local.sin_port = htons(port);  //端口号转换
     31     local.sin_addr.s_addr = inet_addr(ip);  //ip转换
     32     if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){  //绑定
     33         print_log(strerror(errno), FATAL);
     34         exit(3);
     35     }
     36     if(listen(sock, 10) < 0){  //监听
     37         print_log(strerror(errno), FATAL);
     38         exit(4);
     39     }
     40     return sock;
     41 }
     42 
     43 //ret > 1, line != ''读成功,正常字符; ret=1&line='
    '  ret<=0&&line==''
     44 static int get_line(int sock, char line[], int size)  //得到一行请求内容
     45 {
     46     // read 1 char , one by one
     47     char c = ''; 
     48     int len = 0;
     49     while( c != '
    ' && len < size-1){  
     50         int r = recv(sock, &c, 1, 0);  
     51         if(r > 0){                      
     52            if(c == '
    '){
     53                //窥探,只把缓冲区的东西拿出来看看
     54                int ret = recv(sock, &c, 1, MSG_PEEK);
     55                if(ret > 0){
     56                    if(c == '
    '){
     57                        recv(sock, &c, 1, 0);
     58                    }else{
     59                        c = '
    '; 
     60                    }
     61                }
     62            }// 
    ->
     
     -> 
    
     63            line[len++] = c;
     64         }else{
     65             c = '
    ';
     66         }
     67     }
     68     line[len]='';
     69     return len;
     70 }
     71 //不同平台下
    、
    、
    +
    ,意义不同,这里将其统一成
    
     72 
     73 static void echo_string(int sock)
     74 {}
     75 
     76 static int echo_www(int sock, char *path, int size)
     77 {
     78     int fd = open(path, O_RDONLY);
     79     if(fd < 0){
     80         echo_string(sock);
     81         print_log(strerror(errno), FATAL);
     82         return 8;
     83     }
     84 
     85     const char *echo_line="HTTP/1.0 200 OK
    ";   //状态行
     86     send(sock, echo_line, strlen(echo_line), 0);
     87     const char *null_line="
    ";
     88     send(sock, null_line, strlen(null_line), 0); //空行
     89 
     90     if(sendfile(sock, fd, NULL, size) < 0){//在内核区实现两个文件描述符的拷贝,不用定义临时变量,省略两次数据拷贝,效率提高
     91         echo_string(sock);
     92         print_log(strerror(errno), FATAL);
     93         return 9;
     94     }
     95 
     96     close(fd);
     97     return 0;
     98 }
     99 
    100 static void drop_header(int sock)
    101 {
    102     char line[1024];
    103     int ret = -1;
    104     do{
    105         ret = get_line(sock, line, sizeof(line));
    106     }while(ret>0 && strcmp(line, "
    "));
    107 }
    108 
    109 static int exe_cgi(int sock, char *method, 
    110         char *path, char *query_string)
    111 {
    112     int content_len = -1;
    113     char method_env[SIZE/10];
    114     char query_string_env[SIZE];
    115     char content_len_env[SIZE/10];
    116 
    117     if( strcasecmp(method, "GET") == 0 ){//忽略大小写的字符比较,此处为判断请求资源的方法是否为GET方法
    118         drop_header(sock);//如果是GET方法则已从URL中知道用户请求资源所传参数
    119     }else{//POST
    120         char line[1024];
    121         int ret = -1;
    122         do{
    123             ret = get_line(sock, line, sizeof(line));
    124             if(ret > 0 &&
    125                strncasecmp(line,"Content-Length: ", 16)== 0){
    126                 content_len = atoi(&line[16]);//消息正文字长描述
    127             }
    128         }while(ret>0 && strcmp(line, "
    "));
    129         if(content_len == -1){
    130             echo_string(sock);
    131             return 10;
    132         }
    133     }
    134     const char *echo_line="HTTP/1.0 200 OK
    ";  //状态行
    135     send(sock, echo_line, strlen(echo_line), 0);  
    136     const char *type="Content-Type:text/html;charset=ISO-8859-1
    ";
    137     send(sock, type, strlen(type), 0);
    138     const char *null_line="
    ";  
    139     send(sock, null_line, strlen(null_line), 0);  //空行
    140 
    141     printf("query_string: %s
    ", query_string);
    142     //path-> exe
    143     int input[2];
    144     int output[2];
    145     if(pipe(input) < 0 || pipe(output) < 0){
    146         echo_string(sock);
    147         return 11;
    148     }
    149     pid_t id = fork();
    150     if(id < 0){
    151         echo_string(sock);
    152         return 12;
    153     }else if(id == 0){//child
    154         close(input[1]);
    155         close(output[0]);
    156         sprintf(method_env, "METHOD=%s", method);
    157         putenv(method_env);
    158 
    159         if(strcasecmp(method, "GET") == 0){
    160             sprintf(query_string_env, "QUERY_STRING=%s", query_string);
    161             putenv(query_string_env);
    162         }else{ // POST
    163             sprintf(content_len_env, "CONTENT_LENGTH=%d", content_len);
    164             putenv(content_len_env);
    165         }
    166         dup2(input[0], 0);//重定向
    167         dup2(output[1], 1);
    168         execl(path, path, NULL);  //第一个参数:路径及名字,第二个参数:怎么执行,传什么参数
    169         printf("execl error!
    ");
    170         exit(1);
    171     }else{
    172         close(input[0]);
    173         close(output[1]);
    174 
    175         int i = 0;
    176         char c = '';
    177         if(strcasecmp(method, "POST") == 0){
    178             for( ; i < content_len; i++ ){
    179                 recv(sock, &c, 1, 0);
    180                 write(input[1], &c, 1);
    181             }
    182         }
    183 
    184         c='';
    185         while(read(output[0], &c, 1) > 0){
    186             send(sock, &c, 1, 0);
    187         }
    188 
    189         waitpid(id, NULL, 0);
    190         close(input[1]);
    191         close(output[0]);
    192     }
    193 }
    194 
    195 //thread 
    196 void *handler_request(void *arg) 
    197 {
    198     int sock = (int)arg;
    199 #ifdef _DEBUG_  //测试代码
    200     char line[1024];  
    201     do{
    202         int ret = get_line(sock, line, sizeof(line));
    203         if(ret > 0){
    204             printf("%s", line);
    205         }else{
    206             printf("request ...... done!
    ");
    207             break;
    208         }
    209     }while(1);
    210 #else
    211     int ret = 0;
    212     char buf[SIZE];  //读到的请求内容
    213     char method[SIZE/10];  //请求资源的方法
    214     char url[SIZE];  //统一资源标识符
    215     char path[SIZE];  //有效资源路径
    216     int i, j;
    217     int cgi = 0; //设置CGI模式
    218     char *query_string = NULL;  //请求资源字符串(URL中问号后的内容)
    219     if(get_line(sock, buf, sizeof(buf)) <= 0){ //获得一行请求内容
    220         echo_string(sock);
    221         ret = 5;
    222         goto end;
    223     }
    224     i=0;//method ->index
    225     j=0;//buf -> index
    226 
    227     while( !isspace(buf[j]) &&
    228             j < sizeof(buf) &&
    229             i < sizeof(method)-1){
    230         method[i]=buf[j];
    231         i++, j++;
    232     }
    233     method[i] = 0;
    234     if(strcasecmp(method, "GET") &&  //忽略大小写的字符比较,此处为判断请求资源的方法是否为GET方法或POST方法
    235             strcasecmp(method, "POST") ){
    236         echo_string(sock);
    237         ret = 6;
    238         goto end;
    239     }
    240     if(strcasecmp(method, "POST") == 0){  //如果使用POST方法必定是CGI模式
    241         cgi = 1;
    242     }
    243     //buf -> "GET          /      http/1.0"
    244     while(isspace(buf[j]) && j < sizeof(buf)){
    245         j++;
    246     }
    247     i=0;
    248     while(!isspace(buf[j]) && j < sizeof(buf) && i < sizeof(url)-1){
    249         url[i] = buf[j];
    250         i++, j++;
    251     }
    252     url[i] = 0;
    253     printf("method: %s, url: %s
    ", method, url);
    254     query_string = url;
    255     while(*query_string != ''){
    256         if(*query_string == '?'){//如果是GET方法且传参,必定是CGI模式
    257             *query_string = '';
    258             query_string++;
    259             cgi = 1;
    260             break;
    261         }
    262         query_string++;
    263     }
    264     sprintf(path, "wwwroot%s", url);
    265     //method, url, query_string, cgi
    266     if(path[strlen(path)-1] == '/'){ // '/'
    267         strcat(path, "index.html");//如果是GET方法且无参,拼接上首页信息
    268     }
    269     struct stat st;
    270     if(stat(path, &st) != 0){
    271         echo_string(sock);
    272         ret = 7;
    273         goto end;
    274     }else{
    275         if(S_ISDIR(st.st_mode)){  //如果是目录,则拼接上首页信息,默认任何目录下都可以访问首页
    276             strcat(path, "/index.html");
    277         }else if( (st.st_mode & S_IXUSR) ||   //如果是二进制文件
    278                   (st.st_mode & S_IXGRP) || 
    279                   (st.st_mode & S_IXOTH) ){
    280             cgi=1;
    281         }else{
    282         }
    283         //ok->cgi=?, path, query_string, method
    284         if(cgi){
    285             printf("enter CGI
    ");  //进入CGI模式处理
    286             exe_cgi(sock, method, path, query_string);
    287         }else{//非CGI处理
    288             printf("method: %s, url: %s, path: %s, cgi: %d, query_string: %s
    ", method, url, path, cgi, query_string);
    289             drop_header(sock); //!!!!!!!!!!!!!!清除信息(不关心的内容)
    290             echo_www(sock, path, st.st_size);//非CGI模式时的响应
    291         }
    292     }
    293 
    294 end:
    295     printf("quit client...
    ");  //出错退出
    296     close(sock);
    297     return (void*)ret;
    298 #endif
    299 }

    3、相关技术解释:

    (1)CGI:通用网关接口

      基本原理:通用网关接口是一个Web服务器主机提供信息服务的标准接口。通过CGI接口,Web服务器根据客户端提交的资源请求信息,转交给服务器端对应的CGI程序进行处理,最后返回结果给客户端。简单来说就是HTTP服务器与客户端进行“交谈”的一种工具,其程序须运行在网络服务器上

      组成CGI通信系统的是两部分:一部分是html页面,就是在用户端浏览器上显示的页面。另一部分则是运行在服务器上的Cgi程序。绝大多数的CGI程序被用来解释处理来自表单的输入信息,并在服务器产生相应的处理,或将相应的信息反馈给浏览器。CGI程序使网页具有交互功能。

      CGI在客户端与服务器通讯中的处理步骤:

      1)通过Internet把用户请求送到服务器;

      2)服务器接收用户请求并交给相应CGI程序处理;

      3)CGI程序把处理结果传送给服务器;

      4)服务器把结果返回给用户。

      前面已经介绍过服务器和客户端之间的通信,实际上是客户端的浏览器和服务器端的http服务器之间的HTTP通信,我们只需要知道浏览器请求执行服务器上哪个CGI程序就可以了,其他不必深究细节,因为这些过程不需要程序员去操作。服务器和CGI程序之间的通讯才是我们关注的。一般情况下,服务器和CGI程序之间是通过标准输入输出来进行数据传递的,而这个过程需要环境变量的协作方可实现。在服务器端执行步骤:1)服务器将URL指向一个应用程序  2)服务器为应用程序执行做准备  3)应用程序执行,读取标准输入和有关环境变量  4)应用程序进行标准输出。

    (2)CGI关于环境变量

    对于CGI程序来说,它继承了系统的环境变量。CGI环境变量在CGI程序启动时初始化,在结束时销毁。

           当一个CGI程序不是被HTTP服务器调用时,它的环境变量几乎是系统环境变量的复制。

    当这个CGI程序被HTTP服务器调用时,它的环境变量就会多了以下关于HTTP服务器、客户端、CGI传输过程等项目。

    CONTENT_TYPE:application/x-www-form-urlencoded,表示数据来自HTML表单,并且经过了URL编码。

    ACCEPT:客户机所支持的MIME类型清单,内容如:”image/gif,image/jpeg”

    REQUEST_METHOD:本项目涉及常见的两种方法:POSTGET,但我们写CGI程序时,最后还要考虑其他的情况。

      环境变量是一个保存用户信息的内存区。当客户端的用户通过浏览器发出CGI请求时,服务器就寻找本地的相应CGI程序并执行它。在执行CGI程序的同时,服务器把该用户的信息保存到环境变量里。接下来,CGI程序的执行流程是这样的:查询与该CGI程序进程相应的环境变量:第一步是request_method,如果是POST,就从环境变量的len,然后到该进程相应的标准输入取出len长的数据。如果是GET,则用户数据就在环境变量的QUERY_STRING里。

     (3)POST/GET传输方式详解

    1)POST方法

      如果采用POST方法,那么客户端发送的用户数据将存放在CGI进程的标准输入中,即消息正文内,较为隐蔽,且一般没有上限。同时将用户数据的长度赋予环境变量中的CONTENT_LENGTH。客户端用POST方式发送数据有一个相应的MIME类型(通用Internet邮件扩充服务:Multi-purpose Internet Mail Extensions)。目前,MIME类型一般是:application/x-wwww-form-urlencoded,该类型表示数据来自HTML表单。该类型记录在环境变量CONTENT_TYPE中,CGI程序应该检查该变量的值。

    2GET方法

      在该方法下,CGI程序无法直接从服务器的标准输入(用户发送的消息正文)中获取数据,因为服务器把它从标准输入接收到得数据编码到环境变量QUERY_STRING(或PATH_INFO)。

      采用GET方法提交HTML表单数据的时候,客户机将把这些数据附加到由ACTION标记命名的URL的末尾,用一个包括把经过URL编码后的信息与CGI程序的名字分开:http://www.mycorp.com/hello.htmlname=hgq$id=1QUERY_STRING的值为name=hgq&id=1(?左侧为要请求的资源,右侧为参数,参数形式一般为name=value形式,以“&”连接)。或者使用nomal形式的GET方法,无参数,不带正文,只有请求行+消息报头+空行。有些程序员不愿意采用GET方法,因为在他们看来,把动态信息附加在URL的末尾有URL的出发点:URL作为一种标准用语,一般是用作网络资源的唯一定位标示。

    3POSTGET的区别

           以 GET方式接收的数据是有长度限制,而用 POST方式接收的数据是没有长度限制的。并且,以 GET方式发送数据,可以通过 URL的形式来发送,但 POST方式发送的数据必须要通过 Form才到发送。

    CGI程序示例 mathcgi.h :

     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 
     4 void mymath(char *arg)
     5 {
     6     //data1=1000&data2=2000
     7     char *argv[3];
     8     int i = 0;
     9     char *start = arg;
    10     while(*start){
    11         if(*start == '='){
    12             start++;
    13             argv[i++] = start;
    14             continue;
    15         }
    16         if(*start== '&'){
    17             *start = '';
    18         }
    19         start++;
    20     }
    21     argv[i] = NULL;
    22     int data1 = atoi(argv[0]);
    23     int data2 = atoi(argv[1]);
    24     printf("<html><body><h1>");
    25     printf("%d + %d = %d<br/>", data1, data2, data1 + data2);
    26     printf("%d - %d = %d<br/>", data1, data2, data1 - data2);
    27     printf("%d * %d = %d<br/>", data1, data2, data1 * data2);
    28     printf("%d / %d = %d<br/>", data1, data2, data2==0? 0 : data1 / data2);
    29     printf("%d %% %d = %d<br/>", data1, data2, data2==0? 0 : data1 % data2);
    30     printf("</h1></body></html>");
    31 }
    32 
    33 int main()
    34 {
    35     char *method = NULL;
    36     char *query_string = NULL;
    37     char *string_arg = NULL;
    38     int content_len = -1;
    39     char buf[1024];
    40     if((method=getenv("METHOD"))){
    41         if(strcasecmp(method, "GET") == 0){
    42             if((query_string=getenv("QUERY_STRING"))){
    43                 string_arg = query_string;
    44             }
    45         }else{
    46             if(getenv("CONTENT_LENGTH")){
    47                 content_len = atoi(getenv("CONTENT_LENGTH"));
    48                 int i = 0;
    49                 for(; i < content_len; i++){
    50                     read(0, &buf[i], 1);
    51                 }
    52                 buf[i] = '';
    53                 string_arg = buf;
    54             }
    55         }
    56     }
    57 
    58     mymath(string_arg);
    59     return 0;
    60 }

     (2)HTML

    本项目中只是使用了一些基本的HTML知识,下面是简单的 index.html :

     1 <html>
     2     <head>hello http</head>
     3     <body>
     4         <h1>Hello My Web!</h1>
     5         <img src="imag/mgh.jpg" alt="default" width="100" height="100">
     6         <a href="cgi-bin/select_cgi">select</a>
     7         <!--<form action="/cgi-bin/math_cgi" method="POST">
     8         First data:<br>
     9         <input type="text" name="data1" value="0">
    10         <br>
    11         second data:<br>
    12         <input type="text" name="data2" value="1">
    13         <br><br>
    14         <input type="submit" value="Submit">
    15         </form>--!>
    16     </body>
    17 </html>

    到这里就基本可以访问网页信息了:

    4、本机进行环回测试,用的IP是127.0.0.1,Http协议的TCP连接默认端口号为80:

     图片自己选择,此页面实现的是两个数的加减乘除,当点击submit时跳转页面如下:

    此时跳转到cgi_bin目录下的可执行文件debug_cgi,显示加减乘除的结果。

    一个简陋的http服务器就完成了。后面还需要一些其它的扩展,再更新……

    5、遇到的一些问题:

    1)本地环回测试ok,Linux下的浏览器测试也可以,但不能接外部的浏览器访问(没有设置桥接模式)嗯~要是在外部浏览器测试的话千万别忘记关闭防火墙。

    解决:切换超级用户:$service iptables stop

    2)服务器应答时,没有将html格式的页面发送,而是将底层的实现代码展示在浏览器,并且在调试时将本来要打印的调试信息会打印到网页上(在回应空行时将send期望发送的数值写的太大,本来只需要发送两个字节的内容)
    解决:先检查代码,思路正确,在容易出现问题的地方加入调试信息,最后将问题定位在echo_www()函数内 。

    3)不能显示图片(这个问题是没有将所有发送的情况考虑完全,只考虑到目录、可执行程序,但没有考虑到如果请求的是一个路径明确的普通文件)
    解决:测试请求一个路径明确的test.html文件,加入调试信息 ,将问题定位在:如果请求的资源存在,应该如何处理。对于普通文件,找到后并回显给浏览器;如果是目录,应答的是默认页面;如果是可执行程序,执行后返回结果

    4)能显示图片后,但显示的不完整(原因:echo_www中,期望读取一行信息的line值太小,不能存下一张图片)

    5)运行cgi模式时,每次提交数据并进行submit后都会自动出现提醒下载的页面
    原因:在响应报头中,将Content-Type中的”text”写成”test”。而浏览器对于不能识别或解析的实体,都会提醒用户下载。

  • 相关阅读:
    深入理解Java内存(图解堆栈)
    java堆栈区别
    Java之堆栈的区别
    【微信小程序】获取轮播图当前图片下标、滑动展示对应的位数、点击位数展示对应图片
    【微信小程序】转载:微信小程序实战篇-下拉刷新与加载更多
    【微信小程序】转载:微信小程序之购物车功能
    【微信小程序】loading标签使用,可自定义时长
    【微信小程序】日历插件,适用于酒店订房类小程序
    【微信小程序】微信小程序wx.previewImage预览图片
    【微信小程序】小程序和公众号 退款功能教程(含申请退款和退款回调,退款回调地址在商户后台配置或者代码自定义)
  • 原文地址:https://www.cnblogs.com/33debug/p/7389036.html
Copyright © 2011-2022 走看看