12 I/O复用 &&13 多种I/O函数

**

十二章 I/O复用

**
12.1 基于I/O复用的服务器端

采用多进程的方法构建服务器端,在每次客户端请求连接时,就会创建新的进程。因为创建进程需要大量的运算和内存空间,由于每个进程都具有独立的内存空间,因此相互间的数据交换也要求采用相对复杂的方法(IPC属于相对复杂的通信方法)

I/O复用的方法构建服务器端会有效减少上面的缺点,但是该模型并不适用于所有情况,应根据目标服务器的特点采用不同的实现方法。


12.1.1 复用是什么??

“复用” 用行内话说时:在一个通信频道中传递多个数据信号技术
定义是:为了提高物理设备的效率,用最少的物理要素传递最多数据时采用的技术
复用可以分为 时分复用技术和频分复用技术


12.1.2 复用技术子服务端的应用
将复用技术与服务器端相配合,能够减少所需进程数,不论有多少客户端连接,提供服务的进程只有1 个,如下图所示(上图为多进程服务器端模型 下图为I/O复用服务器端模型)
在这里插入图片描述在这里插入图片描述
I/O复用的理解:
多进程服务器端类似一个学生配一个老师
I/O复用服务器端类似多个学生配一个老师,
老师必须通过确认有无学生举手,同样I/O复用服务器端进程需要确认举手(收到数据)的套接字。


12.2 理解select函数并实现服务器端
select 函数是最具代表性的实现I/O复用服务器端的方法;


12.2.1 select 函数的功能和调用顺序

使用select函数可以将多个文件描述符集中到一起统一监视,需要监视的内容如下:

是否存在套接字接收数据?
无需阻塞传输数据的套接字有哪些?
哪些套接字发生了异常?

(监视项称为事件:发生监视项对应的情况称为发生了事件;)


下面介绍select 函数的调用方法和顺序,如下图示:
在这里插入图片描述
分为三个步骤:
首先给出select函数声明:

#include <sys/select.h>
#include <sys/time.h>

int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout);
-> 成功时返回大于0的值,失败返回-1

maxfd:		监视对象 文件描述符数量
readset:	将所有关注“是否存在待读数据”的文件描述符 注册到fd_set型变量,并传递其地址值
writeset:	将所有关注“是否可传输无阻塞数据”的文件描述符 注册到fd_set型变量,并传递其地址值
exceptset:	将所有关注“是否发生异常”的文件描述符 注册到fd_set型变量,并传递其地址值
timeout:	调用select函数后,为防止陷入无限阻塞的状态,传递超时(time-out)信息。
返回值:		发生错误时返回-1,超时返回0, 因发生关注的事件返回时,返回大于0的值,该值等于 发生事件的文件描述符的数量。

在这里插入代码片

12.2.2 步骤一:初始化过程

1)设置文件描述符
select函数可以同时监控多个文件描述符,或者说是套接字。我们需要将这些文件描述符集中到一起,同时进行分类,按照类别进行监视
这里的3个类别分别对应上面3点的监视内容:

接收
传输
异常

使用fd_set数组变量执行此项操作,对于每一类的监视,我们都采用这样的结构作为select函数的输入参数,
如下图,fd_set数组存有 0 和 1的位数组
在这里插入图片描述
其中存放了很多文件描述符,fd0,fd1,fd2…
如果该位置为1,则表示该文件描述符是监视对象,可见图中的情况,文件描述符1和3是监控对象。
由于是位数组,因此我们如果自己去对fd_set变量进行注册和更改,这会很繁琐

因此,我们采用下列的宏来完成:

FD_ZERO(fd_set fdset): 将fd_set变量的所有位初始化为0
FD_SET(int fd, fd_set fdset)
: 在参数fdset指向的变量中注册文件描述符fd的信息
FD_CLR(int fd, fd_set fdset): 从参数fdset指向的变量中清除文件描述符fd的信息
FD_ISSET(int fd, fd_set fdset)
: 若参数fdset指向的变量中包含文件描述符fd的信息,返回 真。
在这里插入图片描述


2) 设置监视范围(检查范围)及超时

#include <sys/select.h>
#include <sys/time.h>

int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout);
-> 成功时返回大于0的值,失败返回-1

maxfd:		监视对象 文件描述符数量
readset:	将所有关注“是否存在待读数据”的文件描述符 注册到fd_set型变量,并传递其地址值
writeset:	将所有关注“是否可传输无阻塞数据”的文件描述符 注册到fd_set型变量,并传递其地址值
exceptset:	将所有关注“是否发生异常”的文件描述符 注册到fd_set型变量,并传递其地址值
timeout:	调用select函数后,为防止陷入无限阻塞的状态,传递超时(time-out)信息。
返回值:		发生错误时返回-1,超时返回0, 因发生关注的事件返回时,返回大于0的值,该值等于 发生事件的文件描述符的数量。

在这里插入代码片

如上所示,select函数用来验证3种监视项的变化情况,根据监视项声明3个fd_set型变量,分别向其中注册文件描述符信息,并把变量的地址值传递到上述函数中的第二到第四个参数中。


那么如何设置监视范围 和 如何设定超时时间呢?

监视范围设置
这与select函数的第一个参数有关,select函数要求第一个参数传递监视对象文件描述符的数量
因此需要得到注册在fd_set变量中的文件描述符数量。

但是每次新建文件描述符时都会增加 1 ,所以只需要将最大的文件描述符值(最后创建的)再加1 传递到select函数中即可。

+1 是因为文件描述符从0开始(这个程序中创建的第一个文件描述符为0 第二个为1…)

超时设置
这与最后一个参数有关,其中timecal结构体的定义如下:

struct timeval
{
	long tv_sec;		//second
	long tv_usec;		//microseconds
};
在这里插入代码片

本来select函数只有在监视的 文件描述符发生变化时,才会返回。
如果没变化就进入阻塞状态。 指定超时时间就是为了防止这种无休止的阻塞

通过将 秒数 填入 tv_sec成员
将毫秒数 填入tv_usec成员,
将结构体变量的地址值传递给select函数的最后一个参数。

此时,如果监视的 文件描述符没有变化,在超过设置的时间后,将会返回 0
如果不想设置超时。让他一直阻塞,只需要传递NULL参数即可。


12.2.3 步骤二 三 :调用select 函数并查看结果

一些返回值对应的情况:
返回0:超时
返回-1:错误
返回大于0的整数:说明有这些文件描述符发生变化

(什么是文件描述符的变化:指监视的文件描述符中发生了相应的监视时间。 read 、 write、except)

select函数返回正整数时,如何知道是那些文件描述符发生变化了呢?
看下图中,调用select函数前后,fd_set变量的变化情况
在这里插入图片描述
由上图可知,select函数调用完成后,向其传递的fd_set变量中将发生变化。
原来为1 的所有位均变为0,但发生变化的文件描述符对应位除外。因此可以认为值仍为1的位置上的文件描述符发生了变化
(为1 代表的是这个文件描述符 是监视对象,也就是说,返回之后,只有发生变化的继续为监视对象,其他的都为0,也就是未注册状态)。


12.2.4 select 函数调用示例
select.c

#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 30

int main(int argc, char *argv[])
{
	/* code */
	// 首先定义一些变量
	fd_set reads, temps;
	int result, str_len;
	char buf[BUF_SIZE];
	struct timeval timeout;

// 初始化select参数变量
	FD_ZERO(&reads);			// 初始化reads 中的各位为0
	FD_SET(0, &reads);			// 设置reads中的第一个位置设置为 1,这个位置是控制台标准输入的文件描述符
/*
	timeout.tv_sec = 5;
	timeout.tv_usec = 5000;	// 不能在这里初始化 timeout变量的值, 
	                 因为调用select函数后其中的成员的值会被替换为 超时前剩余时间,因此在下面进行初始化
*/
	while(1)
	{
		temps = reads;//这里很重要,需要把监听的结构体变量进行保存,因为调用select函数之后,除发生变化的文件描述符外,剩余位都会变成0;
		timeout.tv_sec = 5;	//在这里进行timeout变量初始化,每次循环都会重新初始化变量的值。
		timeout.tv_usec = 0;
		result = select(1, &temps, 0, 0, &timeout);
	
	//1个文件描述符,temp结构体中的描述符监听read ,write和except没有,超时函数时最后的timeout
													// 监听read中的0位置 -》 代表控制台标准输入
													
		if(result == -1){			// 错误的情况
			puts("select() error!");
			break;
		}
		else if(result == 0){		// 超时的情况
			puts("Time out !");
		}
		else{						// 正常情况
			if(FD_ISSET(0, &temps))	// 如果temp中 包含有 0号文件描述符的信息
			{
				str_len = read(0, buf, BUF_SIZE);	// 从 0号文件描述符中读取信息。
				buf[str_len] = 0;
				printf("message from console: %s\n", buf);

			}
		}
	}
	return 0;
}
在这里插入代码片

12.2.5 实现I/O复用服务器端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 100

void error_handling(char* message);

int main(int argc, char *argv[])
{
	/* code */
	// 首先定义变量

	// 套接字相关变量
	int serv_sock, client_sock;
	struct sockaddr_in serv_addr, client_addr;
	socklen_t client_addr_sz;
	char buf[BUF_SIZE];


	// select监视相关变量
	fd_set reads, cpy_reads;
	struct timeval timeout;
	int fd_max, str_len, fd_num;

	if(argc!= 2){
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	// 初始化套接字
	serv_sock = socket(PF_INET, SOCK_STREAM, 0);
	if(serv_sock == -1){
		error_handling("socket() error");
	}
	memset(&serv_addr,0,sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_addr.sin_port  = htons(atoi(argv[1]));

	// 服务器套接字一条龙
	if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1){
		error_handling("bind() error");
	}
	if(listen(serv_sock, 5) == -1){
		error_handling("listen error");
	}

	// 初始化select参数变量
	FD_ZERO(&reads);			// 初始化reads 中的各位为0
	FD_SET(serv_sock, &reads);			// 设置reads中的第一个位置设置为 1,这个位置是控制台标准输入的文件描述符
	fd_max = serv_sock;

	while(1)
	{

		cpy_reads = reads;				// 这里很重要,需要把监听的结构体变量进行保存,因为调用select函数之后,处发生变化的文件描述符外,剩余位都会变成0;
		timeout.tv_sec = 5;			// 在这里进行timeout变量初始化,每次循环都会重新初始化变量的值。
		timeout.tv_usec = 5000;

		if((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout) )== -1)	// 1个文件描述符,temp结构体中的描述符监听read ,write和except没有,超时函数时最后的timeout
		{	// 错误的情况										// 监听read中的0位置 -》 代表控制台标准输入
			puts("select() error!");
			break;
		}
		if(fd_num == 0){		// 超时的情况
			puts("Time out !");
			continue;
		}
								// 正常情况
		for(int i = 0; i < fd_max + 1; i++)	// 对监视的每一个文件描述符进行循环
		{
			if(FD_ISSET(i, &cpy_reads))		// 在这里寻找有 接收数据的 文件描述符
			{
				if(i== serv_sock)		// 如果是服务器端文件描述符有接收数据   那就建立连接! 并把新的套接字放进我们的监视中
				{						// 证明有人进行了 connect 操作,我们看门的serv_sock有动静了
					client_addr_sz = sizeof(client_addr);
					client_sock = accept(serv_sock, (struct sockaddr*)&client_addr, &client_addr_sz);

					FD_SET(client_sock, &reads);	// 注册于客户端相连接的套接字
					if(fd_max < client_sock){		// 同时 第一个参数更新
						fd_max = client_sock;
					}

					printf("connected client:%d\n", client_sock);
				}

				else{					// 如果不是服务器端文件描述符有变动,那就是有数据来了,直接开读
					str_len = read(i, buf, BUF_SIZE);
					if(str_len == 0){	// 读完之后,如果读到了末尾的 EOF,那就清空监视中的文件描述符
						FD_CLR(i, &reads);
						close(i);
						printf("closed client: %d\n", i);
					}
					else{				// 如果read的不是结束符呢?  那就写入到buf中,回声!!!
						write(i, buf ,str_len);
					}
				}
			}
		}
	}
	// while 结束
	close(serv_sock);
	return 0;
}

void error_handling(char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
在这里插入代码片

**

十三章 多种I/O函数

**
之前是基于linux的read&write函数实现数据的I/O,现在使用send和recv以及 readv和writev函数实现

13.1 send&recv函数

13.3.1 Linux下的send&recv函数

send函数声明:

#include <sys/socket.h>

ssize_t send(int sockfd, const void* buf, size_t nbytes, int flags);
-> 成功时返回发送的字节数,失败时返回 -1

sockfd:	表示与数据传输对象的连接的套接字文件描述符
buf:	保存待传输数据的缓冲地址值
nbytes:	待传输的字节数
flags:	传输数据时指定的可选项信息
在这里插入代码片

recv函数声明:

#include <sys/socket.h>

ssize_t recv(int sockfd, void* buf, size_t nbytes, int flags);
-> 成功时返回接收的字节数(收到EOF时返回0),失败时返回 -1

sockfd:	表示与数据接收对象的连接的套接字文件描述符
buf:	保存待接收数据的缓冲地址值
nbytes:	可接收的最大字节数
flags:	接收数据时指定的可选项信息
在这里插入代码片

上面的send & recv函数中的最后一个参数flags是 收发数据时的可选项。该选项可利用位或(bit OR)运算(|运算符)同时传递多个信息。
具体的选项看下图:
在这里插入图片描述


下面讲解一下这里面的参数:

13.1.2 MSG_OOB 发送紧急消息
MSG_OOB(mesage out of band) 那什么是传输带外数据??
设立单独的发送方法和通道以发送紧急消息。

在下面这段代码中,我们通过调用wrtte函数和send函数进行对比,同时采用MSG_OOB参数,看看数据接收端到底会收到什么样的数据

oob_send.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define BUF_SIZE 30
void error_handling(char* message);

int main(int argc, char *argv[])
{
	/* code */
	// 首先我们创建 数据发送端必须的套接字一条龙
	int sock;
	struct sockaddr_in recv_addr;		// 数据发送目标地址
	if(argc!= 3){
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}

	sock = socket(PF_INET, SOCK_STREAM, 0);
	memset(&recv_addr, 0, sizeof(recv_addr));
	recv_addr.sin_family = AF_INET;
	recv_addr.sin_addr.s_addr = inet_addr(argv[1]);
	recv_addr.sin_port = htons(atoi(argv[2]));

	if(connect(sock, (struct sockaddr*)&recv_addr, sizeof(recv_addr)) == -1){
		error_handling("connect error!");
	}

	// 这里我们开始用write 和 send 函数进行对比
	write(sock, "123", strlen("123"));
	usleep(20);
	send(sock, "4", strlen("4"), MSG_OOB);
	usleep(20);
	write(sock, "567", strlen("567"));
	usleep(20);
	send(sock, "890", strlen("890"), MSG_OOB);

	close(sock);
	return 0;
}

void error_handling(char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);

	exit(1);
}

在这里插入代码片

对应的oob_recv.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>

#include <netinet/in.h>
#include <fcntl.h>

#define BUF_SIZE 30
void error_handling(char* message);
void urg_handler(int signo);

int acpt_sock, recv_sock;		// 声明全局套接字

int main(int argc, char *argv[])
{
	/* code */
	if(argc != 2){
		printf("usage: %s <port>\n", argv[0]);
		exit(1);
	}

	// 声明套接字相关变量
	
	struct sockaddr_in acpt_addr, recv_addr;
	socklen_t recv_addr_sz;
	char buf[BUF_SIZE];
	int str_len;

	// 声明信号相关变量
	struct sigaction act; 				// 注册信号函数 sigaction的输入参数
	int sigaction_state;				// 接收注册信号函数 sigaction的 返回值(0 成功/ -1失败)

	// 初始化信号变量
	act.sa_handler = urg_handler;	// 设置一下信号处理函数
	act.sa_flags = 0;					// 设置其他两个暂时用不上的为0
	sigemptyset(&act.sa_mask);

	// 并没有在这里注册信号,为什么呢? 后面看
	// sigaction_state = sigaction(SIGCHLD, &act, 0);

	// 初始化套接字、服务器地址啥的
	acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
	if(acpt_sock == -1){
		error_handling("socket() error");
	}
	memset(&acpt_addr,0,sizeof(acpt_addr));
	acpt_addr.sin_family = AF_INET;
	acpt_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	acpt_addr.sin_port  = htons(atoi(argv[1]));

	// 服务器套接字一条龙
	if(bind(acpt_sock, (struct sockaddr*)&acpt_addr, sizeof(acpt_addr)) == -1){
		error_handling("bind() error");
	}
	if(listen(acpt_sock, 5) == -1){
		error_handling("listen error");
	}

	// 客户端连接套接字
	recv_addr_sz = sizeof(recv_addr);
	recv_sock = accept(acpt_sock, (struct sockaddr*)&recv_addr, &recv_addr_sz);


	// 这里有新东西  为什么在这里才注册信号?
	fcntl(recv_sock, F_SETOWN, getpid());
	sigaction_state = sigaction(SIGURG, &act, 0);

	while((str_len = recv(recv_sock, buf, sizeof(buf), 0)) != 0)
	{
		if(str_len == -1){
			continue;
		}
		buf[str_len] = 0;
		printf("normal:");
		puts(buf);
		fflush(stdout);
	}

	close(recv_sock);
	close(acpt_sock);
	return 0;
}

void error_handling(char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);

	exit(1);
}

void urg_handler(int signo)
{
	int str_len;
	char buf[BUF_SIZE];
	str_len = recv(recv_sock, buf, sizeof(buf) -1, MSG_OOB);
	buf[str_len] = 0;
	printf("Urgent message: %s\n", buf);
}
在这里插入代码片

13.1.3 紧急模式工作原理MSG_OOB

MSG_OOB的真正作用在于:
督促数据接受对象尽快处理数据,并非优先处理某一部分的数据。也就是说,TCP的 “保持传输顺序”的传输特性依然成立。

MSG_OOB可选项状态下,数据传输过程是什么样子的呢?
在调用了send(sock, “890”, strlen(“90”), MSG_OOB);之后
紧急消息传输阶段的输出缓冲如图所示(假设前面的数据已经传出完成了)
在这里插入图片描述
字符串右侧偏移量为 3 的位置存有紧急指针,且紧急指针指向的偏移量之前的部分就是紧急消息。
也就是,实际只用一个字节表示紧急消息信息。可以通过下图中用于传输数据的TCP数据包(段)的结构看的更清楚。下图是设置URG的数据包
在这里插入图片描述
实际上TCP数据包有更多信息,但是这里只标注出来与我们有关的内容

URG = 1: 载有紧急消息的数据包
URG指针: 紧急指针位于偏移量为3 的位置。
但是我们无法得知,到底前面输出缓冲中的多少数据是真正的紧急数据,但是这并不影响什么。因为数据顺序是不会改变的,我们只需要加快这个过程。
换言之,紧急消息的意义在于督促消息处理,而非紧急传输形式受限的消息。


13.1.4 检查输入缓冲 MSG_PEEK+MSG_DONTWAIT

同时设置 MSG_PEEL + MSG_DONTWAIT 选项,验证输入缓冲中是否存在接收的数据。MSG_PEEK + MSG_DONTWAIT -> 瞥一眼不清空输入缓冲 + 就算没读到数据我也不阻塞。
MSG_PEEK 选项并调用recv函数时,即使读取了输入缓冲的数据也不会删除。
因此,MSG_PEEL经常和MSG_DONTWAIT合作,用于调用 以非阻塞方式验证待读数据存在与否的函数。

用示例说明:
peek_send.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

void error_handling(char* message);

int main(int argc, char *argv[])
{
	/* code */
	// 首先我们创建 数据发送端必须的套接字一条龙
	int sock;
	struct sockaddr_in send_addr;		// 数据发送目标地址
	if(argc!= 3){
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}

	sock = socket(PF_INET, SOCK_STREAM, 0);
	memset(&send_addr, 0, sizeof(send_addr));
	send_addr.sin_family = AF_INET;
	send_addr.sin_addr.s_addr = inet_addr(argv[1]);
	send_addr.sin_port = htons(atoi(argv[2]));

	if(connect(sock, (struct sockaddr*)&send_addr, sizeof(send_addr)) == -1){
		error_handling("connect error!");
	}

	write(sock, "123", strlen("123"));
	close(sock);
	return 0;
}

void error_handling(char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);

	exit(1);
}
在这里插入代码片

peek_recv.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define BUF_SIZE 30
void error_handling(char* message);

int main(int argc, char *argv[])
{
	/* code */
	if(argc != 2){
		printf("usage: %s <port>\n", argv[0]);
		exit(1);
	}

	// 声明套接字相关变量
	int acpt_sock, recv_sock;		
	struct sockaddr_in acpt_addr, recv_addr;
	socklen_t recv_addr_sz;
	char buf[BUF_SIZE];
	int str_len, state;


	// 初始化套接字、服务器地址啥的
	acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
	if(acpt_sock == -1){
		error_handling("socket() error");
	}
	memset(&acpt_addr,0,sizeof(acpt_addr));
	acpt_addr.sin_family = AF_INET;
	acpt_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	acpt_addr.sin_port  = htons(atoi(argv[1]));

	// 服务器套接字一条龙
	if(bind(acpt_sock, (struct sockaddr*)&acpt_addr, sizeof(acpt_addr)) == -1){
		error_handling("bind() error");
	}
	if(listen(acpt_sock, 5) == -1){
		error_handling("listen error");
	}

	// 客户端连接套接字
	recv_addr_sz = sizeof(recv_addr);
	recv_sock = accept(acpt_sock, (struct sockaddr*)&recv_addr, &recv_addr_sz);

	while(1)
	{
		str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_PEEK|MSG_DONTWAIT);
		if(str_len > 0){
			break;
		}
	}
	buf[str_len] = 0;
	printf("Buffering %d bytes:%s \n", str_len, buf);

	str_len = recv(recv_sock, buf, sizeof(buf) -1, 0);
	buf[str_len] = 0;
	printf("Read again: %s \n", buf);

	close(acpt_sock);
	close(recv_sock);
	return 0;
}

void error_handling(char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);

	exit(1);
}
在这里插入代码片

13.2 readv & writev 函数
这俩函数和read&&write 函数有什么区别呢?

13.2.1 使用readv & writev函数

功能概括:对数据进行整合传输及发送的函数

writev函数可以将分别保存在多个缓冲中的数据一并发送,通过readv函数可以由多个缓冲分别接受。因此适当使用这2个函数可以减少I/O函数的调用次数。
下面先介绍writev函数。

#include <sys/uio.h>
ssize_t writev(int filedes, const struct iovec* iov, int iovcnt);
-> 成功时返回发送的字节数,失败时返回-1

filedes:	(文件描述符)数据传输对象的套接字文件描述符。但是该函数并不只限于套接字,可以像read函数一样向其传递文件或标准输出描述符。
iov:		(数据地址+大小)iovec结构体数组的地址值,结构体iovec中包含发送数据的位置和大小信息。
iovcnt:	(数据数组个数!)向第二个参数中传递的数组长度。
在这里插入代码片

上述函数的第二个参数出现的数组iovec机构体的声明如下:

struct iovec
{
	void* iov_base;	// 缓冲地址(数组名)
	void* iov_len;	// 缓冲大小
}
在这里插入代码片

结构体 iovec 由保存 待发送数据的缓冲(char型数组)地址值 和 实际发送的数据长度信息构成。
我们通过下面的图片看看这个结构体的结构以及调用方法。
在这里插入图片描述
上图中的解释为:
输出目标:标准输出(控制台),输出内容及长度:ptr[0].iov
_base长度为3的空间数据 + ptr[1].iov_base长度为4的空间数据

用代码说明:
write.c

#include <stdio.h>
#include <sys/uio.h>

int main(int argc, char *argv[])
{
	/* code */
	struct iovec vec[2];
	char buf1[] = "ABCDEFG";
	char buf2[] = "1234567";

	int str_len;

	vec[0].iov_base = buf1;
	vec[0].iov_len = 3;

	vec[1].iov_base = buf2;
	vec[1].iov_len = 4;

	str_len = writev(1, vec, 2);
	puts("");
	printf("write bytes : %d \n", str_len);	
	return 0;
}
在这里插入代码片

readbv函数

#include <sys/uio.h>

ssize_t readv(int filedes, const struct iovec* iov, int iovcnt);
-> 成功时返回发送的字节数,失败时返回-1

filedes:	(文件描述符)传递接收数据的套接字文件描述符。
iov:		(数据地址+大小)数据保存位置和大小信息的iovec结构体数组的地址值
iovcnt:	(数据数组个数!)向第二个参数中传递的数组长度。
在这里插入代码片

代码示例:

#include <stdio.h>
#include <sys/uio.h>
#define BUF_SIZE 100
int main(int argc, char *argv[])
{
	/* code */
	struct iovec vec[2];
	char buf1[BUF_SIZE] = {0,};
	char buf2[BUF_SIZE] = {0,};

	int str_len;

	vec[0].iov_base = buf1;
	vec[0].iov_len = 5;

	vec[1].iov_base = buf2;
	vec[1].iov_len = BUF_SIZE;

	str_len = readv(0, vec, 2);
	printf("Read bytes:%d \n", str_len);
	printf("First message :%s \n", buf1);
	printf("Second message :%s \n", buf2);
	return 0;
}
在这里插入代码片

13.2.2 合理使用readv & writev函数

哪种情况下更适合使用readv和writev函数呢?

实际上:能用就用!

减少函数的调用次数能提高相应性能。更大程度上能够减少数据包的个数,
假设为了提高效率在服务器端阻止了Nagle算法
这时对于writev函数来说,更有价值,如下图,在关闭Nagle算法情况下
在这里插入图片描述
图中的待发送数据位于3个不同的地方,write函数需要3次
若为了提高速度关闭了Nagle算法,极有可能通过3个数据包传递数据
如果用writev将所有数据一次性写入输出缓冲,很有可能仅通过一个数据包传输数据

同时,如果将不同位置的数据按照发送顺序移动(复制)到一个大数组,并通过一次write函数调用传输。 这与调用writev函数效果相同!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr.liang呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值