פיתוח יישומי רשת ב־C++‎ או הצצה ל־Boost.Asio.

ב־16.9.2008, מאת ארתיום; פורסם תחת: תכנה חופשית, פיתוח, תכנה ומחשבים, CppCMS, C++‎‏, Boost‏; ‏0 תגובות

במאמר זה, אביא סקירה קצרה של ספריית Boost.Asio‏‏ -- ספרייה לפיתוח יישומי תקשורת ב־C++‎ בצורה מהירה, יעילה ונוחה. ההכרות שלי איתה התחילה, דווקא מצורכי העבודה. אחרי זמן קצת, הבנתי שהיא מאוד נוחה ואפילו תהיה שימושית עבור CppCMS. למעשה, הספרייה הזו, אפשרה לי לעטוף את ספריית ה־cache של CppCMS ולהפוך אותה למבוזרת --- לבנות פתרון בסגנון memcached --- תוך מספר שעות בלבד.

הספרייה הזו קיימת בשתי גרסאות:

  1. גרסת Boost.Asio: היא חלק מ־Boost החל מגרסתו 1.35.
  2. גרסת Asio עצמאית, שדורשת Boost גרסה 1.33 ומעלה --- מאפשרת לעשות שדרוג של ספרייה ללא תלות בגרסת Boost.

אחד המאפיינים המעניינים שלה היא העובדה, שהספרייה כולה כתובה על בסיס Template Metaprogramming ומהווה אוסף קובצי־"‎.hpp" בלבד.

"בעיית עשרת אלפים קשרים" או "למה צריך ספריות כאלה בכלל?"

מעבר לעובדה ש־Berkeley Sockets API‏ די מסובך, עדין ושונה במערכות הפעלה שונות --- כבר סיבה מספיק טובה לבנות מעטפת עבורו --- אני אתרכז דווקא במשהו אחר, כתיבת יישומים יעילים.

נתחיל מסיפור קצר. נגיד אתם רוצים לפתח שרת מסרים מידיים שמטפל במספר רב של הלקוחות בו זמנית. בסה"כ השרת מבצע תפקיד מאוד פשוט: קבל הודעה מלקוח מסוים, להעביר ללקוח אחר.

יש מספר גישות לבניית השרת:

  1. צור תהליך לכך קשר קיים שיטפל בו --- הפתרון לא יעיל במיוחד אבל שריד מאוד. ככה עובד Apache הישן והטוב. אבל ברגע שהשרת יצטרך לטפל ב־100 קשרים בו זמנית... הוא ימות.
  2. צור חוט (thread) לכל קשר קיים שיטפל בו אחרי כ־1000 קשרים השרת יתחיל למות בגלל חוסר זכרון ובגלל העומס.
  3. תנהל קשרים בעזרת לולאת select/poll ב־thread אחד. זה כבר נשמע יותר טוב, כי צריכת הזכרון תהיה מינימלית. אבל... איתור של אירוע על כל socket, לוקח זמן לינרארי. הפתרון הזה הרבה יותר יעיל מבחינת צריכת הזכרון, אבל הוא מתחיל לגמגם כשמספר הקשרים עובר את ה־1000. (שלא לדבר על כך ש־select בכל מקרה מוגבל ל־1024 קשרים בלינוקס).
  4. אז מה עושים אם אנחנו רוצים להחזיק כ־10,000 קשרים פתוחים ולנהל אותם ביעילות? כדי לפתור אותה, אנחנו צריכים כלי, שידווח לנו: איזה אירוע ועל איזה socket התרחש מבלי להעביר לו/לקבל ממנו רשימת ה־socketים השלמה בכל פעם מחדש. כך נוצר המאמר The 10K Problem‏‏ שדן בבעיה ובפתרונות האפשריים שלה.

Linux מציע להשתמש ב־epoll,‏ FreeBSD בא עם kqueue,‏ OpenSolaris עובד עם ‎/dev/poll וב־Windows עובדים עם Completion port. הכלים האלה עושים פחות או יותר אותו דבר: מאפשרים להעביר אירועים על socketים בצורה א־סיכנרונית ביעילות מרבית.

כך עובד lighttpd האחראי על הזרמת וידיאו ב־YouTube, כך עובד memcached המחזיק את LiveJournal ו־WikiPedia למעלה; ועוד הרבה יישומים אחרים שהביצועים שם חשובים באמת.

אז צריך לממש מנגנון מיוחד לכל מערכת הפעלה? יש פתרונות. קיימות מספר ספריות שעוטפות את אוסף ה־APIים האלה: ביניהם ‏ACE‏ הישן והידוע, libevent‏ הקטן והיעיל ש־memcached משתמש בו.

הראשונה (ACE) אמנם חזקה, אבל כבד מיושנת מבחינת תפיסת העבודה. השנייה (libevent) כתובה ב־C ובכל מקרה דורשת מעטפת C++‎ רצינית. לכן, נוצרה ספריית מודרנית --- Boost.Asio, שאפילו Stroustrup‏ שיבח אותה.

Boost.Asio היא הספרייה שעוטפת את Berkeley Sockets API, עם הרחבות ספציפיות לפי מערכות הפעלה, ובונה framework נוח לפיתוח מערכות סינכרוניות וא־סינכרונית שעובדות בצורה היעילה מאוד. היא מאפשרת לרדת לרמה מאוד נמוכה במידת הצורך, יחד עם זאת, מאפשרת למפתח להתרכז בלוגיקת היישום ולא בטיפול ב־socketים.

עבודה עם ספרייה

אני לא הולך לכתוב עשרות דוגמאות של שימוש ב־Boost.Asio, אפשר למצוא מספיק כאן וכאן. אבל, כן, אספר על שיטת העבודה:

העבודה הסינכרונית היא פשוטה וטבעית, כמו שהייתם עובדים עם ספרייה רגילה: קרא מ־socket, כתוב אליו ועוד. רק יש מחלקות נוחות ל־bufferים ולחיפוש ל־stream כדי לקבוע גבולות הקריאה.

למשל, אם אתה מעוניין לעבוד עם פרוטוקול שמחלק את הכל לפי שורות (קרי הפרדה לפי סימן שורה חדשה) אז נוח מאוד להשתמש בפונקציה asio::read_until שמקבלת "תו" המאפשר הפרדה בין שורות בצורה שקופה.

כשעובדים בצורה א־סינכרונית, אז כל מה שצריך לעשות זה לקרוא לפונקציה asio::async_read_until ולמסור לה callback, שייקרא כשהקריאה תסתיים או אם תהיה שגיאה כלשהי על ה־socket. כל ה־callbackים מבוססים על boost::function, שמאפשרים להעביר כל פונקציה של כל מחלקה עם כל פרמטר כדי לבצע את המשימה. למעשה, Boost.Asio מממשת את התבנית Proactor, בה האובייקטים מקבלים סיגנלים על השלמה של פעולות שהם ביקשו.

טיפול בשגיאות יכול להתבצע בשתי צורות אפשריות:

  1. העברת משתנה שאליו יוזן ערך השגיאה.
  2. זריקת exception מתאים.

הכל בהתאם לתפיסת הפיתוח של אותה התכנה ונוחות המפתח. כך למשל, בשרת cache שבניתי אני עובד כמעט תמיד ללא exceptions, לעומת זאת בלקוח אני כן עובד איתם. מקצר את הקוד ומפשט את תהליכי הפיתוח.

סיכום

ספרייה מתוכננת היטב שמאפשרת לעבוד בצורות שונות, מאוד נוחה ושימושית. מומלץ שכל מפתח C++‎ יסתכל בה. ייתכן שאשכתב את המימוש של SCGI בעזרתה.

הוסף תגובה:

 
 כתובת דוא"ל לא תוצג
 

ניתן לכתוב תגובות עם שימוש בתחביר Markdown.

חובה לאפשר JavaScript כדי להגיב.

דפים

נושאים