开源C/C++网络库比较:ACE、livevent、mongoose和Boost
这几天一直在做linux大批量数据的解决方案,不断的深入了解了一下aio,epoll,libevent,boost::asio。以前只知道他们都是做异步/非阻塞的,但是具体解决的问题的关键点是什么,通过这几天的深入了解,把他们总结一下: aio是linux2.6以后内核实现的异步IO,或者说他才是真正意义上的异步IO。epoll作为select的linux的替代品,解决了select...
这几天一直在做linux大批量数据的解决方案,不断的深入了解了一下aio,epoll,libevent,boost::asio。以前只知道他们都是做异步/非阻塞的,但是具体解决的问题的关键点是什么,通过这几天的深入了解,把他们总结一下:
- aio是linux2.6以后内核实现的异步IO,或者说他才是真正意义上的异步IO。
- epoll作为select的linux的替代品,解决了selectfd_set的限制。性能优于select。而在max平台上替代方案是kqueue。
- libevent是一个跨平台异步解决方案,他根据不同的平台提供了不同的异步方案,采用Reactor模型实现。
- Boost::asio是一个跨平台的网络及底层IO的C++编程库,实现了对TCP、UDP、ICMP、串口的支持。对于读写方式,ASIO支持同步和异步两种方式。采用了epoll来实现,插入了大量的信号处理。Asio库不需要单独便于,但是测试过程中对boost::system的依赖可能会需要编译部分boost中的库。
- muduo采用Reactor模型实现的网络库,只支持Linux 2.6.x下的并发非阻塞TCP网络编程,不跨平台,不支持udp和ipv6。吞吐量方面muduo比libevent2快18%,在事件处理效率方面,muduo与libevent2总体比较接近,muduo吞吐量比boost.asio高15%以上。性能方面作为解决大数据吞吐量很有优势,但是对平台和网络协议支持方面是一个问题。
- ACE也是很经典的网络库,出自《C++网络编程》作者之手,设计精妙程度堪称一流,支持协议范围也很广,但是使用复杂度和学习复杂度较高,一直有“学我者生,用我者死”的评价。
- libev是一个C语言写的,只支持linux系统的库,我以前研究的时候只封装了EPOLL模型, 不知道现在的新版有没有改进。使用方法类似libevent, 但是非常简洁,代码量是最少的一个库,也就几千行代码。显然这样的代码跨平台肯定是无法支持的了, 如果你只需要在linux下面运行,那用这个库也是可以的。
需要注意的是他们的定位不同,aio和epoll主要是对异步提供解决方案不是网络库不提供网络支持,而libevent也是主要解决IO的问题只提供简单的http支持,asio和muduo还有ACE一样是高性能网络库。
下面简单地与ACE做个比较。
1、层次架构:
ACE底层是C风格的OS适配层,上一层基于C++的wrap类,再上一层是一些框架(Accpetor, Connector,Reactor,Proactor等),最上一层是框架上服务。
Boost.ASIO与之类似,底层是OS的适配层,上一层一些模板类,再上一层模板类的参数化(TCP/UDP),再上一层是服务,它只有一种框架为io_service。
livevent在不同的操作系统下,做了多路复用模型的抽象,可以选择使用不同的模型,通过事件函数提供服务。
2、涉及范围:
ACE包含了日志,IPC,线程池,共享内存,配置服务,递归锁,定时器等。
ASIO只涉及到Socket,提供简单的线程操作。
libevent只提供了简单的网络API的封装, 线程池, 内存池, 递归锁等均需要自己实现。
3、设计模式:
ACE主要应用了Reactor,Proactor等。
而ASIO主要应用了Proactor模式。
libevent为Reactor模式
4、线程调度:
ACE的Reactor是单线程调度,Proactor支持多线程调度。
ASIO支持单线程与多线程调度。
libevent的线程调度需要自己来注册不同的事件句柄。
5、事件分派处理:
ACE主要是注册handler类,当事件分派时,调用其handler的虚挂勾函数。实现ACE_Handler / ACE_Svc_Handler / ACE_Event_handler等类的虚函数。
ASIO是基于函数对象的hanlder事件分派。任何函数都可能成为hanlder,少了一堆虚表的维护,调度上优于ACE。
libevent基于注册的事件回调函数来实现事件分发。
6、发布方式:
ACE是开源免费的,不依赖于第3方库, 一般应用使用它时,以动态链接的方式发布动态库。
ASIO是开源免费的,依赖Boost,应用使用时只要include头文件,不需动态库。
libevent为开源免费的,一般编译为静态库进行使用。
7、可移植性:
ACE支持多种平台,可移植性不存在问题,据说socket编程在linux下有不少bugs。
ASIO支持多种平台,可移植性不存在问题。
libevent主要支持linux平台,freebsd平台, 其他平台下通过select模型进行支持, 效率不是太高。
8、开发难度:
基于ACE开发应用,对程序员要求比较高,要用好它,必须非常了解其框架。在其框架下开发,往往new出一个对象,不知在什么地方释放好。
基于ASIO开发应用,要求程序员熟悉函数对象,函数指针,熟悉boost库中的boost::bind。内存管理控制方面。
基于libevent开发应用,相对容易, 具体大家可以参考memcached这个开源的应用,里面使用了libevent这个库。
http://wanglimin2004.blog.163.com/blog/static/115488498201271611723476/
ZeroMQ:
普通socket是端到端,ZeroMQ却可以N:M的关系
3中通讯模式:
请求回应模型,请求段发起请求,等待回应端回应请求。 请求端与回应端是1:N的,可以扩展成N:M的。
发布订阅模型,发布端单向发送数据,不关心信息是否都发送给了订阅端。订阅端只负责接收,不反馈。若交互,需要额外的socket采用请求回应模型实现。
管道模型,管道是单向的,从PUSH端单向的向PULL端单向的推送数据流。
###############################
首先说明一下几个基础概念:
IO操作:
IO操作包括两个部分:
- 等待数据准备好:对于一个套接口上的操作,这一步骤关系到数据从网络到达,并将其复制到内核的某个缓冲区。
- 将数据从内核缓冲区复制到进程缓冲区。
同步IO和异步IO:
- 同步IO导致请求进程阻塞,直到IO操作完成;
- 异步IO不导致请求进程阻塞。
IO多路复用(select,poll,epoll)
使用c++进行网络开发,socket几乎是一切技术的基石。最简单的socket套接字用法就是listen后调用accept阻塞等待客户端的连接,每当有一个连接到来的时候,创建子套接字对连接进行处理,因为网络传输中最影响性能的是IO的读写,这样的话如果短时间内有多个连接请求,socket只能一个一个的去处理。前一个IO进行读写的时候,因为进程阻塞,accept是没法准备接收下一个连接的。
这种情况有个简单的解决方式,就是accept返回后,在新的线程中进行数据收发,这样主线程里面的accept可以继续接收下一个客户端连接请求。这种方式可以同时处理多个IO,但是会产生创建销毁进程的开销,特别是在短任务的情况下开销会更大。
IO多路复用可以在单个线程内监听多个socket连接请求,此模型用到select和poll函数,这两个函数也会使进程阻塞,select先阻塞,有活动套接字才返回,但是和阻塞I/O不同的是,这两个函数可以同时阻塞多个I/O操作,而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写(就是监听多个socket)。select被调用后,进程会被阻塞,内核监视所有select负责的socket,当有任何一个socket的数据准备好了,select就会返回套接字可读,我们就可以调用recvfrom处理数据。正因为阻塞I/O只能阻塞一个I/O操作,而I/O复用模型能够阻塞多个I/O操作,所以才叫做多路复用。
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。
select:
select系统调用的目的是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常事件。poll和select应该被归类为这样的系统 调用:它们可以阻塞地同时探测一组连接请求,直至某一个设备触发了事件或者超过了指定的等待时间——也就是说它们的职责不是做IO,而是帮助调用者寻找当前就绪的设备。
IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。
select的优点:可以在一个线程上同时监听多个连接请求。
select的几大缺点:
(1)每次调用select,都需要把fd集合(文件描述符)从用户态拷贝到内核态,这个开销在fd很多时会很大
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
(3)select支持的文件描述符数量太小了,默认是1024
poll:
poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,poll支持的文件描述符数量没有限制,其他的都差不多。
epoll:
epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。那epoll都是怎么解决的呢?在此之前,我们先看一下epoll和select和poll的调用接口上的不同,select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。
对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select中的实现是类似的)。
对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目在linux上可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。
异步IO(iocp,epoll)
epoll属于IO多路复用,它只是模拟实现了异步IO的功能。 “真正”的异步IO需要操作系统更强的支持。在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。
IOCP全称 IO完成端口。它是一种WIN32的网络I/O模型,既包括了网络连接部分,也负责了部分的I/O操作功能,用于方便我们控制有并发性的网络I/O操作。它有如下特点:
1:它是一个WIN32内核对象,所以无法运行于linux。
2:它自己负责维护了工作线程池,同时也负责了I/O通道的内存池。
3:它自己实现了线程的管理以及I/O请求通知,最小化的做到了线程的上下文切换。
4:它自己实现了线程的优化调度,提高了CPU和内存缓冲的使用率。
真正意义上的异步IO严格的来说只有IOCP,但是epoll也模拟实现了异步IO的功能。
epoll 因为采用 mmap的机制, 使得 内核socket buffer和 用户空间的 buffer共享, 从而省去了 socket data copy, 这也意味着, 当epoll 回调上层的 callback函数来处理 socket 数据时, 数据已经从内核层 "自动" 到了用户空间, 虽然和 用poll 一样, 用户层的代码还必须要调用 read/write, 但这个函数内部实现所触发的深度不同了。
poll 时, poll通知用户空间的Appliation时, 数据还在内核空间, 所以Appliation调用 read API 时, 内部会做 copy socket data from kenel space to user space。
而用 epoll 时, epoll 通知用户空间的Appliation时, 数据已经在用户空间, 所以 Appliation调用 read API 时, 只是读取用户空间的 buffer, 没有 kernal space和 user space的switch了。
IOCP和Epoll之间的异同
异:
1:IOCP是WINDOWS系统下使用。Epoll是Linux系统下使用。
2:IOCP是IO操作完毕之后,通过Get函数获得一个完成的事件通知。
Epoll是当你希望进行一个IO操作时,向Epoll查询是否可读或者可写,若处于可读或可写状态后,Epoll会通过epoll_wait进行通知。
3:IOCP封装了异步的消息事件的通知机制,同时封装了部分IO操作。但Epoll仅仅封装了一个异步事件的通知机制,并不负责IO读写操作,但是因为mmap机制,epoll其实已经省去了IO操作的第二部分(将数据从内核缓冲区复制到进程缓冲区)。
基于上面的描述,我们可以知道Epoll不负责IO操作,所以它只告诉你当前可读可写了,并且将协议读写缓冲填充,由用户去读写控制,此时我们可以做出额 外的许多操作。IOCP则直接将IO通道里的读写操作都做完了才通知用户,当IO通道里发生了堵塞等状况我们是无法控制的。
同:
1:它们都是异步的事件驱动的网络模型。
2:它们都可以向底层进行指针数据传递,当返回事件时,除可通知事件类型外,还可以通知事件相关数据(通知到来时IO已经完全完成)。
还有一个概念,边缘触发和水平触发,可了解也可不了解。详见https://blog.csdn.net/liu0808/article/details/52980413
Libevent
libevent是一个轻量级的基于事件驱动的高性能的开源网络库,并且支持多个平台,对多个平台的I/O复用技术进行了封装。在linux下面集成了poll,epoll;在window下面集成了select,旧版本没有集成IOCP,所以在window上面 libevent的性能并不优秀,新版本的libevent也集成了IOCP,但是只作为网络开发库的话,libevent的综合评价还是不如boost.asio。
对于网络通信Libevent和boost.asio功能相近,但是asio综合性能更好,而且集成到了boost里面,只需要引入头文件即可使用。所以需要开发高性能web服务的时候,推荐使用asio,在这里就不再臃述libevent。
(如对libevent有兴趣可参考https://www.cnblogs.com/nearmeng/p/4043548.html)
Boost.asio
Boost.Asio是利用当代C++的先进方法,跨平台,异步I/O模型的C++网络库,Windows下使用IOCP,Linux下使用epoll。下面是一个asio使用多线程异步IO的网络服务器demo。
#include "stdafx.h"
#ifdef WIN32
#define _WIN32_WINNT 0x0501
#include <stdio.h>
#endif
#include <iostream>
#include <boost/bind.hpp>
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread/thread.hpp>
#include <boost/enable_shared_from_this.hpp>
using namespace boost::asio;
using namespace boost::posix_time;
io_service service;
class talk_to_client;
typedef boost::shared_ptr<talk_to_client> client_ptr;
typedef std::vector<client_ptr> array;
array clients;
#define MEM_FN(x) boost::bind(&self_type::x, shared_from_this())
#define MEM_FN1(x,y) boost::bind(&self_type::x, shared_from_this(),y)
#define MEM_FN2(x,y,z) boost::bind(&self_type::x, shared_from_this(),y,z)
void update_clients_changed();
//业务类,对每个事件进行io读写及处理
class talk_to_client : public boost::enable_shared_from_this<talk_to_client>
, boost::noncopyable {
typedef talk_to_client self_type;
talk_to_client() : sock_(service), started_(false),
timer_(service), clients_changed_(false) {
}
public:
typedef boost::system::error_code error_code;
typedef boost::shared_ptr<talk_to_client> ptr;
void start() {
started_ = true;
clients.push_back(shared_from_this());
last_ping = boost::posix_time::microsec_clock::local_time();
do_read();
}
static ptr new_() {
ptr new_(new talk_to_client);
return new_;
}
void stop() {
sock_.close();
}
ip::tcp::socket & sock() { return sock_; }
std::string username() const { return username_; }
void set_clients_changed() { clients_changed_ = true; }
private:
void on_read(const error_code & err, size_t bytes) {
if (err) stop();
std::string msg(read_buffer_, bytes);
if (msg.find("GET") == 0) on_get(msg);
else if(msg.find("POST") == 0) on_post(msg);
}
void on_get(const std::string & msg) {
std::istringstream in(msg);
in >> username_ >> username_;
std::cout << username_ << " logged in" << std::endl;
std::string str_out = "HTTP/1.1 200 OK\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Access-Control-Allow-Methods: *\r\n"
"Content-Type: application/json;charset=UTF-8\r\n"
"Server: wz simple httpd 1.0\r\n"
"Connection: close\r\n"
"\r\n";
str_out = str_out + "{\"result\":\"asio get ok\"}";
do_write(str_out);
}
void on_post(const std::string & msg) {
std::istringstream in(msg);
in >> username_ >> username_;
std::cout << username_ << " logged in" << std::endl;
std::string str_out = "HTTP/1.1 200 OK\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Access-Control-Allow-Methods: *\r\n"
"Content-Type: application/json;charset=UTF-8\r\n"
"Server: wz simple httpd 1.0\r\n"
"Connection: close\r\n"
"\r\n";
str_out = str_out + "{\"result\":\"asio post ok\"}";
//Sleep(5000);
do_write(str_out);
}
void on_write(const error_code & err, size_t bytes) {
stop();
}
void do_read() {
sock_.async_read_some(buffer(read_buffer_), MEM_FN2(on_read, _1, _2));
}
void do_write(const std::string & msg) {
std::copy(msg.begin(), msg.end(), write_buffer_);
sock_.async_write_some(buffer(write_buffer_, msg.size()),
MEM_FN2(on_write, _1, _2));
}
private:
ip::tcp::socket sock_;
enum { max_msg = 1024 };
char read_buffer_[max_msg];
char write_buffer_[max_msg];
bool started_;
std::string username_;
boost::posix_time::ptime last_ping;
bool clients_changed_;
};
ip::tcp::acceptor acceptor(service, ip::tcp::endpoint(ip::tcp::v4(), 82));//监听端口号
void handle_accept(talk_to_client::ptr client, const boost::system::error_code & err);
void ConnFunc(talk_to_client::ptr client)
{
client->start();
return;
}
void handle_accept(talk_to_client::ptr client, const boost::system::error_code & err) {
client->start();
talk_to_client::ptr new_client = talk_to_client::new_();
acceptor.async_accept(new_client->sock(), boost::bind(handle_accept, new_client, _1));
}
int _tmain(int argc, _TCHAR* argv[])
{
talk_to_client::ptr client = talk_to_client::new_();
acceptor.async_accept(client->sock(), boost::bind(handle_accept, client, _1));
boost::thread_group thrds;
for (int i = 0; i < 4; ++i)//创建了4个线程监听io,每个线程都可以监听多个连接请求。使用多线程主要是为了防止业务处理的时候阻塞,如果只是单纯的转发服务器,单线程效率更高。
{
thrds.create_thread(boost::bind(&boost::asio::io_service::run, &service));
}
thrds.join_all();
//service.stop();
return 0;
}
一般来说,高性能web服务器的io和业务处理都是分离的,服务器开销主要在io上。因为asio已经实现了异步io,所以如果只是作为转发服务器,只使用一个线程处理即可(多线程有线程切换的开销)。比如nginx就是使用单线程异步io的服务器。
但是如果io和业务处理没有分离,比如上例中处理post的时候 sleep了5秒(假设业务处理占用了5秒的时间),如果使用单线程那就会阻塞客户端新的请求(其实请求不会阻塞,只是asio的事件回调函数被阻塞了)。在这种情况下就需要使用如上例的多线程处理。
关于boost.asio介绍不错的两个链接:
https://blog.csdn.net/somestill/article/details/52159948
https://mmoaay.gitbooks.io/boost-asio-cpp-network-programming-chinese/content/Chapter1.html
Mongoose
mongoose是一个超轻量级的网络库,只包含两个文件 mongoose.c 和 mongoose.h两个文件,引入到工程中即可使用,无第三方依赖且跨平台。底层只使用了select,所以性能上当然没法和asio,libevent比,但是开发一般的小型的PC端web服务器足够了,mongoose也是我最终选定的服务器开发解决方案。
但是mongoose使用多线程是有些问题的,下面是使用 6.10版本实现的多线程web服务器demo:
// httpserver.cpp : 定义控制台应用程序的入口点。
//
//#define _SLIST_HEADER_
#include "stdafx.h"
#include <string>
#include <boost/thread/thread.hpp>
#include "mongoose.h"
boost::mutex lock;
static bool stop_webserver = false;
#define MAX_THREADS 10//最大线程数
static const char *s_http_port = "82";
static void ev_handler(struct mg_connection *nc, int ev, void *p)
{
if(NULL == nc || NULL==p)
{
return;
}
switch (ev)
{
case MG_EV_ACCEPT:
{
char addr[32];
mg_sock_addr_to_str(&nc->sa, addr, sizeof(addr),
MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT);
printf("Connection %p from %s\n", nc, addr);
break;
}
case MG_EV_HTTP_REQUEST:
{
char addr[32];
struct http_message *hm = (struct http_message *) p;
const char* uri = hm->uri.p;
mg_sock_addr_to_str(&nc->sa, addr, sizeof(addr),
MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT);
printf("HTTP request from %s: %.*s %.*s\n", addr, (int) hm->method.len,
hm->method.p, (int) hm->uri.len, hm->uri.p);
std::string method = std::string(hm->method.p, hm->method.len);
std::string str_out = "HTTP/1.1 200 OK\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Access-Control-Allow-Methods: *\r\n"
"Content-Type: application/json;charset=UTF-8\r\n"
"Server: wz simple httpd 1.0\r\n"
"Connection: close\r\n"
"\r\n";
if("POST"==method)
{
Sleep(5000);
str_out = str_out + std::string("{\"result\":\"mongoose post ok\"}");
}
else
{
str_out = str_out + std::string("{\"result\":\"mongoose get ok\"}");
}
mg_printf(nc, str_out.c_str());
nc->flags |= MG_F_SEND_AND_CLOSE;
break;
}
}
return;
}
void* process_proc(void* mgr)
{
while(!stop_webserver)
{
mg_mgr_poll((struct mg_mgr*)mgr, 1000);
}
mg_mgr_free((struct mg_mgr*)mgr);
return NULL;
}
int TestHttpServer(void) {
struct mg_mgr mgr;
struct mg_connection *nc;
stop_webserver = false;
mg_mgr_init(&mgr, NULL);
nc = mg_bind(&mgr, s_http_port, ev_handler);
mg_set_protocol_http_websocket(nc);
for(int i = 0; i < MAX_THREADS; i++)
{
mg_start_thread(process_proc,&mgr);
}
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
int result = TestHttpServer();
while(1)
{
Sleep(1000);
}
return result;
}
mongoose6.10版本开启多线程的方法在官网和百度上都没有找到明确的介绍,国内网站上能找到的都是很老版本的demo,公司没有条件翻墙,不清楚谷歌上有没有靠谱的解决方案。我捋了一下源码,感觉只需要使用 mg_start_thread 复制多个线程即可(如上例)。但是在模拟高并发请求的时候,内核事件触发的时候有几率会崩溃。我调试了半天只能确定是事件触发的时候使用了已经释放的nc对象,这种情况大部分是指针对象释放的时候指针没置NULL造成的,但是我经过各种尝试始终没能修复。感觉6.10版本的代码应该是没有bug的,或许是我的使用方式不对,希望知道原因的朋友能够留言赐教一下。
6.10版本学习资源实在太少,几乎只有源码和部分简单的单线程demo,所以我降低了版本,使用了mongoose5.2版本实现多线程的功能。但是也有一些问题,5.2的版本可能是编译环境太老了,我下载下来的时候是不能编译通过的,修改了源码最终能正常使用,高并发测试功能也比较稳定,没有像6.10版本那样崩溃。但是6.10和5.2的源码相比变动实在是太大了,除了底层还是使用select感觉大部分功能和函数都已经变化了。比如多线程的实现,5.2是建立了一个监听线程,多个工作线程进行处理,虽然也能实现并发,但是设计方式确实有点太陈旧(本质上就是多线程阻塞socket加select),demo如下:
// httpserver.cpp : 定义控制台应用程序的入口点。
//
//#define _SLIST_HEADER_
#include "stdafx.h"
#include <string>
#include <boost/thread/thread.hpp>
#include "./utils/mongoose.h"
#include <windows.h>
#define MAX_THREADS 10
static struct mg_server *server[MAX_THREADS];
static bool stop_webserver = false;
static const char* port = "82";
static const char* local_fullpath = "/Users/alex/";
static int request_handler(struct mg_connection *conn)
{
std::string req_str = std::string(conn->request_method);
std::string str_out = "HTTP/1.1 200 OK\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Access-Control-Allow-Methods: *\r\n"
"Content-Type: application/json;charset=UTF-8\r\n"
"Server: wz simple httpd 1.0\r\n"
"Connection: close\r\n";
std::string str_out2;
if("POST" == req_str)
{
str_out2 = "{\"result\":\"xia post ok\"}";
Sleep(3000);
}
else
{
str_out2 = "{\"result\":\"xia get ok\"}";
}
mg_printf(conn,str_out.c_str());
mg_send_data(conn,str_out2.c_str(),str_out2.length());
return 1;
}
void* process_proc(void* p_server)
{
while(!stop_webserver)
{
mg_poll_server((struct mg_server*)p_server, 500);
}
return NULL;
}
bool start_webserver(void) {
stop_webserver = false;
for(int i = 0; i < MAX_THREADS; i++)
{
server[i] = mg_create_server(NULL);
if(i == 0)
{
mg_set_option(server[i], "listening_port", port);
}
else
{
mg_set_listening_socket(server[i], mg_get_listening_socket(server[0]));
}
mg_set_option(server[i], "document_root", local_fullpath);
mg_set_request_handler(server[i], request_handler);
mg_start_thread(process_proc, server[i]);
}
return true;
}
void stop_simple_webserver()
{
stop_webserver = true;
for(int i = 0; i < MAX_THREADS; i++)
{
mg_destroy_server(&server[i]);
}
}
int _tmain(int argc, _TCHAR* argv[])
{
int result = start_webserver();
while(1)
{
Sleep(1000);
}
return result;
}
代码极简,都不用说明了。虽然5.2的这种方式和效果也算可以,可是最新版本都6.12了,始终差强人意。我还是希望能使用最新版本的mongoose实现稳定的多线程服务器。
关于各个版本的mongoose下载地址:https://github.com/cesanta/mongoose/releases
总结:
不论是强大相对复杂的boost.asio,libevent 还是轻简的 mongoose,他们都是封装了select,poll,epoll,iocp。
select,poll都只是IO复用,IO其实还是阻塞的。
iocp是真正意义上的异步IO,因为它使用 win32的 完成端口 这个设计思想让内核完全代替 用户线程完成了监听和读写的功能,当内核发送信号给用户线程的时候一切事情都做完了,收到的数据都被内核放进用户线程空间了,是完全没有IO阻塞的。
epoll严格来说还只是IO复用,但是因为mmap机制, 使得 内核socket buffer和 用户空间的 buffer共享,所以内核收到数据后不需要再让用户线程二次拷贝,也同样模拟实现了异步IO的功能。
当然IO的第一步操作 “等待数据准备好”不论使用哪种方式都是阻塞的,但这种阻塞是在内核,用户线程就不用关心了。
我的理解是不论哪种技术,最最底层都是靠多线程实现的,单线程说到底都是阻塞的,因为单线程是不可能在一个时间片里同时干多件事的(钻牛角尖)。那些强大的技术,比如epoll,iocp等是在内核里面使用多线程和各种优化方案提高了性能,只是最后暴露给我们用户态的是单线程。
关于mongoose6.10版本多线程的bug,希望有兴趣的朋友多多指点一下。本篇博客只是学习记录,参考了多篇博客内容,有些内容并不是原创,有些理解可也能有误,欢迎拍砖。
---------------------
https://blog.csdn.net/u010810750/article/details/81778731
更多推荐
所有评论(0)