六 高级IO函数
6.1略
6.2 dup函数和dup2函数
dup函数和dup2函数可以将标准输入重定向到一个文件,或者重定向到一个网络连接(CGI编程),dup与dup2的作用就是用于复制文件描述符
头文件: <unistd.h>
调用方法:
int dup(int file_descriptor);
int dup2(int file_descriptor_one, int file_descriptor_two)
dup函数创建一个新的文件描述符,与原有文件描述符指向相同的文件,管道,网络连接。
重点:dup返回的文件描述符总是取系统可用的最小整数值;dup2稍有不同,返回不小于file_descriptor_two的整数值。
调用失败后:返回-1, 并设置errno
note:dup(2)创建的文件描述符并不继承原文件描述符的属性。
CGI服务器:关于CGI服务的理解为:
Web服务器一般只用来处理静态文件请求,一旦碰到动态脚本请求,Web服务器主进程就会Fork创建出一个新的进程来启动CGI程序,也就是将动态脚本交给CGI程序来处理。启动CGI程序需要一个过程,如读取配置文件、加载扩展等。当CGI程序启动后会去解析动态脚本,然后将结果返回给Web服务器,最后由Web服务器将结果返回给客户端,之前Fork出来的进程也随之关闭。
CGI服务器程序清单
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.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]);
struct sockaddr_in address;
bzero(&address, sizeof(address));//置字节字符串前n个字节为零,包括‘/n’
address.sin_family = AF_INET;//地址族 AF_INET是使用IPv4地址
inet_pton(AF_INET, ip, &address.sin_addr);//sin_addr为32位ip地址//点分十进制转换为二进制整数
address.sin_port = htons(port);//sin_port:16位TCP/UDP端口号htons为网络字节序与
//主机字节序之间的转换
int sock = socket(PF_INET, SOCK_STREAM, 0);//1.网络通信域(IPv4)2.套接字通信类型(TCP通信)
//3.目前水平仅能设置为零
assert(sock >= 0);
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));//1.待绑定的套接字2.要
assert(ret != -1);//绑定的地方 3.大小
ret = listen(sock, 5);//2.等待连接队列的最大长度
assert(ret != -1);
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
//socket编程中的accept函数的第三个参数的长度必须和int的长度相同,于是便有了socklen_t
int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
if(connfd < 0){
printf("error is %d\n", errno);
}
else{
close(STDOUT_FILENO);
dup(connfd);
printf("abcd\n");
close(connfd);
}
close(sock);
return 0;
}
绑定端口和服务:
./cgi_server 0.0.0.0 9989
在本地或者同网段浏览器中访问:
http://localhost:9989/
note:localhost在不是本机的情况下也可以换成对应服务端的ip地址
显示结果:
运行逻辑:
1程序首先关闭标准输出文件描述符STDOUT_FILENO
2使用dup复试socket文件描述符,dup的返回值为1,因为关闭的标准输出文件描述符的值为1(根据前文提及的dup的特性).
3服务器输出到标准输出中内容就会发送到socket中,然后就显示在了客户端浏览器上
以上为CGI服务器的基本原理。
6.3 readv函数和writev函数
readv函数将数据从文件描述符读到分散的内存块中,即分散读;writev函数则将多块分散的内存数据一并写入文件描述符中,即集中写;他们所包含的头文件为<sys/uio.h>
函数原型为:
ssize_t readv(int fd, const struct iovec* vector, int count);
sszie_t writev(int fd, const struct iovec* vector, int count);
fd:被操作的目标文件描述符
vector参数为元素类型为iovec结构数组的vector数组,iovec描述一块内存结构区
count:vector数组的长度。即有多少块内存需要从内存数据需要读出或者写到fd中
成功:返回成功读取或者写入的字节数
失败:返回-1,并设置errno
以下示例程序为:
将http应答的头部信息(状态行,头部字段,空行)和文档内容分别放置在一块内存中,当发送的时候不需要将这两部分拼接在一起再送,而是可以使用writev函数将他们同时写出。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/uio.h>
#define BUFFER_SIZE 1024
//定义两种状态码和状态信息
static const char* status_line[2] = {"200 oK", "500 Internal server error"};
int main(int argc, char* argv[]){
if(argc <= 3){
printf("usage : %s ip_address port_number filename\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
//将目标文件作为程序的第三个参数输入
const char* file_name = argv[3];
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 sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
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{
//用于保存文件的状态行,头部字段和空行的缓存区
char header_buf[BUFFER_SIZE];
memset(header_buf, '\0', BUFFER_SIZE);//1.指针或者数组2.赋给buffer的值3.buffer的长度
//用于存放目标文件内容的应用程序缓存
char* file_buf;
//用于获取目标文件的属性,比如是否为目录,文件大小
struct stat file_stat;//stat是文件(夹)信息的结构体
//记录文件是否有效
bool valid = true;
//缓存区buffer目前已经使用多少字节的空间
int len = 0;
if(stat(file_name, &file_stat) < 0){//目标文件不存在{}
//stat函数1.文件路径2.缓存区-------返回值为0:正确 返回值为-1:目标文件不存在
valid = false;
}
else{
if(S_ISDIR(file_stat.st_mode)){//目标文件是一个目录,S_ISDIR判断一个路径是不是目录
valid = false;
}
else if(file_stat.st_mode & S_IROTH){//当前用户有读取目标文件的权限
//动态分配缓存区file_buf, 并指定其大小为目标文件大小+1 然后将文件读入file_buf中
int fd = open(file_name, O_RDONLY);
file_buf = new char [file_stat.st_size + 1];
memset(file_buf, '\0', file_stat.st_size + 1);
if(read(fd, file_buf, file_stat.st_size) < 0){
valid = false;
}
}
else{
valid = false;
}
}
//如果目标文件有效,则发送正确的HTTP应答
if(valid){
/*下面这部分内容将http应答的状态行,“Content-Length”头部字段和一个空行依次
加入header_buffer中*/
ret = snprintf(header_buf, BUFFER_SIZE - 1, "%s %s\r\n",
"HTTP/1.1", status_line[0]);//若返回成功则返回欲写入字符串的长度,出错则显示
//负数
len += ret;
ret = snprintf(header_buf + len, BUFFER_SIZE - 1 - len,
"Content-Length: %d\r\n", file_stat.st_size);
len += ret;
ret = snprintf(header_buf + len, BUFFER_SIZE - 1 - len, "%s", "\r\n");
struct iovec iv[2];//iovec定义了一个向量元素,有两个数据成员
iv[0].iov_base = header_buf;//其中缓冲区存放的是readv接收的数据,或者是writev
iv[0].iov_len = strlen(header_buf);//将要写入的数据
iv[1].iov_base = file_buf;
iv[1].iov_len = file_stat.st_size;
ret = writev(connfd, iv, 2);
}
else{//如果目标文件无效,则通知客户端
ret = snprintf(header_buf, BUFFER_SIZE - 1, "%s %s\r\n",
"HTTP/1.1", status_line[1]);
len += ret;
ret = snprintf(header_buf, BUFFER_SIZE - 1 - len, "%s", "\r\n");
send(connfd, header_buf, strlen(header_buf), 0);
}
close(connfd);
delete[] file_buf;
}
close(sock);
return 0;
}
1.生成可执行文件
g++ -std=c++11 -o writev_and_readv writev_and_readv.cpp
2.运行可执行文件
./writev_and_readv 0.0.0.0 9989 writev_and_readv.cpp
3.本地或者同网段浏览器输入:localhost:9989 note:localhost可以改为127.1 或者服务端ip地址
上述代码省略了HTTP请求的接收和解析,只是HTTP应答的发送,在发送的时候直接将文件作为第三各参数传递给服务器程序,一旦连接成功,就可得到该文件。
6.4 sendfile函数
sendfile在两个文件描述符之间传递数据,完全在内核中操作,避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率比较高
函数原型:
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
包含的头文件:<sys/sendfile.h>
int_fd 待读出内容的文件描述符
out_fd 待写入的文件描述符
offset 指定从文件的哪个位置开始读取
count 传输的字节数
成功 返回传输的字节数
失败 返回-1,并设置errno
特别注意 in_fd必须指向真实的文件,不能是socket或者管道,而out_fd必须是一个socket
代码示例:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.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 filename\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);//用来将参数fd所指向的文件状态复制到后一个参数代表的缓冲区中,
//与stat区别在于传入的参数为已经打开的文件描述符
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 sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
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.生成可执行文件 g++ -std=c++11 -o sendfile sendfile.cpp
2.执行 ./sendfile 0.0.0.0 9989 sendfile.cpp
3.浏览器执行:localhost:9989
得到以下结果:
6.5 mmap函数和munmap函数
mmap函数可以用于申请一块内存空间,可以将这段内存作为进程间通信的共享内存。也可以将文件直接映射其中,munmap函数则会释放掉有mmap函数创建的这段内存空间。
头文件:<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💴指定内存段的长度
prot👶设置内存段的访问权限(具体使用查看man手册)共有四种,可以按位或
flags💇控制内存内容被修改后的程序的行为(具体内容查看man手册),可以安位或
fd🗡被映射文件的文件描述符,一般通过open系统调用获得
offset⭕️指定从文件的何处开始映射,对于不需要读入整个文件的情况下
mmap:
成功标志🌞返回目标区域的指针,失败返回MAP_FAILED((void*)-1)
munmap
成功标志🌞返回0,失败标志返回-1并设置errno
6.6 splice函数
splice函数用于在两个文件描述符之间移动数据,也是零拷贝操作
头文件: <fcntl.h>
函数原型:
ssize_t splice(int fd_in, loff_t* off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flags)
fd_in:待输数据的文件描述符, 若fd_in为管道文件描述符, 那么off_in参数必须设置为NULL;若fd_in不是管道文件描述符,比如是socket,off_in表示从输入数据流的何处开始读取数据,此时如果off_in被设置为NULL,表示从从输入数据流的当前位置读入;
fd_out/off_cut;与前两个参数同属性,只是应用于输出数据流;
flag🐾控制数据如何移动。常用值及其含义查看man手册
特别注意:使用splice函数时。fd_in和fd_out至少有一个为管道文件描述符;
返回结果:
成功:返回移动字节的数量,可能返回0,表示没有数据需要移动
失败:返回-1,并设置errno,常见errno查看man手册
示例代码:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.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]);
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 sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
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);//创建管道
ret = splice(connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
assert(ret != -1);
ret = splice(pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
assert(ret != -1);
close(connfd);
}
close(sock);
return 0;
}
代码实现功能:实现了一个零拷贝的回射服务器,它将客户端发来的数据,原样返回客户端,类似echo的功能:
具体实现为🌲通过两次使用splice函数,定义一个管道pipefd,将客户端的内容读到pipfd[1]中,然后从pipefd[0]中读出该内容到客户端,实现了高效的回射服务,期间并未涉及用户空间与内核空间的数据拷贝。
1.编译代码,形成二进制文件 g++ -std=c++11 -o splice splice.cpp
2.启动服务,./splice 0.0.0.0 9989
3.在浏览器访问该端口 localhost:9989,就会发送http协议的头部信息,在浏览器就会返回发送的信息。
orz:也可以使用nc 命令访问:
1.在命令行中使用命令 nc 127.1 9989 发送任意一条信息, 就会接收回来并打印出来
6.7 tee函数
tee函数在两个文件描述符之间复制数据,也是零拷贝操作,它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作:
包含的头文件:<fcntl.h>
函数原型:
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
参数含义与splice相同,特别注意的是fd_in和fd_out必须都是管道文件描述符。
返回结果:
成功:返回在两个文件描述符中复制的字节数量,返回0表示没有复制任何数据
失败:返回-1,并设置errno
代码示例:
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
int main(int argc, char* argv[]){
if(argc != 2){
printf("usage: %s <file>\n", 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
ret = splice(STDIN_FILENO, NULL, pipefd_stdout[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
assert(ret != -1);
//将管道输出定向到文件描述符filefd上, 从而将标准输入的内容写入文件
ret = splice(pipefd_stdout[0], NULL, filefd, NULL, 32768, SPLICE_F_MOVE | SPLICE_F_MORE);
assert(ret != -1);
//将管道pipefd_stdout的输出定向到标准输出, 其内容和写入文件的内容完全一致
ret = splice(pipefd_stdout[0], NULL, STDOUT_FILENO, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
assert(ret != -1);
close(filefd);
close(pipefd_stdout[0]);
close(pipefd_stdout[1]);
close(pipefd_file[0]);
close(pipefd_file[1]);
return 0;
}
代码实现的功能:
同时输出数据到终端和文件的功能。
数据流向示意图:
6.8 fcntl函数
fcntl函数提供了对文件描述符的各种控制操作,fcntl函数是由POSIX规范制定的方法。
包含的头文件🤒<fcntl.h>
函数原型🦅
int fcntl(int fd, int cmd, …);
fd参数是被操作的文件描述符,cmd制定何种类型的操作,根据操作类型不同,可能还需要第三个可选参数,具体操作可查看man手册。
返回状态:
成功时:根据操作的不同而不同
失败时:返回-1, 并设置errno
在网络编程中,常用来将一个文件描述符设置为非阻塞的;
比如:
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;//返回旧的状态
}