Pustaka soket C++ yang sederhana dan modern.
Ini adalah pembungkus C++ tingkat rendah di sekitar perpustakaan soket Berkeley menggunakan kelas socket
, acceptor,
dan connector
yang merupakan konsep familiar dari bahasa lain.
Kelas socket
dasar membungkus pegangan soket sistem, dan mempertahankan masa pakainya. Ketika objek C++ keluar dari cakupan, ia menutup pegangan soket yang mendasarinya. Objek soket umumnya dapat dipindahkan tetapi tidak dapat disalin . Soket dapat ditransfer dari satu cakupan (atau thread) ke cakupan lainnya menggunakan std::move()
.
Perpustakaan saat ini mendukung: IPv4 dan IPv6 di Linux, Mac, dan Windows. Sistem *nix dan POSIX lainnya dapat bekerja dengan sedikit atau tanpa modifikasi.
Soket Unix-Domain tersedia pada sistem *nix yang memiliki implementasi OS untuknya.
Dukungan untuk soket aman menggunakan perpustakaan OpenSSL atau MbedTLS baru-baru ini ditambahkan dengan cakupan dasar. Hal ini akan terus diperluas dalam waktu dekat.
Ada juga beberapa dukungan eksperimental untuk pemrograman bus CAN di Linux menggunakan paket SocketCAN. Hal ini memberikan adaptor bus CAN antarmuka jaringan, dengan batasan yang ditentukan oleh protokol pesan CAN.
Semua kode di perpustakaan berada dalam namespace sockpp
C++.
Cabang 'master' mulai beralih ke API v2.0, dan saat ini sangat tidak stabil. Anda disarankan untuk mengunduh rilis terbaru untuk penggunaan umum.
Versi 1.0 dirilis!
Karena perubahan-perubahan yang dapat mengganggu mulai terakumulasi di cabang pengembangan saat ini, keputusan dibuat untuk merilis API yang telah cukup stabil selama beberapa tahun terakhir sebagai 1.0. Ini dari baris v0.8.x terbaru. Itu akan membuat segala sesuatunya tidak terlalu membingungkan dan memungkinkan kami mempertahankan cabang v1.x.
Pengembangan versi 2.0 sedang berlangsung.
Gagasan untuk memperkenalkan operasi I/O "tanpa kewarganegaraan" di PR #17, (yang tidak pernah digabungkan sepenuhnya) muncul di API 2.0 dengan kelas result<T>
. Ini akan bersifat umum pada tipe pengembalian "sukses" dengan kesalahan diwakili oleh std::error_code
. Hal ini akan membantu mengurangi masalah platform secara signifikan dalam hal pelacakan dan pelaporan kesalahan.
Menggunakan tipe hasil yang seragam menghilangkan kebutuhan akan pengecualian di sebagian besar fungsi, kecuali mungkin konstruktor. Dalam kasus di mana fungsi tersebut mungkin dilempar, fungsi noexcept
yang sebanding juga akan disediakan yang dapat mengatur parameter kode kesalahan alih-alih melempar. Jadi perpustakaan dapat digunakan tanpa pengecualian jika diinginkan oleh aplikasi.
Semua fungsi yang mungkin gagal karena kesalahan sistem akan mengembalikan hasilnya. Itu akan menghilangkan kebutuhan akan "kesalahan terakhir", dan dengan demikian variabel kesalahan terakhir yang di-cache di kelas socket
akan hilang. Kelas soket kemudian hanya akan membungkus pegangan soket, membuatnya lebih aman untuk dibagikan ke seluruh thread dengan cara yang sama seperti pegangan dapat dibagikan - biasanya dengan satu thread untuk membaca dan satu lagi untuk menulis.
Beberapa pekerjaan juga telah mulai memasukkan Secure Sockets ke dalam rilis 2.x perpustakaan menggunakan perpustakaan OpenSSL atau MbedTLS, atau (kemungkinan besar), pilihan waktu pembangunan untuk salah satu perpustakaan tersebut. PR #17, yang tidak aktif selama beberapa tahun, sedang digabungkan dan diperbarui, bersamaan dengan pekerjaan baru untuk melakukan sesuatu yang sebanding dengan OpenSSL. Anda akan dapat memilih salah satu perpustakaan aman atau lainnya saat membangun sockpp
.
Versi 2.0 juga akan naik ke C++17 dan CMake v3.12 atau lebih baru.
Untuk mengikuti pengumuman terbaru untuk proyek ini, ikuti saya di:
Mastodon: @[email protected]
Twitter: @fmpagliughi
Jika Anda menggunakan perpustakaan ini, kirim tweet ke saya atau kirimkan pesan kepada saya, dan beri tahu saya cara Anda menggunakannya. Saya selalu penasaran untuk melihat di mana akhirnya!
Perpustakaan, ketika diinstal biasanya dapat ditemukan dengan find_package(sockpp)
. Ia menggunakan namespace Sockpp
dan nama perpustakaan sockpp
.
File CMakeLists.txt sederhana mungkin terlihat seperti ini:
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)
Kontribusi diterima dan dihargai. Pekerjaan baru dan tidak stabil sedang dilakukan di cabang develop
. Harap kirimkan semua permintaan penarikan terhadap cabang tersebut, bukan master .
Untuk informasi lebih lanjut, lihat: KONTRIBUSI.md
CMake adalah sistem build yang didukung.
Untuk membangun dengan opsi default:
$ cd sockpp
$ cmake -Bbuild .
$ cmake --build build/
Untuk menginstal:
$ cmake --build build/ --target install
Pustaka memiliki beberapa opsi pembangunan melalui CMake untuk memilih antara membuat pustaka statis atau bersama (dinamis) - atau keduanya. Ini juga memungkinkan Anda membuat opsi contoh, dan jika Doxygen diinstal, ini dapat digunakan untuk membuat dokumentasi.
Variabel | Nilai Bawaan | Keterangan |
---|---|---|
SOCKPP_BUILD_SHARE | PADA | Apakah akan membangun perpustakaan bersama |
SOCKPP_BUILD_STATIC | MATI | Apakah akan membangun perpustakaan statis |
SOCKPP_BUILD_DOCUMENTATION | MATI | Membuat dan menginstal dokumentasi API berbasis HTML (memerlukan Doxygen) |
SOCKPP_BUILD_EXAMPLES | MATI | Membangun contoh program |
SOCKPP_BUILD_TESTS | MATI | Bangun pengujian unit (membutuhkan Catch2 ) |
SOCKPP_WITH_CAN | MATI | Sertakan dukungan SocketCAN. (khusus Linux) |
Atur ini menggunakan tombol '-D' di perintah konfigurasi CMake. Misalnya, untuk membuat dokumentasi dan contoh aplikasi:
$ cd sockpp
$ cmake -Bbuild -DSOCKPP_BUILD_DOCUMENTATION=ON -DSOCKPP_BUILD_EXAMPLES=ON .
$ cmake --build build/
Untuk membangun perpustakaan dengan dukungan soket aman, perpustakaan TLS perlu dipilih untuk memberikan dukungan. Saat ini OpenSSL atau MbedTLS sudah bisa digunakan.
Pilih salah satu dari yang berikut saat mengonfigurasi build:
Variabel | Nilai Bawaan | Keterangan |
---|---|---|
SOCKPP_WITH_MBEDTLS | MATI | Amankan Soket dengan MbedTLS |
SOCKPP_WITH_OPENSSL | MATI | Amankan Soket dengan OpenSSL |
Pustaka sockpp
saat ini mendukung MbedTLS v3.3. Saat membangun perpustakaan itu, opsi konfigurasi berikut harus ditentukan dalam file konfigurasi, include/mbedtls/mbedtls_config.h
#define MBEDTLS_X509_TRUSTED_CERTIFICATE_CALLBACK
Untuk mendukung penguliran:
#define MBEDTLS_THREADING_PTHREAD
#define MBEDTLS_THREADING_C
dan atur opsi build CMake:
LINK_WITH_PTHREAD:BOOL=ON
Perhatikan bahwa opsi dalam file konfigurasi seharusnya sudah ada di file tetapi dikomentari secara default. Cukup batalkan komentar, simpan, dan buat.
Pembungkus sockpp
OpenSSL saat ini sedang dibuat dan diuji dengan OpenSSL v3.0
TCP dan aplikasi jaringan "streaming" lainnya biasanya diatur sebagai server atau klien. Akseptor digunakan untuk membuat server TCP/streaming. Itu mengikat alamat dan mendengarkan port yang dikenal untuk menerima koneksi masuk. Ketika koneksi diterima, soket streaming baru dibuat. Soket baru tersebut dapat ditangani secara langsung atau dipindahkan ke thread (atau kumpulan thread) untuk diproses.
Sebaliknya, untuk membuat klien TCP, objek konektor dibuat dan dihubungkan ke server di alamat yang diketahui (biasanya host dan soket). Ketika terhubung, soketnya adalah soket streaming yang dapat digunakan untuk membaca dan menulis secara langsung.
Untuk IPv4, kelas tcp_acceptor
dan tcp_connector
masing-masing digunakan untuk membuat server dan klien. Ini menggunakan kelas inet_address
untuk menentukan alamat titik akhir yang terdiri dari alamat host 32-bit dan nomor port 16-bit.
tcp_acceptor
tcp_acceptor
digunakan untuk menyiapkan server dan mendengarkan koneksi masuk.
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();
Akseptor biasanya duduk dalam satu lingkaran menerima koneksi baru, dan meneruskannya ke proses, thread, atau kumpulan thread lain untuk berinteraksi dengan klien. Dalam standar C++, ini akan terlihat seperti:
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();
}
}
Bahaya desain ulir per sambungan telah didokumentasikan dengan baik, namun teknik yang sama dapat digunakan untuk meneruskan soket ke kumpulan ulir, jika tersedia.
Lihat contoh tcpechosvr.cpp.
tcp_connector
Klien TCP agak lebih sederhana karena objek tcp_connector
dibuat dan dihubungkan, kemudian dapat digunakan untuk membaca dan menulis data secara langsung.
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));
Lihat contoh tcpecho.cpp.
udp_socket
Soket UDP dapat digunakan untuk komunikasi tanpa koneksi:
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);
Lihat contoh udpecho.cpp dan udpechosvr.cpp.
Gaya konektor dan akseptor yang sama dapat digunakan untuk koneksi TCP melalui IPv6 menggunakan kelas:
inet6_address
tcp6_connector
tcp6_acceptor
tcp6_socket
udp6_socket
Contohnya ada di direktori example/tcp.
Hal yang sama berlaku untuk koneksi lokal pada sistem *nix yang mengimplementasikan Unix Domain Sockets. Untuk itu gunakan kelas:
unix_address
unix_connector
unix_acceptor
unix_socket (unix_stream_socket)
unix_dgram_socket
Contohnya ada di direktori example/unix.
Controller Area Network (CAN bus) adalah protokol yang relatif sederhana yang biasanya digunakan oleh mikrokontroler untuk berkomunikasi di dalam mobil atau mesin industri. Linux memiliki paket SocketCAN yang memungkinkan proses untuk berbagi akses ke antarmuka bus CAN fisik menggunakan soket di ruang pengguna. Lihat: Linux SocketCAN
Pada tingkat terendah, perangkat CAN menulis paket individual, yang disebut "frame" ke alamat numerik tertentu di bus.
Misalnya perangkat dengan sensor suhu mungkin membaca suhu secara berkala dan menuliskannya ke bus sebagai bilangan bulat 32-bit mentah, seperti:
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);
}
Penerima untuk mendapatkan bingkai mungkin terlihat seperti ini:
can_address addr("CAN0");
can_socket sock(addr);
can_frame frame;
sock.recv(&frame);
Hierarki kelas soket dibangun berdasarkan kelas socket
dasar. Sebagian besar aplikasi sederhana mungkin tidak menggunakan socket
secara langsung, melainkan menggunakan kelas turunan yang ditentukan untuk kelompok alamat tertentu seperti tcp_connector
dan tcp_acceptor
.
Objek soket menyimpan pegangan pada pegangan soket OS yang mendasarinya dan nilai cache untuk kesalahan terakhir yang terjadi pada soket tersebut. Pegangan soket biasanya berupa deskriptor file bilangan bulat, dengan nilai >=0 untuk soket terbuka, dan -1 untuk soket yang belum dibuka atau tidak valid. Nilai yang digunakan untuk soket yang belum dibuka didefinisikan sebagai konstanta, INVALID_SOCKET
, meskipun biasanya tidak perlu diuji secara langsung, karena objek itu sendiri akan bernilai false jika tidak diinisialisasi atau dalam keadaan error. Pemeriksaan kesalahan yang umum adalah seperti ini:
tcp_connector conn({"localhost", 12345});
if (!conn)
cerr << conn.last_error_str() << std::endl;
Konstruktor default untuk masing-masing kelas soket tidak melakukan apa pun, dan cukup menyetel pegangan yang mendasarinya ke INVALID_SOCKET
. Mereka tidak membuat objek soket. Panggilan untuk secara aktif menghubungkan objek connector
atau membuka objek acceptor
akan membuat soket OS yang mendasarinya dan kemudian melakukan operasi yang diminta.
Suatu aplikasi umumnya dapat melakukan sebagian besar operasi tingkat rendah dengan perpustakaan. Soket yang tidak terhubung dan tidak terikat dapat dibuat dengan fungsi statis create()
di sebagian besar kelas, lalu mengikat dan mendengarkan soket tersebut secara manual.
Metode socket::handle()
mengekspos pegangan OS yang mendasarinya yang kemudian dapat dikirim ke panggilan API platform apa pun yang tidak diekspos oleh perpustakaan.
Objek soket tidak aman untuk thread. Aplikasi yang ingin memiliki banyak thread yang membaca dari soket atau menulis ke soket harus menggunakan beberapa bentuk serialisasi, seperti std::mutex
untuk melindungi akses.
socket
dapat dipindahkan dari satu thread ke thread lainnya dengan aman. Ini adalah pola umum untuk server yang menggunakan satu thread untuk menerima koneksi masuk dan kemudian meneruskan soket baru ke thread lain atau kumpulan thread untuk ditangani. Ini dapat dilakukan seperti:
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));
Dalam hal ini, handle_connection akan menjadi fungsi yang mengambil nilai soket, seperti:
void handle_connection(sockpp::tcp6_socket sock) { ... }
Karena socket
tidak dapat disalin, satu-satunya pilihan adalah memindahkan soket ke fungsi seperti ini.
Ini adalah pola umum, terutama dalam aplikasi klien, untuk memiliki satu thread untuk membaca dari soket dan thread lain untuk menulis ke soket. Dalam hal ini pegangan soket yang mendasarinya dapat dianggap aman untuk thread (satu thread baca dan satu thread tulis). Namun bahkan dalam skenario ini, objek sockpp::socket
masih belum aman untuk thread terutama karena nilai kesalahan yang di-cache. Thread tulis mungkin melihat kesalahan yang terjadi pada thread baca dan sebaliknya.
Solusi untuk kasus ini adalah dengan menggunakan metode socket::clone()
untuk membuat salinan soket. Ini akan menggunakan fungsi dup()
sistem atau yang serupa membuat soket lain dengan salinan duplikat dari pegangan soket. Keuntungan tambahannya adalah setiap salinan soket dapat bertahan seumur hidup secara independen. Soket yang mendasarinya tidak akan ditutup sampai kedua objek keluar dari ruang lingkup.
sockpp::tcp_connector conn({host, port});
auto rdSock = conn.clone();
std::thread rdThr(read_thread_func, std::move(rdSock));
Metode socket::shutdown()
dapat digunakan untuk mengomunikasikan maksud untuk menutup soket dari salah satu objek ini ke objek lainnya tanpa memerlukan mekanisme pensinyalan thread lain.
Lihat contoh tcpechomt.cpp.