פרוטוקול TCP במילים פשוטות

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

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

פרולוג

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

השאלה המתבקשת היא – למה שהעברת מידע פשוטה ברשת ה-IP לא תהיה אמינה? למה שלא פשוט נשלח את המידע והוא יגיע? ואכן בעולם אידיאלי, שבו כל חבילה הנשלחת על הקו היתה מגיעה ליעדה תוך זמן קבוע וללא שגיאות, פרוטוקול TCP היה כמעט חסר ערך לחלוטין, והיינו יכולים להסתפק בפרוטוקולים פשוטים ומהירים יותר כמו UDP. אך מכיוון שרשת האינטרנט מזכירה מעט את דואר ישראל – חבילות הולכות לאיבוד, מגיעות בזמנים שונים, נפגעות במשלוח וכו', הן השולח והן המקבל צריכים לשתף פעולה כדי לאפשר חיבור אמין ויציב.

פרוטוקול TCP הוא למעשה צורת התקשרות מוגדרת מראש, שכאשר מופעלת ע"י שני הצדדים (מחשבים, ראוטרים, שעונים חכמים וכד') מאפשרת, ע"י הוספת כמות קטנה של מידע (header) לכל חבילה, העברת מידע אמינה וללא שגיאות בין שני הצדדים, גם כאשר התווך ביניהם רעוע.

בעיה ראשונה – שינוע מידע רב

רשת האינטרנט, ופרוטוקול IP בפרט, מאפשרים העברת כמויות קטנות בלבד של מידע בכל "משלוח". בכדי להעביר מידע רב, יש לחלק אותו למנות קטנות ולשלוח כל אחת מהן בנפרד. פרוטוקול TCP, בדומה לפרוטוקולים אחרים, מחלק את המידע הנשלח לחבילות הנקראות פקטות (packets), ותפקידו של הצד המקבל הוא להרכיב אותן חזרה לכדי זרם מידע רציף. תכונה זאת של פרוטוקול TCP אינה ייחודית, ורוב הפרוטוקולים בשכבת התעבורה ומטה מחלקים גם הם את המידע לפקטות, כך ש-TCP אינו מיוחד בהקשר זה.

בעיה שניה – סדר ההגעה

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

מכיוון שמטרתו של פרוטוקול TCP הוא לאפשר העברת זרם מידע רציף, שיתקבל בדיוק כפי שנשלח, יש צורך לסדר את הפקטות שהתקבלו לפי הסדר בו נשלחו. הפתרון: הצמדת מספר סידורי רץ (sequence number) לכל פקטה1 ע"י הצד השולח, כך שהצד המקבל יוכל לשחזר את הסדר המקורי שלהן, גם אם הן הגיעו בסדר שונה מהסדר בו נשלחו. לדוגמה, אם צד א' שלח 3 פקטות, וצד ב' קיבל את פקטות 2 ו-3 בלבד, הוא יחכה עד להגעת פקטה מספר 1 ורק לאחר מכן ירכיב את המידע ויעביר אותו הלאה.

בעיה שלישית – פקטות אבודות

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

גם לבעיה זאת פרוטוקול TCP מספק מנגון התמודדות פשוט. זוכרים את המספר הסידורי? אז פרט לסידור הפקטות הוא משמש גם לביצוע "אישור הגעה"; כאשר החבילה הגיעה ליעדה, הצד המקבל שולח חזרה את המספר הסידורי שלה, ובכך מודיע לשולח שהחבילה התקבלה, אישור זה נקרא acknowledgement, או בקיצור ack. במידה והשולח לא מקבל ack תוך זמן מוגדר מראש משליחת החבילה, הוא פשוט שולח אותה שוב.

פרט חשוב נוסף הוא ש-ack על חבילה מאשר גם את הגעתן של כל החבילות שלפניה. כלומר אם קיבלנו חבילה שמספרה 3 אך לא את חבילה 2, לא ניתן לשלוח ack על חבילה 3 לפני שחבילה 2 הגיעה, אך לאחר שחבילה 2 הגיעה, אין צורך לשלוח אישור על כל חבילה בנפרד, ואפשר להסתפק באישור על חבילה 3 בלבד. כך ניתן להקטין את מספר האישורים ע"י שליחה של אישור בודד על מספר פקטות ביחד, ולחסוך רוחב פס.

בנוסף לשידור מחדש מבוסס זמן, אם הצד המקבל הבין שחבילה מסויימת לא הגיעה (כי החבילה הבאה כן הגיעה), הוא רשאי להודיע על כך לצד השולח ע"י שליחת ack נוסף (כפול) על החבילה האחרונה שהגיעה. במידה והשולח מקבל 3 אישורים על אותה חבילה, הוא מבין שהחבילה הבאה לא הגיעה, ושולח אותה שוב. למה 3 אישורים ולא 2? הסבר כאן.

שאלה מתבקשת היא – האין כל ה-acks הללו מוסיפים תקורה רבה מדי על הקו? התשובה המיידית היא – לא, זאת מכיוון שה-acks לרוב אינם נשלחים בתור פקטות בפני עצמן! פרוטוקול TCP מנצל את העובדה שהחיבור הוא דו כיווני (full duplex, שני הצדדים שולחים מידע אחד לשני), כדי לצרף את האישורים לפקטות נתונים שגם ככה היו אמורות להישלח בכיוון ההפוך. תכונה זאת נקראת piggybacking. אך אם צד אחד רק מקבל מידע ולא שולח כלום? במקרה זה הוא יחכה כדי "לצבור" פקטות, ולאחר שעבר זמן מסויים הוא ישלח אישור אחד שיכסה את כל הפקטות בבת אחת.

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

בעיה רביעית – הצב והארנב

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

כדי לפתור בעיה זאת, פרוטוקול TCP מיישם מנגנון הנקרא "חלון הזזה" (sliding window); כל אחד מהצדדים מגדיר לעצמו את כמות המידע המירבית שהוא מסוגל לקבל (למשל מגה בייט אחד), ומודיע על כך לצד השני בכל פקטה שהוא שולח. כאשר אחד הצדדים רוצה לשלוח מידע, הוא ישלח לכל היותר כמות מידע כגודל החלון של הצד השני. כאשר הצד השני קיבל את המידע (אך עדיין לא עיבד אותו), הוא יקטין את גודל החלון, וכאשר הוא יסיים לעבד ולאשר את המידע הוא יגדיל חזרה את גודל החלון.

הנה סרטון נחמד הממחיש את העיקרון:

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

בעיה חמישית – מישהו שומע אותי?

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

דבר דומה קורה ברשת האינטרנט – כאשר אנחנו שולחים פקטה לכתובת IP מסויימת, יש סיכוי שהיא לא תגיע ליעדה, או שאין לה בכלל לאן להגיע כי הצד השני לא מאזין או לא מחובר. פרוטוקול TCP נותן מענה לבעיה זאת באמצעות תהליך של יצירת חיבור (connection) בין שני הצדדים. אם נקביל זאת לשיחת טלפון, זה נראה בערך כך:

חבר: "הלו?"
אני: "היי, מה שלומך?"
חבר: "מצויין, מה איתך?"

תהליך זה נקרא "לחיצת יד תלת שלבית" (Three-way handshake) והוא השלב הראשון בכל חיבור TCP. שמותיהן המקצועיים של שלושת ההודעות הנשלחות הם SYN, SYN-ACK, ACK. לחיצת יד זאת מבטיחה (בסבירות גבוהה) ששני הצדדים מוכנים לדבר אחד עם השני, ומסמלת את תחילת חיבור ה-TCP. בנוסף, כחלק מהתהליך שני הצדדים מסכמים ביניהם איך תתבצע התקשורת – למשל מהו המספר הסידורי של הפקטה הראשונה, מהו גודל החלון ההתחלתי ועוד.

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

אני: "אני סבבה, ואתה?"

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

בעיה שישית – אתה עוד שם?

חיבור TCP אמנם נועד להעברת מידע, אך יתכן שבמשך זמן ארוך לאף אחד מהצדדים לא יהיה מה להגיד (קצת כמו שתיקה מביכה בטלפון). אך איך נבדיל בין מצב זה למצב בו אחד הצדדים פשוט ניתק את השיחה?

פרוטוקול TCP מציע פתרון פשוט – במידה ולא נשלח או התקבל מידע במשך זמן קבוע מראש, אחד הצדדים שולח חבילת מידע ריקה שכל מטרתה היא לוודא שהצד השני עדיין שם. חבילה זו נקראת Keepalive, והצד השני אמור לענות לה כמו לכל חבילת מידע אחרת. במידה והצד השני לא ענה (לאחר מספר נסיונות), ניתן להסיק שהוא כבר לא שם, ולהכריז על החיבור כמנותק. חשוב לציין שתכונה זו של פרוטוקול TCP הינה אופציונלית, כלומר לא כל מימושי הפרוטוקול משתמשים בה.

בעיה שביעית – "את-ה מק-קו-טע"

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

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

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

בעיה שמינית – מספר חיבורים במקביל

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

פרוטוקול TCP, כמו מספר פרוטוקולים נוספים בשכבת התעבורה מאפשר מספר חיבורים במקביל, וכמו הפתרונות הקודמים, גם פתרון זה מבוסס על רעיון פשוט מאוד. כאשר אנו ממענים פקטת TCP, אנו לא שולחים אותה לכתובת IP בלבד, אלא מוסיפים לכתובת גם מספר הנקרא פורט (port). אם נחשוב לרגע על כתובת IP ככתובת של בניין, ניתן לחשוב על הפורטים השונים כמספרי הדירות בבניין. ובדומה לאופן בו מספרי דירות מאפשרים לשלוח דואר למספר דיירים הגרים באותה כתובת, כך הפורט מאפשר העברת מידע לתוכנות שונות ה"גרות" בתוך אותו מחשב.

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

כאשר אנו מבקשים ליצור חיבור TCP חדש, אנו חייבים לציין את מספר הפורט. כדי לפשט עניינים, קיימים מספרי פורטים סטנדרטיים עבור שירותים שונים, למשל פורט 80 משמש לתקשורת HTTP (אתרי אינטרנט), פורט 22 עבור SSH ועוד.

לסיכום

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

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

 

1 המספר הסידורי משקף למעשה כל בית (byte) של נתונים ולא כל חבילה. למשל, אם המספר הסידור של החבילה הוא 100, והיא מכילה 50 בתים, מספר החבילה הבאה יהיה 150.

מאבטחים את הבית בפחות מ-80 שורות קוד

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

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

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

  1. צפייה במצלמת הרשת וזיהוי תנועה.
  2. בדיקה האם אני בבית (האם מכשיר הטלפון שלי מחובר לרשת הביתית).
  3. אם אני לא בבית, שליחת התראה לנייד עם קובץ הוידאו המכיל את התנועה.

נשמע הרבה ל-80 שורות קוד? גם אני חשבתי כך, אבל איזה מזל שיש את python.

אז בואו נעשה את זה!

החומרים הדרושים

את הקוד נכתוב, כאמור, בשפת python (גרסה 2.7) ונשתמש בספריות הבאות:

  • OpenCV – ספריית ראייה ממוחשבת (ולכידת וידאו).
  • scapy – ספריית רשת. נשתמש בה לבדיקה האם מכשיר מחובר לרשת הביתית.
  • pushbullet – לשליחת התרעות באמצעות השירות Pushbullet (ראו בהמשך).
  • numpy – ספריית מתמטיקה.
  • הספריות המובנות multiprocessing ו-datetime.

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

משתמשי Windows – התקנת ספריית scapy עלולה להיות מעט מורכבת. אך אני הצלחתי להתקין באמצעות הקבצים בקישור הבא. שימו לב שיש להתקין גם WinPcap, שהוא דרייבר לכרטיסי רשת.

שלב ראשון – צפיה במצלמה

הספריה OpenCV מאפשרת חיבור למצלמת רשת ולכידת תמונות (פריימים) ממנה. כדי לעשות זאת יש לייצר אובייקט מסוג VideoCapture. על הדרך, נחלץ גם את רוחב וגובה התמונה המתקבלת מהמצלמה:

cap = cv2.VideoCapture(0)
frame_size = (int(cap.get(3)), int(cap.get(4)))

במידה ויש לכם יותר ממצלמה אחת מחוברת למחשב, ניתן לבחור לאיזו מצלמה להתחבר ע"י שינוי הפרמטר של VideoCapture מ-0 למספר אחר.

כעת נרוץ בלולאה ובכל איטרציה נבקש תמונה מהמצלמה ע"י הפונקציה read של VideoCapture:

while cap.isOpened():
    success, frame = cap.read()
    assert success, "failed reading frame"
    now = datetime.datetime.now()

שימו לב שפונקציית read מחזירה שני ערכים: הצלחה/כשלון ואת התמונה עצמה. כמו כן שימו לב לשורת ה-assert, שנועדה לוודא שהצלחנו לקבל תמונה. אם לא הצלחנו, יזרק exception והתוכנית תצא. השורה האחרונה שומרת את הזמן הנוכחי במשתנה now, לו נזדקק בהמשך.

בהמשך (עדיין בתוך הלולאה) נציג למסך את התמונה שלכדנו, ולאחר מכן נשתמש בפונקציה waitKey כדי לבדוק האם נלחץ המקש q, ובמקרה זה לצאת מהלולאה (ומהתוכנית):

while cap.isOpened():
    ...

    cv2.imshow('frame', frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

נסו להריץ את הקוד למעלה. אם המצלמה שלכם מחוברת כהלכה אתם אמורים לראות חלון דומה לזה:

לכידת תמונה ממצלמת הרשת
לכידת תמונה ממצלמת הרשת

שלב שני – זיהוי תנועה

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

  1. טשטוש התמונה והמרתה לגווני אפור, זאת כדי להפחית שינויים מזעריים בין התמונות הנגרמים ע"י המצלמה.
  2. ביצוע diff (חיסור) בין הפריים הנוכחי לפריים הקודם ויצירת תמונה המורכבת מההבדלים ביניהם. הבדלים בין התמונות יהיו קיימים רק אם התרחשה תנועה כלשהי. אם לא היתה תנועה, הפריימים יהיו (כמעט) זהים.
  3. סינון כל ההבדלים מתחת לסף מסויים, כדי להסיר "רעשים" מזעריים הנגרמים ע"י המצלמה עצמה גם אם אין תנועה.
  4. אם התמונה הסופית מכילה פיקסלים שאינם שחורים, נניח שהייתה תנועה ונפעל בהתאם.

נתחיל בכתיבת פונקציה המקבלת שתי תמונות (שכבר טושטשו והומרו לגווני אפור) ומבצעת את שלבים 2-4, כלומר מחזירה ערך בוליאני המציין האם הייתה תזוזה. לצורך כך נשתמש שוב בספריית OpenCV:

def have_motion(frame1, frame2):
    delta = cv2.absdiff(frame1, frame2)
    thresh = cv2.threshold(delta, 25, 255, cv2.THRESH_BINARY)[1]
    return numpy.sum(thresh) > 0

השורה הראשונה בפונקציה מחשבת את ההבדלים בין התמונות ומייצרת את התמונה delta, המייצגת את ההבדלים. השורה השנייה מאפסת את כל הפיקסלים שערכם קטן מ-25, ואת כל השאר קובעת למקסימום (255). ניתן לשנות פרמטר זה כדי לקבוע את הרגישות לתנועה. וכך זה נראה (לחצו להגדלה):

זיהוי תנועה
שלבי זיהוי תנועה. משמאל, הפריים המקורי, עם היד שלי בזמן תנועה. באמצע, ההפרש בין הפריים המקורי לזה שקדם לו. בימין – ההפרש לאחר הרצת הפונקנציה threshold.

כעת כל שנותר הוא להשתמש בפונקצייה have_motion בלולאה הראשית:

frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
frame_gray = cv2.GaussianBlur(frame_gray, (21, 21), 0)

if have_motion(prev_frame, frame_gray):
    last_motion = now
    # TODO: start recording to a video file

prev_frame = frame_gray

שתי השורות הראשונות ממירות את הפריים לגווני אפור ומטשטשות אותו. השורה האחרונה שומרת את הפריים הנוכחי למשתנה prev_frame, שבאיטרציה הבאה של הלולאה יהפוך לפריים הקודם (לאחר לכידת תמונה חדשה מהמצלמה). שימו לב שבמידה והיתה תנועה, אנו שומרים את הזמן הנוכחי למשתנה last_motion, לו נזדקק בהמשך.

שלב שלישי – שמירה לקובץ וידאו

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

motion_filename = now.strftime("%Y_%m_%d_%H_%M_%S_MOTION.avi")
motion_file = cv2.VideoWriter(motion_filename, fourcc, 20.0, frame_size)

אתחול האובייקט מתבצע עם הפרמרטים הבאים:

      1. שם קובץ הפלט, אותו אנו בונים בשורות הראשונה והשנייה לפי הזמן הנוכחי (מהמשתנה now).
      2. אובייקט מסוג CV_FOURCC, המציין את הקידוד בו נרצה לשמור את הקובץ. חשוב לבחור את הקידוד כך שיתמך גם ע"י מערכת ההפעלה שלנו, וגם ע"י המכשיר הנייד אליו שולחים את הקובץ. עבורי הקידוד XVID עובד מצויין, אך יתכן שתזדקקו לקצת ניסוי וטעייה עם רשימת הקידודים הנמצאת באתר fourcc.org.
        fourcc = cv2.cv.CV_FOURCC(*"XVID")
      3. מספר פריימים לשניה.
      4. tuple המכיל את רוחב וגובה הפריים, אותו יצרנו מוקדם יותר.

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

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

if motion_file is not None:
    motion_file.write(frame)

וכדי לסגור את הקובץ ולשלוח אותו, נחכה שהזמן שעבר מאז התנועה האחרונה יעלה על מספר שניות:

if motion_file is not None:
    motion_file.write(frame)
    if now - last_motion > MOTION_RECORD_TIME:
        motion_file.release()
        motion_file = None
        # TODO: send video file

כאשר את הפרמטר MOTION_RECORD_TIME נגדיר בתחילת הקובץ, למשל ל-10 שניות:

MOTION_RECORD_TIME = datetime.timedelta(seconds = 10)

כעת למעשה סיימנו לכתוב תוכנית המזהה ומקליטה תנועה לקובץ וידאו!

שלב רביעי – בדיקה האם אני בבית

גם כאן ישנן דרכים רבות למימוש. לדוגמה: אם למחשב שלנו יש חיבור Bluetooth, ניתן לבדוק אם המכשיר הנייד שלנו בסביבה ע"י סריקת מכשירים קרובים. אני בחרתי בדרך פשוטה של בדיקה האם המכשיר הנייד שלי מחובר לרשת ה-WiFi הביתית.

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

 find mac address <device name>

בנוסף נצטרך למצוא את כתובת ה-IP שלנו וה-subnet mask של הרשת הביתית שלנו, ע"י הרצת הפקודה ipconfig ב-command line של Windows (הוראות עבור מערכות הפעלה אחרות). את שני הנתונים האלו נמיר לטווח כתובות ה-IP של הרשת הביתית (בפורמט CIDR) ע"י שימוש בכלי הבא. זה אולי נשמע קצת מסובך, אבל לרוב כל שנצטרך לעשות הוא להחליף את המספר האחרון בכתובת ה-IP שלנו ב-0 ולהוסיף את המחרוזת /24.

נשמור את שני פריטי המידע הנ"ל במשתנים:

DEVICE_MAC = "3d:f9:c2:d8:0f:d5"
SUBNET = "192.168.1.0/24"

כעת נשתמש בספרייה scapy כדי לבצע סריקת ARP. שאילתת ARP היא הדרך המקובלת להמרת כתובת IP ברשת המקומית לכתובת MAC. אפשר לדמיין שאילתת ARP כצעקה "מיהו בעל כתובת ה-IP הזאת?", אליה עונה בעל הכתובת בלבד: "אני הבעלים של כתובת ה-IP, וה-MAC שלי הוא…".

סריקת ARP שולחת שאילתות ARP עבור כל אחת מכתובות ה-IP האפשריות ברשת ואוספת את התשובות שהתקבלו. אם המכשיר שלנו מחובר לרשת (יש לו כתובת IP ברשת), הוא אמור לענות לפקודת ה-ARP עם כתובת ה-MAC שלו, וכך נדע שהוא מחובר. נכתוב קוד שמבצע את הסריקה הנ"ל, אוסף את התשובות ומחפש בהן את כתובת ה-MAC של המכשיר שלנו:

def is_device_connected(mac_addr):
    answer, _ = scapy.srp(
        scapy.Ether(dst="ff:ff:ff:ff:ff:ff") /
        scapy.ARP(pdst=SUBNET), timeout=2)
    return mac_addr in (rcv.src for _, rcv in answer)

לאחר הרצת הפונקציה scapy.srp, המשתנה answer יכיל את מערך התשובות שהתקבלו לשאילתות ה-ARP. השורה השנייה בודקת האם כתובת ה-MAC שסופקה נמצאת בתוך אחת התשובות.

שלב חמישי ואחרון – שליחת קובץ הוידאו

כעת נוודא שאנחנו לא בבית, ונשלח לעצמנו התראה עם קובץ הוידאו באמצעות השירות Pushbullet:

def push_file(filename):
    if is_device_connected(DEVICE_MAC):
        print "Device is connected, not sending"
        return
    print "Sending", filename
    pushbullet = Pushbullet("PUSHBULLET_API_KEY")
    my_device = pushbullet.get_device("My Device")
    file_data = pushbullet.upload_file(open(filename, "rb"), filename)
    pushbullet.push_file(device = my_device, **file_data)

שימו לב להחליף את המחרוזות PUSHBULLET_API_KEY ו-My Device בערכים המתאימים מתוך חשבון ה-Pushbullet שלכם.

כעת כל שנותר הוא לקרוא לפונקציה push_file כאשר סיימנו להקליט את קובץ הוידאו. אבל… חשוב לשים לב שהרצת הפונקציה לוקחת מספר שניות, מכיוון שסריקת ה-ARP והעלאת הקובץ הן פעולות ארוכות יחסית. אם נקרא לפונקציה push_file ישירות מהלולאה הראשית, התוכנית שלנו "תיתקע" עד ששליחת הקובץ תסתיים, ולכן נאבד מספר שניות של האזנה למצלמה.

כדי שדבר כזה לא יקרה (חור אבטחה!) נרצה להריץ את תהליך השליחה במקביל ללולאה הראשית. ניתן לעשות זאת ע"י שימוש ב-threads, אך ב-python מומלץ להשתמש דווקא בתהליך נפרד. נעשה זאת ע"י שימוש במחלקה Process מתוך הספריה המובנית multiprocessing:

if now - last_motion > MOTION_RECORD_TIME:
    ...
    Process(target = push_file, args = (motion_filename, )).start()

סיימנו! וכך נראית ההודעה שמתקבלת במכשיר:

ההודעה שהתקבלה במכשיר הנייד
ההודעה שהתקבלה במכשיר הנייד

מה הלאה?

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

    1. שמירת כל הוידאו לדיסק, גם אם אין תנועה. אפשר באיכות נמוכה יותר כדי לחסוך מקום בדיסק.
    2. הגבלת גודל קובץ הוידאו הנשלח, למשל לדקה אחת.
    3. הזרמת הוידאו בלייב למכשיר הנייד.
    4. תמיכה במספר מצלמות / מכשירים ניידים במקביל.
    5. זיהוי המכשיר הנייד בדרכים אחרות.
    6. זיהוי פרצופים והתראה על פרצופים חשודים בלבד.

הקוד המלא

ניתן למצוא את הקוד המלא ב-GitHub של I, Code. אשמח אם תמשיכו לפתח את הפרוייקט, למצוא לי באגים, ולשלוח pull requests!

פרוטוקול HTTP – מדריך למתחילים

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

פרוטוקול HTTP – מה זה ולמה צריך את זה?

Hypertext transfer protocol, ובקיצור HTTP, הינו פרוטוקול שרת-לקוח מבוסס טקסט. במילים פשוטות, HTTP מגדיר שפת טקסט פשוטה המאפשרת לצרכן שירות כלשהו – הלקוח – לבקש משאבים מנותן שירות כלשהו – השרת.

לקוח ה-HTTP הנפוץ ביותר הוא הדפדפן, המבצע בקשות אל מול שרת אינטרנט להורדת דפי אינטרנט, תמונות וכדומה. אך זהו לא השימוש היחיד בפרוטוקול: HTTP משמש גם תוכנות אחרות מבוססות שרת לקוח כגון אפליקציות מובייל, מערכות החושפות REST API, שרתי source control, תקשורת בין רכיבים במערכות מורכבות ועוד אינספור דוגמאות אחרות.

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

במודל 4 השכבות (וגם בזה המכיל 7), HTTP שייך לשכבה הגבוהה ביותר – שכבת היישום (האפליקציה). כלומר כדי להשתמש בפרוטוקול HTTP אנחנו צריכים לדאוג קודם כל לחיבור אמין בין השרת ללקוח, שיאפשר לנו העברת טקסט בין שתי הישויות. אחרי שייצרנו חיבור כזה, שכמעט תמיד יהיה מסוג TCP, פרוטוקול HTTP למעשה מגדיר איך יראה המידע שיעבור בין השרת ללקוח, כלומר איך תיראה בקשה למידע, ואיך תיראה תגובה אליה.

מבנה בקשת HTTP

השלב הראשון בשיחת HTTP הוא שליחת בקשה מהלקוח לשרת. הבקשה נשלחת ע"ג חיבור TCP שנפתח בין הלקוח לשרת, ובפרט לפורט עליו מאזין השרת. באינטרנט יהיה זה בד"כ פורט 80, אך ניתן להשתמש בכל פורט.

פרוטוקול HTTP - בקשה
שליחת בקשת HTTP מהלקוח לשרת

כל בקשת HTTP היא למעשה טקסט רגיל המורכב משלושה חלקים המופרדים ביניהם בשורת רווח (התווים \r\n):

  • שורת הפתיחה
  • שורות כותרת (headers)
  • גוף הבקשה (לא חובה)

שורת הפתיחה

שורת הפתיחה מורכבת גם היא משלושה חלקים, המופרדים ביניהם בתו רווח:

  • שיטת הבקשה (method) היא מילה אחת המתארת מה מבקש הלקוח מהשרת לעשות. בפרוטוקול HTTP 1.1 קיימות 8 שיטות, שהנפוצות ביניהן הן GET המבקשת לקבל משהו מהשרת, ו-POST השולחת מידע לשרת. מבחינת הפרוטוקול עצמו, ההבדל בין השיטות הוא כמעט סמנטי בלבד (אין כמעט הבדל בין GET ל-POST), אך בפועל השרתים עצמם הם אלו שיוצרים את ההבדל כאשר הם מתייחסים בצורה שונה לגמרי לכל שיטה (GET מחזיר מידע, POST מעלה מידע).
  • כתובת המשאב המבוקש המציינת לשרת לאיזה אובייקט נרצה לגשת. הכתובת יכולה להיות אבסולוטית, לדוגמה http://icode.co.il/file.html או יחסית: /file1
  • גרסת הפרוטוקול לפיה נבנתה הבקשה, לרוב HTTP 1.1

הנה דוגמה לשורת פתיחה מלאה:

GET /index.html HTTP/1.1

כדי להמחיש כמה פשוט לקרוא פרוטוקול HTTP בקוד, להלן דוגמה לשורת פייתון המחלצת את שורת הפתיחה בבקשת HTTP, ומפרקת אותה (בהנחה שמבנה הבקשה תקין) למרכיביה:

method, url, version = HTTP_REQUEST.split('\r\n')[0].strip().split(' ')

שורות הכותרת

החלק השני בכל בקשת HTTP הוא אוסף של שורות כותרת (header lines) המכילות מידע נוסף על הבקשה (metadata) הדרוש לשרת כדי להשלים אותה. כל כותרת מורכבת משם וערך המופרדים בנקודותיים ורווח. לדוגמה:

Accept-Language: he

שורת כותרת זאת, ששמה Accept-Language וערכה he (קיצור של Hebrew) מציינת לשרת שהתשובה לבקשה יכולה להכיל מידע בעברית.

ישנם שמות כותרות המוגדרים כחלק מהפרוטוקול, אך למעשה אין הגבלה על השמות וניתן להעביר כל כותרת שרוצים, בתנאי ששני הצדדים מכירים אותה (אחרת אחד הצדדים פשוט יתעלם ממנה). תכונה זו של HTTP מאפשרת להרחיב את הפרוטוקול בקלות לשימושים שונים.

החל מגרסה 1.1 קיימת כותרת, Host, אותה חובה להעביר בכל בקשה. כותרת זאת היא כתובת האתר אליו מפנים את הבקשה, והיא נדרשת כדי שהשרת יוכל לתמוך בכמה אתרים במקביל (דוגמה בהמשך).

גוף הבקשה

חלק מהבקשות יכולות להכיל מידע אותו מבקש הלקוח להעביר לשרת. מידע זה נקרא גוף הבקשה והוא החלק היחיד בבקשת HTTP שלא חייב להיות טקסטואלי (כלומר הוא יכול להיות גם מידע בינארי כמו קובץ תמונה).

לדוגמה בבקשת POST גוף הבקשה עשוי להכיל את המידע אותו נרצה לשמור בשרת (פרטי טופס, קובץ וכד'). בקשות GET לרוב לא מכילות גוף.

כאשר מעבירים את גוף הבקשה, יש להוסיף את הכותרת Content-Length המציינת את אורך גוף הבקשה, בבתים (דוגמה בהמשך).

דוגמאות לבקשת HTTP

להלן דוגמה לבקשת GET המכילה שתי כותרות:

GET /index.html HTTP/1.1
Host: icode.co.il
Accept-Encoding: gzip, deflate

בדוגמה זאת אנו מבקשים מהשרת להחזיר את העמוד index.html מהאתר icode.co.il, ומוסיפים שאנו תומכים בקבלת מידע דחוס מסוג gzip.

והנה דוגמה לבקשת POST המכילה גוף. שימו לב לשורת הרווח בין הכותרות לגוף הבקשה:

POST /submit.html HTTP/1.1
Host: icode.co.il
Content-Length: 10

name=tzach

בדוגמה האחרונה, הכותרת Content-Length מכילה את האורך של גוף הבקשה בבתים, במקרה שלנו זהו האורך של הטקסט name=tzach, שהוא בדיוק 10 בתים.

בניית בקשת HTTP בקוד

בדומה לקריאת בקשת HTTP, גם הבנייה שלה היא קלה מאוד לקידוד. הנה דוגמה לבניית בקשה, בהינתן כל החלקים שלה:

request = "{method} {url} HTTP/1.1\r\n{headers}\r\n\r\n{body}".format(
    method = METHOD,
    url = URL,
    headers = '\r\n'.join('%s: %s' % item for item in HEADERS_DICT),
    body = BODY)

מבנה תגובת HTTP

השלב השני והאחרון בכל שיחת HTTP הוא התגובה (response) אותה שולח השרת ללקוח עבור הבקשה. התגובה, כשמה כן היא, מכילה את התשובה של השרת לבקשה. למשל אם הלקוח ביקש קובץ מסויים מהשרת, התגובה עשויה להכיל את תוכן הקובץ, או הודעת שגיאה (למשל במקרה שהקובץ לא קיים).

פרוטוקול HTTP - תגובה
לאחר קבלת הבקשה, השרת שולח תגובה בחזרה ללקוח

בדומה לבקשה, גם התגובה מורכבת משלושת החלקים:

  • שורת מצב המכילה את גרסת הפרוטוקול בה השתמש השרת וקוד מצב.
  • שורות כותרת באותו מבנה כמו בבקשה (ולאחריהן שורת רווח)
  • גוף התגובה (לא חובה)

קוד המצב

נסתכל על דוגמה לשורת מצב החוזרת מהשרת:

HTTP/1.1 200 OK

בדוגמה זאת הערך 200 (בתוספת המחרוזת OK) נקרא קוד המצב (status code) והוא מציין את התוצאה של נסיון השרת לטפל בבקשה שהתקבלה, לדוגמה: הצלחה (200), משאב לא נמצא (404) או שגיאה כללית (500).

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

  • 1XX – בקשה התקבלה אך אין עדיין תגובה (מאוד נדיר וכמעט לא בשימוש).
  • 2XX – הצלחה. הערך הנפוץ ביותר בקבוצה זאת הוא 200 OK, המציין תגובה תקינה לבקשה, אך קיימים גם ערכים אחרים כגון 201 (נוצר).
  • 3XX – המשאב נמצא אבל הועבר. לרוב התגובה תהיה 301 (הועבר באופן קבוע) או 302 (הועבר זמנית).
  • 4XX – קיימת שגיאה בבקשה עצמה, למשל התבקש משאב לא קיים (404), אין הרשאות (403), יש צורך לבצע אימות (401) ועוד.
  • 5XX – התרחשה שגיאה בשרת. שני הערכים הנפוצים הם 500 (שגיאה כללית) ו-503 (שגיאה זמנית).

שורות הכותרת

בדומה לבקשת HTTP, גם התגובה מכילה שורות כותרת (header lines), אשר כל אחת מהן מורכבת משם וערך. לרוב, כל כותרת תהיה אחת מהכותרות הסטנדרטיות בפרוטוקול HTTP, אך למעשה ניתן להחזיר כל כותרת שבא לנו, או אפילו לא להחזיר כותרות כלל.

חלק גדול מכותרות התגובה מתחלקות לקבוצות הבאות:

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

להלן דוגמה לתגובה:

HTTP/1.1 200 OK
Date: Fri, 8 Jan 2016 11:29:34 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 31

<html><body>Hello</body></html>

תגובה זו מכילה שלוש שורות כותרת:

  • Date – שערכה הוא הזמן בשרת בעת יצירת התגובה.
  • Content-Type – שמציינת את סוג המידע אותו מכיל גוף התגובה, במקרה שלנו מדובר בקוד HTML בקידוד UTF-8.
  • Content-Length – אורך גוף התגובה בבתים.

גוף התגובה

לעתים קרובות השרת ירצה להחזיר ללקוח מידע כלשהו בתגובה לבקשה. מידע זה נקרא גוף התגובה, ויכול להיות מכל סוג. דוגמאות נפוצות לגוף התגובה הן קוד HTML, CSS או Javascript, מידע כלשהו בקידוד JSON או XML, קובץ בינארי כגון תמונה, gzip או מסמך.

במידה והתגובה מכילה גוף, היא חייבת להכיל גם את הכותרת Content-Length, שתציין את אורך הגוף בבתים. במידה והכותרת לא קיימת, הלקוח עלול להחשיב את התגובה כלא חוקית ולהתעלם ממנה.

נבחן דוגמה נוספת לתגובה:

HTTP/1.1 500 Internal Server Error
Content-Type: application/json
Content-Length: 23

{"error": "unexpected"}

בדוגמה זאת השרת החזיר קוד מצב 500 המציין שגיאה בשרת. בנוסף, גוף התגובה מטיפוס JSON מכיל מידע נוסף על השגיאה. שימו לב לכותרת Content-Type המכילה את סוג המידע החוזר (הנקרא לפעמים גם MIME type או Media type).

אז מה עושים עם כל המידע הזה?

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

לדוגמה, ניתן להשתמש בידע שצברנו על הפרוטוקול כדי לנתח את תעבורת הרשת של אתר אינטרנט מסויים. יש הרבה כלים המאפשרים לעשות זאת כמו Wireshark, Fiddler, אך האהוב עליי הוא Chrome DevTools המובנה בדפדפן כרום (גם דפדפנים אחרים מכילים מודול כזה, אך לטעמי כרום הוא הטוב ביותר).

מתוך דפדפן כרום, לחצו F12 לפתיחת תפריט המפתחים, ולאחר מכן לחצו על לשונית Network וטענו מחדש את הדף:

Chrome Dev Tools

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

מה הלאה?

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