C语言 select 函数允许进程指示内核等待多个事件的任意一个发生,并在一个或多个事件发生或指定时间才唤醒进程.
#include <sys/select.h>
#include <sys/time.h>
/**
* int select (int __nfds, fd_set *__restrict __readfds,
fd_set *__restrict __writefds,
fd_set *__restrict __exceptfds,
struct timeval *__restrict __timeout);
*
*/
返回:准备好描述符字的正数目,0为超时,-1为出错
一、参数说明
__nfds——传入参数,集合中所有文件描述符的范围,即最大文件描述符值+1
__readfds——传入传出参数,select调用时传入要监听的可读文件描述符集合,select返回时传出发生可读事件的文件描述符集合
__writefds——传入传出参数,select调用时传入要监听的可写文件描述符集合,select返回时传出发生可写事件的文件描述符集合
__exceptfds——传出参数,select返回时传出发生事件(包括可读和可写)中异常事件的文件描述符集合
__timeout——传入参数,设置select阻塞的时间。若设置为NULL,则select一直阻塞直到有事件发生;
若设置为0,则select为非阻塞模式,执行后立即返回;
若设置为一个大于0的数,即select的阻塞时间,若阻塞时间内有事件发生就返回,否则时间到了立即返回
二、 timeval 结构,提供秒数和毫秒数
struct timeval
{
long tv_sec; //秒
long tv_usec;//毫秒
}
1. timeval 设置空指针 NULL时,永远等待下去
2. timeval 设置大于0的值,等待固定时间返回
3. timeval 设置为0,立即返回
fd_set 可看作一个集合,存放可读、可写或异常事件的文件描述符。fd_set集合通常有以下四个宏来操作:
void FD_ZERO(fd_set *fdset); //清空fdset中所有文件描述符
void FD_SET(int fd,fd_set *fdset); //添加文件描述符fd到集合fdset中
void FD_CLR(int fd,fd_set *fdset); //将文件描述符fd从集合fdset中去除
int FD_ISSET(int fd,fd_set *fdset); //判断文件描述符fd是否在集合fdset中
select工作原理
传入要监听的文件描述符集合(可读、可写或异常)开始监听,select处于阻塞状态,当有事件发生或设置的等待时间timeout到了就会返回,返回之前自动去除集合中无事件发生的文件描述符,返回时传出有事件发生的文件描述符集合。
但select传出的集合并没有告诉用户集合中包括哪几个就绪的文件描述符,需要用户后续进行遍历操作。
select 优点
- 优点
- select的可移植性较好,可以跨平台;
- select可设置的监听时间timeout精度更好,可精确到微秒,而poll为毫秒。
- 缺点
- select支持的文件描述符数量上限为1024,不能根据用户需求进行更改;
- select每次调用时都要将文件描述符集合从用户态拷贝到内核态,开销较大;
- select返回的就绪文件描述符集合,需要用户循环遍历所监听的所有文件描述符是否在该集合中,当监听描述符数量很大时效率较低。
select案例
server 端
/*
* @Descripttion:
* @version: 1.0.0
* @Author: xiaoge
* @Date: 2023-05-25 06:22:00
* @LastEditors: xiaoge
* @LastEditTime: 2023-06-07 08:43:08
*/
#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <strings.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#define PORT 1234
#define BACKLOG 5
int main(int argc, char const *argv[])
{
int i, n, j, maxi;
int maxfd, listenfd, connfd, sockfd;
int nready, client[FD_SETSIZE - 1]; // 客户端请求连接最多1023,listen 占用一个
char buf[BUFSIZ], str[INET_ADDRSTRLEN];
struct sockaddr_in c_addr, s_addr;
socklen_t c_addr_len;
fd_set allset, readsset; // 定义监听描述符、发生事件描述符
bzero(&s_addr, sizeof(s_addr));
s_addr.sin_family = AF_INET;
s_addr.sin_addr.s_addr = inet_addr("0.0.0.0");
s_addr.sin_port = htons(PORT);
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bind(listenfd, (struct sockaddr *)&s_addr, sizeof(s_addr));
listen(listenfd, BACKLOG);
// 初始化最大文件描述符为监听listenfd
maxfd = listenfd;
maxi = -1;
for (i = 0; i < FD_SETSIZE; i++)
{
// 数组client存储文件描述符的个数,初始值-1
client[i] = -1;
}
// 初始化监听描述符
FD_ZERO(&allset);
// 监听描述符 添加集合
FD_SET(listenfd, &allset);
while (1)
{
// 监听文件符 设置监听
readsset = allset;
// 监听有无accept 客户端连接请求
nready = select(maxfd + 1, &readsset, NULL, NULL, NULL);
if (nready < 0)
{
printf("select error");
exit(0);
}
if (FD_ISSET(listenfd, &readsset))
{
c_addr_len = sizeof(c_addr);
connfd = accept(listenfd, (struct sockaddr *)&c_addr, &c_addr_len);
inet_ntop(AF_INET, &c_addr.sin_addr.s_addr, str, sizeof(str));
printf("received from %s at port %d\n",
str,
ntohs(c_addr.sin_port));
for (i = 0; i < FD_SETSIZE; i++)
{
// 将connfd赋值给client 数组第一个为-1的元素
if (client[i] < 0)
{
client[i] = connfd;
break;
}
}
if (i == FD_SETSIZE)
{
fputs("too many clients\n", stderr);
exit(1);
}
// 客户端connfd 设置到 总集合中
FD_SET(connfd, &allset);
// 更新最大文件描述符值
if (connfd > maxfd)
maxfd = connfd;
if (i > maxi)
maxi = i;
// 如果此时有事件发生且=1,
nready--;
if (nready == 0)
continue;
}
for (i = 0; i < maxi; i++)
{
sockfd = client[i];
// 客户端描述符失效
if (sockfd < 0)
continue;
if (FD_ISSET(sockfd, &readsset))
{
n = read(sockfd, buf, sizeof(buf));
if (n == 0) // 当客户端关闭连接,服务端也关闭连接
{
close(sockfd);
FD_CLR(sockfd, &allset); // 解除select对该已连接文件描述符的监控
client[i] = -1;
}
else if (n > 0)
{
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
sleep(2);
write(sockfd, buf, n);
}
--nready;
if (nready == 0)
break; // 跳出for循环,还在while中
}
}
}
close(listenfd);
return 0;
}
客户端
/*
* @Descripttion:
* @version: 1.0.0
* @Author: xiaoge
* @Date: 2023-05-25 06:22:06
* @LastEditors: xiaoge
* @LastEditTime: 2023-06-07 08:44:54
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
// 客户端、服务端都在一台主机上,所以直接用本机IP地址
#define SERV_IP "192.168.135.129"
#define SERV_PORT 1234
int main()
{
int n, cfd;
struct sockaddr_in serv_addr;
char buf[BUFSIZ];
//创建socket
cfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); // 将点十进制字节串转换为网络字节序
//客户端如果连接成功
connect(cfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
while (1)
{
//客户端等待输入
fgets(buf, sizeof(buf), stdin);
//客户端写入服务端
write(cfd, buf, strlen(buf));
//等待服务端返回
n = read(cfd, buf, sizeof(buf));
//客户端标准输出
write(STDOUT_FILENO, buf, n);
}
close(cfd);
return 0;
}