C++ היא לא אותה שפה כמו הסטנדרט הראשון שיצא ב-98. יש 3 צדדים שצריך להכיר בשפה על מנת לבנות מוצרים איכותיים:
תכונות השפה. כאן נתמקד בחלק מהתכונות החדשות על מנת לבנות קוד אגנוסטי.
כלי השפה. כלים לבנייה, הרצה, דיבוג וטסט.
כלי תוכנה. כלים לניהול גרסאות, ניהול תלויות והטמעה של המוצרים בפרודקשן.
מה נלמד כאן?
נציג את הכלים שבהם נעבוד כמו כן נסביר על מה שאנחנו משתמשים בו בפוסט הזה.
מה לא נלמד כאן?
לא נלמד לעבוד עם כל כלי בנפרד - לשם כך אני אשים לינקים להמשך למידה. מטרת הפוסט הוא להראות באילו כלים להשתמש וליצור סקרנות עבור הקוראים לאיך לבנות קוד מודרני בשפת C++.
באילו כלים נעבוד
בפרוייקט הזה נלמד להשתמש ב-4 כלים מודרניים לבניית שרתים יעילים על גבי C++.
boost - ספריית C++ לכלים מודרניים.
git - נלמד כיצד להשתמש בגיט בצורה מיטבית עבור ספריות צד שלישי.
cmake - נלמד כיצד לבנות את הקוד שלנו בצורה אגנוסטית למערכת ההפעלה.
docker - ווירטואליזציה ב-0 מאמץ.
עלייתו של Boost
ספריות Boost אלו ספריות רבות תחת פרוייקט אחד שעוסקות בתחומים מגוונים.
החלוקה של הספריות היא לכל תחום, והפרוייקט הגדול נמצא בגיט-האב: Github
namespace beast = boost::beast; // from <boost/beast.hpp> namespace http = beast::http; // from <boost/beast/http.hpp> namespace net = boost::asio; // from <boost/asio.hpp> using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
// This is just a small program state class not related to boost. // Counts usage and returns time. namespace my_program_state { std::size_trequest_count() { static std::size_t count = 0; return ++count; }
std::time_tnow() { return std::time(0); } }
// This class hols the connection, a buffer to use to reading and the http request and response. // These are classes by Boost.Beast. classhttp_connection : public std::enable_shared_from_this<http_connection> { public: http_connection(tcp::socket socket) : socket_(std::move(socket)) { }
// Initiate the asynchronous operations associated with the connection. voidstart() { read_request(); check_deadline(); }
private: // The socket for the currently connected client. tcp::socket socket_;
// The buffer for performing reads. beast::flat_buffer buffer_{8192};
// The request message. http::request<http::dynamic_body> request_;
// The response message. http::response<http::dynamic_body> response_;
// The timer for putting a deadline on connection processing. net::steady_timer deadline_{ socket_.get_executor(), std::chrono::seconds(60)};
// Asynchronously receive a complete request message. voidread_request() { auto self = shared_from_this();
// Determine what needs to be done with the request message. voidprocess_request() { response_.version(request_.version()); response_.keep_alive(false);
switch (request_.method()) { case http::verb::get: response_.result(http::status::ok); response_.set(http::field::server, "Beast"); create_response(); break;
default: // We return responses indicating an error if // we do not recognize the request method. response_.result(http::status::bad_request); response_.set(http::field::content_type, "text/plain"); beast::ostream(response_.body()) << "Invalid request-method '" << std::string(request_.method_string()) << "'"; break; }
write_response(); }
// Construct a response message based on the program state. voidcreate_response() { if (request_.target() == "/count") { response_.set(http::field::content_type, "text/html"); beast::ostream(response_.body()) << "<html>\n" << "<head><title>Request count</title></head>\n" << "<body>\n" << "<h1>Request count</h1>\n" << "<p>There have been " << my_program_state::request_count() << " requests so far.</p>\n" << "</body>\n" << "</html>\n"; } elseif (request_.target() == "/time") { response_.set(http::field::content_type, "text/html"); beast::ostream(response_.body()) << "<html>\n" << "<head><title>Current time</title></head>\n" << "<body>\n" << "<h1>Current time</h1>\n" << "<p>The current time is " << my_program_state::now() << " seconds since the epoch.</p>\n" << "</body>\n" << "</html>\n"; } else { response_.result(http::status::not_found); response_.set(http::field::content_type, "text/plain"); beast::ostream(response_.body()) << "File not found\r\n"; } }
// Asynchronously transmit the response message. voidwrite_response() { auto self = shared_from_this();
// Check whether we have spent enough time on this connection. voidcheck_deadline() { auto self = shared_from_this();
deadline_.async_wait( [self](beast::error_code ec) { if (!ec) { // Close socket to cancel any outstanding operation. self->socket_.close(ec); } }); } };
המחלקה http_connection אחראית על יצירת התשובות עבור בקשות. על מנת לאתחל את השרת עלינו להכיל io_context.
מהו io_context
אובייקט io_context מאגד בתוכו תהליכי הרצה אסינכרוניים. על מנת לאפשר לפונקציות, לתהליכונים ולסוקטים לרוץ ולקבל מידע ללא חסימת שאר התהליכים, יש צורך בלהשתמש במסנכרן. האובייקט הזה אחראי על ההרצה וסנכרון תהליכים כגון קבלת חיבור חדש מהרשת.
קבלת socket
על מנת לקבל חיבור חדש נשתמש ב-acceptor עם הפונקציה - acceptor.async_accept. הפרמטר הראשון הוא החיבור שבו נשתמש עבור חיבור חדש. הפרמטר השני הוא הפונקציה אותה נריץ בקבלה של חיבור.
ההרצה האסינכרונית
בביצוע ioc.run(); אנו מריצים את ה-io_context. הצורה שבה אנחנו מריצים מחדש את ה-Accept אחרי שחיבור אושר היא שוב פעם להריץ את המתודה acceptor.async_accept.
Acceptor היא תבנית נפוצה לחיבור סוקטים.
בדקו לבד כיצד הקוד מתייחס ל-request ואיך כותבים response לתגובה.
איך נוכל לשפר את הפרויקט ההתחלתי?
כמה נקודות מעניינות להתעמק בהן כדי לשפר את הפרוייקט:
לאגד את מתודת http_server אל תוך מחלקה ולנהל את מצב התוכנה בצורת OOP.
לעדכן את הסאב-מודול בצורה רקורסיבית. שימו לב, זה ייעדכן את כלל הספריות של boost, כדאי לכם להתנסות בחלק מהספריות האלו!
1
git submodule update --init --recursive
מה האלנטרנטיבות שלי לשמירת תלויות?
ניהול תלויות הוא תחום בתוכנה לאיך אנחנו שומרים ומנהלים גרסאות בין תלויות. תלות יכולה להיות בקוד - בין מחלקה A למחלקה B. תלות יכולה להיות בין שתי ספריות - C++ ו-Boost. תלות יכולה להיות מערכתית - בין שני רכיבי Web API.
מה בעצם האלטרנטיבה שלי ל-git submodules? לכאלו אנשים מוזרים שלא עובדים עם גיט ספציפי אלה עם כלים אחרים. או לחילופין לא רוצים לבנות את התלויות שלהם בעזרת גיט אלה בעזרת כלי אחר.
Conan
https://conan.io/ קונאן הוא כלי שקיים כבר כמה שנים. הכלי בצורתו הפשוטה יודע לפרסם חבילות ולצרוך חבילות בהתאם לגרסא.
קודם כל נצייין את גרסת המינימום שאנחנו רוצים שהסקריפט ירוץ. אם מוסיפים את השורה הזו - היא חייבת להיות שורה ראשונה בסקריפט.
נוסיף שם לפרוייקט
1
PROJECT(CPP_HTTP_SERVER_ON_DOCKER)
נצרוך את ספריית Boost
1 2 3 4 5 6
add_library(boost_beast INTERFACE)
target_include_directories(boost_beast SYSTEM INTERFACE "${CMAKE_CURRENT_LIST_DIR}/3rdparty/boost/libs/beast/include")
השורה add_library מוסיפה לבילד את הספרייה שרשמנו, שהיא boost_beast. מכיוון שזו ספריית Header only אנחנו צריכים להגדיר אותו עם INTERFACE. שזה אומר רק Header ללא קבצי LIB או DLL.
מכיוון שזו ספריית Header-Only כל מה שפרוייקט צריך זה את המיקומים ל-Submoduels של התלויות. מכיוון של-Boost.Beast יש תלויות חיצוניות בספריות Boost שונות, נצטרך להוסיף אותם לפרוייקט. שימו לב שפשוט הוספתי את כל הספריות :)
וכעת סיימנו את הסקריפט שלנו! בשורות בודדות הגדרנו תהליך בנייה אגנוסטי לחלוטין - גם לווינדוס וגם ללינוקס!
כדי לבנות את הפרוייקט בדרך כלל נשתמש בתקיית build. בתוך התיקייה ניצור תיקייה חדשה בשם build. נהיה על התיקייה ונריץ את הפקודה הבא:
1
cmake ..
זה בעצם יכין את כל מה שcmake צריך לבנייה.
אצלי הוא מזהה לבד שזה בווינדוס ומייצר את הפרוייקטים הרלוונטים ל-Visual Studio:
נוכל לבנות בעזרת הפקודה:
1
cmake --build . --config Release
זה ייבנה לנו את הפרוייקט ב-Release.
אם עשיתם את הכל כראוי תוכלו לראות את השרת שלכם ב- build/Release. הריצו:
1
cpp_http_server.exe 127.0.0.1 8000
וכנסו לכתובת localhost:8000/count:
כבר עכשיו יש לכם שרת עובד עם תהליך בנייה אגנוסטי!
מה עכשיו?
פרסום השרת! אבל איך נפרסם את השרת? ניתן לבנות סקריפט שמאגד את הכל ואורז את קובץ ה-Exe שלנו. אך בשביל לפרסם אותו בתוך שרת נצטרך להרים מערכת הפעלה בווירטואליזציה, לקנפג את הכל לבד - זה לא כיף.
בשביל זה יש לנו טכנולוגיה חדשה שנקראת Docker.
Docker
דוקר היא שיטה לווירטואליזציה נוחה וחלוקה של משאבים ברמת האפליקציה ולא ברמת מערכת ההפעלה.
לעומת הרצה של מכונות ווירטואליות כאשר כל מכונה עם מערכת הפעלה משלה. אנו יכולים לנצל מערכת הפעלה אחת, ומסנכרן אחד על מנת להריץ קונטיינרים רבים.
בונים DOCKERFILE
קובץ דוקר הוא בעצם המתכון לתמונת דוקר - Docker Image. כל שורה בקובץ דוקר היא גם שכבה - Docker Layer.
הדוקר יודע לשמור שכבות ובכך לא לבצע אותם כל הזמן מחדש במידה ויש שכפול של שכבות. בתהליך הזה הדוקר יידע כיצד להגדיר את השכבות ובסוף ייצור תמונה שתייצג את השרת הסופי שלנו.
לאחר מכן תמונה כזו יכולה להיות משומשת ליצירת Container. מה שרץ בסופו של דבר זה Conatiner ולא - Image. כפי שאמרנו ה-Image רק מתאר איך השרת שלנו בנוי.
בניית הקובץ
כדי לבנות את קובץ ההגדרות יש ליצור קובץ בשם Dockerfile ללא סיומת.
כשורה ראשונה נבסס את התמונה שלנו על תמונה של ubuntu - מערכת מבוססת לינוקס.
1
FROM ubuntu:latest
כמו ב-cmake זו השורה הראשונה והיא מבססת את התמונה הבסיסית שבה נשתמש.
התקנת כלים
1 2
RUN apt-get -y update && apt-get install -y RUN apt-get -y install g++ cmake
בעזרת RUN נריץ פקודות על גבי המכונה.
הפקודה הראשונה מעדכנת את החבילות המותקנות. הפקודה השנייה תתקין את הקומפיילר g++.
העתקה של כל הקוד
1
COPY . .
פשוט נעתיק את כל התוכן של התיקייה לתיקיית היעד הבסיסית.
תהליך ה-cmake בתוך הקונטיינר
1 2 3 4 5 6
RUNmkdir build
WORKDIR /build
RUN cmake .. RUN cmake --build . --config Release
זה מה שעשינו ידנית עם ה-cmake. הפקודה WORKDIR משנה את ה-Current Directory כדי לעבוד בתוך תיקיית build.
הפצת הפורט והרצה
1 2 3 4
ENV PORT=8080 EXPOSE8080
CMD"/build/cpp_http_server""0.0.0.0""8080"
אנו מגדירים משתנה סביבה בשם PORT כרגע אנו לא משתמשים בו אך זה נכון יותר להגדיר משתנה סביבה שיגדיר מהו הפורט.
הפקודה EXPOSE 8080 מגדירה אילו פורטים הקונטיינר הולך לחשוף.
ולבסוף נשתמש בפקודת CMD שמגדירה אילו פקודה “מריצה” את הקונטיינר.
אנו נריץ את השרת שלנו עם localhost והפורט 8080.
הרצת דוקר
במערכות ווינדוס ניתן להתקין Docker Desktop. לשימוש אישי ולא קנייני.
התוכנה עוזרת לכם לנהל את התמונות והקונטיינרים שיש לכם על המכונה.
המטרה היא להריץ את הקונטיינר על לינוקס בתוך הווינדוס שלנו.
CI/CD הוא עקרון אוטומציה לתהליכי הקונגפיגורציה וההתקנה.
נרצה שכחלק מהפרוייקט הרוחבי שלנו, נדע להריץ את הבילד, טסטים והתקנה בכל זמן כחלק מהפיתוח.
תהליכי אג’ייל שונים מיושמים בצורות שונות בחברות שונות - לא משנה איך התהליך אצלכם מתנהל, מה שחשוב לזכור הוא שאוטומציה מלאה לתהליכים מקלה לנו על שימושם ובסופו של דבר עוזרת לפתור באגים הרבה לפני שזה מגיע ללקוח או לשרת המבצעי שלנו.
שלב הרצת ווירטואליזציה יהיה השלב שבנו נתכנן ונריץ את תהליכי ההתקנה השונים שיש במערכת. נכין בעצם את כל הקוד והמוצרים להרצה.
שלב הפרסום הוא השלב שבו נטמיע את המערכת שלנו או בענן, או אצל לקוח, או בסביבה לבדיקות.
העקרון המנחה שלנו הוא כמה שיותר אוטומציה בתהליכים!
תיקון סקריפט דוקר
נצטרך להוסיף שני דברים. התקנת גיט ועדכון המודולים של boost.
על מנת לפרסם את התוצר המוגמר אתם מוזמנים להתנסות בהגדרות פרסום לאחד מהעננים הפופולריים.
זה קצת יוצא מה-Scope לכן לא אסביר הלאה :)
סיכום
אז מה ראינו כאן?
התנסנו בכתיבת שרת C++ בעזרת Boost.Beast. ראינו כיצד Cmake מקל לנו על החיים ליצירת פרוייקטים אגנוסטיים. השתמשנו בווירטואליזצית דוקר על מנת להריץ את הקוד שלנו.
ולבסוף חיברנו את Github Actions לכדי CI/CD התחלתי.