簡單、現代的 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 範例。