这里需要熟练掌握一些文件操作时使用的函数(fopen、feof、fgetc、fputs等) 啥是标准I/O函数呢? 下面列出一些常用的
fopen fclose FILE *fopen(const char *path,const char *mode); 其中,path是我们要打开的流,而mode就是我们打开文件的方式了,也就决定你所打开的文件将被怎样的去对待啦,有如下几种方式: "r":只读方式打开,打开的文件必须存在。 "r+" :读写方式打开,文件必须存在。 "w" : 只写方式打开,文件不存在则创建,文件存在则清空。 "w+" : 读写方式打开,文件不存在则创建,文件存在则清空。 "a" : 只写方式打开,追加的方式写到文件的尾部,文件不存在则创建。 "a+": 读写方式打开,文件不存在创建,从头开始读,从尾开始写。 fgetc fputc 功能: 从文件中读取字符。 头文件: #include <stdio.h> 函数原型: int fgetc(FILE *stream); 返回值: 返回所读取的一个字节。如果读到文件末尾或者读取出错时返回EOF 功能: 将字符写到文件中。 头文件: #include <stdio.h> 函数原型: int fputc(int c, FILE *stream); 返回值: 在正常调用情况下,函数返回写入文件的字符的ASCII码值,出错时,返回EOF(-1)。当正确写入一个字符或一个字节的数据后,文件内部写指针会自动后移一个字节的位置。EOF是在头文件 stdio.h中定义的宏。 fgets fputs fgets(); 功能: 从文件中读取字符串 头文件: #include <stdio.h> 函数原型: char *fgets(char *buf, int size, FILE *stream); 参数说明: *s 字符型指针,指向用来存储所得数据的地址。 size 整型数据,指明存储数据的大小。 *stream 文件结构体指针,将要读取的文件流。 返回值: 1.成功,则返回第一个参数buf; 2.在读字符时遇到end-of-file,则eof指示器被设置,如果还没读入任何字符就遇到这种情况,则buf保持原来的内容,返回NULL; 3.如果发生读入错误,error指示器被设置,返回NULL,buf的值可能被改变. fputs(); 功能: 向指定的文件写入一个字符串(不自动写入字符串结束标记符‘\0’) 头文件: #include <stdio.h> 函数原型: int fputs(const char *s, FILE *stream); 参数说明: *s s是字符型指针,可以是字符串常量,或者存放字符串的数组首地址。 返回值: 返回值为非负整数;否则返回EOF(符号常量,其值为-1)。 fread fwrite fread(); 功能: 从文件流中读数据,最多读取nmemb个项,每个项size个字节 头文件: #include <stdio.h> 函数原型: size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); 参数说明: *ptr 用于接收数据的内存地址 size 要读的每个数据项的字节数,单位是字节 nmemb 要读nmemb个数据项,每个数据项size个字节. *stream 文件流 返回值: 返回真实读取的项数,若大于nmemb则意味着产生了错误。另外,产生错误后,文件位置指示器是无法确定的。 若其他stream或buffer为空指针,或在unicode模式中写入的字节数为奇数, 此函数设置errno为EINVAL以及返回0. fwrite(); 功能: 向文件写入一个数据块 头文件: #include <stdio.h> 函数原型: size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); 返回值: 返回实际写入的数据块数目关于移植性: 其实所有的标准函数都具有这种性质。为了支持所有操作系统(编译器),这些函数都是按照ANSI C标准定义的。这不仅仅局限于网络编程,而是适用于所有编程领域。
关于利用缓冲提高性能: 使用标准I/O函数时会得到额外的缓冲支持。之前说过,在创建套接字时操作系统会为套接字生成用于I/O的缓冲,这两者是什么关系呢? 如下图所示: 上图可以看出,使用标书nI/O函数传输数据时,经过了2个缓冲。 例如:通过fputs函数传输字符串“xxxxx”时,数据先传递到标准I/O函数缓冲。然后数据将移动到套接字输出缓冲中,最后将字符串发送到对方主机。
套接字中的缓冲主要是为了实现TCP协议而设立的,例如TCP传输中数据没有接收到需要再次传递,这是就从保存有数据的输出缓冲中读取数据。
另一方面,使用标准I/O函数缓冲的主要目的是:为了提高性能。
那么如何提高性能呢?使用缓冲可以提高性能?为啥? 通常需要传输的数据越多,有无缓冲带来的性能差异越大 我们换个角度说,性能的提高可以从下面两个方面进程描述:
传输的数据量数据向输出缓冲移动的次数首先从数据量角度: 先提个问题:一个字节数据发送10次(10个数据包)与累计10个字节发送1尺的情况分别传输了多少个字节? 发送数据时使用的数据包中包含有头信息,头信息与数据大小无关,是按照一定的格式填入的。也就是说,假设一个头信息占用40个字节(实际更大): 前者:10 + 40 * 10 = 410 字节 后者:40 * 1 + 10 = 50字节。可以看到,这两种方式需要传递的数据量有很大的差别。
接着从向输出缓冲移动的次数: 前者移动10次花费的时间将近后者移动一次花费的10倍。
上面讲了为什么缓冲能够提升性能。下面我们实际来测试一下,看看效果是否和分析的一样。 分别利用标准I/O函数和系统函数编写文件复制程序,这主要是为了检验缓冲提高性能的程度。首先是利用系统函数复制文件的示例。
syscpy.c
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #define BUF_SIZE 3 int main(int argc, char *argv[]) { int fd1, fd2, len; char buf[BUF_SIZE]; fd1=open("news.txt", O_RDONLY); fd2=open("cpy.txt", O_WRONLY|O_CREAT|O_TRUNC); while((len=read(fd1, buf, sizeof(buf)))>0) write(fd2, buf, len); close(fd1); close(fd2); return 0; }下面是采用标准I/O函数复制文件的代码: 利用fputs和fgets函数复制文件,因此这是一种基于缓冲的复制。
stdcpy.c
#include <stdio.h> #define BUF_SIZE 3 int main(int argc, char *argv[]) { FILE* fp1; FILE* fp2; char buf[BUF_SIZE]; fp1 = fopen("news.txt","r"); fp2 = fopen("cyp.txt", "w"); while(fgets(buf, BUF_SIZE, fp1) != NULL) fputs(buf, fp2); fclose(fp1); fclose(fp1); return 0; }这两种方法在文件不大的情况下差异并不明显,如果文件越大,差别越明显。
在c语言中,打开文件时,如果希望同时进行读写操作,应该以 r+、w+、a+模式打开。 但是由于缓冲,每次切换读写工作状态时应该刁颖fflush函数。 这也会影响基于缓冲的性能提高。 而且为了使用标准I/O函数,需要FILE结构体指针。而创建套接字时默认返回的是文件描述符。因此需要将文件描述符转化为FILE指针。
如前所述,创建套接字时返回文件描述符,而为了使用标准I/O函数,只能将其转换为FILE结构体指针。下面介绍转换方法。
可以通过fdopen函数将创建套接字时返回的文件描述符转换为标准I/O函数中使用的FILE结构体指针
#include <stdio.h> FILE* fdopen(int fildes, const char* mode); -> 成功时返回转换为FILE结构体指针,失败时返回NULL. fildes:需要转换的文件描述符 mode:将创建的FILE结构体指针的模式(mode)信息上面第二个参数 mode与fopen函数中的打开模式相同。常用的参数有 读模式:“r” 和 写模式:“w”。下面简单使用一下
desto.c
#include <stdio.h> #define BUF_SIZE 3 int main(int argc, char *argv[]) { FILE* fp1; FILE* fp2; char buf[BUF_SIZE]; fp1 = fopen("news.txt","r"); fp2 = fopen("cyp.txt", "w"); while(fgets(buf, BUF_SIZE, fp1) != NULL) fputs(buf, fp2); fclose(fp1); fclose(fp1); return 0; }捋一下思路:
使用open函数得到的int类型的文件描述符使用fdopen函数将上一步得到的文件描述符进行转换,得到FILE* 类型的结构体指针利用新得到的结构体指针,使用系统函数fputs进行写入。接下来介绍与fdopen函数提供相反功能的函数,如下
#include <stdio.h> int fileno(FILE* stream); ->成功时返回转换后的文件描述符,失败时返回-1这个用法很简单,向该函数传递FILE指针参数返回相应文件描述符,接下来给出fileno函数的调用示例。
todes.c
#include <stdio.h> #include <fcntl.h> int main(void) { FILE *fp; int fd = open("data.dat",O_WRONLY|O_CREAT|O_TRUNC); if(fd == -1) { fputs("file open error", stdout); return -1; } printf("First file descripor:%d \n", fd); fp = fdopen(fd,"w"); fputs("tcp ip socket programming\n",fp); printf("Second file descriptor : %d\n", fileno(fp)); fclose(fp); return 0; }前面说了标准I/O函数的优缺点,以及文件描述符转换为FILE指针的方法。 下面将配合套接字进行操作,修改第四章的回声服务器以及回声客户端的代码,改为基于标准I/O函数的数据交换形式。未修改的代码看这里 第四章
服务器端:echo_stdserv.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 1024 void error_handling(char *message); int main(int argc, char *argv[]) { int serv_sock, clnt_sock; char message[BUF_SIZE]; int str_len, i; struct sockaddr_in serv_adr; struct sockaddr_in clnt_adr; socklen_t clnt_adr_sz; FILE * readfp; FILE * writefp; if(argc != 2){ printf("Usage:%s <port>\n", argv[0]); exit(1); } serv_sock = socket(PF_INET, SOCK_STREAM, 0); if(serv_sock == -1){ error_handling("socket error "); } memset(&serv_adr, 0 , sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); serv_adr.sin_port = htons(atoi(argv[1])); if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1){ error_handling("bind error"); } if(listen(serv_sock, 5) == -1){ error_handling("listen error"); } clnt_adr_sz = sizeof(clnt_adr); for(int i = 0;i < 5;i++) { clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz); if(clnt_sock == -1){ error_handling("accept error"); } else{ printf("Connected client %d\n", clnt_sock); } readfp = fdopen(clnt_sock, "r"); writefp = fdopen(clnt_sock, "w"); while(!feof(readfp)) { fgets(message, BUF_SIZE, readfp); fputs(message, writefp); fflush(writefp); } fclose(readfp); fclose(writefp); } close(serv_sock); return 0; } void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }客户端:echo_client.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 1024 void error_handling(char *message); int main(int argc, char *argv[]) { int sock; char message[BUF_SIZE]; int str_len; struct sockaddr_in serv_adr; FILE * readfp; FILE * writefp; if(argc!=3) { printf("Usage : %s <IP> <port>\n", argv[0]); exit(1); } sock=socket(PF_INET, SOCK_STREAM, 0); if(sock==-1) error_handling("socket() error"); memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family=AF_INET; serv_adr.sin_addr.s_addr=inet_addr(argv[1]); serv_adr.sin_port=htons(atoi(argv[2])); if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) { error_handling("connect error"); } else { puts("connected......"); } readfp = fdopen(sock, "r"); writefp = fdopen(sock, "w"); while(1) { fputs("Input message(Q to quit): ", stdout); fgets(message, BUF_SIZE, stdin); // 1 if(!strcmp(message,"q\n") || !strcmp(message,"Q\n")) break; fputs(message, writefp); // 2 fflush(writefp); fgets(message, BUF_SIZE, readfp); // 3 printf("Messgae from server: %s\n", message); } fclose(writefp); fclose(readfp); return 0; } void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }看起来和使用write 函数以及 read函数实现的效果是完全一样的,而且代码看起来好像简单了起来= - =
fflush函数是用来将输入或输出缓冲中的数据全部输出的函数。 上面的过程中,调用基于字符串fgets 、fputs函数提供服务,并调用fflush函数。标准I/O函数为了提高性能,内部提供额外的缓冲。 因此如果不调用fflush函数则无法保证立即将数据传输到客户端。
第四章的回声客户端在接受到数据后,需要将数据转化为字符串(数据的尾部插入0),但是上面代码中却没有这一过程。
因为使用标准I/O函数后可以按字符串单位进行数据交换。