经过之前四篇博文的介绍,可以大致清楚各种模型的编程步骤。现在我们来回顾下各种模型(转载请指明出于breaksoftware的csdn博客)
模型编程步骤对比
《朴素、Select、Poll和Epoll网络编程模型实现和分析——朴素模型》中介绍的是最基本的网络编程模型,我们使用单线程去实现,它的步骤很简单
如同这幅流程图,它简单到非常单薄。这个模型暴露出来的问题是在一个线程中,它一次只能处理一个请求。当然我们可以通过多线程或者多进程的模式对该模型进行改进:主线程监听接入socket请求,将其保存到一段空间中。其他线程从该段空间中获取socket并处理。但是这种改善不是从网络编程模型的角度出发的,而是多线程/多进程的一种应用,这种应用也可以用于之后几个模型。所以我们暂且抛开这种优化来讨论。
《朴素、Select、Poll和Epoll网络编程模型实现和分析——Select模型》解决了朴素模型同步的执行的问题,而且还解决了一次只能处理一个请求的问题。
可以见得这种模型稍微厚实了一点,但是它也有一个问题:最多只能同时处理1024个请求。致命的是这种限制是在内核代码级别的(详见之前文章的分析)。为了解决这个问题于是就有了poll模型。
《朴素、Select、Poll和Epoll网络编程模型实现和分析——Poll模型》中介绍的Poll模型结构和Select模型是非常相似的,但是它不再依赖于结构体的位数来限制最多处理的socket数,而是采用一个数组去保存数据。
poll模型的问题是其效率随着同时连接的socket数而下降。因为它和Select模型有着相同的缺陷——每次只是知道有事件发生,但是不知道是哪个socket有事件发生,于是需要遍历所有的socket。这样socket如果越来越多,它的效率自然会下降。为了解决这个问题,于是有了Epoll模型。
《朴素、Select、Poll和Epoll网络编程模型实现和分析——Epoll模型》算是目前最优秀的解决多连接的模型。但是它也是最复杂的模型。
模型效率对比
首先明确一个立场,没有实际测试数据的对比都是空谈。网上一般充斥的一种观点就是EPOLL模型效率最好,Poll和Select模型相当,朴素模型最差。但是这个观点是错误的。因为不同的模型在不同的使用场景下有着不同的效率。
比如,在我们之前测试的场景下。朴素模型的平均处理能力大概是15000次/秒,Select的平均处理能力是7000次/秒,Poll的平均处理能力是7500次/秒,Epoll的平均处理能力是11000次/秒。为什么朴素模型的处理能力是最高的?因为我们这个是一个典型的短连接、一次性交互的场景,这种场景下如果得到一个请求马上去读和写,而不经过其他复杂的过程,自然是最快的。Poll模型和Select模型能力相当。Epoll是对Poll模型的优化,所以它的效率比Select和Poll要好一些。
我们使用valgrind对上述四种模型的执行情况进行分析。
首先我们看朴素模型
朴素模型的main函数自身(即刨除下面列出的函数的其他执行时间)的执行时间占比是非常小的(5.45%),其主要的耗时操作是读和写操作(server_write占38.94%,server_read占27.64%)。
我们再看下效率第二的Epoll模型
可见Epoll模型中,server_write操作耗时占比最高(29.32%),其次是main函数自身耗时占(25.7%),再次是server_read(22.48%)。
再看下效率倒数第二的Poll模型
Poll模型中main函数自身耗时占比最高(46.93%),其次是server_write(21.44%),再次是server_read(16.44%)。我们看到main函数自身耗时提高非常多。
最后我们看看效率最低的Select模型
Select模型中,main函数自身耗时占比最多(97.99%),其次是server_write(0,83%),再次是server_read(0.64%)。那么main函数中什么是最耗时的呢?我对Select模型源码进行了细分,将之前循环遍历各个Socket的逻辑提炼出来
代码语言:javascript复制void deal_socket(int index, int listen_sock, fd_set* active_fd_set, fd_set* read_fd_set) {
if (listen_sock == index) {
/* Connection request on original socket. */
int new_sock;
new_sock = accept(listen_sock, NULL, NULL);
if (new_sock < 0) {
perror("accept error");
exit(EXIT_FAILURE);
}
request_add(1);
FD_SET(new_sock, active_fd_set);
} else {
if (0 == server_read(index)) {
server_write(index);
}
close(index);
FD_CLR(index, active_fd_set);
}
}
void deal_request(int listen_sock, fd_set* active_fd_set, fd_set* read_fd_set) {
int index = 0;
for (index = 0; index < FD_SETSIZE; index) {
if (FD_ISSET(index, read_fd_set)) {
deal_socket(index, listen_sock, active_fd_set, read_fd_set);
}
}
}
再进行测试,我们得出以下结果
deal_request函数函数内部理论上最大调用deal_socket的次数是30万*1024=3072万,而实际调用deal_socket的次数只有60万。这说明平均一次调用deal_request只会有2个socket被命中从而调用deal_socket。也就是说,1次select操作只有2个socket会被处理,而其他1022次循环操作都是不被命中的比较。而恰恰最耗时的就是deal_request操作(deal_socket操作占用deal_request时间不足2%),所以我们可以得出select模型中最耗时的就是对socket的遍历操作。而其根本原因是select函数每次返回时可以被处理的socket数量太少——2个。
那么epoll和poll模型的循环使用率是多少?我们对它们的代码做点改造,我们引入两个标记函数——total_loop_count和deal_sock_count,前者用于统计一共循环了多少次,后者用于统计需要处理的socket多少次。我们先看Poll模型的结果。
可见Poll模型中对pollfd数组遍历的次数一共是60万次,有效处理socket也是60万次。使用率100%,而不像Select模型的使用率只有60/3072=2%。同样epoll的使用率也是100%。
通过上述分析,我们可以看出在短连接、一次性读写完数据的情况下,朴素模型的效率是最高的,其次是Epoll、Poll、Select。其实Select模型性能和Poll是差不多的,在我们测试场景下产生的差距是因为我们实现Select模式的方式问题。我们对其可以参照Poll模型进行改造,使用一个int型数组保存接入的socket,同时动态记录可用socket的个数。这样我们每次遍历就只遍历连续的可用socket数组空间,而不用像例子中将整个数组都遍历,从而提高循环操作的有效率。
那么什么时候才是该使用Select、Poll或者Epoll模型的呢?我们将在下一篇博文中对其进行分析。