Pengetahuan latar belakang
Sinkron, asinkron, memblokir, tidak memblokir
Pertama -tama, konsep -konsep ini sangat mudah dibingungkan, tetapi mereka terlibat dalam NIO, jadi mari kita ringkasnya.
Sinkronisasi: Ketika panggilan API kembali, penelepon tahu bagaimana operasi dihasilkan (berapa banyak byte yang sebenarnya dibaca/ditulis).
Asynchronous: Dibandingkan dengan sinkronisasi, penelepon tidak tahu hasil operasi ketika panggilan API kembali, dan panggilan balik akan memberi tahu hasilnya nanti.
Pemblokiran: Ketika tidak ada data yang harus dibaca atau semua data tidak dapat ditulis, menangguhkan utas saat ini menunggu.
Non-blocking: Saat membaca, Anda dapat membaca sebanyak data yang dapat Anda baca dan kemudian kembali. Saat menulis, Anda dapat menulis sebanyak data yang dapat Anda tulis dan kemudian kembali.
Untuk operasi I/O, menurut dokumen situs web resmi Oracle, standar divisi sinkronisasi dan asinkron adalah "apakah penelepon perlu menunggu operasi I/O selesai". Ini "menunggu operasi I/O ini selesai" tidak berarti bahwa data harus dibaca atau semua data harus ditulis, tetapi apakah penelepon perlu menunggu ketika operasi I/O benar -benar dilakukan, seperti waktu ketika data ditransmisikan antara buffer tumpukan protokol TCP/IP dan buffer JVM.
Oleh karena itu, metode baca () dan write () yang umum digunakan adalah I/O sinkron. I/O sinkron dibagi menjadi dua mode: pemblokiran dan non-blocking. Jika itu adalah mode non-blocking, itu akan dikembalikan secara langsung ketika mendeteksi bahwa tidak ada data untuk dibaca, dan operasi I/O tidak benar-benar dilakukan.
Singkatnya, di Java, sebenarnya hanya ada tiga mekanisme: pemblokiran sinkron I/O, I/O non-blocking sinkron dan asinkron I/O. Apa yang kita bicarakan di bawah ini adalah dua yang pertama. JDK1.7 telah mulai memperkenalkan I/O asinkron, yang disebut NiO.2.
IO tradisional
Kita tahu bahwa kemunculan teknologi baru selalu disertai dengan perbaikan dan perbaikan, dan begitu pula kemunculan Javanio.
I/O tradisional memblokir I/O, dan masalah utamanya adalah pemborosan sumber daya sistem. Misalnya, untuk membaca data koneksi TCP, kami memanggil metode inputstream baca (), yang akan menyebabkan utas saat ini ditangguhkan dan tidak akan dibangunkan sampai data tiba. Utas menempati sumber daya memori (tumpukan utas penyimpanan) selama periode kedatangan data, tetapi tidak melakukan apa pun. Inilah yang dikatakan pepatah, menempati lubang dan bukan kotoran. Untuk membaca data koneksi lain, kita harus memulai utas lain. Ini mungkin baik ketika tidak ada banyak koneksi bersamaan, tetapi ketika jumlah koneksi mencapai skala tertentu, sumber daya memori akan dikonsumsi oleh sejumlah besar utas. Di sisi lain, switching utas membutuhkan perubahan status prosesor, seperti nilai -nilai penghitung dan register program, sehingga beralih di antara sejumlah besar utas juga merupakan pemborosan sumber daya.
Dengan pengembangan teknologi, sistem operasi modern menyediakan mekanisme I/O baru yang dapat menghindari pemborosan sumber daya ini. Berdasarkan hal ini, Javanio lahir, dan fitur perwakilan NIO tidak memblokir I/O. Segera setelah itu, kami menemukan bahwa hanya menggunakan I/O non-blocking tidak dapat menyelesaikan masalah, karena dalam mode non-blocking, metode baca () akan segera kembali ketika data tidak dibaca. Kami tidak tahu kapan data akan tiba, sehingga kami hanya dapat terus menelepon metode baca () untuk mencoba lagi. Ini jelas merupakan pemborosan sumber daya CPU. Dari yang berikut ini, kita dapat mengetahui bahwa komponen pemilih dilahirkan untuk menyelesaikan masalah ini.
Komponen inti Javanio
1. Channel
konsep
Semua operasi I/O di Javanio didasarkan pada objek saluran, seperti halnya operasi aliran didasarkan pada objek aliran, jadi perlu untuk memahami saluran apa itu terlebih dahulu. Konten berikut dikutip dari dokumentasi JDK1.8
Achannel mewakili koneksi Lampiran ke Komponen SuchasahardWaredEvice, Afile, Annetworksocket, Oroprogram yang dapat dilakukan pada satu oPi/ooperasi khas ormory, Forexamplereading Orwriting.
Dari konten di atas, kita dapat melihat bahwa saluran mewakili koneksi ke entitas tertentu, yang dapat berupa file, soket jaringan, dll. Dengan kata lain, saluran adalah jembatan yang disediakan oleh Javanio untuk program kami untuk berinteraksi dengan layanan I/O yang mendasari dari sistem operasi.
Saluran adalah deskripsi yang sangat mendasar dan abstrak, berinteraksi dengan layanan I/O yang berbeda, melakukan operasi I/O yang berbeda, dan mengimplementasikan implementasi yang berbeda. Oleh karena itu, yang spesifik termasuk filechannel, socketchannel, dll.
Saluran mirip dengan aliran saat digunakan. Ini dapat membaca data ke dalam buffer atau menulis data di buffer ke saluran.
Tentu saja, ada juga perbedaan, yang terutama tercermin dalam dua poin berikut:
Saluran dapat dibaca dan ditulis, saat aliran satu arah (jadi dibagi menjadi inputstream dan outputstream)
Saluran memiliki mode I/O non-blocking
menyelesaikan
Implementasi saluran yang paling umum digunakan di Javanio adalah sebagai berikut, dan dapat dilihat bahwa mereka sesuai dengan kelas operasi I/O tradisional satu per satu.
FileChannel: baca dan tulis file
Datagramchannel: Komunikasi Jaringan Protokol UDP
Socketchannel: Komunikasi Jaringan Protokol TCP
ServerSocketchannel: Dengarkan Koneksi TCP
2.Buffer
Buffer yang digunakan dalam NIO bukan array byte sederhana, tetapi kelas buffer yang dienkapsulasi. Melalui API yang disediakannya, kita dapat memanipulasi data secara fleksibel. Mari kita lihat lebih dekat.
Sesuai dengan tipe dasar Java, NIO menyediakan berbagai jenis buffer, seperti Bytebuffer, Charbuffer, IntBuffer, dll. Perbedaannya adalah bahwa panjang unit buffer berbeda saat membaca dan menulis (membaca dan menulis dalam satuan variabel tipe yang sesuai).
Ada 3 variabel yang sangat penting dalam buffer. Mereka adalah kunci untuk memahami mekanisme kerja buffer, yaitu
kapasitas (kapasitas total)
Posisi (posisi pointer saat ini)
Batas (Posisi Batas Baca/Tulis)
Metode kerja buffer sangat mirip dengan array karakter dalam C. Dalam analogi, kapasitas adalah total panjang array, posisi adalah variabel subskrip bagi kita untuk membaca/menulis karakter, dan batas adalah posisi karakter akhir. Situasi 3 variabel di awal buffer adalah sebagai berikut
Selama proses membaca/menulis buffer, posisi akan bergerak mundur, dan batas adalah batas gerakan posisi. Tidak sulit membayangkan bahwa ketika menulis ke buffer, batas harus diatur ke ukuran kapasitas, dan ketika membaca ke buffer, batas harus diatur ke posisi akhir aktual data. (Catatan: Menulis Data Buffer ke Saluran adalah Operasi Baca Buffer, dan Membaca Data dari Saluran ke Buffer adalah Operasi Penulisan Buffer)
Sebelum membaca/menulis operasi pada buffer, kami dapat memanggil beberapa metode tambahan yang disediakan oleh kelas buffer untuk mengatur nilai posisi dan batas dengan benar, terutama sebagai berikut
flip (): Setel batas ke nilai posisi, lalu atur posisi ke 0. Panggilan sebelum membaca buffer.
Rewind (): Cukup atur posisi 0. Biasanya dipanggil sebelum membaca ulang data buffer, misalnya, itu akan digunakan saat membaca data buffer yang sama dan menulisnya ke beberapa saluran.
Clear (): Kembali ke keadaan awal, yaitu batas sama dengan kapasitas, posisi yang ditetapkan ke 0. Panggil buffer sebelum menulis.
Compact (): Pindahkan data yang belum dibaca (data antara posisi dan batas) ke awal buffer dan atur posisi ke posisi berikutnya di akhir data ini. Bahkan, setara dengan menulis data seperti itu ke buffer lagi.
Kemudian, lihat contoh, gunakan FileChannel untuk membaca dan menulis file teks, dan gunakan contoh ini untuk memverifikasi karakteristik saluran yang dapat dibaca dan dapat ditulis dan penggunaan dasar buffer (perhatikan bahwa filechannel tidak dapat diatur ke mode non-blocking).
Filechannel channel = new randomAccessFile ("test.txt", "rw"). Getchannel (); channel.position (channel.size ()); // pindahkan pointer file ke akhir (append write) bytebuffer bytebuffer = bytebuffer.allocate (20); // write data ke BufferbyTeFer = byteBuffer. dunia!/n ".getbytes (standardcharsets.utf_8)); // buffer -> channelbytebuffer.flip (); while (bytebuffer.hasremaining ()) {channel.write (bytebuffer);} channel.position (0);//Pindahkan file pointer ke awal dari awal dari awal); CharsetDecoder decoder = standardcharsets.utf_8.newDecoder (); // Baca semua data bytebuffer.clear (); while (channel.read (byteBuffer)! = -1 || byteBuffer.position ()> 0) {byteBuffer.flip (); // decode charbuffer.clear (); decoder.decode (bytebuffer, charbuffer, false); System.out.print (charbuffer.flip (). ToString ()); bytebuffer.compact (); // Mungkin ada data yang tersisa} channel.close ();Dalam contoh ini, dua buffer digunakan, di mana Bytebuffer adalah buffer data untuk membaca dan menulis saluran, dan charbuffer digunakan untuk menyimpan karakter yang diterjemahkan. Penggunaan clear () dan flip () seperti yang disebutkan di atas. Perlu dicatat bahwa metode compact () terakhir adalah, bahkan jika ukuran charbuffer sepenuhnya cukup untuk mengakomodasi bytebuffer data yang diterjemahkan, compact ini () juga penting. Ini karena pengkodean UTF-8 dari karakter Cina yang umum digunakan untuk 3 byte, sehingga ada kemungkinan besar bahwa hal itu akan terjadi di pemotongan tengah. Silakan lihat gambar di bawah ini:
Ketika dekoder membaca 0xe4 di akhir buffer, itu tidak dapat dipetakan ke unicode. Parameter ketiga dari metode decode (), false, digunakan untuk membuat decoder memperlakukan byte yang tidak dapat dibatalkan dan data selanjutnya sebagai data tambahan. Oleh karena itu, metode decode () akan berhenti di sini, dan posisi akan kembali ke posisi 0xe4. Dengan cara ini, byte pertama yang dikodekan oleh kata "medium" dibiarkan dalam buffer, dan harus dipadatkan ke depan dan disambung bersama dengan data urutan yang benar dan selanjutnya. Mengenai pengkodean karakter, Anda dapat merujuk pada " Penjelasan ANSI, UNICODE, BMP, UTF dan konsep pengkodean lainnya "
BTW, CharsetDecoder dalam contoh juga merupakan fitur baru Javanio, jadi Anda seharusnya menemukan sedikit. Operasi NIO berorientasi buffer (I/O tradisional berorientasi aliran).
Pada titik ini, kami telah belajar tentang penggunaan dasar saluran dan buffer. Selanjutnya, kita akan berbicara tentang komponen penting untuk membiarkan utas mengelola beberapa saluran.
3.elektor
Apa itu pemilih
Selector adalah komponen khusus yang digunakan untuk mengumpulkan keadaan (atau peristiwa) dari setiap saluran. Kami pertama -tama mendaftarkan saluran ke pemilih dan mengatur acara yang kami pedulikan, dan kemudian kami dapat dengan tenang menunggu acara tersebut terjadi dengan memanggil metode Select ().
Saluran ini memiliki 4 acara berikut untuk kita dengarkan:
Terima: Ada koneksi yang dapat diterima
Hubungkan: Hubungkan dengan sukses
BACA: Ada data untuk dibaca
Tulis: Anda dapat menulis data
Mengapa menggunakan pemilih
Seperti disebutkan di atas, jika Anda menggunakan pemblokiran I/O, Anda perlu multi-thread (buang-buang memori), dan jika Anda menggunakan I/O yang tidak memblokir, Anda harus terus-menerus mencoba lagi (konsumsi CPU). Munculnya pemilih memecahkan masalah yang memalukan ini. Dalam mode non-blocking, melalui pemilih, utas kami hanya berfungsi untuk saluran siap, dan tidak perlu mencoba secara membabi buta. Misalnya, ketika tidak ada data yang dicapai di semua saluran, tidak ada peristiwa baca yang terjadi, dan utas kami akan ditangguhkan pada metode Select (), sehingga menyerahkan sumber daya CPU.
Cara menggunakan
Seperti yang ditunjukkan di bawah ini, buat pemilih dan daftarkan saluran.
Catatan: Untuk mendaftarkan saluran ke pemilih, Anda harus terlebih dahulu mengatur saluran ke mode non-blocking, jika tidak pengecualian akan dilemparkan.
Selector selector = selector.open (); channel.configureblocking (false); selectionKey key = channel.register (selector, selectionkey.op_read);
Parameter kedua dari metode register () disebut "set bunga", yang merupakan set acara yang Anda khawatirkan. Jika Anda peduli dengan beberapa acara, pisahkan dengan "bital atau operator", mis.
SelectionKey.op_read | SelectionKey.op_write
Metode penulisan ini tidak terbiasa dengannya. Ini dimainkan dalam bahasa pemrograman yang mendukung operasi bit. Menggunakan variabel integer dapat mengidentifikasi banyak negara. Bagaimana cara melakukannya? Ini sebenarnya sangat sederhana. Misalnya, pertama -tama telah ditentukan sebelumnya beberapa konstanta, dan nilainya (biner) adalah sebagai berikut
Dapat ditemukan bahwa bit dengan nilainya dari 1 semuanya terhuyung -huyung, sehingga nilai yang diperoleh setelah melakukan bitwise atau perhitungan pada mereka tidak memiliki ambiguitas, dan mereka dapat disimpulkan secara terbalik dari variabel mana yang dihitung. Cara menilai, ya, itu adalah operasi "bit dan". Misalnya, sekarang ada nilai variabel set status 0011. Kita hanya perlu menentukan apakah nilai "0011 & op_read" adalah 1 atau 0 untuk menentukan apakah set tersebut berisi status OP_READ.
Kemudian, perhatikan bahwa metode register () mengembalikan objek seleksi, yang berisi informasi untuk pendaftaran ini, dan kami juga dapat memodifikasi informasi pendaftaran melalui itu. Dari contoh lengkap di bawah ini, kita dapat melihat bahwa setelah SELECT (), kami juga mendapatkan saluran dengan status siap dengan mendapatkan kumpulan selectionkeys.
Contoh lengkap
Konsep dan hal -hal teoretis telah dijelaskan (sebenarnya, setelah menulisnya di sini, saya menemukan bahwa saya tidak banyak menulis, yang sangat memalukan (⊙ˍ⊙)). Mari kita lihat contoh lengkap.
Contoh ini menggunakan Javanio untuk mengimplementasikan server utamanya. Fungsinya sangat sederhana. Itu mendengarkan koneksi klien. Ketika koneksi dibuat, ia membaca pesan klien dan menanggapi pesan kepada klien.
Perlu dicatat bahwa saya menggunakan karakter '/0' (byte dengan nilai 0) untuk mengidentifikasi akhir pesan.
Server berulir tunggal
Public Class NioServer {public static void main (string [] args) melempar ioException {// Buat selectorSelector selector = selector.open (); // menginisialisasi saluran koneksi tcp serversocketchannel listenchannel = serversocketchannel.open (); listenchannel.bind (inetsocketaddress baru (9999)); listenchannel.configureblocking (false); // Daftarkan ke selector (dengarkan peristiwa penerimaannya) listenchannel.register (selector, selection.op_accept); // Buat buffer bytebuffer buffer = byteBuffer.allocate (100); while (true) {selector.select (); // blok sampai suatu peristiwa didengarkan terjadi iterator <fectionKey> keyiter = selector.selectedkeys (). iterator (); // akses acara saluran yang dipilih melalui iTerator ke avonel (KEYITER.HASNEXT ()) {selectionKey KEY = KEYITER.NEXT (); channel = ((serversocketchannel) key.channel ()). AcCECT (); channel.configurblocking (false); channel.register (selector, selection.op_read); System.out.println ("Koneksi yang ditetapkan dengan [" + channel.getRemoteAddress () + "]!");} lainnya (" + channel.getRemoteAddress () +"]! ");} lainnya (" + KEYGET.GETREMOTEADDRESS () + "]!");} lainnya (" + KEYGET.GETREMOTEADDress () +" buffer.clear (); // baca sampai akhir aliran, menunjukkan bahwa koneksi TCP telah terputus, // Oleh karena itu, perlu untuk menutup saluran atau membatalkan mendengarkan acara baca // sebaliknya, itu akan loop tanpa batas jika ((by socketchan) key.channel (). buffer.flip (); while (buffer.hasremaining ()) {byte b = buffer.get (); if (b == 0) {// /0system.out.println () di akhir pesan klien; // respons klien buffer.clear (); buffer.put ("hello, klien!/0" .getbytes ()); Buffer.put ("Hello, Client!/0" .getbytes ()); {((Socketchannel) key.channel ()). Tulis (buffer);}} else {System.out.print ((char) b);}}} // Untuk peristiwa yang telah diproses, Anda harus menghapus keyiter.remove ();}}}} secara manual secara manual secara manual ();Klien
Klien ini murni digunakan untuk pengujian. Untuk membuatnya kurang sulit, ia menggunakan metode penulisan tradisional, dan kodenya sangat singkat.
Jika Anda perlu lebih ketat dalam pengujian, Anda harus menjalankan sejumlah besar klien secara bersamaan untuk menghitung waktu respons server, dan tidak mengirim data segera setelah koneksi dibuat, sehingga memberikan permainan penuh untuk keuntungan I/O yang tidak diblokir di server.
Klien kelas publik {public static void main (string [] args) melempar pengecualian {socket socket = socket baru ("localhost", 9999); inputStream adalah = socket.getInputStream (); outputStream os = socket.getutStream ();/ int b; while ((b = is.read ())! = 0) {System.out.print ((char) b);} system.out.println (); socket.close ();}}Meringkaskan
Di atas adalah semua tentang pemahaman cepat tentang komponen inti NIO di Java. Saya harap ini akan membantu semua orang. Teman yang tertarik dapat terus merujuk ke konten lain yang relevan dari situs web ini. Jika ada kekurangan, silakan tinggalkan pesan untuk menunjukkannya. Terima kasih teman atas dukungan Anda untuk situs ini!