sockpp是一个开源、简单、现代的C++套接字库,地址为:https://github.com/fpagliughi/sockpp,最新发布版本为0.8.1,license为BSD-3-Clause。目前支持Linux、Windows、Mac上的IPv4、IPv6和Unix域套接字。其它*nix和POSIX系统只需很少的修改或无需修改即可工作。
1.套接字基类包装(wrap)系统套接字句柄,并维持其生命周期。当C++对象超出范围时,它会关闭底层套接字句柄。套接字对象通常是可移动的,但不可复制。可以使用 std::move()将套接字从一个作用域(或线程)传输到另一个作用域。
2.库中的所有代码都位于sockpp C++命名空间内。
3.TCP和其它"流"网络应用程序通常设置为服务器或客户端。接受器用于创建TCP/流服务器。它绑定一个地址并侦听已知端口以接受传入连接。 当连接被接受时,会创建一个新的流式套接字。该新套接字可以直接处理或移动到线程(或线程池)进行处理。
相反,要创建TCP客户端,需要创建连接器对象并将其连接到已知地址(通常是主机和套接字)的服务器。连接后,套接字是一种流式套接字,可以直接用于读写。
对于IPv4,sockpp::tcp_acceptor和sockpp::tcp_connector类分别用于创建服务器和客户端。它们使用sockpp::inet_address类来指定由32位主机地址和16位端口号组成的端点地址。
sockpp::tcp_acceptor通常位于一个循环中接受新连接,并将它们传递给另一个进程、线程或线程池以与客户端交互。
TCP客户端稍微简单一些,创建一个sockpp::tcp_connector对象并连接,然后可以直接读写数据。
4.每个套接字类的默认构造函数不执行任何操作,只是将底层句柄设置为INVALID_SOCKET。它们不创建套接字对象。
5.套接字对象不是线程安全的。想要有多个线程从套接字读取或写入套接字的应用程序应该使用某种形式的序列化,例如std::mutex来保护访问。套接字可以安全地从一个线程移动到(moved)另一个线程。这是服务器的一种常见模式,它使用一个线程接受传入连接,然后将新套接字传递给另一个线程或线程池进行处理。
由于套接字无法复制,唯一的选择是将套接字移动到这样的函数。这是一种常见的模式,尤其是在客户端应用程序中,让一个线程从套接字读取数据,另一个线程向套接字写入数据。在这种情况下,底层套接字句柄可以被认为是线程安全的(一个读线程和一个写线程)。但即使在这种情况下,sockpp::socket对象仍然不是线程安全的,特别是由于缓存的错误值(cached error value)。写入线程可能会看到读取线程上发生的错误,反之亦然。
这种情况的解决方案是使用socket::clone()方法来复制套接字。这将使用系统的dup()函数或类似的函数创建另一个带有套接字句柄的重复副本的套接字。这样做的另一个好处是套接字的每个副本都可以保持独立的生命周期。在两个对象超出范围之前,底层套接字不会关闭。
在Windows和Linux上编译的sockpp的shell脚本如下:
#! /bin/bash
if [ $# != 2 ]; then
echo "Error: requires two parameters: 1: windows or linux; 2: release or debug"
echo "For example: $0 windows debug"
exit -1
fi
if [ $1 != "windows" ] && [ $1 != "linux" ]; then
echo "Error: the first parameter can only be windows or linux"
exit -1
fi
if [ $2 != "debug" ] && [ $2 != "release" ]; then
echo "Error: the second parameter can only be debug or release"
exit -1
fi
if [ $1 == "windows" ] && [ $2 == "debug" ]; then
cmake \
-G"Visual Studio 17 2022" -A x64 \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CONFIGURATION_TYPES=Debug \
-DSOCKPP_BUILD_SHARED=OFF \
-DSOCKPP_BUILD_STATIC=ON \
-DSOCKPP_BUILD_EXAMPLES=ON \
-DCMAKE_INSTALL_PREFIX=install/debug \
-Bbuild \
.
cmake --build build/ --target install --config debug
fi
if [ $1 == "windows" ] && [ $2 == "release" ]; then
cmake \
-G"Visual Studio 17 2022" -A x64 \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CONFIGURATION_TYPES=Release \
-DSOCKPP_BUILD_SHARED=OFF \
-DSOCKPP_BUILD_STATIC=ON \
-DSOCKPP_BUILD_EXAMPLES=ON \
-DCMAKE_INSTALL_PREFIX=install/release \
-Bbuild \
.
cmake --build build/ --target install --config release
fi
if [ $1 == "linux" ] && [ $2 == "debug" ]; then
cmake \
-DCMAKE_BUILD_TYPE=Debug \
-DSOCKPP_BUILD_SHARED=OFF \
-DSOCKPP_BUILD_STATIC=ON \
-DSOCKPP_BUILD_EXAMPLES=ON \
-DCMAKE_INSTALL_PREFIX=install/debug \
-Bbuild \
.
cmake --build build/ --target install --config debug
fi
if [ $1 == "linux" ] && [ $2 == "release" ]; then
cmake \
-DCMAKE_BUILD_TYPE=Release \
-DSOCKPP_BUILD_SHARED=OFF \
-DSOCKPP_BUILD_STATIC=ON \
-DSOCKPP_BUILD_EXAMPLES=ON \
-DCMAKE_INSTALL_PREFIX=install/release \
-Bbuild \
.
cmake --build build/ --target install --config release
fi
rc=$?
if [[ ${rc} != 0 ]]; then
echo "Error: please check: ${rc}"
exit ${rc}
fi
以下为IPv4的测试代码:
1.客户端测试代码如下:
int test_sockpp_client()
{
sockpp::initialize();
sockpp::tcp_connector conn({host, port});
if (!conn) {
std::cerr << "Error: connecting to server at: "
<< sockpp::inet_address(host, port)
<< ", message: " << conn.last_error_str() << std::endl;
return -1;
}
std::cout << "created a connection from: " << conn.address() << std::endl;
std::cout << "created a connection to " << conn.peer_address() << std::endl;
// set a timeout for the responses
if (!conn.read_timeout(std::chrono::seconds(5))) {
std::cerr << "Error: setting timeout on TCP stream: " << conn.last_error_str() << std::endl;
}
const std::vector<std::string> addr{"csdn", "github", "gitlab"};
std::unique_ptr<unsigned char[]> buf(new unsigned char[len]);
int index{0};
std::atomic<bool> quit{ false };
std::thread th([&quit] {
std::this_thread::sleep_for(std::chrono::seconds(20));
quit = true;
});
while (true) {
if (quit) break;
auto ret = conn.write(addr[index]);
if (ret != addr[index].size()) {
std::cerr << "Error: writing to the TCP stream: " << conn.last_error_str() << std::endl;
break;
}
memset(buf.get(), 0, len);
ret = conn.read(buf.get(), len);
if (ret == -1) {
std::cerr << "Error: reading from TCP stream: " << conn.last_error_str() << std::endl;
break;
}
std::cout << addr[index] << ": " << buf.get() << std::endl;
if (++index == addr.size()) index = 0;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
th.join();
return 0;
}
2.服务器端测试代码如下:
int test_sockpp_server()
{
sockpp::initialize();
sockpp::tcp_acceptor acc(port);
if (!acc) {
std::cerr << "Error: creating the acceptor: " << acc.last_error_str() << std::endl;
return -1;
}
while (true) {
sockpp::inet_address peer;
// accept a new client connection
sockpp::tcp_socket sock = acc.accept(&peer);
std::cout << "received a connection request from: " << peer << std::endl;
if (!sock) {
std::cerr << "Error: accepting incoming connection: " << acc.last_error_str() << std::endl;
}
else {
// create a thread and transfer the new stream to it
std::thread th2(run_echo, std::move(sock));
th2.detach();
}
}
return 0;
}
3.辅助code如下所示:
namespace {
constexpr char* host{"127.0.0.1"};
constexpr in_port_t port{ 8888 };
constexpr int len {64};
void run_echo(sockpp::tcp_socket sock)
{
std::cout << "thread id: " << std::this_thread::get_id() << std::endl;
std::map<std::string, std::string> addr;
addr["csdn"] = "https://blog.csdn.net/fengbingchun";
addr["github"] = "https://github.com/fengbingchun";
std::unique_ptr<unsigned char[]> buf(new unsigned char[len]);
while (true) {
memset(buf.get(), 0, len);
auto ret = sock.read(buf.get(), len);
if (ret == -1) {
std::cerr << "Error: reading from TCP stream: " << sock.last_error_str() << std::endl;
break;
}
auto it = addr.find(std::string((char*)buf.get()));
if (it != addr.end()) {
sock.write(it->second);
}
else
sock.write("unkonwn");
}
}
} // namespace
Windows上执行结果如下所示:模拟1个服务器端,3个客户端
Linux上执行结果如下图所示:模拟1个服务器端,3个客户端
GitHub:https://github.com/fengbingchun/OpenSSL_Test