目录
一、select
1.1 select初识
1.2 select函数
1.3 scoket就绪条件
1.4 select基本工作流程
1.5 select服务器
1.6 select的优点
1.7 select的缺点
1.8 select的适用场景
二、poll
2.1 poll函数
2.2 poll服务器
2.3 poll的优点 && 缺点
三、epoll
3.1 epoll初识
3.2 epoll相关系统调用
3.3 epoll工作原理
3.4 epoll服务器
3.5 epoll的优点
3.6 epoll的工作方式
select是系统提供的一个多路转接接口
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数说明:
参数timeout的取值:
返回值说明:
select调用失败时,错误码可能被设置为:
fd_set结构
fd_set结构与sigset_t结构类似,fd_set本质也是一个位图,用位图中对应的位来表示要监视的文件描述符
调用select函数之前就需用fd_set结构定义出对应的文件描述符集,然后将需要监视的文件描述符添加到文件描述符集中,这个添加的过程本质就是在进行位操作,但是这个位操作不需要用户自己进行,系统提供了一组专门的接口,用于对fd_set类型的位图进行各种操作
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的全部位
timeval结构
传入select函数的最后一个参数timeout,是一个指向timeval结构的指针。timeval结构用于描述一段时间长度,该结构中包含两个成员,其中tv_sec表示的是秒,tv_usec表示的是微秒
读就绪
写就绪
异常就绪
注意:带外数据和TCP的紧急模式相关,TCP报头中的URG标志位和16位紧急指针搭配使用,就能够发送/接收带外数据
若要实现一个简单的select服务器,该服务器要做的就是读取客户端发来的数据并进行打印,那么该select服务器的工作流程如下:
注意:
Socket类
编写一个Socket类,对套接字相关的接口进行一定封装,为了让外部能直接调用Socket类中封装的函数,于是将部分函数定义成静态成员函数
//网络套接字封装
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "Log.hpp"
class Socket
{
const static int gbacklog = 15;
public://服务端客户端通用
static int SocketCreate() {
int SocketFd = socket(AF_INET, SOCK_STREAM, 0);
if(SocketFd < 0) {
LogMessage(FATAL, "socket create fail, %d:%s", errno, strerror(errno));
exit(1);
}
LogMessage(NORMAL, "socket create success, SocketFd:%d", SocketFd);
return SocketFd;
}
public://服务端专用
static void Bind(int listenSocketFd, uint16_t serverPort, std::string serverIp = "0.0.0.0") {
struct sockaddr_in local;
memset(&local, '\0', sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(serverPort);
inet_pton(AF_INET, serverIp.c_str(), &local.sin_addr);
if(bind(listenSocketFd, (struct sockaddr*)&local, sizeof local) < 0) {
LogMessage(FATAL, "bind fail, %d:%s", errno, strerror(errno));
exit(2);
}
LogMessage(NORMAL, "bind success, serverPort:%d", serverPort);
}
static void Listen(int listenSocketFd) {
if(listen(listenSocketFd, gbacklog) < 0) {
LogMessage(FATAL, "listen fail, %d:%s", errno, strerror(errno));
exit(3);
}
LogMessage(NORMAL, "listen success");
}
static int Accept(int listenSocketFd, std::string* clientIp, uint16_t* clientPort) {
struct sockaddr_in client;
socklen_t length = sizeof client;
int serviceSocketFd = accept(listenSocketFd, (struct sockaddr*)&client, &length);
if(serviceSocketFd < 0) {
LogMessage(ERROR, "accept fail, %d:%s", errno, strerror(errno));
exit(4);
}
if(clientIp != nullptr) *clientIp = inet_ntoa(client.sin_addr);
if(clientPort != nullptr) *clientPort = ntohs(client.sin_port);
return serviceSocketFd;
}
public://客户端专用
bool Connect(int clientSocketFd, std::string& serverIp, uint16_t& serverPort) {
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(serverIp.c_str());
server.sin_port = htons(serverPort);
if(connect(clientSocketFd, (struct sockaddr*)&server, sizeof server) == 0) return true;
else return false;
}
public:
Socket() {}
~Socket() {}
};
SelectServer类
#ifndef __SELECT_SVR_H__
#define __SELECT_SVR_H__
#include
#include
#include
#include "Socket.hpp"
#include "Log.hpp"
#include
#include
#include
#include
using namespace std;
#define BITS 8
#define NUM (sizeof(fd_set) * BITS)
#define FD_NONE -1
// 只完成读取,写入和异常不做处理
class SelectServer
{
public:
SelectServer(const uint16_t &port = 9090) : _port(port)
{
_listenSocketFd = Socket::SocketCreate();
Socket::Bind(_listenSocketFd, _port);
Socket::Listen(_listenSocketFd);
LogMessage(DEBUG, "create base socket success");
_fd_array[0] = _listenSocketFd;
for (int i = 1; i < NUM; ++i)
_fd_array[i] = FD_NONE;
}
~SelectServer()
{
if (_listenSocketFd > 0)
close(_listenSocketFd);
}
public:
void Start()
{
while (true)
{
DebugPrint();
fd_set readfds;
FD_ZERO(&readfds);
int maxFd = _listenSocketFd;
for (int i = 0; i < NUM; ++i)
{
if (_fd_array[i] == FD_NONE)
continue;
else
FD_SET(_fd_array[i], &readfds);
if (maxFd < _fd_array[i]) maxFd = _fd_array[i];
}
int number = select(maxFd + 1, &readfds, nullptr, nullptr, nullptr);
switch (number)
{
case 0:
LogMessage(DEBUG, "%s", "Time Out ...");
break;
case -1:
LogMessage(WARNING, "Select Fail: %d : %s", errno, strerror(errno));
break;
default:
LogMessage(DEBUG, "Get a event");
HandlerEvent(readfds);
break;
}
}
}
private:
void Accepter()
{
string clientIp;
uint16_t clientPort = 0;
int socketfd = Socket::Accept(_listenSocketFd, &clientIp, &clientPort);
if (socketfd < 0)
{
LogMessage(ERROR, "accept error");
return;
}
LogMessage(DEBUG, "Get a link success : [%s : %d] , socketFd : %d", clientIp.c_str(), clientPort, socketfd);
int pos = 1;
for (; pos < NUM; ++pos)
if (_fd_array[pos] == FD_NONE) break;
if (pos == NUM) { // 满了
LogMessage(ERROR, "%s:%d", "SelectServer already full, close:", socketfd);
close(socketfd);
}
else { // 找到空位置
_fd_array[pos] = socketfd;
}
}
void Recver(int i)
{
LogMessage(DEBUG, "message in , get IO event:%d", _fd_array[i]);
char buffer[1024];
int num = recv(_fd_array[i], buffer, sizeof(buffer) - 1, 0);
if(num > 0) {
buffer[num] = 0;
LogMessage(DEBUG, "client[%d]#%s", _fd_array[i], buffer);
}
else if(num == 0) {
LogMessage(DEBUG, "client[%d] link close, me too...", _fd_array[i]);
close(_fd_array[i]);
_fd_array[i] = FD_NONE;
}
else {
LogMessage(WARNING, "%d recv error, %d : %s", _fd_array[i], errno, strerror(errno));
close(_fd_array[i]);
_fd_array[i] = FD_NONE;
}
}
void HandlerEvent(const fd_set &readfds)
{
for (int i = 0; i < NUM; ++i)
{
// 去掉不合法的fd
if (_fd_array[i] == FD_NONE) continue;
// 判断是否就绪
if (FD_ISSET(_fd_array[i], &readfds))
{
if (i == 0 && _fd_array[i] == _listenSocketFd) Accepter(); //链接事件
else Recver(i);// 读事件
}
}
}
void DebugPrint()
{
cout << "_fd_array[]:";
for (int i = 0; i < NUM; ++i) {
if (_fd_array[i] != FD_NONE) cout << _fd_array[i] << " ";
}
cout << endl;
}
private:
uint16_t _port;
int _listenSocketFd;
int _fd_array[NUM];
};
#endif
select服务器测试
使用telnet工具连接服务器,此时通过telnet向服务器发送的数据就能够被服务器读到并且打印输出了
虽然SelectServer仅是一个单进程、单线程服务器,但却可以同时为多个客户端提供服务,因为select函数调用后会告知select服务器是哪个客户端对应的连接事件就绪,此时select服务器就可以读取对应客户端发来的数据,读取完后又会调用select函数等待某个客户端连接的读事件就绪
当服务器检测到客户端退出后,也会关闭对应的连接,并将对应的套接字从_fd_array数组中清除
存在的问题
综上所述,本博客中的SelectServer仅仅是一个Demo,用于理解select函数的使用
上述优点也是所有多路转接接口的优点
select可监控的文件描述符个数
调用select函数时传入的readfds、writefds以及exceptfds都是fd_set结构,fd_set结构本质是一个位图,用一个bit位来标记一个文件描述符,因此select可监控的文件描述符个数取决于fd_set类型的bit位个数
#include
#include
using namespace std;
int main()
{
cout << sizeof(fd_set)* 8 << endl;//1字节 8bit位
return 0;
}
运行代码后可以发现,select可监控的文件描述符个数为1024
一个进程能打开的文件描述符个数
进程控制块task_struct中有一个files指针,该指针指向一个struct files_struct结构,进程的文件描述符表fd_array就存储在该结构中,其中文件描述符表fd_array的大小定义为NR_OPEN_DEFAULT,NR_OPEN_DEFAULT的值实际就是32
但不意味着一个进程最多只能打开32个文件描述符,进程能打开的文件描述符个数是可以扩展的,通过ulimit -a命令可以看到进程能打开的文件描述符上限
select可监控的文件描述符个数是1024,除去监听套接字,那么最多只能连接1023个客户端
多路转接接口select、poll和epoll,需在一定的场景下使用,若场景不适宜,可能会适得其反
多连接中只有少量连接是比较活跃的,如聊天工具,登录QQ后大部分时间其实是没有聊天的,此时服务器端不可能调用一个read函数阻塞等待读事件就绪
多连接中大部分连接都很活跃,如企业当中进行数据备份时,两台服务器之间不断在交互数据,这时连接是特别活跃的,几乎不需要等的过程,也就没必要使用多路转接接口了
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明:
参数timeout的取值:
返回值说明:
poll调用失败时,错误码可能被设置为:
struct pollfd结构
events和revents的取值:
这些值都以宏的方式定义,二进制序列中有且只有一个bit位是1,且为1的bit位各不相同
poll的工作流程和select基本类似,下面也实现一个简单poll服务器,只读取客户端发来的数据并进行打印
PollServer类
#ifndef __POLL_SVR_H__
#define __POLL_SVR_H__
#include
#include
#include
#include "Socket.hpp"
#include "Log.hpp"
#include
#include
#include
using namespace std;
#define FD_NONE -1
// 只完成读取,写入和异常不做处理
class PollServer
{
public:
PollServer(const nfds_t nfds, const uint16_t &port = 9090) : _port(port), _nfds(nfds), _fds(nullptr)
{
_listenSocketFd = Socket::SocketCreate();
Socket::Bind(_listenSocketFd, _port);
Socket::Listen(_listenSocketFd);
LogMessage(DEBUG, "create base socket success");
_fds = new struct pollfd[_nfds];
_fds[0].fd = _listenSocketFd;
_fds[0].events = POLLIN;
for(int i = 1; i < _nfds; ++i) {
_fds[i].fd = FD_NONE;
_fds[i].events = _fds[i].revents = 0;
}
_timeout = 1000;
}
~PollServer() {
if (_listenSocketFd > 0) close(_listenSocketFd);
if (_fds != nullptr) delete[] _fds;
}
public:
void Start()
{
while (true)
{
DebugPrint();
int number = poll(_fds, _nfds, _timeout);
switch (number)
{
case 0:
LogMessage(DEBUG, "%s", "Time Out ...");
break;
case -1:
LogMessage(WARNING, "Poll Fail: %d : %s", errno, strerror(errno));
break;
default:
HandlerEvent();
break;
}
}
}
private:
void Accepter()
{
string clientIp;
uint16_t clientPort = 0;
int socketfd = Socket::Accept(_listenSocketFd, &clientIp, &clientPort);
if (socketfd < 0)
{
LogMessage(ERROR, "accept error");
return;
}
LogMessage(DEBUG, "Get a link success : [%s : %d] , socketFd : %d", clientIp.c_str(), clientPort, socketfd);
int pos = 1;
for (; pos < _nfds; ++pos)
if (_fds[pos].fd == FD_NONE) break;
if (pos == _nfds) { // 满了
//可以进行自动扩容
LogMessage(ERROR, "%s:%d", "PollServer already full, close:", socketfd);
close(socketfd);
}
else { // 找到空位置
_fds[pos].fd = socketfd;
_fds[pos].events = POLLIN;
}
}
void Recver(int i)
{
LogMessage(DEBUG, "message in , get IO event:%d", _fds[i].fd);
char buffer[1024];
int num = recv(_fds[i].fd, buffer, sizeof(buffer) - 1, 0);
if(num > 0) {
buffer[num] = 0;
LogMessage(DEBUG, "client[%d]#%s", _fds[i].fd, buffer);
}
else if(num == 0) {
LogMessage(DEBUG, "client[%d] link close, me too...", _fds[i].fd);
close(_fds[i].fd);
_fds[i].fd = FD_NONE;
_fds[i].events = _fds[i].revents = 0;
}
else {
LogMessage(WARNING, "%d recv error, %d : %s", _fds[i].fd, errno, strerror(errno));
close(_fds[i].fd);
_fds[i].fd = FD_NONE;
_fds[i].events = _fds[i].revents = 0;
}
}
void HandlerEvent()
{
for (int i = 0; i < _nfds; ++i)
{
// 去掉不合法的fd
if (_fds[i].fd == FD_NONE) continue;
// 判断是否就绪
if (_fds[i].revents & POLLIN)
{
if (_fds[i].fd == _listenSocketFd) Accepter(); //链接事件
else Recver(i);// 读事件
}
}
}
void DebugPrint()
{
cout << "fds[]:";
for(int i = 0; i < _nfds; ++i) {
if(_fds[i].fd == FD_NONE) continue;
cout << _fds[i].fd << " ";
}
cout << endl;
}
private:
uint16_t _port;
int _listenSocketFd;
struct pollfd* _fds;
nfds_t _nfds = 100;
int _timeout;
};
#endif
_fds数组的大小是固定设置的,因此在将新获取连接对应的文件描述符添加到fds数组时,可能会因为fds数组已满而添加失败,这时poll服务器只能将刚刚获取上来的连接对应的套接字进行关闭
poll服务器测试
在调用poll函数时,将timeout的值设置成1000,因此运行服务器后每隔1000毫秒没有客户端发来连接请求,那么服务器就会超时返回
用telnet工具连接poll服务器后,poll函数在检测到监听套接字的读事件就绪后就会调用accept获取建立好的连接,并打印输出客户端的IP和端口号等信息,此时客户端发来的数据也能成功被poll服务器收到并进行打印输出
poll服务器也是单进程、单线程服务器,同样可以为多个客户端服务
当服务器端检测到客户端退出后,也会关闭对应的连接,并将对应的套接字从_fds数组中清除
优点
说明一下:
缺点
epoll是系统提供的一个多路转接接口
epoll_create函数
int epoll_create(int size);
注意: 当不再使用时,须调用close函数关闭epoll模型对应的文件描述符,当所有引用epoll实例的文件描述符都已关闭时,内核将销毁该实例并释放相关资源
epoll_ctl函数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数说明:
第二个参数op的取值有以下三种:
返回值:函数调用成功返回0,调用失败返回-1,同时错误码会被设置
第四个参数对应的struct epoll_event结构如下:
struct epoll_event结构中有两个成员,第一个成员events表示的是需监视的事件,第二个成员data为联合体结构,一般选择使用该结构中的fd,表示需要监听的文件描述符
events的常用取值如下:
这些取值是以宏的方式定义,二进制序列中有且只有一个bit位是1,且为1的bit位是各不相同的
epoll_wait函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数说明:
参数timeout的取值:
返回值:
epoll_wait调用失败时,错误码可能被设置为:
红黑树 && 就绪队列
当某一进程调用epoll_create函数,Linux内核会创建一个eventpoll结构体,即epoll模型,eventpoll结构体中的成员rbr、rdlist与epoll的使用方式密切相关
struct eventpoll{
...
//红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监视的事件
struct rb_root rbr;
//就绪队列中则存放着将要通过epoll_wait返回给用户的满足条件的事件
struct list_head rdlist;
...
}
在epoll中,对于每一个事件都有一个对应的epitem结构体,红黑树和就绪队列中的节点分别是基于epitem结构中的rbn成员和rdllink成员的,epitem结构中的成员ffd记录的是指定的文件描述符值,event成员记录的就是该文件描述符对应的事件
struct epitem{
struct rb_node rbn; //红黑树节点
struct list_head rdllink; //双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
注意:
回调机制
所有添加到红黑树中的事件,都与设备(网卡)驱动程序建立回调方法,该回调方法在内核中被称为ep_poll_callback
注意:
Epoll类
将epoll相关的系统调用进行封装,便于后续使用
#include
#include
#include
using namespace std;
class Epoll
{
public:
static const int gsize = 256;
public:
static int EpollCreate()
{
int epollFd = epoll_create(gsize);
if(epollFd > 0) return epollFd;
exit(5);//创建失败直接终止
}
static bool EpollCtl(int epollFd, int op, int socketFd, uint32_t events)
{
struct epoll_event ev;
ev.events = events;
ev.data.fd = socketFd;
int num = epoll_ctl(epollFd, op, socketFd, &ev);
return num == 0;
}
static int EpollWait(int epollFd, struct epoll_event* revs, int num, int timeout) {
return epoll_wait(epollFd, revs, num, timeout);
}
};
EpollServer类
#ifndef __EPOLL_SERVER_HPP__
#define __EPOLL_SERVER_HPP__
#include
#include
#include
#include
#include
#include "Epoll.hpp"
#include "Log.hpp"
#include "Socket.hpp"
using namespace std;
namespace ns_epoll
{
class EpollServer
{
using func_t = function;
public:
EpollServer(func_t handler, const uint16_t& port = 9090):_port(port), _revsNum(64),_handlerRequest(handler) {
//申请空间
_revs = new struct epoll_event[_revsNum];
//创建监听套接字
_listenSocketFd = Socket::SocketCreate();
Socket::Bind(_listenSocketFd, _port);
Socket::Listen(_listenSocketFd);
//创建epoll模型
_epollFd = Epoll::EpollCreate();
LogMessage(DEBUG, "init success, listenSocketFd : %d, epollFd : %d", _listenSocketFd, _epollFd);
//将监听套接字添加到epoll中
if(Epoll::EpollCtl(_epollFd, EPOLL_CTL_ADD, _listenSocketFd, EPOLLIN))
LogMessage(DEBUG, "Add listenSocketFd to epoll success");
else exit(6);
}
~EpollServer() {
if(_listenSocketFd >= 0) close(_listenSocketFd);
if( _epollFd >= 0) close(_epollFd);
if(_revs != nullptr) delete[] _revs;
}
public:
void Start()
{
int timeout = -1;
while(true)
{
LoopOnce(timeout);
}
}
public:
void LoopOnce(int timeout) {
int num = Epoll::EpollWait(_epollFd, _revs, _revsNum, timeout);
switch (num)
{
case 0:
LogMessage(DEBUG, "Time Out...");
break;
case -1:
LogMessage(WARNING, "epoll wait error: %s", strerror(errno));
break;
default:
LogMessage(DEBUG, "Get a event");
HandlerEvents(num);
break;
}
}
void HandlerEvents(int number)
{
assert(number);
for(int i = 0; i < number; ++i)
{
uint32_t revent = _revs[i].events;
int socketFd = _revs[i].data.fd;
if(revent & EPOLLIN) //读事件就绪
{
if(socketFd == _listenSocketFd) Accetper(_listenSocketFd);
else Recver(socketFd);
}
}
}
void Accetper(int listenSocketFd)
{
string clientIp;
uint16_t clientPort;
int socketFd = Socket::Accept(listenSocketFd, &clientIp, &clientPort);
if(socketFd < 0) {
LogMessage(WARNING, "Accept error");
return;
}
if(!Epoll::EpollCtl(_epollFd, EPOLL_CTL_ADD, socketFd, EPOLLIN)) return;
LogMessage(DEBUG, "Add new link : %d to epoll success", socketFd);
}
void Recver(int socketFd)
{
char buffer[10240];
ssize_t n = recv(socketFd, buffer, sizeof(buffer) - 1, 0);
if(n > 0) {
buffer[n] = 0;
_handlerRequest(buffer);
}
else if(n == 0) {
LogMessage(NORMAL, "client %d close link, me too...", socketFd);
bool ret = Epoll::EpollCtl(_epollFd, EPOLL_CTL_DEL, socketFd, 0);
assert(ret);
close(socketFd);
}
else {
LogMessage(NORMAL, "client %d recv error, close error socketFd", socketFd);
bool ret = Epoll::EpollCtl(_epollFd, EPOLL_CTL_DEL, socketFd, 0);
assert(ret);
close(socketFd);
}
}
private:
int _listenSocketFd;
int _epollFd;
uint16_t _port;
struct epoll_event* _revs;
int _revsNum;
func_t _handlerRequest;
};
}
#endif
epoll服务器测试
编写epoll服务器在调用epoll_wait函数时,将timeout的值设置成了-1,因此运行服务器后若没有客户端发来连接请求,那么服务器就会调用epoll_wait函数后阻塞等待
使用telnet工具连接epoll服务器后,epoll服务器调用的epoll_wait函数在检测到监听套接字的读事件就绪后就会调用accept获取建立好的连接,并打印输出客户端的IP和端口号,此时客户端发来的数据也能成功被epoll服务器收到并进行打印输出
该epoll服务器同样为单进程、单线程服务器,但可以为多个客户端提供服务
使用 ls /proc/PID/fd 命令,查看当前epoll服务器的文件描述符的使用情况。文件描述符0、1、2是默认打开的,分别对应的是标准输入、标准输出和标准错误,3号文件描述符对应的是监听套接字,4号文件描述符对应epoll句柄,5号和6号文件描述符分别对应访问服务器的两个客户端
当服务器端检测到客户端退出后,也会关闭对应连接,此时epoll服务器对应的5号和6号文件描述符就关闭了
注意:
与select和poll的不同之处
水平触发(LT,Level Triggered)
epoll默认状态下就是LT工作模式
边缘触发(ET,Edge Triggered)
若要将epoll改为ET工作模式,则需在添加事件时设置EPOLLET选项
ET工作模式下如何进行读写
因为在ET工作模式下,只有底层就绪事件无到有或由有到多发生变化的时候才会通知用户,这就倒逼用户当读事件就绪时必须一次性将数据全部读取完毕,当写事件就绪时必须一次性将发送缓冲区写满,否则可能再也没有机会进行读写了
因此读数据时必须循环调用recv函数进行读取,写数据时必须循环调用send函数进行写入
强调:ET工作模式下,recv和send操作的文件描述符必须设置为非阻塞状态,这是必须的,不是可选的
对比LT和ET