ברוכים הבאים ליחידת "תכנות רשת" בקורס "תכנות מתקדם בשפת Java" (20554). יחידה זו חיונית להבנת האופן שבו יישומים שונים, הפועלים על מחשבים נפרדים, יכולים לתקשר ביניהם דרך הרשת. נצלול לעולם התקשורת מבוססת שקעים (Sockets), נכיר את הפרוטוקולים המרכזיים TCP ו-UDP, ונלמד כיצד לבנות יישומים מבוזרים ב-Java שיכולים לדבר זה עם זה.
יסודות תקשורת רשת ב-Java
תכנות רשת מאפשר ליישומים להחליף נתונים על גבי רשת מחשבים. בבסיסו עומד מודל הלקוח-שרת, שבו יישום אחד (השרת) ממתין לבקשות, ויישום אחר (הלקוח) יוזם את התקשורת.
פרוטוקולי תקשורת: TCP ו-UDP
כדי שיישומים יוכלו לתקשר, עליהם להסכים על סט כללים – פרוטוקול. שני הפרוטוקולים המרכזיים בשכבת התעבורה הם TCP ו-UDP, ולכל אחד מהם מאפיינים שונים המשפיעים על בחירת השימוש בו.
TCP (Transmission Control Protocol)
חיבורי: דורש יצירת חיבור (לחיצת יד משולשת) לפני העברת נתונים.
אמין: מבטיח הגעת נתונים, סדר נכון, וטיפול בשגיאות (שידור חוזר).
מבוסס זרם (Stream-based): הנתונים נשלחים כזרם רציף של בתים.
איטי יחסית: בשל תקורה של אמינות ובקרת זרימה.
שימושים: גלישה באינטרנט (HTTP/HTTPS), העברת קבצים (FTP), דואר אלקטרוני (SMTP).
UDP (User Datagram Protocol)
חסר חיבור (Connectionless): שולח חבילות נתונים (דאטאגרמות) ללא יצירת חיבור מוקדמת.
לא אמין: לא מבטיח הגעת נתונים, סדר, או טיפול בשגיאות.
מבוסס חבילות (Datagram-based): כל חבילה עצמאית.
מהיר: בשל מיעוט תקורה.
שימושים: סטרימינג וידאו/אודיו, משחקי רשת, DNS.
שקעים (Sockets) ב-Java
שקע הוא נקודת קצה לתקשורת בין תהליכים (Inter-Process Communication - IPC) ברשת. ב-Java, אנו משתמשים במחלקות מיוחדות ליצירה וניהול של שקעים.
שקעי שרת ולקוח ב-Java
עבור תקשורת TCP, נשתמש בשתי מחלקות עיקריות:
ServerSocket: מחלקה בצד השרת, המאזינה לחיבורים נכנסים בפורט ספציפי. כאשר לקוח מתחבר, ה-ServerSocketיוצר אובייקטSocketחדש לטיפול בחיבור זה.Socket: מחלקה המשמשת הן בצד הלקוח (ליצירת חיבור לשרת) והן בצד השרת (לייצוג החיבור הספציפי עם לקוח לאחר שנוצר). דרך אובייקט זה מתבצעת העברת הנתונים.
תהליך תקשורת TCP בסיסי
צד השרת:
- יצירת
ServerSocketבפורט מסוים:new ServerSocket(port). - האזנה לחיבורים נכנסים:
serverSocket.accept(). שיטה זו חוסמת עד שלקוח מתחבר, ומחזירה אובייקטSocketחדש. - קבלת זרמי קלט/פלט מה-
Socket:socket.getInputStream()ו-socket.getOutputStream(). - קריאה וכתיבה של נתונים דרך הזרמים.
- סגירת ה-
Socketוה-ServerSocket.
צד הלקוח:
- יצירת
Socketוחיבור לשרת:new Socket(serverAddress, port). - קבלת זרמי קלט/פלט מה-
Socket:socket.getInputStream()ו-socket.getOutputStream(). - קריאה וכתיבה של נתונים דרך הזרמים.
- סגירת ה-
Socket.
טיפול בחיבורים מרובים וניהול משאבים
יישום שרת טיפוסי צריך להיות מסוגל לטפל במספר לקוחות במקביל. ב-Java, הדבר מושג לרוב באמצעות תהליכונים (Threads).
טיפול בלקוחות מרובים באמצעות תהליכונים
כאשר השרת מקבל חיבור חדש (serverSocket.accept()), הוא יכול להקצות תהליכון חדש (או להשתמש ב-ExecutorService) לטיפול בלקוח זה. כך, השרת הראשי יכול להמשיך להאזין לחיבורים חדשים, בעוד התהליכונים הנפרדים מטפלים בתקשורת עם הלקוחות הקיימים.
try-catch-finally או, עדיף, ב-try-with-resources כדי להבטיח שכל המשאבים (שקעים, זרמים) נסגרים כראוי, גם במקרה של שגיאה. אי סגירת משאבים עלולה להוביל לדליפות זיכרון, חסימת פורטים, וחוסר יציבות של היישום.שאלות לדיון
- תאר מצב שבו היית בוחר להשתמש בפרוטוקול UDP על פני TCP, והסבר מדוע.
- כיצד ניתן להבטיח ששרת Java יוכל לטפל בעשרות לקוחות בו-זמנית מבלי לקרוס או להאט משמעותית?
- אילו אתגרים אבטחתיים קיימים בתכנות רשת מבוסס שקעים, וכיצד ניתן להתמודד איתם ב-Java?
- הסבר את חשיבות השימוש ב-
try-with-resourcesבעבודה עם שקעים וזרמים בתכנות רשת.
נקודות לתשובת מודל
- בחירת פרוטוקול: UDP מתאים ליישומים הדורשים מהירות וסבילות לאובדן נתונים (למשל, סטרימינג בזמן אמת, משחקים מקוונים), שבהם שידור חוזר של חבילות עלול לגרום לעיכובים בלתי קבילים. TCP מתאים ליישומים הדורשים אמינות מלאה וסדר נתונים (למשל, העברת קבצים, דואר אלקטרוני).
- טיפול בלקוחות מרובים: שימוש בתהליכונים (Threads) או ב-
ExecutorService. כל חיבור חדש (Socket) מטופל על ידי תהליכון נפרד, המאפשר לשרת להמשיך להאזין לחיבורים נוספים. יש לדון בניהול משאבי תהליכונים (pool size). - אתגרים אבטחתיים: האזנה (eavesdropping), התקפות מניעת שירות (DoS), הזרקת נתונים זדוניים. התמודדות: שימוש ב-SSL/TLS (למשל,
SSLSocket/SSLServerSocket) להצפנה ואימות, סינון קלט, הגבלת קצב חיבורים. - חשיבות
try-with-resources: מבטיח שמשאבים (כמוSocket,ServerSocket,InputStream,OutputStream) ייסגרו אוטומטית בסיום הבלוק, גם אם מתרחשת חריגה. זה מונע דליפות משאבים, חסימת פורטים, ומבטיח יציבות יישומים.