select的作用
系统提供select函数来实现多路复用输入/输出模型。
● select系统调用是用来让我们的程序同时监视多个文件描述符的状态变化;
● 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态变化;
select函数
#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds,
fd_set* exceptfds, struct timeval* timeout);
参数介绍
● nfds 是需要监视的最大文件描述符值+1;
● readfds,writefds,exceptfds 分别对应需要检测的可读文件描述符集合,可写文件描述符集合,异常文件描述符集合;
● timeout 用来设置 select 的等待时间。通常有以下三种设置方式:
NULL:阻塞式等待,select将一直被阻塞,直到某个文件描述符上发送了事件;
0:非阻塞式等待,调用select后检测文件描述符的状态,然后立即返回;
特定的时间值:阻塞式等待一定时间,若期间没有事件发生,select将超时返回;
fd_set结构
这个结构其实就是一个数组,不过这里是把数组当作位图来使用的。用位图中的位来表示要监视的文件描述符。
为了能将 fd_set 以位图的形式看待,底层提供了一组操作 fd_set 的接口,来比较方便的操作位图。
void FD_CLR(int fd, fd_set* set); //清除 set 中的 fd 文件描述符 —— 删除
void FD_ISSET(int fd, fd_set* set); //判断 set 中 fd 文件描述符是否被设置 —— 判断
void FD_SET(int fd, fd_set* set); //在 set 中设置 fd 文件描述符 —— 添加
void FD_ZERO(fd_set* set); //重置 set ,将所有位清零 —— 初始化
timeval结构
● tv_sec:表示秒数;
● tv_usec:表示微秒数(10^6 微秒 = 1 秒);
函数返回值
● 大于0:执行成功,返回所有集合中,文件描述符就绪的总数;
● 0:等待超时,在timeout时间内,没有文件描述符就绪;
● -1:执行出错,错误原因可通过 errno 错误码得知;
select工作原理
select 中的 readfds,writefds,exceptfds 是输入输出型参数,我们需要根据这三个集合中的输入输出内容,来判断哪些文件描述符已经就绪。
输入时
● 将存在需要我们关心相应事件的文件描述符,设置到对应的 fd_set 中;
● fd_set 的比特位的位置,代表哪一个 sock,比特位上的内容代表是否要关心;
输出时
● 对于被关心且已经就绪的文件描述符,在相应的 fd_set 中仍被设置,用来告诉用户哪些已经就绪;
● 对于被关心但未就绪的文件描述符,在相应的 fd_set 中会被清除,来告诉用户哪些还未就绪;
select核心功能
以读为例:
● 用户告知内核,你要帮我关心哪些 fd 上的读事件就绪;
● 内核告知用户,你所关心的 fd 中,哪些已经读事件就绪;
第三方数组
在 select 返回后,我们需要遍历历史的每个 fd ,并在 fd_set 中检测该 fd 上是否有事件就绪。
另外,select 返回后会把以前加入的,但并无事件发生的 fd 清空。意味着每次开始 select ,都要对 fd_set 进行重新设置。
所以,用户需要定义数组或其它数据结构,来把历史 fd 都保存起来。对于每个 fd_set(readfds,writefds,exceptfds),都需要它们自己对应的第三方数组。
综上,第三方数组 array 的作用是:
● 用于在 select 返回后,array 作为源数据和 fd_set 进行 FD_ISSET判断;
● 用于重新设置 fd_set 的数据源(FD_ZERO最先),扫描 array 的同时取得 fd 的最大值,将其加一作为 select 的第一个参数;
select优点
可以一次等待多个 fd ,在一定程度上,提高了 IO 的效率。
select缺点
● 每次都要重新设置 fd_set ;每次 select 返回,都要遍历 array ,对有效位置做检测;
● fd_set 的大小是有限的,所以 select 同时检测的 fd 是有上限的;
● select 底层需要轮询式的检测哪些 fd 上的哪些事件的就绪状态;
● select 可能会较为高频率的进行用户到内核,内核到用户的的拷贝问题;
基于select的服务器示例
Sock.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
return sock;
}
static void Bind(int sock, uint16_t port)
{
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
std::cerr << "bind error" << std::endl;
exit(3);
}
}
static void Listen(int sock)
{
if(listen(sock, 5) < 0)
{
std::cerr << "Listen error" << std::endl;
exit(4);
}
}
static int Accept(int sock)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(sock, (struct sockaddr*)&peer, &len);
if(fd >= 0)
{
return fd;
}
return -1;
}
static void Connect(int sock, std::string ip, std::string port)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons((stoi(port)));
server.sin_addr.s_addr = inet_addr(ip.c_str());
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
{
std::cout << "Connect Success!" << std::endl;
}
else
{
std::cout << "Connect Failed!" << std::endl;
exit(5);
}
}
};
select_server.cc
#include <iostream>
#include <sys/select.h>
#include <string>
#include "Sock.hpp"
#define NUM (sizeof(fd_set) * 8)
int fd_array[NUM];
void Usage(std::string proc)
{
std::cout << "Usage: "
<< "\n\t" << proc << " port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = (uint16_t)atoi(argv[1]);
int listen_sock = Sock::Socket();
Sock::Bind(listen_sock, port);
Sock::Listen(listen_sock);
//初始化fd_array
for (int i = 0; i < NUM; ++i)
{
fd_array[i] = -1;
}
//事件循环
fd_set rfds;
fd_array[0] = listen_sock;
for (;;)
{
FD_ZERO(&rfds);
int max_fd = fd_array[0];
//每次都需要重新填充fd_set
for (int i = 0; i < NUM; ++i)
{
if (fd_array[i] == -1)
{
continue;
}
FD_SET(fd_array[i], &rfds);
if (max_fd < fd_array[i])
{
max_fd = fd_array[i];
}
}
// struct timeval timeout = {5, 0};
int n = select(max_fd + 1, &rfds, nullptr, nullptr, nullptr); //阻塞等待
switch (n)
{
case -1:
std::cerr << "select error" << std::endl;
break;
case 0:
std::cout << "select timeout" << std::endl;
break;
default:
std::cout << "some fds have already" << std::endl;
for (int i = 0; i < NUM; ++i)
{
if (fd_array[i] == -1)
{
continue;
}
//对有效位置做检测
if (FD_ISSET(fd_array[i], &rfds))
{
std::cout << "sock: " << fd_array[i] << " 上面的读事件已经就绪" << std::endl;
//此时调用accept,read, recv时不会被阻塞
// listen套接字就绪
if (fd_array[i] == listen_sock)
{
std::cout << "listen_sock: " << listen_sock << " 有新连接到来" << std::endl;
// accept
int sock = Sock::Accept(listen_sock);
//获取新连接成功
if (sock >= 0)
{
std::cout << "listen_sock: " << listen_sock << " 获取新连接成功" << std::endl;
//在fd_array中找一个空位置存储新来的连接
int pos = 1;
for (; pos < NUM; ++pos)
{
if (fd_array[pos] == -1)
break;
}
// fd_array中还有位置
if (pos < NUM)
{
std::cout << "新连接: " << sock << " 已经被添加到fd_array[" << pos << "]的位置" << std::endl;
fd_array[pos] = sock;
}
// fd_array已满
else
{
std::cout << "服务器已经满载,关闭新连接" << std::endl;
close(sock);
}
}
//获取新连接失败
else
{
std::cout << "accept failed!" << std::endl;
}
}
//普通套接字就绪
else
{
//此时可以使用read, recv来读
//但本次读取不一定能将数据读完,因为存在粘包问题
std::cout << "普通连接: " << fd_array[i] << " 上有数据读取" << std::endl;
char recv_beffer[1024] = {0};
ssize_t s = recv(fd_array[i], recv_beffer, sizeof(recv_beffer) - 1, 0);
//读取数据成功
if (s > 0)
{
recv_beffer[s] = '\0';
std::cout << "client[" << fd_array[i] << "]# " << recv_beffer << std::endl;
}
//对方关闭连接
else if (s == 0)
{
std::cout << "sock: " << fd_array[i] << " 已关闭连接" << std::endl;
close(fd_array[i]);
std::cout << "已将fd_array[" << i << "]"
<< " 上的普通sock: " << fd_array[i] << " 去掉了" << std::endl;
fd_array[i] = -1;
}
//读取失败
else
{
std::cout << "recv: " << fd_array[i] << " 失败" << std::endl;
close(fd_array[i]);
std::cout << "已将fd_array[" << i << "]"
<< " 上的普通sock: " << fd_array[i] << " 去掉了" << std::endl;
fd_array[i] = -1;
}
}
}
}
break;
}
}
return 0;
}