热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

假脱机打印程序与虚拟设备

一、 设计目的 理解虚拟设备的工作原理,理解守护程序的概念。图7-1表示假脱机打印程序的工作原理。    在网络环境下,连在网络服务器上的打印机要为多

一、 设计目的

 理解虚拟设备的工作原理,理解守护程序的概念。

7-1表示假脱机打印程序的工作原理。

 

 

 

 

在网络环境下,连在网络服务器上的打印机要为多个终端服务,每个终端上的用户都可以通过客户端程序向服务器发送打印请求,服务器端的打印请求接收程序接收来自客户端的打印请求,并将该请求存放到磁盘上的打印请求队列中,由服务器端的假脱机打印程序在CPU空闲时从打印请求队列中取出请求信息,并将文件输出到打印机中。这种工作方式不是将文件直接输出到打印机,而是先将待打印的文件缓存到磁盘上,然后立即返回用户程序,从而缩短了用户响应时间,为用户提供了虚拟的快速打印机。这里的磁盘缓存空间就是虚拟设备。服务器端的打印请求接收程序和打印程序都是守护程序,即从开机后就一直运行的程序。

 

二、 设计要求

  利用多线程技术编写假脱机打印程序,并设计测试数据以验证程序的正确性。

1、界面要求:

程序采用简单的控制台界面,运行后在屏幕上显示功能菜单,列出该程序具有的功能,供用户选择。

2、功能要求:

1)发送打印请求;

2)查看假脱机打印队列;

3)打印文件;

4)退出。

用户选择功能后应该转到相应的处理程序,并在需要时显示程序的执行结果。

若用户选择(1)则提示用户输入待打印的文件名称,程序接收输入后将打印请求传送到打印队列中,并回到主菜单;

若用户选择(2)则在屏幕上列出打印队列情况,提示按任意键回到主界面;

若用户选择(3)则打印队首的文件,显示所打印的文件名称,按任意键回到主界面;

若用户选择(4)则退出程序的执行。

 

三、 算法设计与分析



  1. 程序结构设计

 

















字段名称


作用


file_name


文件名称


file_size


文件大小(以KB为单位)


 

需要两个数据结构,一个FILE_INFO用来描述打印请求,包括文件名称和文件大小,如图7-2所示。另一个数据结构SPOOL用来描述打印请求队列,如图7-3所示。图7-4描述了程序运行时刻结构体SPOOL的内容。

 

























字段名称


作用


spool_count


记录打印队列中的文件个数


spool_in


记录下一个打印请求存放的位置


spool_out


记录下一个被打印文件的位置


spool_queue


打印请求队列(用数组实现)


 

 













































spool_count


3


 


spool_in


3


 


spool_out


0


 


spool_queue[0]


sever


4KB


spool_queue[1]


client


3KB


spool_queue[2]


myfile


7KB


spool_queue[3]


 


 


spool_queue[4]


 


 

7.4  程序结构

1、线程划分

为了模拟假脱机打印程序,需要三个线程,主线程用于显示主菜单,接收用户的功能选择,显示打印队列情况;打印请求接收/发送线程接收用户的打印请求,并将打印请求存放到打印请求队列;打印线程用来从打印队列中取文件并将其输出到屏幕。

2、线程互斥要求

三个线程都需要通过控制台终端与用户交互,因此对终端的使用要互斥,以免屏幕混乱(用互斥体h_screen_mutex实现);三个线程都要访问打印请求队列,因此对它要进行互斥操作(用互斥体h_spool_mutex实现);

3、同步要求

主线程与打印请求接收/发送线程要同步,当用户选择功能(1)时,主线程要通知打印请求接收/发送线程开始接收用户请求(用初始值为0的信号量h_print实现),然后主线程要等待打印请求接收/发送线程发来接收完毕的信号(用初始值为0的信号量h_sendthread_to_mainthread实现);

主线程要与打印线程同步,当用户选择功能(3)时,主线程要通知打印线程开始打印文件(用初始值为0的信号量h_semaphore_spool实现),然后主线程要等待打印线程发来打印完毕的信号(用初始值为0的信号量h_spoolthread_to_mainthread实现);

(注意:在实际的系统中,打印线程不会等待用户从控制台发命令才开始打印,而是只要有打印请求且CPU空闲就循环不停地打印文件,直至打印队列为空,这时打印线程睡眠。本设计这样做是为了避免打印队列迅速变空。)

请求接收/发送线程和打印线程要同步,当打印队列为空时打印线程阻塞,直到请求接收/发送线程将新的请求放入队列;当打印队列满时,请求接收/发送线程阻塞,直到打印线程打印完一个文件空出新位置才能将其唤醒。这两个线程的同步遵循生产者/消费者模型,同步信号量为h_spool_empty和h_spool_full,前者跟踪空位置,后者跟踪打印请求。

4、函数设计

为了简化程序设计,我们只考虑单机环境,而且将打印请求队列存放在内存中,而不是存放在磁盘中,另外用屏幕输出模拟实际的打印机输出。该程序包括三个线程:主线程模拟客户端程序,sendthread线程模拟打印请求接收程序,spool_thread线程模拟打印程序。

该程序使用以下全局变量:

spool_buffer是打印请求队列,互斥体h_spool_mutex用来实现对spool_buffer的互斥访问,互斥体h_screen_mutex用来实现对终端的互斥访问,h_send和h_spool_thread分别是打印请求接收/发送线程和打印线程的句柄,h_semaphore_spool和h_spoolthread_to_mainthread是主线程和打印线程之间的同步信号量,h_print和h_sendthread_to_mainthread是主线程和打印请求接收/发送线程之间的同步信号量,h_spool_full和h_spool_empty是打印线程和打印请求接收/发送线程之间的同步信号量。

该程序共有五个函数,它们的名称及作用如图7-5所示。
































函数名称


作用


sendthread


接收用户的打印请求并将其发送到打印请求队列中


spool_thread


从打印请求队列中取待打印文件并将其输出到屏幕上


print_space


显示若干个空格


list_spool_queue


列出打印队列


main


创建线程,初始化信号量,显示主菜单,根据用户选择执行相应功能


7-5  假脱机打印程序包括的函数及其作用


下面给出每个函数的算法描述。

1)list_spool_queue函数

{

申请打印队列互斥使用权P(h_spool_mutex);

申请屏幕互斥使用权P(h_screen_mutex);

清屏;

显示打印队列中当前请求个数;

显示表头;

对请求队列中的每一个请求

{在屏幕上输出待打印文件名称以及大小};

释放打印队列互斥使用权V(h_spool_mutex);

释放屏幕互斥使用权V(h_screen_mutex);

}

2)sendthread函数

{

  while(1){

    等待主线程发送唤醒信号直到用户选择“发送打印请求功能”P(h_print);

    申请屏幕互斥使用权P(h_screen_mutex);

清屏;

    提示并接收用户输入文件名称;

    释放屏幕互斥使用权V(h_screen_mutex);

    产生随机数作为文件大小;

    申请打印队列中的空闲位置P(h_spool_empty);

申请打印队列互斥使用权P(h_spool_mutex);

将请求放入打印队列当前位置;

调整位置指针至下一位置;

释放打印队列互斥使用权V(h_spool_mutex);

通知打印线程多了一个打印请求V(h_spool_full);

唤醒主线程继续画主菜单V(h_sendthread_to_mainthread);

}

}

3)spool_thread函数

{

  while(1){

    等待主线程发送唤醒信号直到用户选择“打印文件功能”P(h_semaphore_spool);

    等待直到sendthread线程发来打印请求P(h_spool_full);

    申请打印队列互斥使用权P(h_spool_mutex);

    申请屏幕互斥使用权P(h_screen_mutex);

    将打印队列中的文件数减1;

    在屏幕上输出正在打印的文件名称;

    回收该位置(置文件名称为空白,文件大小为0);

    下调打印指针;

释放屏幕互斥使用权V(h_screen_mutex);

释放打印队列互斥使用权V(h_spool_mutex);

通知sendthread线程多了一个空位置V(h_spool_empty);

唤醒主线程继续画主菜单V(h_spoolthread_to_mainthread);

  }

}

4)main函数

{

创建两个互斥体;

创建六个同步信号量;

创建两个线程;

while(1){

申请屏幕互斥使用权P(h_screen_mutex);

显示主菜单;

接收用户功能选择;

清屏;

释放屏幕互斥使用权V(h_screen_mutex);

根据功能选择进行分支{

选择了功能1:

唤醒发送线程允许其发送请求到打印队列V(h_print);

等待发送线程发送完打印请求P(h_sendthread_to_mainthread);

跳出循环;

选择了功能2:

显示打印队列;

跳出循环;

选择了功能3:

唤醒打印线程允许其打印文件V(h_semaphore_spool);

等待打印线程打印完文件P(h_spoolthread_to_mainthread);

跳出循环;

选择了功能4:

返回;

}

}

}

 

 

然后就直接放代码好了

1 #include
2 #include
3 #include
4 #include
5 #include <string.h>
6 #define SIZE rand()%1000//取0-999中的一个随机数
7 typedef struct{
8 char file_name[100];//文件名称
9 int file_size; //文件大小
10 int v; //排队序号
11 }FILE_INFO; //文件结构体
12
13 typedef struct{
14 int spool_count; //队列中文件的个数
15 int spool_in; //下一个打印请求存放的位置
16 int spool_out; //下一个被打印文件的位置
17 FILE_INFO spool_queue[5];//打印请求队列
18 }SPOOL; //打印请求队列
19
20 SPOOL spool_buffer; //打印请求队列
21 HANDLE h_spool_mutex; //线程互斥 (三个线程都要访问打印请求队列,实现对spool_buffer的互斥访问)
22 HANDLE h_screen_mutex; //屏幕互斥 (对终端的互斥访问;避免屏幕混乱)
23
24 HANDLE h_send; //申明打印请求接收/发送线程
25 HANDLE h_spool_thread; //申明打印线程
26
27 HANDLE h_semaphore_spool; //主线程通知打印线程开始打印文件(主线程同步信号量)
28 HANDLE h_spoolthread_to_mainthread;//等待打印线程发来打印结束的信号量(打印线程同步信号量)
29
30 HANDLE h_sendthread_to_mainthread; //等待打印请求接收和发送线程结束的信号量
31 HANDLE h_print; //主线程要通知打印请求接收和发送线程开始接收用户请求的信号量
32
33 HANDLE h_spool_full; //打印请求个数
34 HANDLE h_spool_empty; //空位置
35
36 //接收用户的打印请求并将其发送到打印请求队列中
37 DWORD WINAPI sendthread(LPVOID lpParameter)
38 {
39 FILE_INFO file_info;
40 while(1)
41 {
42 WaitForSingleObject(h_print,INFINITE); //等待主线程发送唤醒信号直到用户选择“发送打印请求功能”
43 WaitForSingleObject(h_screen_mutex,INFINITE); // 申请屏幕互斥使用权
44 printf("输入文件名:"); //提示并接收用户输入文件名称
45 scanf("%s",file_info.file_name);
46 ReleaseMutex(h_screen_mutex); // 释放屏幕互斥使用权
47 srand( (unsigned)time( NULL ) ); //产生随机数作为文件大小
48 file_info.file_size=SIZE;
49 printf("文件大小为:%d\n",file_info.file_size);
50 WaitForSingleObject(h_spool_empty,INFINITE); //申请打印队列中的空闲位置
51 WaitForSingleObject(h_spool_mutex,INFINITE); //申请打印队列互斥使用权
52 spool_buffer.spool_count++;
53 file_info.v=spool_buffer.spool_count;
54 spool_buffer.spool_queue[spool_buffer.spool_in]=file_info; //将请求放入打印队列当前位置
55 spool_buffer.spool_in=(spool_buffer.spool_in+1)%5; //调整位置指针至下一位置
56 ReleaseMutex(h_spool_mutex); //释放打印队列互斥使用权
57 ReleaseSemaphore(h_spool_full,1,NULL); //通知打印线程多了一个打印请求
58 ReleaseSemaphore(h_sendthread_to_mainthread,1,NULL); //唤醒主线程继续画主菜单
59 }
60 }
61
62
63 //输出空格
64 void print_space(int num){
65 int i;
66 for(i=0;i){
67 printf(" ");
68 }
69 }
70
71 //列出打印队列
72 void list_spool_queue()
73 {
74 char buffer[10];//存一下转换后的数据,字符型形式比较灵活?
75 WaitForSingleObject(h_spool_mutex,INFINITE); //结束阻塞状态,申请打印队列互斥使用权
76 WaitForSingleObject(h_screen_mutex,INFINITE); //申请屏幕互斥使用权
77 // system("cls"); 终端弹出所以清屏用不了
78 //显示表头
79 printf(" 假脱机队列中的文件数:%d\n\n",spool_buffer.spool_count);
80 printf(" 打印序列 \n");
81 printf("|--------|-------------------------------------|--------------|--------------|\n");
82 printf("| 序号 | 文件名 | 文件大小(KB) | 排队序号 |\n");
83 printf("|--------|-------------------------------------|--------------|--------------|\n");
84 for(int i=0;i<5;i++)
85 {
86 printf("| %d",i);
87 itoa(i, buffer, 10);//整型数字变量转换成字符数组变量
88 print_space(7-strlen(buffer));
89 printf("| %s",spool_buffer.spool_queue[i].file_name);
90 print_space(36-strlen(spool_buffer.spool_queue[i].file_name));
91 printf("| %d",spool_buffer.spool_queue[i].file_size);
92 itoa(spool_buffer.spool_queue[i].file_size,buffer,10);
93 print_space(12-strlen(buffer));
94 printf("| %d",spool_buffer.spool_queue[i].v);
95 itoa(spool_buffer.spool_queue[i].v,buffer,10);
96 print_space(12-strlen(buffer));
97 printf(" |\n");
98 }
99 printf("|--------|-------------------------------------|--------------|--------------|\n");
100
101 ReleaseMutex(h_spool_mutex); //释放打印队列互斥使用权V(h_spool_mutex);
102 ReleaseMutex(h_screen_mutex); //释放屏幕互斥使用权V(h_screen_mutex);
103 }
104
105
106 //从打印请求队列中取待打印文件并将其输出到屏幕上
107 DWORD WINAPI spool_thread(LPVOID lpParameter)
108 {
109 while(1)
110 {
111 WaitForSingleObject(h_semaphore_spool,INFINITE); //等待主线程发送唤醒信号直到用户选择“打印文件功能”
112 WaitForSingleObject(h_spool_full,INFINITE); //等待直到sendthread线程发来打印请求
113 WaitForSingleObject(h_spool_mutex,INFINITE); //申请打印队列互斥使用权
114 WaitForSingleObject(h_screen_mutex,INFINITE); //申请屏幕互斥使用权
115
116 spool_buffer.spool_count--; //将打印队列中的文件数减1
117 printf("打印一个文件:\n文件名:%s 文件大小:%d\n",spool_buffer.spool_queue[spool_buffer.spool_out].file_name,spool_buffer.spool_queue[spool_buffer.spool_out].file_size);
118 //在屏幕上输出正在打印的文件名称
119 strcpy(spool_buffer.spool_queue[spool_buffer.spool_out].file_name,"");
120 spool_buffer.spool_queue[spool_buffer.spool_out].file_size=0; // 回收该位置(置文件名称为空白,文件大小为0)
121 spool_buffer.spool_queue[spool_buffer.spool_out].v=0; //下调打印指针
122 for(int i=0;i<5;i++)
123 if(spool_buffer.spool_queue[i].v>0)
124 spool_buffer.spool_queue[i].v--;
125 spool_buffer.spool_out=(spool_buffer.spool_out+1)%5; //寻找下一个要打印文件的位置
126 ReleaseMutex(h_screen_mutex); //释放屏幕互斥使用权
127 ReleaseMutex(h_spool_mutex); //释放打印队列互斥使用权
128 ReleaseSemaphore(h_spool_empty,1,NULL); //通知sendthread线程多了一个空位置
129 ReleaseSemaphore(h_spoolthread_to_mainthread,1,NULL); //唤醒主线程继续画主菜单
130 }
131 return 0;
132 }
133
134 //主函数
135 int main(){
136 char select;
137 h_send=CreateThread(NULL,0,sendthread,NULL,0,NULL); //创建线程
138
139 h_spool_thread=CreateThread(NULL,0,spool_thread,NULL,0,NULL);
140 h_spool_mutex=CreateMutex(NULL,FALSE,NULL); //创建两个互斥体
141
142 h_screen_mutex=CreateMutex(NULL,FALSE,NULL);
143 h_spool_full=CreateSemaphore(NULL,0,5,NULL); //打印线程和打印请求接收/发送线程之间的同步信号量
144
145 h_spool_empty=CreateSemaphore(NULL,5,5,NULL);
146 h_print=CreateSemaphore(NULL,0,1,NULL); //主线程和打印请求接收/发送线程之间的同步信号量
147
148 h_sendthread_to_mainthread=CreateSemaphore(NULL,0,1,NULL);
149 h_semaphore_spool=CreateSemaphore(NULL,0,1,NULL); //主线程和打印线程之间的同步信号量
150
151 h_spoolthread_to_mainthread=CreateSemaphore(NULL,0,1,NULL);
152 WaitForSingleObject(h_screen_mutex,INFINITE); //申请屏幕互斥使用权
153
154 while(1){
155 printf("|-----------------------------------|\n");
156 printf("| (1):发送打印请求 |\n");
157 printf("| (2):查看假脱机打印队列 |\n");
158 printf("| (3):打印文件 |\n");
159 printf("| (4):退出 |\n");
160 printf("|-----------------------------------|\n");
161 printf("| 输入选择功能:");
162 do{
163 scanf("%c",&select);
164 }while(select!='1'&&select!='2'&&select!='3'&&select!='4');
165 //system("cls");
166 ReleaseMutex(h_screen_mutex);
167 switch(select){
168 case '1':
169 if(spool_buffer.spool_count<5){
170 ReleaseSemaphore(h_print,1,NULL); //唤醒发送线程允许其发送请求到打印队列V(h_print);
171 WaitForSingleObject(h_sendthread_to_mainthread,INFINITE); //等待发送线程发送完打印请求P(h_sendthread_to_mainthread);
172 }
173 else
174 printf("当前打印队列已满!\n");
175 break;
176
177 case '2':
178 if(spool_buffer.spool_count==0)
179 printf("当前打印队列为空!\n");
180 else
181 list_spool_queue(); //全部显示
182 break;
183
184 case '3':
185 if(spool_buffer.spool_count>0){
186 ReleaseSemaphore(h_semaphore_spool,1,NULL); //唤醒打印线程允许其打印文件V(h_semaphore_spool);
187 WaitForSingleObject(h_spoolthread_to_mainthread,INFINITE); //等待打印线程打印完文件P(h_spoolthread_to_mainthread);
188 }
189 else
190 printf("当前打印队列为空!\n");
191 break;
192
193 case '4':
194 return 0;
195 }
196 WaitForSingleObject(h_screen_mutex,INFINITE);
197 printf("\n按任意键回到菜单\n");
198 getch();
199 //system("cls");
200 ReleaseMutex(h_screen_mutex);
201 }
202 return 0;
203 }

由于实在是太菜了,所以就写了一大堆注释。

也不一定完全对,做个参考吧



推荐阅读
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • PHP设置MySQL字符集的方法及使用mysqli_set_charset函数
    本文介绍了PHP设置MySQL字符集的方法,详细介绍了使用mysqli_set_charset函数来规定与数据库服务器进行数据传送时要使用的字符集。通过示例代码演示了如何设置默认客户端字符集。 ... [详细]
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • Centos7.6安装Gitlab教程及注意事项
    本文介绍了在Centos7.6系统下安装Gitlab的详细教程,并提供了一些注意事项。教程包括查看系统版本、安装必要的软件包、配置防火墙等步骤。同时,还强调了使用阿里云服务器时的特殊配置需求,以及建议至少4GB的可用RAM来运行GitLab。 ... [详细]
  • 本文介绍了在Hibernate配置lazy=false时无法加载数据的问题,通过采用OpenSessionInView模式和修改数据库服务器版本解决了该问题。详细描述了问题的出现和解决过程,包括运行环境和数据库的配置信息。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • 本文介绍了如何找到并终止在8080端口上运行的进程的方法,通过使用终端命令lsof -i :8080可以获取在该端口上运行的所有进程的输出,并使用kill命令终止指定进程的运行。 ... [详细]
  • 禁止程序接收鼠标事件的工具_VNC Viewer for Mac(远程桌面工具)免费版
    VNCViewerforMac是一款运行在Mac平台上的远程桌面工具,vncviewermac版可以帮助您使用Mac的键盘和鼠标来控制远程计算机,操作简 ... [详细]
  • 本文详细介绍了云服务器API接口的概念和作用,以及如何使用API接口管理云上资源和开发应用程序。通过创建实例API、调整实例配置API、关闭实例API和退还实例API等功能,可以实现云服务器的创建、配置修改和销毁等操作。对于想要学习云服务器API接口的人来说,本文提供了详细的入门指南和使用方法。如果想进一步了解相关知识或阅读更多相关文章,请关注编程笔记行业资讯频道。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • 如何基于ggplot2构建相关系数矩阵热图以及一个友情故事
    本文介绍了如何在rstudio中安装ggplot2,并使用ggplot2构建相关系数矩阵热图。同时,通过一个友情故事,讲述了真爱难觅的故事背后的数据量化和皮尔逊相关系数的概念。故事中的小伙伴们在本科时参加各种考试,其中有些沉迷网络游戏,有些热爱体育,通过他们的故事,展示了不同兴趣和特长对学习和成绩的影响。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 本文介绍了使用PHP实现断点续传乱序合并文件的方法和源码。由于网络原因,文件需要分割成多个部分发送,因此无法按顺序接收。文章中提供了merge2.php的源码,通过使用shuffle函数打乱文件读取顺序,实现了乱序合并文件的功能。同时,还介绍了filesize、glob、unlink、fopen等相关函数的使用。阅读本文可以了解如何使用PHP实现断点续传乱序合并文件的具体步骤。 ... [详细]
author-avatar
14795823364-
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有