1 标准I/O函数的优点
- C语言标准IO整理
1.1 标准I/O函数的两个优点
-
标准I/O函数具有良好的移植性。
-
标准I/O函数可以利用缓冲提高性能
从图中可以看出,使用标准I/O函数传输数据时,经过两个缓冲。例如,使用fputs函数传输字符串 “Hello” 时,首先将数据传递到标准I/O函数的缓冲。然后数据将移动到套接字输出缓冲,最后将字符串发送到对方主机。
既然知道了两个缓冲的关系,接下来再说明各自的用途。设置缓冲的主要目的是为了提高性能,但是套接字的缓冲主要是为了实现TCP协议而设立的。例如,TCP传输中丢失数据时将再次传递,而再次发送数据则意味着某地保存了数据。这个数据就存在套接字的输出缓冲。
实际上,缓冲并非在所有情况下都能带来卓越的性能。但是需要传输的数据越多,有无缓冲带来的性能差异越大,可以通过如下两种角度说明性能的提高。- 传输的数据量;
- 数据项输出缓冲区移动的次数;
比较一个字节的数据发送10次(10个数据包)的情况和累计10个字节发送一次的情况。发送数据时使用的数据包中含有头信息。头信息和数据大小无关,是按照一定的格式填入的。即使假设该头信息占用40个字节(实际更大),需要传递的数据量也存在较大差别。
- 1个字节 10次 40 ✖10 = 400字节
- 10个字节 1此 40 ✖ 1 = 40字节
另外,为了发送数据,向套接字输出缓冲区移动数据也会消耗不少时间,但这同样与移动次数有关。1个字节数据共移动10次花费的时间将此10个字节数据移动一次花费时间的10倍。
1.2 标准IO函数和系统函数之间的性能对比
我的news.txt才只有59.4kb大就已经有明显差距了
首先是利用系统函数复制文件的示例。
#include <stdio.h>
#include <fcntl.h>
#include <time.h>#define BUFF_SIZE 30int main(int argc, char* argv[]) {int fd1, fd2;int len;char buf[BUFF_SIZE];clock_t start, end;fd1 = open("news.txt", O_RDONLY);fd2 = open("cpy.txt", O_WRONLY | O_CREAT | O_TRUNC);start = clock();while (len = read(fd1, buf, sizeof(buf)) > 0) {write(fd2, buf, len);}end = clock();double duration = (double)(end - start) / CLOCKS_PER_SEC;printf("文件复制执行时间:%f\n", duration);close(fd1);close(fd2);return 0;
}
然后是标准IO
#include <stdio.h>
#include <time.h>#define BUFF_SIZE 3 //用最短数组长度构成int main(int argc, char* argv[]) {FILE* fp1;FILE* fp2;char buf[BUFF_SIZE];clock_t start, end;fp1 = fopen("news.txt", "r");fp2 = fopen("cpy.txt", "w");start = clock();while (fgets(buf, BUFF_SIZE, fp1) != NULL) {fputs(buf, fp2);}end = clock();double duration = (double)(end - start) / CLOCKS_PER_SEC;printf("文件复制执行时间:%f\n", duration);close(fp1);close(fp2);return 0;
}
1.3 标准IO的缺点
- 不容易进行双向通信
- 有时可能频繁调用fflush函数(切换读写工作状态时发生)
- 需要以FILE结构体指针的形式返回文件描述符
2 使用标准IO函数
如前所述,创建套接字时返回文件描述符,而为了使用标准IO函数,hi只能将其转化为FILE结构体指针。先介绍转换方法。
2.1 利用fdopen函数转换为FILE结构体指针
可以通过fdopen函数将创建套接字时返回的文件描述符转换为标准I/O函数中使用的FILE结构体指针。
#include <stdio.h>/**
* @param fildes 需要转换的文件描述符,mode为结构体指针的模式信息,常用的有"r", "w"
* @return 成功时返回FILE结构体指针,失败时返回NULL
*/
FILE *fdopen(int fildes, const char* mode);
#include <stdio.h>
#include <fcntl.h>int main() {FILE *fp;int fd = open("data.dat", O_WRONLY | O_CREAT | O_TRUNC);if (fd == -1) {fputs("file open failed \n", stdout);return -1;}fp = fdopen(fd, "w");fputs("Network C programming \n", fp);fclose(fp);return 0;
}
2.2 利用fileno函数转化为文件描述符
该函数功能与fdopen相反。
#include <stdio.h>int fileno(FILE* stream);
#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 \n", stdout);return -1;}printf("first file description: %d \n", fd);fp = fdopen(fd, "w");fputs("TCP/IP SOCKET PROGRAMMING \n", fp);printf("second file description: %d \n", fileno(fp));fclose(fp);return 0;
}
2.3 基于套接字的标准I/O函数使用
将第四章的回声客户端和服务端改为基于标准I/O函数的数据交换形式。
echo_stdserv.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>#define BUF_SIZE 1024void ErrorHandler(char* message) {fputs(message, stderr);fputc('\n', stderr);exit(1);
}int main(int argc, char* argv[]) {if (argc != 2) {printf("Usage : %s <port> \n", argv[0]);exit(1);}int serv_sock = socket(PF_INET, SOCK_STREAM, 0);if (serv_sock == -1) {ErrorHandler("socket error");}struct sockaddr_in serv_adr;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) {ErrorHandler("bind error");}if (listen(serv_sock, 5) == -1) {ErrorHandler("listen error");}struct sockaddr_in clnt_adr;socklen_t clnt_size = sizeof(clnt_adr);int clnt_sock;char msg[BUF_SIZE];int str_len;FILE* readfp;FILE* writefp;for (; ;) {clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_size);if (clnt_sock == -1) {ErrorHandler("accept error");} else {printf("Connected client %d \n", clnt_sock);}readfp = fdopen(clnt_sock, "r");writefp = fdopen(clnt_sock, "w"); //这里不要写错了,排错排了好久,操作的都是clntsockwhile(!feof(readfp)) {fgets(msg, BUF_SIZE, readfp);fputs(msg, writefp);fflush(writefp);}fclose(readfp);fclose(writefp); }close(serv_sock);return 0;
}
echo_stdclnt.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>#define BUF_SIZE 1024void ErrorHandler(char* message) {fputs(message, stderr);fputc('\n', stderr);exit(1);
}int main(int argc, char* argv[]) {if (argc != 3) {printf("Usage %s <IP><PORT>\n", argv[0]);}int sock = socket(PF_INET, SOCK_STREAM, 0);if (sock == -1) {ErrorHandler("socket error");}struct sockaddr_in serv_adr;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) {ErrorHandler("connect error");} else {printf("connected.....\n");}FILE* readfp = fdopen(sock, "r");FILE* writefp = fdopen(sock, "w");char message[BUF_SIZE];for (;;) {fputs("Input message(Q to quit): ", stdout);fgets(message, BUF_SIZE, stdin);if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) {break;}fputs(message, writefp);fflush(writefp);fgets(message, BUF_SIZE, readfp);printf("message from server: %s", message);}fclose(writefp);fclose(readfp);return 0;
}
3 分离IO流
3.1 2次I/O流分离
之前我们使用两种方法分离过I/O流。
1. 第一种是通过fork函数复制出一个文件描述符,以区分输入和输出中使用的文件描述符。虽然文件描述符本身并不根据输入和输出进行区分,但我们分开了2个文件描述符的用途,因此也属于“流”的分离;
2. 第二种是通过2次fdopen
函数的调用,创建读模式FILE指针和写模式FILE指针。即,我们分离了输入和输出工具,所以也属于“流”的分离。
3.2 分离“流”的好处
- 第一种分离方法的目的
- 通过分开输入过程代码和输出过程降低实现难度
- 与输入无关的输出操作可以提高速度
- 第二种分离方式的目的
- 为了将FILE指针按读模式和写模式进行区分
- 可以通过区分读写模式降低实现难度
- 通过区分I/O缓冲提高缓冲性能
3.3 “流”分离带来的EOF问题
我们在学习多进程服务端时,调用shutdown函数实现基于半关闭的EOF传递方法,此种“流”分离没有问题。但是基于fdopen函数的“流”则不同,我们不知道在这种情况下如何实现半关闭,因此有可能犯以下错误:
“半关闭?不是可以针对输出模式的FILE指针调用fclose函数吗?这样可以向对方传递EOF,变成可以接收数据但无法发送数据的半关闭状态。”
下面我们使用代码来进行验证
效果:客户端收到服务端的信息,服务端没收到客户端的Thanks。
- 服务端
seq_serv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024int main(int argc, char* argv[]) {int serv_sock = socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in serv_adr, clnt_adr;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]));bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr));listen(serv_sock, 5);int clnt_adr_size = sizeof(clnt_adr);int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_size);FILE* readfp = fdopen(clnt_sock, "r");FILE* writefp = fdopen(clnt_sock, "w");fputs("from server: hi client?\n", writefp);fputs("i love all the world! \n", writefp);fputs("怕了把?\n", writefp);fflush(writefp);fclose(writefp); //关闭写指针后,测试读指针是否还能正常工作char buf[BUF_SIZE];fgets(buf, sizeof(buf), readfp);fputs(buf, stdout);fclose(readfp);return 0;
}
- 客户端
seq_client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUF_SIZE 1024int main(int argc, char* argv[]) {int sock = socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in serv_adr;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]));connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr));FILE* readfp = fdopen(sock, "r");FILE* writefp = fdopen(sock, "w");char buf[BUF_SIZE];while (1) {if (fgets(buf, sizeof(buf), readfp) == NULL) {break;}fputs(buf, stdout);fflush(stdout);}fputs("FROM CLIENT: Thanks! \n", writefp);fflush(writefp);fclose(writefp);fclose(readfp);return 0;
}
4 文件描述符的复制和半关闭
4.1 中止流时无法半关闭的原因
从图中可以看出读模式和写模式的FILE指针都是基于同一文件描述符创建的。因此针对任意一个FILE
指针调用fclose
函数都会关闭文件描述符。
解决放哪也很简单,创建FILE指针之前先复制文件描述符即可。
但是这样只是准备好了半关闭环境,剩余的文件描述符仍然可以进行I/O,所以并没有发送EOF,因此还需要一些特殊处理。
4.2 复制文件描述符
通过下列两个函数之一完成。
#include <unistd.h>int dup(int fildes);
//fildes:需要复制的文件描述符;fildes2明确指定的文件描述符整数值
int dup2(int fildes, int fildes2);
dup.c
#include <stdio.h>
#include <unistd.h>int main() {int cfd1, cfd2;char str1[] = "hi~ \n";char str2[] = "it is a nice day~ \n";cfd1 = dup(1);cfd2 = dup2(cfd1, 7);printf("cfd1 = %d, fd2 = %d \n", cfd1, cfd2);write(cfd1, str1, sizeof(str1));write(cfd2, str2, sizeof(str2));close(cfd1);close(cfd2);write(1, str1, sizeof(str1));close(1);write(1, str2, sizeof(str2));return 0;
}
4.3 复制文件描述符后流的分离
更改seq的服务端,关闭文件指针后同时发送EOF。
因为发送了EOF,所以才能退出循环
seq_serv2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUF_SIZE 1024int main(int argc, char* argv[]) {int serv_sock = socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in serv_adr;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]));bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr));listen(serv_sock, 5);struct sockaddr_in clnt_adr;socklen_t clnt_size = sizeof(clnt_adr);int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_size);FILE* readfp = fdopen(clnt_sock, "r");FILE* writefp = fdopen(dup(clnt_sock), "w");fputs("from server: hi? \n", writefp);fputs("from server: love you \n", writefp);fflush(writefp);shutdown(fileno(writefp), SHUT_WR);//发送EOFfclose(writefp);char buf[BUF_SIZE];fgets(buf, sizeof(buf), readfp);fputs(buf, stdout);fclose(readfp);return 0;
}