Sinetlib
Sinetlib是一个仿照Muduo实现的基于Reactor模式的多线程网络库,附有异步日志,要求Linux 2.6以上内核版本。同时内嵌一个简洁HTTP服务器,可实现路由分发及静态资源访问。
底层使用Epoll LT模式实现I/O复用,非阻塞I/O
多线程、定时器依赖于c++11提供的std::thread、std::chrono库
Reactor模式,主线程accept请求后,使用Round Robin分发给线程池中线程处理
基于小根堆的定时器管理队列
双缓冲技术实现的异步日志
使用智能指针及RAII机制管理对象生命期
使用状态机解析HTTP请求,支持HTTP长连接
HTTP服务器支持URL路由分发及访问静态资源,可实现RESTful架构
OS: Ubuntu 16.04
Complier: g++ 5.4
Build: CMake
C++网络编程实战项目--Sinetlib网络库(1)——概述
C++网络编程实战项目--Sinetlib网络库(2)——I/O复用与事件分发
C++网络编程实战项目--Sinetlib网络库(3)——事件循环与跨线程调用
C++网络编程实战项目--Sinetlib网络库(4)——线程池和整体框架
C++网络编程实战项目--Sinetlib网络库(5)——HTTP服务器设计与实现
主线程负责连接的建立,并通过Round Robin方式将连接分配给工作线程处理
需先安装Cmake:
$ sudo apt-get update $ sudo apt-get install cmake
开始构建
$ git clone git@github.com:silence1772/Sinetlib.git $ cd Sinetlib $ ./build.sh
执行完上述脚本后编译结果在新生成的build文件夹内,示例程序在build/bin下。
库和头文件分别安装在/usr/local/lib和/usr/local/include,该库依赖c++11及pthread库,使用方法如下:
$ g++ main.cpp -std=c++11 -lSinetlib -lpthread
在执行生成的可执行文件时可能会报找不到动态库文件,需要添加动态库查找路径,首先打开配置文件:
$ sudo vim /etc/ld.so.conf
在打开的文件末尾添加这句,保存退出:
include /usr/local/lib
使修改生效:
$ sudo /sbin/ldconfig
Sinetlib的使用十分简单,用户只需设置四个回调即可。四个回调抽象的是TCP连接建立后、有消息到达、答复消息完成、连接关闭这四个状态,用户可以设置各个状态对应的执行函数。
在此之前,用户需要先创建Looper和Server,Server的第二和第三个参数分别是监听的端口号和线程池中线程数,一般情况下建议线程数与CPU核心数接近,以最大程度发挥多线程性能。
#include "server.h"#include <iostream>void OnConnection(const std::shared_ptr<Connection>& conn) { std::cout << "OnConnection" << std::endl; }void OnMessage(const std::shared_ptr<Connection>& conn, IOBuffer* buf, Timestamp t) { std::cout << "OnMessage" << std::endl; }void OnReply(const std::shared_ptr<Connection>& conn) { std::cout << "OnReply" << std::endl; }void OnClose(const std::shared_ptr<Connection>& conn) { std::cout << "OnClose" << std::endl; }int main() { Looper loop; Server s(&loop, 8888, 4); s.SetConnectionEstablishedCB(OnConnection); s.SetMessageArrivalCB(OnMessage); s.SetReplyCompleteCB(OnReply); s.SetConnectionCloseCB(OnClose); s.Start(); loop.Start(); }
可通过telnet测试上述程序,测试结果如下(左边为telnet程序,带有$号为用户输入内容,右边为服务器输出):
$ telnet 127.0.0.1 8888 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. OnConnection $ test message OnMessage $ ^] telnet> quit OnClose
在提供给用户设置的各个回调函数中,都有一个指向当前连接的指针,在消息到达回调中还暴露了保存接收到的消息的IOBuffer缓冲区指针。
用户可以对这两个指针进行操作,通过读取缓冲区和调用Connection::Send()实现消息的收发。
Connection类:
// 发送数据到连接的对端void Send(const void* data, size_t len);void Send(const std::string& message);void Send(IOBuffer& buffer);// 关闭连接中自己的这一端,这会激发TCP的四次挥手进而关闭整个连接void Shutdown();// 获取连接描述符const int GetFd()// 获取输入输出缓冲区const IOBuffer& GetInputBuffer()const IOBuffer& GetOutputBuffer()
IOBuffer:
// 获取可读、可写、预留区的大小size_t GetReadableSize() size_t GetWritableSize() size_t GetPrependSize()// 获取可读、可写区的首指针const char* GetReadablePtr()const char* GetWritablePtr()// 寻找回车换行符/r/nconst char* FindCRLF()const char* FindCRLF(const char* start_ptr)// 寻找换行符/nconst char* FindEOL()const char* FindEOL(const char* start_ptr)// 向后移动可读区指针到实际位置void Retrieve(size_t len)// 移动可读区指针到指定位置void RetrieveUntil(const char* end)// 将可读、可写区指针均移到初始位置,即重置缓冲区void RetrieveAll()// 添加数据进缓冲区void Append(const char* data, size_t len) void Append(const std::string& str)// 添加数据进预留区void Prepend(const void* data, size_t len)
Sinetlib内嵌了一个HTTP服务器,设计上借鉴了golang的mux包实现路由分发的思想,同时还支持静态资源访问。
用户可以根据需要设置路由及匹配条件,目前支持URL、请求参数、请求头及请求方法的匹配,并且可以从中提取出参数。 一个请求必须满足所有条件才能匹配这个路由,得到它的Handler。
首先要设置相应的路由处理函数,该函数由用户实现,如果要实现静态资源访问,则需使用HttpServer的文件处理函数,该函数会将访问映射到用户指定的路径。
Route::SetHandler(YourHandler); Route::SetHandler(HttpServer::GetFileHandler("/home/mys/"));
可以对url进行匹配,并且可以设置正则表达式匹配规则,比如下面第一句匹配对于/path/xxx的访问,并且可以通过key来获取到xxx,此处限定了key必须为字母。 也可以不设置正则匹配条件,如第二句的key可以匹配任何字符。
Route::SetPath("/path/{key:[a-zA-Z]+}"); Route::SetPath("/path/{key}");
匹配方法头、头部字段、参数:
Route::SetMethod("GET"); Route::SetHeader("Header-Name", "Header-Value"); Route::SetQuery("Filed", "Value");
匹配前缀,可通过“file_path"获取”/file/"后面的路径:
Route::SetPrefix("/file/");
完整的程序使用如下,该程序有两个路由,其中第二个为静态资源服务。
#include "httpserver.h"void MyHandler(const HttpRequest& request, std::unordered_map<std::string, std::string>& match_map, HttpResponse* response) { response->SetStatusCode(HttpResponse::OK); response->SetStatusMessage("OK"); response->SetContentType("text/html"); std::string body = "temp test page"; response->AddHeader("Content-Length", std::to_string(body.size())); response->AppendHeaderToBuffer(); response->AppendBodyToBuffer(body); }int main() { Looper loop; HttpServer s(&loop, 8888, 4); s.NewRoute() ->SetPath("/path/{name:[a-zA-Z]+}") ->SetQuery("query", "t") ->SetHeader("Connection", "keep-alive") ->SetHandler(MyHandler); s.NewRoute() ->SetPrefix("/file/") ->SetHandler(s.GetFileHandler("/home/mys/")); s.Start(); loop.Start(); }
该程序可匹配下面两个HTTP请求样例,第二个请求对应访问位于/home/mys/test.jpg的文件,可通过浏览器访问127.0.0.1:8888/path/myname?query=t和127.0.0.1:8888/file/test.jpg实现下列请求
GET /path/myname?query=t HTTP/1.1 Connection: keep-alive GET /file/test.jpg HTTP/1.1
用户使用时可通过HttpServer::NewRoute()创建一个新路由,并设置相应的匹配条件及处理函数。在有请求到来时会按照用户创建路由的顺序进行匹配,当有一个路由下的条件全部匹配,即可得到该路由的处理函数并执行。
对于静态资源访问,需要使用Route::SetPrefix("/prefix/")设置前缀,并配合使用HttpServer::GetFileHandler(“/map/path/")设置映射路径,那么对于/prefix/my/src的访问将会被映射到/map/path/my/src
成功匹配请求后即可执行对应的处理函数,这里暴露给了用户HttpRequest、map<string, string>、HttpResponse三个类,大概的使用就是从HttpRequest中取得请求的各项内容,同时可以从map中根据之前设置的key取得相应的value,比如上述程序的第一个路由就可取出‘name’的值。然后用户再把响应的内容写入HttpResponse即可。
需要注意的是HttpResponse的使用,HttpResponse内有一个发送缓冲区,用户需要先设置该类的头部信息,然后执行AppendHeaderToBuffer()把这些信息写入缓冲区,然后再直接往缓冲区中写入消息的主体。
HttpRequest接口如下:
// 获取方法头const char* GetMethodStr()// 获取HTTP版本Version GetVersion()// 获取URLconst std::string& GetPath()// 获取参数const std::string GetQuery(const std::string& field)// 获取头部字段std::string GetHeader(const std::string& field)// 获取接收时间Timestamp GetReceiveTime()
HttpResponse接口如下:
// 设置短连接void SetCloseConnection(bool on)// 设置响应状态码void SetStatusCode(HttpStatusCode code)// 设置状态信息void SetStatusMessage(const std::string& message)// 添加头部字段void AddHeader(const std::string& field, const std::string& value)// 设置Content-Typevoid SetContentType(const std::string& content_type)// 将响应头写入到缓冲区中void AppendHeaderToBuffer();// 将消息主体写入到缓冲区中void AppendBodyToBuffer(std::string& body);
2018-11-15 修复对于短连接未写完数据就关闭连接的错误
Connection::Shutdown()没有判断当前数据是否发送完就直接关闭连接,导致当数据一次不能写完而连接又被shutdown时,一直在写一个已关闭的连接,产生死循环。
2018-11-15 修复文件句柄泄漏
当连接总数超过1000左右时程序异常终止,检查core文件定位到fopen()打开文件失败,查看errno发现打开的文件数超过系统限制1024。重新运行进程,cd 到 /proc/进程号/fd,查看目录内容即为打开的文件描述符,发现当新连接建立时新增socket描述符,当连接断开时却不减少,最终定位到connection.cpp中析构函数没有close掉socket。
2018-11-15 修复HTTP服务器中打开dir没有关闭导致泄漏的问题
2018-11-29 修复多线程下unordered_map不安全的错误
旧版本对于每个connection的解析器parser都是由主线程管理,存放在map里,当工作线程同时去map里去parser时就会产生错误,新版模仿muduo使用类似c++17的std::any,将parser直接嵌入到connection里,以避免出现不可重入现象
2018-11-29 修复cpu占用100%错误
当连接发生EPOLLERR时未能调用关闭连接回调,由于使用LT模式导致一直产生EPOLLERR事件,陷入死循环占用cpu;在eventbase的事件分发里对EPOLLERR事件调用关闭回调即可解决
2019-02-19 使用无锁队列替换原有任务队列
使用一个高性能并发队列moodycamel::ConcurrentQueue替代原有vector with mutex,经测试,在单生产者单消费者下性能提升10%-50%,多生产者情况下性能提升可达到100%-500%,并随着线程数的增多提升。
Mail: silence1772@qq.com
to be continued
上一篇:sinesp-client
下一篇:sinesp-nodejs
还没有评论,说两句吧!
热门资源
seetafaceJNI
项目介绍 基于中科院seetaface2进行封装的JAVA...
spark-corenlp
This package wraps Stanford CoreNLP annotators ...
Keras-ResNeXt
Keras ResNeXt Implementation of ResNeXt models...
capsnet-with-caps...
CapsNet with capsule-wise convolution Project ...
inferno-boilerplate
This is a very basic boilerplate example for pe...
智能在线
400-630-6780
聆听.建议反馈
E-mail: support@tusaishared.com