返回:Linux网络编程学习笔记
select、poll、epoll三者优缺点对比
多路I/O函数 | 优点 | 缺点 |
select | 1.函数诞生时间早,跨平台性好,windows、linux、macOS、Unix、类Unix均支持 | 1.监听的文件描述符上限为1024;2.需要添加业务逻辑,来监听满足条件的fd,代码编程难度会提高(业务逻辑是指添加一个1024大小的数组,通过数组来管理满足条件的fd,详见代码) |
poll | 1.自带数组结构,可将监听和返回事件集合分离;2.可以拓展文件描述符监听上限,突破1024 | 1.不能跨平台,仅Linux支持;2.无法直接定位满足监听事件的文件描述符,编码难度较大 |
epoll | 1.解决了监听事件难于管理的问题,编程比较简单;2.监听事件上限可以突破1024 | 1.不能跨平台,仅Linux支持。 |
/* According to earlier standards */
#include
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
参数说明:
int nfds //监听的最大文件描述符+1,作为监听循环的上限
fd_set *read_fds, //监视的可读文件句柄集合。
fd_set *write_fds, //监视的可写文件句柄集合。
fd_set *excepr_fds, //监视的异常文件句柄集合。
struct timeval *timeout //本次select()的超时结束时间。
方法一:通过循环判断文件lfd+1到maxfd之间是否有读事件,来操作套接字的读和写。
缺点:效率比较低,在lfd+1到maxfd之间如果有文件关闭了,也会进入FD_ISSET的判断。
方法二:定义数组int client[1024],用来管理lfd+1到maxfd之间需要监听的文件,具体方法如下:
1.当lfd有读事件时会生成cfd,通过for循环查找靠前没有使用的client元素,使client[i]=cfd(未使用的client[i]会置-1);
2.在使用FD_ISSET之前,先判断client[i]是否大于0,大于0的情况才进行判断。
具体的操作如下:
参考链接:linux select函数详解
详见底部代码
主要疑问:select函数实现的是一个非阻塞忙轮询的逻辑,accept函数为何不会阻塞?
原因很简单,下方代码在使用accept函数之前,首先通过FD_ISSET函数判断了listenfd是有读事件发生的,所以不阻塞。只有在listenfd没有读事件且使用accept函数的情况下,才会阻塞,通过条件判断规避了阻塞的情况。
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if (nready < 0)
perr_exit("select error");
if (FD_ISSET(listenfd, &rset)) { /* 说明有新的客户端链接请求 */
clie_addr_len = sizeof(clie_addr);
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不会阻塞 */
FD_SET(connfd, &allset); /* 向监控文件描述符集合allset添加新的文件描述符connfd */
if (maxfd < connfd)
maxfd = connfd;
if (0 == --nready) /* 只有listenfd有事件, 后续的 for 不需执行 */
continue;
}
NAME
poll, ppoll - wait for some event on a file descriptor
SYNOPSIS
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include
#include
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *tmo_p, const sigset_t *sigmask);
DESCRIPTION
poll() performs a similar task to select(2): it waits for one of a set of file descriptors to
become ready to perform I/O.
The set of file descriptors to be monitored is specified in the fds argument, which is an array
of structures of the following form:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
说明:
(1)struct pollfd结构体介绍
struct pollfd {
int fd; //待监听文件描述符
short events; //待监听文件描述符对应的监听事件,可以为POLLIN、POLLOUT、POLLERR ...
short revents; //传入时赋值0。如果满足对应事件返回非0。
};
(2)nfds 监听数组实际的有效监听个数
(3)timeout 超时时长。单位ms(-1 阻塞;0 不阻塞,立即返回;>0 超时时长,单位毫秒)
poll函数返回值:满足对应监听事件的文件描述符总个数。
poll函数的使用与select类似,使用前需要定义struct pollfd类型结构体,例如监听1024个文件描述符的定义如下:
struct pollfd pfds[1024];
在使用poll函数之前,需要先设置pfds的成员,以监听套接字lfd为例,使用第0号元素结构体pfds[0]来监听lfd,监听的事件为读事件POLLIN,revent置0;
使用poll之后,发现有连接事件出现,就通过accept函数创建连接套接字cfd,并将cfd放进pfds里面进行监听;
如果连接套接字上有读事件发生,就调用read和write进行处理,程序的具体流程如下,代码省略。
(1)epoll_create
NAME
epoll_create, epoll_create1 - open an epoll file descriptor
SYNOPSIS
#include
int epoll_create(int size);
int epoll_create1(int flags);
size:创建红黑树的监听节点数量(仅供内核参考,分配对应大小的资源)
epoll_create返回值:成功返回指向新创建的红黑树根节点的fd;失败返回 -1和errno。
(2)epoll_ctl
NAME
epoll_ctl - control interface for an epoll file descriptor
SYNOPSIS
#include
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:epoll_create创建的红黑树根节点的文件描述符
op:对应如下三种操作
EPOLL_CTL_ADD:将fd添加到监听红黑树
EPOLL_CTL_MOD:修改fd在监听红黑树上的监听事件
EPOLL_CTL_DEL:将fd从监听红黑树上摘下
fd:待监听的文件描述符
event:传入参数,epoll监听事件结构体,events为返回的监听事件类型(可选EPOLLIN、EPOLLOUT、EPOLLER等),
data为一联合体,其泛参成员ptr可用于构建epoll反应堆模型。
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
epoll_ctl:成功返回0,失败返回-1和errno。
(3)epoll_wait
NAME
epoll_wait, epoll_pwait - wait for an I/O event on an epoll file descriptor
SYNOPSIS
#include
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
epfd:epoll_create创建的红黑树根节点的文件描述符
events:数组型传出参数,满足监听条件的文件描述符对应的epoll_event结构体
maxevents:数组元素的总个数
timeout:超时时间,-1 阻塞;0 立即返回,不阻塞;>0 单位ms。
epoll_wait返回值:成功返回有多少个文件描述符就绪,时间到返回0,出错返回-1。
epoll函数的编程难度比较简单,分为三步:
第1步创建监听红黑树树根epollfd;
第2步将listen_socket添加到红黑树上;
第3步实施监听,当返回listen_sock读事件时,创建conn_sock,并添加到监听红黑树,当返回conn_sock对应的读事件时,进行服务器与客户端相关的操作。
使用举例:
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
/* Code to set up listening socket, 'listen_sock',
(socket(), bind(), listen()) omitted */
epollfd = epoll_create1(0); //第1步创建监听红黑树树根epollfd
if (epollfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = listen_sock; //第2步将listen_socket添加到红黑树上
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;) { //第3步实施监听,当返回listen_sock读事件时,创建conn_sock,并添加到监听红黑树,当返回conn_sock对应的读事件时,进行服务器与客户端相关的操作
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (n = 0; n < nfds; ++n) {
if (events[n].data.fd == listen_sock) {
conn_sock = accept(listen_sock,
(struct sockaddr *) &addr, &addrlen);
if (conn_sock == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
setnonblocking(conn_sock);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
&ev) == -1) {
perror("epoll_ctl: conn_sock");
exit(EXIT_FAILURE);
}
} else {
do_use_fd(events[n].data.fd);
}
}
}
说明:编译时需要添加wrap.h和wrap.c
#include
#include
#include
#include
#include
#include
#include "wrap.h"
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
int i, j, n, nready;
int maxfd = 0;
int listenfd, connfd;
char buf[BUFSIZ]; /* #define INET_ADDRSTRLEN 16 */
struct sockaddr_in clie_addr, serv_addr;
socklen_t clie_addr_len;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family= AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port= htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(listenfd, 128);
fd_set rset, allset; /* rset 读事件文件描述符集合 allset用来暂存 */
maxfd = listenfd;
FD_ZERO(&allset);
FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */
while (1) {
rset = allset; /* 每次循环时都从新设置select监控信号集 */
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if (nready < 0)
perr_exit("select error");
if (FD_ISSET(listenfd, &rset)) { /* 说明有新的客户端链接请求 */
clie_addr_len = sizeof(clie_addr);
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不会阻塞 */
FD_SET(connfd, &allset); /* 向监控文件描述符集合allset添加新的文件描述符connfd */
if (maxfd < connfd)
maxfd = connfd;
if (0 == --nready) /* 只有listenfd有事件, 后续的 for 不需执行 */
continue;
}
for (i = listenfd+1; i <= maxfd; i++) { /* 检测哪个clients 有数据就绪 */
if (FD_ISSET(i, &rset)) {
if ((n = Read(i, buf, sizeof(buf))) == 0) { /* 当client关闭链接时,服务器端也关闭对应链接 */
Close(i);
FD_CLR(i, &allset); /* 解除select对此文件描述符的监控 */
} else if (n > 0) {
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Write(i, buf, n);
}
}
}
}
Close(listenfd);
return 0;
}
使用client[1024]数组来打理需要监听的文件
说明:编译时需要添加wrap.h和wrap.c
#include
#include
#include
#include
#include
#include
#include "wrap.h"
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
int i, j, n, maxi;
int nready, client[FD_SETSIZE]; /* 自定义数组client, 防止遍历1024个文件描述符 FD_SETSIZE默认为1024 */
int maxfd, listenfd, connfd, sockfd;
char buf[BUFSIZ], str[INET_ADDRSTRLEN]; /* #define INET_ADDRSTRLEN 16 */
struct sockaddr_in clie_addr, serv_addr;
socklen_t clie_addr_len;
fd_set rset, allset; /* rset 读事件文件描述符集合 allset用来暂存 */
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family= AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port= htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(listenfd, 128);
maxfd = listenfd; /* 起初 listenfd 即为最大文件描述符 */
maxi = -1; /* 将来用作client[]的下标, 初始值指向0个元素之前下标位置 */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* 用-1初始化client[] */
FD_ZERO(&allset);
FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */
while (1) {
rset = allset; /* 每次循环时都从新设置select监控信号集 */
nready = select(maxfd+1, &rset, NULL, NULL, NULL); //2 1--lfd 1--connfd
if (nready < 0)
perr_exit("select error");
if (FD_ISSET(listenfd, &rset)) { /* 说明有新的客户端链接请求 */
clie_addr_len = sizeof(clie_addr);
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不会阻塞 */
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
ntohs(clie_addr.sin_port));
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) { /* 找client[]中没有使用的位置 */
client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */
break;
}
if (i == FD_SETSIZE) { /* 达到select能监控的文件个数上限 1024 */
fputs("too many clients\n", stderr);
exit(1);
}
FD_SET(connfd, &allset); /* 向监控文件描述符集合allset添加新的文件描述符connfd */
if (connfd > maxfd)
maxfd = connfd; /* select第一个参数需要 */
if (i > maxi)
maxi = i; /* 保证maxi存的总是client[]最后一个元素下标 */
if (--nready == 0)
continue;
}
for (i = 0; i <= maxi; i++) { /* 检测哪个clients 有数据就绪 */
if ((sockfd = client[i]) < 0)
continue;
if (FD_ISSET(sockfd, &rset)) {
if ((n = Read(sockfd, buf, sizeof(buf))) == 0) { /* 当client关闭链接时,服务器端也关闭对应链接 */
Close(sockfd);
FD_CLR(sockfd, &allset); /* 解除select对此文件描述符的监控 */
client[i] = -1;
} else if (n > 0) {
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Write(sockfd, buf, n);
Write(STDOUT_FILENO, buf, n);
}
if (--nready == 0)
break; /* 跳出for, 但还在while中 */
}
}
}
Close(listenfd);
return 0;
}
wrap.h
#ifndef __WRAP_H_
#define __WRAP_H_
void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
#endif
wrap.c
#include
#include
#include
#include
#include
void perr_exit(const char *s)
{
perror(s);
exit(-1);
}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
int n;
again:
if ((n = accept(fd, sa, salenptr)) < 0) {
if ((errno == ECONNABORTED) || (errno == EINTR))
goto again;
else
perr_exit("accept error");
}
return n;
}
int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
if ((n = bind(fd, sa, salen)) < 0)
perr_exit("bind error");
return n;
}
int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
if ((n = connect(fd, sa, salen)) < 0)
perr_exit("connect error");
return n;
}
int Listen(int fd, int backlog)
{
int n;
if ((n = listen(fd, backlog)) < 0)
perr_exit("listen error");
return n;
}
int Socket(int family, int type, int protocol)
{
int n;
if ((n = socket(family, type, protocol)) < 0)
perr_exit("socket error");
return n;
}
ssize_t Read(int fd, void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = read(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = write(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
int Close(int fd)
{
int n;
if ((n = close(fd)) == -1)
perr_exit("close error");
return n;
}
/*参三: 应该读取的字节数*/
ssize_t Readn(int fd, void *vptr, size_t n)
{
size_t nleft; //usigned int 剩余未读取的字节数
ssize_t nread; //int 实际读到的字节数
char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ((nread = read(fd, ptr, nleft)) < 0) {
if (errno == EINTR)
nread = 0;
else
return -1;
} else if (nread == 0)
break;
nleft -= nread;
ptr += nread;
}
return n - nleft;
}
ssize_t Writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
if (nwritten < 0 && errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;
}
static ssize_t my_read(int fd, char *ptr)
{
static int read_cnt;
static char *read_ptr;
static char read_buf[100];
if (read_cnt <= 0) {
again:
if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
if (errno == EINTR)
goto again;
return -1;
} else if (read_cnt == 0)
return 0;
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return 1;
}
ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = my_read(fd, &c)) == 1) {
*ptr++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
*ptr = 0;
return n - 1;
} else
return -1;
}
*ptr = 0;
return n;
}