Unix网络编程代码 第13章 守护进程和inetd超级服务器

简介: 1. 概述     守护进程是在后台运行且不与任何控制终端关联的进程。unix系统通常有很多守护进程在后台运行,执行不同的管理任务。    守护进程没有控制终端通常源于它们由系统初始化脚本启动。然而守护进程也可能从某个终端由用户在shell提示符下键入命令行启动,这样的守护进程必须亲自脱离与控制终端的关联,从而避免与作业控制,终端会话管理,终端产生信号等发生任何不期望的交互,也可以避免在后台运行的守护进程非预期的输出到终端。

1. 概述

    守护进程是在后台运行且不与任何控制终端关联的进程。unix系统通常有很多守护进程在后台运行,执行不同的管理任务。
    守护进程没有控制终端通常源于它们由系统初始化脚本启动。然而守护进程也可能从某个终端由用户在shell提示符下键入命令行启动,这样的守护进程必须亲自脱离与控制终端的关联,从而避免与作业控制,终端会话管理,终端产生信号等发生任何不期望的交互,也可以避免在后台运行的守护进程非预期的输出到终端。
    守护进程有多种启动方法:
    1.在系统启动阶段,许多守护进程由系统初始化脚本启动。这些脚本通常位于/etc目录或以/etc/rc开头的某个目录中,它们的具体位置和内容却是实现相关的。由这些脚本启动的守护进程一开始拥有超级用户权限。
    有若干个网络服务器通常从这些脚本启动:inetd超级服务器,web服务器,邮件服务器(经常是sendmail)。
    2. 许多网络服务器由inetd超级服务器启动。inetd自身由上一条中的某个脚本启动。inetd监听网络请求,每当有一个请求到达时,启动相应的实际服务器(telnet服务器,FTP服务器等)
    3. cron守护进程按照规则定期执行一些程序,而由它启动执行的程序同样作为守护进程运行。cron自身由第一条启动方法中的某个脚本启动
    4. at命令用于指定将来某个时刻的程序执行。这些程序的执行时刻到来时,通常由cron守护进程启动执行它们,因此这些程序同样作为守护进程运行。
    5.守护进程还可以从用户终端或在前台或在后台启动。这么做往往是为了测试守护进程或重启因某种原因而终止了的某个守护进程。

    因为守护进程没有控制终端,所以当有事发生时它们得有输出消息的某种方法可用,而这些消息既可能是普通的通告性消息,也可能是需由系统管理员处理的紧急事件消息。syslog函数是输出这些消息的标准方法,它把这些消息发送给syslogd守护进程。

 

2. syslog函数,openlog函数和closelog函数

备注:遇到类似的函数,具体说明请查看APUE

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
 
  1. #include <syslog.h>  
  2. void syslog(int priority, const char *message,...);  
  3. void openlog(const char *ident, int options, int facility);  
  4. void closelog(void);  

 

1) 作为守护进程运行的协议无关时间获取服务器程序

服务器程序daytimetcpsrv.c:

 

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
 
  1. #include <stdio.h>  
  2. #include <netdb.h>   
  3. #include <sys/socket.h>  
  4. #include <time.h>  
  5. #include <syslog.h>  
  6. #include <string.h>  
  7. #include <stdlib.h>  
  8. #include <fcntl.h>  
  9. #include <signal.h>  
  10. #include <unistd.h>  
  11.   
  12. extern int errno;  
  13. int daemon_proc;  
  14.   
  15. #define MAXLINE 1024  
  16. #define MAXFD 64  
  17.   
  18. int daemon_init(const char *pname, int facility);  
  19. int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp);  
  20.   
  21. int main(int argc, char **argv)  
  22. {  
  23.     int     listenfd, connfd;  
  24.     socklen_t   len;  
  25.     char    buff[MAXLINE];  
  26.     time_t  ticks;  
  27.     struct  sockaddr_in cliaddr;  
  28.   
  29.     daemon_init(argv[0], 0);  
  30.   
  31.     listenfd = tcp_listen(argv[1], argv[2], NULL);  
  32.   
  33.     for (; ;){  
  34.         len = sizeof(cliaddr);  
  35.         connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &len);  
  36.         inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff));  
  37.         strcat(buff, ".this is a test\n");  
  38.         syslog(LOG_INFO, buff);  
  39.   
  40.         ticks = time(NULL);  
  41.         snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));  
  42.         write(connfd, buff, strlen(buff));  
  43.   
  44.         close(connfd);  
  45.     }  
  46. }  
  47.   
  48. int daemon_init(const char *pname, int facility)  
  49. {  
  50.     int     i;  
  51.     pid_t   pid;  
  52.     if ((pid = fork()) < 0)  
  53.         return -1;  
  54.     else if (pid)  
  55.         _exit(0);  
  56.   
  57.     if (setsid() < 0)  
  58.         return -1;  
  59.     signal(SIGHUP, SIG_IGN);  
  60.     if ((pid = fork()) < 0)  
  61.         return -1;  
  62.     else if (pid)  
  63.         _exit(0);  
  64.   
  65.     daemon_proc = 1;  
  66.   
  67.     chdir("/");  
  68.     for (i = 0; i < MAXFD; i++)  
  69.         close(i);  
  70.   
  71.     open("/dev/null", O_RDONLY);  
  72.     open("/dev/null", O_RDWR);  
  73.     open("/dev/null", O_RDWR);  
  74.   
  75.     openlog(pname, LOG_PID, facility);  
  76. }  
  77. int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp)  
  78. {  
  79.     int     listenfd, n;  
  80.     const int on = 1;  
  81.     struct  addrinfo hints, *res, *ressave;  
  82.   
  83.     bzero(&hints, sizeof(struct addrinfo));  
  84.     hints.ai_flags = AI_PASSIVE;  
  85.     hints.ai_family = AF_UNSPEC;  
  86.     hints.ai_socktype = SOCK_STREAM;  
  87.   
  88.     if ((n = getaddrinfo(host, serv, &hints, &res)) != 0){  
  89.         printf("tcp_listen error for %s,%s:%s\n", host, serv, gai_strerror(n));  
  90.         exit(1);  
  91.     }  
  92.   
  93.     ressave = res;  
  94.   
  95.     do{  
  96.         listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);  
  97.         if (listenfd < 0)  
  98.             continue;  
  99.         setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));  
  100.         if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0)  
  101.             break;  
  102.         close(listenfd);  
  103.     } while ((res = res->ai_next) != NULL);  
  104.   
  105.     if (res == NULL)  
  106.         printf("tcp_listen error for %s,%s\n", host, serv);  
  107.   
  108.     listen(listenfd, 5);  
  109.     if (addrlenp)  
  110.         *addrlenp = res->ai_addrlen;  
  111.   
  112.     freeaddrinfo(ressave);  
  113.   
  114.     return listenfd;  
  115. }  

 

客户端程序daytimetcpcli.c:

 

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
 
  1. #include <stdio.h>  
  2. #include <netdb.h>  
  3. #include <sys/socket.h>  
  4. #define MAXLINE 1024  
  5.   
  6. int tcp_connect(const char *host, const char *serv);  
  7.   
  8. int main(int argc, char **argv)  
  9. {  
  10.     int     sockfd, n;  
  11.     char    recvline[MAXLINE + 1];  
  12.     socklen_t   len;  
  13.     struct  sockaddr_in cliaddr;  
  14.   
  15.     if (argc != 3){  
  16.         printf("argument should be 3\n");  
  17.         exit(1);  
  18.     }  
  19.     sockfd = tcp_connect(argv[1], argv[2]);  
  20.   
  21.     len = sizeof(cliaddr);  
  22.     getpeername(sockfd, (struct sockaddr *)&cliaddr, len);  
  23.     inet_ntop(AF_INET, &cliaddr.sin_addr, recvline, sizeof(recvline));  
  24.     printf("connect to %s\n", recvline);  
  25.   
  26.     while ((n = read(sockfd, recvline, MAXLINE)) > 0){  
  27.         recvline[n] = 0;  
  28.         fputs(recvline, stdout);  
  29.     }  
  30.   
  31.     exit(0);  
  32. }  
  33.   
  34. int tcp_connect(const char *host, const char *serv)  
  35. {  
  36.     int     sockfd, n;  
  37.     struct  addrinfo hints, *res, *ressave;  
  38.     struct  sockaddr_in *cliaddr;  
  39.   
  40.     bzero(&hints, sizeof(struct addrinfo));  
  41.     hints.ai_family = AF_UNSPEC;  
  42.     hints.ai_socktype = SOCK_STREAM;  
  43.   
  44.     if ((n = getaddrinfo(host, serv, &hints, &res)) != 0){  
  45.         printf("tcp_connect error for %s,%s:%s\n", host, serv, gai_strerror(n));  
  46.         exit(1);  
  47.     }  
  48.   
  49.     ressave = res;  
  50.     do{  
  51.         sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);  
  52.         if (sockfd < 0)  
  53.             continue;  
  54.         if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)  
  55.             break;  
  56.   
  57.         cliaddr = (struct sockaddr_in *)res->ai_addr;  
  58.         close(sockfd);  
  59.     } while ((res = res->ai_next) != NULL);  
  60.   
  61.     if (res == NULL)  
  62.         printf("tcp_connect error for %s,%s\n", host, serv);  
  63.   
  64.     freeaddrinfo(ressave);  
  65.   
  66.     return sockfd;  
  67. }  



 

程序运行如下:

 

服务端:

 

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
 
  1. leichaojian@ThinkPad-T430i:~$ ./daytimetcpsrv ThinkPad-T430i 9878  

客户端:

 

 

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
 
  1. leichaojian@ThinkPad-T430i:~$ ./daytimetcpcli ThinkPad-T430i 9878  
  2. connect to 0.0.0.0  
  3. Wed Oct  8 21:07:35 2014  



然后我们查看/var/log/syslog这个文件,通过查找字符串“this is a test”,发现如下的语句:

 

 

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
 
  1. Oct  8 21:07:35 ThinkPad-T430i ./daytimetcpsrv[10528]: 127.0.0.1.this is a test  



 

 

2) 对daemon_init函数的分析

(1)fork

用于产生子进程

(2)setsid

setsid用于创建一个新的回话。当前进程变为新会话的会话头进程以及新进程组的进程组头进程,从而不再有控制终端。

(3)忽略SIGHUP信号并再次fork

忽略SIGHUP信号并再次调用fork。该函数返回时,父进程实际上是上一次调用fork产生的子进程,它被终止掉,留下新的子进程继续运行。再次fork的目的是确保本守护进程将来即使打开了一个终端设备,也不会自动获得控制终端。当没有控制终端的一个会话头进程打开一个终端设备时(该终端不会是当前某个其他会话的控制终端),该终端自动成为这个会话头进程的控制终端。然而再次调用fork之后,我们确保新的子进程不再是一个会话头进程,从而不能自动获得一个控制终端。这里必须霍略SIGHUP信号,因为当会话头进程(即首次fork产生的子进程)终止时,其会话中的所有进程(即再次fork产生的子进程)都收到SIGHUP信号。

(4)将stdin,stdout和stderr重定向到/dev/null

因为之前关闭了所有的描述符,所以要打开这三个基本描述符并且重定向,让read返回0,write系统调用丢弃所写的数据(书上说如果调用了syslog函数,则不要调用类似printf之类的函数,因为会被简单的忽略掉)。因为如果继续关闭,则万一有新的进程打开一个描述符,却占用了0,1,2这三个描述符,则可能导致将错误的数据发送给客户端。

 

3. inetd守护进程

    旧的服务器只是等待客户请求的到达,如FTP,Telnet,TFTP等。这些进程都是在系统自举阶段从/etc/rc文件中启动,而且每个进程执行几乎相同的启动任务:创建一个套接字,把本服务器的众所周知端口捆绑到该套接字,等待一个连接或一个数据报,然后派生子进程。子进程为客户提供服务,父进程则继续等待下一个客户请求。这个模型存在两个问题:

(1)所有这些守护进程含有几乎相同的启动代码,既表现在创建套接字上,也表现在演变成守护进程上(类似我们的daemon_init函数)

(2)每个守护进程在进程表中占据一个表项,然而它们大部分时间处于睡眠状态。

    而新版本的系统通过提供inetd守护进程(因特网超级服务器)来简化问题:

(1)通过inetd处理普通守护进程的大部分启动细节来简化守护进程的编写。这么一来每个服务器不再有调用daemon_init函数的必要。

(2)单个进程就能为多个服务等待外来的客户请求,以此取代每个服务一个进程的做法。这么做减少了系统中的进程总数。

1) inetd守护进程的工作流程

(0)对xinetd.conf文件的说明

 

字段 说明
service_name 必须在/etc/services文件中定义
socket_type stream(对于tcp)或dgram(对于udp)
protocol 必须在/etc/protocols文件中定义:tcp或udp
wait-falg 对于TCP一半为nowait,对于UDP一般为wait
login-name 来自/etc/passwd的用户名,一般为root
server-program 调用exec指定的完整路径名
server-program-arguments 调用exec指定的命令行参数

 

 

下面是xinetd.conf文件中的若干行:

 

ftp stream tcp nowait root /usr/bin/ftpd ftpd -l
telnet stream tcp nowait root /usr/bin/telnetd telnetd

 

(1)socket()

    在启动阶段,读入/etc/xinetd.conf文件并给该文件中指定的每个服务创建一个适当类型(字节流或数据报)的套接字。inetd能够处理的服务器的最大数目取决于inetd能够创建的描述符的最大数目。新创建的每个套接字都被加入到将由某个select调用使用的一个描述符集中。

(2)bind()

    为每个套接字调用bind,指定捆绑相应服务器的众所周知端口和通配地址。这个TCP或UDP端口号通过调用getservbyname获得,作为函数参数的是相应服务器在配置文件中的service-name字段和protocol字段。

(3)listen()

    对于每个TCP套接字,调用listen以接收外来的连接请求。对于数据报套接字则不执行本步骤

(4)select()等待可读条件

    创建完毕所有套接字之后,调用select等待其中任何一个套接字变为可读。TCP监听套接字将在有一个新连接准备好可被接受时变为可读,UDP套接字将在有一个数据报到达时变为可读。inetd的不部分时间花在阻塞于select调用内部,等待某个套接字变为可读。

(5)accept()

    当select返回指出某个套接字已可读之后,如果该套接字是一个TCP套接字,而且其服务器的wait-flag值为nowait,那就调用accept接受这个新连接。

(6)fork()

    inetd守护进程调用fork派生进程,并由子进程处理服务请求。子进程关闭要处理的套接字描述符之外的所有描述符:对于TCP服务器来说,这个套接字是由accept返回的新的已连接套接字,对于UDP服务器来说,这个套接字是父进程最初创建的UDP套接字。子进程dup2三次,把这个待处理套接字的描述符复制到描述符0,1和2,然后关闭原套接字描述符(由accept返回的已连接的TCP套接字)。

    子进程然后调用exec执行由相应的server-program字段指定的程序来具体处理请求,相应的server-program-arguments字段值则作为命令行参数传递给该程序。

    如果第五步中的select返回的是一个字节流套接字,那么父进程必须关闭已连接套接字(就像标准并发服务器那样)。父进程再次调用select,等待下一个变为可读的套接字。(因为TCP设置的nowait,意味着inetd不必等待某个子进程终止就可以接收对于该子进程所提供之服务的另一个连接。如果对于某个子进程所提供之服务的另一个连接确实在该子进程终止之前到达:accept返回,那么父进程再次调用select:意味着要关闭已连接的套接字,继续执行步骤4,5,6)

    给一个数据报服务指定wait标志导致父进程执行的步骤发生变化。这个标志要求inet必须在这个套接字再次称为slect调用的候选套接字之前等待当前服务该套接字的子进程终止。发生的变化有以下几点:

[1]fork返回到父进程时,父进程保存子进程的进程ID。这么做使得父进程能够通过查看由waitpid返回的值确定这个子进程的终止时间

[2]父进程通过使用FD_CLR宏关闭这个套接字在select所用描述符集中对应的位,达成在将来的select调用中禁止这个套接字的目的。这点意味着子进程将接管该套接字,直到自身终止为止。

[3]当子进程终止时,父进程被通知一个SIGCHLD信号,而父进程的信号处理函数将取得这个子进程的进程ID。父进程通过打开相应的套接字在select所用描述符集中对应的位,使得该套接字重新成为select的候选套接字。

 

2)inetd守护进程的服务器程序

 

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
 
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <sys/socket.h>  
  4. #include <string.h>  
  5. #include <signal.h>  
  6. #include <fcntl.h>  
  7. #include <unistd.h>  
  8. #include <time.h>  
  9. #include <netinet/in.h>  
  10.   
  11. #define MAXLINE 1024  
  12. int main(int argc, char **argv)  
  13. {  
  14.     socklen_t       len;  
  15.     struct sockaddr_in cliaddr;  
  16.     char            buff[MAXLINE];  
  17.     time_t          ticks;  
  18.   
  19.     openlog(argv[0], 0);  
  20.   
  21.     len = sizeof(cliaddr);  
  22.     getpeername(0, (struct sockaddr *)&cliaddr, &len);  
  23.     inet_ntop(AF_INET, (struct sockaddr *)&cliaddr.sin_addr, buff, sizeof(buff));  
  24.     printf("connect from %s\n", buff);  
  25.   
  26.     ticks = time(NULL);  
  27.     snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));  
  28.     write(0, buff, strlen(buff));  
  29.   
  30.     close(0);  
  31.     exit(0);  
  32. }  


在/etc/service中增加:

 

 

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
 
  1. mydaytime 9999/tcp  


在/etc/xinetd.conf中增加:

 

 

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
 
  1. mydaytime stream tcp nowait leichaojian /home/leichaojian/newdaytimetcpserv3 newdaytimetcpserv3  


程序输出:

 

 

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
 
    1. leichaojian@ThinkPad-T430i:~$ ./newdaytimetcpserv3  
    2. connect from 0.0.0.0  
    3. Fri Oct  3 14:27:31 2014  

要运行这个例子程序,

1.先要添加服务:做法,在/etc/services最后加上: mydaytime 9999/tcp

2.安装xinetd: sudo yum install xinetd

3.编辑配置:在/etc/xinetd.d/目录下新建一个mydaytime文件,内容如下:

service mydaytime
{
socket_type = stream
protocol = tcp
wait = no
user =root
server =/home/xpmo/unp/server

 其中server是例子程序的路径

4.重启xinetdsudo killall -HUP xinetd

5.查看启动成功否:

sudo netstat -tlp

如果看到mydaytime就说明启动成功了。

安装yum install telnet

 

相关文章
|
2月前
|
IDE 网络安全 开发工具
IDE之vscode:连接远程服务器代码(亲测OK),与pycharm链接服务器做对比(亲自使用过了),打开文件夹后切换文件夹。
本文介绍了如何使用VS Code通过Remote-SSH插件连接远程服务器进行代码开发,并与PyCharm进行了对比。作者认为VS Code在连接和配置多个服务器时更为简单,推荐使用VS Code。文章详细说明了VS Code的安装、远程插件安装、SSH配置文件编写、服务器连接以及如何在连接后切换文件夹。此外,还提供了使用密钥进行免密登录的方法和解决权限问题的步骤。
989 0
IDE之vscode:连接远程服务器代码(亲测OK),与pycharm链接服务器做对比(亲自使用过了),打开文件夹后切换文件夹。
|
2月前
|
IDE 网络安全 开发工具
IDE之pycharm:专业版本连接远程服务器代码,并配置远程python环境解释器(亲测OK)。
本文介绍了如何在PyCharm专业版中连接远程服务器并配置远程Python环境解释器,以便在服务器上运行代码。
478 0
IDE之pycharm:专业版本连接远程服务器代码,并配置远程python环境解释器(亲测OK)。
|
2月前
|
Linux C语言 C++
vsCode远程执行c和c++代码并操控linux服务器完整教程
这篇文章提供了一个完整的教程,介绍如何在Visual Studio Code中配置和使用插件来远程执行C和C++代码,并操控Linux服务器,包括安装VSCode、安装插件、配置插件、配置编译工具、升级glibc和编写代码进行调试的步骤。
390 0
vsCode远程执行c和c++代码并操控linux服务器完整教程
|
2月前
|
前端开发 Java
学习SpringMVC,建立连接,请求,响应 SpringBoot初学,如何前后端交互(后端版)?最简单的能通过网址访问的后端服务器代码举例
文章介绍了如何使用SpringBoot创建简单的后端服务器来处理HTTP请求,包括建立连接、编写Controller处理请求,并返回响应给前端或网址。
61 0
学习SpringMVC,建立连接,请求,响应 SpringBoot初学,如何前后端交互(后端版)?最简单的能通过网址访问的后端服务器代码举例
|
3月前
|
设计模式 数据库连接 PHP
PHP中的设计模式:如何提高代码的可维护性与扩展性在软件开发领域,PHP 是一种广泛使用的服务器端脚本语言。随着项目规模的扩大和复杂性的增加,保持代码的可维护性和可扩展性变得越来越重要。本文将探讨 PHP 中的设计模式,并通过实例展示如何应用这些模式来提高代码质量。
设计模式是经过验证的解决软件设计问题的方法。它们不是具体的代码,而是一种编码和设计经验的总结。在PHP开发中,合理地使用设计模式可以显著提高代码的可维护性、复用性和扩展性。本文将介绍几种常见的设计模式,包括单例模式、工厂模式和观察者模式,并通过具体的例子展示如何在PHP项目中应用这些模式。
|
3月前
|
数据安全/隐私保护
Haskell网络编程:代理服务器的高级使用技巧
Haskell网络编程:代理服务器的高级使用技巧
|
3月前
|
网络安全 开发工具 云计算
服务器看代码阿里云
随着云计算技术的发展,阿里云作为国内领先的云计算服务提供商,其服务器受到广大用户青睐。本文主要介绍如何在阿里云服务器上便捷地查看与管理代码,如使用SSH连接服务器并通过命令行工具打开文件,以及利用Git进行版本控制和协作开发,提高代码管理效率。无论个人开发者还是企业团队,都能借助阿里云服务器高效地部署与管理应用程序,提升工作效率及产品质量。
82 10
|
3月前
|
存储 Linux 网络安全
存放位置阿里云服务器代码的
阿里云服务器提供虚拟化计算、存储与网络服务。创建服务器时,可基于不同需求选择代码存放位置:文件系统支持直接通过SSH访问与编辑;公共目录如 `/var/www/html` 适合Web应用;对象存储OSS适用于大数据处理;代码托管服务如 GitLab 则利于版本控制与团队协作。合理选择有助于提升开发效率。
78 7
|
3月前
|
开发者
HTTP状态码是由网页服务器返回的三位数字响应代码,用于表示请求的处理结果和状态
HTTP状态码是由网页服务器返回的三位数字响应代码,用于表示请求的处理结果和状态
42 1
|
3月前
|
网络安全 云计算
阿里云服务器代码
阿里云作为中国领先的云计算服务提供商,为用户提供了丰富的云服务器实例。本文详细介绍如何在阿里云上定位服务器代码,包括利用控制台搜索实例并访问详细页面查找相关信息,使用`ssh`和`cat`等命令行工具远程连接及读取文件内容,以及在遇到困难时及时联系阿里云技术支持获取帮助的具体方法。无论您的实例类型与操作系统有何不同,总有一种方式能帮您顺利找到所需的服务器代码。
38 3

热门文章

最新文章