一、引言
write
和 read
是通用的系统调用;recv
和 send
是专为网络通信设计的扩展接口,数据系统调用,但是用户可以对其进行封装;fread
和 fwrite
是标准 IO 库函数。
二、系统调用
系统调用在调用时会通过内核缓冲区对数据进行缓冲。
延迟写入
当程序调用 write
函数时,数据并不会直接写入磁盘,而是暂存内核缓冲区,批量地写入磁盘。这样多次小文件写入会被合并为单次大块磁盘操作,减少磁头寻道时间。
预读
当程序调用 read
函数时,内核会预测程序的访问模式(如顺序读取),提前加载后续数据到缓冲区。例如读取第一块数据时,内核可能会预读第二、三块数据,后续访问可以直接从内存获取,避免多次磁盘寻址。
三、标准 IO 函数
标准 IO 函数具有良好的移植性,并且标准 IO 除了可以使用内核缓冲区进行缓冲之外,还可以利用用户层的标准缓冲区提高性能。常用的标准 IO 函数有:fread
、 fwrite
、fput
、fdopen
、 fileno
。
函数 | 作用 | 输入类型 | 输出类型 |
---|---|---|---|
fwrite |
写入数据块到流 | 数组指针、流指针 | 成功写入个数 |
fread |
从流读取数据块 | 数组指针、流指针 | 成功读取个数 |
fputs |
写入字符串到流 | 字符串指针、流指针 | 状态码 |
fdopen |
将文件描述符转换为流指针 | 文件描述符 | 文件流指针 |
fileno |
获取流的文件描述符 | 文件流指针 | 文件描述符 |
fopen |
打开文件流 | 字符串指针 | 文件流指针 |
fgets |
按行读取文本 | 文件流指针 | 状态码 |
函数原型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
// ptr:需要写入的数据的指针,size:每个元素的大小,nmemb:要写入的元素个数,stream:输出流指针
size_t fread(void *buffer, size_t size, size_t count, FILE *stream);
// buffer:接收数据的指针,size:每个元素的大小,count:要读的元素个数,stream:输入流指针
int fputs(const char *str, FILE *stream);
//str:以空字符结尾的数据指针指向需要发送的数据,stream:输出流指针
char *fgets(char *str, int n, FILE *stream);
//str:以空字符结尾的数据指针用来接收数据,n:最多读取 n-1 个字符,stream:输入流指针
FILE *fopen(const char *filename, const char *mode);
//filename:文件路径名,mode:流模式
FILE *fdopen(int fildes, const char *mode);
//fildes:已打开的文件描述符,mode:流模式(如 "r", "w+"),需要与文件描述符的原始模式一致
int fileno(FILE *stream);
//stream:文件流指针
标准 IO 函数通过合并多次小数据写入为一次系统调用,从而减少用户态与内核态的切换开销,提升效率。
标准 IO 的缺点:不容易进行双向通信,有时需要频繁调用 fflush 函数,需要以 FILE 结构体指针的形式返回文件描述符。
四、IO 流分离
流指的是“数据流动”,可以比喻为数据收发的一种桥梁。
流分离就是把输入流和输出流分离,这样可以降低代码实现难度并且提高缓冲性能。
使用标准 IO 函数进行网络通信。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#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, clnt_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=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);
clnt_adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);
readfp=fdopen(clnt_sock, "r");
writefp=fdopen(clnt_sock, "w");
fputs("FROM SERVER: Hi~ client? \n", writefp);
fputs("I love all of the world \n", writefp);
fputs("You are awesome! \n", writefp);
fflush(writefp);
fclose(writefp);
fgets(buf, sizeof(buf), readfp); fputs(buf, stdout);
fclose(readfp);
return 0;
}
如上是服务端代码,尝试进行半关闭。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#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 sock;
char buf[BUF_SIZE];
struct sockaddr_in serv_addr;
FILE * readfp;
FILE * writefp;
sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
serv_addr.sin_port=htons(atoi(argv[2]));
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
readfp=fdopen(sock, "r");
writefp=fdopen(sock, "w");
while(1)
{
if(fgets(buf, sizeof(buf), readfp)==NULL)
break;
fputs(buf, stdout);
fflush(stdout);
}
fputs("FROM CLIENT: Thank you! \n", writefp);
fflush(writefp);
fclose(writefp); fclose(readfp);
return 0;
}
如上是客户端代码,当服务端尝试半关闭后继续向服务端发送数据。
通过运行得知,服务端无法收到客户端第 36 行代码发送的数据,这意味着服务端尝试半关闭时会关闭整个套接字。
为了实现半关闭可以使用 dup
函数对文件描述符进行复制,然后分别使用读和写进行打开得到读和写的文件流。半关闭时使用 shutdown
函数对文件流指针指向的文件描述符(这样即使复制再多的文件描述符,套接字也会进入半关闭状态)。
服务端代码如下(客户端代码无需更改):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#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, clnt_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=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);
clnt_adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);
readfp=fdopen(clnt_sock, "r");
writefp=fdopen(dup(clnt_sock), "w");
fputs("FROM SERVER: Hi~ client? \n", writefp);
fputs("I love all of the world \n", writefp);
fputs("You are awesome! \n", writefp);
fflush(writefp);
shutdown(fileno(writefp), SHUT_WR);
fclose(writefp);
fgets(buf, sizeof(buf), readfp); fputs(buf, stdout);
fclose(readfp);
return 0;
}