转载和来源于:罗剑锋的 C++ 实战笔记
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 个:
使用 curl_easy_init()
创建一个句柄,类型是 CURL*
。但我们完全没有必要关心句柄的类型,直接用 auto 推导就行。
使用 curl_easy_setopt()
设置请求的各种参数,比如请求方法、URL、header/body 数据、超时、回调函数等。这是最关键的操作。
使用 curl_easy_perform()
发送数据,返回的数据会由回调函数处理。
使用 curl_easy_cleanup()
清理句柄相关的资源,结束会话。
#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 是对 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。
#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;
}
libcurl
和 cpr
处理的都是 HTTP 协议,方便但协议自身也有一些限制,比如必须要一来一回,必须点对点直连,在超大数据量通信的时候就不是太合适。并且 libcurl
和 cpr
只能充当 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() 来收发数据了:
#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 会把一切都处理得很好。
简单易用的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 编程:
#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;
}
#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;
}
libcurl 是一个功能完善、稳定可靠的应用层通信库,最常用的就是 HTTP 协议;
cpr 是对 libcurl 的 C++ 封装,接口简单易用;libcurl 和 cpr 都只能作为客户端来使用,不能编写服务器端应用;
ZMQ 是一个高级的网络通信库,支持多种通信模式,可以把消息队列功能直接嵌入应用程序,搭建出高效、灵活、免管理的分布式系统。
sockpp 是一个简单易用的底层sock封装库。
最后, C++20,原本预计会加入期待已久的 networking 库,但现在已经被推迟到了下一个版本(C++23)
networking 库基于已有多年实践的 boost.asio
,采用前摄器模式(Proactor)统一封装了操作系统的各种异步机制(epoll
、kqueue
、IOCP
),而且支持协程。
本文链接: C++网络库
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
发布日期: 2022-10-23
最新构建: 2024-12-26
欢迎任何与文章内容相关并保持尊重的评论😊 !