シンプルでモダンな C++ ソケット ライブラリ。
これは、他の言語でよく知られている概念であるsocket
、 acceptor,
およびconnector
クラスを使用する、Berkeley ソケット ライブラリのかなり低レベルの C++ ラッパーです。
基本socket
クラスはシステム ソケット ハンドルをラップし、その有効期間を維持します。 C++ オブジェクトがスコープ外になると、基礎となるソケット ハンドルが閉じられます。ソケット オブジェクトは通常、移動可能ですが、コピーはできません。 std::move()
使用して、ソケットをあるスコープ (またはスレッド) から別のスコープ (またはスレッド) に転送できます。
このライブラリは現在、Linux、Mac、および Windows 上の IPv4 および IPv6 をサポートしています。他の *nix および POSIX システムは、ほとんどまたはまったく変更を加えなくても動作するはずです。
Unix ドメイン ソケットは、OS が実装されている *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 操作のアイデア (完全にはマージされませんでした) は、 result<T>
クラスを備えた 2.0 API に導入されています。これはstd::error_code
で表されるエラーを含む "success" 戻り値の型に対して汎用的になります。これにより、エラーの追跡と報告に関するプラットフォームの問題が大幅に軽減されます。
統一された結果型を使用すると、コンストラクターを除くほとんどの関数で例外が必要なくなります。関数がスローする可能性がある場合には、スローする代わりにエラー コード パラメーターを設定できる、同等のnoexcept
関数も提供されます。したがって、アプリケーションが必要に応じてライブラリを例外なく使用できます。
システム エラーにより失敗する可能性のあるすべての関数は結果を返します。これにより、「最後のエラー」の必要性がなくなり、 socket
クラスにキャッシュされた最後のエラー変数が消えます。ソケット クラスはソケット ハンドルのみをラップするため、ハンドルを共有するのと同じ方法でスレッド間で安全に共有できるようになります (通常、1 つのスレッドで読み取り用、別のスレッドで書き込み用)。
OpenSSL または MbedTLS ライブラリ、あるいは (おそらく) どちらかをビルド時に選択して、セキュア ソケットをライブラリの 2.x リリースに組み込む作業も開始されています。数年間休眠状態にあった PR #17 は、OpenSSL と同等の機能を実現するための新しい作業とともに、マージおよび更新されています。 sockpp
構築するときに、安全なライブラリのいずれかを選択できます。
2.0 バージョンは、C++17 および CMake v3.12 以降にも移行します。
このプロジェクトの最新情報を入手するには、次のアドレスでフォローしてください。
マストドン: @[email protected]
Twitter: @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
例は、examples/tcp ディレクトリにあります。
Unix ドメイン ソケットを実装する *nix システム上のローカル接続にも同じことが当てはまります。そのためには、次のクラスを使用します。
unix_address
unix_connector
unix_acceptor
unix_socket (unix_stream_socket)
unix_dgram_socket
サンプルは、examples/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
などの特定のアドレス ファミリに対して定義された派生クラスを使用します。
ソケット オブジェクトは、基礎となる OS ソケット ハンドルへのハンドルと、そのソケットで発生した最後のエラーのキャッシュされた値を保持します。ソケット ハンドルは通常、整数のファイル記述子であり、開いているソケットの場合は値 >=0、開いていないソケットまたは無効なソケットの場合は -1 になります。開かれていないソケットに使用される値は定数INVALID_SOCKET
として定義されますが、オブジェクト自体が初期化されていない場合やエラー状態にある場合はfalseと評価されるため、通常は直接テストする必要はありません。一般的なエラー チェックは次のようになります。
tcp_connector conn({"localhost", 12345});
if (!conn)
cerr << conn.last_error_str() << std::endl;
各ソケット クラスのデフォルトのコンストラクターは何もせず、単純に基になるハンドルをINVALID_SOCKET
に設定します。ソケット オブジェクトは作成されません。 connector
オブジェクトをアクティブに接続するか、 acceptor
オブジェクトを開く呼び出しにより、基礎となる OS ソケットが作成され、要求された操作が実行されます。
通常、アプリケーションはライブラリを使用してほとんどの低レベル操作を実行できます。接続されていないソケットとバインドされていないソケットは、ほとんどのクラスで静的なcreate()
関数を使用して作成し、それらのソケットを手動でバインドしてリッスンできます。
socket::handle()
メソッドは、基礎となる OS ハンドルを公開します。このハンドルは、ライブラリによって公開されていないプラットフォーム API 呼び出しに送信できます。
ソケット オブジェクトはスレッドセーフではありません。複数のスレッドでソケットからの読み取りまたはソケットへの書き込みを行う必要があるアプリケーションでは、アクセスを保護するためにstd::mutex
などの何らかの形式のシリアル化を使用する必要があります。
socket
あるスレッドから別のスレッドに安全に移動できます。これは、1 つのスレッドを使用して受信接続を受け入れ、その後、新しいソケットを処理のために別のスレッドまたはスレッド プールに渡すサーバーの一般的なパターンです。これは次のように行うことができます。
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
コピーできないため、ソケットをこのような関数に移動するしかありません。
特にクライアント アプリケーションでは、1 つのスレッドでソケットから読み取りを行い、別のスレッドでソケットに書き込みを行うのが一般的なパターンです。この場合、基礎となるソケット ハンドルはスレッド セーフであると見なされます (1 つの読み取りスレッドと 1 つの書き込みスレッド)。ただし、このシナリオでも、特にキャッシュされたエラー値が原因で、 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()
メソッドを使用すると、別のスレッド シグナリング メカニズムを必要とせずに、これらのオブジェクトの 1 つから別のオブジェクトにソケットを閉じる意図を伝えることができます。
tcpechomt.cpp の例を参照してください。