C 进阶 — 文件操作

C 进阶 --- 文件操作

主要内容
  1. 文件概述
  2. 文件打开和关闭
  3. 文件顺序读写
  4. 文件随机读写
  5. 文本文件和二进制文件
  6. 文件缓冲区

一 文件概述

1.1 使用文件的原因

例 先前通讯录程序,数据存放在内存中,当程序退出时,通讯录中的数据自然就不存在了。如何把信息记录下来?一般数据持久化的方法有,数据存放在磁盘文件、存放到数据库等方式。使用文件将数据直接存放在电脑的硬盘上

1.2 什么是文件

磁盘上的文件即是文件

但程序设计中的文件一般指两种:程序文件、数据文件(从文件功能角度分类)

程序文件

包括源程序文件( 后缀为.c ),目标文件( windows 环境后缀为 .obj ),可执行程序( windows 环境后缀为 .exe )

数据文件

程序运行时读写的数据,以前章节所处理数据输入输出都是以终端为对象( 从终端的键盘输入数据,运行结果显示到显示器上 )

有时候会把信息输出到磁盘,当需要时再从磁盘上把数据读取到内存中使用( 这里处理的就是磁盘上文件 )

文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用。文件名包含三部分 :文件路径+文件名主干+文件后缀,例如: c:\code\test.txt,为方便文件标识常被称为 文件名

二 文件打开和关闭

2.1 文件指针

缓冲文件系统中,关键概念是文件类型指针 ,简称文件指针

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名 FILE

例如,VS 编译环境提供的 stdio.h 头文件中有以下的文件类型申明

c++ 复制代码
struct _iobuf {
        char *_ptr;
        int   _cnt;
        char *_base;
        int   _flag;
        int   _file;
        int   _charbuf;
        int   _bufsiz;
        char *_tmpfname;
       };
typedef struct _iobuf FILE;

不同 C 编译器的 FILE 类型的内容不完全相同,但大同小异。每当打开一个文件时,系统会自动创建一个 FILE 结构变量,并填充其中信息,一般都是通过一个 FILE 指针来维护这个 FILE 结构变量

创建一个 FILE* 指针变量

c++ 复制代码
FILE* pf;//文件指针变量

定义 pf 是一个指向 FILE 类型数据的指针变量,使 pf 指向某个文件的文件信息区(一个结构体变量)。通过该文件信息区中的信息能够访问该文件,即通过文件指针变量找到与它关联的文件,如下图

2.2 文件打开和关闭

文件在读写前应先打开文件 ,在使用结束后应关闭文件

在打开文件时,会返回一个 FILE* 指针变量指向该文件(建立指针和文件的关系)

ANSIC 规定使用 fopen 函数来打开文件,fclose 来关闭文件

c++ 复制代码
//打开文件
FILE * fopen ( const char * filename, const char * mode );

//关闭文件
int fclose ( FILE * stream );

打开方式如下

文件使用方式 含义 如果指定文件不存在
"r"(只读) 为了输入数据,打开一个已经存在的文本文件 出错
"w"(只写) 为了输出数据,打开一个文本文件 建立一个新的文件
"a"(追加) 向文本文件尾添加数据 建立一个新的文件
"rb"(只读) 为了输入数据,打开一个二进制文件 出错
"wb"(只写) 为了输出数据,打开一个二进制文件 建立一个新的文件
"ab"(追加) 向一个二进制文件尾添加数据 建立一个新的文件
"r+"(读写) 为了读和写,打开一个文本文件 出错
"w+"(读写) 为了读和写,建议一个新的文件 建立一个新的文件
"a+"(读写) 打开一个文件,在文件尾进行读写 建立一个新的文件
"rb+"(读写) 为了读和写打开一个二进制文件 出错
"wb+"(读写) 为了读和写,新建一个新的二进制文件 建立一个新的文件
"ab+"(读写) 打开一个二进制文件,在文件尾进行读和写 建立一个新的文件

实例代码

c++ 复制代码
/* fopen fclose example */
#include <stdio.h>
int main ()
{
    FILE * pFile;
    //打开文件
    pFile = fopen ("myfile.txt","w");
    //文件操作
    if (pFile!=NULL)
    {
        fputs ("fopen example",pFile);
        //关闭文件
        fclose (pFile);
    }
    return 0;
}

三 文件顺序和随机读写

在编程中,(Stream)是一种用来顺序传输数据的抽象,数据可以是输入的(如从键盘、文件或网络读取)或者是输出的(如打印到屏幕或写入文件)

C 中的流是通过标准库实现的

流的分类

1、标准输出流(stdin):用于接收用户输入,类似键盘输入这种,默认缓冲方式为行缓冲,即当检测到换行符或缓冲区满时会提交数据

2、标准输入流(stdout):用于输出到屏幕,默认缓冲方式也为行缓冲

3、标准错误流(stderr):用于输出错误信息,一般采取立即输出的方式

3.1 顺序读写函数

c++ 复制代码
功能 函数名 适用于
字符输入函数 fgetc 所有输入流
字符输出函数 fputc 所有输出流
文本行输入函数 fgets 所有输入流
文本行输出函数 fputs 所有输出流
格式化输入函数 fscanf 所有输入流
格式化输出函数 fprintf 所有输出流
二进制输入 fread 文件
二进制输出 fwrite 文件
3.1.1 流中获取字符 fgetc
c++ 复制代码
int fgetc ( FILE * stream );
Returns the character currently pointed by the internal file position indicator of the specified stream. The internal file position indicator is then advanced to the next character.
返回指定流的内部文件位置指示器当前指向的字符。然后,内部文件位置指示器前进到下一个字符

If the stream is at the end-of-file when called, the function returns EOF and sets the end-of-file indicator for the stream (feof).
如果调用时流位于文件末尾,则函数将返回 EOF 并设置流的文件结束指示器 (feof)

If a read error occurs, the function returns EOF and sets the error indicator for the stream (ferror).
如果发生读取错误,该函数将返回 EOF 并为流设置错误指示符 (ferror)

fgetc and getc are equivalent, except that getc may be implemented as a macro in some libraries.
fgetc 和 getc 是等效的,只是 getc 在某些库中可能作为宏实现

代码示例

c++ 复制代码
/* fgetc example: money counter */
#include <stdio.h>
int main ()
{
    FILE * pFile;
    int c;
    int n = 0;
    pFile=fopen ("myfile.txt","r");
    if (pFile==NULL) perror ("Error opening file");
    else
    {
        do {
            c = fgetc (pFile);
            if (c == '$') n++;
        } while (c != EOF);
        fclose (pFile);
        printf ("The file contains %d dollar sign characters ($).\n",n);
    }
    return 0;
}
3.1.2 将字符写入流 fputc
c++ 复制代码
int fputc ( int character, FILE * stream );
c++ 复制代码
Writes a character to the stream and advances the position indicator.
将字符写入流并前进位置指示器

The character is written at the position indicated by the internal position indicator of the stream, which is then automatically advanced by one.
字符被写入流的内部位置指示器指示的位置,然后自动前进 1

代码示例

c++ 复制代码
/* fputc example: alphabet writer */
#include <stdio.h>

int main ()
{
    FILE * pFile;
    char c;

    pFile = fopen ("alphabet.txt","w");
    if (pFile!=NULL) {

        for (c = 'A' ; c <= 'Z' ; c++)
            fputc ( c , pFile );

        fclose (pFile);
    }
    return 0;
}
3.1.3 从流中获取字符串 fgets
c++ 复制代码
char * fgets ( char * str, int num, FILE * stream );
Reads characters from stream and stores them as a C string into str until (num-1) characters have been read or either a newline or the end-of-file is reached, whichever happens first.
从 stream 中读取字符并将其作为 C 字符串存储到 str 中,直到读取 (num-1) 个字符,或者到达换行符或文件末尾,以先发生者为准

A newline character makes fgets stop reading, but it is considered a valid character by the function and included in the string copied to str.
换行符会 fgets 停止读取,但它会被函数视为有效字符,并包含在复制到 str 的字符串中

A terminating null character is automatically appended after the characters copied to str.
终止 null 字符会自动附加到复制到 str 的字符之后

Notice that fgets is quite different from gets: not only fgets accepts a stream argument, but also allows to specify the maximum size of str and includes in the string any ending newline character.
请注意,它与 gets 完全不同:fgets 不仅接受 stream 参数,还允许指定 str 的最大大小,并在字符串中包含任何结束换行符

代码示例

c++ 复制代码
/* fgets example */
#include <stdio.h>

int main()
{
    FILE * pFile;
    char mystring [100];

    pFile = fopen ("myfile.txt" , "r");
    if (pFile == NULL) perror ("Error opening file");
    else {
        if ( fgets (mystring , 100 , pFile) != NULL )
            puts (mystring);
        fclose (pFile);
    }
    return 0;
}
3.1.4 将字符串写入流 fputs
c++ 复制代码
int fputs ( const char * str, FILE * stream );
c++ 复制代码
Writes the C string pointed by str to the stream.
将 str 指向的 C 字符串写入流

The function begins copying from the address specified (str) until it reaches the terminating null character ('\0'). This terminating null-character is not copied to the stream.
该函数从指定的地址 (str) 开始复制,直到到达终止 null 字符 ( '\0' )。此终止 null 字符不会复制到流中

Notice that fputs not only differs from puts in that the destination stream can be specified, but also fputs does not write additional characters, while puts appends a newline character at the end automatically.
请注意,它 fputs 不仅与 puts 的不同之处在于可以指定目标流,而且 fputs 不会写入其他字符,而 puts 会自动在末尾附加换行符

代码示例

c++ 复制代码
/* fputs example */
#include <stdio.h>

int main ()
{
    FILE * pFile;
    char sentence [256];

    printf ("Enter sentence to append: ");
    fgets (sentence,256,stdin);
    pFile = fopen ("mylog.txt","a");
    fputs (sentence,pFile);
    fclose (pFile);
    return 0;
}
3.1.5 从流中读取格式化数据 fscanf
c++ 复制代码
int fscanf ( FILE * stream, const char * format, ... );
c++ 复制代码
Reads data from the stream and stores them according to the parameter format into the locations pointed by the additional arguments.
从流中读取数据,并根据参数格式将其存储到其他参数指向的位置

The additional arguments should point to already allocated objects of the type specified by their corresponding format specifier within the format string.
其他参数应指向已分配的对象,该对象由格式字符串中的相应格式说明符指定的类型

代码示例

c++ 复制代码
/* fscanf example */
#include <stdio.h>

int main ()
{
    char str [80];
    float f;
    FILE * pFile;

    pFile = fopen ("myfile.txt","w+");
    fprintf (pFile, "%f %s", 3.1416, "PI");
    rewind (pFile);
    fscanf (pFile, "%f", &f);
    fscanf (pFile, "%s", str);
    fclose (pFile);
    printf ("I have read: %f and %s \n",f,str);
    return 0;
}
3.1.6 将格式化数据写入流 fprintf
c++ 复制代码
int fprintf ( FILE * stream, const char * format, ... );
c++ 复制代码
Writes the C string pointed by format to the stream. If format includes format specifiers (subsequences beginning with %), the additional arguments following format are formatted and inserted in the resulting string replacing their respective specifiers.
将 format 指向的 C 字符串写入流。如果 format 包含格式说明符(子序列以 开头), % 则 format 后面的其他参数将被格式化并插入到结果字符串中,以替换其各自的说明符

After the format parameter, the function expects at least as many additional arguments as specified by format.
在 format 参数之后,该函数至少需要与 format 指定的相同数量的附加参数

代码示例

c++ 复制代码
/* fprintf example */
#include <stdio.h>

int main ()
{
    FILE * pFile;
    int n;
    char name [100];

    pFile = fopen ("myfile.txt","w");
    for (n=0 ; n<3 ; n++)
    {
        puts ("please, enter a name: ");
        gets (name);
        fprintf (pFile, "Name %d [%-10.10s]\n",n+1,name);
    }
    fclose (pFile);

    return 0;
}

示例代码,对格式化的数据进行文件读写

c++ 复制代码
#include <stdio.h>
struct S
{
	char arr[10];
	int num;
	float sc;
};

int main()
{
	struct S s = { "abcdef", 10, 5.5f };
	//对格式化的数据进行写文件
	FILE*pf = fopen("test.dat", "w");
	if (NULL == pf)
	{
		perror("fopen");
		return 1;
	}
	//写文件
	fprintf(pf, "%s %d %f", s.arr, s.num, s.sc);

	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}


int main()
{
	struct S s = {0};
	//对格式化的数据进行写文件
	FILE* pf = fopen("test.dat", "r");
	if (NULL == pf)
	{
		perror("fopen");
		return 1;
	}
	//读文件
	fscanf(pf, "%s %d %f", s.arr, &(s.num), &(s.sc));

	//打印
	fprintf(stdout, "%s %d %f\n", s.arr, s.num, s.sc);

	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

puts 将字符串写入 stdout

c++ 复制代码
Writes the C string pointed by str to the standard output (stdout) and appends a newline character ('\n').
将 str 指向的 C 字符串写入标准输出 (stdout) 并附加换行符 ( '\n' )

The function begins copying from the address specified (str) until it reaches the terminating null character ('\0'). This terminating null-character is not copied to the stream.
该函数从指定的地址 (str) 开始复制,直到到达终止 null 字符 ( '\0' ),此终止 null 字符不会复制到流中

Notice that puts not only differs from fputs in that it uses stdout as destination, but it also appends a newline character at the end automatically (which fputs does not).
请注意,它与 puts fputs 的不同之处在于它使用 stdout 作为目标,而且还会自动在末尾附加一个换行符(而 fputs 则不会)

gets 从 stdin 获取字符串

[NOTE: This function is no longer available in C or C++ (as of C11 & C++14)]
[注意:此功能在C或C++中不再可用(从C11和C++14开始)]

Reads characters from the standard input (stdin) and stores them as a C string into str until a newline character or the end-of-file is reached.
从标准输入 (stdin) 中读取字符,并将其作为 C 字符串存储到 str 中,直到到达换行符或文件末尾

The newline character, if found, is not copied into str.
如果找到换行符,则不会将其复制到 str 中

A terminating null character is automatically appended after the characters copied to str.
终止 null 字符会自动附加到复制到 str 的字符之后

Notice that gets is quite different from fgets: not only gets uses stdin as source, but it does not include the ending newline character in the resulting string and does not allow to specify a maximum size for str (which can lead to buffer overflows).
请注意,这与 fgets 完全不同:不仅 gets 使用 stdin 作为源,而且它在结果字符串中不包含结束换行符,并且不允许指定 str 的最大大小(这可能导致缓冲区溢出)
3.1.7 二进制输入 fread
c++ 复制代码
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
c++ 复制代码
Reads an array of count elements, each one with a size of size bytes, from the stream and stores them in the block of memory specified by ptr.
从流中读取 count 元素数组,每个元素的大小为 size 字节,并将它们存储在 ptr 指定的内存块中

The position indicator of the stream is advanced by the total amount of bytes read
流的位置指示器按读取的总字节数前进。

The total amount of bytes read if successful is (size*count).
如果成功,则读取的总字节数为 (size*count) 

代码示例

c++ 复制代码
struct S
{
	char arr[10];
	int num;
	float sc;
};

int main()
{
	struct S s = { "abcde", 10, 5.5f };
	//二进制的形式写
	file*pf = fopen("test.dat", "w");
	if (pf == null)
	{
		perror("fopen");
		return 1;
	}
	//写文件
	fwrite(&s, sizeof(struct s), 1, pf);

	//关闭文件
	fclose(pf);
	pf = null;

	return 0;
}
3.1.8 二进制读取 fwrite
c++ 复制代码
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
Writes an array of count elements, each one with a size of size bytes, from the block of memory pointed by ptr to the current position in the stream.
将 count 元素数组(每个元素的大小为 size 字节)从 ptr 指向的内存块写入流中的当前位置

The position indicator of the stream is advanced by the total number of bytes written.
流的位置指示器按写入的总字节数前进

Internally, the function interprets the block pointed by ptr as if it was an array of (size*count) elements of type unsigned char, and writes them sequentially to stream as if fputc was called for each byte.
在内部,该函数将 ptr 指向的块解释为一个 (size*count) 元素的数组,该数组是 unsigned char 类型的 (size*count) 元素,并将它们按顺序写入 stream,就像 fputc 为每个字节调用一样

示例代码

c++ 复制代码
struct S
{
	char arr[10];
	int num;
	float sc;
};

int main()
{
	struct S s = {0};
	//二进制的形式读
	FILE*pf = fopen("test.dat", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件
	fread(&s, sizeof(struct S), 1, pf);

	printf("%s %d %f\n", s.arr, s.num, s.sc);

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

3.2 下列函数区别

3.2.1 输入函数

scanf / fscanf / sscanf

都支持读取格式化数据,但支持的数据源不一样。scanffscanf 的子集,scanf 仅针对于标准输入 stdinfscanf 针对于 File*,而 sscanf 仅针对于字符串 char* str

3.2.2 输出函数

printf/fprintf/sprintf 同上类似

四 文件随机读写

4.1 fseek

根据文件指针的位置和偏移量来定位文件指针

c++ 复制代码
int fseek ( FILE * stream, long int offset, int origin );

代码示例

c++ 复制代码
/* fseek example */
#include <stdio.h>
int main ()
{
    FILE * pFile;
    pFile = fopen ( "example.txt" , "wb" );
    fputs ( "This is an apple." , pFile );
    fseek ( pFile , 9 , SEEK_SET );
    fputs ( " sam" , pFile );
    fclose ( pFile );
    return 0;
}

4.2 ftell

返回文件指针相对于起始位置的偏移量

c++ 复制代码
long int ftell ( FILE * stream );

代码示例

c++ 复制代码
/* ftell example : getting size of a file */
#include <stdio.h>
int main ()
{
    FILE * pFile;
    long size;
    pFile = fopen ("myfile.txt","rb");
    if (pFile==NULL) perror ("Error opening file");
    else
    {
        fseek (pFile, 0, SEEK_END);   // non-portable
        size=ftell (pFile);
        fclose (pFile);
        printf ("Size of myfile.txt: %ld bytes.\n",size);
    }
    return 0;
}

4.3 rewind

让文件指针的位置回到文件的起始位置

c++ 复制代码
void rewind ( FILE * stream );

代码示例

c++ 复制代码
/* rewind example */
#include <stdio.h>
int main ()
{
    int n;
    FILE * pFile;
    char buffer [27];
    pFile = fopen ("myfile.txt","w+");
    for ( n='A' ; n<='Z' ; n++)
        fputc ( n, pFile);
    rewind (pFile);
    fread (buffer,1,26,pFile);
    fclose (pFile);
    buffer[26]='\0';
    puts (buffer);
    return 0;
}

五 文本文件和二进制文件

根据数据的组织形式,数据文件被称为文本文件二进制文件

  1. 数据在内存中以二进制的形式存储,如果不加转换输出到外存 二进制文件
  2. 以 ASCII 字符形式存储的文件就是 文本文件

数据在内存中的存储

c++ 复制代码
#include <stdio.h>
int main()
{
    int a = 10000;
    FILE* pf = fopen("test.txt", "wb");
    fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
    fclose(pf);
    pf = NULL;
    return 0;
}

VS 查看二进制文件

右键打开方式选择二进制

查看内存存储

六 文件缓冲区

6.1 文件读取结束的判定

  1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )

    例如:fgetc 判断是否为 EOF ,fgets 判断返回值是否为 NULL

  2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数

    例如:fread 判断返回值是否小于实际要读的个数

错误使用的 feof(在文件读取过程中,不能用 feof 函数返回值直接来判断文件是否结束),feof 作用:当文件读取结束时,判断读取结束的原因是错误或到文件末尾

代码示例

c++ 复制代码
//文本文件例子
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int c; // 注意:int,非 char,要求处理 EOF
    FILE* fp = fopen("test.txt", "r");
    if(!fp) {
        perror("File opening failed");
        return EXIT_FAILURE;
   }
 	//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
    while ((c = fgetc(fp)) != EOF) // 标准C I/O 读取文件循环
   { 
       putchar(c);
   }
   //判断是什么原因结束的
    if (ferror(fp))
        puts("I/O error when reading");
    else if (feof(fp))
        puts("End of file reached successfully");
    fclose(fp);
}

//二进制文件例子
#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
    double a[SIZE] = {1.,2.,3.,4.,5.};
    FILE *fp = fopen("test.bin", "wb"); // 必须用二进制模式
    fwrite(a, sizeof *a, SIZE, fp); // 写 double 的数组
    fclose(fp);
    double b[SIZE];
    fp = fopen("test.bin","rb");
    size_t ret_code = fread(b, sizeof * b, SIZE, fp); // 读 double 的数组
    if(ret_code == SIZE) {
        puts("Array read successfully, contents: ");
        for(int n = 0; n < SIZE; ++n) printf("%f ", b[n]);
        putchar('\n');
    } else { // error handling
        if (feof(fp))
            printf("Error reading test.bin: unexpected end of file\n");
        else if (ferror(fp)) {
            perror("Error reading test.bin");
        }
    }
    fclose(fp);
}

6.2 文件缓冲区

ANSIC 标准采用 缓冲文件系统 处理数据文件,系统自动地在内存中为程序中每一个正在使用的文件开辟一块 文件缓冲区。从内存向磁盘输出数据会先送到内存中的缓冲区,缓冲区装满后一起写入磁盘。从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),再从缓冲区将数据送到程序数据区(程序变量等),缓冲区的大小右 C 编译器决定

示例代码

c++ 复制代码
#include <stdio.h>
#include <windows.h>
//VS2013 WIN10环境测试
int main()
{
    FILE*pf = fopen("test.txt", "w");
    fputs("abcdef", pf); // 数据被放在输出缓冲区
    printf("打开 test.txt 文件, 睡眠 10 秒 \n");
    Sleep(10000); // 发现文件没有内容
    printf("刷新缓冲区 \n");
    fflush(pf); // 刷新缓冲区, 将输出缓冲区的数据写到文件(磁盘)
    printf("再睡眠 10 秒, 再次打开 test.txt 文件 \n");
    Sleep(10000); // 发现文件有内容
    fclose(pf);
    //注:fclose在关闭文件的时候,也会刷新缓冲区
    pf = NULL;
    return 0;
}

结论:因缓冲区存在 C 操作文件时,需刷新缓冲区或在操作结束时关闭文件,否则可能导致文件读写有问题

相关推荐
hacker7072 小时前
探索数据的艺术:R语言与Origin的完美结合
开发语言·r语言·origin
炸鸡配泡面2 小时前
Qt 12.28 day3
java·开发语言
get_money_2 小时前
代码随想录38 322. 零钱兑换,279.完全平方数,本周小结动态规划,139.单词拆分,动态规划:关于多重背包,你该了解这些!背包问题总结篇。
java·开发语言·笔记·算法·动态规划
不听话的好孩子4 小时前
基于深度学习(HyperLPR3框架)的中文车牌识别系统-Qt开发UI
开发语言·qt·ui
SomeB1oody5 小时前
【Rust自学】7.6. 将模块拆分为不同文件
开发语言·后端·rust
向宇it5 小时前
【从零开始入门unity游戏开发之——C#篇36】C#的out协变和in逆变如何解决泛型委托的类型转换问题
java·开发语言·unity·c#·游戏引擎
犬余6 小时前
设计模式之迭代器模式:图书馆漫步指南
java·开发语言·设计模式·迭代器模式
Keven__Java6 小时前
Java开发-后端请求成功,前端显示失败
java·开发语言·前端
C7211BA6 小时前
基于python的扫雷游戏
开发语言·python·游戏
Just_Do_IT_OK6 小时前
Docker--Grafana
java·开发语言