Linux网络编程:多进程 多线程_并发服务器

文章目录:

一:wrap常用函数封装

wrap.h 

wrap.c

server.c封装实现

client.c封装实现

二:多进程process并发服务器

server.c服务器

实现思路

代码逻辑 

client.c客户端

三:多线程thread并发服务器

server.c服务器

实现思路

代码逻辑 

client.c客户端
​​​​


  read 函数的返回值

read 函数的返回值:

	1. > 0 实际读到的字节数

	2. = 0 已经读到结尾(对端已经关闭)【 !重 !点 !】

	3. -1 应进一步判断errno的值:

		errno = EAGAIN or EWOULDBLOCK: 设置了非阻塞方式 读。 没有数据到达。 

		errno = EINTR 慢速系统调用被 中断。

		errno = “其他情况” 异常。

一:wrap常用函数封装

wrap.h 

//声明了一些网络编程中常用的函数

//所有的函数都放在条件编译的代码块中,这样只有在编译时定义了 __WRAP_H_ 这个宏
	#ifndef __WRAP_H_
	#define __WRAP_H_


	//perr_exit:用来处理错误并退出程序
		void perr_exit(const char *s);
	
	//Accept:用于接受一个TCP连接请求
	//它接受一个文件描述符 fd,一个指向 struct sockaddr 结构的指针 sa,以及一个指向 socklen_t 类型的指针 salenptr
		int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
	
	//Bind:用于将一个网络地址(包括IP地址和端口号)绑定到一个文件描述符上
	//它接受一个文件描述符 fd,一个指向 struct sockaddr 结构的指针 sa,以及 socklen_t 类型的 salen
		int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
	
	//Connect:用于建立与远程主机的TCP连接
	//它接受一个文件描述符 fd,一个指向 struct sockaddr 结构的指针 sa,以及 socklen_t 类型的 salen
		int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
	
	//Listen:用于在服务器端创建一个TCP监听队列
	//它接受一个文件描述符 fd 和一个指定的最大连接等待数 backlog
		int Listen(int fd, int backlog);
	
	//Socket:用于创建一个新的套接字
	//它接受一个地址族 family,一个套接字类型 type,以及一个协议编号 protocol
		int Socket(int family, int type, int protocol);
	
	//Read:用于从文件中读取数据
	//它接受一个文件描述符 fd,一个指向要读取数据的缓冲区的指针 ptr,以及要读取的最大字节数 nbytes
		ssize_t Read(int fd, void *ptr, size_t nbytes);
	
	//Write:用于将数据写入文件
	//它接受一个文件描述符 fd,一个指向要写入数据的缓冲区的指针 ptr,以及要写入的字节数 nbytes
		ssize_t Write(int fd, const void *ptr, size_t nbytes);
	
	//Close:用于关闭文件
	//它接受一个文件描述符 fd
		int Close(int fd);
	
	//Readn:用来读取指定数量的字节
	//它接受一个文件描述符 fd,一个指向要读取数据的缓冲区的指针 vptr,以及要读取的字节数 n
		ssize_t Readn(int fd, void *vptr, size_t n);
	
	//Writen:用来写入指定数量的字节
	//它接受一个文件描述符 fd,一个指向要写入数据的缓冲区的指针 vptr,以及要写入的字节数 n
		ssize_t Writen(int fd, const void *vptr, size_t n);
	
	//my_read:用来读取数据到一个字符数组中
	//它接受一个文件描述符 fd,一个指向要读取数据的缓冲区的指针 ptr
		ssize_t my_read(int fd, char *ptr);
	
	//Readline:用来读取一行数据
	//它接受一个文件描述符 fd,一个指向要读取数据的缓冲区的指针 vptr,以及要读取的最大字节数 maxlen
		ssize_t Readline(int fd, void *vptr, size_t maxlen);


	#endif

wrap.c

//这个头文件包含了标准库的函数和变量,包括用于内存分配、输入/输出、错误处理等功能的函数和变量
	#include <stdlib.h>		
//这个头文件包含了标准输入/输出库的函数和变量,包括用于文件操作、标准输入/输出等功能的函数和变量	
	#include <stdio.h>	
//这个头文件包含了用于Unix和类Unix系统中的函数和变量,包括与进程控制、系统调用等相关的函数和变量		
	#include <unistd.h>			
//这个头文件包含了用于错误处理的函数和变量,包括用于表示错误码的宏和全局变量
	#include <errno.h>			
//这个头文件包含了用于网络编程的函数和变量,包括用于创建、操作套接字等功能的函数和变量
	#include <sys/socket.h>		
 
 
 
//perr_exit:用来处理错误并退出程序
	void perr_exit(const char *s)
	{
		perror(s);
		exit(-1);
	}
	 
//Accept:用于接受一个TCP连接请求	 
	int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
	{
		int n;
		 
		again:
			if ((n = accept(fd, sa, salenptr)) < 0) {
				if ((errno == ECONNABORTED) || (errno == EINTR))
					goto again;
				else
					perr_exit("accept error");
			}
			return n;
	}
	 
//Bind:用于将一个网络地址(包括IP地址和端口号)绑定到一个文件描述符上	 
	int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
	{
		int n;
	 
		if ((n = bind(fd, sa, salen)) < 0)
			perr_exit("bind error");
	 
		return n;
	}
	 
//Connect:用于建立与远程主机的TCP连接	 
	int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
	{
		int n;
		n = connect(fd, sa, salen);
		if (n < 0) {
			perr_exit("connect error");
		}
	 
		return n;
	}
	 
//Listen:用于在服务器端创建一个TCP监听队列	 
	int Listen(int fd, int backlog)
	{
		int n;
	 
		if ((n = listen(fd, backlog)) < 0)
			perr_exit("listen error");
	 
		return n;
	}
	 
//Socket:用于创建一个新的套接字	 
	int Socket(int family, int type, int protocol)
	{
		int n;
	 
		if ((n = socket(family, type, protocol)) < 0)
			perr_exit("socket error");
	 
		return n;
	}
	 
//Read:用于从文件中读取数据	 
	ssize_t Read(int fd, void *ptr, size_t nbytes)
	{
		ssize_t n;
		 
		again:
			if ( (n = read(fd, ptr, nbytes)) == -1) {
				if (errno == EINTR)
					goto again;
				else
					return -1;
			}
		 
			return n;
	}
	 
//Write:用于将数据写入文件	 
	ssize_t Write(int fd, const void *ptr, size_t nbytes)
	{
		ssize_t n;
		 
		again:
			if ((n = write(fd, ptr, nbytes)) == -1) {
				if (errno == EINTR)
					goto again;
				else
					return -1;
			}
			return n;
	}
	 
//Close:用于关闭文件	 
	int Close(int fd)
	{
		int n;
		if ((n = close(fd)) == -1)
			perr_exit("close error");
	 
		return n;
	}
	 
//Readn:用来读取指定数量的字节	 
	/*参三: 应该读取的字节数  读 N 个字节*/                          //socket 4096  readn(cfd, buf, 4096)   nleft = 4096-1500
	ssize_t Readn(int fd, void *vptr, size_t n)
	{
		size_t  nleft;              //usigned int 剩余未读取的字节数
		ssize_t nread;              //int 实际读到的字节数
		char   *ptr;
	 
		ptr = vptr;
		nleft = n;                  //n 未读取字节数
	 
		while (nleft > 0) {
			if ((nread = read(fd, ptr, nleft)) < 0) {
				if (errno == EINTR)
					nread = 0;
				else
					return -1;
			} else if (nread == 0)
				break;
	 
			nleft -= nread;   //nleft = nleft - nread 
			ptr += nread;
		}
		return n - nleft;
	}
	 
//Writen:用来写入指定数量的字节	 
	ssize_t Writen(int fd, const void *vptr, size_t n)
	{
		size_t nleft;
		ssize_t nwritten;
		const char *ptr;
	 
		ptr = vptr;
		nleft = n;
		while (nleft > 0) {
			if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
				if (nwritten < 0 && errno == EINTR)
					nwritten = 0;
				else
					return -1;
			}
			nleft -= nwritten;
			ptr += nwritten;
		}
		return n;
	}
	 
//my_read:用来读取数据到一个字符数组中	 
	static ssize_t my_read(int fd, char *ptr)
	{
		static int read_cnt;
		static char *read_ptr;
		static char read_buf[100];
	 
		if (read_cnt <= 0) {
		again:
				if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {   //"hello\n"
					if (errno == EINTR)
						goto again;
					return -1;
				} else if (read_cnt == 0)
					return 0;
		 
				read_ptr = read_buf;
			}
			read_cnt--;
			*ptr = *read_ptr++;
		 
			return 1;
	}
	 
//Readline:用来读取一行数据	 
	/*readline读一行 --- fgets*/    
	//传出参数 vptr
	ssize_t Readline(int fd, void *vptr, size_t maxlen)
	{
		ssize_t n, rc;
		char    c, *ptr;
		ptr = vptr;
	 
		for (n = 1; n < maxlen; n++) {
			if ((rc = my_read(fd, &c)) == 1) {   //ptr[] = hello\n
				*ptr++ = c;
				if (c == '\n')
					break;
			} else if (rc == 0) {
				*ptr = 0;
				return n-1;
			} else
				return -1;
		}
		*ptr = 0;
	 
		return n;
}
 

利用封装函数: 联合编译server.c 和 wrap.c 生成 server、 联合编译 client.c 和 wrap.c 生成 client

server.c封装实现

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
 
#include "wrap.h"
 
#define SERV_PORT 6666
 
int main(void)
{
    int sfd, cfd;
    int len, i;
    char buf[BUFSIZ], clie_IP[BUFSIZ];
 
    struct sockaddr_in serv_addr, clie_addr;
    socklen_t clie_addr_len;
 
 
 
 
	/*1.socket函数:创建用于建立连接的socket,返回的文件描述符存入link_fd*/
		sfd = Socket(AF_INET, SOCK_STREAM, 0);
 
		int opt = 1;
		//设置套接字选项:文件描述符,要设置的选项是套接字的选项,要设置的选项是“重用地址”选项,包含了要设置的选项的值,选项值的长度
		setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	 
		bzero(&serv_addr, sizeof(serv_addr));           	 //将指定内存区域的内容初始化为0	
		serv_addr.sin_family = AF_INET;        				 //IPv4         
		serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  	 //获取本机任意有效IP
		serv_addr.sin_port = htons(SERV_PORT);          	 //转为网络字节序的 端口号
 
	/*2.bind函数:绑定服务器端的socket绑定地址结构(IP+port)*/
		Bind(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
 
	/*3.listen函数:设定监听(连接)上线*/
		Listen(sfd, 2);                                
 
		printf("wait for client connect ...\n");
	 
		clie_addr_len = sizeof(clie_addr_len);
	
	/*4.accept函数:阻塞等待客户端建立连接*/
		cfd = Accept(sfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
	
	/*建立连接后打印客户端的IP和端口号    获取客户端地址结构*/
		printf("cfd = ----%d\n", cfd);
		printf("client IP: %s  port:%d\n", 
				inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)), 
				ntohs(clie_addr.sin_port));
 
    while (1) {
		//5. read(fd)	读socket获取客户端数据
			len = Read(cfd, buf, sizeof(buf));
			Write(STDOUT_FILENO, buf, len);
 
		//6. 小--大写	toupper()
			for (i = 0; i < len; i++)
				buf[i] = toupper(buf[i]);
				
		//7. write(fd)
			Write(cfd, buf, len); 
    }
	
	//8. close()
		Close(sfd);
		Close(cfd);
 
    return 0;
}

client.c封装实现

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
#include "wrap.h"
 
#define SERV_IP "127.0.0.1"
#define SERV_PORT 6666
 
int main(void)
{
    int sfd, len;
    struct sockaddr_in serv_addr;
    char buf[BUFSIZ]; 
 
 
	//1. socket()	创建socket
    sfd = Socket(AF_INET, SOCK_STREAM, 0);                    
 
 		bzero(&serv_addr, sizeof(serv_addr));           	 		 //将指定内存区域的内容初始化为0	
		serv_addr.sin_family = AF_INET;        				 		 //IPv4         
		inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr);  	 //本地字节序(string IP) ---> 网络字节序
		serv_addr.sin_port = htons(SERV_PORT);          	 		 //转为网络字节序的 端口号
 
	//2. connect();	与服务器建立连接
		Connect(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
 
    while (1) {
        fgets(buf, sizeof(buf), stdin);
		
		//3. write()	写数据到 socket
			int r = Write(sfd, buf, strlen(buf));       
			printf("Write r ======== %d\n", r);
		
		//4. read()	读转换后的数据
			len = Read(sfd, buf, sizeof(buf));
			printf("Read len ========= %d\n", len);
		
		//5. 显示读取结果
			Write(STDOUT_FILENO, buf, len);
	}
	
	//6. close()
		Close(sfd);

    return 0;
}
 

二:多进程process并发服务器

server.c服务器

实现思路

使用多进程并发服务器时要考虑以下几点:
    1.父进程最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符)
    2.系统内创建进程个数(与内存大小相关)
    3.进程创建过多是否降低整体服务性能(进程调度)

	1. Socket();		        创建 监听套接字 lfd
	2. Bind()	                绑定地址结构 Strcut scokaddr_in addr;
	3. Listen();	
	4. while (1) {

		cfd = Accpet();			接收客户端连接请求。
		pid = fork();
		if (pid == 0){			子进程 read(cfd) --- 小-》大 --- write(cfd)

			close(lfd)		    关闭用于建立连接的套接字 lfd

			read()
			小--大
			write()

		} else if (pid > 0) {	
			close(cfd);		    关闭用于与客户端通信的套接字 cfd	
			contiue;
		}
	  }

	5. 子进程:
		    close(lfd)
		    read()
		    小--大
		    write()	

	   父进程:
		    close(cfd);
		    注册信号捕捉函数:	SIGCHLD
		    在回调函数中, 完成子进程回收
			while (waitpid());

代码逻辑 

#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <ctype.h>
#include <unistd.h>
 
#include "wrap.h"
 
#define MAXLINE 8192
#define SERV_PORT 8000
 
 
//在回调函数中, 完成子进程回收
	void do_sigchild(int num)
	{
		while (waitpid(0, NULL, WNOHANG) > 0)
			;
	}
 
int main(void)
{
    struct sockaddr_in servaddr, cliaddr;
    socklen_t cliaddr_len;
    int listenfd, connfd;
    char buf[MAXLINE];
    char str[INET_ADDRSTRLEN];
    int i, n;
    pid_t pid;
    struct sigaction newact;
 
	//5.父进程
    newact.sa_handler = do_sigchild;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
	//注册信号捕捉函数:	SIGCHLD
		sigaction(SIGCHLD, &newact, NULL);
 
 
 
	//1. Socket();		        创建 监听套接字 lfd
		listenfd = Socket(AF_INET, SOCK_STREAM, 0);
 
		int opt = 1;
		setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	 
		bzero(&servaddr, sizeof(servaddr));
		servaddr.sin_family = AF_INET;
		servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
		servaddr.sin_port = htons(SERV_PORT);
	 
	//2. Bind()	                绑定地址结构 Strcut scokaddr_in addr;
		Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
 
	//3. Listen();
		Listen(listenfd, 20);
 
    printf("Accepting connections ...\n");
    while (1) {
        cliaddr_len = sizeof(cliaddr);
		
		//accept()	阻塞监听客户端连接,接收客户端连接请求
			connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
			printf("-------------------------%d\n", connfd);
 
        pid = fork();		
		//4.子进程 read(connfd) --- 小-》大 --- write(connfd)
			if (pid == 0) {
				//关闭用于建立连接的套接字 lfd
				Close(listenfd);
				while (1) {
					//read()
						n = Read(connfd, buf, MAXLINE);
						
						if (n == 0) {
							printf("the other side has been closed.\n");
							break;
						}
						printf("received from %s at PORT %d\n",
								inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
								ntohs(cliaddr.sin_port));
							
					//小--大
						for (i = 0; i < n; i++)
							buf[i] = toupper(buf[i]);
	 
					//write()
						Write(STDOUT_FILENO, buf, n);
						Write(connfd, buf, n);
				}
				Close(connfd);
				return 0;
			} else if (pid > 0) {
				Close(connfd);		//关闭用于与客户端通信的套接字 connfd
			} else
				perr_exit("fork");
    }
    return 0;
}

client.c客户端

/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "wrap.h"

#define MAXLINE 8192
#define SERV_PORT 8000

int main(int argc, char *argv[])
{
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    int sockfd, n;

    sockfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);

    Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    while (fgets(buf, MAXLINE, stdin) != NULL) {
        Write(sockfd, buf, strlen(buf));
        n = Read(sockfd, buf, MAXLINE);
        if (n == 0) {
            printf("the other side has been closed.\n");
            break;
        }
        else
            Write(STDOUT_FILENO, buf, n);
    }

    Close(sockfd);

    return 0;
}

三:多线程thread并发服务器

server.c服务器

实现思路

在使用线程模型开发服务器时需考虑以下问题:
    1.调整进程内最大文件描述符上限
    2.线程如有共享数据,考虑线程同步
    3.服务于客户端线程退出时,退出处理。(退出值,分离态)
    4.系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU
	1. Socket();		            创建 监听套接字 lfd
        
	2. Bind()		                绑定地址结构 Strcut scokaddr_in addr;

	3. Listen();		

	4. while (1) {		
		cfd = Accept(lfd, );
		pthread_create(&tid, NULL, tfn, (void *)cfd);
		pthread_detach(tid);  		// pthead_join(tid, void **);  新线程---专用于回收子线程
	  }

	5. 子线程:
		void *tfn(void *arg) 
		{
			// close(lfd)			不能关闭。 主线程要使用lfd
			read(cfd)
			小--大
			write(cfd)
			pthread_exit((void *)10);	
		}

代码逻辑 

#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
 
#include "wrap.h"
 
#define MAXLINE 8192
#define SERV_PORT 8000

//定义一个结构体, 将地址结构跟cfd捆绑 
	struct s_info {                     
		struct sockaddr_in cliaddr;
		int connfd;
	};
 
 
//子线程
	void *do_work(void *arg)
	{
		int n,i;
		struct s_info *ts = (struct s_info*)arg;
		char buf[MAXLINE];
		char str[INET_ADDRSTRLEN];      //#define INET_ADDRSTRLEN 16  可用"[+d"查看
	 
		while (1) {		
			//读客户端
				//read(cfd)
				n = Read(ts->connfd, buf, MAXLINE);            

			//跳出循环,关闭cfd
				if (n == 0) {
					printf("the client %d closed...\n", ts->connfd);
					break;                                              
				}
			
			 //打印客户端信息(IP/PORT)
				printf("received from %s at PORT %d\n",
						inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)),
						ntohs((*ts).cliaddr.sin_port));                
	 
			//小写-->大写
				for (i = 0; i < n; i++) 
					buf[i] = toupper(buf[i]);                       
			//写出至屏幕
			Write(STDOUT_FILENO, buf, n);   
			//回写给客户端
			Write(ts->connfd, buf, n);                              
		}
		Close(ts->connfd);
	 
		return (void *)0;
}
 
int main(void)
{
    struct sockaddr_in servaddr, cliaddr;
    socklen_t cliaddr_len;
    int listenfd, connfd;
    pthread_t tid;
 
    struct s_info ts[256];     								   //创建结构体数组.
    int i = 0;
 
 
	//1. Socket();		            创建 监听套接字 lfd
		listenfd = Socket(AF_INET, SOCK_STREAM, 0);                     			//创建一个socket, 得到lfd
	 
		bzero(&servaddr, sizeof(servaddr));                             			//地址结构清零
		servaddr.sin_family = AF_INET;
		servaddr.sin_addr.s_addr = htonl(INADDR_ANY);                  				//指定本地任意IP
		servaddr.sin_port = htons(SERV_PORT);                                   	//指定端口号 
 
	//2. Bind()		                绑定地址结构 Strcut scokaddr_in addr;
		Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));         	//绑定
 
	//3. Listen();
		Listen(listenfd, 128);                                                  	//设置同一时刻链接服务器上限数
 
    printf("Accepting client connect ...\n");
 
    while (1) {
        cliaddr_len = sizeof(cliaddr);
		
		//accept()	阻塞监听客户端连接
			connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); 	//阻塞监听客户端链接请求
				ts[i].cliaddr = cliaddr;
				ts[i].connfd = connfd;
 
        pthread_create(&tid, NULL, do_work, (void*)&ts[i]);
        pthread_detach(tid);                                                    	//子线程分离,防止僵线程产生.
        i++;
    }
 
    return 0;
}

client.c客户端

/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 8000

int main(int argc, char *argv[])
{
	struct sockaddr_in servaddr;
	char buf[MAXLINE];
	int sockfd, n;

	sockfd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr.s_addr);
	servaddr.sin_port = htons(SERV_PORT);

	Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

	while (fgets(buf, MAXLINE, stdin) != NULL) {
		Write(sockfd, buf, strlen(buf));
		n = Read(sockfd, buf, MAXLINE);
		if (n == 0)
			printf("the other side has been closed.\n");
		else
			Write(STDOUT_FILENO, buf, n);
	}

	Close(sockfd);

	return 0;
}

猜你喜欢

转载自blog.csdn.net/liu17234050/article/details/132384149
今日推荐