مكتبة مقبس C++ بسيطة وحديثة.
هذا عبارة عن غلاف C++ منخفض المستوى إلى حد ما حول مكتبة مآخذ التوصيل Berkeley باستخدام فئات socket
acceptor,
connector
وهي مفاهيم مألوفة في اللغات الأخرى.
تقوم فئة socket
الأساسية بتغليف مقبض مقبس النظام، وتحافظ على عمره الافتراضي. عندما يخرج كائن C++ عن النطاق، فإنه يغلق مقبض مأخذ التوصيل الأساسي. تكون كائنات المقبس بشكل عام قابلة للنقل ولكنها غير قابلة للنسخ . يمكن نقل المقبس من نطاق (أو مؤشر ترابط) إلى آخر باستخدام std::move()
.
تدعم المكتبة حاليًا: IPv4 وIPv6 على Linux وMac وWindows. يجب أن تعمل أنظمة *nix وPOSIX الأخرى مع تعديلات قليلة أو معدومة.
تتوفر مقابس Unix-Domain على أنظمة *nix التي تحتوي على نظام تشغيل خاص بها.
تمت إضافة دعم للمآخذ الآمنة باستخدام مكتبات OpenSSL أو MbedTLS مؤخرًا مع التغطية الأساسية. وسوف يستمر هذا في التوسع في المستقبل القريب.
هناك أيضًا بعض الدعم التجريبي لبرمجة ناقل CAN على Linux باستخدام حزمة المقبس CAN. وهذا يمنح محولات ناقل CAN واجهة شبكة، مع قيود يفرضها بروتوكول رسائل CAN.
جميع التعليمات البرمجية الموجودة في المكتبة موجودة ضمن مساحة الاسم sockpp
C++.
يبدأ الفرع "الرئيسي" في التحرك نحو واجهة برمجة التطبيقات v2.0، وهو غير مستقر بشكل خاص في الوقت الحالي. يُنصح بتنزيل أحدث إصدار للاستخدام العام.
تم إطلاق الإصدار 1.0!
نظرًا لأن التغييرات العاجلة بدأت تتراكم في فرع التطوير الحالي، فقد تم اتخاذ القرار بإصدار واجهة برمجة التطبيقات (API) التي كانت مستقرة إلى حد ما خلال السنوات القليلة الماضية مثل 1.0. هذا من أحدث سطر v0.8.x. وهذا سيجعل الأمور أقل إرباكًا ويسمح لنا بالحفاظ على الفرع v1.x.
جاري تطوير الإصدار 2.0.
إن فكرة وجود عمليات إدخال/إخراج "عديمة الحالة" التي تم تقديمها في PR #17، (والتي لم يتم دمجها بالكامل مطلقًا) تأتي في واجهة برمجة التطبيقات 2.0 مع فئة result<T>
. سيكون عامًا على نوع الإرجاع "النجاح" مع تمثيل الأخطاء بواسطة std::error_code
. من المفترض أن يساعد هذا في تقليل مشكلات النظام الأساسي بشكل كبير فيما يتعلق بتتبع الأخطاء والإبلاغ عنها.
يؤدي استخدام نوع نتيجة موحد إلى إزالة الحاجة إلى الاستثناءات في معظم الوظائف، باستثناء الوظائف المنشئة. في تلك الحالات التي قد تُلقي فيها الدالة، سيتم أيضًا توفير دالة noexcept
قابلة للمقارنة والتي يمكنها تعيين معلمة رمز الخطأ بدلاً من الرمي. لذلك يمكن استخدام المكتبة دون أي استثناءات إذا رغب التطبيق في ذلك.
جميع الوظائف التي قد تفشل بسبب خطأ في النظام ستعيد نتيجة. سيؤدي ذلك إلى إلغاء الحاجة إلى "الخطأ الأخير"، وبالتالي سيختفي متغير الخطأ الأخير المخزن مؤقتًا في فئة socket
. ستقوم فئات المقبس بعد ذلك بتغليف مقبض المقبس فقط، مما يجعلها أكثر أمانًا للمشاركة عبر سلاسل الرسائل بنفس الطريقة التي يمكن بها مشاركة المقبض - عادةً مع مؤشر ترابط واحد للقراءة وآخر للكتابة.
وقد بدأت بعض الأعمال أيضًا في دمج مآخذ التوصيل الآمنة في إصدار 2.x من المكتبة باستخدام مكتبات OpenSSL أو MbedTLS، أو (على الأرجح)، اختيار وقت البناء لواحدة أو أخرى. يتم الآن دمج وتحديث 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
. يرجى إرسال جميع طلبات السحب مقابل هذا الفرع، وليس الفرع الرئيسي .
لمزيد من المعلومات، راجع: 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 | عن | إنشاء وتثبيت وثائق API المستندة إلى HTML (يتطلب Doxygen) |
SOCKPP_BUILD_EXAMPLES | عن | بناء برامج الأمثلة |
SOCKPP_BUILD_TESTS | عن | بناء اختبارات الوحدة (يتطلب Catch2 ) |
SOCKPP_WITH_CAN | عن | تشمل دعم المقبس. (لينكس فقط) |
قم بتعيينها باستخدام المفتاح "-D" في أمر تكوين CMake. على سبيل المثال، لإنشاء الوثائق وأمثلة التطبيقات:
$ 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/streaming. فهو يربط عنوانًا ويستمع إلى منفذ معروف لقبول الاتصالات الواردة. عند قبول الاتصال، يتم إنشاء مقبس تدفق جديد. يمكن التعامل مع هذا المقبس الجديد مباشرة أو نقله إلى سلسلة رسائل (أو مجموعة مؤشرات ترابط) للمعالجة.
على العكس من ذلك، لإنشاء عميل 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.
يمكن استخدام نفس نمط الموصلات والمستقبلات لاتصالات TCP عبر IPv6 باستخدام الفئات:
inet6_address
tcp6_connector
tcp6_acceptor
tcp6_socket
udp6_socket
الأمثلة موجودة في دليل الأمثلة/tcp.
وينطبق الشيء نفسه على الاتصال المحلي على أنظمة *nix التي تطبق Unix Domain Switches. لذلك استخدم الفئات:
unix_address
unix_connector
unix_acceptor
unix_socket (unix_stream_socket)
unix_dgram_socket
الأمثلة موجودة في دليل الأمثلة/يونيكس.
شبكة منطقة التحكم (CAN bus) هي بروتوكول بسيط نسبيًا تستخدمه عادةً وحدات التحكم الدقيقة للتواصل داخل السيارة أو الآلة الصناعية. يحتوي نظام التشغيل Linux على حزمة SwitchCAN التي تسمح للعمليات بمشاركة الوصول إلى واجهة ناقل CAN الفعلية باستخدام المقابس الموجودة في مساحة المستخدم. انظر: Linux المقبس CAN
في المستوى الأدنى، تقوم أجهزة 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
، على الرغم من أنها لا تحتاج عادةً إلى اختبارها مباشرةً، حيث سيتم تقييم الكائن نفسه إلى خطأ إذا لم تتم تهيئته أو في حالة خطأ. سيكون فحص الأخطاء النموذجي كما يلي:
tcp_connector conn({"localhost", 12345});
if (!conn)
cerr << conn.last_error_str() << std::endl;
لا تفعل المُنشئات الافتراضية لكل فئة من فئات المقبس شيئًا، وتقوم ببساطة بتعيين المقبض الأساسي على INVALID_SOCKET
. لا يقومون بإنشاء كائن مأخذ توصيل. سيؤدي استدعاء كائن connector
بشكل نشط أو فتح كائن acceptor
إلى إنشاء مقبس نظام تشغيل أساسي ثم تنفيذ العملية المطلوبة.
يمكن للتطبيق بشكل عام إجراء معظم العمليات ذات المستوى المنخفض مع المكتبة. يمكن إنشاء مآخذ توصيل غير متصلة وغير منضمة باستخدام وظيفة create()
في معظم الفئات، ثم ربط هذه المقابس والاستماع إليها يدويًا.
يكشف أسلوب socket::handle()
عن مؤشر نظام التشغيل الأساسي والذي يمكن بعد ذلك إرساله إلى أي استدعاء لواجهة برمجة تطبيقات النظام الأساسي التي لم يتم كشفها بواسطة المكتبة.
كائن مأخذ التوصيل ليس آمنًا لمؤشر الترابط. التطبيقات التي تريد قراءة عدة سلاسل رسائل من مأخذ توصيل أو الكتابة إلى مأخذ توصيل يجب أن تستخدم شكلاً من أشكال التسلسل، مثل 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.