לדלג לתוכן

קריאות מרוחקות וחוזי בקשה

כתבנו framing, המרות מספרים, פירוש שדות וטיפול בנתונים חלקיים. נניח שעכשיו רוצים לחשוף עשרים פעולות בין שני processes. אפשר לכתוב הכול ידנית, אבל חלק גדול מהעבודה חוזר: איך להעביר מספר, מה משמעות pointer, מי מקצה פלט ואיך מחברים תשובה לקריאה.

המנגנון Windows RPC מאפשר לתאר interface ולייצר stubs שמבצעים marshalling. בצד הלקוח נראה קריאה לפונקציה, אבל מתחתיה יש קידוד, transport, dispatch בצד אחר וקידוד תשובה. התחביר המקומי אינו הופך את הכשל למקומי. תהליך מרוחק יכול לסיים את הפעולה ואז להיעלם לפני שהתשובה מגיעה אלינו.

נגדיר interface קטן

נבנה שירות שמחשב סכום של מספרים מ1 עד n. הפעולה אינה משנה מצב, ולכן נוכל להפריד תחילה את המכניקה של RPC משאלת ביצוע כפול. נשתמש בncalrpc, כלומר transport מקומי למכונה, ונדרוש לקוח מאומת. המדיניות של הניסוי מאפשרת לכל לקוח מקומי מאומת לבצע את החישוב המצומצם הזה; אין כאן גישה למידע פרטי של משתמש אחר.

שמרו את ההגדרה בcourse_rpc.idl.

[
    uuid(d4af7e62-73f9-4b89-a8d1-5d7e938a50fa),
    version(1.0),
    pointer_default(unique)
]
interface CourseRpc
{
    unsigned long Compute(
        [in] handle_t binding,
        [in] unsigned hyper request_id,
        [in, range(1, 1000000)] unsigned long n,
        [out] unsigned hyper *result
    );
}

המזהה UUID מבדיל את הinterface, והגרסה מגדירה חוזה תאימות. הפרמטר binding הוא binding handle מפורש של RPC; הוא אינו pointer שאמור לעבור כפי שהוא לכתובת בתהליך השני. המספר מסוג unsigned hyper מגדיר מספר בן 64 ביט בIDL. תיאור הכיוון אומר אילו נתונים נשלחים ואילו מוחזרים. אפשר להשוות את המבנה להסבר על קובץ IDL.

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

זיכרון שהstub צריך

נוסיף קובץ memory.c ונקשר אותו גם לשרת וגם ללקוח. הruntime עשוי להשתמש בפונקציות ההקצאה שהקוד שנוצר מצפה להן.

#include <rpc.h>
#include <stdlib.h>

void* __RPC_USER midl_user_allocate(size_t bytes) {
    return malloc(bytes);
}

void __RPC_USER midl_user_free(void* memory) {
    free(memory);
}

בדוגמה אנחנו משתמשים בC בגבול הstubs כדי לראות את הABI ואת טיפול החריגות של RPC בלי לערבב אותם עם destructors של C++. בתוך מוצר C++ נעטוף את הגבול הזה, נתפוס חריגות יישומיות בצד השרת ולא ניתן להן לצאת באופן מקרי דרך הממשק שנוצר.

השרת

שמרו את הקוד הבא בserver.c. השרת בודק את רמת האימות בsecurity callback, ומפסיק לקבל קריאות כאשר לוחצים Enter בconsole. הוא אינו מפרסם פעולת shutdown לכל לקוח רק כדי שיהיה נוח לסיים ניסוי.

#include <windows.h>
#include <rpc.h>
#include <stdio.h>
#include <stdlib.h>
#include "course_rpc.h"

static void check(RPC_STATUS status, const char* operation) {
    if (status != RPC_S_OK) {
        fprintf(stderr, "%s: %lu\n", operation, status);
        exit(1);
    }
}

static RPC_STATUS RPC_ENTRY allow_call(RPC_IF_HANDLE interface_handle, void* context) {
    unsigned long level = 0, service = 0;
    RPC_STATUS status;
    (void)interface_handle;
    status = RpcBindingInqAuthClientW((RPC_BINDING_HANDLE)context, NULL, NULL,
                                     &level, &service, NULL);
    if (status != RPC_S_OK || service != RPC_C_AUTHN_WINNT ||
        level < RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
        return RPC_S_ACCESS_DENIED;
    return RPC_S_OK;
}

unsigned long Compute(handle_t binding, unsigned __int64 request_id,
                      unsigned long n, unsigned __int64* result) {
    (void)binding;
    if (!result || n < 1 || n > 1000000) return ERROR_INVALID_PARAMETER;
    *result = ((unsigned __int64)n * (n + 1)) / 2;
    printf("request=%llu n=%lu\n", request_id, n);
    return ERROR_SUCCESS;
}

int main(void) {
    check(RpcServerUseProtseqEpW((RPC_WSTR)L"ncalrpc", 8,
          (RPC_WSTR)L"AmitTechCourseRpc", NULL), "endpoint");
    check(RpcServerRegisterAuthInfoW(NULL, RPC_C_AUTHN_WINNT, NULL, NULL), "auth");
    check(RpcServerRegisterIfEx(CourseRpc_v1_0_s_ifspec, NULL, NULL,
          RPC_IF_ALLOW_LOCAL_ONLY | RPC_IF_ALLOW_SECURE_ONLY,
          8, allow_call), "interface");
    check(RpcServerListen(1, 8, TRUE), "listen");
    puts("local RPC server ready; press Enter to stop");
    (void)getchar();
    check(RpcMgmtStopServerListening(NULL), "stop listening");
    check(RpcMgmtWaitServerListen(), "drain calls");
    check(RpcServerUnregisterIf(CourseRpc_v1_0_s_ifspec, NULL, TRUE), "unregister");
    return 0;
}

הדגל secure only אינו מעניק למשתמש הרשאת מנהל; הוא מסנן לפי תנאי האימות של הממשק. הcallback בודק גם את רמת ההגנה שביקשנו. בעבודה עסקית נבדוק בעלות והרשאה בתוך הפעולה עצמה, משום שהחלטת הרשאה יכולה להשתנות לפי האובייקט ולפי הבקשה. אפשר לקרוא את דגלי רישום הממשק ואת שאילתת פרטי האימות.

המספר שמסרנו כMaxCalls אינו תחליף לתקציב קשיח של המוצר. יש לו משמעות לפי סוג הרישום והtransport, והruntime מתאר הגבלה שאינה תמיד bound מדויק. אם הפעולה מקצה משאבים יקרים, נציב admission control משלנו לפני הרכישה. פרטי הרישום מופיעים בRpcServerRegisterIfEx.

הלקוח

שמרו את הקוד הבא בclient.c. הוא יוצר binding מפורש, מגדיר אימות ורק אז קורא לstub שנוצר. שגיאת transport נתפסת בנפרד מקוד התוצאה של פעולת החישוב.

#include <windows.h>
#include <rpc.h>
#include <stdio.h>
#include "course_rpc.h"

int main(void) {
    RPC_WSTR text = NULL;
    RPC_BINDING_HANDLE binding = NULL;
    RPC_STATUS status;
    unsigned long application_status = ERROR_GEN_FAILURE;
    unsigned long transport_status = RPC_S_OK;
    unsigned __int64 result = 0;

    status = RpcStringBindingComposeW(NULL, (RPC_WSTR)L"ncalrpc", NULL,
                                      (RPC_WSTR)L"AmitTechCourseRpc", NULL, &text);
    if (status != RPC_S_OK) goto cleanup;
    status = RpcBindingFromStringBindingW(text, &binding);
    if (status != RPC_S_OK) goto cleanup;
    status = RpcBindingSetAuthInfoW(binding, NULL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
                                    RPC_C_AUTHN_WINNT, NULL, RPC_C_AUTHZ_NONE);
    if (status != RPC_S_OK) goto cleanup;
    RpcTryExcept {
        application_status = Compute(binding, 41, 10, &result);
    }
    RpcExcept(1) {
        transport_status = RpcExceptionCode();
    }
    RpcEndExcept
    if (transport_status != RPC_S_OK) {
        status = transport_status;
        goto cleanup;
    }
    printf("application=%lu result=%llu\n", application_status, result);
    status = application_status;

cleanup:
    if (binding) RpcBindingFree(&binding);
    if (text) RpcStringFreeW(&text);
    if (status != RPC_S_OK) fprintf(stderr, "call/setup failed: %lu\n", status);
    return status == RPC_S_OK ? 0 : 1;
}

החריגות כאן הן חריגות RPC שהmacros מטפלים בהן. בC++ נשמור את הבלוק הזה בפונקציה קטנה שאין בה אובייקטים שדורשים unwinding של C++, ונחזיר תוצאה מובנית לwrapper. לא נניח שcatch (std::exception&) מתאר את כל הכשלים של הקריאה.

יצירת stubs ובנייה

פתחו טרמינל x64 של כלי הפיתוח והSDK. הפקודה הראשונה מייצרת header וקובצי C. נבנה כל צד עם הstub שלו, בלי לקשר גם את client stub וגם את מימוש השרת לאותו קובץ הרצה.

midl /env x64 /robust /h course_rpc.h /client stub /server stub course_rpc.idl
cl /nologo /W4 /TC server.c memory.c course_rpc_s.c /Fe:rpc_server.exe rpcrt4.lib
cl /nologo /W4 /TC client.c memory.c course_rpc_c.c /Fe:rpc_client.exe rpcrt4.lib

הריצו את השרת, ובטרמינל אחר את הלקוח. בהרצה תקינה של הקלט הנתון נצפה לסכום 55 ולקוד יישומי אפס. אחר כך עצרו את השרת ונסו שוב, כדי לראות כשל binding או קריאה בנפרד מכשל של פונקציית החישוב. תהליך הבנייה הכללי מתואר במדריך RPC של Microsoft.

הפעולה בוצעה והתשובה אבדה

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

נשתמש במזהה בקשה יציב, בזהות הלקוח ובתוכן שמזהה את המשמעות המקורית. בצד השרת נשמור תוצאה לוגית לפי המפתח הזה. אם יש side effect חיצוני, נצטרך מנגנון שמחבר אותו להכרעה או שמאפשר reconciliation; טבלת dedup בזיכרון בלבד לא שורדת crash.

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

שינוי חוזה

הוספת פרמטר לIDL ושמירה על אותה חתימה לכאורה אינה שדרוג חופשי. הstubs והלקוחות שנבנו קודם מצפים לתיאור אחר. ננהל גרסת interface ונגדיר אילו צירופי גרסאות נתמכים. במבנים משתנים נגדיר גדלים, כיוונים וגבולות, ונבדוק את הקוד שנוצר ואת העלות שלו לפני שנשלח מערך גדול דרך pointer שנראה תמים בקוד המקור.