Linux select函数的使用


一、select函数

int select (
int __nfds,  //指定被监听的文件描述符总数
fd_set *__readfds, //可读事件文件描述符集合
fd_set *__writefds, //可写事件文件描述符集合
fd_set *__exceptfds, //异常事件文件描述符集合
struct timeval *__timeout //该函数的超时时间
);

1.fd_set结构体

typedef struct
  {
    /* XPG4.2 requires this member name.  Otherwise avoid the name
       from the global namespace.  */
#ifdef __USE_XOPEN
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
 } fd_set;

从文档中复制下来的,删除了一些没有用的东西

虽然看起来复杂,但其实就是一个整型数组,该整形数组中的每一位(bit)标记一个文件描述符,且有最大数量,一般来说最好不要超过1024个

由于是位操作,很麻烦,所以官方提供了四个宏

FD_ZERO

fd_set se;
FD_ZERO(&se)

该宏用于清楚fd_set结构体中所有的位

FD_SET

	fd_set se;
	FD_SET(1,&se);

该宏用于设置某位

FD_CLR

	fd_set se;
	FD_CLR(1,&se);	//清楚fs_set的位

该宏用于清除某位

FD_ISSET

	fd_set se;
	FD_ISSET(1,&se);	//测试fs_set的位是否被设置

该宏用于测试位是否被设置

2.timeval 结构体

struct timeval
{
  __time_t tv_sec;		/* Seconds.  */
  __suseconds_t tv_usec;	/* Microseconds.  */
};

该结构体有两个成员,一个为秒,一个为微妙

当这两个值均为0时,select函数将立即返回,如果传入NULL,则一直阻塞等待,直到某个文件描述符就绪

3.返回值

当select成功时,将返回就绪的文件描述符总数

如果超过时间没有任何文件描述符就绪,将返回0

如果失败,则返回-1,并设置errno

二、文件描述符就绪条件

网络编程中,下列情况被认为socket可读:

  • socket内核接收缓存区中的字节数大于或等于其低水位标记SO_RECVLOWAT.此时可以无阻塞的读该socket,并且返回字节数大于0
  • socket通信的对方关闭连接,此时对socket读操作将返回0
  • 监听socket上有新的连接请求
  • socket上有未处理的错误,此时可以使用getsockopt来读取和清除该错误

socket可写的情况:

  • socket内核发送缓存区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT。此时可无阻塞的写该socket,并且写操作返回字节数大于0
  • socket的写操作被关闭,对关闭了写操作的socket执行写操作将触发SIGPIPE信号
  • socket使用非阻塞connect连接成功或者失败之后
  • socket上有未处理的错误,此时可以使用getsockopt来读取和清除该错误

socket异常:

  • 只有当socket上接收到带外数据

三、使用

以下代码通过使用该函数实现了一个简陋的回声服务器

#include<iostream>
#include<unistd.h>
#include<assert.h>
#include<sys/wait.h>
#include<sys/select.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<sys/types.h>
#include<string.h>
using namespace std;
int main(int arg, char* argv[])
{	
	int sock_listen,sock_cli; //创建监听socket和与客户端通信socket
	sock_listen = socket(PF_INET,SOCK_STREAM,0); //创建socket
	if (sock_listen == -1) return -1;

	sockaddr_in addr;
	addr.sin_addr.s_addr = inet_addr("0.0.0.0");
	addr.sin_family = AF_INET;
	addr.sin_port = htons(8888);
	int ret=bind(sock_listen,(sockaddr*)&addr,sizeof(addr));
	if (ret == -1) return -1;

	ret=listen(sock_listen,5);
	if (ret == -1) return -1;

	fd_set rfd;
	FD_ZERO(&rfd);//清零
	FD_SET(sock_listen, &rfd); //将监听socket置信
	int maxFd = sock_listen; //记录当前最大文件标识值
	fd_set copy_rfd; //rfd的副本,因为每次调用完select都会改变原值,所以需要用一个副本
	int d = 0;
	while (true) {
		copy_rfd = rfd; //每次循环调用副本
		int ret = select(maxFd + 1, &copy_rfd, NULL, NULL, NULL); //根据副本等待读事件
		if (ret == -1) { //返回-1则代表失败,打印错误
			printf("%d:%s\n", errno, strerror(errno));
			break;
		}
		//循环所有文件标识位
		for (int i = 0; i <= maxFd + 1; i++) {
			if (!FD_ISSET(i, &copy_rfd)) continue; //遇到没有被设置的跳过,即该文件标识没有事件需要处理

			if (i == sock_listen) { //如果有需要处理的事件是监听socket上的,则必然为有客户端连接
				sockaddr_in tmpAddr;
				socklen_t len = sizeof(tmpAddr);
				sock_cli = accept(sock_listen, (sockaddr*)&tmpAddr, &len);
				if (sock_cli==-1) {
					printf("%d:%s\n", errno, strerror(errno)); //接收客户端连接出现错误,打印错误信息
					break;
				}
				FD_SET(sock_cli, &rfd); //将客户端通信socket置信
				if (maxFd < sock_cli) { //如果客户socket文件标识大于最大文件标识,则取代之
					maxFd = sock_cli;
				}

			}
			else {
				char buf[0xFF]{};
				size_t len = recv(sock_cli,buf,0xFF,0); //接收客户端数据
				if (len == -1) { //发生错误
					FD_CLR(sock_cli, &rfd); //清除客户端置位
					continue;
				}
				else if (len == 0) { //客户端断开连接
					FD_CLR(sock_cli, &rfd); //清除客户端置位
					continue;
				}
				cout << buf << endl;
				send(sock_cli,buf,len,0); //将接收到的数据发送回客户端
			}

		}
	}
}

下面简述一下该代码的流程:

  1. 首先,先做好前置工作:创建、绑定监听
  2. 然后就是要创建一个fd_set变量,然后使用上面介绍的宏,对它进行操作
  3. FD_ZERO将所有标志置零,FD_SET将监听套接字的标志置1,并且创建了一个fd_set变量的副本,方便后面操作
  4. 然后进入循环,通过select函数,我们就可以很方便的监听所有套接字(目前只有一个监听套接字)
  5. 一旦该函数返回了,就说明至少有一个套接字触发了事件,所以我们就需要对所有值进行遍历查找,是哪个套接字发生了变化
  6. 最后再根据具体情况对比,判断是哪个套接字,如果是监听套接字,那就只有一个别人连接上来的消息,所以就要在标志中新增一个客户端的标志
  7. 而如果是其它,那就是与客户端通信的套接字,又由于这里我们只进行监听了接收信息,那必然是收到消息了
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
下面是一个简单的 Linux `select` 使用实例,用于监听标准输入和标准输出是否有数据可读写。 ```c #include <stdio.h> #include <stdlib.h> #include <sys/select.h> #include <unistd.h> int main() { fd_set rfds, wfds; // 用于存储文件描述符集合的 fd_set 结构体 int maxfd = STDIN_FILENO + 1; // 待监视的最大文件描述符值加一 struct timeval timeout = {5, 0}; // 超时时间为 5 秒 while (1) { FD_ZERO(&rfds); // 将读文件描述符集合清零 FD_ZERO(&wfds); // 将写文件描述符集合清零 FD_SET(STDIN_FILENO, &rfds); // 将标准输入加入到读文件描述符集合 FD_SET(STDOUT_FILENO, &wfds); // 将标准输出加入到写文件描述符集合 // 调用 select 函数进行 I/O 多路复用 int ret = select(maxfd, &rfds, &wfds, NULL, &timeout); if (ret < 0) { perror("select error"); exit(1); } else if (ret == 0) { printf("timeout\n"); } else { // 判断标准输入是否可读 if (FD_ISSET(STDIN_FILENO, &rfds)) { char buf[1024]; int n = read(STDIN_FILENO, buf, sizeof(buf)); if (n < 0) { perror("read error"); exit(1); } else if (n == 0) { printf("EOF\n"); break; } else { printf("read: %s", buf); } } // 判断标准输出是否可写 if (FD_ISSET(STDOUT_FILENO, &wfds)) { char buf[1024] = "hello world\n"; int n = write(STDOUT_FILENO, buf, sizeof(buf)); if (n < 0) { perror("write error"); exit(1); } else { printf("write: %s", buf); } } } } return 0; } ``` 该程序通过 `FD_SET` 函数将标准输入和标准输出加入到读和写文件描述符集合中,然后调用 `select` 函数进行 I/O 多路复用。当标准输入可读或标准输出可写时,`select` 函数会返回,程序通过 `FD_ISSET` 函数判断哪个文件描述符可读或可写,并进行相应的读写操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

余识-

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

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

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

打赏作者

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

抵扣说明:

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

余额充值