• 【Linux】Linux进程间通信(二)


    在这里插入图片描述

    ​📝个人主页:@Sherry的成长之路
    🏠学习社区:Sherry的成长之路(个人社区)
    📖专栏链接:Linux
    🎯长路漫漫浩浩,万事皆有期待

    上一篇博客:【Linux】Linux进程间通信(一)

    命名管道

    命名管道的原理

    匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间的通信,通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。
    如果要实现两个毫不相关进程之间的通信,可以使用命名管道来做到。命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。

    注意:

    普通文件是很难做到通信的,即便做到通信也无法解决一些安全问题。
    命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。
    使用命令创建命名管道
    我们可以使用mkfifo命令创建一个命名管道。

    mkfifo fifo
    
    • 1

    在这里插入图片描述

    可以看到,创建出来的文件的类型是p,代表该文件是命名管道文件。
    在这里插入图片描述

    使用这个命名管道文件,就能实现两个进程之间的通信了。我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进行读取。
    现象就是当进程A启动后,进程B会每秒从命名管道中读取一个字符串打印到显示器上。这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。
    在这里插入图片描述

    之前我们说过,当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉,在这里就可以很好的得到验证:当我们终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。

    创建一个命名管道

    在程序中创建命名管道使用mkfifo函数,mkfifo函数的函数原型如下:

    int mkfifo(const char *pathname, mode_t mode);
    
    • 1

    mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。

    若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
    若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。(注意当前路径的含义)

    mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。

    例如,将mode设置为0666,则命名管道文件创建出来的权限如下:

    prw-rw-rw-
    
    • 1

    但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。

    prw-rw-r--
    
    • 1

    若想创建出来命名管道文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。

    umask(0); //将文件默认掩码设置为0
    
    • 1

    mkfifo函数的返回值。

    命名管道创建成功,返回0。
    命名管道创建失败,返回-1。

    创建命名管道示例:

    使用以下代码即可在当前路径下,创建出一个名为myfifo的命名管道。

    #include 
    #include 
    #include 
    
    #define FILE_NAME "myfifo"
    
    int main()
    {
    	umask(0); //将文件默认掩码设置为0
    	if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
    		perror("mkfifo");
    		return 1;
    	}
    
    	//create success...
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    运行代码后,命名管道myfifo就在当前路径下被创建了。
    在这里插入图片描述

    命名管道的打开规则

    1、如果当前打开操作是为读而打开FIFO时。

    O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
    O_NONBLOCK enable:立刻返回成功。

    2、如果当前打开操作是为写而打开FIFO时。

    O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
    O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。

    用命名管道实现serve&client通信

    实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了。

    服务端的代码如下:

    //server.c
    #include "comm.h"
    
    int main()
    {
    	umask(0); //将文件默认掩码设置为0
    	if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
    		perror("mkfifo");
    		return 1;
    	}
    	int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
    	if (fd < 0){
    		perror("open");
    		return 2;
    	}
    	char msg[128];
    	while (1){
    		msg[0] = '\0'; //每次读之前将msg清空
    		//从命名管道当中读取信息
    		ssize_t s = read(fd, msg, sizeof(msg)-1);
    		if (s > 0){
    			msg[s] = '\0'; //手动设置'\0',便于输出
    			printf("client# %s\n", msg); //输出客户端发来的信息
    		}
    		else if (s == 0){
    			printf("client quit!\n");
    			break;
    		}
    		else{
    			printf("read error!\n");
    			break;
    		}
    	}
    	close(fd); //通信完毕,关闭命名管道文件
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    而对于客户端来说,因为服务端运行起来后命名管道文件就已经被创建了,所以客户端只需以写的方式打开该命名管道文件,之后客户端就可以将通信信息写入到命名管道文件当中,进而实现和服务端的通信。

    客户端的代码如下:

    //client.c
    #include "comm.h"
    
    int main()
    {
    	int fd = open(FILE_NAME, O_WRONLY); //以写的方式打开命名管道文件
    	if (fd < 0){
    		perror("open");
    		return 1;
    	}
    	char msg[128];
    	while (1){
    		msg[0] = '\0'; //每次读之前将msg清空
    		printf("Please Enter# "); //提示客户端输入
    		fflush(stdout);
    		//从客户端的标准输入流读取信息
    		ssize_t s = read(0, msg, sizeof(msg)-1);
    		if (s > 0){
    			msg[s - 1] = '\0';
    			//将信息写入命名管道
    			write(fd, msg, strlen(msg));
    		}
    	}
    	close(fd); //通信完毕,关闭命名管道文件
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    对于如何让客户端和服务端使用同一个命名管道文件,这里我们可以让客户端和服务端包含同一个头文件,该头文件当中提供这个共用的命名管道文件的文件名,这样客户端和服务端就可以通过这个文件名,打开同一个命名管道文件,进而进行通信了。

    共用头文件的代码如下:

    //comm.h
    #pragma once
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    代码编写完毕后,先将服务端进程运行起来,之后我们就能在客户端看到这个已经被创建的命名管道文件。
    在这里插入图片描述

    接着再将客户端也运行起来,此时我们从客户端写入的信息被客户端写入到命名管道当中,服务端再从命名管道当中将信息读取出来打印在服务端的显示器上,该现象说明服务端是能够通过命名管道获取到客户端发来的信息的,换句话说,此时这两个进程之间是能够通信的。
    在这里插入图片描述

    当客户端和服务端运行起来时,我们还可以通过ps命令查看这两个进程的信息,可以发现这两个进程确实是两个毫不相关的进程,因为它们的PID和PPID都不相同。也就证明了,命名管道是可以实现两个毫不相关进程之间的通信的。
    在这里插入图片描述

    服务端和客户端之间的退出关系

    当客户端退出后,服务端将管道当中的数据读完后就再也读不到数据了,那么此时服务端也就会去执行它的其他代码了(在当前代码中是直接退出了)。
    在这里插入图片描述

    当服务端退出后,客户端写入管道的数据就不会被读取了,也就没有意义了,那么当客户端下一次再向管道写入数据时,就会收到操作系统发来的13号信号(SIGPIPE),此时客户端就被操作系统强制杀掉了。
    在这里插入图片描述

    通信是在内存当中进行的

    若是我们只让客户端向管道写入数据,而服务端不从管道读取数据,那么这个管道文件的大小会不会发生变化呢?

    //server.c
    #include "comm.h"
    
    int main()
    {
    	umask(0); //将文件默认掩码设置为0
    	if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
    		perror("mkfifo");
    		return 1;
    	}
    	int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
    	if (fd < 0){
    		perror("open");
    		return 2;
    	}
    	while (1){
    		//服务端不读取管道信息
    	}
    	close(fd); //通信完毕,关闭命名管道文件
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    可以看到,尽管服务端不读取管道当中的数据,但是管道当中的数据并没有被刷新到磁盘,使用ll命令看到命名管道文件的大小依旧为0,也就说明了双方进程之间的通信依旧是在内存当中进行的,和匿名管道通信是一样的。
    在这里插入图片描述

    用命名管道实现派发计算任务

    需要注意的是两个进程之间的通信,并不是简单的发送字符串而已,服务端是会对客户端发送过来的信息进行某些处理的。

    这里我们以客户端向服务端派发计算任务为例,客户端通过管道向服务端发送双操作数的计算请求,服务端接收到客户端的信息后需要计算出相应的结果。

    这里我们无需更改客户端的代码,只需改变服务端处理通信信息的逻辑即可。

    //server.c
    #include "comm.h"
    
    int main()
    {
    	umask(0); //将文件默认掩码设置为0
    	if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
    		perror("mkfifo");
    		return 1;
    	}
    	int fd = open(FILE_NAME, O_RDONLY); //打开命名管道文件
    	if (fd < 0){
    		perror("open");
    		return 2;
    	}
    	char msg[128];
    	while (1){
    		msg[0] = '\0'; //每次读之前将msg清空
    		//从命名管道当中读取信息
    		ssize_t s = read(fd, msg, sizeof(msg)-1);
    		if (s > 0){
    			msg[s] = '\0'; //手动设置'\0',便于输出
    			printf("client# %s\n", msg);
    			//服务端进行计算任务
    		    char* lable = "+-*/%";
    			char* p = msg;
    			int flag = 0;
    			while (*p){
    				switch (*p){
    				case '+':
    					flag = 0;
    					break;
    				case '-':
    					flag = 1;
    					break;
    				case '*':
    					flag = 2;
    					break;
    				case '/':
    					flag = 3;
    					break;
    				case '%':
    					flag = 4;
    					break;
    				}
    				p++;
    			}
    			char* data1 = strtok(msg, "+-*/%");
    			char* data2 = strtok(NULL, "+-*/%");
    			int num1 = atoi(data1);
    			int num2 = atoi(data2);
    			int ret = 0;
    			switch (flag){
    			case 0:
    				ret = num1 + num2;
    				break;
    			case 1:
    				ret = num1 - num2;
    				break;
    			case 2:
    				ret = num1 * num2;
    				break;
    			case 3:
    				ret = num1 / num2;
    				break;
    			case 4:
    				ret = num1 % num2;
    				break;
    			}
    			printf("%d %c %d = %d\n", num1, lable[flag], num2, ret); //打印计算结果
    		}
    		else if (s == 0){
    			printf("client quit!\n");
    			break;
    		}
    		else{
    			printf("read error!\n");
    			break;
    		}
    	}
    	close(fd); //通信完毕,关闭命名管道文件
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    此时服务端接收到客户端的信息后,需要进行的处理动作就不是将其打印到显示器了,而是需要将信息经过进一步的处理,从而得到相应的结果。
    在这里插入图片描述

    用命名管道实现进程遥控

    比较有意思的是,我们可以通过一个进程来控制另一个进程的行为,比如我们从客户端输入命令到管道当中,再让服务端将管道当中的命令读取出来并执行。

    下面我们只实现了让服务端执行不带选项的命令,若是想让服务端执行带选项的命令,可以对管道当中获取的命令进行解析处理。这里的实现非常简单,只需让服务端从管道当中读取命令后创建子进程,然后再进行进程程序替换即可。

    这里也无需更改客户端的代码,只需改变服务端处理通信信息的逻辑即可。

    #include "comm.h"
    
    int main()
    {
    	umask(0); //将文件默认掩码设置为0
    	if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
    		perror("mkfifo");
    		return 1;
    	}
    	int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
    	if (fd < 0){
    		perror("open");
    		return 2;
    	}
    	char msg[128];
    	while (1){
    		msg[0] = '\0'; //每次读之前将msg清空
    		//从命名管道当中读取信息
    		ssize_t s = read(fd, msg, sizeof(msg)-1);
    		if (s > 0){
    			msg[s] = '\0'; //手动设置'\0',便于输出
    			printf("client# %s\n", msg);
    			if (fork() == 0){
    				//child
    				execlp(msg, msg, NULL); //进程程序替换
    				exit(1);
    			}
    			waitpid(-1, NULL, 0); //等待子进程
    		}
    		else if (s == 0){
    			printf("client quit!\n");
    			break;
    		}
    		else{
    			printf("read error!\n");
    			break;
    		}
    	}
    	close(fd); //通信完毕,关闭命名管道文件
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    此时服务端接收到客户端的信息后,便进行进程程序替换,进而执行客户端发送过来的命令。
    在这里插入图片描述

    用命名管道实现文件拷贝

    这里我们再用命名管道实现一下文件的拷贝。

    需要拷贝的文件是file.txt,该文件当中的内容如下:
    在这里插入图片描述

    我们要做的就是,让客户端将file.txt文件通过管道发送给服务端,在服务端创建一个file-bat.txt文件,并将从管道获取到的数据写入file-bat.txt文件当中,至此便实现了file.txt文件的拷贝。

    其中服务端需要做的就是,创建命名管道并以读的方式打开该命名管道,再创建一个名为file-bat.txt的文件,之后需要做的就是将从管道当中读取到的数据写入到file-bat.txt文件当中即可。

    服务端的代码如下:

    //server.c
    #include "comm.h"
    
    int main()
    {
    	umask(0); //将文件默认掩码设置为0
    	if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
    		perror("mkfifo");
    		return 1;
    	}
    	int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
    	if (fd < 0){
    		perror("open");
    		return 2;
    	}
    	//创建文件file-bat.txt,并以写的方式打开该文件
    	int fdout = open("file-bat.txt", O_CREAT | O_WRONLY, 0666);
    	if (fdout < 0){
    		perror("open");
    		return 3;
    	}
    	char msg[128];
    	while (1){
    		msg[0] = '\0'; //每次读之前将msg清空
    		//从命名管道当中读取信息
    		ssize_t s = read(fd, msg, sizeof(msg)-1);
    		if (s > 0){
    			write(fdout, msg, s); //将读取到的信息写入到file-bat.txt文件当中
    		}
    		else if (s == 0){
    			printf("client quit!\n");
    			break;
    		}
    		else{
    			printf("read error!\n");
    			break;
    		}
    	}
    	close(fd); //通信完毕,关闭命名管道文件
    	close(fdout); //数据写入完毕,关闭file-bat.txt文件
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    而客户端需要做的就是,以写的方式打开这个已经存在的命名管道文件,再以读的方式打开file.txt文件,之后需要做的就是将file.txt文件当中的数据读取出来并写入管道当中即可。

    客户端的代码如下:

    //client.c
    #include "comm.h"
    
    int main()
    {
    	int fd = open(FILE_NAME, O_WRONLY); //以写的方式打开命名管道文件
    	if (fd < 0){
    		perror("open");
    		return 1;
    	}
    	int fdin = open("file.txt", O_RDONLY); //以读的方式打开file.txt文件
    	if (fdin < 0){
    		perror("open");
    		return 2;
    	}
    	char msg[128];
    	while (1){
    		//从file.txt文件当中读取数据
    		ssize_t s = read(fdin, msg, sizeof(msg));
    		if (s > 0){
    			write(fd, msg, s); //将读取到的数据写入到命名管道当中
    		}
    		else if (s == 0){
    			printf("read end of file!\n");
    			 break;
    		}
    		else{
    			printf("read error!\n");
    			break;
    		}
    	}
    	close(fd); //通信完毕,关闭命名管道文件
    	close(fdin); //数据读取完毕,关闭file.txt文件
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    共用头文件的代码和之前的一样,如下:

    //comm.h
    #pragma once
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    编写完代码后,先运行服务端,再运行客户端,一瞬间这两个进程就相继运行结束了。
    在这里插入图片描述

    此时使用ll命令就可以看到,已经完成了file.txt文件的拷贝。
    在这里插入图片描述

    使用cat命令打印file-bat.txt文件当中的内容,发现和file.txt文件当中的内容相同,拷贝文件成功。
    在这里插入图片描述

    使用管道实现文件的拷贝有什么意义?

    因为这里是使用管道在本地进行的文件拷贝,所以看似没什么意义,但我们若是将这里的管道想象成“网络”,将客户端想象成“Windows Xshell”,再将服务端想象成“centos服务器”。那我们此时实现的就是文件上传的功能,若是将方向反过来,那么实现的就是文件下载的功能。
    在这里插入图片描述

    命名管道和匿名管道的区别

    匿名管道由pipe函数创建并打开。
    命名管道由mkfifo函数创建,由open函数打开。
    FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于它们创建与打开的方式不同,一旦这些工作完成之后,它们具有相同的语义。

    命令行当中的管道
    现有data.txt文件,文件当中的内容如下:
    在这里插入图片描述

    我们可以利用管道(“|”)同时使用cat命令和grep命令,进而实现文本过滤。

    cat data.txt | grep sherry
    
    • 1

    在这里插入图片描述

    那么在命令行当中的管道(“|”)到底是匿名管道还是命名管道呢?

    由于匿名管道只能用于有亲缘关系的进程之间的通信,而命名管道可以用于两个毫不相关的进程之间的通信,因此我们可以先看看命令行当中用管道(“|”)连接起来的各个进程之间是否具有亲缘关系。

    下面通过管道(“|”)连接了三个进程,通过ps命令查看这三个进程可以发现,这三个进程的PPID是相同的,也就是说它们是由同一个父进程创建的子进程。
    在这里插入图片描述

    而它们的父进程实际上就是命令行解释器,这里为bash。
    在这里插入图片描述

    也就是说,由管道(“|”)连接起来的各个进程是有亲缘关系的,它们之间互为兄弟进程。
    在这里插入图片描述
    现在我们已经知道了,若是两个进程之间采用的是命名管道,那么在磁盘上必须有一个对应的命名管道文件名,而实际上我们在使用命令的时候并不存在类似的命名管道文件名,因此命令行上的管道实际上是匿名管道。

    总结:

    今天我们学习了Linux进程间通信的相关知识,了解了进程间通信介绍,管道等 。接下来,我们将继续学习Linux的其他知识。希望我的文章和讲解能对大家的学习提供一些帮助。

    当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

    在这里插入图片描述

  • 相关阅读:
    基于bp神经网络汽车自动变速器最佳挡位判断(Matlab代码实现)
    C++二分算法:得到山形数组的最少删除次数
    【力扣每日一题】2023.9.18 打家劫舍Ⅲ
    shell脚本编程基础(上)
    四、java版 SpringCloud分布式微服务云架构之Java 对象和类
    JS对树结构进行模糊搜索,支持多关键字多字段搜索以及排序
    FastAPI 学习之路(八)路径参数和数值的校验
    LLM之RAG实战(二十七)| 如何评估RAG系统
    【网络编程】
    京东按关键字搜索商品 API 返回值说明
  • 原文地址:https://blog.csdn.net/m0_73258399/article/details/134433689