dup函数和dup2函数可以将标准输入重定向到一个文件,或者重定向到一个网络连接(CGI编程),dup与dup2的作用就是用于复制文件描述符
头文件:
调用方法:
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#include #include #include #include #include #include #include #include int main(int argc, char* argv[]){ if(argc <=2 ){ printf("usage: %s ip_address port_numbern",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 %dn", errno); } else{ close(STDOUT_FILENO); dup(connfd); printf("abcdn"); 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函数则将多块分散的内存数据一并写入文件描述符中,即集中写;他们所包含的头文件为
函数原型为:
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#include #include #include #include #include #include #include #include #include #include #include #include #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 filenamen", 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 : %dn", errno); } else{ //用于保存文件的状态行,头部字段和空行的缓存区 char header_buf[BUFFER_SIZE]; memset(header_buf, ' ', 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, ' ', file_stat.st_size + 1); if(read(fd, file_buf, file_stat.st_size) < 0){ valid = false; } } else{ valid = false; } } //如果目标文件有效,则发送正确的HTTP应答 if(valid){ ret = snprintf(header_buf, BUFFER_SIZE - 1, "%s %srn", "HTTP/1.1", status_line[0]);//若返回成功则返回欲写入字符串的长度,出错则显示 //负数 len += ret; ret = snprintf(header_buf + len, BUFFER_SIZE - 1 - len, "Content-Length: %drn", file_stat.st_size); len += ret; ret = snprintf(header_buf + len, BUFFER_SIZE - 1 - len, "%s", "rn"); 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 %srn", "HTTP/1.1", status_line[1]); len += ret; ret = snprintf(header_buf, BUFFER_SIZE - 1 - len, "%s", "rn"); 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);
包含的头文件:
int_fd 待读出内容的文件描述符
out_fd 待写入的文件描述符
offset 指定从文件的哪个位置开始读取
count 传输的字节数
成功 返回传输的字节数
失败 返回-1,并设置errno
特别注意 in_fd必须指向真实的文件,不能是socket或者管道,而out_fd必须是一个socket
代码示例:
#include#include #include #include #include #include #include #include #include #include #include #include #include int main(int argc, char* argv[]){ if(argc <= 3){ printf("usage: %s ip_address port_number filenamen", 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 %dn", 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函数创建的这段内存空间。
头文件:
函数原型:
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函数用于在两个文件描述符之间移动数据,也是零拷贝操作
头文件:
函数原型:
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#include #include #include #include #include #include #include #include #include int main(int argc, char* argv[]){ if(argc <= 2){ printf("usage: %s ip_address port_numbern", 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: %dn", 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函数在两个文件描述符之间复制数据,也是零拷贝操作,它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作:
包含的头文件:
函数原型:
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
参数含义与splice相同,特别注意的是fd_in和fd_out必须都是管道文件描述符。
返回结果:
成功:返回在两个文件描述符中复制的字节数量,返回0表示没有复制任何数据
失败:返回-1,并设置errno
代码示例:
#include #include#include #include #include #include int main(int argc, char* argv[]){ if(argc != 2){ printf("usage: %s 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规范制定的方法。
包含的头文件裸
函数原型礪
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;//返回旧的状态
}



