اختاپوس خسته

یادداشت‌هایی پیرامون کد، زندگی و دوستان

برنامه‌نویسی شبکه در ویندوز/لینوکس: libcpnet

کارفرمای کاری که الان دارم انجام میدم اصرار داره که برنامه‌ش علاوه‌بر لینوکس روی ویندوز هم به‌خوبی اجرا بشه. مدیر من کاملاً این محدودیت رو پذیرفته و به کارفرما گفته که با سخت‌افزاری که مشخصاتش رو اعلام می‌کنیم و با مشخصات کارکردی که اعلام می‌کنیم، روی ویندوز هم می‌تونید برنامه رو اجرا کنید. خوب این تصمیم مشکلات بسیار بزرگی برای برنامه‌نویس به‌وجود میاره. ازجمله برنامه‌نویسی شبکه… این پست به بررسی این مشکلات و ارائهٔ یک راه حل خوب خواهد پرداخت (: اگر قصد دارید کدی بنویسید که هم روی ویندوز و هم روی سیستم‌عامل‌های واقعی بتونه از شبکه استفاده کنه حتماً ادامهٔ مطلب رو بخونید.

در لینوکس API ساده و بسیار کاربردی برای برنامه‌نویسی شبکه وجود داره که تقریباً تمام این API استاندارد شده. توابع زبان سی که خیلی ساده کارهایی رو با فلسفهٔ یونیکس (≈ هرچیزی یک فایل هست برای نوشتن و خواندن) انجام میده. به راحتی و با استفاده از این API سطح پایین میشه برنامه‌های شبکه با کارایی بالا نوشت.

مشکل از جایی شروع میشه که این برنامه‌ها بخوان سمت ویندوز برن. متأسفانه سیستم‌عامل ویندوز از نظر طراحی هیچ استاندارد خاصی رو رعایت نکرده و تنها قسمت‌هایی از مشخصات POSIX رو به‌صورت دست و پا شکسته و ناقص پیاده‌سازی کرده. از طرف دیگه API خوبی هم برای کار کردن با سوکت‌ها نداره. نصف توابعی که فراهم شده POSIX هستند و نصف دیگه کاملاً من‌درآوردی.

معمولاً برنامه‌های بزرگ برای حل این مشکلات از کتابخانه‌هایی که پیاده‌‌سازی‌های چندسکویی (cross-platform) فراهم کردند استفاده می‌کنند. معروف‌ترین این پیاده‌سازی‌ها poco, ACE, asio و ØMQ هستند. مشکلی که استفاده از این کتابخانه‌ها داره اینه که:

  • خیلی پیچیده و بزرگ هستند. (بعضی‌ها حتا چندین مگابایت پیش‌نیازی اضافه می‌کنند. مثل کیوت)
  • برای کارهای خاص و الگوهای پیچیده ساخته شده‌اند. (مثلا asio برای معماری‌های async و ZMQ برای الگوهای ارتباطی خاص)

به همین دلیل جای خالی یک کتابخانهٔ ساده و بسیار سبک که با کمترین هزینه؛ برای ویندوز و لینوکس رابط برنامه‌نویسی سطح سوکت فراهم کنه احساس می‌شد. متأسفانه من همچین کدی پیدا نکردم. و شروع کردم به نوشتن یک کتابخانهٔ آزاد و خیلی خیلی ساده به زبان سی که امکان برنامه‌نویسی شبکه در سطح سوکت رو برای هر دو سیستم‌عامل فراهم می‌کنه. نتیجه چیزی شد که خودم خیلی ازش راضی بودم و تصمیم گرفتم منتشرش کنم.

این کتابخانه از اینجا قابل دسترسی هست: https://github.com/soroush/libcpnet

ویژگی‌ها

  • API سطح سوکت هست. هیچ پیش‌فرضی در مورد الگوی برنامه‌نویسی یا پروتکل‌های سطح بالاتر از انتقال گرفته نشده؛
  • سوکت‌های UDP و TCP تقریباً به‌طور کامل پشتیبانی میشن؛
  • کد استاندارد سی هست (درواقع C99) که البته با C11 هم کامپایل میشه؛
  • کد روی سیزده پلتفرم مختلف تست شده. (ویژوال استودیو ۲۰۱۷ و ۲۰۱۵ – release و debug – استاتیک و داینامیک + نسخه‌های 4.7 4.8 4.9 5.4 و 6 از gcc)؛

محدودیت‌ها

  • در حال حاضر فقط سوکت‌های datagram و stream (در واقع UDP و TCP) رو پیاده‌سازی کردم. این‌ها پرکاربردتر از بقیهٔ انواع سوکت‌ها هستند. البته سوکت‌های انواع دیگه رو هم به مرور اضافه می‌کنم؛
  • این API کامل نیست. یعنی تمام توابع رو (مخصوصاً توابعی رو که هم توی لینوکس و هم توی ویندوز یکسان هستند) پوشش نمیده. دلیلش واضحه: دلیلی ندیدم پیاده‌سازی کنم.

با وجود این محدودیت‌ها کتابخانه کاملاً کاربردی و قابل استفاده هست و مشکلات زیادی رو حل می‌کنه.

کد

هیچ چیز بهتر از کد نمی‌تونه توضیح بده (: کد سمت کلاینت برای اتصال از نوع TCP یه همچین چیزی میشه:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* Client-side sample code */
/* Initialize networking API (Only needed in Windows) */
net_init();
/* Start a client */
socket_t socket = net_socket(SOCK_STREAM);
/* Connect to port 50001 */
if(net_connect(socket, "127.0.0.1", 50001) != 0)
{
    fprintf(stderr, "Unable to connect: %s", net_last_error();
    exit(-1);
}
char buffer[1024];
/* Write to socket */
ssizet io_size = net_write(socket, buffer, 1024);
if(io_size < 0)
{
    fprintf(stderr, "Unable to write: %s", net_last_error();
    exit(-1);
}
io_size = net_read(socket, buffer, 1024);
if(read_size <= 0) {
    fprintf(stderr, "Unable to read: %s", net_last_error();
    exit(-1);
}
/* ... */
/* Cleanup (Only needed in Windows) */
net_clean();

و کد سمت سرور برای باز کردن یک پورت و جواب دادن به اتصالات مختلف:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* Initialize networking API (if any needed) */
net_init();
/* Start a server */
socket_t socket = net_socket(SOCK_STREAM);
/* Bind on port 50001 */
uint16_t port = 50001;
if(net_bind(socket, NULL, &port) != 0) {
    fprintf(stderr, "Unable to bind: %s", net_last_error());
    exit(-1);
}
/* Listen for incomming connections (backlog=10) */
net_listen(socket, 10);
/* Accept clients */
char address[46];
socket_t client = net_accept(socket, address, &port);
fprintf(stderr, "Client connected. Remote Address:`%s' Assigned port number: %d\n", address, port);
char buffer[1024];
ssize_t r = net_read(client, buffer, 1024);
if(r < 0) {
    fprintf(stderr, "Unable to read from socket: ", net_last_error());
    exit(-1);
}
if(r == 0) {
    printf("Remote client stopped.");
    net_close(client);
    exit(-1);
}
ssize_t w = net_write(client, buffer, r);
if(w <= 0) {
    fprintf(stderr, "Unable to write to socket: ", net_last_error());
    exit(-1);
}

چند تا نکته:

  • همهٔ توابع با ‪net_‬ شروع میشن.
  • روی لینوکس لازم نیست ‪net_init()‬ و ‪net_clean()‬ رو بنویسید، اما ضرری هم نداره (هیچ کاری نمی‌کنه)

دیدگاه‌ها