מדריך ליצירת שירות צ'אט ב-WebSocket ל-Cloud Run

במדריך הזה מוסבר איך ליצור שירות צ'אט בזמן אמת עם כמה חדרים באמצעות WebSockets עם חיבור קבוע לתקשורת דו-כיוונית. ב-WebSockets, גם הלקוח וגם השרת יכולים לשלוח הודעות אחד לשני בלי לבצע בדיקה חוזרת של השרת כדי לראות אם יש עדכונים.

אפשר להגדיר ב-Cloud Run שימוש בזיקה לסשן, אבל הזיקה הזו היא במקרה הטוב, כלומר כל בקשה חדשה עדיין יכולה להיות מנותבת למופע אחר. כתוצאה מכך, הודעות המשתמשים בשירות הצ'אט צריכות להיות מסונכרנות בכל המקרים, ולא רק בין הלקוחות שמחוברים למקרה אחד.

תכנון שירות צ'אט בזמן אמת

שירות הצ'אט לדוגמה הזה משתמש במכונה של Memorystore for Redis כדי לאחסן ולסנכרן את הודעות המשתמשים בכל המכונות. ‫Redis משתמש במנגנון Pub/Sub (לא לבלבל עם המוצר Cloud Pub/Sub) כדי לשלוח נתונים ללקוחות מנויים שמחוברים לכל מופע, וכך למנוע בדיקות HTTP חוזרות כדי לקבל עדכונים.

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

מופעי Redis מוגנים מפני האינטרנט באמצעות כתובות IP פרטיות עם גישה מבוקרת ומוגבלת לשירותים שפועלים באותה רשת וירטואלית פרטית (VPC) כמו מופע Redis. מומלץ להשתמש ביציאה ישירה מ-VPC.

מגבלות

  • במדריך הזה לא מוצג אימות של משתמשי קצה או שמירת נתוני סשן במטמון. מידע נוסף על אימות של משתמשי קצה זמין במדריך Cloud Run בנושא אימות של משתמשי קצה.

  • במדריך הזה לא מוטמע מסד נתונים כמו Firestore לאחסון ולשליפה ללא הגבלה של היסטוריית הודעות בצ'אט.

  • כדי שהשירות לדוגמה הזה יהיה מוכן להפקה, צריך להוסיף לו עוד רכיבים. מומלץ להשתמש במופע Redis במסלול הרגיל כדי לספק זמינות גבוהה באמצעות שכפול ומעבר אוטומטי לגיבוי במקרה של כשל.

מטרות

  • כתיבה, פיתוח ופריסה של שירות Cloud Run שמשתמש ב-WebSockets.

  • מתחברים למכונת Memorystore for Redis כדי לפרסם הודעות חדשות ולרשום מנוי להודעות חדשות במכונות.

  • מחברים את שירות Cloud Run ל-Memorystore באמצעות תעבורת נתונים יוצאת (egress) ישירה של VPC.

עלויות

במסמך הזה משתמשים ברכיבים הבאים של Google Cloud, והשימוש בהם כרוך בתשלום:

כדי להעריך את ההוצאות בהתאם לתחזית השימוש שלכם, אתם יכולים להיעזר במחשבון העלויות.

משתמשים חדשים של Google Cloud ? יכול להיות שאתם זכאים לתקופת ניסיון בחינם.

לפני שמתחילים

  1. נכנסים לחשבון Google Cloud . אם אתם משתמשים חדשים ב- Google Cloud, צרו חשבון כדי שתוכלו להעריך את הביצועים של המוצרים שלנו בתרחישים מהעולם האמיתי. לקוחות חדשים מקבלים בחינם גם קרדיט בשווי 300$ להרצה, לבדיקה ולפריסה של עומסי העבודה.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  3. Verify that billing is enabled for your Google Cloud project.

  4. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  5. Verify that billing is enabled for your Google Cloud project.

  6. מפעילים את ממשקי ה-API של Cloud Run,‏ Memorystore for Redis,‏ Artifact Registry ו-Cloud Build.

    תפקידים שנדרשים להפעלת ממשקי API

    כדי להפעיל ממשקי API, צריך את תפקיד ה-IAM 'אדמין של Service Usage' (roles/serviceusage.serviceUsageAdmin), שכולל את ההרשאה serviceusage.services.enable. איך מקצים תפקידים

    הפעלת ממשקי ה-API

  7. מתקינים ומפעילים את ה-CLI של gcloud.

התפקידים הנדרשים

כדי לקבל את ההרשאות שדרושות להשלמת המדריך, צריך לבקש מהאדמין להקצות לכם את תפקידי ה-IAM הבאים בפרויקט:

להסבר על מתן תפקידים, ראו איך מנהלים את הגישה ברמת הפרויקט, התיקייה והארגון.

יכול להיות שאפשר לקבל את ההרשאות הנדרשות גם באמצעות תפקידים בהתאמה אישית או תפקידים מוגדרים מראש.

הגדרת ברירות מחדל ב-gcloud

כדי להגדיר את gcloud עם ערכי ברירת מחדל לשירות Cloud Run:

  1. מגדירים את פרויקט ברירת המחדל:

    gcloud config set project PROJECT_ID

    מחליפים את PROJECT_ID בשם הפרויקט שיצרתם לצורך המדריך הזה.

  2. מגדירים את gcloud לאזור שבחרתם:

    gcloud config set run/region REGION

    מחליפים את REGION באזור נתמך ב-Cloud Run לבחירתכם.

מיקומים של Cloud Run

‫Cloud Run הוא שירות אזורי, כלומר התשתית שמריצה את שירותי Cloud Run ממוקמת באזור ספציפי ומנוהלת על ידי Google כך שתהיה זמינה באופן יתירתי בכל התחומים באותו אזור.

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

‫Cloud Run זמין באזורים הבאים:

בכפוף לתמחור ברמה 1

בכפוף לתמחור ברמה 2

  • africa-south1 (יוהנסבורג)
  • asia-east2 (הונג קונג)
  • asia-northeast3 (סיאול, קוריאה הדרומית)
  • asia-southeast1 (סינגפור)
  • asia-southeast2 (ג'אקארטה)
  • asia-south2 (דלהי, הודו)
  • australia-southeast1 (סידני)
  • australia-southeast2 (מלבורן)
  • europe-central2 (ורשה, פולין)
  • europe-west10 (Berlin)
  • europe-west12 (טורינו)
  • europe-west2 (לונדון, בריטניה) סמל של עלה רמה נמוכה של CO2
  • europe-west3 (פרנקפורט, גרמניה)
  • europe-west6 (ציריך, שווייץ) סמל של עלה רמה נמוכה של CO2
  • me-central1 (דוחה)
  • me-central2 (דמאם)
  • northamerica-northeast1 (מונטריאול) סמל של עלה רמה נמוכה של CO2
  • northamerica-northeast2 (טורונטו) סמל של עלה רמה נמוכה של CO2
  • southamerica-east1 (סאו פאולו, ברזיל) סמל של עלה רמה נמוכה של CO2
  • southamerica-west1 (סנטיאגו, צ'ילה) סמל של עלה רמה נמוכה של CO2
  • us-west2 (לוס אנג'לס)
  • us-west3 (סולט לייק סיטי)
  • us-west4 (לאס וגאס)

אם כבר יצרתם שירות Cloud Run, תוכלו לראות את האזור בלוח הבקרה של Cloud Run בGoogle Cloud מסוף.

אחזור של דוגמת הקוד

כדי לאחזר את דוגמת קוד לשימוש:

  1. משכפלים את המאגר לדוגמה ומעבירים אותו למכונה המקומית:

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    אפשרות נוספת היא להוריד את הדוגמה כקובץ ZIP ולחלץ אותה.

  2. עוברים לספרייה שמכילה את הקוד לדוגמה של Cloud Run:

    Node.js

    cd nodejs-docs-samples/run/websockets/

הסבר על קוד WebSockets

‫Socket.io היא ספרייה שמאפשרת תקשורת דו-כיוונית בזמן אמת בין הדפדפן לבין השרת. למרות ש-Socket.io הוא לא הטמעה של WebSocket, הוא עוטף את הפונקציונליות כדי לספק API פשוט יותר למספר פרוטוקולי תקשורת עם תכונות נוספות כמו אמינות משופרת, חיבור מחדש אוטומטי ושידור לכל הלקוחות או לחלק מהם.

שילוב בצד הלקוח

<script src="/socket.io/socket.io.js"></script>

הלקוח יוצר מופע Socket חדש לכל חיבור. מכיוון שהדוגמה הזו מעובדת בצד השרת, אין צורך להגדיר את כתובת ה-URL של השרת. מופע השקע יכול לשדר אירועים ולהאזין להם.

// Initialize Socket.io
const socket = io('', {
  transports: ['websocket'],
});
// Emit "sendMessage" event with message
socket.emit('sendMessage', msg, error => {
  if (error) {
    console.error(error);
  } else {
    // Clear message
    $('#msg').val('');
  }
});
// Listen for new messages
socket.on('message', msg => {
  log(msg.user, msg.text);
});

// Listen for notifications
socket.on('notification', msg => {
  log(msg.title, msg.description);
});

// Listen connect event
socket.on('connect', () => {
  console.log('connected');
});

שילוב בצד השרת

בצד השרת, שרת Socket.io מאותחל ומצורף לשרת ה-HTTP. בדומה לצד הלקוח, אחרי ששרת Socket.io יוצר חיבור ללקוח, נוצר מופע של Socket לכל חיבור, שאפשר להשתמש בו כדי לשלוח הודעות ולהאזין להן. בנוסף, Socket.io מספק ממשק ליצירת 'חדרים' או ערוץ שרירותי שסוקטים יכולים להצטרף אליו ולעזוב אותו.

// Initialize Socket.io
const server = require('http').Server(app);
const io = require('socket.io')(server);

const {createAdapter} = require('@socket.io/redis-adapter');
// Replace in-memory adapter with Redis
const subClient = redisClient.duplicate();
io.adapter(createAdapter(redisClient, subClient));
// Add error handlers
redisClient.on('error', err => {
  console.error(err.message);
});

subClient.on('error', err => {
  console.error(err.message);
});

// Listen for new connection
io.on('connection', socket => {
  // Add listener for "signin" event
  socket.on('signin', async ({user, room}, callback) => {
    try {
      // Record socket ID to user's name and chat room
      addUser(socket.id, user, room);
      // Call join to subscribe the socket to a given channel
      socket.join(room);
      // Emit notification event
      socket.in(room).emit('notification', {
        title: "Someone's here",
        description: `${user} just entered the room`,
      });
      // Retrieve room's message history or return null
      const messages = await getRoomFromCache(room);
      // Use the callback to respond with the room's message history
      // Callbacks are more commonly used for event listeners than promises
      callback(null, messages);
    } catch (err) {
      callback(err, null);
    }
  });

  // Add listener for "updateSocketId" event
  socket.on('updateSocketId', async ({user, room}) => {
    try {
      addUser(socket.id, user, room);
      socket.join(room);
    } catch (err) {
      console.error(err);
    }
  });

  // Add listener for "sendMessage" event
  socket.on('sendMessage', (message, callback) => {
    // Retrieve user's name and chat room  from socket ID
    const {user, room} = getUser(socket.id);
    if (room) {
      const msg = {user, text: message};
      // Push message to clients in chat room
      io.in(room).emit('message', msg);
      addMessageToCache(room, msg);
      callback();
    } else {
      callback('User session not found.');
    }
  });

  // Add listener for disconnection
  socket.on('disconnect', () => {
    // Remove socket ID from list
    const {user, room} = deleteUser(socket.id);
    if (user) {
      io.in(room).emit('notification', {
        title: 'Someone just left',
        description: `${user} just left the room`,
      });
    }
  });
});

בנוסף, Socket.io מספק מתאם Redis לשידור אירועים לכל הלקוחות, בלי קשר לשרת שמשרת את השקע. ‫Socket.io משתמש רק במנגנון Pub/Sub של Redis ולא שומר נתונים.

const {createAdapter} = require('@socket.io/redis-adapter');
// Replace in-memory adapter with Redis
const subClient = redisClient.duplicate();
io.adapter(createAdapter(redisClient, subClient));

המתאם של Redis ב-Socket.io יכול לעשות שימוש חוזר בלקוח Redis שמשמש לאחסון היסטוריית ההודעות בחדר. כל קונטיינר ייצור חיבור למופע Redis, ו-Cloud Run יכול ליצור מספר גדול של מופעים. זה הרבה פחות מ-65,000 החיבורים ש-Redis יכול לתמוך בהם.

חיבור מחדש

הזמן הקצוב לתפוגה ב-Cloud Run הוא 60 דקות. לכן צריך להוסיף לוגיקה של חיבור מחדש למקרה של פסק זמן. במקרים מסוימים, Socket.io מנסה להתחבר מחדש באופן אוטומטי אחרי אירועים של ניתוק או שגיאת חיבור. אין ערובה לכך שהלקוח יתחבר מחדש לאותו מופע.

// Listen for reconnect event
socket.io.on('reconnect', () => {
  console.log('reconnected');
  // Emit "updateSocketId" event to update the recorded socket ID with user and room
  socket.emit('updateSocketId', {user, room}, error => {
    if (error) {
      console.error(error);
    }
  });
});
// Add listener for "updateSocketId" event
socket.on('updateSocketId', async ({user, room}) => {
  try {
    addUser(socket.id, user, room);
    socket.join(room);
  } catch (err) {
    console.error(err);
  }
});

המופעים יישארו פעילים אם יש חיבור פעיל עד שכל הבקשות ייסגרו או יגיעו לזמן קצוב לתפוגה. גם אם משתמשים בהעדפת סשן ב-Cloud Run, יכול להיות שבקשות חדשות יאזנו את העומס על קונטיינרים פעילים, וכך יאפשרו לקונטיינרים להקטין את קנה המידה. אם אתם חוששים ממספר גדול של קונטיינרים שנשארים פעילים אחרי עלייה חדה בתעבורת נתונים, אתם יכולים להקטין את ערך הזמן הקצוב לתפוגה המקסימלי, כדי שהמערכת תנקה את שקעי ה-TCP שלא בשימוש בתדירות גבוהה יותר.

משלוח השירות

  1. יוצרים מכונה של Memorystore for Redis:

    gcloud redis instances create INSTANCE_ID --size=1 --region=REGION

    מחליפים את מה שכתוב בשדות הבאים:

    • INSTANCE_ID: השם של המכונה, לדוגמה, my-redis-instance.
    • REGION_ID: האזור של כל המשאבים והשירותים – לדוגמה, europe-west1.

    למופע יוקצה באופן אוטומטי טווח כתובות IP מתוך טווח רשת השירות שמוגדר כברירת מחדל. במדריך הזה נעשה שימוש בזיכרון בנפח 1GB‎ למטמון המקומי של ההודעות במכונת Redis. מידע נוסף על קביעת הגודל הראשוני של מופע Memorystore לתרחיש השימוש שלכם

  2. מגדירים משתנה סביבה עם כתובת ה-IP של הרשת המורשית של מופע Redis:

     export REDISHOST=$(gcloud redis instances describe INSTANCE_ID --region REGION --format "value(host)")
  3. יוצרים חשבון שירות שישמש כזהות בשירות. כברירת מחדל, אין לו הרשאות מלבד חברות בפרויקט.

    gcloud iam service-accounts create chat-identity
    gcloud projects add-iam-policy-binding PROJECT_ID \
    --member=serviceAccount:chat-identity@PROJECT_ID.iam.gserviceaccount.com \
    --role=roles/serviceusage.serviceUsageConsumer
  4. מריצים את הפקודה הבאה כדי למצוא את השם של רשת ה-VPC עם הרשאת הגישה למכונת Redis:

      gcloud redis instances describe INSTANCE_ID --region REGION --format "value(authorizedNetwork)"
    

    מחליפים את מה שכתוב בשדות הבאים:

    • INSTANCE_ID: השם של המכונה, לדוגמה, my-redis-instance.
    • REGION_ID: האזור של כל המשאבים והשירותים – לדוגמה, europe-west1.

    רושמים את השם של רשת ה-VPC.

  5. יוצרים את קובץ האימג' של הקונטיינר ופורסים אותו ב-Cloud Run:

    gcloud run deploy chat-app --source . \
        --allow-unauthenticated \
        --timeout 3600 \
        --service-account chat-identity \
        --network NETWORK \
        --subnet SUBNET \
        --update-env-vars REDISHOST=$REDISHOST

    מחליפים את מה שכתוב בשדות הבאים:

    • NETWORK הוא השם של רשת ה-VPC המורשית שאליה מחובר מופע Redis.
    • SUBNET הוא השם של רשת המשנה. רשת המשנה צריכה להיות בגודל /26 או יותר. יציאה ישירה מ-VPC תומכת בטווחים של כתובות IPv4‏ RFC 1918‏, RFC 6598 ו-Class E.

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

ניסיון שימוש בשירות

כדי לנסות את השירות המלא:

  1. בדפדפן, עוברים לכתובת ה-URL שמופיעה בשלב הפריסה.

  2. כדי להיכנס, מוסיפים את השם שלכם ואת שם חדר הצ'אט.

  3. שולחים הודעה לחדר.

אם תבחרו להמשיך לפתח את השירותים האלה, חשוב לזכור שהגישה שלהם לניהול זהויות והרשאות גישה (IAM) מוגבלת לשאר השירותים של Google Cloud , ותצטרכו להקצות להם תפקידי IAM נוספים כדי לגשת לשירותים רבים אחרים.

הסרת המשאבים

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

מחיקת הפרויקט

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

הדרך הקלה ביותר לבטל את החיוב היא למחוק את הפרויקט שיצרתם בשביל המדריך.

כדי למחוק את הפרויקט:

  1. במסוף Google Cloud , נכנסים לדף Manage resources.

    כניסה לדף Manage resources

  2. ברשימת הפרויקטים, בוחרים את הפרויקט שרוצים למחוק ולוחצים על Delete.
  3. כדי למחוק את הפרויקט, כותבים את מזהה הפרויקט בתיבת הדו-שיח ולוחצים על Shut down.

מחיקת משאבי הדרכה

  1. מוחקים את שירות Cloud Run שפרסתם במדריך הזה. שירותי Cloud Run לא צוברים עלויות עד שהם מקבלים בקשות.

    כדי למחוק את שירות Cloud Run, מריצים את הפקודה הבאה:

    gcloud run services delete SERVICE-NAME

    מחליפים את SERVICE-NAME בשם השירות.

    אפשר גם למחוק שירותים של Cloud Run מGoogle Cloud המסוף.

  2. מסירים את הגדרת ברירת המחדל של האזור gcloud שהוספתם במהלך ההגדרה של המדריך:

     gcloud config unset run/region
    
  3. מסירים את הגדרות הפרויקט:

     gcloud config unset project
    
  4. מחיקת משאבים אחרים Google Cloud שנוצרו במדריך הזה:

המאמרים הבאים