Linux:多路转接(上)——select
目录
一、select接口
1.认识select系统调用
2.对各个参数的认识
二、编写select服务器
一、select接口
1.认识select系统调用
int select(int nfds, fd_set readfds, fd_set writefds, fd_set exceptfds, struct timeval* timeout);
-
头文件:sys/time.h、sys/types.h、unistd.h
-
功能:select负责IO中多个描述符等的那部分,该函数会在描述符的读写异常事件就绪时,提醒进程进行处理。
-
参数:后面讲。
-
返回值:返回值大于0表示有相应个文件描述符就绪,返回值等于0表示没有文件描述符就绪,超时返回,返回值小于0表示select调用失败。
2.对各个参数的认识
(1)struct timeval* timeout
它是一个struct timeval类型的类指针,定义如下:
struct timeval
{
time_t tv_sec; /* Seconds. */
suseconds_t tv_usec; /* Microseconds. */
};
内部有两个成员,第一个是秒,第二个是微秒。
这个timeout如果传参nullptr,默认select阻塞等待,只有当一个或者多个文件描述符就绪时才会通知上层进程去读取数据。
参数如果传入struct timeval timeout = {0, 0},秒和微秒时间都设置成0,此时select就使用非阻塞等待,需要程序员编写轮询检测代码。
参数如果设置了具体值,如struct timeval timeout = {5, 0},时间设置成5秒,select就会在5秒内阻塞等待,如果在5秒内有文件描述符就绪,则通知上层;如果没有则超时返回。
(2)int nfds
表示要等待的所有文件描述符中的最大值加一。
假设要等待3个文件描述符,分别为3、4和7,则传参时就需要传7+1=8给nfds。
(3)fd_set
fd_set是一个等待读取就绪文件描述符的位图。
它的每一个比特位代表一个文件描述符,比特位的状0和1表示该比特位是否被select监听。
下面就是fd_set位图的示意图,表示偏移量为1、3、5的三个文件描述符需要被select监视。
fd_set类型的大小为128字节,每个字节有8个比特位,所以fd_set类型能包含1024个比特位,也表明select最多能监视1024个文件描述符。
虽然我们知道fd_set属于位图结构,但是我们并不清楚其内部实现。
所以在对位图数据进行增删 查改时一定要使用系统提供的增删查改接口。
FD_ZERO(fd_set *fdset);——将fd_set清零,集合中不含任何文件描述符,可用于初始化
FD_SET(int fd, fd_set *fdset);——将fd加入fd_set集合
FD_CLR(int fd, fd_set *fdset);——将fd从fd_set集合中移除
FD_ISSET(int fd, fd_set *fdset);——检测fd是否在fd_set集合中,不在则返回0
(4)fd_set* reads
fd_set* reads、fd_set* writefds、fd_set* exceptfds中间的这三个参数属于输出型参数。
在这里我以fd_set* reads为例进行讲解。
fd_set* reads表示读位图,传递的参数表示需要被监视的文件描述符,而且select只关心是这些文件描述符内否有数据需要被读取。
假如说,我们定义了一个fd_set变量使用FD_SET将文件描述符1、3、5填入变量,最后将该变量的指针传入函数。
在select正常返回或超时返回时,它会更改这个变量。
比方说,select调用完成后将位图改为下面的样式,表明文件描述符1、3准备好了,可以由系统调用去读取。由于两个文件描述符就绪,所以返回值为2。
在下次进行select调用时,我们还能再次修改该位图,增加或减少需要监听的文件描述符。
select再次返回时,该位图依旧会被修改,从而指示在这一次调用后哪些文件描述符已经准备就绪。
也就是说,传参时这个位图代表需要监听的描述符,调用返回时这个位图代表已就绪的文件描述符。
fd_set* reads与fd_set* writefds、fd_set* exceptfds在使用上是一样的,只不过fd_set* writefds只关心进程向文件描述符中写数据的操作,而fd_set* exceptfds只关心该文件描述符是否出现了错误。
它们也会以同样的方式修改自己对应的fd_set变量,从而达到通知进程的目的。
二、编写select服务器
selectserver.hpp:服务器
socket.hpp:套接字
text.cpp:启动服务器
socket.hpp
/*
* @Author: 码农 2646995216@qq.com
* @Date: 2025-03-18 15:13:58
* @LastEditors: 码农 2646995216@qq.com
* @LastEditTime: 2025-03-18 23:49:48
* @FilePath: /netcomputer/socket.hpp
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
enum
{
Socket_false = 1, // 创建套接字失败
Bind_false, // 绑定失败
Listen_false, // 监听失败
Accept_false, // 等待客户端连接失败
Connect_false, // 连接服务器失败
};
const int backlog = 5;
class Sock
{
public:
// 构造函数
Sock(int listensockfd = -1)
: _listensockfd(listensockfd)
{
}
// 创建套接字
void Socket()
{
_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // AF_INET:代表IPv4,SOCK_STREAM:代表Tcp协议
if (_listensockfd < 0)
{
cout << "socket false" << endl;
exit(Socket_false);
}
cout << "socket success" << endl;
//打开端口复用保证程序退出后可以立即正常启动
int opt = 1;
setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
}
// 绑定,传端口号,是因为sockaddr_in需要,用来保存客户端的IP地址和端口号
void Bind(uint16_t port)
{
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; // 代表0.0.0.0
socklen_t len = sizeof(local);
int n = bind(_listensockfd, (struct sockaddr *)&local, len);
if (n < 0)
{
cout << "bind false" << endl;
exit(Bind_false);
}
cout << "bind success" << endl;
}
// 监听
void Listen()
{
int n = listen(_listensockfd, backlog);
if (n < 0)
{
cout << "listen false" << endl;
exit(Listen_false);
}
cout << "listen success" << endl;
}
// 如果没有客户端连接服务端,则accept会阻塞等待新连接,等待客户端的连接
// 可以通过Accept函数,拿到客户端的ip地址,端口号,用于网络通信的描述符sock
int Accept(string &clientip, uint16_t &clientport)
{
sockaddr_in addr;
socklen_t len = sizeof(addr);
// 调用accpt函数,会将客户端的数据保存在addr中(ip,port)
int sock = accept(_listensockfd, (struct sockaddr *)&addr, &len);
if (sock < 0)
{
cout << "accept false" << endl;
exit(Accept_false);
}
cout << "accept success" << endl;
// 将客户端的ip和port,放入
clientip = inet_ntoa(addr.sin_addr); // 将in_addr_t类型的ip转为char*类型的ip
clientport = ntohs(addr.sin_port); // 将其转为主机字节序
return sock;
}
// 连接服务器,这个ip和port是我们在启动客户端的时候输入的
bool Connect(string &ip, uint16_t &port)
{
sockaddr_in peer;
memset(&peer, 0, sizeof(peer));//初始化
peer.sin_family = AF_INET;
peer.sin_addr.s_addr = inet_addr(ip.c_str());
peer.sin_port = htons(port);
socklen_t len = sizeof(peer);
int n = connect(_listensockfd, (struct sockaddr *)&peer, len);
if (n < 0)
{
cout << "connect false" << endl;
//exit(Connect_false);
return false;
}
cout << "connect success" << endl;
return true;
}
//关闭网络文件适配符
void Close()
{
close(_listensockfd);
}
//获取网络文件适配符
int Getlistensockfd()
{
return _listensockfd;
}
// 析构函数
~Sock()
{
}
private:
int _listensockfd; // 网络文件适配符
};
selectserver.hpp
#include<iostream>
#include"socket.hpp"
#include <sys/select.h>
#include <sys/time.h>
const int fd_num_max=1024;
const uint16_t default_fd = -1;//将所有需要管理的文件描述符放入一个数组,-1是数组中的无效元素
class SelectServer
{
public:
//构造函数
SelectServer(uint16_t port=default_fd)
:_port(port)
{
//初始化数组
for(int i=0;i<fd_num_max;i++)
{
fd_array[i]=default_fd;
}
}
//初始化
void Init()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
}
//处理客户端发来的链接请求
void Accepter()
{
//客户端发来请求,建立链接
string clienttip;
uint16_t clientport=0;
int sock=_listensock.Accept(clienttip,clientport);
if(sock<0)
{
cout<<"Accepter false"<<endl;
return;
}
cout<<"Accepter success,sock: "<<sock<<endl;
//由于fd_array[0]存储了监听文件描述符,即从1开始
int pos=1;
for(;pos<fd_num_max;pos++)
{
//找到数组中没有,放入文件描述符的位置
if(fd_array[pos]!=default_fd)
continue;
else
break;
}
//数组中文件描述符存满了
if(pos==fd_num_max)
{
cout<<"server is full"<<endl;
close(sock);
}
else
{
//将客户端建立链接的位置放入数组中,以便下次存入位图中,让select监听该位置
fd_array[pos]=sock;
//打印数组中的文件描述符
PrintFd();
}
}
//处理客户端发来的信息
void Recver(int fd,int pos)
{
//用来存储客户端发来的数据
char buffer[1024];
ssize_t n=read(fd,buffer,sizeof(buffer)-1);//最后一个换行符不要
if(n>0)
{
buffer[n]=0;
cout<<"get a messge: "<<buffer<<endl;
}
else if(n==0)
{
cout<<"client quit,me too"<<endl;
close(fd);
//由于客户端退出了,即该文件描述符不需要被select监听,即将其关闭
fd_array[pos]=default_fd;
}
else
{
cout<<"read false"<<endl;
close(fd);
fd_array[pos]=default_fd;
}
}
//处理已经就绪的文件描述符
void Hander(fd_set& rfds)
{
for(int i=0;i<fd_num_max;i++)
{
int fd=fd_array[i];
//相等,代表该位置没有文件描述符,即跳过
if(fd==default_fd) continue;
//判断该文件描述符,有没有被设置进位图中
if(FD_ISSET(fd,&rfds))
{
//客户端要与服务器建立连接
if(fd==_listensock.Getlistensockfd())
{
Accepter();
}
else//代表该文件描述符,已经发来了信息,需要我们去读
{
Recver(fd,i);
}
}
}
}
//启动服务器
void Start()
{
int listensock=_listensock.Getlistensockfd();//保存监听套接字
fd_array[0]=listensock;将listen套接字放在第一个,因为在程序运行的全过程中都不会被修改
while(true)
{
fd_set rfds;//创建位图
FD_ZERO(&rfds);//将位图初始化
int maxfd=fd_array[0];//由于select的第一个参数,需要最大文件描述符+1
for(int i=0;i<fd_num_max;i++)
{
//如果这个位置等于-1,则说明该位置没有文件描述符,即该位置不需要被select监视
if(fd_array[i]==default_fd) continue;
//走到这里,说明该位置需要被监视,即将在位图中的该位置设置为1,代表该位置需要被监视
FD_SET(fd_array[i],&rfds);
//找到最大的文件描述符
if(maxfd<fd_array[i])
{
maxfd=fd_array[i];
cout<<"maxfd update"<<endl;
}
//第一个参数代表秒,第二个参数代表毫秒
//struct timeval timeout={5,0};代表每过多少时间,select就会读取一次,不会阻塞等待
//返回已经就绪的个数
int n=select(maxfd+1,&rfds,nullptr,nullptr,nullptr/*&timout*/);//非阻塞等待
switch(n)
{
case 0:
cout<<"time out"<<endl;
break;
case -1:
cout<<"select false"<<endl;
break;
default:
cout<<"get a new link!!"<<endl;
Hander(rfds);//去处理已经就绪了的文件描述符
break;
}
}
}
}
//打印
void PrintFd()
{
cout<<"online fd list: ";
for(int i=0;i<fd_num_max;i++)
{
if(fd_array[i]==default_fd)
continue;
cout<<fd_array[i]<<" ";
}
cout<<endl;
}
//析构函数
~SelectServer()
{
_listensock.Close();
}
private:
uint16_t _port; //端口号
Sock _listensock; //套接字
int fd_array[fd_num_max]; //用于储存所有需要管理的文件描述符的数组
};
text.cpp
#include "selectserver.hpp"
#include <memory>
int main(int argv,char* args[])
{
int port=stoi(args[1]);
std::unique_ptr<SelectServer> svr(new SelectServer(port));
svr->Init();
svr->Start();
return 0;
}
运行: