一、select
1.select 模型服务程序的流程
创建socket的集合fd_set =》 把监听的socket和客户端的socket加入集合fd_set=》 select(maxfd,fd_set,NULL,NULL,NULL); =》 用FD_ISSET判断fd_set中有事件的socket =》 a.监听的socket有事件,表示有新客户端的连接请求 b.客户端的socket有事件:1)有数据可读;2)socket连接断开
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <unistd.h>
#include <vector>
#include <string.h>
struct Message {
int proid;
int v;
char data[1024];
};
int main(int argc,char** argv) {
int server_fd = socket(AF_INET,SOCK_STREAM,0);//_protocol:0-> 1
if (server_fd < 0) {
return 1;
}
int opt = 1;
if(setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))) {
return 1;
}
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = 8888;
if(bind(server_fd,(sockaddr*)&addr,sizeof(addr))) {
return 1;
}
if(listen(server_fd,5)) { // 5!!!
return 1;
}
int fds_max = server_fd + 1;
fd_set fd_read;
FD_ZERO(&fd_read);
std::vector<int> vec_client_fds;
while (true)
{
fd_set fd_tmp = fd_read;
int fd_num = select(fds_max,&fd_tmp,nullptr,nullptr,nullptr);
if(fd_num < 0) {
continue;
}
if(FD_ISSET(server_fd,&fd_read))
{
sockaddr_in addr{};
socklen_t len = sizeof(addr);
int client_fd = accept(server_fd,(sockaddr*)&addr,&len);
if(client_fd < 0) {
continue;
}
FD_SET(client_fd,&fd_read);
if(client_fd > fds_max)
fds_max = client_fd;
vec_client_fds.push_back(client_fd);
}
for (auto it = vec_client_fds.begin();it != vec_client_fds.end();it++) {
if(FD_ISSET(*it,&fd_read)) {
Message msg;
ssize_t read_buf = recv(*it,&msg,sizeof(msg),0);
if(read_buf <= 0)//close or error
{
close(*it);
FD_CLR(*it,&fd_read);
it = vec_client_fds.erase(it);
}else if(read_buf == sizeof(Message)){
//response
Message msg;
msg.proid = 10001;
msg.v = 1;
std::string str = "hello from server";
strncpy(msg.data,str,strlen(str)+1);
msg.data[strlen(str)] = '\0';
send(*it,&msg,sizeof(msg),0);
}
}
}
}
return 0;
}
二、poll
poll 函数是 Unix/Linux 系统中用于监视多个文件描述符状态变化的一种机制。poll 函数能够同时监视多个文件描述符,以检查其上是否发生了感兴趣的事件(如可读、可写、错误等)。为了实现这一功能,poll 函数使用了一个 pollfd 结构体数组作为输入参数,每个 pollfd 结构体代表了一个被监视的文件描述符及其相关的事件。
三、epoll
epoll_create(); epoll_ctl(); epoll_wait();
#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);
if (epollfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;) {
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);
}
}
}
关键区别对比
| 特性 | Select/Poll | Epoll (边缘触发) |
|---|---|---|
| 触发方式 | 水平触发 | 边缘触发 |
| 通知频率 | 只要有数据就重复通知 | 只在状态变化时通知一次 |
| 数据读取 | 可以部分读取 | 必须一次性读完所有数据 |
| 阻塞风险 | 低(知道有数据) | 高(可能阻塞在recv) |
| 非阻塞要求 | 推荐但不必须 | 必须 |
-
Epoll必须设置非阻塞:因为边缘触发模式要求一次性读完所有数据,否则会丢失事件通知
-
Select/Poll可以不设置非阻塞:因为水平触发模式会重复通知,直到所有数据被读取
服务器启动 → 创建socket() → 绑定bind() → 监听listen()
这个监听socket会被立即加入到 readset 中
当监听socket"可读"时,不代表有数据,而是代表有新的客户端正在连接
调用select即委托内核去检测文件描述符 readset是否有可读(包括监听socket和通信socket(客户端简建立了连接的socket)), writeset是否可写,是否有异常
内核挂起进程,先检测到监听的文件描述符在缓冲区有数据时,使用accpet建立连接(不阻塞),FD_SET更新readset,下一轮则会检测到通信的文件描述符缓冲区是否有数据,则使用recv/read接收数据。这里可以用多线程处理这2类描述符
fd_set tmp = readfds;
int ret = select(maxfd+1,&tmp,NULL,NULL,NULL);
这里需要使用一个拷贝:因为
当 select 返回时,它会修改你传入的集合。它将这些集合中的内容改变为:只有那些真正处于就绪状态的文件描述符对应的位仍然被设置为1。而那些你监视了但未就绪的文件描述符,其对应的位会被内核清零。
select 连接上限1024(默认)
检测效率: epoll > poll/select
数据结构:红黑树, 线性表(结构体)
socket :1个监听的文件描述符 + n个通信的文件描述符
nfds :文件描述符的最大值+1
读文件描述符集合,写描述符,异常文件描述符
fd_set: bitMap位图存储(8bit = 1byte),大小为:1024/8bit=128 byte = 128/4= 32 int(32个整形数)
poll接口主要解决了select接口的两个问题,一个是select监视的fd有上限,另一个是select每次调用前都需要借助第三方数组,向fd_set里面重新设置关心的fd。
select() 和 poll() 系统调用的本质一样,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll() 没有最大文件描述符数量的限制(但是数量过大后性能也是会下降)。
poll() 和 select() 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
ET和LT对比
1.select和poll其实也是工作在LT模式下. epoll既可以支持LT, 也可以支持ET
2.LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是一次响应就绪过程中就把所有的数据都处理完
3.ET的性能比LT性能更高( epoll_wait 返回的次数少了很多
4.使用 ET 模式的 epoll, 需要将文件描述设置为非阻塞.