今天来讲关于高性能服务器三个系统调用的函数使用。今天主要偏代码实现。博主马上就要更新到数据库部分了,希望大家支持博主呀。
目录
select
select接口介绍
int nfds:所关心的文件描述符中,最大的值+1。
fd_set* readfds:输入输出型参数。输入是,用户告诉内核,你要帮我关心哪个文件描述符的读事件。 输出是,内核告诉用户,你让我关心的哪个文件描述符的读事件就绪。
fd_set* writefds:输入输出型参数。输入是,用户告诉内核,你要帮我关心哪个文件描述符的写事件。 输出是,内核告诉用户,你让我关心的哪个文件描述符的写事件就绪。
fd_set* readfds:输入输出型参数。输入是,用户告诉内核,你要帮我关心哪个文件描述符的异常事件。 输出是,内核告诉用户,你让我关心的哪个文件描述符的异常事件就绪。
struct timeval* timeout:设置阻塞等待时间。
返回值:>0表示有几个文件描述符就绪了;=0表示超时返回了;<0表示出错了。
关于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
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
select的执行流程
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(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,nullptr,nullptr,nullptr)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
代码测试
makefile
select_server:select_server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f select_server
select_server.cc
#include <iostream>
#include <sys/select.h>
#include "Sock.hpp"
#include <string>
#include <unistd.h>
using namespace std;
#define NUM (sizeof(fd_set) * 8)
int fd_array[NUM]; //内容 >= 0,合法的fd,如果是-1,表示没有值fd
void Usage(string proc)
{
cout << "Usage: " << proc << "port" << 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
}
}
// FD_SET(listen_sock, &rfds);
struct timeval timeout = {5, 0};
// accept一次只能等一个文件描述符
//我们服务器上的所有fd(包括listen_sock),都要交给select进行检测!!
// recv,read,write,send,accept: 只负责自己的核心工作,真正的读写(listen_sock 是进行-> accept)
int n = select(max_fd + 1, &rfds, nullptr, nullptr, &timeout); //暂时阻塞
switch (n)
{
case -1:
cerr << "select error" << endl;
break;
case 0:
cout << "select timeout" << endl;
break;
default:
cout << "有fd对应的事件就绪" << endl;
//到这里要开始读取数据了
for (int i = 0; i < NUM; i++)
{
//下面的fd是合法的fd,合法的fd不一定是就绪(有数据)的fd
if (FD_ISSET(fd_array[i], &rfds))
{
cout << "Sock: " << "上面有读事件, 可以读取了" << endl;
//一定是读事件就绪了
//就绪的fd就在fd_array[i]中保存
// read, recv时,一定不会被阻塞
//读事件就绪,一定可以read, recv吗??不一定
if (fd_array[i] == listen_sock)
{
cout << "listen_sock: " << "获取新链接成功" << endl;
// accept
int sock = Sock::Accept(listen_sock);
if (sock >= 0)
{
cout << "listen_sock: " << listen_sock << "获取新链接成功" << endl;
//获取成功
//可以read, recv吗??绝对不可以!
//新链接的到来,不意味着数据到来!!什么时候数据到来呢??不知道
//可是,谁可以最清楚的知道哪些fd,上面可以读取了?select!
//无法直接将fd设置进select,但是,好在我们有fd_array[]!
int pos = 1; //不用从0开始
for ( ; pos < NUM; pos++)
{
if (fd_array[pos] == -1)
{
break; //找到空位置
}
}
// 1、找到了一个位置没有被使用
if (pos < NUM)
{
cout << "新链接: " << sock << " 已经被添加到了数组[" << pos << "]的位置" << endl;
fd_array[pos] = sock;
}
else
{
// 2、找完了所有的fd_array[],都没有合法的位置
//说明服务器已经满载,没法处理新的请求了
cout << "服务器已经满载了,关闭新的链接" << endl;
close(sock); //关闭给客户端提供服务的sock,这时候客户端会自动关闭页面
}
}
}
else //等完后
{
// read, recv
//普通的sock, 读事件就绪了,可以进行读取了
//可是,本次读取一定能读完吗?读完,就一定没有数据包粘包问题吗?
//今天不在这里解决,在后面epoll那里有场景时解决
cout << "Sock: " << fd_array[i] << " 上面有普通读取" << 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;
cout << "client[" << fd_array[i] << "]#" << recv_buffer << endl;
}
else if (s == 0)
{
cout << "Sock: " << fd_array[i] << "关闭了, client退出了" << endl;
//对端关闭了链接
close(fd_array[i]);
//防止下次循环时,去select一个关闭的fd,就报错了
fd_array[i] = -1;
std::cout << "已经在数组下标fd_array[" << i << "]中去掉了sock: " << fd_array[i] << endl;
}
else
{
//读取失败
close(fd_array[i]);
fd_array[i] = -1;
std::cout << "已经在数组下标fd_array[" << i << "]中去掉了sock: " << fd_array[i] << endl;
}
}
}
}
break;
}
}
return 0;
}
Sock.hpp
#pragma once
#include<iostream>
#include<string>
#include<string.h>
#include<stdlib.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
cerr << "socket err" << endl;
exit(2);
}
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; //服务端 ip地址
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 << ";isten 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, 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);
}
}
};
测试结果
select总结
poll
poll接口介绍
struct poll* fds:输入输出型参数,我们后面再介绍。
nfds_t nfds:fds数组的长度。
int timeout:单位时间是毫秒,阻塞等待的时间。(-1是永久阻塞)
返回值:>0表示有几个文件描述符就绪了;=0表示超时返回了;<0表示出错了。
pollfd结构
struct pollfd
{
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
events和revents的取值
poll测试demo
poll_server.cc
#include <iostream>
#include <unistd.h>
#include <poll.h>
using namespace std;
//下面的代码不涉及跨网络
int main()
{
struct pollfd rfds;
rfds.fd = 0;
rfds.events = POLLIN;
rfds.revents = 0;
while (true)
{
int n = poll(&rfds, 1, -1); //-1表示永久阻塞等待
switch (n)
{
case 0:
cout << "time out ..." << endl;
break;
case -1:
cerr << "poll error" << endl;
break;
default:
cout << "有事件发生..." << endl;
if(rfds.revents & POLLIN)
{
cout << rfds.fd << "上面的读事件发生了" << endl;
char buffer[128];
ssize_t s = read(0, buffer, sizeof(buffer)-1);
if(s > 0)
{
cout << "有人说# " << buffer << endl;
}
}
break;
}
}
return 0;
}
测试结果
总结
poll的优点
1、pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比select更方便。
2、poll并没有最大数量限制 (但是数量过大后性能也是会下降)。
poll的缺点
poll中监听的文件描述符数目增多时:
1、和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符.2、每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.
3、同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.
epoll
epoll接口介绍
epoll_create
int size:这个参数现在的操作系统基本没意义了,我们填的时候只需要填128的倍数就可以了。
返回值:返回一个文件句柄,其实就是文件描述符
epoll_ctl
int epfd:该参数是epoll_create创建的文件句柄的返回值
int op:选项操作,后面讲。
int fd:对哪个文件描述符进行操作
struct epoll_event* event:我们后面讲
第二个参数的取值
EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD:修改已经注册的fd的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd
struct epoll_event结构如下:
对于上面结构体中data(共用体)字段暂时关心fd变量就可以了,是针对某个文件描述符的设置。
而对于events可以是以下几个宏的集合:
EPOLLIN: 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT: 表示对应的文件描述符可以写;
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR: 表示对应的文件描述符发生错误;
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里
epoll_wait
int epfd:epoll_create的返回值(创建的文件句柄)
struct epoll_event* events:输出型参数,哪些文件描述符上的哪些事件已经就绪
int maxevents:告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size。
int timeout:超时时间 (毫秒,0会立即返回,-1是永久阻塞).
epoll的工作原理
epoll_create:创建文件句柄,在底层建立一棵红黑树,每个节点对应建立回调机制,创建就绪队列。
epoll_ctl:在红黑树中增加/删除/修改节点。
epoll_wait:检测就绪队列中数据是否有文件描述符的数据就绪。
实际上当硬件收到数据时(根据冯诺依曼体系),接着把数据拷贝到内存缓冲区中,OS系统会首先检测到数据的到来,在数据就绪的同时,这时候会触发之前该红黑树结点建立的回调机制,把对应的就绪数据对应的fd添加到就绪队列中(双向链表),这时候,上层只需要调用epoll_wait就可以以O(1)的时间复杂度,检测是否有事件就绪。
代码测试
epoll_server.cc
#include <iostream>
#include <string>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include "Sock.hpp"
using namespace std;
#define SIZE 128
#define NUM 64
static void Usage(string proc)
{
cerr << "Usage: " << proc << " port" << endl;
}
// ./epoll_server port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
// 1.建立tcp 监听socket
uint16_t port = (uint16_t)atoi(argv[1]);
int listen_sock = Sock::Socket();
Sock::Bind(listen_sock, port);
Sock::Listen(listen_sock);
// 2.创建epoll模型,获得epfd(文件描述符)
int epfd = epoll_create(SIZE); // SIZE在后面的linux版本中无意义了,我们最好填128的整数倍
// 3. 先将listen_sock和它所关心的事件,添加到内核
struct epoll_event ev;
ev.events = EPOLLIN; //关心的事件
ev.data.fd = listen_sock; //关心的fd
// ev.data
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev);
// 4.事件循环
volatile bool quit = false;
struct epoll_event revs[NUM];
while (!quit)
{
int timeout = 1000; //毫秒 (-1表示永久阻塞 0表示非阻塞)
//这里传入数组,仅仅是尝试从内核中拿回来已经就绪的事件
int n = epoll_wait(epfd, revs /*按序返回*/, NUM /*事件个数*/, timeout);
switch (n)
{
case 0:
cout << "time out ..." << endl;
break;
case -1:
cerr << "epoll error ..." << endl;
break;
default:
cout << "有事件已经就绪了!" << endl;
// 5.处理就绪事件
for (int i = 0; i < n; i++) // n表示就绪的文件描述符的个数
{
int sock = revs[i].data.fd; //暂时方案
cout << "文件描述符: " << sock << " 上面有事件就绪了" << endl;
//我们要找到就绪事件中的fd就要借助data
if (revs[i].events & EPOLLIN)
{
cout << "文件描述符: " << sock << "读事件就绪" << endl;
if (sock == listen_sock)
{
cout << "文件描述符: " << sock << "链接数据就绪" << endl;
// 5.1 处理链接事件
int fd = Sock::Accept(listen_sock); // accept后的sock就绪才是真正的有数据到来
if (fd >= 0)
{
cout << "获取新链接成功了: " << fd << endl;
//不能直接读取,有链接到来(链接到来也相当于数据)不一定有数据就绪
//一旦直接读,就会阻塞了!
struct epoll_event _ev;
_ev.events = EPOLLIN;
_ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &_ev); //新的fd托管给epoll
cout << "已经将" << fd << "托管给epoll了" << endl;
}
else
{
// Do Nothing
}
}
else
{
// 5.2正常的读取处理
cout << "文件描述符: " << sock << "正常数据就绪" << endl;
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer)-1, 0);//虽然是0,但是这里读取时并不会被阻塞
if(s > 0)
{
buffer[s] = 0;
cout << "client [" << sock << "]# " << buffer << endl;
//将我们关心的事件更改成为EPOLLOUT
//struct epoll_event _ev;
//_ev.events = EPOLLOUT;
//_ev.data.fd = sock;
//epoll_ctl(epfd, EPOLL_CTL_MOD, sock, &_ev);
}
else if(s == 0)
{
//对端关闭链接
cout << "client quit ..." << endl;
close(sock);//关闭链接
epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr);//要删除sock, events事件就不关心了,设置成nullptr
cout << "sock: " << sock << "delete from epoll success" << endl;
}
else
{
//读取失败
cout << "recv error" << endl;
close(sock);//关闭链接
epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr);//要删除sock, events事件就不关心了,设置成nullptr
cout << "sock: " << sock << "delete from epoll success" << endl;
}
}
}
else if(revs[i].events & EPOLLOUT)
{
//处理写事件
}
else
{}
}
break;
}
}
close(epfd); //关闭句柄
close(listen_sock); //关闭监听套接字
return 0;
}
Sock.hpp
#pragma once
#include<iostream>
#include<string>
#include<string.h>
#include<stdlib.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
cerr << "socket err" << endl;
exit(2);
}
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; //服务端 ip地址
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 << ";isten 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, 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);
}
}
};
测试结果
epoll的优点
接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
没有数量限制: 文件描述符数目无上限
epoll工作方式
epoll有2种工作方式-水平触发(LT)和边缘触发(ET)。
水平触发Level Triggered 工作模式
epoll默认状态下就是LT工作模式。
1、当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
2、如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait仍然会立刻返回并通知socket读事件就绪.3、直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
4、支持阻塞读写和非阻塞读写
如果底层有数据,就会不断提醒上层去读取数据。
边缘触发Edge Triggered工作模式
如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式.
1、当epoll检测到socket上事件就绪时, 必须立刻处理.
2、如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候,epoll_wait 不会再返回了.
3、也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会.
4、只支持非阻塞的读写
对比LT和ET模式
LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数.
但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完。
相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的。
另一方面, ET 的代码复杂程度更高了。
我们在代码演示层面上看一下:
我们发现ET模式下,当有数据到来时,就只提醒一次,而LT模式下,只要底层有数据,就会不断提醒上层。
ET模式下fd设置为非阻塞的原因
为什么会不设置阻塞呢?
就好比我们循环读取数据,如果有320字节的数据,一次读取100字节,我们第四次读取了20字节,没有达到预期,说明数据已经读取完了,就返回结束。
那如果到来的数据只有300字节呢?第三次读取100字节后,第四次要不要读取呢?我们不能 站在上帝视角来评判,站在OS角度,还是要继续读取的,但是这时候已经没有数据了,底层调用的read,accept等接口一直在等待数据,实际上已经没有数据了,这时候这个进程就会永久卡在这里。
而对于ET模式阻塞和非阻塞都是可以的。
看到这里,给博主点个赞吧~