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 30
int 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 1024
void 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"); //这里不要写错了,排错排了好久,操作的都是clntsock
while(!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 1024
void 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 1024
int 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 1024
int 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 1024
int 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);//发送EOF
fclose(writefp);
char buf[BUF_SIZE];
fgets(buf, sizeof(buf), readfp);
fputs(buf, stdout);
fclose(readfp);
return 0;
}