בניית מערכת צ'אט באינטרנט

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

דוגמה פשוטה זו של מערכת לקוח / שרת נועדה להדגים כיצד לבנות יישומים רק בעזרת הזרמים הזמינים ב- API הסטנדרטי. הצ'אט משתמש בשקעי TCP / IP כדי לתקשר, וניתן להטמיע אותו בקלות בדף אינטרנט. לעיון אנו מספקים סרגל צד המסביר רכיבי תכנות רשת Java הרלוונטיים ליישום זה. אם אתה עדיין מתחיל לעלות במהירות, הסתכל תחילה בסרגל הצד. אם אתה כבר בקיא בג'אווה, אתה יכול לקפוץ ישר פנימה ופשוט להפנות לסרגל הצד.

בניית לקוח צ'אט

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

ממשק ChatClient

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

ChatClient בכיתה

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

ייבא java.net. *; ייבא java.io. *; ייבא java.awt. *; מחלקה ציבורית ChatClient מרחיבה מסגרת מיישמת ניתן להפעיל {// public ChatClient (כותרת מחרוזת, InputStream i, OutputStream o) ... // הפעלה בטלנית ציבורית () ... // handle בוליאני ציבורי Event (אירוע e) ... // public סטטי ריק ריק (מחרוזת טוענת []) זורק IOException ...}

ChatClientבכיתה מרחיבה Frame; זה אופייני ליישום גרפי. אנו מיישמים את Runnableהממשק כך שנוכל להפעיל מכשיר Threadשמקבל הודעות מהשרת. הקונסטרוקטור מבצע את ההגדרה הבסיסית של ה- GUI, run()השיטה מקבלת הודעות מהשרת, handleEvent()השיטה מטפלת באינטראקציה של המשתמש, main()והשיטה מבצעת את חיבור הרשת הראשוני.

DataInputStream מוגן i; DataOutputStream מוגן o; פלט TextArea מוגן; קלט TextField מוגן; מאזין חוטים מוגן; ChatClient ציבורי (כותרת מחרוזת, InputStream i, OutputStream o) {super (title); this.i = DataInputStream חדש (BufferedInputStream חדש (i)); this.o = DataOutputStream חדש (BufferedOutputStream חדש (o)); setLayout (BorderLayout חדש ()); הוסף ("מרכז", פלט = TextArea חדש ()); output.setEditable (false); הוסף ("דרום", קלט = חדש TextField ()); חבילה (); הופעה (); input.requestFocus (); מאזין = אשכול חדש (זה); listener.start (); }

הקונסטרוקטור לוקח שלושה פרמטרים: כותרת לחלון, זרם קלט וזרם פלט. ChatClientמתקשר מעל הזרמים שצוינו; אנו יוצרים זרמי נתונים מאוחסנים i ו- o כדי לספק מתקני תקשורת יעילים ברמה גבוהה יותר על פני זרמים אלה. לאחר מכן הקמנו את ממשק המשתמש הפשוט שלנו, המורכב TextAreaמהפלט TextFieldוהקלט. אנו מתכננים ומציגים את החלון ומתחילים Threadמאזין שמקבל הודעות מהשרת.

הפעלה בטלנית ציבורית () {נסה {בעוד (נכון) {קו מחרוזת = i.readUTF (); output.appendText (שורה + "\ n"); }} לתפוס (IOException לשעבר) {ex.printStackTrace (); } סוף סוף {מאזין = null; input.hide (); לאמת (); נסה {o.close (); } לתפוס (IOException לשעבר) {ex.printStackTrace (); }}}

כאשר שרשור המאזין נכנס לשיטת הריצה, אנו יושבים בלולאה אינסופית וקוראים Strings מזרם הקלט. כאשר a Stringמגיע, אנו מצרפים אותו לאזור הפלט ונחזור על הלולאה. IOExceptionיכול להתרחש אם החיבור לשרת אבד. במקרה זה אנו מדפיסים את החריג ומבצעים ניקוי. שים לב שזה יהיה אותת על ידי EOFExceptionמן readUTF()השיטה.

כדי לנקות, אנחנו ראשון להקצות מאזין הפנייה שלנו זה Threadכדי null; זה מציין לשאר הקוד שהשרשור הסתיים. לאחר מכן אנו מסתירים את שדה הקלט ומתקשרים validate()כך שהממשק יונח שוב, וסוגרים את OutputStreamo כדי לוודא שהחיבור נסגר.

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

handle בוליאני ציבורי Event (אירוע e) {if ((e.target == קלט) && (e.id == Event.ACTION_EVENT)) {נסה {o.writeUTF ((String) e.arg); o.flush (); } לתפוס (ex IOException) {ex.printStackTrace (); listener.stop (); } input.setText (""); לחזור אמיתי; } אחר אם ((e.target == this) && (e.id == Event.WINDOW_DESTROY)) {if (מאזין! = null) listener.stop (); להתחבא (); לחזור אמיתי; } להחזיר super.handleEvent (ה); }

בשנות ה handleEvent()שיטה, אנחנו צריכים לבדוק אם שני אירועים משמעותיים UI:

הראשון הוא אירוע פעולה ב- TextField, מה שאומר שהמשתמש לחץ על מקש Return. כשאנחנו תופסים אירוע זה, אנו כותבים את ההודעה לזרם הפלט, ואז מתקשרים flush()כדי להבטיח שהוא יישלח מיד. זרם הפלט הוא a DataOutputStream, כך שנוכל להשתמש בו writeUTF()כדי לשלוח a String. אם IOExceptionמתרחש הקשר חייב להיכשל, לכן אנו עוצרים את שרשור המאזין; זה יבצע באופן אוטומטי את כל הניקיון הדרוש.

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

סטטי ציבורי ריק ריק (מחרוזת טוענת []) זורק IOException {אם (args.length! = 2) זורק RuntimeException חדש ("תחביר: ChatClient"); שקע s = שקע חדש (args [0], Integer.parseInt (args [1])); ChatClient חדש ("Chat" + args [0] + ":" + args [1], s.getInputStream (), s.getOutputStream ()); }

main()השיטה מתחילה הלקוח; אנו מבטיחים כי המספר הנכון של הארגומנטים סופקו, אנו פותחים א Socketלמארח ולנמל שצוינו, ויוצרים ChatClientמחובר לזרמי השקע. יצירת השקע עשויה לזרוק חריג שייצא משיטה זו ויוצג.

בניית שרת רב-הליכי

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

יש שתי מחלקות בתוכנית זו: המחלקה הראשית,, ChatServerהיא שרת המקבל חיבורים מלקוחות ומקצה אותם לאובייקטים חדשים של מטפלים בחיבורים. ChatHandlerהכיתה באמת עושה את העבודה של האזנה להודעות ושידור אותם לכל הלקוחות המחוברים. חוט אחד (החוט הראשי) מטפל בחיבורים חדשים, ויש חוט ( ChatHandlerהכיתה) לכל לקוח.

כל חדש ChatClientיתחבר ל ChatServer; זה ChatServerיעביר את החיבור למופע חדש של ChatHandlerהכיתה שיקבל הודעות מהלקוח החדש. במסגרת ChatHandlerהכיתה נשמרת רשימה של המטפלים הנוכחיים; broadcast()השיטה משתמשת ברשימה זו כדי להעביר מסר לכל המחוברים ChatClientים.

ChatServer בכיתה

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

ייבא java.net. *; ייבא java.io. *; ייבא java.util. *; class class ChatServer {// public ChatServer (int port) זורק IOException ... // public static void main (String args []) זורק IOException ...}

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

ChatServer ציבורי (יציאת int) זורק IOException {ServerSocket server = ServerSocket חדש (port); בעוד (נכון) {לקוח שקע = server.accept (); System.out.println ("מתקבל מ-" + client.getInetAddress ()); ChatHandler c = ChatHandler חדש (לקוח); c.start (); }}

קונסטרוקטור זה, שמבצע את כל עבודות השרת, הוא פשוט למדי. אנו יוצרים a ServerSocketואז יושבים בלולאה שמקבלים לקוחות accept()בשיטה ServerSocket. עבור כל חיבור, אנו יוצרים מופע חדש של ChatHandlerהמחלקה, ומעביר את החדש Socketכפרמטר. לאחר שיצרנו מטפל זה, אנו מתחילים אותו start()בשיטה שלו . זה מתחיל שרשור חדש לטיפול בחיבור כך שלולאת השרת הראשית שלנו תוכל להמשיך לחכות על חיבורים חדשים.

סטטי ציבורי ריק ריק (String args []) זורק IOException {if (args.length! = 1) זורק RuntimeException חדש ("תחביר: ChatServer"); ChatServer חדש (Integer.parseInt (טענות [0])); }

main()השיטה יוצרת מופע של ChatServer, העברת הנמל שורת הפקודה כפרמטר. זה הנמל שאליו הלקוחות יתחברו.

צ'אט הנדלר בכיתה

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

static

Vector.

ייבא java.net. *; ייבא java.io. *; ייבא java.util. *; מחלקה ציבורית ChatHandler מרחיב אשכול {// ציבור ChatHandler (Sockets s) זורק IOException ... // הפעלה בטלנית ציבורית () ...}

אנו מרחיבים את Threadהכיתה כדי לאפשר לשרשור נפרד לעבד את הלקוח המשויך. הקונסטרוקטור מקבל א Socketאליו אנו מתחברים; run()השיטה, המכונה על ידי חוט חדש, מבצע את עיבוד הלקוח בפועל.

שקעי שקע מוגנים; DataInputStream מוגן i; DataOutputStream מוגן o; ChatHandler ציבורי (שקעים) זורק IOException {this.s = s; i = DataInputStream חדש (BufferedInputStream חדש (s.getInputStream ())); o = DataOutputStream חדש (BufferedOutputStream חדש (s.getOutputStream ())); }

הקונסטרוקטור שומר הפניה לשקע הלקוח ופותח קלט וזרם פלט. שוב, אנו משתמשים בזרמי נתונים שנאגרו; אלה מספקים לנו קלט / פלט יעיל ושיטות לתקשר סוגי נתונים ברמה גבוהה - במקרה זה, Stringש.

protected static Vector handlers = new Vector (); public void run () { try { handlers.addElement (this); while (true) { String msg = i.readUTF (); broadcast (msg); } } catch (IOException ex) { ex.printStackTrace (); } finally { handlers.removeElement (this); try { s.close (); } catch (IOException ex) { ex.printStackTrace(); } } } // protected static void broadcast (String message) ... 

The run() method is where our thread enters. First we add our thread to the Vector of ChatHandlers handlers. The handlers Vector keeps a list of all of the current handlers. It is a static variable and so there is one instance of the Vector for the whole ChatHandler class and all of its instances. Thus, all ChatHandlers can access the list of current connections.

Note that it is very important for us to remove ourselves from this list afterward if our connection fails; otherwise, all other handlers will try to write to us when they broadcast information. This type of situation, where it is imperative that an action take place upon completion of a section of code, is a prime use of the try ... finally construct; we therefore perform all of our work within a try ... catch ... finally construct.

The body of this method receives messages from a client and rebroadcasts them to all other clients using the broadcast() method. When the loop exits, whether because of an exception reading from the client or because this thread is stopped, the finally clause is guaranteed to be executed. In this clause, we remove our thread from the list of handlers and close the socket.

protected static void broadcast (String message) { synchronized (handlers) { Enumeration e = handlers.elements (); while (e.hasMoreElements ()) { ChatHandler c = (ChatHandler) e.nextElement (); try { synchronized (c.o) { c.o.writeUTF (message); } c.o.flush (); } catch (IOException ex) { c.stop (); } } } } 

This method broadcasts a message to all clients. We first synchronize on the list of handlers. We don't want people joining or leaving while we are looping, in case we try to broadcast to someone who no longer exists; this forces the clients to wait until we are done synchronizing. If the server must handle particularly heavy loads, then we might provide more fine-grained synchronization.

בתוך בלוק מסונכרן זה אנו מקבלים אחד Enumerationמהמטפלים הנוכחיים. Enumerationבכיתה מספקת דרך נוחה איטרציות כל האלמנטים של Vector. הלולאה שלנו פשוט כותבת את המסר לכל אלמנט של Enumeration. שים לב שאם מתרחש חריג בזמן כתיבה ל- a ChatClient, אנו קוראים stop()לשיטת הלקוח ; זה עוצר את שרשור הלקוח ולכן מבצע את הניקוי המתאים, כולל הסרת הלקוח ממטפלים.