目录
1. 初识select
1.1 select函数原型
1.2 理解select执行过程
1.3 socket就绪条件
1.4 select的特点
1.5 select优缺点
2. 基于select的多人聊天程序
server源代码:
client的登录:
结果演示:
Linux Network🌷
1. 初识select
- select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
- 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;
总结:
select只负责等待,read、recv、send、write、accept负责自己的核心的业务功能(读、写);
在这里我们会想,read、recv、send、write、accept也有等待的功能啊,但是这些系统调用接口只能等待一个fd;
1.1 select函数原型
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
- 参数nfds是需要监视的最大的文件描述符值+1;
- rdset、wrset、exset分别对应于需要检测的可读文件描述符的集合、可写文件描述符的集合及异常文件描述符的集合;
- 参数timeout的结构为timeval,用来设置select()的等待时间;
- 其中后四个参数都是输入输出型参数;
- NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
- 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生;
- 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回;
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int 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的全部位
fd_set的大小
fd_set是一个位图结构,它是有大小的,下面我们来查看一下Linux环境下的 fd_set 的大小:
#include <iostream>
#include <sys/select.h>
using namespace std;
int main()
{
cout << "sizeof(fd_set): " << sizeof(fd_set) << endl;
cout << "How many fd can opened by fd_set: " << sizeof(fd_set)*8 << endl;
return 0;
}
- 执行成功则返回文件描述词状态已改变的个数;
- 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回;
- 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的值变成不可预测。
- EBADF 文件描述词为无效的或该文件已关闭;
- EINTR 此调用被信号所中断;
- EINVAL 参数n 为负值;
- ENOMEM 核心内存不足;
1.2 理解select执行过程
* (1)执行 fd_set set; FD_ZERO(&set); 则 set 用位表示是 0000,0000;* (2)若 fd = 5, 执行 FD_SET(fd,&set); 后set 变为 0001,0000( 第 5 位置为 1);* (3)若再加入 fd = 2 , fd=1, 则 set 变为 0001,0011;* (4)执行select(6,&set,0,0,0)非阻塞等待;* (5)若 fd=1,fd=2 上都发生可读事件,则 select 返回,此时 set 变为 0000,0011;* 注意:没有事件发生的 fd=5 被清空;
select核心功能:以读为例,核心的两点:
1. 用户告知内核,你要帮我关心哪些fd上的读事件就绪;
2. 内核告知用户,你所关心的哪些fd上的读事件已经就绪;
1.3 socket就绪条件
- socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
- socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
- 监听的socket上有新的连接请求;
- socket上有未处理的错误;
- socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记
- SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
- socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
- socket使用非阻塞connect连接成功或失败之后;
- socket上有未读取的错误;
- socket上收到带外数据. 关于带外数据, 和TCP紧急模式相关(回忆TCP协议头中, 有一个紧急指针的字段),
1.4 select的特点
- 可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)=128,每bit表示一个文件 描述符,则我服务器上支持的最大文件描述符是512*8=1024;
- 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,
一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断;二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得 fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
1.5 select优缺点
select的优点:
- 可以一次等待多个fd,在一定程度上提高IO的效率;
select的缺点:
- 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便;;
- select底层需要轮询式的检测那些fd上的事件是否就绪;
- select可能会较为高频率的进行用户到内核,内核到用户的频繁拷贝问题;
- select支持的文件描述符数量太小;
关于select缺点:打开的文件描述符数量大小限制:
一个进程可以打开的文件描述符数量是有限的;
在一般虚拟机上,可以打开的文件描述符是32个,但内核也是支持扩展文件描述符的;
在云服务器上,可以打开的文件描述符数量如下:
虽然进程本身可以打开的文件描述符数量是有限的,但经过查看在云服务器上一个进程可以打开的文件描述符是100001个,因此select可以打开的文件描述符数量有限确实是select的一个缺点;
2. 基于select的多人聊天程序
server源代码:
- Sock.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
cerr << "socket error" << 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;
memset(&local, 0, sizeof(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)
{
cerr << "bind error!" << endl;
exit(3);
}
}
static void Listen(int sock)
{
if (listen(sock, 5) < 0)
{
cerr << "listen error !" << 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, uint16_t port)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip.c_str());
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
{
cout << "Connect Success!" << endl;
}
else
{
cout << "Connect Failed!" << endl;
exit(5);
}
}
};
- server.cc
#include <iostream>
#include <string>
#include <sys/select.h>
#include "Sock.hpp"
#define NUM (sizeof(fd_set) * 8)
int fd_array[NUM]; //内容>=0,合法的fd,如果是-1,该位置没有fd
static void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}
// ./select_server 8080
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);
for (int i = 0; i < NUM; i++)
{
fd_array[i] = -1;
}
// accept: 不应该,accept的本质叫做通过listen_sock获取新链接
// 前提是listen_sock上面有新链接,accept怎么知道有新链接呢??
// 不知道!!!accept阻塞式等待
// 站在多路转接的视角,我们认为,链接到来,对于listen_sock,就是读事件就绪!!!
// 对于所有的服务器,最开始的时候,只有listen_sock
//事件循环
fd_set rfds;
fd_array[0] = listen_sock;
for (;;)
{
FD_ZERO(&rfds);
int max_fd = fd_array[0];
for (int i = 0; i < NUM; i++)
{
if (fd_array[i] == -1)
continue;
//下面的都是合法的fd
FD_SET(fd_array[i], &rfds); //所有要关心读事件的fd,添加到rfds中
if (max_fd < fd_array[i])
{
max_fd = fd_array[i]; //更新最大fd
}
}
struct timeval timeout = {0, 0}; // 5s
// 我们的服务器上的所有的fd(包括listen_sock),都要交给select进行检测!!
// recv,read,write,send,accept : 只负责自己最核心的工作:真正的读写(listen_sock:accept)
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 << "有fd对应的事件就绪啦!" << std::endl;
for (int i = 0; i < NUM; i++)
{
if (fd_array[i] == -1)
continue;
//下面的fd都是合法的fd,合法的fd不一定是就绪的fd
if (FD_ISSET(fd_array[i], &rfds))
{
std::cout << "sock: " << fd_array[i] << " 上面有了读事件,可以读取了" << std::endl;
// 一定是读事件就绪了!!!
// 就绪的fd就在fd_array[i]保存!
// read, recv时,一定不会被阻塞!
// 读事件就绪,就一定是可以recv,read吗??不一定!!
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;
// 获取成功
// recv,read了呢?绝对不能!
// 新链接到来,不意味着有数据到来!!什么时候数据到来呢?不知道
// 可是,谁可以最清楚的知道那些fd,上面可以读取了?select!
// 无法直接将fd设置进select,但是,好在我们有fd_array[]!
int pos = 1;
for (; pos < NUM; pos++)
{
if (fd_array[pos] == -1)
break;
}
// 1. 找到了一个位置没有被使用
if (pos < NUM)
{
std::cout << "新链接: " << sock << " 已经被添加到了数组[" << pos << "]的位置" << std::endl;
fd_array[pos] = sock;
}
else
{
// 2. 找完了所有的fd_array[],都没有找到没有被使用位置
// 说明服务器已经满载,没法处理新的请求了
std::cout << "服务器已经满载了,关闭新的链接" << std::endl;
close(sock);
}
}
}
else
{
// 普通的sock,读事件就绪啦!
// 可以进行读取啦,recv,read
// 可是,本次读取就一定能读完吗?读完,就一定没有所谓的数据包粘包问题吗?
// 但是,我们今天没法解决!我们今天没有场景!仅仅用来测试
std::cout << "sock: " << fd_array[i] << " 上面有普通读取" << std::endl;
char recv_buffer[1024] = {0};
ssize_t s = recv(fd_array[i], recv_buffer, sizeof(recv_buffer) - 1, 0);
if (s > 0)
{
recv_buffer[s] = '\0';
std::cout << "client[ " << fd_array[i] << "]# " << recv_buffer << std::endl;
}
else if (s == 0)
{
std::cout << "sock: " << fd_array[i] << "关闭了, client退出啦!" << std::endl;
//对端关闭了链接
close(fd_array[i]);
std::cout << "已经在数组下标fd_array[" << i << "]"
<< "中,去掉了sock: " << fd_array[i] << std::endl;
fd_array[i] = -1;
}
else
{
//读取失败
close(fd_array[i]);
std::cout << "已经在数组下标fd_array[" << i << "]"
<< "中,去掉了sock: " << fd_array[i] << std::endl;
fd_array[i] = -1;
}
}
}
}
break;
}
}
return 0;
}
client的登录:
客户端使用 telnet 进行登录,如果没有的可以使用如下命令进行下载:
sudo yum install telnet
结果演示:
I/O多路转接之select终于告一段落了,下篇我们继续poll和epoll🎈
如果本篇博客对您有所帮助的话,还请点赞、收藏并关注我✨
才疏学浅,如果有所疏漏的话,还请评论指出!