0%

网络编程高级I/O函数

网络编程基础(3):Linux网络编程高级I/O函数

包含如下几个重点函数:pipe、dup/dup2函数readv/writev、sendfile、mmap/munmap、splice和tee函数fcntl
注意:这一章节主要是对,文件描述符的操作,有利于传输数据效率的提高。

总体框架

三类函数

  • 创建文件描述符的函数pipe、dup/dup2函数
  • 用于控制读写数据的函数readv/writev、sendfile、mmap/munmap、splice和tee函数。
  • 用于控制I/O行为和属性的函数fcntl函数

pipe函数

用于创建一个管道,用以实现进程间的通讯。
定义如下

1
2
3
4
5
6
7
#include <unistd.h>
int pipe(int fd[2]);

/*
成功时返回0,并将一对打开的文件描述符填入其参数指向的数组
失败时返回-1,并设置errno
*/

关键点

  • fd[0]只能用于读取管道数据, fd[1]只能用于向管道写入数据。如果想要实现双向通信,应该使用两个管道。
  • 若调用read读取时,为0意味着读到了文件结束标志(EOF)。同时若针对该管道的读取端fd[0]引用计数为0(意味着没有需要管道数据的进程),则针对写fd[1]进行write操作将失败,并引发SIGPIPE信号。
  • 管道默认大小是65536字节,可通过fcntl函数进行后期更改。
1
2
3
4
5
6
7
8
9
//直接创建双向管道的函数socketpair
#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int fd[2]);

/*
domain只能采用AF_UNIX,因为是本地进程间通信。
此时创建得到的这对文件描述符都是既可读又可写。
*/

dup/dup2函数

用于复制文件描述符

1
2
3
4
5
6
7
8
9
10
#include <unistd.h>
int dup(int fd);
int dup2(int fd1, int fd2);

/*
dup中的fd,是原有的fd文件描述符
dup2 中第二个参数为,指定的新的文件描述符。
该值将成为复制出的新的文件描述符值的最小值。
失败时返回-1,并设置errno
*/

readv&&writev函数

readv函数是从文件描述符读到分散的内存块中,即分散读。

writev函数则将多块分散的内存数据一并写入文件描述符,即集中写。

1
2
3
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec* vector, int count);
ssize_t writev(int fd, const struct iovec* vector, int count);

sendfile函数

sendfile函数在两个文件描述符之间直接传递数据(完全在内核中操作),从而避免数据拷贝,效率更高。

1
2
3
4
5
6
7
8
9
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t* offsert, size_t count);

/*
out_fd: 待写入内容的文件描述符
in_fd: 待读出内容的文件描述符
offsert: 指定从读入文件流的哪个位置开始读,若为空,采用默认起始位置
count: 指定在文件描述符之间传输的字节数
*/

关键点

  • in_fd必须指向真实的文件,不能是socket和管道。
  • out_fd则必须是socket。

程序测试

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//服务器端程序
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>

int main(int argc, char* argv[])
{
if( argc <= 3)
{
printf("usage: %s ip_address port_number backlog\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
const char* file_name = argv[3];

int filefd = open(file_name, O_RDONLY);
assert(filefd > 0);
struct stat stat_buf;
fstat(filefd, &stat_buf);

int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);

struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);

int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);

ret = listen(sock, 5);
assert(ret != -1);

struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);

int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
if(connfd < 0)
{
printf("errno is: %d\n", errno);
}
else
{
sendfile(connfd, filefd, NULL, stat_buf.st_size);
close(connfd);
}

close(sock);
return 0;
}

服务器端运行命令

1
2
gao@gao-VirtualBox:~/桌面/net_code$ gcc SendFile.c -o send_file
gao@gao-VirtualBox:~/桌面/net_code$ ./send_file 127.0.0.1 9090 context.txt

利用telnet客户端进行测试

1
2
3
4
5
6
7
gao@gao-VirtualBox:~/桌面/net_code$ telnet 127.0.0.1 9090
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
须知少时凌云志
曾许人间第一流
Connection closed by foreign host.

mmap&&munmap函数

mmap函数用于申请一段内存空间,可以将这段内存作为进程间通信的共享内存,也可以将文件直接映射到其中。

munmap函数则释放由mmap函数创建的这段内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <sys/mman.h>
void* mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *start, size_t length);

/*
start:可指定内存开始的位置,输入NULL会自动分配一个地址
length:指定内存段的长度
port:设置内存段的访问权限。
权限包含以下几种:PROT_READ(可读) PROT_WRITE(可写) PROT_EXEC(可执行) PROT_NONE(不可访问)

flags:控制内存段的行为
常见行为包含以下几种:
MAP_SHARED(进程间共享该内存)
MAP_PRIVATE(内存段为调用进程私有,不会反映到映射文件中)

fd:被映射文件对应的文件描述符,一般通过open调用获得
offset:指定文件从何处开始映射。
*/

/*
mmap函数成功时返回0,失败时返回MAP_FAILED,并设置errno。
munmap函数成功时返回0,失败时返回-1,并设置errno。
*/

splice函数

splice函数用于在两个文件描述符之间移动数据。也是零拷贝操作。

定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <fcntl.h>
ssize_t splice(int fd_src, loff_t *off_src, int fd_des, loff_t *off_des, size_t len, unsigned int flags);

/*
fd_in:待输入数据的文件描述符,如果是一个管道文件,那么off_in必须设为NULL
off_in:若不为管道文件,且不为NULL意味着从输入数据流的具体何处开始读取数据,设为NULL是默认位置。
fd_out:含义相近用于输出数据流
off_out:含义相近用于输出数据流
len:指定移动数据的长度
flags:控制数据如何移动,可设置为如下值:
SPLICE_F_MORE,给内核一个提示,后续的splice调用将读取更多的数据。
*/

/*
fd_in和fd_out必须有一个是管道文件描述符。
函数调用成功时返回移动的字节数,可能为0,表示没有数据需要移动。
失败时,返回-1,并设置errno。
*/

程序测试

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#define _GNU_SOURCE 1
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>

int main(int argc, char* argv[])
{
if( argc <= 2)
{
printf("usage: %s ip_address port_number \n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);

int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);

struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);

int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);

ret = listen(sock, 5);
assert(ret != -1);

struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);

int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
if(connfd < 0)
{
printf("errno is: %d\n", errno);
}
else
{
int pipefd[2];
assert(ret != -1);
ret = pipe(pipefd); //创建管道

//将connfd上数据定向到管道
ret = splice(connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MORE);
assert(ret != -1);

//再将管道中数据,回传给connfd
ret = splice(pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MORE);
assert(ret != -1);
close(connfd);
}

close(sock);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
#服务器执行
gao@gao-VirtualBox:~/桌面/net_code$ gcc Splice.c -o Splice
gao@gao-VirtualBox:~/桌面/net_code$ ./Splice 127.0.0.1 9090

#telnet客户端测试
gao@gao-VirtualBox:~/桌面/net_code$ telnet 127.0.0.1 9090
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
须知少时凌云志 曾许人间第一流
须知少时凌云志 曾许人间第一流
Connection closed by foreign host.

tee函数

tee函数在两个管道文件描述符之间复制数据,也是零拷贝操作。

定义如下

1
2
3
4
5
6
#include <fcntl.h>
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);

/*
含义可参考splice函数的参数。
*/

程序测试(利用splice和tee函数实现同时输出数据到终端和文件)

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
45
46
47
48
49
50
51
52
53
54
#define _GNU_SOURCE 1
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>
#include <fcntl.h>

int main(int argc, char* argv[])
{
if( argc != 2)
{
printf("usage: %s ip_address port_number backlog\n", basename(argv[0]));
return 1;
}

int filefd = open(argv[1], O_CREAT | O_WRONLY | O_TRUNC, 0666);
assert(filefd > 0);

//管道文件描述符,负责标准输入输出
int pipefd_stdout[2];
int ret = pipe(pipefd_stdout);
assert(ret != -1);

//管道文件描述符,负责实体文件
int pipefd_file[2];
ret = pipe(pipefd_file);
assert(ret != -1);

/*将标准输入内容输入管道pipefd_stdout, 一端为管道,采用splice*/
ret = splice(STDIN_FILENO, NULL, pipefd_stdout[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MORE);
assert(ret != -1);

/*将负责输入的管道内容,拷贝到负责实体文件的管道,双端管道,采用tee*/
ret = tee(pipefd_stdout[0], pipefd_file[1], 32768, SPLICE_F_NONBLOCK);
assert(ret != -1);

/*将负责文件的管道输出定向到具体文件描述符,一端是管道,采用splice*/
ret = splice(pipefd_file[0], NULL, filefd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MORE);
assert(ret != -1);

/*将负责输入的管道内容,拷贝到标准输出,一端是管道,采用splice*/
ret = splice(pipefd_stdout[0], NULL, STDOUT_FILENO, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MORE);
assert(ret != -1);

close(filefd);
close(pipefd_stdout[0]);
close(pipefd_stdout[1]);
close(pipefd_file[0]);
close(pipefd_file[1]);
return 0;
}
1
2
3
4
5
6
7
#运行程序
gao@gao-VirtualBox:~/桌面/net_code/第六章$ gcc Tee.c -o Tee
gao@gao-VirtualBox:~/桌面/net_code/第六章$ ./Tee context.txt
须知少时凌云志 曾许人间第一流 ##输入的内容
须知少时凌云志 曾许人间第一流 ##控制台返回内容
gao@gao-VirtualBox:~/桌面/net_code/第六章$ cat context.txt
须知少时凌云志 曾许人间第一流 ##将内容写入文件

fcntl函数

fcnti函数执行各种描述符的控制操作

定义如下

1
2
#include <fcntl.h>
int fcntl(int fd, int cmd, ....);

常用选项
常用选项
常用选项

经典的使用案例(设置文件描述符为非阻塞)

1
2
3
4
5
6
7
8
9
int setnonblocking(int fd){
/*获取旧的状态标志*/
int old_option = fcntl(fd, F_GETFL);
/*更改为非阻塞状态*/
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
/*返回旧的状态,防止以后恢复*/
return old_option;
}
1
2
3
4
5
6
//参考UNIX网络编程
int flag = fcntl(fd, F_GETFL, 0);
flag |= O_NONBLOCK;
fcntl(fd, F_SETFL, flag);
/*这种方式是直接设置,不考虑保留原状态*/
/*注意,不可以直接fcntl(fd, F_SETFL, O_NONBLOCK);,这会导致其余状态均丢失*/