资源算法 Sinetlib

Sinetlib

2020-01-15 | |  30 |   0 |   0

Sinetlib: A High Perfermance C++ Network Library

Introduction

Sinetlib是一个仿照Muduo实现的基于Reactor模式的多线程网络库,附有异步日志,要求Linux 2.6以上内核版本。同时内嵌一个简洁HTTP服务器,可实现路由分发及静态资源访问。

Feature

  • 底层使用Epoll LT模式实现I/O复用,非阻塞I/O

  • 多线程、定时器依赖于c++11提供的std::thread、std::chrono库

  • Reactor模式,主线程accept请求后,使用Round Robin分发给线程池中线程处理

  • 基于小根堆的定时器管理队列

  • 双缓冲技术实现的异步日志

  • 使用智能指针及RAII机制管理对象生命期

  • 使用状态机解析HTTP请求,支持HTTP长连接

  • HTTP服务器支持URL路由分发及访问静态资源,可实现RESTful架构

Envoirment

  • OS: Ubuntu 16.04

  • Complier: g++ 5.4

  • Build: CMake

Tutorial

C++网络编程实战项目--Sinetlib网络库(1)——概述

C++网络编程实战项目--Sinetlib网络库(2)——I/O复用与事件分发

C++网络编程实战项目--Sinetlib网络库(3)——事件循环与跨线程调用

C++网络编程实战项目--Sinetlib网络库(4)——线程池和整体框架

C++网络编程实战项目--Sinetlib网络库(5)——HTTP服务器设计与实现

Model

主线程负责连接的建立,并通过Round Robin方式将连接分配给工作线程处理

图片.png

Build

需先安装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

Usage

net

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)

http

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);

Fix List

  • 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%,并随着线程数的增多提升。

Contact

More

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...