טיפול פשוט בקצבי זמן ברשת

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

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

  • פעולות חסימה : קריאה או כתיבה של דוכנים, פעולה ממתינה עד שמכשיר ה- I / O יהיה מוכן
  • פעולות ללא חסימה : נעשה קריאה או כתיבה, הפעולה מבוטלת אם מכשיר ה- I / O אינו מוכן

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

לעתים קרובות משתמשים בפתרון זה, אך קיימת אלטרנטיבה הרבה יותר פשוטה. Java גם תומך nonblocking רשת I / O, שיכול להיות מופעל על שום Socket, ServerSocketאו DatagramSocket. ניתן לציין את משך הזמן המרבי שפעולת קריאה או כתיבה תיעצר לפני שתחזיר את השליטה ליישום. עבור לקוחות רשת זה הפיתרון הקל ביותר ומציע קוד פשוט יותר לניהול.

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

קלט / פלט של רשת ללא חסימה

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

כאשר Java 1.1 שוחרר, הוא כלל שינויי API java.netבחבילה שאפשרו למתכנתים לציין אפשרויות שקע. אפשרויות אלה מקנות למתכנתים שליטה רבה יותר בתקשורת שקעים. אפשרות אחת במיוחד SO_TIMEOUTהיא שימושית ביותר מכיוון שהיא מאפשרת למתכנתים לציין את משך הזמן שפעולת קריאה תחסום. אנו יכולים לציין עיכוב קצר, או בכלל, ולהפוך את קוד הרשת שלנו ללא חסימה.

בואו נסתכל איך זה עובד. שיטה חדשה setSoTimeout ( int )נוספה לשיעורי השקע הבאים:

  • java.net.Socket
  • java.net.DatagramSocket
  • java.net.ServerSocket

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

  • ServerSocket.accept()
  • SocketInputStream.read()
  • DatagramSocket.receive()

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

// צור שקע דאטאגרם ביציאה 2000 כדי להאזין לחבילות UDP נכנסות DatagramSocket dgramSocket = DatagramSocket חדש (2000); // השבת חסימת פעולות קלט / פלט, על ידי ציון זמן פסק זמן של חמש שניות dgramSocket.setSoTimeout (5000);

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

קטע הקוד הבא מראה כיצד לטפל בפעולת פסק זמן בקריאה משקע TCP:

// הגדר את זמן הקצאת השקע לחיבור של עשר שניות. SetSoTimeout (10000); נסה {// צור DataInputStream לקריאה משקע DataInputStream din = DataInputStream חדש (connection.getInputStream ()); // קרא נתונים עד סוף הנתונים עבור (;;) {קו מחרוזת = din.readLine (); אם (שורה! = null) System.out.println (שורה); עוד נשבר; }} // חריג שנזרק כאשר מתרחש פסק זמן לרשת לתפוס (InterruptedIOException iioe) {System.err.println ("זמן קצוב למארח מרחוק במהלך פעולת הקריאה"); } // חריג שנזרק כאשר שגיאת קלט / פלט כללית ברשת מתרחשת תפוס (IOException ioe) {System.err.println ("שגיאת קלט / פלט ברשת -" + ioe); }

עם רק כמה שורות קוד נוספות try {}לחסימת תפיסה, קל מאוד לתפוס פסק זמן של רשת. יישום יכול להגיב למצב מבלי להיתקע בעצמו. לדוגמה, זה יכול להתחיל בהודעה למשתמש, או בניסיון ליצור חיבור חדש. כאשר משתמשים בשקעי דאטאגרמה, השולחים חבילות מידע מבלי להבטיח מסירה, אפליקציה יכולה להגיב לפסק זמן של רשת על ידי שליחה חוזרת של חבילה שאבדה במעבר. יישום תמיכת פסק זמן זה לוקח מעט מאוד זמן ומוביל לפיתרון נקי מאוד. ואכן, הפעם היחידה בה קלט / פלט ללא חסימה אינו הפיתרון האופטימלי היא כאשר אתה צריך לאתר גם פסק זמן בפעולות חיבור, או כאשר סביבת היעד שלך אינה תומכת ב- Java 1.1.

טיפול בפסק זמן בפעולות חיבור

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

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

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

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

// התחבר לשרת מרוחק לפי שם מארח, עם חיבור שקע פסק זמן של ארבע שניות = TimedSocket.getSocket ("server.my-network.net", 23, 4000); 

אם הכל ילך כשורה, שקע יוחזר, בדיוק כמו java.net.Socketהקבלנים הסטנדרטיים . אם לא ניתן ליצור את החיבור לפני פסק הזמן שצוין, השיטה תיפסק ותשליך java.io.InterruptedIOException, בדיוק כמו פעולות אחרות לקריאת שקע כאשר נקבע פסק זמן setSoTimeoutבשיטה. די קל, הא?

מקפיד קוד רשת רב-הברגה למחלקה אחת

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

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

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

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

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

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

for (;;) { // Check to see if a connection is established if (st.isConnected()) { // Yes ... assign to sock variable, and break out of loop sock = st.getSocket(); break; } else { // Check to see if an error occurred if (st.isError()) { // No connection could be established throw (st.getException()); } try { // Sleep for a short period of time Thread.sleep ( POLL_DELAY ); } catch (InterruptedException ie) {} // Increment timer timer += POLL_DELAY; // Check to see if time limit exceeded if (timer > delay) { // Can't connect to server throw new InterruptedIOException ("Could not connect for " + delay + " milliseconds"); } } } 

Inside the blocked thread

While the connection is regularly polled, the second thread attempts to create a new instance of java.net.Socket. Accessor methods are provided to determine the state of the connection, as well as to get the final socket connection. The SocketThread.isConnected() method returns a boolean value to indicate whether a connection has been established, and the SocketThread.getSocket() method returns a Socket. Similar methods are provided to determine if an error has occurred, and to access the exception that was caught.

כל השיטות הללו מספקות ממשק מבוקר SocketThreadלמופע, מבלי לאפשר שינוי חיצוני של משתני חברים פרטיים. דוגמת הקוד הבאה מציגה את run()שיטת השרשור . כאשר, ואם בונה השקעים מחזיר a Socket, הוא יוקצה למשתנה חבר פרטי, אליו נותנות שיטות הגישה. בפעם הבאה שתשאול מצב חיבור, SocketThread.isConnected()בשיטה, השקע יהיה זמין לשימוש. באותה טכניקה משתמשים בזיהוי שגיאות; אם java.io.IOExceptionהוא נתפס, זה יאוחסן חבר פרטי, אשר ניתן לגשת דרך isError()ו getException()הרצוע שיטות.