開源基於asio的網絡通訊框架asio2,支持TCP,UDP,HTTP,RPC,SSL,跨平臺,支持可靠UDP,支持TCP自動拆包,TCP數據報模式等

開源基於asio的網絡通訊框架asio2,支持TCP,UDP,HTTP,RPC,SSL,跨平臺,支持可靠UDP,支持TCP自動拆包,TCP數據報模式等 

C++開發網絡通訊程序時用asio是個不錯的選擇,但asio自己是一套函數集,本身還要處理諸如「通訊線程池管理、鏈接及生命週期管理、多線程收發數據的同步保護等」。所以這裏對asio進行了一層封裝,大大簡化了對asio的使用。代碼使用了C++17相關功能,因此只能用在C++17以上。html

其中http和websocket部分用的是boost::beast,所以若是須要用到http或websocket的功能,則必須使用boost庫,若是用不到http則直接使用獨立的asio便可。在config.hpp中經過對ASIO_STANDALONE這個宏定義的開關,便可設置是使用boost::asio仍是使用asio standalone.linux

代碼大量使用了CRTP模板編程實現(沒有使用virtual而用CRTP實現的靜態多態),所以編譯比較耗時,但執行效率相對較好一點。c++


github地址https://github.com/zhllxt/asio2git

碼雲地址https://gitee.com/zhllxt/asio2github

A open source cross-platform c++ library for network programming based on asio,support for tcp,udp,http,rpc,ssl and so on.web

  • 支持TCP,UDP,HTTP,WEBSOCKET,RPC,ICMP,SERIAL_PORT等;
  • 支持可靠UDP(基於KCP),支持SSL,支持從內存字符串加載SSL證書;
  • TCP支持數據拆包功能(按指定的分隔符對數據自動進行拆包,保證用戶收到的數據是一個完整的數據包);實現了TCP的數據報模式(相似WEBSOCKET);
  • 支持windows,linux,32位,64位;
  • 依賴asio(boost::asio或獨立asio都可,若須要HTTP功能必須使用boost::asio),依賴C++17;
  • 代碼採用hpp頭文件方式,以源碼級鏈入,無需編譯,只需在工程的Include包含目錄中添加asio2路徑,而後在源碼中#include <asio2/asio2.hpp>包含頭文件便可;
  • demo目錄包含大量的示例工程(工程基於VS2017建立),各類使用方法請參考示例代碼;

TCP:

服務端:

asio2::tcp_server server;
server.bind_recv([&server](std::shared_ptr<asio2::tcp_session> & session_ptr, std::string_view s)
{
    printf("recv : %u %.*s\n", (unsigned)s.size(), (int)s.size(), s.data());
    // 異步發送(全部發送操做都是異步且線程安全的)
    session_ptr->send(s);
    // 發送時指定一個回調函數,當發送完成後會調用此回調函數,bytes_sent表示實際發送的字節數,
    // 發送是否有錯誤能夠用asio2::get_last_error()函數來獲取錯誤碼
    // session_ptr->send(s, [](std::size_t bytes_sent) {});
}).bind_connect([&server](auto & session_ptr)
{
    printf("client enter : %s %u %s %u\n",
        session_ptr->remote_address().c_str(), session_ptr->remote_port(),
        session_ptr->local_address().c_str(), session_ptr->local_port());
    // 能夠用session_ptr這個會話啓動一個定時器,這個定時器是在這個session_ptr會話的數據收
    // 發線程中執行的,這對於鏈接狀態的判斷或其它需求頗有用(尤爲在UDP這種無鏈接的協議中,有
    // 時須要在數據處理過程當中使用一個定時器來延時作某些操做,並且這個定時器還須要和數據處理
    // 在同一個線程中安全觸發)
    //session_ptr->start_timer(1, std::chrono::seconds(1), []() {});
}).bind_disconnect([&server](auto & session_ptr)
{
    printf("client leave : %s %u %s\n",
        session_ptr->remote_address().c_str(),
        session_ptr->remote_port(), asio2::last_error_msg().c_str());
});
server.start("0.0.0.0", "8080");
//server.start("0.0.0.0", "8080", '\n'); // 按\n自動拆包(能夠指定任意字符)
//server.start("0.0.0.0", "8080", "\r\n"); // 按\r\n自動拆包(能夠指定任意字符串)
//server.start("0.0.0.0", "8080", match_role('#')); // 按match_role指定的規則自動拆包(match_role請參考demo代碼)(用於對用戶自定義的協議拆包)
//server.start("0.0.0.0", "8080", asio::transfer_exactly(100)); // 每次接收固定的100字節
//server.start("0.0.0.0", "8080", asio2::use_dgram); // 數據報模式的TCP,不管發送多長的數據,雙方接收的必定是相應長度的整包數據

  

客戶端:

asio2::tcp_client client;
client.bind_connect([&](asio::error_code ec)
{
    if (asio2::get_last_error())
        printf("connect failure : %d %s\n", asio2::last_error_val(), asio2::last_error_msg().c_str());
    else
        printf("connect success : %s %u\n", client.local_address().c_str(), client.local_port());

    client.send("<abcdefghijklmnopqrstovuxyz0123456789>");
}).bind_disconnect([](asio::error_code ec)
{
    printf("disconnect : %d %s\n", asio2::last_error_val(), asio2::last_error_msg().c_str());
}).bind_recv([&](std::string_view sv)
{
    printf("recv : %u %.*s\n", (unsigned)sv.size(), (int)sv.size(), sv.data());

    client.send(sv);
})
    //.bind_recv(on_recv) // 綁定全局函數
    //.bind_recv(std::bind(&listener::on_recv, &lis, std::placeholders::_1)) // 綁定成員函數(具體請查看demo代碼)
    //.bind_recv(&listener::on_recv, lis) // 按lis對象的引用來綁定成員函數(具體請查看demo代碼)
    //.bind_recv(&listener::on_recv, &lis) // 按lis對象的指針來綁定成員函數(具體請查看demo代碼)
    ;
client.async_start("0.0.0.0", "8080"); // 異步鏈接服務端
//client.start("0.0.0.0", "8080"); // 同步鏈接服務端
//client.async_start("0.0.0.0", "8080", '\n'); // 按\n自動拆包(能夠指定任意字符)
//client.async_start("0.0.0.0", "8080", "\r\n"); // 按\r\n自動拆包(能夠指定任意字符串)
//client.async_start("0.0.0.0", "8080", match_role); // 按match_role指定的規則自動拆包(match_role請參考demo代碼)(用於對用戶自定義的協議拆包)
//client.async_start("0.0.0.0", "8080", asio::transfer_exactly(100)); // 每次接收固定的100字節
//client.start("0.0.0.0", "8080", asio2::use_dgram); // 數據報模式的TCP,不管發送多長的數據,雙方接收的必定是相應長度的整包數據

// 發送時也能夠指定use_future參數,而後經過返回值future來阻塞等待直到發送完成,發送結果的錯誤碼和發送字節數
// 保存在返回值future中(注意,不能在通訊線程中用future去等待,這會阻塞通訊線程進而致使死鎖)
// std::future<std::pair<asio::error_code, std::size_t>> future = client.send("abc", asio::use_future); 

  

UDP:

服務端:

asio2::udp_server server;
// ... 綁定監聽器(請查看demo代碼)
server.start("0.0.0.0", "8080"); // 常規UDP
//server.start("0.0.0.0", "8080", asio2::use_kcp); // 可靠UDP

  

客戶端: 

asio2::udp_client client;
// ... 綁定監聽器(請查看demo代碼)
client.start("0.0.0.0", "8080");
//client.async_start("0.0.0.0", "8080", asio2::use_kcp); // 可靠UDP

  

RPC:

服務端:

asio2::rpc_server server;
// ... 綁定監聽器(請查看demo代碼)
A a; // A的定義請查看demo代碼
server.bind("add", add); // 綁定RPC全局函數
server.bind("mul", &A::mul, a); // 綁定RPC成員函數
server.bind("cat", [&](const std::string& a, const std::string& b) { return a + b; }); // 綁定lambda表達式
server.bind("get_user", &A::get_user, a); // 綁定成員函數(按引用)
server.bind("del_user", &A::del_user, &a); // 綁定成員函數(按指針)
//server.start("0.0.0.0", "8080", asio2::use_dgram); // 使用TCP數據報模式做爲RPC通訊底層支撐,啓動服務端時必需要使用use_dgram參數
server.start("0.0.0.0", "8080"); // 使用websocket做爲RPC通訊底層支撐(須要到rcp_server.hpp文件末尾代碼中選擇使用websocket)

  

客戶端: 

asio2::rpc_client client;
// ... 綁定監聽器(請查看demo代碼)
//client.start("0.0.0.0", "8080", asio2::use_dgram); // 使用TCP數據報模式做爲RPC通訊底層支撐,啓動服務端時必需要使用use_dgram參數
client.start("0.0.0.0", "8080"); // 使用websocket做爲RPC通訊底層支撐
asio::error_code ec;
// 同步調用RPC函數
int sum = client.call<int>(ec, std::chrono::seconds(3), "add", 11, 2);
printf("sum : %d err : %d %s\n", sum, ec.value(), ec.message().c_str());
// 異步調用RPC函數,第一個參數是回調函數,當調用完成或超時會自動調用該回調函數,若是超時或其它錯誤,
// 錯誤碼保存在ec中,這裏async_call沒有指定返回值類型,則lambda表達式的第二個參數必需要指定類型
client.async_call([](asio::error_code ec, int v)
{
    printf("sum : %d err : %d %s\n", v, ec.value(), ec.message().c_str());
}, "add", 10, 20);
// 這裏async_call指定了返回值類型,則lambda表達式的第二個參數能夠爲auto類型
client.async_call<int>([](asio::error_code ec, auto v)
{
    printf("sum : %d err : %d %s\n", v, ec.value(), ec.message().c_str());
}, "add", 12, 21);
// 返回值爲用戶自定義數據類型(user類型的定義請查看demo代碼)
user u = client.call<user>(ec, "get_user");
printf("%s %d ", u.name.c_str(), u.age);
for (auto &[k, v] : u.purview)
{
    printf("%d %s ", k, v.c_str());
}
printf("\n");

u.name = "hanmeimei";
u.age = ((int)time(nullptr)) % 100;
u.purview = { {10,"get"},{20,"set"} };
// 若是RPC函數的返回值爲void,則用戶回調函數只有一個參數便可
client.async_call([](asio::error_code ec)
{
}, "del_user", std::move(u));

HTTP:

服務端:

asio2::http_server server;
server.bind_recv([&](std::shared_ptr<asio2::http_session> & session_ptr, http::request<http::string_body>& req)
{
    // 在收到http請求時嘗試發送一個文件到對端
    {
        // 若是請求是非法的,直接發送錯誤信息到對端並返回
        if (req.target().empty() ||
        req.target()[0] != '/' ||
        req.target().find("..") != beast::string_view::npos)
        {
            session_ptr->send(http::make_response(http::status::bad_request, "Illegal request-target"));
            session_ptr->stop(); // 同時直接斷開這個鏈接
            return;
        }

        // Build the path to the requested file
        std::string path(req.target().data(), req.target().size());
        path.insert(0, std::filesystem::current_path().string());
        if (req.target().back() == '/')
            path.append("index.html");

        // 打開文件
        beast::error_code ec;
        http::file_body::value_type body;
        body.open(path.c_str(), beast::file_mode::scan, ec);

        // 若是打開文件失敗,直接發送錯誤信息到對端並直接返回
        if (ec == beast::errc::no_such_file_or_directory)
        {
            session_ptr->send(http::make_response(http::status::not_found,
            std::string_view{ req.target().data(), req.target().size() }));
            return;
        }

        // Cache the size since we need it after the move
        auto const size = body.size();

        // 生成一個文件形式的http響應對象,而後發送給對端
        http::response<http::file_body> res{
        std::piecewise_construct,
        std::make_tuple(std::move(body)),
        std::make_tuple(http::status::ok, req.version()) };
        res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
        res.set(http::field::content_type, http::extension_to_mimetype(path));
        res.content_length(size);
        res.keep_alive(req.keep_alive()); 
        res.chunked(true);
        // Specify a callback function when sending
        //session_ptr->send(std::move(res));
        session_ptr->send(std::move(res), [&res](std::size_t bytes_sent)
        {
            auto opened = res.body().is_open(); std::ignore = opened;
            auto err = asio2::get_last_error(); std::ignore = err;
        });
        //session_ptr->send(std::move(res), asio::use_future);
        return;
    }

    std::cout << req << std::endl;
    if (true)
    {
        // 用make_response生成一個http響應對象,狀態碼200表示操做成功,"suceess"是HTTP消息的body部份內容
        auto rep = http::make_response(200, "suceess");
        session_ptr->send(rep, []()
        {
            auto err = asio2::get_last_error(); std::ignore = err;
        });
    }
    else
    {
        // 也能夠直接發送一個http標準響應字符串,內部會將這個字符串自動轉換爲http響應對象再發送出去
        std::string_view rep =
            "HTTP/1.1 404 Not Found\r\n"\
            "Server: Boost.Beast/181\r\n"\
            "Content-Length: 7\r\n"\
            "\r\n"\
            "failure";
        // test send string sequence, the string will automatically parsed into a standard http request
        session_ptr->send(rep, [](std::size_t bytes_sent)
        {
            auto err = asio2::get_last_error(); std::ignore = err;
        });
    }
});
server.start(host, port);

客戶端:

asio2::error_code ec;
auto req1 = http::make_request("http://www.baidu.com/get_user?name=a"); // 經過URL字符串生成一個http請求對象
auto req2 = http::make_request("GET / HTTP/1.1\r\nHost: 127.0.0.1:8443\r\n\r\n"); // 經過http協議字符串生成一個http請求對象
req2.set(http::field::timeout, 5000); // 給請求設置一個超時時間
auto rep1 = asio2::http_client::execute("http://www.baidu.com/get_user?name=a", ec); // 經過URL字符串直接請求某個網址,返回結果在rep1中,若是有錯誤,錯誤碼保存在ec中
auto rep2 = asio2::http_client::execute("127.0.0.1", "8080", req2); // 經過IP端口以及前面生成的req2請求對象來發送一個http請求
std::cout << rep2 << std::endl; // 顯示http請求結果
std::stringstream ss;
ss << rep2;
std::string result = ss.str(); // 經過這種方式將http請求結果轉換爲字符串

 

其它的HTTP使用方式以及WEBSOCKET使用方式請參考demo代碼

 

ICMP:

class ping_test // 模擬在一個類對象中使用ping組件(其它全部如TCP/UDP/HTTP等組件同樣能夠在類對象中使用)
{
    asio2::ping ping;
public:
    ping_test() : ping(10) // 構造函數傳入的10表示只ping 10次後就結束,傳入-1表示一直ping
    {
        ping.timeout(std::chrono::seconds(3)); // 設置ping超時
        ping.interval(std::chrono::seconds(1)); // 設置ping間隔
        ping.body("0123456789abcdefghijklmnopqrstovuxyz");
        ping.bind_recv(&ping_test::on_recv, this) // 綁定當前這個類的成員函數做爲監聽器
            .bind_start(std::bind(&ping_test::on_start, this, std::placeholders::_1)) // 也是綁定成員函數
            .bind_stop([this](asio::error_code ec) { this->on_stop(ec); }); // 綁定lambda
    }
    void on_recv(asio2::icmp_rep& rep)
    {
        if (rep.lag.count() == -1) // 若是延時的值等於-1表示超時了
            std::cout << "request timed out" << std::endl;
        else
            std::cout << rep.total_length() - rep.header_length()
                << " bytes from " << rep.source_address()
                << ": icmp_seq=" << rep.sequence_number()
                << ", ttl=" << rep.time_to_live()
                << ", time=" << std::chrono::duration_cast<std::chrono::milliseconds>(rep.lag).count() << "ms"
                << std::endl;
    }
    void on_start(asio::error_code ec)
    {
        printf("start : %d %s\n", asio2::last_error_val(), asio2::last_error_msg().c_str());
    }
    void on_stop(asio::error_code ec)
    {
        printf("stop : %d %s\n", asio2::last_error_val(), asio2::last_error_msg().c_str());
    }
    void run()
    {
        if (!ping.start("127.0.0.1"))
        //if (!ping.start("123.45.67.89"))
        //if (!ping.start("stackoverflow.com"))
            printf("start failure : %s\n", asio2::last_error_msg().c_str());
        while (std::getchar() != '\n');
        ping.stop();
        // ping結束後能夠輸出統計信息,包括丟包率,平均延時時長等
        printf("loss rate : %.0lf%% average time : %lldms\n", ping.plp(),
            std::chrono::duration_cast<std::chrono::milliseconds>(ping.avg_lag()).count());
    }
};

  

串口:

請查看demo示例代碼serial port 部分

 

SSL: 

TCP/HTTP/WEBSOCKET均支持SSL功能(須要在config.hpp中將#define ASIO2_USE_SSL宏定義放開)

asio2::tcps_server server;
// 從內存字符串加載SSL證書(具體請查看demo代碼)
server.set_cert("test", cer, key, dh); // cer,key,dh這三個字符串的定義請查看demo代碼
// 從文件加載SSL證書
//server.set_cert_file("test", "server.crt", "server.key", "dh512.pem");

  

TCP/HTTP/WEBSOCKET服務端、客戶端等SSL功能請到DEMO代碼中查看。

 

其它:

定時器

// 框架中提供了定時器功能,使用很是簡單,以下:
asio2::timer timer;
// 參數1表示定時器ID,參數2表示定時器間隔,參數3爲定時器回調函數
timer.start_timer(1, std::chrono::seconds(1), [&]()
{
    printf("timer 1\n");
    if (true) // 知足某個條件時關閉定時器,固然也能夠在其它任意地方關閉定時器
        timer.stop_timer(1);
});

  

還有其它一些輔助類的功能,請在源碼或使用中去體會吧.

相關文章
相關標籤/搜索