uhttpd 代码分析

uhttpd main函数分析

int main(int argc, char **argv)
{
    struct alias *alias;
    /* 设置命令的别名
    struct alias {
        struct list_head list;//命令列表
        char *alias;//命令别名
        char *path;//命令路径
    };
    
    struct list_head {
        struct list_head *next;//列表下一个对象
        struct list_head *prev;//列表前一个对象
    };
     */

    bool nofork = false;
    /* 进程还没有开始克隆自己 */

    char *port;
    /* 端口 */

    int opt;
    /* 一个在for里面使用的普通变量 */
    
    int ch;
    /* 一个在while里面使用的普通变量 */

    int cur_fd;
    /* 保存了"/dev/null"句柄 */

    int bound = 0;
    /* 绑定的监听端口个数 */

#ifdef HAVE_TLS
/* 当安装了tls插件 */

    int n_tls = 0;
    /* 如果安装了tls,则不为0 */

    const char *tls_key = NULL;
    /* ASN.1 server private key file */
    
    const char *tls_crt = NULL;
    /* ASN.1 server certificate file */

#endif

    BUILD_BUG_ON(sizeof(uh_buf) < PATH_MAX); /* 条件为真,编译报错,uh_buf为4096,path_max一般为260 */
    /*
    #define __BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))
    */

    uh_dispatch_add(&cgi_dispatch);
    /*
    添加cgi调度程序到双向链表
    */
    
    init_defaults_pre();
    /*
    初始化配置变量
    */
    
    signal(SIGPIPE, SIG_IGN);
    /*
    函数原型:sig_t signal(int signum,sig_t handler);
    功能:设置某一信号的对应动作
    第一个参数signum指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。
    第二个参数handler描述了与信号关联的动作。
    SIGPIPE:在reader中止之后写Pipe的时候发送
    SIG_IGN:表示忽略该信号
    函数说明:
        signal()会依参数signum 指定的信号编号来设置该信号的处理函数。
        当指定的信号到达时就会跳转到参数handler指定的函数执行。
        当一个信号的信号处理函数执行时,如果进程又接收到了该信号,
        该信号会自动被储存而不会中断信号处理函数的执行,直到信号处理函数执行完毕再重新调用相应的处理函数。
        但是如果在信号处理函数执行时进程收到了其它类型的信号,该函数的执行就会被中断。
    */


    /*
    原型:int getopt(int argc,char * const argv[ ],const char * optstring);
    功能:用来分析命令行参数。
    参数:argc和argv是由main()传递的参数个数和内容。参数optstring则代表欲处理的选项字符串。
    函数说明:此函数会返回在argv 中下一个的选项字母,此字母会对应参数optstring 中的字母。
            如果选项字符串里的字母后接着冒号“:”,则表示还有相关的参数,全域变量optarg 即会指向此额外参数。
            如果getopt()找不到符合的参数则会印出错信息,并将全域变量optopt设为“?”字符,
            如果不希望getopt()印出错信息,则只要将全域变量opterr设为0即可。
    */
    
    while (ch = getopt(argc, argv, "A:aC:c:Dd:E:fh:H:I:i:K:k:L:l:m:N:n:p:qRr:Ss:T:t:U:u:Xx:y:") != -1) 
    {
        /*
        每次解释一个命令行传递过来的参数
        */
        
        switch(ch) 
        {
        
#ifdef HAVE_TLS
/* 如果安装了tls插件,命令行又带相关的参数,执行相应的配置 */
        case 'C':
            tls_crt = optarg;
            break;

        case 'K':
            tls_key = optarg;
            break;

        case 'q':
            conf.tls_redirect = 1;
            break;

        case 's':
            n_tls++;
            /* fall through */
#else
/* 如果没有安装tls插件,命令行又带相关的参数,显示错误 ,但继续执行*/
        case 'C':
        case 'K':
        case 'q':
        case 's':
            fprintf(stderr, "uhttpd: TLS support not compiled, "
                            "ignoring -%c\n", ch);
            break;
#endif

        case 'p':
            optarg = strdup(optarg);
            bound += add_listener_arg(optarg, (ch == 's'));
            break;

        case 'h':
            if (!realpath(optarg, uh_buf)) 
            {
                fprintf(stderr, "Error: Invalid directory %s: %s\n",
                        optarg, strerror(errno));
                exit(1);
            }
            conf.docroot = strdup(uh_buf);
            break;

        case 'H':
            if (uh_handler_add(optarg)) 
            {
                fprintf(stderr, "Error: Failed to load handler script %s\n",
                    optarg);
                exit(1);
            }
            break;

        case 'E':
            if (optarg[0] != '/') 
            {
                fprintf(stderr, "Error: Invalid error handler: %s\n",
                        optarg);
                exit(1);
            }
            conf.error_handler = optarg;
            break;

        case 'I':
            if (optarg[0] == '/') 
            {
                fprintf(stderr, "Error: Invalid index page: %s\n",
                        optarg);
                exit(1);
            }
            uh_index_add(optarg);
            break;

        case 'S':
            conf.no_symlinks = 1;
            break;

        case 'D':
            conf.no_dirlists = 1;
            break;

        case 'R':
            conf.rfc1918_filter = 1;
            break;

        case 'n':
            conf.max_script_requests = atoi(optarg);
            break;

        case 'N':
            conf.max_connections = atoi(optarg);
            break;

        case 'x':
            fixup_prefix(optarg);
            conf.cgi_prefix = optarg;
            break;

        case 'y':
            alias = calloc(1, sizeof(*alias));
            if (!alias) 
            {
                fprintf(stderr, "Error: failed to allocate alias\n");
                exit(1);
            }
            alias->alias = strdup(optarg);
            alias->path = strchr(alias->alias, '=');
            if (alias->path)
                *alias->path++ = 0;
            list_add(&alias->list, &conf.cgi_alias);
            break;

        case 'i':
            optarg = strdup(optarg);
            port = strchr(optarg, '=');
            if (optarg[0] != '.' || !port) 
            {
                fprintf(stderr, "Error: Invalid interpreter: %s\n",
                        optarg);
                exit(1);
            }

            *port++ = 0;
            uh_interpreter_add(optarg, port);
            break;

        case 't':
            conf.script_timeout = atoi(optarg);
            break;

        case 'T':
            conf.network_timeout = atoi(optarg);
            break;

        case 'k':
            conf.http_keepalive = atoi(optarg);
            break;

        case 'A':
            conf.tcp_keepalive = atoi(optarg);
            break;

        case 'f':
            nofork = 1;
            break;

        case 'd':
            optarg = strdup(optarg);
            port = alloca(strlen(optarg) + 1);
            if (!port)
                return -1;

            /* "decode" plus to space to retain compat */
            for (opt = 0; optarg[opt]; opt++)
                if (optarg[opt] == '+')
                    optarg[opt] = ' ';

            /* opt now contains strlen(optarg) -- no need to re-scan */
            if (uh_urldecode(port, opt, optarg, opt) < 0) 
            {
                fprintf(stderr, "uhttpd: invalid encoding\n");
                return -1;
            }

            printf("%s", port);
            return 0;
            break;

        /* basic auth realm */
        case 'r':
            conf.realm = optarg;
            break;

        /* md5 crypt */
        case 'm':
            printf("%s\n", crypt(optarg, "$1$"));
            return 0;
            break;

        /* config file */
        case 'c':
            conf.file = optarg;
            break;

#ifdef HAVE_LUA
/* 如果安装了lua插件,命令行又带相关的参数,执行相应的配置 */
        case 'l':
            conf.lua_prefix = optarg;
            break;

        case 'L':
            conf.lua_handler = optarg;
            break;
#else
/* 如果没有安装lua插件,命令行又带相关的参数,显示错误 ,但继续执行*/
        case 'l':
        case 'L':
            fprintf(stderr, "uhttpd: Lua support not compiled, "
                            "ignoring -%c\n", ch);
            break;
#endif

#ifdef HAVE_UBUS
/* 如果安装了ubus插件,命令行又带相关的参数,执行相应的配置 */
        case 'a':
            conf.ubus_noauth = 1;
            break;

        case 'u':
            conf.ubus_prefix = optarg;
            break;

        case 'U':
            conf.ubus_socket = optarg;
            break;

        case 'X':
            conf.ubus_cors = 1;
            break;
#else
/* 如果没有安装ubus插件,命令行又带相关的参数,显示错误 ,但继续执行*/
        case 'a':
        case 'u':
        case 'U':
        case 'X':
            fprintf(stderr, "uhttpd: UBUS support not compiled, "
                            "ignoring -%c\n", ch);
            break;
#endif

        default:
            return usage(argv[0]);
            /* 没有参数或者有不明参数,显示帮助并退出 */
        }
    }

    uh_config_parse();
    /*
    原型:static void uh_config_parse(void)
    功能:解析config文件
    */

    if (!conf.docroot) /* 如果conf结构体里的docroot还没有赋值,就执行 */
    {
        if (!realpath(".", uh_buf)) /* uh_buf为当前工作目录的绝对路径指针 */
        {
            /*
            原型:char *realpath(const char *path, char *resolved_path);
            功能:用来将参数path所指的相对路径转换成绝对路径
            返回值:成功则返回指向resolved_path的指针,失败返回NULL
            */
            fprintf(stderr, "Error: Unable to determine work dir\n");
            return 1;
        }
        conf.docroot = strdup(uh_buf); /* 结构体conf里的docroot字段保存了当前工作路径 */
        /*
        原型:extern char *strdup(char *s);
        功能: 将字符串拷贝到新建的位置处
        返回值:返回一个指针,指向为复制字符串分配的空间;如果分配空间失败,则返回NULL值
        */
        
    }

    init_defaults_post();
    /* 初始化主页名和cgi的绝对路径 */

    if (!bound) /* 如果没有绑定监听端口,则报错退出 */
    {
        fprintf(stderr, "Error: No sockets bound, unable to continue\n");
        return 1;
    }

#ifdef HAVE_TLS /* 如果安装了tls,则执行 */
    if (n_tls) /* 安装了tls,n_tls就不为0 */
    {
        if (!tls_crt || !tls_key) /* 没有公匙或者没有私匙,就报错并退出 */
        {
            fprintf(stderr, "Please specify a certificate and "
                    "a key file to enable SSL support\n");
            return 1;
        }

        if (uh_tls_init(tls_key, tls_crt)) /* 初始化tls,成功返回0 */
            return 1;
    }
#endif

#ifdef HAVE_LUA /* 如果安装了lua程序,则执行 */
    if (conf.lua_handler || conf.lua_prefix) /* 两个变量其中一个有值就执行 */
    {
        /*
        conf.lua_handler = optarg;    //file lua脚本文件
        conf.lua_prefix = optarg;     //string 默认为'/lua'
        */
        if (!conf.lua_handler || !conf.lua_prefix) /* 两个变量其中一个没值就报错 */
        {
            fprintf(stderr, "Need handler and prefix to enable Lua support\n");
            return 1;
        }
        
        if (uh_plugin_init("uhttpd_lua.so")) /* 加载lua动态链接库 */
            return 1;
    }
#endif

#ifdef HAVE_UBUS /* 如果安装了ubus程序,则执行 */
    /* 不是很理解这里,conf.ubus_prefix是否有值对结果不是很重要 */
    if (conf.ubus_prefix && uh_plugin_init("uhttpd_ubus.so")) /* 插件初始化成功则会继续执行 */
        return 1;        
#endif

    /* 程序如果还没有克隆自己,那现在开始克隆 */
    if (!nofork) {
    
        switch (fork()) {
        /*
         在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。
         在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。
         我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
        */
        
        case -1: /* 如果出现错误,fork返回一个负值 */
            perror("fork()"); /* 显示错误 */
            /*
            perror(s) 用来将上一个函数发生错误的原因输出到标准设备(stderr)。
            参数 s 所指的字符串会先打印出,后面再加上错误原因字符串。
            */
            exit(1); /* 1是错误退出 */

        case 0: /* 子进程执行这里,守护进程设置 */
            
            /* chdir 系统调用函数(同cd)
                原型: int chdir(const char *path );
                功 能:更改当前工作目录。
                参 数:Path 目标目录,可以是绝对目录或相对目录。
                返回值:成功返回0 ,失败返回-1
            */
            if (chdir("/")) /* 跳转成功不执行 */
                perror("chdir()"); /* 显示错误 */

            /* open 系统调用函数
                原型:int open(constchar*pathname,intflags);
                参数1:pathname 是待打开/创建文件的POSIX路径名
                参数2:flags 用于指定文件的打开/创建模式 
                        O_RDONLY只读模式
                        O_WRONLY只写模式
                        O_RDWR读写模式
                返回值:成功则返回文件描述符,否则返回-1
            */
            cur_fd = open("/dev/null", O_WRONLY);
            /* 由open返回的文件描述符一定是最小的未用描述符数值,打开的应该至少是文件描述符3 */
            
            if (cur_fd > 0) {
            
                /*
                函数名: dup2
                功能: 复制文件句柄
                用法: int dup2(int oldfd,int newfd);
                */
                
                dup2(cur_fd, 0);
                /* 功能是令文件描述符0 指向fd 所指向的文件,即“输入重定向”的功能 */
                
                dup2(cur_fd, 1);
                /* 功能是令文件描述符1 指向fd 所指向的文件,即“输出重定向”的功能 */
                
                dup2(cur_fd, 2);
                /* 功能是令文件描述符2 指向fd 所指向的文件,即“错误重定向”的功能 */
            }
            break;

        default: /* 父进程执行这里 */
            exit(0); /* 0是正常退出 */
        }
    }

    return run_server();
}

主函数用到的函数:
uh_config_parse

/* uhttpd配置文件解析 */
static void uh_config_parse(void)
{
    const char *path = conf.file;
    /* config文件地址赋值给变量path */
    
    FILE *c;
    /* 文件流对象c */
    
    char line[512];
    /* 大小为512字节的数组 */
    
    char *col1;
    /* 指向特定字符的指针,可以进行指针运算 */
    
    char *col2;
    /* 指向特定字符的指针,可以进行指针运算 */
    
    char *eol;
    /* 代表一个字符数组存储空间的首地址,可以进行指针运算 */

    if (!path) /* 如果config文件地址没有定义,则赋默认值 */
        path = "/etc/httpd.conf";

    c = fopen(path, "r"); /* 打开config文件,只读模式,并将句柄赋给文件流对象c */
    
    if (!c) /* 如果句柄为空,则返回 */
        return;

    memset(line, 0, sizeof(line)); /* 用0填充line数组 */
    /*
    原型:void *memset(void *s, int ch, size_t n);
    解释:将s中当前位置后面的n个字节用 ch 替换并返回 s 
    */

    while (fgets(line, sizeof(line) - 1, c)) 
    {
        /* 从文件结构体指针stream中读取数据,每次读取一行。
        原型:char *fgets(char *buf, int bufsize, FILE *stream);
        参数1:*buf 字符型指针,指向用来存储所得数据的地址。
        参数2:bufsize 整型数据,指明存储数据的大小。
        参数3:*stream 文件结构体指针,将要读取的文件流。
        返回值:成功,则返回第一个参数buf;失败NULL
        */
        
        /* 查看了配置文件,好像下面判断里的代码都不会被执行 */
        if ((line[0] == '/') && (strchr(line, ':') != NULL)) /* 行开头为'/',并且行里存在字符':',就执行下面 */
        {
            /*
            原型:char *strchr(char* _Str,char _Ch)
            功能:查找字符串_Str中首次出现字符_Ch的位置
            返回值:成功则返回要查找字符第一次出现的位置,失败返回NUL
            */
        
            /* 下面条件中只要有一个成立,就退出对line数组的操作 */
            if (!(col1 = strchr(line, ':')) || /* ':'在line的首地址赋值给col1,然后取反,意思是存在就是变成0,不存在为1 */
                (*col1++ = 0) ||               /* 给col1地址赋0,然后col1指针加1 */
                !(col2 = strchr(col1, ':')) || /* ':'在line的第二地址赋值给col2,然后取反,意思是存在就是变成0,不存在为1 */
                (*col2++ = 0) ||               /* 给col2地址赋0,然后col2指针加1 */
                !(eol = strchr(col2, '\n')) || /* '\n'在line的地址赋值给eol,然后取反,意思是存在就是变成0,不存在为1 */
                (*eol++  = 0))                 /* 给eol地址赋0,然后eol指针加1 */
                continue;

            uh_auth_add(line, col1, col2);
            
        } 
        else if (!strncmp(line, "I:", 2)) /* 行开头的两个字符为"I:",则执行下面 */
        {
            /*
            原型:int strncmp ( const char * str1, const char * str2, size_t num );
            功能:比较str1和str2字符串的前num个字符
            返回值:若str1与str2的前n个字符相同,则返回0;
                    若s1大于s2,则返回大于0的值;
                    若s1 若小于s2,则返回小于0的值
            */
        
            if (!(col1 = strchr(line, ':')) || 
                (*col1++ = 0) ||
                !(eol = strchr(col1, '\n')) || 
                (*eol++  = 0))
                continue;

            uh_index_add(strdup(col1));
            
        } 
        else if (!strncmp(line, "E404:", 5)) /* 行开头的5个字符为"E404:",则执行下面 */
        {
            if (!(col1 = strchr(line, ':')) || 
                (*col1++ = 0) ||
                !(eol = strchr(col1, '\n')) || 
                (*eol++  = 0))
                continue;

            conf.error_handler = strdup(col1);
            
        }
        else if ((line[0] == '*') && (strchr(line, ':') != NULL)) /* 行开头为'*',并且行里存在字符':',就执行下面 */
        {
            if (!(col1 = strchr(line, '*')) || 
                (*col1++ = 0) ||
                !(col2 = strchr(col1, ':')) || 
                (*col2++ = 0) ||
                !(eol = strchr(col2, '\n')) || 
                (*eol++  = 0))
                continue;

            uh_interpreter_add(col1, col2);
            
        }
    }

    fclose(c); /* 关闭文件流对象 */
}

init_defaults_post

/* 初始化主页名和cgi的绝对路径 */
static void init_defaults_post(void)
{
    /* 把默认主页名列表保存到链接表index_files */
    uh_index_add("index.html");
    uh_index_add("index.htm");
    uh_index_add("default.html");
    uh_index_add("default.htm");

    /* 配置cgi的一些全局参数 */
    if (conf.cgi_prefix)  /* option 'cgi_prefix' '/cgi-bin' */
    {
        char *str = malloc(strlen(conf.docroot) + strlen(conf.cgi_prefix) + 1);
        /*
            *str = malloc(strlen("/www/cgi_bin")+1)
        */

        strcpy(str, conf.docroot);
        strcat(str, conf.cgi_prefix);
        /*
        原型:extern char *strcat(char *dest, const char *src);
        功能:把src所指字符串添加到dest结尾处(覆盖dest结尾处的'\0')。
        */
        
        conf.cgi_docroot_path = str; /* 配置文件的cgi_docroot_path指向cgi目录的绝对路径 */
        conf.cgi_prefix_len = strlen(conf.cgi_prefix); /* '/cgi_bin'的长度为8 */
    }
}

uh_index_add

/* 本函数在file文件下 */
void uh_index_add(const char *filename)
{
    struct index_file *idx;
    /* index_file
    struct list_head list;
    const char *name;
    */

    idx = calloc(1, sizeof(*idx));
    /*
    原型:void *calloc(size_t n, size_t size);
    功能:在内存的动态存储区中分配n个长度为size的连续空间
    返回值:函数返回一个指向分配起始地址的指针;如果分配不成功,返回NULL。
    */
    
    idx->name = filename;
    list_add_tail(&idx->list, &index_files); /* index_files是一个全局结构体,双链接结构 */
    /*
    功能:把传进来的文件名保存到链接表
    */
}

/* 为了代码逻辑清晰做的调整,一个待加入的链表值,一个前链表,一个链表头 */
static inline void list_add_tail(struct list_head *_new, struct list_head *head)
{
    _list_add(_new, head->prev, head);
}

/* 向双向链表插入新的值 */
static inline void _list_add(struct list_head *_new, struct list_head *prev, struct list_head *next)
{
    next->prev = _new;
    _new->next = next;
    _new->prev = prev;
    prev->next = _new;
    /*
     ------          ------          ------
    | next |        | next |        | next |
    |------|        |------|        |------|
    | prev |        | prev |        | prev |
     ------          ------          ------
      NEXT            NEW             PREV
      
  NEXT->prev -------> NEW
      
      NEXT  <------ NEW->next
                    NEW->prev ------> PREV
                    
                      NEW <-------- PREV->next
    */
}

uh_plugin_init

/* plugin.c  插件初始化 */
int uh_plugin_init(const char *name) /* name为插件文件名 */
{
    struct uhttpd_plugin *p; /* 动态链接库地址 */
    const char *sym; /* 动态链接库符号 */
    void *dlh; /* 动态链接库名柄 */

    dlh = dlopen(name, RTLD_LAZY | RTLD_GLOBAL); /* 打开动态链接库 */
    if (!dlh)  /* 无法打开动态链接库,报错退出 */
    {
        fprintf(stderr, "Could not open plugin %s: %s\n", name, dlerror());
        return -ENOENT;
    }

    sym = "uhttpd_plugin";
    p = dlsym(dlh, sym); /* 保存动态链接库地址 */
    if (!p) /* 无法获得动态链接库地址,报错退出 */
    {
        fprintf(stderr, "Could not find symbol '%s' in plugin '%s'\n", sym, name);
        return -ENOENT;
    }

    list_add(&p->list, &plugins); /* 把动态链接库保存到双链接表plugins */
    return p->init(&ops, &conf); /* 初始化插件的配置内容 */
}

usage

static int usage(const char *name)
{
    fprintf(stderr,
        "Usage: %s -p [addr:]port -h docroot\n"
        "    -f              Do not fork to background\n"
        "    -c file         Configuration file, default is '/etc/httpd.conf'\n"
        "    -p [addr:]port  Bind to specified address and port, multiple allowed\n"
#ifdef HAVE_TLS
        "    -s [addr:]port  Like -p but provide HTTPS on this port\n"
        "    -C file         ASN.1 server certificate file\n"
        "    -K file         ASN.1 server private key file\n"
        "    -q              Redirect all HTTP requests to HTTPS\n"
#endif
        "    -h directory    Specify the document root, default is '.'\n"
        "    -E string       Use given virtual URL as 404 error handler\n"
        "    -I string       Use given filename as index for directories, multiple allowed\n"
        "    -S              Do not follow symbolic links outside of the docroot\n"
        "    -D              Do not allow directory listings, send 403 instead\n"
        "    -R              Enable RFC1918 filter\n"
        "    -n count        Maximum allowed number of concurrent script requests\n"
        "    -N count        Maximum allowed number of concurrent connections\n"
#ifdef HAVE_LUA
        "    -l string       URL prefix for Lua handler, default is '/lua'\n"
        "    -L file         Lua handler script, omit to disable Lua\n"
#endif
#ifdef HAVE_UBUS
        "    -u string       URL prefix for UBUS via JSON-RPC handler\n"
        "    -U file         Override ubus socket path\n"
        "    -a              Do not authenticate JSON-RPC requests against UBUS session api\n"
        "    -X        Enable CORS HTTP headers on JSON-RPC api\n"
#endif
        "    -x string       URL prefix for CGI handler, default is '/cgi-bin'\n"
        "    -y alias[=path]    URL alias handle\n"
        "    -i .ext=path    Use interpreter at path for files with the given extension\n"
        "    -t seconds      CGI, Lua and UBUS script timeout in seconds, default is 60\n"
        "    -T seconds      Network timeout in seconds, default is 30\n"
        "    -k seconds      HTTP keepalive timeout\n"
        "    -d string       URL decode given string\n"
        "    -r string       Specify basic auth realm\n"
        "    -m string       MD5 crypt given string\n"
        "\n", name
    );
    return 1;
}

/*
TLS: TLS(安全传输层协议)用于在两个通信应用程序之间提供保密性和数据完整性。
LUA: LUA是一个小巧的脚本语言
UBUS: UBUS是新openwrt引入的一个消息总线,主要作用是实现不同应用程序之间的信息交互
*/

本文章由作者:佐须之男 整理编辑,原文地址: uhttpd 代码分析
本站的文章和资源来自互联网或者站长的原创,按照 CC BY -NC -SA 3.0 CN协议发布和共享,转载或引用本站文章应遵循相同协议。如果有侵犯版权的资 源请尽快联系站长,我们会在24h内删除有争议的资源。欢迎大家多多交流,期待共同学习进步。
分享到:更多

相关推荐

网友评论(0)