Membangun sistem obrolan Internet

Anda mungkin pernah melihat salah satu dari banyak sistem obrolan berbasis Java yang muncul di Web. Setelah membaca artikel ini, Anda akan memahami cara kerjanya - dan mengetahui cara membangun sistem obrolan sederhana Anda sendiri.

Contoh sederhana dari sistem klien / server ini dimaksudkan untuk mendemonstrasikan cara membangun aplikasi hanya dengan menggunakan aliran yang tersedia di API standar. Obrolan menggunakan soket TCP / IP untuk berkomunikasi, dan dapat disematkan dengan mudah di halaman Web. Sebagai referensi, kami menyediakan bilah sisi yang menjelaskan komponen pemrograman jaringan Java yang relevan dengan aplikasi ini. Jika Anda masih ingin mempercepat, lihat sidebar terlebih dahulu. Jika Anda sudah mahir dalam Java, Anda dapat langsung masuk dan cukup merujuk ke sidebar untuk referensi.

Membangun klien obrolan

Kami mulai dengan klien obrolan grafis sederhana. Dibutuhkan dua parameter baris perintah - nama server dan nomor port untuk disambungkan. Itu membuat koneksi soket dan kemudian membuka jendela dengan wilayah keluaran besar dan wilayah masukan kecil.

Antarmuka ChatClient

Setelah pengguna mengetik teks ke dalam wilayah masukan dan menekan Kembali, teks tersebut dikirim ke server. Server menggemakan kembali semua yang dikirim oleh klien. Klien menampilkan semua yang diterima dari server di wilayah keluaran. Ketika banyak klien terhubung ke satu server, kami memiliki sistem obrolan sederhana.

Kelas ChatClient

Kelas ini mengimplementasikan klien obrolan, seperti yang dijelaskan. Ini melibatkan pengaturan antarmuka pengguna dasar, menangani interaksi pengguna, dan menerima pesan dari server.

import java.net. *; impor java.io. *; import java.awt. *; public class ChatClient extends Frame mengimplementasikan Runnable {// public ChatClient (String judul, InputStream i, OutputStream o) ... // public void run () ... // public boolean handleEvent (Event e) ... // public static void main (String args []) memunculkan IOException ...}

The ChatClientkelas meluas Frame; ini tipikal untuk aplikasi grafis. Kami mengimplementasikan Runnableantarmuka sehingga kami dapat memulai Threadyang menerima pesan dari server. Konstruktor melakukan pengaturan dasar GUI, run()metode menerima pesan dari server, handleEvent()metode menangani interaksi pengguna, dan main()metode melakukan koneksi jaringan awal.

DataInputStream dilindungi i; dilindungi DataOutputStream o; keluaran TextArea yang dilindungi; input TextField dilindungi; pendengar Thread yang dilindungi; public ChatClient (String judul, InputStream i, OutputStream o) {super (judul); this.i = new DataInputStream (new BufferedInputStream (i)); this.o = new DataOutputStream (new BufferedOutputStream (o)); setLayout (new BorderLayout ()); add ("Center", output = new TextArea ()); output.setEditable (false); add ("Selatan", input = new TextField ()); Pak (); tampilkan (); input.requestFocus (); listener = Thread baru (ini); listener.start (); }

Konstruktor mengambil tiga parameter: judul untuk jendela, aliran masukan, dan aliran keluaran. The ChatClientberkomunikasi selama aliran tertentu; kami membuat aliran data i dan o yang di-buffer untuk menyediakan fasilitas komunikasi tingkat tinggi yang efisien melalui aliran ini. Kami kemudian menyiapkan antarmuka pengguna sederhana kami, yang terdiri dari TextAreaoutput dan TextFieldinput. Kami mengatur tata letak dan menampilkan jendela, dan memulai Threadpendengar yang menerima pesan dari server.

public void run () {coba {while (true) {String line = i.readUTF (); output.appendText (baris + "\ n"); }} menangkap (IOException ex) {ex.printStackTrace (); } akhirnya {listener = null; input.hide (); validate (); coba {o.close (); } menangkap (IOException ex) {ex.printStackTrace (); }}}

Ketika thread listener memasuki metode run, kita duduk di loop tak terbatas membaca Stringdari input stream. Saat a Stringtiba, kami menambahkannya ke wilayah keluaran dan mengulangi loop. Sebuah IOExceptionbisa terjadi jika koneksi ke server telah hilang. Dalam acara itu, kami mencetak pengecualian dan melakukan pembersihan. Perhatikan bahwa ini akan ditandai oleh EOFExceptiondari readUTF()metode.

Untuk membersihkan, pertama-tama kami menetapkan referensi pendengar kami untuk ini Threadke null; ini menunjukkan ke kode lainnya bahwa utas telah dihentikan. Kami kemudian menyembunyikan bidang input dan memanggil validate()sehingga antarmuka diletakkan kembali, dan menutup OutputStreamo untuk memastikan bahwa koneksi ditutup.

Perhatikan bahwa kami melakukan semua pembersihan dalam finallyklausa, jadi ini akan terjadi apakah IOExceptionterjadi di sini atau utas dihentikan secara paksa. Kami tidak segera menutup jendela; asumsinya adalah bahwa pengguna mungkin ingin membaca sesi bahkan setelah koneksi terputus.

public boolean handleEvent (Acara e) {if ((e.target == input) && (e.id == Event.ACTION_EVENT)) {coba {o.writeUTF ((String) e.arg); o.flush (); } menangkap (IOException ex) {ex.printStackTrace (); listener.stop (); } input.setText (""); kembali benar; } lain jika ((e.target == ini) && (e.id == Event.WINDOW_DESTROY)) {if (listener! = null) listener.stop (); sembunyikan (); kembali benar; } return super.handleEvent (e); }

Dalam handleEvent()metode ini, kita perlu memeriksa dua kejadian UI yang signifikan:

Yang pertama adalah peristiwa aksi di TextField, yang berarti bahwa pengguna telah menekan tombol Kembali. Ketika kita menangkap kejadian ini, kita menulis pesan ke aliran keluaran, kemudian memanggil flush()untuk memastikan bahwa pesan itu dikirim segera. Aliran keluarannya adalah a DataOutputStream, jadi kita bisa menggunakan writeUTF()untuk mengirim file String. Jika IOExceptionterjadi koneksi pasti gagal, jadi kami menghentikan thread listener; ini secara otomatis akan melakukan semua pembersihan yang diperlukan.

Peristiwa kedua adalah pengguna mencoba menutup jendela. Terserah programmer untuk menangani tugas ini; kami menghentikan utas pendengar dan menyembunyikan Frame.

public static void main (String args []) melempar IOException {if (args.length! = 2) throw new RuntimeException ("Syntax: ChatClient"); Socket s = Socket baru (args [0], Integer.parseInt (args [1])); ChatClient baru ("Chat" + args [0] + ":" + args [1], s.getInputStream (), s.getOutputStream ()); }

The main()metode dimulai klien; kami memastikan bahwa jumlah argumen yang benar telah diberikan, kami membuka Socketke host dan port yang ditentukan, dan kami membuat ChatClientaliran yang terhubung ke soket. Membuat soket dapat memunculkan pengecualian yang akan keluar dari metode ini dan ditampilkan.

Membangun server multithread

Kami sekarang mengembangkan server obrolan yang dapat menerima banyak koneksi dan yang akan menyiarkan semua yang dibaca dari klien mana pun. Ini terprogram untuk membaca dan menulis Stringdalam format UTF.

Ada dua kelas dalam program ini: kelas utama ChatServer,, adalah server yang menerima koneksi dari klien dan menugaskannya ke objek penangan koneksi baru. The ChatHandlerkelas sebenarnya karya mendengarkan pesan dan penyiaran mereka untuk semua klien yang terhubung. Satu utas (utas utama) menangani koneksi baru, dan ada utas ( ChatHandlerkelas) untuk setiap klien.

Setiap yang baru ChatClientakan terhubung ke ChatServer; ini ChatServerakan menyerahkan koneksi ke instance baru ChatHandlerkelas yang akan menerima pesan dari klien baru. Di dalam ChatHandlerkelas, daftar penangan saat ini disimpan; yang broadcast()metode menggunakan daftar ini untuk mengirimkan pesan kepada semua terhubung ChatClients.

Kelas ChatServer

Kelas ini berkaitan dengan menerima koneksi dari klien dan meluncurkan utas penangan untuk memprosesnya.

import java.net. *; impor java.io. *; import java.util. *; public class ChatServer {// public ChatServer (int port) melempar IOException ... // public static void main (String args []) melempar IOException ...}

Kelas ini adalah aplikasi mandiri sederhana. Kami menyediakan konstruktor yang melakukan semua pekerjaan aktual untuk kelas, dan main()metode yang benar-benar memulainya.

public ChatServer (int port) melempar IOException {ServerSocket server = new ServerSocket (port); sementara (benar) {Socket client = server.accept (); System.out.println ("Diterima dari" + client.getInetAddress ()); ChatHandler c = ChatHandler baru (klien); c. mulai (); }}

Konstruktor ini, yang melakukan semua pekerjaan server, cukup sederhana. Kami membuat ServerSocketdan kemudian duduk dalam satu lingkaran menerima klien dengan accept()metode ServerSocket. Untuk setiap koneksi, kami membuat instance baru dari ChatHandlerkelas, meneruskan new Socketsebagai parameter. Setelah kita membuat handler ini, kita mulai dengan start()metodenya. Ini memulai utas baru untuk menangani koneksi sehingga loop server utama kami dapat terus menunggu koneksi baru.

public static void main (String args []) melempar IOException {if (args.length! = 1) throw new RuntimeException ("Syntax: ChatServer"); ChatServer baru (Integer.parseInt (args [0])); }

The main() method creates an instance of the ChatServer, passing the command-line port as a parameter. This is the port to which clients will connect.

Class ChatHandler

This class is concerned with handling individual connections. We must receive messages from the client and re-send these to all other connections. We maintain a list of the connections in a

static

Vector.

import java.net.*; import java.io.*; import java.util.*; public class ChatHandler extends Thread { // public ChatHandler (Socket s) throws IOException ... // public void run () ... } 

We extend the Thread class to allow a separate thread to process the associated client. The constructor accepts a Socket to which we attach; the run() method, called by the new thread, performs the actual client processing.

 protected Socket s; protected DataInputStream i; protected DataOutputStream o; public ChatHandler (Socket s) throws IOException { this.s = s; i = new DataInputStream (new BufferedInputStream (s.getInputStream ())); o = new DataOutputStream (new BufferedOutputStream (s.getOutputStream ())); } 

The constructor keeps a reference to the client's socket and opens an input and an output stream. Again, we use buffered data streams; these provide us with efficient I/O and methods to communicate high-level data types -- in this case, Strings.

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.

Dalam blok tersinkronisasi ini kita mendapatkan salah Enumerationsatu penangan saat ini. The Enumerationkelas menyediakan cara yang nyaman untuk iterate melalui semua elemen dari Vector. Loop kami hanya menulis pesan ke setiap elemen dari Enumeration. Perhatikan bahwa jika pengecualian terjadi saat menulis ke a ChatClient, maka kami memanggil metode klien stop(); ini menghentikan utas klien dan oleh karena itu melakukan pembersihan yang sesuai, termasuk menghapus klien dari penangan.