Unique's Blog

C++网络库

2022-10-23 · 3072字 · 14 min read
🏷️  C++

转载和来源于:罗剑锋的 C++ 实战笔记

libcurl:高可移植、功能丰富的通信库

libcurl,来源于著名的curl 项目,也是 curl 的底层核心。

它最早只支持 HTTP 协议,但现在已经扩展到支持所有的应用层协议,比如 HTTPS、FTP、LDAP、SMTP 等,功能强大。

libcurl 使用纯 C 语言开发,兼容性、可移植性非常好,基于 C 接口可以很容易写出各种语言的封装,所以 Python、PHP 等语言都有 libcurl 相关的库。

ubuntu 下安装:

apt-get install libcurl4-openssl-dev

libcurl 的接口可以粗略地分成两大类:easy 系列和 multi 系列。其中,easy 系列是同步调用,比较简单;multi 系列是异步的多线程调用。

使用 libcurl 收发 HTTP 数据的基本步骤有 4 个:

  1. 使用 curl_easy_init() 创建一个句柄,类型是 CURL*。但我们完全没有必要关心句柄的类型,直接用 auto 推导就行。

  2. 使用 curl_easy_setopt() 设置请求的各种参数,比如请求方法、URL、header/body 数据、超时、回调函数等。这是最关键的操作。

  3. 使用 curl_easy_perform() 发送数据,返回的数据会由回调函数处理。

  4. 使用 curl_easy_cleanup() 清理句柄相关的资源,结束会话。

Code示例
#include <assert.h>
#include <curl/curl.h>
#include <iostream>

using namespace std;

// 回调函数的原型
// 回调会被多次调用,每次调用都会传递一块数据;ptr指向传递的数据,该数据的大小为nmemb;size始终为1
// size * nmemb 是缓冲区的大小
// ptr 是缓冲区的地址
// userdata 是用户自定义的数据
size_t write_callback(char*, size_t, size_t, void*);

int main() {
    auto curl = curl_easy_init(); // 创建CURL句柄
    assert(curl);

    curl_easy_setopt(curl, CURLOPT_URL, "http://nginx.org"); // 设置请求URI
    // 避免代理设置为长连接 Proxy-Connection:close或者不使用代理
    curl_easy_setopt(curl, CURLOPT_PROXY, "");
    curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);

    // 没有设置回调函数时,内部的默认回调,把得到的 HTTP 响应数据输出到标准流
    // c++11, 无捕获的 lambda 表达式可以显式转换成一个函数指针=>C使用
    curl_easy_setopt(
        curl, CURLOPT_WRITEFUNCTION, // 设置回调函数
        (decltype(&write_callback))  // decltype获取函数指针类型,显式转换
            [](char* ptr, size_t size, size_t nmemb, void* userdata) {
                cout << "size = " << size << "*" << nmemb << "=" << size * nmemb << endl; // 简单的处理
                return size * nmemb;                                                      // 返回接收的字节数
            });

    auto res = curl_easy_perform(curl); // 发送数据
    if (res != CURLE_OK) {              // 检查是否执行成功
        cout << curl_easy_strerror(res) << endl;
    }
    curl_easy_cleanup(curl); // 清理句柄相关的资源
    return 0;
}

推荐使用cpr代替它

cpr:更现代、更易用的通信库

cpr 是对 libcurl 的一个 C++11 封装,使用了很多现代 C++ 的高级特性,对外的接口模仿了 Python 的 requests 库,非常简单易用。

安装

下载源码,用 cmake 编译安装

git clone git@github.com:whoshuu/cpr.git
cmake . -DUSE_SYSTEM_CURL=ON -DBUILD_CPR_TESTS=OFF
make && make install

或者在 cmake 项目中直接使用[CMakeLists.txt]:

include(FetchContent)
FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git
                         GIT_TAG 871ed52d350214a034f6ef8a3b8f51c5ce1bd400) # The commit hash for 1.9.0. Replace with the latest from: https://github.com/libcpr/cpr/releases
FetchContent_MakeAvailable(cpr)

target_link_libraries(cpr_example PRIVATE cpr::cpr)

说明macos 下指定 clang++/clang 编译器生成项目;gcc 编译会失败

cpr 里,HTTP 协议的概念都被实现为相应的函数或者类,内部再转化为 libcurl 操作,主要的有:

  • GET/HEAD/POST 等请求方法,使用同名的 Get/Head/Post 函数;

  • URL 使用 Url 类,它是 string 的封装;

  • URL 参数使用 Parameters 类,KV 结构,近似 map;

  • 请求头字段使用 Header 类,它其实是 map 的别名,使用定制的函数实现了大小写无关比较;

  • Cookie 使用 Cookies 类,也是 KV 结构,近似 map;

  • 请求体使用 Body 类;超时设置使用 Timeout 类。

cpr 也支持异步处理,但它内部没有使用 libcurl 的 multi 接口,而是使用了标准库里的 future 和 async。

Code示例
#include <cpr/cpr.h>
#include <iostream>
#include <string>

int main(int argc, char** argv) {
    cpr::Response r = cpr::Get(cpr::Url{"https://api.github.com/repos/whoshuu/cpr/contributors"},
                               cpr::Authentication{"user", "pass", cpr::AuthMode::BASIC},
                               cpr::Parameters{{"anon", "true"}, {"key", "value"}});
    std::cout << "Status code: " << r.status_code << '\n';
    std::cout << "Header:\n";
    for (const std::pair<const std::basic_string<char>, std::basic_string<char>>& kv : r.header) {
        std::cout << '\t' << kv.first << ':' << kv.second << '\n';
    }
    std::cout << "Text: " << r.text << '\n';
    return 0;
}

ZMQ:高效、多功能的通信库

libcurlcpr 处理的都是 HTTP 协议,方便但协议自身也有一些限制,比如必须要一来一回,必须点对点直连,在超大数据量通信的时候就不是太合适。并且 libcurlcpr 只能充当 HTTP 的客户端

ZMQ 不仅是一个单纯的网络通信库,更像是一个高级的异步并发框架。

https://zeromq.org/get-started/

Zero Message Queue——零延迟的消息队列,意味着它除了可以收发数据外,还可以用作消息中间件,解耦多个应用服务之间的强依赖关系,搭建高效、有弹性的分布式系统,从而超越原生的 Socket。

ubuntu 下安装

apt-get install libzmq3-dev

ZMQ 是用 C++ 开发的,但出于兼容的考虑,对外提供的是纯 C 接口。它也有很多 C++ 封装,可以使用 cppzmq,只有头文件的封装,简单使用。

ZMQ 把自身定位于更高层次的“异步消息队列”,定义了 5 种不同的工作模式,来适应实际中常见的网络通信场景。

  • 原生模式(RAW),没有消息队列功能,相当于底层 Socket 的简单封装;

  • 结对模式(PAIR),两个端点一对一通信;

  • 请求响应模式(REQ-REP),也是两个端点一对一通信,但请求必须有响应;

  • 发布订阅模式(PUB-SUB),一对多通信,一个端点发布消息,多个端点接收处理;

  • 管道模式(PUSH-PULL),或者叫流水线,可以一对多,也可以多对一;

前四种模式类似 HTTP 协议、Client-Server 架构,管道模式非常适合进程间无阻塞传送海量数据。管道模式介绍示意:

在 ZMQ 里有两个基本的类。

  • 第一个是 context_t,它是 ZMQ 的运行环境。使用 ZMQ 的任何功能前,必须要先创建它。

  • 第二个是 socket_t,表示 ZMQ 的套接字,需要指定刚才说的那 5 种工作模式。注意它与原生 Socket 没有任何关系,只是借用了名字来方便理解。

ZMQ 套接字支持 TCP/IP,还支持进程内和进程间通信,这在本机交换数据时会更高效:

  • TCP 通信地址的形式是 tcp://…,指定 IP 地址和端口号;

  • 进程内通信地址的形式是 inproc://…,指定一个本地可访问的路径;

  • 进程间通信地址的形式是 ipc://…,也是一个本地可访问的路径。

用 bind()/connect() 这两个函数把 ZMQ 套接字连接起来之后,就可以用 send()/recv() 来收发数据了:

Code示例
#include "zmq.hpp"

#include <iostream>
#include <memory>
#include <string>
#include <thread>

using namespace std;

int main() {
    const auto thread_num = 1;               // 并发线程数z
    zmq::context_t context(thread_num);      // ZMQ环境变量
    auto make_sock = [&](auto mode) {        // 定义一个lambda表达式
        return zmq::socket_t(context, mode); // 创建ZMQ套接字
    };

    const auto addr = "ipc:///tmp/shm/zmq.sock"s; // 通信地址

    auto receiver = [=]() {              //  lambda表达式接收数据
        auto sock = make_sock(ZMQ_PULL); // 创建ZMQ套接字,拉数据

        sock.bind(addr);                  // 绑定套接字
        assert(sock.handle() != nullptr); // 断言套接字有效

        auto buf = make_unique<char[]>(1024);     // 创建一个1024字节的缓冲区
        zmq::mutable_buffer msg(buf.get(), 1024); // 创建ZMQ消息
        sock.recv(msg, zmq::recv_flags::none);    // 接收数据
        cout << (char*)msg.data() << endl;
    };

    auto sender = [=]() {                // lambda表达式发送数据
        auto sock = make_sock(ZMQ_PUSH); // 创建ZMQ套接字,推数据

        sock.connect(addr); // 连接到对端
        assert(sock.handle() != nullptr);

        string s = "hello zmq";
        sock.send(zmq::const_buffer{s.data(), s.length()}); // 发送消息
    };

    thread t1(sender);
    thread t2(receiver);

    t1.join();
    t2.join();
}

使用 ZMQ 完全不需要考虑底层的 TCP/IP 通信细节,它会保证消息异步、安全、完整地到达服务器,让你关注网络通信之上的业务逻辑。


其他说明

  • ZMQ 环境的线程数。它的默认值是 1,适当增大一些就可以提高 ZMQ 的并发处理能力。

  • 收发消息时的本地缓存数量,ZMQ 的术语叫 High Water Mark。如果收发的数据过多,数量超过 HWM,ZMQ 要么阻塞,要么丢弃消息。

HWM 需要调用套接字的成员函数 setsockopt() 来设置,注意收发使用的是两个不同的标志:

sock.setsockopt(ZMQ_RCVHWM, 1000); // 接收消息最多缓存1000条
sock.setsockopt(ZMQ_SNDHWM, 100); // 发送消息最多缓存100条

即使设置 100 万以上的值,ZMQ 会把一切都处理得很好。


sockpp

简单易用的C++ Socket 库,这是一个低层级的 socket 包装库,提供 socket, connector, acceptor 等相关接口。

下载并安装

https://github.com/fpagliughi/sockpp

git clone https://github.com/fpagliughi/sockpp
cd sockpp
mkdir build && cd build
cmake .. # 默认使用 -DSOCKPP_BUILD_SHARED=ON
cmake --build . --target install

MacOS 下如果使用 gcc/g++,则使用 gcc/g++ toolchain 编译安装

使用方式非常简单,例如 TCP 编程:

TCP Server
#include <iostream>
#include <thread>
#include "sockpp/tcp_acceptor.h"
#include "sockpp/version.h"

using namespace std;
// The thread function. 
void run_echo(sockpp::tcp_socket sock)
{
	ssize_t n;
	char buf[512];
	
	while ((n = sock.read(buf, sizeof(buf))) > 0)
		sock.write_n(buf, n);
	cout << "Connection closed from " << sock.peer_address() << endl;
}

int main(int argc, char* argv[])
{
	cout << "Sample TCP echo server for 'sockpp' "
		<< sockpp::SOCKPP_VERSION << '\n' << endl;

	in_port_t port = (argc > 1) ? atoi(argv[1]) : 12345;

	sockpp::socket_initializer sockInit;
	sockpp::tcp_acceptor acc(port);

	if (!acc) {
		cerr << "Error creating the acceptor: " << acc.last_error_str() << endl;
		return 1;
	}
	cout << "Awaiting connections on port " << port << "..." << endl;

	while (true) {
		sockpp::inet_address peer;

		// Accept a new client connection
		sockpp::tcp_socket sock = acc.accept(&peer);
		cout << "Received a connection request from " << peer << endl;

		if (!sock) {
			cerr << "Error accepting incoming connection: " 
				<< acc.last_error_str() << endl;
		}
		else {
			// Create a thread and transfer the new stream to it.
			thread thr(run_echo, std::move(sock));
			thr.detach();
		}
	}
	return 0;
}
TCP Client
#include <iostream>
#include <string>
#include "sockpp/tcp_connector.h"
#include "sockpp/version.h"

using namespace std;
using namespace std::chrono;

int main(int argc, char* argv[])
{
	cout << "Sample TCP echo client for 'sockpp' "
		<< sockpp::SOCKPP_VERSION << '\n' << endl;

	string host = (argc > 1) ? argv[1] : "localhost";
	in_port_t port = (argc > 2) ? atoi(argv[2]) : 12345;

	sockpp::socket_initializer sockInit;
	sockpp::tcp_connector conn({host, port});
	if (!conn) {
		cerr << "Error connecting to server at "
			<< sockpp::inet_address(host, port)
			<< "\n\t" << conn.last_error_str() << endl;
		return 1;
	}

	cout << "Created a connection from " << conn.address() << endl;

	// Set a timeout for the responses
	if (!conn.read_timeout(seconds(5))) {
	    cerr << "Error setting timeout on TCP stream: "
	            << conn.last_error_str() << endl;
	}

	string s, sret;
	while (getline(cin, s) && !s.empty()) {
		if (conn.write(s) != ssize_t(s.length())) {
			cerr << "Error writing to the TCP stream: "
				<< conn.last_error_str() << endl;
			break;
		}

		sret.resize(s.length());
		ssize_t n = conn.read_n(&sret[0], s.length());

		if (n != ssize_t(s.length())) {
			cerr << "Error reading from TCP stream: "
				<< conn.last_error_str() << endl;
			break;
		}
		cout << sret << endl;
	}
	return (!conn) ? 1 : 0;
}

小结

  1. libcurl 是一个功能完善、稳定可靠的应用层通信库,最常用的就是 HTTP 协议;

  2. cpr 是对 libcurl 的 C++ 封装,接口简单易用;libcurl 和 cpr 都只能作为客户端来使用,不能编写服务器端应用;

  3. ZMQ 是一个高级的网络通信库,支持多种通信模式,可以把消息队列功能直接嵌入应用程序,搭建出高效、灵活、免管理的分布式系统。

  4. sockpp 是一个简单易用的底层sock封装库。

最后, C++20,原本预计会加入期待已久的 networking 库,但现在已经被推迟到了下一个版本(C++23)

networking 库基于已有多年实践的 boost.asio,采用前摄器模式(Proactor)统一封装了操作系统的各种异步机制(epollkqueueIOCP),而且支持协程。

本文链接: C++网络库

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

发布日期: 2022-10-23

最新构建: 2024-12-26

本文已被阅读 0 次,该数据仅供参考

欢迎任何与文章内容相关并保持尊重的评论😊 !

共 43 篇文章 | Powered by Gridea | RSS
©2020-2024 Nuo. All rights reserved.