Java IO לעומת NIO

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

טיפול בקלט ופלט הם משימות נפוצות עבור מתכנתי Java. במדריך זה, נסתכל על מְקוֹרִי java.io (IO) ספריות וחדשות יותר java.nio (NIO) ספריות וכיצד הם נבדלים בעת תקשורת ברשת.

2. תכונות עיקריות

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

2.1. IO - java.io

ה java.io החבילה הוצגה ב- Java 1.0, עם קוֹרֵא הוצג בג'אווה 1.1. זה מספק:

  • InputStream ו OutputStream - המספקים נתונים אחד בכל פעם
  • קוֹרֵא ו סוֹפֵר - עטיפות נוחות לנחלים
  • מצב חסימה - לחכות להודעה מלאה

2.2. NIO - java.nio

ה java.nio החבילה הוצגה ב- Java 1.4 ומתעדכן ב- Java 1.7 (NIO.2) עם פעולות קבצים משופרות ו- ASynchronousSocketChannel. זה מספק:

  • בַּלָםלקרוא נתחי נתונים בכל פעם
  • CharsetDecoder - למיפוי בתים גולמיים לתווים קריאים
  • עָרוּץ - לתקשורת עם העולם החיצון
  • בוחר - כדי לאפשר ריבוב על a SelectableChannel ולספק גישה לכל אחד עָרוּץs שמוכנים לקלט / פלט
  • מצב שאינו חוסם - לקרוא כל מה שמוכן

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

3. הגדר את שרת הבדיקה שלנו

כאן נשתמש ב- WireMock כדי לדמות שרת אחר כדי שנוכל להריץ את הבדיקות שלנו באופן עצמאי.

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

בואו נוסיף את התלות של Maven עבור WireMock עם מִבְחָן תְחוּם:

 com.github.tomakehurst wiremock-jre8 2.26.3 מבחן 

בשיעור מבחן, בואו נגדיר JUnit @כְּלָל כדי להפעיל את WireMock בנמל חופשי. לאחר מכן נגדיר אותו כך שיחזיר לנו תגובת HTTP 200 כשאנו מבקשים משאב מוגדר מראש, עם גוף ההודעה כטקסט כלשהו בפורמט JSON:

@Rule WireMockRule ציבורי wireMockRule = WireMockRule חדש (wireMockConfig (). DynamicPort ()); מחרוזת פרטית REQUESTED_RESOURCE = "/ test.json"; @ לפני ההתקנה הריקלית הציבורית () {stubFor (get (urlEqualTo (REQUESTED_RESOURCE)) .willReturn (aResponse () .withStatus (200) .withBody ("{\" response \ ": \" זה עבד! \ "}")) ); }

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

4. חסימת IO - java.io

בואו נסתכל כיצד פועל מודל ה- IO החוסם המקורי על ידי קריאת נתונים מאתר אינטרנט. נשתמש ב- java.net. שקע כדי לקבל גישה לאחת מיציאות מערכת ההפעלה.

4.1. שלח בקשה

בדוגמה זו, ניצור בקשת GET כדי לאחזר את המשאבים שלנו. ראשית, בואו ליצור שֶׁקַע כדי לגשת לנמל ששרת WireMock שלנו מאזין ל:

שקע שקע = שקע חדש ("localhost", wireMockRule.port ())

עבור תקשורת HTTP או HTTPS רגילה, היציאה תהיה 80 או 443. עם זאת, במקרה זה אנו משתמשים wireMockRule.port () כדי לגשת ליציאה הדינמית שהקמנו קודם.

עכשיו בואו לפתוח OutputStream על השקע, עטוף ב OutputStreamWriter ולהעביר אותו לא PrintWriter לכתוב את המסר שלנו. ובואו לוודא שאנחנו שוטפים את המאגר כך שהבקשה שלנו תישלח:

OutputStream clientOutput = socket.getOutputStream (); כותב PrintWriter = PrintWriter חדש (OutputStreamWriter חדש (clientOutput)); writer.print ("GET" + TEST_JSON + "HTTP / 1.0 \ r \ n \ r \ n"); writer.flush ();

4.2. המתן לתגובה

בואו לפתוח InputStreamעל השקע כדי לגשת לתגובה, קרא את הזרם באמצעות BufferedReader, ולאחסן אותו ב- StringBuilder:

InputStream serverInput = socket.getInputStream (); קורא BufferedReader = BufferedReader חדש (InputStreamReader חדש (קלט שרת)); StringBuilder ourStore = StringBuilder חדש ();

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

עבור (קו מחרוזת; (שורה = reader.readLine ())! = null;) {ourStore.append (שורה); ourStore.append (System.lineSeparator ()); }

5. IO שאינו חוסם - java.nio

עכשיו, בואו נסתכל איך ניו מודל ה- IO הלא חוסם של החבילה עובד עם אותה דוגמא.

הפעם, אנחנו ליצור java.nio.channel.SocketChannel כדי לגשת לנמל בשרת שלנו במקום a java.net. שקע, ולהעביר אותו an InetSocketAddress.

5.1. שלח בקשה

ראשית, בואו נפתח את שלנו SocketChannel:

כתובת InetSocketAddress = InetSocketAddress חדש ("localhost", wireMockRule.port ()); SocketChannel socketChannel = SocketChannel.open (כתובת);

ועכשיו, בואו נקבל UTF-8 סטנדרטי ערכת לקודד ולכתוב את ההודעה שלנו:

ערכת Charset = StandardCharsets.UTF_8; socket.write (charset.encode (CharBuffer.wrap ("GET" + REQUESTED_RESOURCE + "HTTP / 1.0 \ r \ n \ r \ n")));

5.2. קרא את התגובה

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

מכיוון שנעבד טקסט, נצטרך ByteBuffer עבור הבתים הגולמיים ו- CharBuffer עבור הדמויות שהומרו (בסיוע א CharsetDecoder):

ByteBuffer byteBuffer = ByteBuffer.allocate (8192); CharsetDecoder charsetDecoder = charset.newDecoder (); CharBuffer charBuffer = CharBuffer.allocate (8192);

שֶׁלָנוּ CharBuffer יישאר מקום שנותר אם הנתונים נשלחים בערכת תווים מרובת בתים.

שים לב שאם אנו זקוקים לביצועים מהירים במיוחד, אנו יכולים ליצור a MappedByteBuffer בזיכרון מקורי באמצעות ByteBuffer.allocateDirect (). עם זאת, במקרה שלנו, שימוש לְהַקְצוֹת() מהגיב הסטנדרטי מהיר מספיק.

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

אז בואו קרא מתוך שלנו SocketChannel, מעביר את זה שלנו ByteBuffer לאחסון הנתונים שלנו. שֶׁלָנוּ לקרוא מ ה SocketChannel יסיים עם שלנו ByteBufferשל המיקום הנוכחי מוגדר לבית הבא לכתוב אליו (ממש אחרי בתו האחרון שנכתב), אך עם הגבול ללא שינוי:

socketChannel.read (byteBuffer)

שֶׁלָנוּ SocketChannel.read () מחזיר את מספר הבתים שנקראו זה יכול להיכתב במאגר שלנו. זה יהיה -1 אם השקע נותק.

כאשר למאגר שלנו לא נותר מקום כי עדיין לא עיבדנו את כל הנתונים שלו, אז SocketChannel.read () יחזיר אפס בתים שנקראו אבל שלנו buffer.position () עדיין יהיה גדול מאפס.

כדי לוודא שנתחיל לקרוא מהמקום הנכון במאגר, נשתמש Buffer.flip() כדי להגדיר את שלנו ByteBufferהמיקום הנוכחי לאפס והגבול שלו לבית האחרון שנכתב על ידי ה- SocketChannel. לאחר מכן נשמור את תוכן החיץ באמצעות שלנו storeBufferContents שיטה, שנראה בהמשך. לבסוף, נשתמש buffer.compact () לדחוס את המאגר ולהגדיר את המיקום הנוכחי מוכן לקריאה הבאה שלנו מתוך SocketChannel.

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

בעוד (socketChannel.read (byteBuffer)! = -1 || byteBuffer.position ()> 0) {byteBuffer.flip (); storeBufferContents (byteBuffer, charBuffer, charsetDecoder, ourStore); byteBuffer.compact (); }

ובל נשכח סגור() השקע שלנו (אלא אם כן פתחנו אותו בבלוק ניסיון עם משאבים):

socketChannel.close ();

5.3. אחסון נתונים מהמאגר שלנו

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

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

אז עכשיו, בואו נשתמש בהשלמה שלנו storeBufferContents () שיטת העברה במאגרים שלנו, CharsetDecoder, ו StringBuilder:

void storeBufferContents (ByteBuffer byteBuffer, CharBuffer charBuffer, CharsetDecoder charsetDecoder, StringBuilder ourStore) {charsetDecoder.decode (byteBuffer, charBuffer, נכון); charBuffer.flip (); ourStore.append (charBuffer); charBuffer.clear (); }

6. מסקנה

במאמר זה ראינו כיצד מְקוֹרִי java.io בלוקים מודליים, ממתין לבקשה ושימושים זרםכדי לתפעל את הנתונים שהוא מקבל.

בניגוד, ה java.nio ספריות מאפשרות תקשורת שאינה חוסמת באמצעות בַּלָםs ו- עָרוּץוהוא יכול לספק גישה ישירה לזיכרון לביצועים מהירים יותר. עם זאת, עם מהירות זו מגיעה המורכבות הנוספת של טיפול במאגרים.

כרגיל, הקוד של מאמר זה זמין באתר GitHub.