Pengendalian timeout rangkaian yang mudah

Banyak pengaturcara takut memikirkan pengendalian masa tamat rangkaian. Ketakutan yang biasa berlaku ialah pelanggan rangkaian mudah tunggal tanpa sokongan timeout akan menjadi mimpi ngeri multithread yang kompleks, dengan utas terpisah yang diperlukan untuk mengesan timeout rangkaian, dan beberapa bentuk proses pemberitahuan yang sedang dijalankan antara thread yang disekat dan aplikasi utama. Walaupun ini adalah satu pilihan untuk pembangun, itu bukan satu-satunya pilihan. Berurusan dengan timeout rangkaian tidak semestinya menjadi tugas yang sukar, dan dalam banyak kes anda sepenuhnya boleh mengelakkan menulis kod untuk utas tambahan.

Semasa bekerja dengan sambungan rangkaian, atau jenis peranti I / O, terdapat dua klasifikasi operasi:

  • Menyekat operasi : Membaca atau menulis gerai, operasi menunggu sehingga peranti I / O siap
  • Operasi tanpa sekatan : Percubaan membaca atau menulis dilakukan, operasi dibatalkan sekiranya peranti I / O tidak siap

Jaringan Java, secara lalai, adalah bentuk penyekat I / O. Oleh itu, apabila aplikasi rangkaian Java membaca dari sambungan soket, biasanya akan menunggu selama-lamanya sekiranya tidak ada tindak balas segera. Sekiranya tidak ada data yang tersedia, program akan terus menunggu, dan tidak ada pekerjaan lebih lanjut yang dapat dilakukan. Salah satu penyelesaian, yang menyelesaikan masalah tetapi menimbulkan kerumitan tambahan, adalah dengan membuat urutan kedua menjalankan operasi; dengan cara ini, jika utas kedua menjadi tersekat, aplikasi masih dapat bertindak balas terhadap arahan pengguna, atau bahkan menamatkan benang terhenti jika perlu.

Penyelesaian ini sering digunakan, tetapi ada alternatif yang lebih mudah. Java juga menyokong nonblocking rangkaian I / O, yang boleh diaktifkan di mana-mana Socket, ServerSocketatau DatagramSocket. Adalah mungkin untuk menentukan jangka masa maksimum operasi membaca atau menulis akan berhenti sebelum mengembalikan kawalan ke aplikasi. Untuk pelanggan rangkaian, ini adalah penyelesaian termudah dan menawarkan kod yang lebih mudah dan boleh dikendalikan.

Satu-satunya kelemahan untuk tidak menyekat rangkaian I / O di bawah Java adalah bahawa ia memerlukan soket yang ada. Oleh itu, walaupun kaedah ini sangat sesuai untuk operasi baca atau tulis yang normal, operasi penyambungan boleh berhenti untuk jangka masa yang lebih lama, kerana tidak ada kaedah untuk menentukan jangka masa tamat untuk operasi sambung. Banyak aplikasi memerlukan kemampuan ini; anda boleh, bagaimanapun, dengan mudah mengelakkan kerja tambahan menulis kod tambahan. Saya telah menulis kelas kecil yang membolehkan anda menentukan nilai masa tamat untuk sambungan. Ia menggunakan utas kedua, tetapi butiran dalamannya disembunyikan. Pendekatan ini berfungsi dengan baik, kerana ia menyediakan antara muka I / O tanpa sekatan, dan perincian utas kedua tersembunyi dari pandangan.

I / O rangkaian tanpa sekatan

Cara paling mudah untuk melakukan sesuatu sering menjadi kaedah yang terbaik. Walaupun kadang-kadang perlu menggunakan benang dan menyekat I / O, dalam kebanyakan kes, I / O tanpa penyekat memberi jalan penyelesaian yang lebih jelas dan lebih elegan. Dengan hanya beberapa baris kod, anda boleh memasukkan sokongan timeout untuk sebarang aplikasi soket. Tidak percaya? Teruskan membaca.

Ketika Java 1.1 dirilis, itu termasuk perubahan API pada java.netpaket yang memungkinkan programmer menentukan pilihan soket. Pilihan ini memberi pengaturcara kawalan yang lebih besar terhadap komunikasi soket. Satu pilihan khususnya, SO_TIMEOUTsangat berguna, kerana membolehkan pengaturcara menentukan berapa lama masa operasi pembacaan akan disekat. Kami dapat menentukan kelewatan pendek, atau tidak sama sekali, dan menjadikan kod rangkaian kami tidak disekat.

Mari kita lihat bagaimana ini berfungsi. Kaedah baru, setSoTimeout ( int )telah ditambahkan ke kelas soket berikut:

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

Kaedah ini membolehkan kami menentukan jangka masa maksimum, dalam milisaat, yang akan disekat oleh operasi rangkaian berikut:

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

Setiap kali kaedah ini dipanggil, jam mula berdetak. Sekiranya operasi tidak disekat, operasi akan diset semula dan dimulakan semula apabila salah satu kaedah ini dipanggil semula; Akibatnya, tidak ada masa tamat yang boleh berlaku melainkan anda melakukan operasi I / O rangkaian. Contoh berikut menunjukkan betapa mudahnya menangani timeout, tanpa memerlukan banyak urutan pelaksanaan:

// Buat soket datagram pada port 2000 untuk mendengarkan paket UDP masuk DatagramSocket dgramSocket = DatagramSocket baru (2000); // Lumpuhkan sekatan operasi I / O, dengan menetapkan waktu tamat lima dgramSocket.setSoTimeout (5000);

Menetapkan nilai timeout menghalang operasi rangkaian kami daripada menyekat selama-lamanya. Pada ketika ini, anda mungkin tertanya-tanya apa yang akan berlaku apabila operasi rangkaian tamat. Daripada mengembalikan kod ralat, yang mungkin tidak selalu diperiksa oleh pemaju, a java.io.InterruptedIOExceptiondilemparkan. Pengendalian pengecualian adalah kaedah terbaik untuk menangani keadaan ralat, dan membolehkan kita memisahkan kod normal kita dari kod pengendalian ralat kita. Selain itu, siapa yang memeriksa setiap nilai pulangan untuk rujukan nol secara religius? Dengan membuang pengecualian, pemaju terpaksa menyediakan pengendali tangkapan untuk waktu habis.

Coretan kod berikut menunjukkan cara menangani operasi timeout ketika membaca dari soket TCP:

// Tetapkan masa tamat soket selama sepuluh saat sambungan.setSoTimeout (10000); cuba {// Buat DataInputStream untuk membaca dari soket DataInputStream din = DataInputStream baru (connection.getInputStream ()); // Baca data hingga akhir data untuk (;;) {String line = din.readLine (); jika (line! = null) System.out.println (line); lain pecah; }} // Pengecualian dilemparkan ketika timeout rangkaian berlaku tangkapan (InterruptIOException iioe) {System.err.println ("Hosted time out habis semasa operasi baca"); } // Pengecualian dilemparkan apabila ralat I / O rangkaian umum berlaku tangkapan (IOException) {System.err.println ("Ralat Rangkaian I / O -" + permintaan); }

Dengan hanya beberapa baris kod tambahan untuk try {}blok penangkapan, sangat mudah untuk menangkap masa tamat rangkaian. Aplikasi kemudian dapat bertindak balas terhadap keadaan tanpa berhenti. Sebagai contoh, ini boleh dimulakan dengan memberi tahu pengguna, atau dengan berusaha membuat sambungan baru. Ketika menggunakan soket datagram, yang mengirim paket informasi tanpa menjamin pengiriman, aplikasi dapat merespon batas waktu rangkaian dengan mengirim ulang paket yang hilang dalam perjalanan. Melaksanakan sokongan timeout ini memerlukan sedikit masa dan membawa kepada penyelesaian yang sangat bersih. Sesungguhnya, satu-satunya masa bahawa I / O tanpa sekatan bukanlah penyelesaian yang optimum adalah ketika anda juga perlu mengesan masa tamat pada operasi penyambungan, atau ketika persekitaran sasaran anda tidak menyokong Java 1.1.

Pengendalian timeout pada operasi sambung

Sekiranya matlamat anda adalah untuk mencapai pengesanan dan pengendalian timeout yang lengkap, maka anda perlu mempertimbangkan operasi penyambungan. Ketika membuat contoh java.net.Socket, usaha untuk membuat sambungan dibuat. Sekiranya mesin hos aktif, tetapi tidak ada perkhidmatan yang dijalankan di port yang ditentukan dalam java.net.Socketkonstruktor, ConnectionExceptionwasiat akan dilemparkan dan kawalan akan kembali ke aplikasi. Namun, jika mesin mati, atau jika tidak ada jalan ke host tersebut, sambungan soket akhirnya akan habis dengan sendirinya kemudian. Sementara itu, aplikasi anda tetap dibekukan, dan tidak ada cara untuk mengubah nilai masa tamat.

Walaupun panggilan soket konstruktor akhirnya akan kembali, ia memperkenalkan kelewatan yang ketara. Salah satu cara untuk mengatasi masalah ini adalah menggunakan utas kedua, yang akan melakukan penyambungan yang berpotensi menyekat, dan secara berterusan membuat tinjauan benang itu untuk melihat apakah sambungan telah terjalin.

Walau bagaimanapun, ini tidak selalu membawa kepada penyelesaian yang elegan. Ya, anda boleh menukar klien rangkaian anda menjadi aplikasi multithreaded, tetapi seringkali jumlah kerja tambahan yang diperlukan untuk melakukan ini adalah melarang. Ini menjadikan kodnya lebih kompleks, dan ketika menulis hanya aplikasi rangkaian sederhana, jumlah usaha yang diperlukan sukar untuk dibenarkan. Sekiranya anda menulis banyak aplikasi rangkaian, anda akan mendapati roda itu sering mencipta semula. Namun, ada jalan penyelesaian yang lebih mudah.

Saya telah menulis kelas mudah digunakan semula yang boleh anda gunakan dalam aplikasi anda sendiri. Kelas menghasilkan sambungan soket TCP tanpa berhenti untuk jangka masa yang lama. Anda hanya memanggil getSocketkaedah, menentukan nama host, port, dan timeout delay, dan menerima soket. Contoh berikut menunjukkan permintaan sambungan:

// Sambungkan ke pelayan jauh dengan nama hos, dengan sambungan Soket empat masa tamat = TimedSocket.getSocket ("server.my-network.net", 23, 4000); 

Sekiranya semuanya berjalan lancar, soket akan dikembalikan, sama seperti java.net.Socketpembina standard . Sekiranya sambungan tidak dapat dibuat sebelum tamat waktu yang ditentukan, kaedah akan berhenti, dan akan melemparkan java.io.InterruptedIOException, seperti operasi pembacaan soket yang lain apabila waktu tamat telah ditentukan menggunakan setSoTimeoutkaedah. 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 kaedah ini menyediakan antara muka terkawal untuk SocketThreadcontoh, tanpa membenarkan pengubahsuaian luaran pemboleh ubah anggota peribadi. Contoh kod berikut menunjukkan run()kaedah utas. Bila, dan jika, konstruktor soket mengembalikan a Socket, ia akan diberikan kepada pemboleh ubah anggota peribadi, yang mana kaedah aksesor memberikan akses. Pada masa berikutnya keadaan sambungan ditanyakan, menggunakan SocketThread.isConnected()kaedah tersebut, soket akan tersedia untuk digunakan. Teknik yang sama digunakan untuk mengesan kesalahan; jika java.io.IOExceptiontertangkap, ia akan disimpan di anggota peribadi, yang dapat diakses melalui kaedah isError()dan getException()aksesor.