Penanganan sederhana waktu tunggu jaringan

Banyak programmer takut memikirkan penanganan timeout jaringan. Ketakutan umum adalah bahwa klien jaringan single-threaded yang sederhana tanpa dukungan timeout akan menggelembung menjadi mimpi buruk multithread yang kompleks, dengan thread terpisah yang diperlukan untuk mendeteksi batas waktu jaringan, dan beberapa bentuk proses notifikasi yang bekerja antara thread yang diblokir dan aplikasi utama. Meskipun ini adalah salah satu opsi untuk pengembang, ini bukan satu-satunya. Berurusan dengan waktu tunggu jaringan bukanlah tugas yang sulit, dan dalam banyak kasus Anda dapat sepenuhnya menghindari penulisan kode untuk utas tambahan.

Saat bekerja dengan koneksi jaringan, atau jenis perangkat I / O apa pun, ada dua klasifikasi operasi:

  • Operasi pemblokiran : Membaca atau menulis terhenti, operasi menunggu hingga perangkat I / O siap
  • Operasi nonblocking : Upaya membaca atau menulis dilakukan, operasi dibatalkan jika perangkat I / O belum siap

Jaringan Java, secara default, merupakan bentuk pemblokiran I / O. Jadi, ketika aplikasi jaringan Java membaca dari koneksi soket, biasanya aplikasi akan menunggu tanpa batas waktu jika tidak ada tanggapan langsung. Jika tidak ada data yang tersedia, program akan terus menunggu, dan tidak ada pekerjaan lebih lanjut yang dapat dilakukan. Satu solusi, yang memecahkan masalah tetapi memperkenalkan sedikit kerumitan ekstra, adalah meminta thread kedua melakukan operasi; Dengan cara ini, jika thread kedua diblokir, aplikasi masih dapat menanggapi perintah pengguna, atau bahkan menghentikan thread yang terhenti jika perlu.

Solusi ini sering digunakan, tetapi ada alternatif yang lebih sederhana. Java juga mendukung nonblocking jaringan I / O, yang dapat diaktifkan pada setiap Socket, ServerSocket, atau DatagramSocket. Dimungkinkan untuk menentukan lamanya waktu maksimum operasi baca atau tulis akan terhenti sebelum mengembalikan kontrol ke aplikasi. Untuk klien jaringan, ini adalah solusi termudah dan menawarkan kode yang lebih sederhana dan lebih mudah diatur.

Satu-satunya kelemahan dari nonblocking network I / O di bawah Java adalah bahwa ia membutuhkan soket yang sudah ada. Jadi, meskipun metode ini sempurna untuk operasi baca atau tulis normal, operasi koneksi dapat terhenti untuk periode yang lebih lama, karena tidak ada metode untuk menentukan periode waktu tunggu untuk operasi penghubung. Banyak aplikasi membutuhkan kemampuan ini; Anda dapat, bagaimanapun, dengan mudah menghindari pekerjaan ekstra untuk menulis kode tambahan. Saya telah menulis kelas kecil yang memungkinkan Anda menentukan nilai batas waktu untuk koneksi. Ini menggunakan utas kedua, tetapi detail internal disarikan. Pendekatan ini bekerja dengan baik, karena menyediakan antarmuka I / O yang tidak memblokir, dan detail utas kedua disembunyikan dari tampilan.

Jaringan I / O nonblocking

Cara paling sederhana dalam melakukan sesuatu seringkali ternyata cara terbaik. Meskipun terkadang perlu menggunakan utas dan memblokir I / O, dalam sebagian besar kasus, I / O non-pemblokiran cocok untuk solusi yang jauh lebih jelas dan lebih elegan. Dengan hanya beberapa baris kode, Anda dapat memasukkan dukungan batas waktu untuk aplikasi soket apa pun. Tidak percaya padaku Lanjutkan membaca.

Ketika Java 1.1 dirilis, itu termasuk perubahan API ke java.netpaket yang memungkinkan pemrogram untuk menentukan opsi soket. Opsi ini memberi programmer kendali yang lebih besar atas komunikasi soket. Satu opsi khususnya,, SO_TIMEOUTsangat berguna, karena memungkinkan pemrogram menentukan jumlah waktu yang akan diblokir oleh operasi baca. Kami dapat menentukan penundaan singkat, atau tidak sama sekali, dan membuat kode jaringan kami tidak diblokir.

Mari kita lihat cara kerjanya. Metode baru, setSoTimeout ( int )telah ditambahkan ke kelas soket berikut:

  • java.net.Socket
  • java.net.DatagramSocket
  • java.net.ServerSocket

Metode ini memungkinkan kita menentukan panjang waktu tunggu maksimum, dalam milidetik, yang akan diblokir oleh operasi jaringan berikut:

  • ServerSocket.accept()
  • SocketInputStream.read()
  • DatagramSocket.receive()

Kapan pun salah satu metode ini dipanggil, jam mulai berdetak. Jika operasi tidak diblokir, itu akan mengatur ulang dan hanya restart setelah salah satu metode ini dipanggil lagi; Akibatnya, tidak ada waktu tunggu yang dapat terjadi kecuali Anda menjalankan operasi I / O jaringan. Contoh berikut menunjukkan betapa mudahnya menangani waktu tunggu, tanpa menggunakan beberapa utas eksekusi:

// Buat soket datagram pada port 2000 untuk mendengarkan paket UDP yang masuk DatagramSocket dgramSocket = new DatagramSocket (2000); // Nonaktifkan pemblokiran operasi I / O, dengan menetapkan batas waktu lima detik dgramSocket.setSoTimeout (5000);

Menetapkan nilai waktu tunggu mencegah operasi jaringan kami memblokir tanpa batas. Pada titik ini, Anda mungkin bertanya-tanya apa yang akan terjadi ketika waktu operasi jaringan habis. Daripada mengembalikan kode kesalahan, yang mungkin tidak selalu diperiksa oleh pengembang, a java.io.InterruptedIOExceptiondilempar. Penanganan pengecualian adalah cara terbaik untuk menangani kondisi kesalahan, dan memungkinkan kita untuk memisahkan kode normal kita dari kode penanganan kesalahan kita. Selain itu, siapa yang secara religius memeriksa setiap nilai pengembalian untuk referensi nol? Dengan memberikan pengecualian, pengembang dipaksa untuk menyediakan penangan tangkapan untuk waktu tunggu.

Cuplikan kode berikut menunjukkan cara menangani operasi waktu tunggu saat membaca dari soket TCP:

// Setel batas waktu soket untuk sepuluh detik connection.setSoTimeout (10000); coba {// Buat DataInputStream untuk membaca dari soket DataInputStream din = new DataInputStream (connection.getInputStream ()); // Baca data hingga akhir data untuk (;;) {String line = din.readLine (); jika (baris! = null) System.out.println (baris); lain istirahat; }} // Pengecualian dilempar ketika waktu tunggu jaringan terjadi catch (InterruptedIOException iioe) {System.err.println ("Host jarak jauh habis waktu selama operasi baca"); } // Pengecualian dilempar ketika kesalahan jaringan I / O umum terjadi catch (IOException ioe) {System.err.println ("Network I / O error -" + ioe); }

Dengan hanya beberapa baris kode tambahan untuk try {}blok tangkap, sangat mudah untuk menangkap waktu tunggu jaringan. Sebuah aplikasi kemudian dapat menanggapi situasi tersebut tanpa terhenti. Misalnya, ini bisa dimulai dengan memberi tahu pengguna, atau dengan mencoba membuat koneksi baru. Saat menggunakan soket datagram, yang mengirim paket informasi tanpa menjamin pengiriman, aplikasi dapat merespons waktu tunggu jaringan dengan mengirim ulang paket yang hilang saat transit. Menerapkan dukungan batas waktu ini membutuhkan sedikit waktu dan menghasilkan solusi yang sangat bersih. Memang, satu-satunya saat nonblocking I / O bukanlah solusi optimal adalah ketika Anda juga perlu mendeteksi timeout pada operasi koneksi, atau ketika lingkungan target Anda tidak mendukung Java 1.1.

Penanganan timeout pada operasi koneksi

Jika tujuan Anda adalah mencapai deteksi dan penanganan timeout lengkap, Anda harus mempertimbangkan operasi koneksi. Saat membuat instance java.net.Socket, upaya untuk membuat koneksi dibuat. Jika mesin host aktif, tetapi tidak ada layanan yang berjalan pada port yang ditentukan dalam java.net.Socketkonstruktor, a ConnectionExceptionakan dilempar dan kontrol akan kembali ke aplikasi. Namun, jika mesin mati, atau jika tidak ada rute ke host itu, koneksi soket pada akhirnya akan habis dengan sendirinya nanti. Sementara itu, aplikasi Anda tetap dibekukan, dan tidak ada cara untuk mengubah nilai waktu tunggu.

Meskipun panggilan konstruktor soket pada akhirnya akan kembali, panggilan tersebut menimbulkan penundaan yang signifikan. Salah satu cara untuk mengatasi masalah ini adalah dengan menggunakan utas kedua, yang akan melakukan koneksi yang berpotensi memblokir, dan untuk terus mengumpulkan utas itu untuk melihat apakah koneksi telah dibuat.

Namun, ini tidak selalu mengarah pada solusi yang elegan. Ya, Anda dapat mengubah klien jaringan Anda menjadi aplikasi multithread, tetapi seringkali jumlah pekerjaan ekstra yang diperlukan untuk melakukan ini menjadi penghalang. Itu membuat kode lebih kompleks, dan ketika menulis hanya aplikasi jaringan sederhana, jumlah usaha yang dibutuhkan sulit untuk dibenarkan. Jika Anda menulis banyak aplikasi jaringan, Anda akan sering menemukan kembali kemudi. Namun, ada solusi yang lebih sederhana.

Saya telah menulis kelas sederhana yang dapat digunakan kembali yang dapat Anda gunakan dalam aplikasi Anda sendiri. Kelas menghasilkan koneksi soket TCP tanpa berhenti untuk waktu yang lama. Anda cukup memanggil getSocketmetode, menentukan nama host, port, dan penundaan waktu tunggu, dan menerima soket. Contoh berikut menunjukkan permintaan koneksi:

// Sambungkan ke server jarak jauh dengan nama host, dengan waktu tunggu habis empat detik Koneksi Socket = TimedSocket.getSocket ("server.my-network.net", 23, 4000); 

Jika semuanya berjalan dengan baik, soket akan dikembalikan, seperti java.net.Socketkonstruktor standar . Jika koneksi tidak dapat dibuat sebelum waktu tunggu yang Anda tentukan terjadi, metode akan berhenti, dan akan melempar java.io.InterruptedIOException, seperti operasi pembacaan soket lainnya ketika waktu tunggu telah ditentukan menggunakan setSoTimeoutmetode. Cukup mudah, ya?

Encapsulating multithreaded network code into a single class

While the TimedSocket class is a useful component in itself, it's also a very good learning aid for understanding how to deal with blocking I/O. When a blocking operation is performed, a single-threaded application will become blocked indefinitely. If multiple threads of execution are used, however, only one thread need stall; the other thread can continue to execute. Let's take a look at how the TimedSocket class works.

When an application needs to connect to a remote server, it invokes the TimedSocket.getSocket() method and passes details of the remote host and port. The getSocket() method is overloaded, allowing both a String hostname and an InetAddress to be specified. This range of parameters should be sufficient for the majority of socket operations, though custom overloading could be added for special implementations. Inside the getSocket() method, a second thread is created.

The imaginatively named SocketThread will create an instance of java.net.Socket, which can potentially block for a considerable amount of time. It provides accessor methods to determine if a connection has been established or if an error has occurred (for example, if java.net.SocketException was thrown during the connect).

While the connection is being established, the primary thread waits until a connection is established, for an error to occur, or for a network timeout. Every hundred milliseconds, a check is made to see if the second thread has achieved a connection. If this check fails, a second check must be made to determine whether an error occurred in the connection. If not, and the connection attempt is still continuing, a timer is incremented and, after a small sleep, the connection will be polled again.

This method makes heavy use of exception handling. If an error occurs, then this exception will be read from the SocketThread instance, and it will be thrown again. If a network timeout occurs, the method will throw a java.io.InterruptedIOException.

The following code snippet shows the polling mechanism and error-handling code.

for (;;) { // Check to see if a connection is established if (st.isConnected()) { // Yes ... assign to sock variable, and break out of loop sock = st.getSocket(); break; } else { // Check to see if an error occurred if (st.isError()) { // No connection could be established throw (st.getException()); } try { // Sleep for a short period of time Thread.sleep ( POLL_DELAY ); } catch (InterruptedException ie) {} // Increment timer timer += POLL_DELAY; // Check to see if time limit exceeded if (timer > delay) { // Can't connect to server throw new InterruptedIOException ("Could not connect for " + delay + " milliseconds"); } } } 

Inside the blocked thread

While the connection is regularly polled, the second thread attempts to create a new instance of java.net.Socket. Accessor methods are provided to determine the state of the connection, as well as to get the final socket connection. The SocketThread.isConnected() method returns a boolean value to indicate whether a connection has been established, and the SocketThread.getSocket() method returns a Socket. Similar methods are provided to determine if an error has occurred, and to access the exception that was caught.

Semua metode ini menyediakan antarmuka terkontrol ke SocketThreadinstance, tanpa mengizinkan modifikasi eksternal variabel anggota pribadi. Contoh kode berikut menunjukkan metode utas run(). Ketika, dan jika, konstruktor soket mengembalikan a Socket, itu akan ditetapkan ke variabel anggota privat, di mana metode pengakses menyediakan akses. Saat berikutnya status koneksi dipertanyakan, dengan menggunakan SocketThread.isConnected()metode ini, soket akan tersedia untuk digunakan. Teknik yang sama digunakan untuk mendeteksi kesalahan; jika a java.io.IOExceptiontertangkap, itu akan disimpan dalam anggota pribadi, yang dapat diakses melalui metode isError()dan pengakses getException().