מבוא לבורר Java NIO

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

במאמר זה נחקור את חלקי המבוא של Java NIO בוחר רְכִיב.

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

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

2. מדוע להשתמש בבורר?

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

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

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

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

3. התקנה

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

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

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

4. יצירת בורר

ניתן ליצור בורר על ידי הפעלת הסטטי לִפְתוֹחַ שיטת ה- בוחר class, שישתמש בספק הבורר המוגדר כברירת מחדל של המערכת ליצירת בורר חדש:

בורר בורר = Selector.open ();

5. רישום ערוצים ניתנים לבחירה

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

אך לפני ערוץ נרשם בבורר, עליו להיות במצב שאינו חוסם:

channel.configureBlocking (false); מקש SelectionKey = channel.register (בורר, SelectionKey.OP_READ);

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

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

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

  • לְחַבֵּר כאשר לקוח מנסה להתחבר לשרת. מיוצג על ידי SelectionKey.OP_CONNECT
  • לְקַבֵּל כאשר השרת מקבל חיבור מלקוח. מיוצג על ידי SelectionKey.OP_ACCEPT
  • לקרוא כאשר השרת מוכן לקריאה מהערוץ. מיוצג על ידי SelectionKey.OP_READ
  • לִכתוֹב כאשר השרת מוכן לכתוב לערוץ. מיוצג על ידי SelectionKey.OP_WRITE

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

6. ה SelectionKey לְהִתְנַגֵד

כפי שראינו בסעיף הקודם, כאשר נרשם ערוץ עם בורר, אנו מקבלים SelectionKey לְהִתְנַגֵד. אובייקט זה מחזיק נתונים המייצגים את רישום הערוץ.

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

6.1. מערך הריבית

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

ראשית, יש לנו את הריבית שהגדרת SelectionKeyשל interestOps שיטה. ואז יש לנו את האירוע קבוע ב SelectionKey הסתכלנו קודם.

כאשר אנו ושני הערכים הללו, אנו מקבלים ערך בוליאני המספר לנו אם צופים באירוע או לא:

int interestSet = selectionKey.interestOps (); בוליאני isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT; בוליאני isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT; בוליאני isInterestedInRead = interestSet & SelectionKey.OP_READ; בוליאני isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;

6.2. הסט המוכן

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

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

דרך חלופית וקצרה יותר לעשות זאת היא שימוש SelectionKey 'שיטות הנוחות לאותה מטרה:

selectionKey.isAcceptable (); selectionKey.isConnectable (); selectionKey.isReadable (); selectionKey.isWriteable ();

6.3. הערוץ

גישה לערוץ הנצפה מה- SelectionKey האובייקט פשוט מאוד. אנחנו פשוט קוראים עָרוּץ שיטה:

ערוץ ערוץ = key.channel ();

6.4. הסלקטור

בדיוק כמו להשיג ערוץ, קל מאוד להשיג את הערוץ בוחר אובייקט מה SelectionKey לְהִתְנַגֵד:

בורר בורר = key.selector ();

6.5. הצמדת חפצים

אנו יכולים לצרף אובייקט ל- a SelectionKey. לפעמים אולי נרצה לתת לערוץ מזהה מותאם אישית או לצרף כל סוג של אובייקט Java שנרצה לעקוב אחריו.

הצמדת אובייקטים היא דרך שימושית לעשות זאת. כך אתה מצרף ומקבל חפצים מ- SelectionKey:

key.attach (אובייקט); אובייקט אובייקט = key.attachment ();

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

מקש SelectionKey = channel.register (בורר, SelectionKey.OP_ACCEPT, אובייקט);

7. בחירת מקשים לערוץ

עד כה בדקנו כיצד ליצור בורר, לרשום אליו ערוצים ולבדוק את המאפיינים של ה- SelectionKey אובייקט המייצג את רישום הערוץ לבורר.

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

int ערוצים = selector.select ();

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

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

הגדר selectKeys = selector.selectedKeys ();

הסט שקיבלנו הוא של SelectionKey אובייקטים, כל מקש מייצג ערוץ רשום שמוכן לפעולה.

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

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

8. דוגמה מלאה

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

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

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

8.1. השרת

הנה הקוד שלנו עבור EchoServer.java:

מחלקה ציבורית EchoServer {גמר סטטי פרטי פרטי מחרוזת POISON_PILL = "POISON_PILL"; סטטי ציבורי ריק ריק (String [] args) זורק IOException {בורר בורר = Selector.open (); ServerSocketChannel serverSocket = ServerSocketChannel.open (); serverSocket.bind (InetSocketAddress חדש ("localhost", 5454)); serverSocket.configureBlocking (false); serverSocket.register (בורר, SelectionKey.OP_ACCEPT); חיץ ByteBuffer = ByteBuffer.allocate (256); בעוד (נכון) {selector.select (); הגדר selectKeys = selector.selectedKeys (); איטרטור איטר = selectKeys.iterator (); בעוד (iter.hasNext ()) {מפתח SelectionKey = iter.next (); אם (key.isAcceptable ()) {register (selector, serverSocket); } אם (key.isReadable ()) {answerWithEcho (חיץ, מפתח); } iter.remove (); }}} antid ריק סטטי answerWithEcho (חיץ ByteBuffer, מקש SelectionKey) זורק IOException {לקוח SocketChannel = (SocketChannel) key.channel (); client.read (חיץ); אם (מחרוזת חדשה (buffer.array ()). לקצץ (). שווה (POISON_PILL)) {client.close (); System.out.println ("לא מקבל יותר הודעות לקוח"); } אחר {buffer.flip (); client.write (חיץ); buffer.clear (); }} רישום ריק סטטי פרטי (בורר בורר, ServerSocketChannel serverSocket) זורק IOException {לקוח SocketChannel = serverSocket.accept (); client.configureBlocking (false); client.register (בורר, SelectionKey.OP_READ); } התחלה ציבורית סטטית ציבורית () זורקת IOException, InterruptedException {String javaHome = System.getProperty ("java.home"); מחרוזת javaBin = javaHome + File.separator + "bin" + File.separator + "java"; מחרוזת classpath = System.getProperty ("java.class.path"); מחרוזת className = EchoServer.class.getCanonicalName (); Builder ProcessBuilder = ProcessBuilder חדש (javaBin, "-cp", classpath, className); להחזיר builder.start (); }}

זה מה שקורה; אנו יוצרים a בוחר חפץ על ידי קריאה לסטטי לִפְתוֹחַ שיטה. לאחר מכן אנו יוצרים ערוץ גם על ידי קריאה לסטטי שלו לִפְתוֹחַ שיטה, במיוחד א ServerSocketChannel למשל.

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

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

אנחנו לא צריכים את SelectionKey מופע של ערוץ זה בשלב זה, כך שלא נזכור אותו.

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

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

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

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

הפעולה היחידה א ServerSocketChannel יכול להתמודד הוא לְקַבֵּל מבצע. כאשר אנו מקבלים את החיבור מלקוח, אנו מקבלים א SocketChannel אובייקט שעליו אנו יכולים לקרוא וכותב. הגדרנו אותו למצב שאינו חוסם ורושמים אותו לפעולת READ לבורר.

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

כאשר אנו רוצים לכתוב למאגר שממנו קראנו, עלינו להתקשר ל לְהַעִיף() שיטה.

לבסוף הגדרנו את המאגר למצב כתיבה על ידי קריאת ה- לְהַעִיף שיטה ופשוט כתוב לה.

ה הַתחָלָה() השיטה מוגדרת כך שניתן להתחיל את שרת ההד כתהליך נפרד במהלך בדיקת היחידות.

8.2. הלקוח

הנה הקוד שלנו עבור EchoClient.java:

Class class EchoClient {לקוח SocketChannel סטטי פרטי; מאגר ByteBuffer סטטי פרטי; מופע סטטי EchoClient סטטי; התחלת EchoClient סטטית ציבורית () {if (מופע == null) מופע = EchoClient חדש (); מופע חזרה; } עצירה בטלנית ציבורית סטטית () זורקת את IOException {client.close (); חיץ = null; } EchoClient פרטי () {נסה {client = SocketChannel.open (InetSocketAddress חדש ("localhost", 5454)); חיץ = ByteBuffer.allocate (256); } לתפוס (IOException e) {e.printStackTrace (); }} sendMessage מחרוזת ציבורית (מחרוזת msg) {buffer = ByteBuffer.wrap (msg.getBytes ()); תגובת מחרוזת = null; נסה את {client.write (buffer); buffer.clear (); client.read (חיץ); תגובה = מחרוזת חדשה (buffer.array ()). לקצץ (); System.out.println ("תגובה =" + תגובה); buffer.clear (); } לתפוס (IOException e) {e.printStackTrace (); } תשובת תשובה; }}

הלקוח פשוט יותר מהשרת.

אנו משתמשים בתבנית יחיד כדי לייצר אותה בתוך ה- הַתחָלָה שיטה סטטית. אנו קוראים לבנאי הפרטי משיטה זו.

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

לאחר מכן אנו יוצרים חיץ שאליו נוכל לכתוב וממנו נוכל לקרוא.

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

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

8.3. בדיקה

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

כעת אנו יכולים להריץ את המבחן:

מחלקה ציבורית EchoTest {שרת תהליכים; לקוח EchoClient; @ לפני הגדרת הריק הציבורי () זורק IOException, InterruptedException {server = EchoServer.start (); לקוח = EchoClient.start (); } @Test ציבורי בטל givenServerClient_whenServerEchosMessage_thenCorrect () {String resp1 = client.sendMessage ("שלום"); מחרוזת resp2 = client.sendMessage ("עולם"); assertEquals ("שלום", resp1); assertEquals ("עולם", resp2); } @ לאחר פירוק חלל פומבי () זורק את IOException {server.destroy (); EchoClient.stop (); }}

9. מסקנה

במאמר זה סקרנו את השימוש הבסיסי ברכיב Java NIO Selector.

קוד המקור השלם וכל קטעי הקוד עבור מאמר זה זמינים בפרויקט GitHub שלי.