פתרון פרויקט רכיב עבודות מתקשר¶
נחלק את הפתרון לשלוש שכבות: ליבה שמכריעה מצב עבודה, transport שמביא בתים, וadapter שמתרגם את חוזה COM לפעולות הליבה. הקוד הבא נותן מימוש מלא של רכיב הHTTPS הסינכרוני וניסוי עצמאי עבורו. את הליבה ואת חיי השירות מחברים למימושים מהפרק הקודם; אחריהם מופיע חוזה האינטגרציה של COM והסבר למסלולי הכשל.
רכיב הרשת¶
נשתמש כאן בWinHTTP כדי שהאינטגרציה תעסוק בחיי עבודה ולא במימוש HTTP על גבי Schannel. שכבת TLS עדיין מבצעת אימות certificate ושם יעד. ההשוואה למכונת ההתקדמות של Schannel מההרצאה מאפשרת להבין אילו אחריויות עברו לספרייה.
הקובץ fetch.cpp שלהלן מקבל hostname וpath, פונה בHTTPS לפורט 443, מסרב לredirect ומגביל body ל64 KiB. הוא אינו מקבל URL מורכב ואינו מבטל validation. הוא גם מבטל authentication אוטומטי וcookies, כדי שבקשת הבדיקה לא תצרף זהות סביבתית או מצב שלא הוגדרו בחוזה. בפרויקט השירות יקרא לפונקציה עם יעד שמגיע מתצורה מאומתת; פרמטרי שורת הפקודה כאן נועדו לבדיקה עצמאית של הרכיב.
#include <windows.h>
#include <winhttp.h>
#include <array>
#include <cstdint>
#include <cstdio>
#include <stdexcept>
#include <string>
#include <vector>
#include <utility>
class InternetHandle {
HINTERNET value_ = nullptr;
public:
explicit InternetHandle(HINTERNET value) : value_(value) {
if (!value_) throw std::runtime_error("WinHTTP handle creation failed");
}
~InternetHandle() { WinHttpCloseHandle(value_); }
InternetHandle(const InternetHandle&) = delete;
InternetHandle& operator=(const InternetHandle&) = delete;
operator HINTERNET() const { return value_; }
};
void checked(BOOL result, const char* operation) {
if (!result) {
const DWORD error = GetLastError();
throw std::runtime_error(std::string(operation) + " error=" + std::to_string(error));
}
}
std::vector<unsigned char> fetch(const wchar_t* host, const wchar_t* path) {
if (!host || !*host || !path || *path != L'/')
throw std::invalid_argument("expected host and absolute path");
InternetHandle session(WinHttpOpen(L"AmitTechLab/1", WINHTTP_ACCESS_TYPE_NO_PROXY,
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0));
checked(WinHttpSetTimeouts(session, 3000, 3000, 3000, 3000), "timeouts");
InternetHandle connection(WinHttpConnect(session, host, INTERNET_DEFAULT_HTTPS_PORT, 0));
InternetHandle request(WinHttpOpenRequest(connection, L"GET", path, nullptr,
WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE));
DWORD disabled = WINHTTP_DISABLE_AUTHENTICATION | WINHTTP_DISABLE_COOKIES;
checked(WinHttpSetOption(request, WINHTTP_OPTION_DISABLE_FEATURE,
&disabled, sizeof(disabled)), "disable ambient state");
DWORD redirects = WINHTTP_OPTION_REDIRECT_POLICY_NEVER;
checked(WinHttpSetOption(request, WINHTTP_OPTION_REDIRECT_POLICY,
&redirects, sizeof(redirects)), "redirect policy");
DWORD revocation = WINHTTP_ENABLE_SSL_REVOCATION;
checked(WinHttpSetOption(request, WINHTTP_OPTION_ENABLE_FEATURE,
&revocation, sizeof(revocation)), "revocation");
checked(WinHttpSendRequest(request, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
WINHTTP_NO_REQUEST_DATA, 0, 0, 0), "send");
checked(WinHttpReceiveResponse(request, nullptr), "receive headers");
DWORD status = 0, size = sizeof(status);
checked(WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &status, &size, WINHTTP_NO_HEADER_INDEX), "status");
if (status != 200) throw std::runtime_error("HTTP status=" + std::to_string(status));
constexpr std::size_t limit = 64 * 1024;
std::vector<unsigned char> body;
std::array<unsigned char, 4096> buffer{};
for (;;) {
DWORD received = 0;
checked(WinHttpReadData(request, buffer.data(),
static_cast<DWORD>(buffer.size()), &received), "read body");
if (!received) break;
if (received > limit - body.size()) throw std::runtime_error("body exceeds limit");
body.insert(body.end(), buffer.begin(), buffer.begin() + received);
}
return body;
}
int wmain(int argc, wchar_t** argv) {
if (argc != 3) {
std::fputs("usage: fetch.exe host /path\n", stderr);
return 2;
}
try {
const auto body = fetch(argv[1], argv[2]);
std::uint64_t checksum = 0;
for (unsigned char byte : body) checksum = checksum * 131 + byte;
std::printf("bytes=%zu checksum=%llu\n", body.size(),
static_cast<unsigned long long>(checksum));
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}
הchecksum כאן הוא חישוב לימודי עם wrap מוגדר של unsigned, ואינו hash לאימות אבטחה. אין להסתמך על תוכן קבוע של אתר חיצוני בבדיקת regression. לבדיקות המדויקות נשתמש בשרת HTTPS של המעבדה עם קבצים קבועים וcertificate שהונפק לhostname שבו פונים. אמון בCA של המעבדה מוגדר רק בVM המתאימה ומתועד כחלק מהסביבה.
הtimeouts הם של שלבים וקריאות, ואינם מבטיחים deadline כולל: שרת ששולח מעט נתונים בכל פעם יכול להאריך את ההורדה. זאת מגבלה מפורשת של הרכיב הסינכרוני הזה. בגרסה עם deadline קשיח נעבור לWinHTTP אסינכרוני, נשמור context עד הודעת סגירה סופית ונחבר timer למכונת המצבים. לא נציג סגירת handle מתהליכון אקראי כתחליף מוכח לחוזה cancellation.
חוזה בין הרשת לליבה¶
הworker מקבל snapshot של התצורה, identity של העבודה וtoken ביטול לוגי. לפני תחילת הרשת הוא בודק אם העבודה כבר בוטלה. אחרי חזרת הרשת ולפני commit הוא בודק שוב את מצב העבודה תחת אותה הכרעה שמנהלת commit וביטול. אם ביטול ניצח, התוצאה לא מתפרסמת כהצלחה.
הביטול בגרסה הסינכרונית אינו עוצר מייד קריאת WinHTTP שכבר רצה. הסטטוס יהיה cancel-requested עד שהworker חוזר, והbuffer והcontext נשארים חיים. הממשק יכול להפסיק להמתין, אך השירות עדיין סופר את העבודה כחלק ממגבלת העומס. כך אין דרך לעקוף capacity באמצעות שליחה וביטול מהירים.
הcommit שומר את התוצאה ואת הכרעת request ID יחד לפי מודל ההתמדה של פרק 8. אם תהליך נופל בזמן ההורדה, GET של משאב בדיקה הוא פעולה שניתן לחזור עליה, אך התוכן יכול להשתנות בין הורדות. אם נדרשת זהות תוכן, נשמור ETag או digest שמגיע מחוזה מהימן ונגדיר כיצד מטפלים בשינוי.
חוזה של הממשק¶
ההגדרה הבאה היא בסיס IDL של ממשק העבודה. המזהים הם טקסט מוגבל שמתפרש בשרת לפורמט UUID; השרת עדיין מאמת אורך ותוכן לפני שימוש. לצורך העברה אוטומטית של Automation משתמשים בממשק dual ובtype library, או בונים proxy/stub מפורש עם MIDL. הבחירה צריכה להופיע בחבילת ההתקנה.
import "oaidl.idl";
import "ocidl.idl";
[
object, uuid(946190B4-05F9-4D85-8CB8-6D69D64E7331),
dual, oleautomation, pointer_default(unique)
]
interface ICourseJobs : IDispatch {
[id(1)] HRESULT Start([in] BSTR requestId, [out, retval] BSTR* jobId);
[id(2)] HRESULT Query([in] BSTR jobId, [out] LONG* state,
[out] BSTR* result);
[id(3)] HRESULT Cancel([in] BSTR jobId, [out, retval] LONG* state);
};
[
uuid(946190B5-05F9-4D85-8CB8-6D69D64E7331), version(1.0)
]
library CourseJobsLib {
importlib("stdole2.tlb");
[uuid(946190B6-05F9-4D85-8CB8-6D69D64E7331)]
coclass CourseJobs { [default] interface ICourseJobs; };
};
הadapter מאפס כל output בתחילת הקריאה, מאמת את זהות הלקוח, מבצע parsing מוגבל ומפעיל את הליבה. הוא אינו שומר BSTR מושאל בתוך worker. הוא מעתיק את המזהה לערך בבעלות הליבה ומקצה BSTR חדש עבור output. כל חריגת C++ נעצרת בגבול ומומרת לHRESULT מתאים. החלטת הרשאה נעשית על העבודה עצמה, ולא בQueryInterface.
המימוש של IDispatch יכול להשתמש בtype information שנוצרה מהIDL, למשל דרך DispGetIDsOfNames וDispInvoke, במקום להחזיק טבלת שמות ידנית שאינה תואמת את הtype library. הfactory, ספירת references וחיי תהליך השרת נשענים על המנגנונים של הרצאה 9.6. הלקוח מעביר את הממשק לworker באמצעות המנגנון של הרצאה 9.8, ומשאיר את הSTA מתקדם עד שחרור הreferences.
סגירה וכשל¶
בסגירה הליבה עוברת לclosing ודוחה התחלות חדשות. בקשות בירור קיימות יכולות להמשיך לקבל מצב, בהתאם למדיניות שהוגדרה. ביטול מבקש שינוי מצב ואינו מוחק את רשומת העבודה. השרת מתנקז לפני פריקת הקוד והruntime, או מסיים את התהליך לפי deadline תפעולי תוך שמירת מצב התאוששות מתועד.
אם התקבלה שגיאת COM אחרי Start, הלקוח מחזיק עדיין את request ID המקורי. הוא מתחבר מחדש ומברר את אותה כוונה; הוא אינו מחליף מזהה רק כדי שהקריאה "תעבור". אם אין ברשומה הכרעה מתמשכת, התוצאה מוצגת כלא ידועה עד השלמת מדיניות ההתאוששות.
בדיקות הפתרון¶
נכין bodyים בגודל 0, 65536 ו65537 בתים. שני הראשונים מותרים לפי החוזה, והשלישי נדחה בלי לפרסם checksum. נבדוק redirect ליעד אחר ונצפה לדחיית status שאינו 200. נבדוק certificate לא מתאים ונצפה לכשל בשכבת האימות, לפני פרסום תוצאה.
נזריק pause לפני קריאת הרשת, לפני commit ואחרי commit ולפני reply. בכל נקודה נבקש ביטול ונפיל חיבור לקוח. נשווה את התוצאה למכונת המצבים, ולא למועד שבו המשתמש הפסיק לראות spinner. לבסוף נריץ שני לקוחות, נסגור אחד ונבדוק שהעבודות והreferences של השני ממשיכים לפי החוזה.
הקוד לעיל הוא רכיב רשת עצמאי וחוזה אינטגרציה מפורט. השלמת הפרויקט כוללת את חיבור הadapter, רישום השרת, תמיכת marshalling ובדיקות התהליך הנפרד; אלה נבחנים מול התוצר של הלומד ואינם מוכחים מעצם קומפילציית fetch.cpp. בדוח נפריד בין בדיקות רכיב שעברו לבין בדיקות מערכת שעדיין דורשות את סביבת המעבדה.