(六)高级IO函数

六 高级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地址

2233

上述代码省略了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

得到以下结果:

2233

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协议的头部信息,在浏览器就会返回发送的信息。

2233

orz:也可以使用nc 命令访问:

1.在命令行中使用命令 nc 127.1 9989 发送任意一条信息, 就会接收回来并打印出来

2233

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;
}

代码实现的功能:

同时输出数据到终端和文件的功能。

数据流向示意图:

2233

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;//返回旧的状态
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值