listenSocket = socket(); //调用socket系统调用创建一个主动套接字
bind(listenSocket); //绑定地址和端口
listen(listenSocket); //将默认的主动套接字转换为服务器使用的被动套接字,也就是监听套接字
while (1) { //循环监听是否有客户端连接请求到来
connSocket = accept(listenSocket); //接受客户端连接
recv(connsocket); //从客户端读取数据,只能同时处理一个客户端
send(connsocket); //给客户端返回数据,只能同时处理一个客户端
}
每次只能处理一个客户端连接,我们可以利用多线程来提高服务端的处理能力
listenSocket = socket(); //调用socket系统调用创建一个主动套接字
bind(listenSocket); //绑定地址和端口
listen(listenSocket); //将默认的主动套接字转换为服务器使用的被动套接字,即监听套接字
while (1) { //循环监听是否有客户端连接到来
connSocket = accept(listenSocket); //接受客户端连接,返回已连接套接字
pthread_create(processData, connSocket); //创建新线程对已连接套接字进行处理
}
//处理已连接套接字上的读写请求
processData(connSocket){
recv(connsocket); //从客户端读取数据,只能同时处理一个客户端
send(connsocket); //给客户端返回数据,只能同时处理一个客户端
}
但是Redis的主流程是在一个线程中执行的,无法使用多线程的方式来提升并发处理能力
那么还有什么其他方法能提高Redis的处理能力。这就可以使用IO多复用机制了
IO多路复用的实现主要有如下3种,分别是select,poll,epoll,来看一下有什么区别!
本文总结自《Redis 源码剖析与实战-09 | Redis事件驱动框架(上):何时使用select、poll、epoll?》
//
// *__readfds 监听的读数据事件集合
// *__writefds 监听的写数据事件集合
// *__exceptfds 监听的异常事件集合
//
int select (int __nfds, fd_set *__readfds, fd_set *__writefds, fd_set *__exceptfds, struct timeval *__timeout)
typedef struct {
…
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
…
} fd_set
__fd_mask类型是long int类型的别名,__FD_SETSIZE 和 __NFDBITS 这两个宏定义的大小默认为1024和32
所以fd_set结构体的定义就是一个long int类型的数组,该数组一共有32个元素(1024/32=32),每个元素是32位(long int类型的大小),每一位可以表示一个文件描述符的状态
我们现在知道了select函数对每一个描述符集合,都可以监听1024个描述符
select调用过程如下
select函数有如下2个缺陷
使用demo
int sock_fd,conn_fd; //监听套接字和已连接套接字的变量
sock_fd = socket() //创建套接字
bind(sock_fd) //绑定套接字
listen(sock_fd) //在套接字上进行监听,将套接字转为监听套接字
fd_set rset; //被监听的描述符集合,关注描述符上的读事件
int max_fd = sock_fd
//初始化rset数组,使用FD_ZERO宏设置每个元素为0
FD_ZERO(&rset);
//使用FD_SET宏设置rset数组中位置为sock_fd的文件描述符为1,表示需要监听该文件描述符
FD_SET(sock_fd,&rset);
//设置超时时间
struct timeval timeout;
timeout.tv_sec = 3;
timeout.tv_usec = 0;
while(1) {
//调用select函数,检测rset数组保存的文件描述符是否已有读事件就绪,返回就绪的文件描述符个数
n = select(max_fd+1, &rset, NULL, NULL, &timeout);
//调用FD_ISSET宏,在rset数组中检测sock_fd对应的文件描述符是否就绪
if (FD_ISSET(sock_fd, &rset)) {
//如果sock_fd已经就绪,表明已有客户端连接;调用accept函数建立连接
conn_fd = accept();
//设置rset数组中位置为conn_fd的文件描述符为1,表示需要监听该文件描述符
FD_SET(conn_fd, &rset);
}
//依次检查已连接套接字的文件描述符
for (i = 0; i < maxfd; i++) {
//调用FD_ISSET宏,在rset数组中检测文件描述符是否就绪
if (FD_ISSET(i, &rset)) {
//有数据可读,进行读数据处理
}
}
}
// pollfd 结构体数组
// nfds_t __fds数组的元素个数
// __timeout poll函数阻塞的超时时间
int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);
pollfd结构体包含了要监听的描述符,以及该描述符上要监听的事件类型
struct pollfd {
int fd; //进行监听的文件描述符
short int events; //要监听的事件类型
short int revents; //实际发生的事件类型
};
pollfd中监听的事件类型,是通过宏定义的,有可读,可写,错误事件
#define POLLRDNORM 0x040 //可读事件
#define POLLWRNORM 0x100 //可写事件
#define POLLERR 0x008 //错误事件
poll调用过程如下
而第三部又分为如下两种情况
和select函数相比,poll的改进之处在于,它允许监听超过1024个文件描述符,但是当调用了poll函数后,我们仍然需要遍历每个文件描述符,监测该描述符是否就绪,然后进行处理
使用demo
int sock_fd,conn_fd; //监听套接字和已连接套接字的变量
sock_fd = socket() //创建套接字
bind(sock_fd) //绑定套接字
listen(sock_fd) //在套接字上进行监听,将套接字转为监听套接字
//poll函数可以监听的文件描述符数量,可以大于1024
#define MAX_OPEN = 2048
//pollfd结构体数组,对应文件描述符
struct pollfd client[MAX_OPEN];
//将创建的监听套接字加入pollfd数组,并监听其可读事件
client[0].fd = sock_fd;
client[0].events = POLLRDNORM;
maxfd = 0;
//初始化client数组其他元素为-1
for (i = 1; i < MAX_OPEN; i++)
client[i].fd = -1;
while(1) {
//调用poll函数,检测client数组里的文件描述符是否有就绪的,返回就绪的文件描述符个数
n = poll(client, maxfd+1, &timeout);
//如果监听套件字的文件描述符有可读事件,则进行处理
if (client[0].revents & POLLRDNORM) {
//有客户端连接;调用accept函数建立连接
conn_fd = accept();
//保存已建立连接套接字
for (i = 1; i < MAX_OPEN; i++){
if (client[i].fd < 0) {
client[i].fd = conn_fd; //将已建立连接的文件描述符保存到client数组
client[i].events = POLLRDNORM; //设置该文件描述符监听可读事件
break;
}
}
maxfd = i;
}
//依次检查已连接套接字的文件描述符
for (i = 1; i < MAX_OPEN; i++) {
if (client[i].revents & (POLLRDNORM | POLLERR)) {
//有数据可读或发生错误,进行读数据处理或错误处理
}
}
}
epoll实例内部维护了2个结构,分别记录要监听的文件描述符和已经就绪的文件描述符。这样我们在使用epoll机制时,就不用像select和poll一样,遍历查询哪些文件描述符已经就绪了。这样一来epoll的效率就比select和poll有了更高的提升
使用demo
int sock_fd,conn_fd; //监听套接字和已连接套接字的变量
sock_fd = socket() //创建套接字
bind(sock_fd) //绑定套接字
listen(sock_fd) //在套接字上进行监听,将套接字转为监听套接字
epfd = epoll_create(EPOLL_SIZE); //创建epoll实例,
//创建epoll_event结构体数组,保存套接字对应文件描述符和监听事件类型
ep_events = (epoll_event*)malloc(sizeof(epoll_event) * EPOLL_SIZE);
//创建epoll_event变量
struct epoll_event ee
//监听读事件
ee.events = EPOLLIN;
//监听的文件描述符是刚创建的监听套接字
ee.data.fd = sock_fd;
//将监听套接字加入到监听列表中
epoll_ctl(epfd, EPOLL_CTL_ADD, sock_fd, &ee);
while (1) {
//等待返回已经就绪的描述符
n = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
//遍历所有就绪的描述符
for (int i = 0; i < n; i++) {
//如果是监听套接字描述符就绪,表明有一个新客户端连接到来
if (ep_events[i].data.fd == sock_fd) {
conn_fd = accept(sock_fd); //调用accept()建立连接
ee.events = EPOLLIN;
ee.data.fd = conn_fd;
//添加对新创建的已连接套接字描述符的监听,监听后续在已连接套接字上的读事件
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ee);
} else { //如果是已连接套接字描述符就绪,则可以读数据
...//读取数据并处理
}
}
}
多路复用机制 | 监听的文件描述符数量 | 查找就绪的文件描述符 |
---|---|---|
select | 1024 | 遍历所有描述符 |
poll | 自定义 | 遍历所有描述符 |
epoll | 自定义 | 自动返回就绪的描述符 |
[1]https://time.geekbang.org/column/article/407901