多路复用

发布时间 2023-09-12 11:48:13作者: 若达萨罗

多路复用

  1. 概念
    • 事先把需要监听的文件描述符加入到集合中,然后在规定的时间内或者无限时间进行等待,如果在规定时间内,集合内的文件描述符没有数据变化,则超时接受,就会进入下一个规定时间等待,一旦集合中的文件描述符有数据变化,则其它没有发生数据变化的文件描述符会被踢出到集合外
      阻塞IO:只能同时监听一个套接字
      非阻塞IO:一直询问,问IO口有没有数据到达。非常浪费cpu资源
      信号驱动:主要用于UDP通信

所谓多路复用,指的是通过某个特定的接口(select()、poll()等),来同时监听多路套接字,就达到不需要多进程与多线程,又可以处理多个阻塞套接字的目的。
对于套接字sockfd,其就绪状态有三种:
读就绪,写就绪,异常就绪

  1. 特点

    • 不再由应用程序自己去监听客户端连接。取而代之的是由内核去代替应用程序监视文件,并且可以同时对多个IO口进行监听。由多路复用实现的TCP服务器就叫多路IO转接服务器,也叫做多任务IO服务器
  2. 函数接口

#include <sys/select.h>
	// 函数作用:对集合中的文件描述符进行监听
	int select(
		int nfds,   // 参数1:nfds参数指定要测试的描述符的范围。在每个集合中检查第一个nfds描述符;那即,检查描述符集中从0到nfds-1的描述符。
		fd_set *restrict readfds,   // 参数2:如果readfds参数不是空指针,它指向fd_set类型的对象,该对象在输入时指定文件描述符要检查是否准备好读取,并在输出时指示哪些文件描述符准备好读取。
		fd_set *restrict writefds,   // 参数3:如果writefds参数不是空指针,它指向fd_set类型的对象,该对象在输入时指定文件描述符要检查是否准备写入,并在输出时指示哪些文件描述符准备写入。
		fd_set *restrict errorfds,   // 参数4:如果errorfds参数不是空指针,它指向fd_set类型的对象,该对象在输入时指定文件描述符要检查挂起的错误条件,并在输出时指示哪些文件描述符有挂起的错误条件。
		struct timeval *restrict timeout   // 参数5:设置等待时间,3种情况
							// 1.设置为NULL,永远等待下去,这个函数就阻塞,知道由文件描述符状态发生变化
							// 2.设置timeval,等待固定时间
							// 3.设置timeval里的时间均为0,检测文件描述符会立即返回,轮询-->非阻塞
	);
	// 返回值:成功返回状态发生变化的文件描述符的总数

struct timeval
{
	long tv_sec;
	long tv_usec
}

// 此宏将文件从set中删除描述符fd。删除一个不存在于set中的集合时无操作的,不会产生错误。
void FD_CLR(int fd , fd_set * set );
// select()根据以下描述的规则对sets的内容进行修改。在调用select()之后,可以使用 FD_ISSET() 宏来测试文件描述符是否仍然存在于集合中。 如果文件描述符 fd 存在于集合中,则 FD_ISSET() 返回非零值,否则返回零。
int  FD_ISSET(int fd , fd_set * set );
// 此宏将文件描述符fd添加到set中。添加一个已经存在于set中的集合时无操作的,不会产生错误。
void FD_SET(int fd , fd_set * set );
// 此宏用于清除(从中删除所有文件描述符)set。它应该被调用作为初始化文件描述符set的第一步。
void FD_ZERO(fd_set * set );

注意:

  1. select能监听的文件描述符个数受限于FD_SETSIZE,一般是1024,单纯改变进程打开的文件描述符个数,不能改变select监听文件个数
  2. 解决1024以下的客户端时用select是合适的,但如果连接的客户端过多,select采用大的事轮询,会大大降低服务器响应效率
  3. 可以进行跨平台

信号驱动

  1. 信号驱动原理
    • 所谓信号驱动,即用信号来驱使服务器妥善处理多个远端套接字,信号方式的思路比较简单;每当远端有数据到达,那么就在本端触发信号SIGIO,然后利用信号的异步特性来处理远端消息。
void my_fun(int sig)
{
	// 读取数据
}
signal(SIGIO, my_fun);

套接字sockfd------>只要是远端有数据到来,这个信号就会自动产生,捕捉信号就去执行函数读取数据出来

  1. 适用场景

    • 由于不管套接字接受何种数据,内核一律触发SIGIO,因此这种看似理想的方式,却不适合TCP,因为在TCP中,当客户端发来连接请求,普通数据,数据回执等情况就会触发信号,这就使得服务器端仅凭此信号无法知道下一步要做什么,因此信号驱动模型的服务器模型,一般只适合用于UDP协议
  2. 信号驱动的实现步骤

    1. 设置SIGIO的响应函数,信号SIGIO默认会杀死进程,因此必须要设其响应函数,当进程收到信号的时候,说明有数据到达,则在响应函数中接收数据即可
    2. 设置SIGIO的属主进程,信号SIGIO由内核针对套接字产生,而内核套接字可以在多个应用程序中有效(例如父子进程将套接字遗传给子进程),因此必定指定该信号属主。
    3. 给套接字设置信号触发模式,也就是让套接字工作在信号模式下。因此在默认情况下,套接字收到数据就不会触发SIGIO,必须将套接字文件描述符设定为异步工作模式,它才会触发该信号
    4. 设置套接字属主:fcntl()
#include <unistd.h>
#include <fcntl.h>

F_SETOWN(int)
int fcntl(sock_fd, F_SETOWN, getpid());--------------->sock_fd与自己的进程绑定在一起

5. 添加信号触发模式属性
int status = fcntl(sock_fd, F_GETFL);
status |= O_ASYNC;
fcntl(sock_fd, F_SETFL, status);

案例:

#include <stdio.h>
#include <strings.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>

#define SERVER_PORT 8080

int sock_fd;
struct sockaddr_in otherAddr;
int len = sizeof(otherAddr);
char buf[1024] = "";
void my_func(int sig)
{
	bzero(buf, sizeof(buf));
	bzero(&otherAddr, sizeof(otherAddr));
	recvfrom(sock_fd, buf, sizeof(buf), 0, (struct sockaddr*)&otherAddr, &len);
	printf("recv: %s\n", buf);
}


int main(int argc, char const *argv[])
{
	sock_fd = socket(PF_INET, SOCK_DGRAM, 0);
	if(-1 == sock_fd)
	{
		perror("socket");
		return -1;
	}ss

	struct sockaddr_in ownAddr;
	bzero(&ownAddr, sizeof(ownAddr));
	ownAddr.sin_family = AF_INET;
	ownAddr.sin_port = htons(SERVER_PORT);
	ownAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	bind(sock_fd, (struct sockaddr*)&ownAddr, sizeof(ownAddr));

	signal(SIGIO, my_func);
	fcntl(sock_fd, F_SETOWN, getpid());


	int status = fcntl(sock_fd, F_GETFL);
	status |= O_ASYNC;
	fcntl(sock_fd, F_SETFL, status);

	while(1)
		pause();

	return 0;
}