מדריך לשקעי Java

1. סקירה כללית

התנאי שֶׁקַע תִכנוּת מתייחס לתוכניות כתיבה המתבצעות על פני מספר מחשבים שבהם ההתקנים כולם מחוברים זה לזה באמצעות רשת.

ישנם שני פרוטוקולי תקשורת שבהם ניתן להשתמש בתכנות שקעים: פרוטוקול Datagram של משתמשים (UDP) ופרוטוקול בקרת העברה (TCP).

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

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

2. הגדרת פרויקט

Java מספקת אוסף של מחלקות וממשקים המטפלים בפרטי תקשורת ברמה נמוכה בין הלקוח לשרת.

אלה כלולים בעיקר ב java.net חבילה, לכן עלינו לבצע את הייבוא ​​הבא:

ייבא java.net. *;

אנחנו גם צריכים את java.io חבילה אשר נותנת לנו זרמי קלט ופלט לכתוב אליהם ולקרוא מהם תוך כדי תקשורת:

ייבא java.io. *;

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

3. דוגמה פשוטה

בואו נלכלך את הידיים הכי הרבה בסיסי של דוגמאות הכוללות לקוח ושרת. זה הולך להיות יישום תקשורת דו-כיווני שבו הלקוח מברך את השרת והשרת מגיב.

בואו ניצור את יישום השרת בכיתה שנקראת GreetServer.java עם הקוד הבא.

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

מחלקה ציבורית GreetServer {ServerSocket פרטי ServerSocket; לקוח שקע פרטי פרטית PrintWriter out; פרטי BufferedReader ב; התחלה בטלנית ציבורית (יציאת int) {serverSocket = ServerSocket חדש (יציאה); clientSocket = serverSocket.accept (); out = PrintWriter חדש (clientSocket.getOutputStream (), נכון); in = BufferedReader חדש (InputStreamReader חדש (clientSocket.getInputStream ())); ברכת מחרוזת = in.readLine (); אם ("שלום שרת". שווה (ברכה)) {out.println ("שלום לקוח"); } אחר {out.println ("ברכה לא מוכרת"); }} עצירה בטלנית ציבורית () {in.close (); out.close (); clientSocket.close (); serverSocket.close (); } ראשי ריק סטטי ציבורי (String [] args) {GreetServer server = GreetServer new (); server.start (6666); }}

בואו גם ליצור לקוח שנקרא GreetClient.java עם קוד זה:

כיתה ציבורית GreetClient {private Socket clientSocket; פרטית PrintWriter out; פרטי BufferedReader ב; התחל ריק מתחיל (Connection ip, int port) {clientSocket = Socket new (ip, port); out = PrintWriter חדש (clientSocket.getOutputStream (), נכון); in = BufferedReader חדש (InputStreamReader חדש (clientSocket.getInputStream ())); } מחרוזת ציבורי sendMessage (מחרוזת msg) {out.println (msg); מחרוזת resp = in.readLine (); החזרת resp; } stopConnection חלל ציבורי () {in.close (); out.close (); clientSocket.close (); }}

נתחיל את השרת; ב- IDE שלך אתה עושה זאת פשוט על ידי הפעלתו כיישום Java.

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

@Test הציבור בטל givenGreetingClient_whenServerRespondsWhenStarted_thenCorrect () {לקוח GreetClient = GreetClient חדש (); client.startConnection ("127.0.0.1", 6666); תגובת מחרוזת = client.sendMessage ("שלום שרת"); assertEquals ("שלום לקוח", תגובה); }

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

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

4. איך עובדים שקעים

נשתמש בדוגמה לעיל כדי לעבור בין חלקים שונים בסעיף זה.

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

4.1. השרת

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

ServerSocket serverSocket = ServerSocket חדש (6666);

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

Socket clientSocket = serverSocket.accept ();

כאשר קוד השרת נתקל ב- לְקַבֵּל שיטה, זה נחסם עד שלקוח מגיש אליו בקשת חיבור.

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

בשלב זה, החדש שֶׁקַע האובייקט מכניס את השרת לחיבור ישיר עם הלקוח, ואז נוכל לגשת לזרמי הפלט והקלט בכדי לכתוב ולקבל הודעות אל הלקוח וממנו בהתאמה:

PrintWriter out = PrintWriter חדש (clientSocket.getOutputStream (), נכון); BufferedReader in = BufferedReader חדש (InputStreamReader חדש (clientSocket.getInputStream ()));

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

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

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

עבור כל לקוח חדש, השרת זקוק לשקע חדש שהוחזר על ידי ה- לְקַבֵּל שִׂיחָה. ה serverSocket משמש להמשך האזנה לבקשות חיבור תוך טיפול בצרכי הלקוחות המחוברים. עדיין לא התיר לנו זאת בדוגמה הראשונה שלנו.

4.2. הלקוח

על הלקוח לדעת את שם המארח או ה- IP של המחשב עליו פועל השרת ואת מספר היציאה עליו השרת מאזין.

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

Socket clientSocket = שקע חדש ("127.0.0.1", 6666);

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

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

PrintWriter out = PrintWriter חדש (clientSocket.getOutputStream (), נכון); BufferedReader in = BufferedReader חדש (InputStreamReader חדש (clientSocket.getInputStream ()));

זרם הקלט של הלקוח מחובר לזרם הפלט של השרת, בדיוק כמו שזרם הקלט של השרת מחובר לזרם הפלט של הלקוח.

5. תקשורת רציפה

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

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

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

בואו ניצור שרת חדש בשם EchoServer.java שמטרתם היחידה היא להדהד את כל המסרים שהוא מקבל מלקוחות:

מחלקה ציבורית EchoServer {התחלת חלל ציבורית (יציאת int) {serverSocket = ServerSocket חדש (יציאה); clientSocket = serverSocket.accept (); out = PrintWriter חדש (clientSocket.getOutputStream (), נכון); in = BufferedReader חדש (InputStreamReader חדש (clientSocket.getInputStream ())); מחרוזת inputLine; ואילו ((inputLine = in.readLine ())! = null) {if (".". שווה (inputLine)) {out.println ("להתראות"); לשבור; } out.println (inputLine); }}

שימו לב שהוספנו תנאי סיום בו לולאת while יוצאת כאשר אנו מקבלים תו תקופתי.

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

ה EchoClient דומה ל GreetClientכדי שנוכל לשכפל את הקוד. אנו מפרידים ביניהם לשם הבהירות.

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

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

בואו ניצור להכין שיטה ליזום חיבור עם השרת:

@ לפני התקנת החלל הציבורי () {client = EchoClient חדש (); client.startConnection ("127.0.0.1", 4444); }

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

@ לאחר הריק הציבורי דמעה () {client.stopConnection (); }

בואו ונבדוק את שרת ההד שלנו בכמה בקשות:

@Test public void givenClient_whenServerEchosMessage_thenCorrect () {String resp1 = client.sendMessage ("שלום"); מחרוזת resp2 = client.sendMessage ("עולם"); מחרוזת resp3 = client.sendMessage ("!"); מחרוזת resp4 = client.sendMessage ("."); assertEquals ("שלום", resp1); assertEquals ("עולם", resp2); assertEquals ("!", resp3); assertEquals ("להתראות", resp4); }

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

6. שרת עם מספר לקוחות

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

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

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

המשמעות היא שהשרת שלנו יהיה חזק יותר ועמיד על פני מספר בקשות של לקוחות מרובים.

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

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

די לדבר, בואו ניצור שרת אחר שנקרא EchoMultiServer.java. בתוכו, ניצור מחלקת חוט מטפל לניהול התקשורת של כל לקוח בשקע שלו:

מחלקה ציבורית EchoMultiServer {שרת שקע פרטי ServerSocket; התחלה בטלנית ציבורית (יציאת int) {serverSocket = ServerSocket חדש (יציאה); בעוד EchoClientHandler (נכון) חדש (serverSocket.accept ()). התחל (); } עצירה בטלנית ציבורית () {serverSocket.close (); } מחלקה סטטית פרטית EchoClientHandler מאריך אשכול {private Socket clientSocket; פרטית PrintWriter out; פרטי BufferedReader ב; EchoClientHandler ציבורי (שקע שקע) {this.clientSocket = שקע; } הפעלה בטלנית ציבורית () {out = PrintWriter חדש (clientSocket.getOutputStream (), נכון); in = BufferedReader חדש (InputStreamReader חדש (clientSocket.getInputStream ())); מחרוזת inputLine; בעוד ((inputLine = in.readLine ())! = null) {if (".". שווה (inputLine)) {out.println ("ביי"); לשבור; } out.println (inputLine); } בקרוב(); out.close (); clientSocket.close (); }}

שימו לב שעכשיו אנחנו מתקשרים לְקַבֵּל בתוך א בזמן לוּלָאָה. בכל פעם שה- בזמן לולאה מבוצעת, היא חוסמת את לְקַבֵּל להתקשר עד שלקוח חדש מתחבר, ואז לשרשור המטפל, EchoClientHandler, נוצר עבור לקוח זה.

מה שקורה בתוך החוט הוא מה שעשינו בעבר ב EchoServer שם טיפלנו רק בלקוח יחיד. אז ה EchoMultiServer מאציל עבודה זו ל EchoClientHandler כדי שיוכל להמשיך להקשיב ללקוחות רבים יותר ב בזמן לוּלָאָה.

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

בואו נתחיל את השרת שלנו בשיטה העיקרית שלו ביציאה 5555.

לשם הבהרה, עדיין נניח מבחנים בסוויטה חדשה:

@Test ציבורי בטל שניתןClient1_whenServerResponds_thenCorrect () {EchoClient client1 = EchoClient חדש (); client1.startConnection ("127.0.0.1", 5555); מחרוזת msg1 = client1.sendMessage ("שלום"); מחרוזת msg2 = client1.sendMessage ("עולם"); מחרוזת לסיים = client1.sendMessage ("."); assertEquals (msg1, "שלום"); assertEquals (msg2, "עולם"); assertEquals (מסתיים, "ביי"); } @Test ציבורי בטל שניתןClient2_whenServerResponds_thenCorrect () {EchoClient client2 = EchoClient חדש (); client2.startConnection ("127.0.0.1", 5555); מחרוזת msg1 = client2.sendMessage ("שלום"); מחרוזת msg2 = client2.sendMessage ("עולם"); מחרוזת לסיים = client2.sendMessage ("."); assertEquals (msg1, "שלום"); assertEquals (msg2, "עולם"); assertEquals (מסתיים, "ביי"); }

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

7. מסקנה

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

קוד המקור המלא של המאמר נמצא - כרגיל - בפרויקט GitHub.