标准I/O
常见标准I/O函数
fopen
fopen()函数主要用于对文件和终端的输入输出。
#include <stdio.h>
FILE *fopen(const char *filename, const char *mode);
-
filaname:指定的文件名,fopen会把它与一个文件流关联起来。
-
mode:指定文件的打开方式:
“r”/“rb”:只读方式打开
“w”/“wb”:只写方式打开,把文件长度截为0(你可把它想象为>)
“a”/“ab”:只写方式打开,新内容追加到文件尾(你可以想象成>>)
“r+”/“r+b”/“rb+”:以修改方式打开(读写)
“w+”/“w+b”/“wb+”:以修改方式打开,同时把文件内容截为0
“a+”/“a+b”/“ab+”:以修改方式打开,新内容追加到文件结尾
注意:linux会把所有文件都看成是二进制文件,所以那个"b"表示文件是二进制。
返回值:成功时返回一个FILE*指针,失败返回NULL值,NULL定义在stdio.h中。
fread
主要作用是从一个文件流里读取数据,数据从stream读到由ptr指定的数据缓冲区里面。
#include<stdio.h>
size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);
- ptr:数据读到ptr指定的缓冲区里面。
- size:每个数据记录的长度(类似与char,int,long,float之类,指代每次读取块的大小)。
- nitems:传输的记录个数。
- stream:指定要读取的数据缓冲区。
返回值:读到缓冲区的记录个数(非字节),如果读到文件尾,其返回值可能小于nitems,甚至可以是0(读取文件为空)。
fwrite
主要从stream获取数据记录写到ptr中,返回值是成功写入的记录个数。
#include<stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t items, FILE *stream);
它的接口和fread很像,参考fread。
fclose
关闭指定的文件流stream,使所有未写出的内容全部写出。
#include<stdio.h>
int fclose(FILE *stream);
- stream:指定要关闭的文件流stream。
返回值:如果成功返回0,失败返回EOF,同时会向全局变量errorn写入错误信息码。
fflush
fflush()函数的主要作用是把文件流的所有未写出的内容立刻写出。
#include<stdio.h>
int fflush(FILE *stream);
接口同fclose。
fseek
主要作用是在文件流里面为下一次读写指定位置。
#include<stdio.h>
int fseek(FILE *stream, long int offset, int whence);
-
stream:指定操作的文件流。
-
offset:指定位置。
-
whence:指定偏移方式,具体形式类似系统调用lseek。
返回值:成功返回0,失败返回-1,同时设置errno指出错误。
fgetc,getc,getchar
fgetc()从文件流取出下一个字节并把它作为一个字符返回,如果到达文件结尾或者出现错误的时候返回EOF。其中getchar()从标准输入读取数据。
#include<stdio.h>
int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar();
fputc,putc,putchar
fputc()函数把一个字符写道一个输出文件流中。如果成功返回写入的值,失败则返回EOF。其中puchar()函数将单个字符写道标准输出。
#include<stdio.h>
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar();
fgets和gets
fgets()函数从输入文件流stream读取一个字符串,并把读到的字符写到ptr指向的缓冲区,当遇到如下情况停止:遇到换行符,已经传输n-1个字符,或者到达文件尾。它会把遇到的换行符也传递到接收字符串里去,再加上一个表示结尾的空字节\0。
gets()函数从标准输入读取数据并丢弃遇到的换行符,它在接受字符串的尾部加上NULL。
#include<stdio.h>
char *fgets(char *ptr, int n, FILE *stream);
char *gets(char *ptr);
printf,fprintf,sprintf
printf系列函数能够对各种不同类型的参数进行格式化编排和输出。
printf函数把自己的输出送到标准输出。
fprintf函数把自己的输出送到一个指定的文件流。
sprintf函数把自己的输出和一个结尾空字符写到s指向的缓冲区。
#include<stdio.h>
int printf(const char *format, ...);
int sprintf(char *s, const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
-
format:指定输出的格式。
-
s:字符串缓冲区。
-
stream:指定的文件流。
scanf,fscanf和sscanf
scanf系列函数从一个文件流读取数据并写到指定地址的变量。
scanf函数将读入的值保存在对应的变量里。
#include<stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int ssacnf(const char *s, const char *format, ...);
文件流错误
通过检查文件流的状态来确定是否发生错误。
ferror函数测试一个文件流的错误标识,如果被设置返回一个非0值,否则返回0。
feof函数测试一个文件流的文件尾标识,如果被设置就返回一个非0值,否则返回0。
clearerr函数清楚有stream指定的文件流的文件尾标识和错误标识。
#include<stdio.h>
int ferror(FILE *stream);
int feof(FILE *stream);
void clearerr(FILE *stream);
文件流和文件描述符
每个文件流都有一个底层描述符相关联。
fileno函数可以确定文件流使用那个底层描述符。
fdopen函数在一个已经打开的文件描述符上创建一个新的文件流。
#include<stdio.h>
int fileno(FILE *stream);
FILE *fdopen(int fildes, const char *mode);
使用标准I/O函数
标准IO函数的两个优点:
- 标准I/O函数具有良好的移植性(Portability)。
- 标准I/O函数可以利用缓冲提高性能。
标准IO函数的两个缺点:
- 不容易进行双向通信。
- 有时可能频繁调用fflush函数。
- 需要以FILE结构体指针的形式返回文件描述符。
创建套接字时返回文件描述符,而为了使用标准I/O函数,只能将其转换为FILE结构体指针
,先介绍器转换方法。
利用fdopen函数转换为FILE结构体指针
#include <stdio.h>
FILE * fdopen(int fildes.const char * mode);
//成功时返回转换的FILE结构体指针,失败时返回NULL。
- fildes:需要转换的文件描述符。
- mode:将要创建的FILE结构体指针的模式(mode)信息。
上述函数的第二个参数与fopen函数中的打开模式相同。常用的参数有读模式"r"和写模式"w"。
示例说明
#include <stdio.h>#include <fcnt1.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;
}
fp = fdopen(fd,"w");
fputs("Network C programming ln", fp);
fclose(fp);
return 0;
}
利用fileno函数转换为文件描述符
#inc1ude <stdio.h>
int fileno(FILE * stream);
//成功时返回转换后的文件描述符,失败时返回-1。
示例说明
#include <stdio.h>
#include <fcnt1.h>
int main(void){
FILE *fp;
int fd=open("data.dat",o_WRONLYlO_CREATIO_TRUNC);
if(fd==-1)
{
fputs("file open error", stdout);
return -1;
}
printf("First file descriptor: %d \n", fd);
fp=fdopen(fd,"w");
fputs("TCP/IP SOCKET PROGRAMMING \n", fp);
printf("Second file descriptor: %d ln", fileno(fp));
fclose(fp);
return 0;
}
I/O分离问题
"流"分离带来的EOF问题
调用的fclose函数完全终止了套接字而不是半关闭。
解决办法:创建FILE指针前复制文件描述符即可,复制后另外创建1个文件描述符,然后利于各自的文件描述符生成读模式FILE指针和写模式FILE指针。这就为半关闭准备好了环境,因为套接字和文件描述符之间具有如下关系:销毁所有文件描述符后才能销毁套接字。
但是这样做只是准备了半关闭环境,要进入真正的半关闭状态需要特殊处理,还剩余的一个文件描述符可以同时进行I/O。
复制文件描述符
#include <unistd.h>
int dup(int fildes);
int dup2(int fildes,int fildes2);
//成功时返回复制的文件描述符,失败时返回-1。
- fildes:需要复制的文件描述符。
- fildes2:明确指定的文件描述符整数值。
复制文件描述符后"流"的分离
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#tdefine BUF_SIZE 1024
int main(int argc,char *argv[]){
int serv_sock,c1nt_sock;
FILE* readfp;
FILE* writefp;
struct sockaddr_in serv_adr, clnt_adr;socklen_t clnt_adr_sz;
char buf[BUF_SIZE]={0,};
serv_sock=socket(PF_INET,sOCK_STREAM,0);
memset(&serv_adr,0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=hton1(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr));
listen(serv_sock, 5);
clnt_adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock,(struct sockaddr*)&c1nt_adr,&c1nt_adr_sz);
readfp=fdopen(clnt_sock,"r");
writefp=fdopen(dup(clnt_sock),"w");
fputs("FROM SERVER: Hi~ client? ln", writefp);
fputs("I love all of the world ln", writefp);
fputs( "You are awesome! ln", writefp);
fflush(writefp);
shutdown(fileno(writefp), SHUT_WR);
fclose(writefp);
fgets(buf, sizeof(buf),readfp);
fputs(buf,stdout);
fclose(readfp);
return 0;
}
- 调用fdopen函数生成FILE指针。特别是针对dup函数的返回值生成
FILE指针。 - 针对fileno函数返回的文件描述符调用shutdown函数。因此,服务器端进入半关闭状态,并向客户端发送EOF。这一行就是之前所说的发送EOF的方法。调用shutdown函数时,无论复制出多少文件描述符都进入半关闭状态,同时传递EOF。
优于select的epoll
select复用方法其实由来已久,因此,利用该技术后,无论如何优化程序性能也无法同时接人上百个客户端(当然,硬件性能不同,差别也很大)。这种select方式并不适合以Web服务器端开发为主流的现代开发环境,所以要学习Linux平台下的epoll。
基于select的I/O复用技术速度慢的原因
最主要的缺点是:
- 调用select函数后常见的针对所有文件描述符的循环语句;
- 每次调用select函数时都需要向该函数传递监视对象信息。
改进措施:仅向操作系统传递1次监视对象,监视范围或内容发生变化时只通知发生变化的事项。
这种处理方式Linux支持方式是epoll,Windows的支持方式是IOCP。
select优点:
- 服务器端接入者少;
- 程序应具有兼容性。
实现epoll时必要的函数和结构体
能够克服select函数缺点的epoll函数具有如下优点,这些优点正好与之前的select函数缺点相反。
- 无需编写以监视状态变化为目的的针对所有文件描述符的循环语句;
- 调用对应于select函数的
epoll_wait
函数时无需每次传递监视对象信息。
epoll服务器端实现中需要的3个函数:
- epoll_create:创建保存epoll文件描述符的空间;
- epoll_ctl:向空间注册并注销文件描述符;
- epoll_wait:与select函数类似,等待文件描述符发生变化。
select方式和epoll方式的不同
-
select方式中为了保存监视对象文件描述符,直接声明了fd_set变量。但epoll方式下由操作系统负责保存监视对象文件描述符,因此需要向操作系统请求创建保存文件描述符的空间,此时使用的函数就是epoll_create。
-
为了添加和删除监视对象文件描述符,select方式中需要FD_SET、FD_CLR函数。但在epoll方式中,通过epoll_ctl函数请求操作系统完成。
-
select方式下调用select函数等待文件描述符的变化,而epoll中调用epoll_wait函数。
-
select方式中通过fd_set变量查看监视对象的状态变化(事件发生与否),而epoll方式中通过如下结构体epoll_event将发生变化的(发生事件的)文件描述符单独集中到一起。
结构体epoll_event
struct epol1_event{
__uint32_tevents;
epoll_data_t data;
}
typedef union epoll_data{
void * ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
}epoll_data_t;
声明足够大的epoll_event结构体数组
后,传递给epoll_wait函数时,发生变化的文件描述符信息将被填入该数组。因此,无需像select函数那样针对所有文件描述符进行循环。
epoll_create
#include <sys/epoll.h>
int epoll_create(int size);
//成功时返回epol1文件描述符,失败时返回-1。
- size:epoll实例的大小。
调用epoll_create函数时创建的文件描述符保存空间称为"epoll例程",但有些情况下名称不同,需要稍加注意。通过参数size传递的值决定epoll例程的大小,但该值只是向操作系统提的建议。换言之,size并非用来决定epoll例程的大小,而仅供操作系统参考。Linux 2.6.8之后的内核将完全忽略传入epoll_create函数的size参数,因为内核会根据情况调整epoll例程的大小。
epoll_create函数创建的资源与套接字相同,也由操作系统管理。因此,该函数和创建套接字的情况相同,也会返回文件描述符。也就是说,该函数返回的文件描述符主要用与于区分epoll例程。需要终止时,与其他文件描述符相同,也要调用close函数。