简单、现代的 C++ 套接字库。
这是 Berkeley 套接字库的相当低级的 C++ 包装器,使用其他语言中熟悉的概念socket
、 acceptor,
和connector
类。
socket
基类包装系统套接字句柄,并维持其生命周期。当 C++ 对象超出范围时,它会关闭底层套接字句柄。套接字对象通常是可移动的,但不可复制。可以使用std::move()
将套接字从一个作用域(或线程)传输到另一个作用域。
该库当前支持:Linux、Mac 和 Windows 上的 IPv4 和 IPv6。其他 *nix 和 POSIX 系统只需很少的修改或无需修改即可工作。
Unix 域套接字可在具有操作系统实现的 *nix 系统上使用。
最近添加了对使用 OpenSSL 或 MbedTLS 库的安全套接字的支持,并具有基本覆盖范围。在不久的将来,这一举措还将继续扩大。
还有一些使用 SocketCAN 包在 Linux 上进行 CAN 总线编程的实验性支持。这为 CAN 总线适配器提供了一个网络接口,但具有 CAN 消息协议规定的限制。
库中的所有代码都位于sockpp
C++ 命名空间内。
“master”分支正在开始转向 v2.0 API,目前特别不稳定。建议您下载最新版本以供一般使用。
1.0版本发布了!
随着当前开发分支中开始积累重大更改,我们决定将过去几年相当稳定的 API 发布为 1.0。这是来自最新的 v0.8.x 行。这将使事情变得不那么混乱,并允许我们维护 v1.x 分支。
2.0版本的开发正在进行中。
PR #17 中引入的“无状态”I/O 操作的想法(从未完全合并)将在 2.0 API 中通过result<T>
类出现。它将是通用的“成功”返回类型,错误由std::error_code
表示。这应该有助于显着减少跟踪和报告错误的平台问题。
使用统一的结果类型可以消除大多数函数中对异常的需要,构造函数除外。在函数可能抛出异常的情况下,还将提供类似的noexcept
函数,该函数可以设置错误代码参数而不是抛出异常。因此,如果应用程序需要的话,可以毫无例外地使用该库。
所有可能因系统错误而失败的函数都会返回结果。这将消除对“最后一个错误”的需要,因此socket
类中缓存的最后一个错误变量将消失。然后,套接字类将仅包装套接字句柄,从而使它们可以更安全地跨线程共享,就像共享句柄一样 - 通常一个线程用于读取,另一个线程用于写入。
一些工作还开始使用 OpenSSL 或 MbedTLS 库,或者(可能)使用其中之一的构建时选择,将安全套接字合并到该库的 2.x 版本中。 PR #17 已经休眠了几年,正在合并和更新,同时还有新的工作要做与 OpenSSL 类似的事情。在构建sockpp
时,您将能够选择一个安全库或另一个。
2.0 版本还将升级到 C++17 和 CMake v3.12 或更高版本。
要了解该项目的最新公告,请关注我:
乳齿象: @[email protected]
推特: @fmpagliughi
如果您正在使用此库,请向我发推文或给我发送消息,并让我知道您如何使用它。我总是很好奇它的结局!
安装后,通常可以使用find_package(sockpp)
发现该库。它使用命名空间Sockpp
和库名称sockpp
。
一个简单的CMakeLists.txt文件可能如下所示:
cmake_minimum_required(VERSION 3.12)
project(mysock VERSION 1.0.0)
find_package(sockpp REQUIRED)
add_executable(mysock mysock.cpp)
target_link_libraries(mysock Sockpp::sockpp)
贡献被接受并受到赞赏。新的和不稳定的工作是在develop
分支中完成的,请提交针对该分支的所有拉取请求,而不是master 。
更多信息请参考:CONTRIBUTING.md
CMake 是受支持的构建系统。
使用默认选项构建:
$ cd sockpp
$ cmake -Bbuild .
$ cmake --build build/
安装:
$ cmake --build build/ --target install
该库通过 CMake 有多个构建选项,可以在创建静态库或共享(动态)库或两者之间进行选择。它还允许您构建示例选项,如果安装了 Doxygen,则可以使用它来创建文档。
多变的 | 默认值 | 描述 |
---|---|---|
SOCKPP_BUILD_SHARED | 在 | 是否构建共享库 |
SOCKPP_BUILD_STATIC | 离开 | 是否构建静态库 |
SOCKPP_BUILD_DOCUMENTATION | 离开 | 创建并安装基于 HTML 的 API 文档(需要Doxygen) |
SOCKPP_BUILD_EXAMPLES | 离开 | 构建示例程序 |
SOCKPP_BUILD_TESTS | 离开 | 构建单元测试(需要Catch2 ) |
SOCKPP_WITH_CAN | 离开 | 包括 SocketCAN 支持。 (仅限 Linux) |
使用 CMake 配置命令中的“-D”开关设置这些。例如,要构建文档和示例应用程序:
$ cd sockpp
$ cmake -Bbuild -DSOCKPP_BUILD_DOCUMENTATION=ON -DSOCKPP_BUILD_EXAMPLES=ON .
$ cmake --build build/
要构建具有安全套接字支持的库,需要选择 TLS 库来提供支持。目前可以使用OpenSSL或MbedTLS 。
配置构建时选择以下选项之一:
多变的 | 默认值 | 描述 |
---|---|---|
SOCKPP_WITH_MBEDTLS | 离开 | 使用 MbedTLS 的安全套接字 |
SOCKPP_WITH_OPENSSL | 离开 | 使用 OpenSSL 保护套接字 |
sockpp
库当前支持 MbedTLS v3.3。构建该库时,应在配置文件include/mbedtls/mbedtls_config.h中定义以下配置选项
#define MBEDTLS_X509_TRUSTED_CERTIFICATE_CALLBACK
支持线程:
#define MBEDTLS_THREADING_PTHREAD
#define MBEDTLS_THREADING_C
并设置 CMake 构建选项:
LINK_WITH_PTHREAD:BOOL=ON
请注意,配置文件中的选项应该已经存在于文件中,但默认情况下已被注释掉。只需取消注释,保存并构建即可。
sockpp
OpenSSL 包装器当前正在使用 OpenSSL v3.0 进行构建和测试
TCP 和其他“流”网络应用程序通常设置为服务器或客户端。接受器用于创建 TCP/流服务器。它绑定一个地址并侦听已知端口以接受传入连接。当连接被接受时,会创建一个新的流式套接字。该新套接字可以直接处理或移动到线程(或线程池)进行处理。
相反,要创建 TCP 客户端,需要创建连接器对象并将其连接到已知地址(通常是主机和套接字)的服务器。连接后,套接字是一种流式套接字,可以直接用于读写。
对于 IPv4, tcp_acceptor
和tcp_connector
类分别用于创建服务器和客户端。它们使用inet_address
类来指定由 32 位主机地址和 16 位端口号组成的端点地址。
tcp_acceptor
tcp_acceptor
用于设置服务器并侦听传入连接。
int16_t port = 12345;
sockpp::tcp_acceptor acc(port);
if (!acc)
report_error(acc.last_error_str());
// Accept a new client connection
sockpp::tcp_socket sock = acc.accept();
接受器通常位于一个循环中接受新连接,并将它们传递给另一个进程、线程或线程池以与客户端交互。在标准 C++ 中,这可能如下所示:
while (true) {
// Accept a new client connection
sockpp::tcp_socket sock = acc.accept();
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();
}
}
每个连接一个线程设计的危险已有详细记录,但如果有可用的技术,也可以使用相同的技术将套接字传递到线程池中。
请参阅 tcpechosvr.cpp 示例。
tcp_connector
TCP客户端稍微简单一些,创建并连接一个tcp_connector
对象,然后可以直接读写数据。
sockpp::tcp_connector conn;
int16_t port = 12345;
if (!conn.connect(sockpp::inet_address("localhost", port)))
report_error(conn.last_error_str());
conn.write_n("Hello", 5);
char buf[16];
ssize_t n = conn.read(buf, sizeof(buf));
请参阅 tcpecho.cpp 示例。
udp_socket
UDP 套接字可用于无连接通信:
sockpp::udp_socket sock;
sockpp::inet_address addr("localhost", 12345);
std::string msg("Hello there!");
sock.send_to(msg, addr);
sockpp::inet_address srcAddr;
char buf[16];
ssize_t n = sock.recv(buf, sizeof(buf), &srcAddr);
请参阅 udpecho.cpp 和 udpechosvr.cpp 示例。
相同类型的连接器和接受器可用于 IPv6 上的 TCP 连接,使用以下类:
inet6_address
tcp6_connector
tcp6_acceptor
tcp6_socket
udp6_socket
示例位于 example/tcp 目录中。
对于实现 Unix 域套接字的 *nix 系统上的本地连接也是如此。为此,请使用以下类:
unix_address
unix_connector
unix_acceptor
unix_socket (unix_stream_socket)
unix_dgram_socket
示例位于 example/unix 目录中。
控制器局域网(CAN 总线)是一种相对简单的协议,通常由微控制器用来在汽车或工业机器内部进行通信。 Linux 具有SocketCAN包,允许进程使用用户空间中的套接字共享对物理 CAN 总线接口的访问。请参阅:Linux SocketCAN
在最低级别,CAN 设备将称为“帧”的单独数据包写入总线上的特定数字地址。
例如,带有温度传感器的设备可能会定期读取温度并将其作为原始 32 位整数写入总线,例如:
can_address addr("CAN0");
can_socket sock(addr);
// The agreed ID to broadcast temperature on the bus
canid_t canID = 0x40;
while (true) {
this_thread::sleep_for(1s);
// Write the time to the CAN bus as a 32-bit int
int32_t t = read_temperature();
can_frame frame { canID, &t, sizeof(t) };
sock.send(frame);
}
获取帧的接收器可能如下所示:
can_address addr("CAN0");
can_socket sock(addr);
can_frame frame;
sock.recv(&frame);
套接字类层次结构建立在基本socket
类的基础上。大多数简单的应用程序可能不会直接使用socket
,而是使用为特定地址族定义的派生类,例如tcp_connector
和tcp_acceptor
。
套接字对象保留底层操作系统套接字句柄的句柄以及该套接字发生的最后一个错误的缓存值。套接字句柄通常是一个整数文件描述符,值 >=0 表示打开的套接字,-1 表示未打开或无效的套接字。用于未打开套接字的值被定义为常量INVALID_SOCKET
,尽管它通常不需要直接测试,因为如果对象未初始化或处于错误状态,则该对象本身将评估为false 。典型的错误检查如下:
tcp_connector conn({"localhost", 12345});
if (!conn)
cerr << conn.last_error_str() << std::endl;
每个套接字类的默认构造函数不执行任何操作,只是将底层句柄设置为INVALID_SOCKET
。他们不创建套接字对象。主动连接connector
对象或打开acceptor
对象的调用将创建底层操作系统套接字,然后执行请求的操作。
应用程序通常可以使用该库执行大多数低级操作。在大多数类中,可以使用静态create()
函数创建未连接和未绑定的套接字,然后手动绑定和侦听这些套接字。
socket::handle()
方法公开了底层操作系统句柄,然后可以将其发送到库未公开的任何平台 API 调用。
套接字对象不是线程安全的。想要有多个线程从套接字读取或写入套接字的应用程序应该使用某种形式的序列化,例如std::mutex
来保护访问。
socket
可以安全地从一个线程移动到另一个线程。这是服务器的一种常见模式,它使用一个线程接受传入连接,然后将新套接字传递给另一个线程或线程池进行处理。这可以这样做:
sockpp::tcp6_socket sock = acc.accept(&peer);
// Create a thread and transfer the new socket to it.
std::thread thr(handle_connection, std::move(sock));
在这种情况下, handle_connection将是一个按值获取套接字的函数,例如:
void handle_connection(sockpp::tcp6_socket sock) { ... }
由于socket
无法复制,唯一的选择是将套接字移动到这样的函数。
这是一种常见的模式,尤其是在客户端应用程序中,让一个线程从套接字读取数据,另一个线程向套接字写入数据。在这种情况下,底层套接字句柄可以被认为是线程安全的(一个读线程和一个写线程)。但即使在这种情况下, sockpp::socket
对象仍然不是线程安全的,特别是由于缓存的错误值。写入线程可能会看到读取线程上发生的错误,反之亦然。
这种情况的解决方案是使用socket::clone()
方法来复制套接字。这将使用系统的dup()
函数或类似的函数创建另一个带有套接字句柄的重复副本的套接字。这样做的另一个好处是套接字的每个副本都可以保持独立的生命周期。在两个对象超出范围之前,底层套接字不会关闭。
sockpp::tcp_connector conn({host, port});
auto rdSock = conn.clone();
std::thread rdThr(read_thread_func, std::move(rdSock));
socket::shutdown()
方法可用于将关闭套接字的意图从这些对象之一传达给另一个对象,而不需要其他线程信号机制。
请参阅 tcpechomt.cpp 示例。