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 ChatClient
kelas meluas Frame
; ini tipikal untuk aplikasi grafis. Kami mengimplementasikan Runnable
antarmuka sehingga kami dapat memulai Thread
yang 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 ChatClient
berkomunikasi 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 TextArea
output dan TextField
input. Kami mengatur tata letak dan menampilkan jendela, dan memulai Thread
pendengar 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 String
dari input stream. Saat a String
tiba, kami menambahkannya ke wilayah keluaran dan mengulangi loop. Sebuah IOException
bisa terjadi jika koneksi ke server telah hilang. Dalam acara itu, kami mencetak pengecualian dan melakukan pembersihan. Perhatikan bahwa ini akan ditandai oleh EOFException
dari readUTF()
metode.
Untuk membersihkan, pertama-tama kami menetapkan referensi pendengar kami untuk ini Thread
ke null
; ini menunjukkan ke kode lainnya bahwa utas telah dihentikan. Kami kemudian menyembunyikan bidang input dan memanggil validate()
sehingga antarmuka diletakkan kembali, dan menutup OutputStream
o untuk memastikan bahwa koneksi ditutup.
Perhatikan bahwa kami melakukan semua pembersihan dalam finally
klausa, jadi ini akan terjadi apakah IOException
terjadi 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 IOException
terjadi 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 Socket
ke host dan port yang ditentukan, dan kami membuat ChatClient
aliran 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 String
dalam 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 ChatHandler
kelas sebenarnya karya mendengarkan pesan dan penyiaran mereka untuk semua klien yang terhubung. Satu utas (utas utama) menangani koneksi baru, dan ada utas ( ChatHandler
kelas) untuk setiap klien.
Setiap yang baru ChatClient
akan terhubung ke ChatServer
; ini ChatServer
akan menyerahkan koneksi ke instance baru ChatHandler
kelas yang akan menerima pesan dari klien baru. Di dalam ChatHandler
kelas, daftar penangan saat ini disimpan; yang broadcast()
metode menggunakan daftar ini untuk mengirimkan pesan kepada semua terhubung ChatClient
s.
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 ServerSocket
dan kemudian duduk dalam satu lingkaran menerima klien dengan accept()
metode ServerSocket
. Untuk setiap koneksi, kami membuat instance baru dari ChatHandler
kelas, meneruskan new Socket
sebagai 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, String
s.
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 ChatHandler
s 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 ChatHandler
s 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 Enumeration
satu penangan saat ini. The Enumeration
kelas 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.