سوکت‌های برکلی

سوکت‌های برکلی (به انگلیسی: Berkeley sockets) یا سوکت‌های بی‌اس‌دی (به انگلیسی: BSD sockets) کتاب‌خانه‌ای شامل رابط‌های برنامه‌نویسی نرم‌افزار برای کار با سوکت‌های اینترنتی و سوکت‌های دامنه یونیکس است، که از این سوکت‌ها برای ارتباطات بین پردازشی استفاده می‌شوند. سوکت‌های برکلی به عنوان رابط برنامه‌نویسی نرم‌افزار از سیستم‌عامل ۴/۲بی‌اس‌دی سرچشمه گرفتند که این سیستم‌عامل در سال ۱۹۸۳ منتشر شد. امروزه تمام سیستم‌عامل‌های مدرن یک پیاده‌سازی از سوکت‌های برکلی را به همراه دارند چون این سوکت‌ها روش استاندارد برای دسترسی به اینترنت هستند. این رابط‌ها در اصل به زبان سی نوشته شدند اما بیشتر زبان‌های برنامه‌نویسی رابط‌های مشابهی را در دسترس کاربر قرار می‌دهند و می‌توان از آن‌ها در اکثر زبان‌های برنامه‌نویسی مدرن استفاده کرد.

نمودار گردشی سوکت‌های TCP

فایل‌های سرآیند ویرایش

رابط‌های سوکت برکلی در چند فایل سرآیند تعریف شده‌اند. البته در پیاده‌سازی‌های مختلف، این فایل‌ها کمی در نام و محتوا با هم متفاوت هستند. اما به‌طور کلی این فایل‌های سرآیند به شرح زیر هستند:

<sys/socket.h>

توابع و ساختمان داده‌های اصلی مرتبط با سوکت‌ها در این فایل تعریف شده‌است.

<netinet/in.h>

دربرگیرنده AF_INET و AF_INET6 و پروتکل‌های متناظر با آن‌ها PF_INET و PF_INET6 است. این پروتکل‌ها به‌طور گسترده در اینترنت استفاده می‌شوند. این فایل هم دربرگیرنده آدرس‌های IP و هم شماره پورت‌های TCP و UDP است.

<sys/un.h>

این فایل حاوی خانواده آدرس‌های PF_UNIX/PF_LOCAL است. این دسته از آدرس‌ها برای برقرای ارتباط میان برنامه‌های موجود بر روی یک سیستم استفاده می‌شوند و از آن‌ها در شبکه استفاده نمی‌شوند.

<arpa/inet.h>

حاوی توابعی برای دستکاری آدرس‌های IP عددی است.

<netdb.h>

حاوی توابعی برای ترجمه کردن اسامی پروتکل‌ها و میزبان‌ها به معادل عددی آنهاست.

ساختارها ویرایش

ساختارهای مختلفی برای کار با سوکت‌ها وجود دارد که یکی از ساده‌ترین آن‌ها ساختار sockaddr_n است که در فایل netinet/in.h تعریف شده است:

struct sockaddr_in {
        uint8_t sin_len;
        sa_family_t     sin_family;
        in_port_t       sin_port;
        struct  in_addr sin_addr;
        char    sin_zero[8];
};

در ساختار sockaddr_in، فیلدهای sin_port و sin_addr باید به صورت Network byte order باشند. چرا که این اطلاعات قرار است بر روی شبکه ارسال شوند و هر ماشینی هم روش مخصوص به خود را برای پردازش داده‌های موجود در این فیلدها را دارد و بنابراین این فیلدها باید به صورت استاندارد بر روی شبکه ارسال شوند تا از تداخل جلوگیری شود. به کمک توابع htons()‎ و htonl()‎ می‌توان تبدیل مورد نظر را انجام داد. همین‌طور دیگر فیلدها هم در صورتی‌که استفاده نمی‌شوند، باید با مقادیر ۰ پر شوند. این کار با استفاده از تابع memset()‎ قابل انجام است. این تابع در فایل سرآیند string.h تعریف شده‌است. به مثال زیر توجه کنید:

#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

/* pseudo code */

struct sockaddr_in mystruct;

mystruct.sin_family = AF_INET;
mystruct.sin_port = htons(1234);
mystruct.sin_addr.s_addr = htonl(INADDR_ANY);

memset( &(mystruct.sinzero), '\0', 8 );

ساختار معروف دیگر ساختار addrinfo می‌باشد:

     struct addrinfo {
     int ai_flags;      /* input flags */
     int ai_family;      /* protocol family for socket */
     int ai_socktype;      /* socket type */
     int ai_protocol;      /* protocol for socket */
     socklen_t ai_addrlen;   /* length of socket-address */
     struct sockaddr *ai_addr; /* socket-address for socket */
     char *ai_canonname;     /* canonical name for service location */
     struct addrinfo *ai_next; /* pointer to next in list */
     };

توابع ویرایش

برخی از مهمترین توابع عبارتند از:

نام عملکرد
socket()‎ یک سوکت از یک نوع خاص (مثلاً AF_INET یا AF_INET6) ایجاد می‌کند و منابع سیستم را به آن اختصاص می‌دهد و سپس یک توصیف‌گر فایل به آن سوکت برمی‌گرداند.
bind()‎ این تابع معمولاً در سمت سرویس‌دهنده استفاده می‌شود و یک سوکت ایجاد شده توسط socket()‎ را به یک آدرس مرتبط می‌دهد.
listen()‎ این تابع سوکت مورد نظر را آماده پذیرش اتصالات ورودی می‌کند و به این ترتیب سرویس‌گیرنده‌ها می‌توانند به سیستم متصل شوند.
connect()‎ این تابع در سمت سرویس‌گیرنده استفاده می‌شود و یک شماره پورت تصادفی به سوکت اختصاص داده و سعی در برقراری ارتباط با سرویس‌دهنده می‌کند.
accept()‎ در سمت سرویس‌دهنده استفاده می‌شود. این تابع یک اتصال راه دور که از طرف یک سرویس‌گیرنده آغاز شده را پذیرفته و یک سوکت جدید (برای هر اتصال) برمی‌گرداند که از طریق این سوکت جدید می‌توان با سرویس‌گیرنده ارتباط برقرار کرد.
send()‎ و recv()‎ برای خواندن و نوشتن بر روی سوکت‌ها استفاده می‌شوند. از read()‎ و write()‎ هم می‌توان استفاده کرد اما send و recv مخصوص این کار نوشته شده‌اند.
close()‎ سوکت را می‌بندد و منابع اختصاص داده شده به آن را آزاد می‌کند.

socket()‎ ویرایش

سوکت‌ها نقاط پایانی اتصالات هستند. این فراخوان سیستمی یک سوکت جدید ایجاد کرده و یک توصیف‌گر فایل به آن سوکت برمی‌گرداند. socket()‎ سه آرگومان دریافت می‌کند:

  • domain: این آرگومان پروتکل مورد نظر را مشخص می‌کند. برای مثال:
    • AF_INET برای سوکت‌های نوع IPv4
    • AF_INET6 برای سوکت‌های نوع IPv6
    • AF_UNIX برای سوکت‌های محلی (از یک فایل استفاده می‌کند)
  • type می‌تواند یکی از مقادیر زیر را بگیرد:
    • SOCK_STREAM سوکت‌های قابل اطمینان و مبتنی بر جریان
    • SOCK_DGRAM سوکت‌های دیتاگرام
    • SOCK_SEQPACKET
    • SOCK_RAW
  • protocol این پارامتر پروتکل لایه انتقال را مشخص می‌کند. معمولاً مقدار این پارامتر با صفر مقدار دهی می‌شود تا پروتکل پیشفرض به صورت خودکار انتخاب شود.

این تابع در هنگام شکست مقدار ‎-۱ را برمی‌گرداند و در صورت موفقیت هم یک توصیف‌گر فایل به سوکت مورد نظر برمی‌گرداند که از نوع عدد صحیح است.

این تابع به شکل زیر تعریف شده است:

int socket(int domain, int type, int protocol);

bind()‎ ویرایش

این فراخوان سیستمی سوکت مورد نظر را به یک آدرس دلخواه مرتبط می‌کند. وقتی که یک سوکت جدید با socket()‎ ایجاد می‌شود، تنها نوع آن سوکت مشخص می‌شود. (IPv4 یا IPv6) و socket()‎ هیچ آدرسی به آن اختصاص داده نمی‌شود. قبل از اینکه سوکت بتواند برای پذیرش درخواست‌های سرویس‌گیرنده‌ها استفاده شود، باید با استفاده از bind()‎ یک آدرس دلخواه را به آن اتصال داد. bind()‎ سه آرگومان دریافت می‌کند.

  • sockfd یک توصیف‌گر فایل به سوکتی که می‌خواهیم آدرسی را به آن مرتبط کنیم.
  • my_addr اشاره‌گری به یک ساختار sockaddr که آدرس مورد نظر در آن قرار دارد.
  • addrlen یک فیلد از نوع socklen_t که اندازه ساختار sockaddr را مشخص می‌کند.

اگر سوکت با موفقیت به آدرس مورد نظر مرتبط شد، عدد ۰ و در غیر این صورت عدد ‎-۱ برمی‌گردد.

این تابع به شکل زیر اعلان شده است:

int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);

listen()‎ ویرایش

بعد از اینکه یک آدرس دلخواه به سوکت اختصاص یافت (از طریق bind()‎)، فراخوان سیستمی listen()‎ سوکت مورد نظر را برای دریافت اتصالات ورودی آماده می‌کند تا کلاینتها بتوانند به سیستم متصل شوند. اتصالات ورودی به ترتیب در یک صف قرار می‌گیرند. listen()‎ به دو آرگومان احتیاج دارد:

  • sockfd یک توصیف‌گر فایل به سوکت مورد نظر.
  • backlog این آرگومان تعداد درخواست‌های در حال انتظار در صف را مشخص می‌کند.

وقتی که یک اتصال مورد پذیرش قرار گرفت، از صف حذف می‌شود تا فضای کافی برای اتصالات جدید به وجود آید. این تابع در هنگام موفقیت مقدار ۰ و در هنگام شکست مقدار ‎-۱ را برمی‌گرداند. این تابع به شکل زیر اعلان شده است:

int listen(int sockfd, int backlog);

accept()‎ ویرایش

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

برنامه‌هایی که بر روی سوکت خاصی در حال گوش فرادادن برای دریافت اتصالات کلاینت‌ها هستند (با استفاده از listen()‎)، می‌توانند با استفاده از فراخوان accept()‎ درخواست‌های کلاینت‌ها را پذیرفته و اتصال جدیدی را با آن‌ها برقرار کنند. فراخوان accept()‎ یکی از درخواست‌های موجود در صف listen()‎ را پذیرفته و یک توصیف‌گر پرونده جدید برمی‌گرداند که این توصیف‌گر پرونده، علاوه بر توصیف‌گری است که برنامه در حال گوش فرادادن به آن است. به این ترتیب، برنامه می‌تواند هم‌زمان از طریق یک توصیف‌گر درخواست‌های جدید را بپذیرد و برای ارتباط با هر کلاینت هم یک توصیف‌گر مجزا خواهد داشت. این فراخوان سه پارامتر دارد:

sockfd: توصیف‌گر سوکتی که برنامه از طریق آن مشغول گوش فرادادن به درخواست‌ها است. cliaddr: اشاره‌گری به ساختار sockaddr به منظور دریافت کردن اطلاعات کلاینت addrlen: اشاره‌گری به یک نوع socklen_t که اندازه ساختاری که برای دریافت اطلاعت کلاینت ارسال شده (دومین پارامتر) را مشخص می‌کند. پس از اینکه تابع accept برگشت، این پارامتر تغییر می‌کند و مشخص می‌کند ک عملاً چند بایت از ساختار مورد نظر استفاده شده‌است.

مقدار برگشتی تابع accept در صورت موفقیت، یک توصیف‌گر پرونده است که برنامه از طریق آن می‌تواند با کلاینت ارتباط برقرار کند. تمامی ارتباطات با کلاینت از طریق این توصیف‌گر صورت خواهد گرفت. در صورت شکست، مقدار -۱ برمی‌گردد و متغیر سراسری errno هم با کد خطای مورد نظر مقداردهی می‌شود.

connect()‎ ویرایش

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

یک کلاینت با استفاده از این فراخوان می‌تواند به یک سرور متصل شود. این تابع در صورت موفقیت مقدار ۰ و در صورت شکست مقدار -۱ را برمی‌گرداند و متغیر errno را کد خطای مناسب مقداردهی می‌کند. اگر اجرای این تابع با شکست مواجه شود، برنامه‌های پورتابل می‌بایست بلافاصله توصیف‌گر پروندهٔ مورد استفاده را ببندند. چون این تضمین وجود ندارد که توصیف‌گر مورد نظر پس از شکست خوردن connect قابل استفادهٔ مجدد باشد. در استاندارد SUS وضعیت توصیف‌گر پرونده پس از شکست خوردن connect «تعریف‌نشده» توصیف شده‌است.

منابع ویرایش

مشارکت‌کنندگان ویکی‌پدیا. «Berkeley sockets». در دانشنامهٔ ویکی‌پدیای انگلیسی، بازبینی‌شده در ۱۳ ژوئیه ۲۰۱۳.