第十篇文章中将select函数换做了poll,解决了客户端最大并发数量限制的问题,但通过压力测试会发现连接建立的速度是比较慢的,不管时select还是poll,都会存在这个问题。
select会受fd_set集合大小的限制,该大小不易更改,需要重新编译内核;同时也受系统所能打开的最大文件描述符的限制,该限制易更改。
poll只受系统所能打开的最大文件描述符的限制,前面也专门提到了该如何更改这个限制;该值的极限值受物理内存大小的限制,查看/proc/sys/fs/file-max文件可查看当前系统所能打开的最大文件描述符,4G内存的电脑上这一数值差不多有378675。该两者在使用时,都需要内核遍历所有的文件描述符,直到找到所有发生事件的文件描述符,并通知应用程序,所以当文件描述符非常多时,该两者的效率就会明显下降,这在上一篇中用压力测试客户端可看出来。
1、select、poll和epoll的区别
相比于select与poll,epoll最大的好处在于它不会随着监听文件描述符数目的增长而降低效率。
内核中的select与poll的实现是采用轮询来处理的,轮询的fd数目越多,自然耗时越多
epoll的实现是基于回调的,如果fd有期望的事件发生,就通过回调函数将其加入epoll就绪队列中,也就是说它只关心“活跃”的fd,与fd数目无关;
内核/用户空间内存拷贝问题,如何让内核把fd消息通知给用户空间呢?在这个问题上,select/poll采用内存拷贝方法,而epoll采用共享内存的方式;
epoll不仅会告诉应用程序有I/O事件到来,还会告诉应用程序相关的信息,这些信息是应用程序填充的,因此根据这些信息,应用程序就能直接定位到事件,而不必遍历整个fd集合
2、改进poll服务端
将上一篇文章中的服务端程序用epoll重新实现,提高效率!
handle.h/handle.c 与上一篇文章相同
客户端程序暂时不做更改,client.c/connclient.c 与上一篇文章相同
服务端源码
因为要使用容器,所以采用C++改写,编译命令
g++ -Wall -g server.cpp handle.c -o server
server.cpp
#include <string.h>
#include <signal.h>
#include <sys/epoll.h>
#include "handle.h"
#include <vector>
#include <iostream>
#include <algorithm>
typedef std::vector<struct epoll_event> Eventlist;
#define FD_MAXSIZE 2048 //最大客户端数量
//为了防止SIGPIPE信号产生而终止了进程,所以捕获此信号
void handle_sigpipe(void)
{
struct sigaction act;
act.sa_handler = SIG_IGN; //忽略SIGPIPE信号
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
if(sigaction(SIGPIPE, &act, NULL) == -1)
handle_error("sigaction");
}
/*
* activate_noblock -- 设置IO为非阻塞模式
* 参数
fd:套接字
*/
void activate_nonblock(int fd)
{
int iret = 0;
int flags = fcntl(fd,F_GETFL);
if(flags == -1)
handle_error("fcntl");
flags |= O_NONBLOCK;
iret = fcntl(fd,F_SETFL,flags);
if(iret == -1)
handle_error("fcntl");
}
int main(void)
{
init(FD_MAXSIZE);
handle_sigpipe(); //捕获SIGPIPE信号
int sk_fd = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
if(sk_fd < 0)
handle_error("socket");
//使用REUSEADDR,不必等待TIME_WAIT 状态消失,就可以重新使用端口
int on = 1;
if(setsockopt(sk_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
{
close(sk_fd);
handle_error("setsockopt");
}
struct sockaddr_in sr_addr;
memset(&sr_addr,0,sizeof(sr_addr));
sr_addr.sin_family = AF_INET;
sr_addr.sin_port = htons(5188);
sr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sk_fd, (struct sockaddr*)&sr_addr, sizeof(sr_addr)) < 0)
{
close(sk_fd);
handle_error("bind");
}
//被动套接字
if(listen(sk_fd, SOMAXCONN) < 0) //内核为此套接字排队的最大连接数由SOMAXCONN宏指定
{
close(sk_fd);
handle_error("listen");
}
std::vector<int> clients; //客户端连接套接字集合
int epfd; //epoll实例描述符
//创建一个EPOLL实例,EPOLL_CLOEXEC指定当进程被替换时,自动关闭epfd
epfd = epoll_create1(EPOLL_CLOEXEC);
struct epoll_event event;
event.data.fd = sk_fd; //监听套接字
event.events = EPOLLIN | EPOLLET; //监听的事件
epoll_ctl(epfd, EPOLL_CTL_ADD, sk_fd, &event); //监听套接字添加事件管理
Eventlist events(16); //记录哪些IO产生了事件
struct sockaddr_in peeraddr;
socklen_t peerlen;
int conn_sk;
int nready;
int count = 0; //客户端数目
while(true)
{
//检测哪些IO发生了事件
nready = epoll_wait(epfd, &*events.begin(), static_cast<int>(events.size()), -1);
if(nready == 0)
{
printf("epoll_wait time out!\n");
continue;
}
else if(nready == -1)
{
if(errno == EINTR)
continue;
handle_error("epoll_wait");
}
if(nready == static_cast<int>(events.size())) //events集合空间不够用了
events.resize(events.size()*2);
//遍历事件
for(int i = 0; i < nready; i++)
{
if(events[i].data.fd == sk_fd) //监听套接字发生了事件
{
peerlen = sizeof(struct sockaddr_in);
conn_sk = accept(sk_fd, (struct sockaddr*)&peeraddr, &peerlen);
if(conn_sk < 0)
{
if(errno == EINTR)
continue;
handle_error("accept");
}
clients.push_back(conn_sk); //已连接套接字添加到客户端套接字集合中
activate_nonblock(conn_sk); //设置套接字为非阻塞模式
event.data.fd = conn_sk;
event.events = EPOLLIN | EPOLLET; //监听的事件
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sk, &event); //添加事件管理
printf("Count = %d: Connect ip = %s\tport = %d\n",count++,inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
}
else if(events[i].events & EPOLLIN)
{
conn_sk = events[i].data.fd;
if(conn_sk < 0)
continue;
char recvbuf[1024] = {0};
int iret = recvline(conn_sk,recvbuf,sizeof(recvbuf));
if(iret == -1)
handle_error("recvline");
if(iret == 0)
{
printf("Client was closed!\n");
event = events[i];
//从监听集合中删除它
epoll_ctl(epfd, EPOLL_CTL_DEL, conn_sk,&event);
//删除该客户端的已连接套接字
clients.erase(std::remove(clients.begin(), clients.end(),conn_sk), clients.end());
close(conn_sk);
}
fputs(recvbuf, stdout);
writen(conn_sk, recvbuf, strlen(recvbuf));
}
}
}
for(unsigned int i = 0; i < clients.size(); i++)
{
close(clients[i]);
}
close(sk_fd);
return 0;
}
3、压力测试
编译上一篇中的压力测试客户端:
gcc -Wall -g -std=gnu99 connclient.c -o client
通过连接服务端,可明显发现速度快多了!