CodeGym /Kurslar /JAVA 25 SELF /NIO Channels və ByteBuffer

NIO Channels və ByteBuffer

JAVA 25 SELF
Səviyyə, Dərs
Mövcuddur

1. NIO Channels-ə giriş

Klassik Java IO-da (java.io) hər şey “bir axın — bir fayl və ya resurs” prinsipinə əsaslanır. Oxuma və ya yazma başlayar-başlamaz, axın bloklanır və əməliyyatın bitməsini gözləyir. Sadə hallarda bu rahatdır, lakin yüklü sistemlərdə bu yanaşma dar boğaza çevrilir: əlaqələr minlərlədirsə, minlərlə axın gözləmə ilə məşğul olur.

NIO-da (New IO) yanaşma fərqlidir. Burada giriş-çıxış bloklanmayan ola bilər və axın boşuna dayanmalı deyil. Bəzi məlumatlar hələ gəlməkdə ikən, o, başqa vəzifəyə keçə bilər. Bu, həqiqətən az sayda axınla çoxlu sayda bağlantını xidmət etməyə imkan verir.

Fərq detallarda da görünür. “Köhnə” IO-da iş axınlar ətrafında qurulur: onlar baytları və ya simvolları oxuyur və yazır, lakin əməliyyatlar zamanı həmişə bloklanır. NIO-da isə əsas anlayışlar kanallar (Channels) və buferlər (Buffers) olur. Onlar serverlər üçün vacib olan bloklanmayan giriş-çıxışı reallaşdırmağa, eləcə də zero-copy texnikasından istifadə etməyə imkan verir — məlumatlar artıq köçürmələrdən yan keçərək birbaşa ötürülür.

Müqayisə: axınlar (Streams) vs kanallar (Channels)

Axınlar (InputStream/OutputStream):

  • Baytları tək-tək və ya massivlərlə oxuyur/yazırlar.
  • Fayldakı mövqe üzərində birbaşa nəzarət yoxdur.
  • Çox böyük fayllarla effektiv iş yoxdur.

Kanallar (Channel):

  • Məlumatı buferlər (Buffer) vasitəsilə oxuyur/yazırlar.
  • Pozisiyanı idarə etmək mümkündür (təsadüfi giriş daxil olmaqla).
  • Asinxronluğu və bloklanmayan rejimi dəstəkləyirlər.
  • Sürətli köçürmə üçün zero-copy istifadə etməyə imkan verirlər.

2. FileChannel və SeekableByteChannel

Buferlərdən istifadə edərək məlumatların oxunması və yazılması

FileChannel — fayllarla işləmək üçün əsas kanaldır. Onu FileInputStream, FileOutputStream-dan və ya NIO.2 — Files.newByteChannel vasitəsilə ( SeekableByteChannel qaytarır) əldə etmək olar.

Nümunə: FileChannel və ByteBuffer ilə faylın oxunması

import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelReadExample {
    public static void main(String[] args) throws Exception {
        try (RandomAccessFile file = new RandomAccessFile("data.txt", "r");
             FileChannel channel = file.getChannel()) {

            ByteBuffer buffer = ByteBuffer.allocate(1024); // 1 KB-lik bufer

            int bytesRead = channel.read(buffer); // buferə oxuyuruq
            while (bytesRead != -1) {
                buffer.flip(); // buferi oxu rejiminə keçiririk
                while (buffer.hasRemaining()) {
                    System.out.print((char) buffer.get());
                }
                buffer.clear(); // növbəti oxu üçün buferi təmizləyirik
                bytesRead = channel.read(buffer);
            }
        }
    }
}

Fayla yazma:

try (RandomAccessFile file = new RandomAccessFile("output.txt", "rw");
     FileChannel channel = file.getChannel()) {

    ByteBuffer buffer = ByteBuffer.wrap("Hello, NIO!\n".getBytes());
    channel.write(buffer);
}

NIO.2: kanalı Files.newByteChannel ilə açmaq

import java.nio.file.*;
import java.nio.channels.SeekableByteChannel;
import static java.nio.file.StandardOpenOption.*;

Path path = Paths.get("data.txt");
try (SeekableByteChannel ch = Files.newByteChannel(path, READ)) {
    ByteBuffer buf = ByteBuffer.allocate(256);
    ch.read(buf);
}

Pozisiyalaşdırma (position()) və ölçünün dəyişdirilməsi (truncate())

  • position() — faylda cari mövqeyi öyrənməyə və ya təyin etməyə imkan verir (təxmini “kursor” kimidir).
  • truncate(long size) — faylı göstərilən ölçüyə qədər kəsir.
channel.position(100);   // 100-cü bayta keçirik
channel.truncate(1024);  // faylı 1 KB-ə qədər kəsirik

Birbaşa və pozision giriş fayllara

  • Birbaşa giriş: faylın istənilən yerindən oxumaq/yazmaq olar, təkcə ardıcıl deyil.
  • Pozision giriş: kanalın cari pozisiyasını dəyişmədən konkret mövqedən oxumaq/yazmaq olar.
ByteBuffer buffer = ByteBuffer.allocate(4);
channel.read(buffer, 128);   // 128 mövqeyindən 4 bayt oxuyuruq, channel.position() dəyişmədən

3. ByteBuffer: bu necə işləyir

Əsas parametrlər: capacity, limit, position, mark

  • capacity — buferin maksimal ölçüsü (yaradılarkən təyin olunur).
  • limit — oxuma/yazma sərhədi (susmaya görə capacity ilə bərabərdir).
  • position — cari mövqe (hara yazırıq/haradan oxuyuruq).
  • mark — sonradan qayıda biləcəyiniz bir “nişan”.

Buferin həyat dövrü:

  1. Məlumatı buferə yazırıq (məsələn, kanaldan read() ilə oxuyuruq).
  2. flip() — buferi oxu rejiminə keçiririk (position = 0, limit = cari position).
  3. Buferdən məlumat oxuyuruq (get()).
  4. clear() — növbəti yazı üçün buferi təmizləyirik (position = 0, limit = capacity).

Nümunə:

ByteBuffer buffer = ByteBuffer.allocate(8);
buffer.put((byte) 42);
buffer.flip();                // indi oxumaq olar
byte value = buffer.get();    // 42
buffer.clear();               // yeni yazıya hazırdır

Buferlərin yaradılması: allocate() vs allocateDirect()

ByteBuffer-in bufer yaratmaq üçün iki əsas yolu var və fərq praktikada hiss olunur. Metod allocate() buferi JVM heap-də yerləşdirir: tez yaradılır və əksər tapşırıqlar üçün uyğundur, lakin natıv giriş-çıxışda heap ilə OS yaddaşı arasında əlavə köçürmələr baş verə bilər.

Metod allocateDirect() JVM heap-dən kənarda (“native memory”) yaddaş ayırır. Belə buferin yaradılması daha bahalıdır və idarəsi çətindir, lakin böyük faylların oxunması/yazılması və ya şəbəkə əməliyyatlarında çox vaxt əlavə köçürmələr olmadığı üçün daha sürətlidir.

Fikir sadədir: böyük həcmdə performans vacibdirsə — “birbaşa” buferlərdən istifadə edin. Kiçik və tez-tez əməliyyatlar üçün onların yaradılma xərcləri faydanı üstələyə bilər.

ByteBuffer directBuffer = ByteBuffer.allocateDirect(4096);

4. Yüksək performanslı əməliyyatlar: transferTo() və transferFrom()

Metodlar transferTo() və transferFrom()

FileChannel sinfində “sıfır köçürmə” prinsipi ilə işləməyə imkan verən iki metod var — transferTo() və transferFrom(). Onların ideyası ondan ibarətdir ki, məlumatları birbaşa fayl kanalları arasında və ya, məsələn, fayl ilə şəbəkə arasında ötürmək olar. JVM demək olar ki, iştirak etmir: əməliyyat OS tərəfindən icra olunur və Java daxilindəki buferlərə toxunulmur.

Nəticədə böyük faylların kopyalanması nəzərəçarpacaq dərəcədə sürətli işləyir: daha az köçürmə, user space ilə kernel space arasında daha az keçid, prosessora daha az yük.

Nümunə: zero-copy vasitəsilə faylın kopyalanması

import java.nio.channels.FileChannel;
import java.nio.file.*;

public class ZeroCopyExample {
    public static void main(String[] args) throws Exception {
        try (FileChannel src = FileChannel.open(Paths.get("input.bin"), StandardOpenOption.READ);
             FileChannel dst = FileChannel.open(Paths.get("output.bin"), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {

            long size = src.size();
            long transferred = src.transferTo(0, size, dst);
            System.out.println("Kopyalanan baytlar: " + transferred);
        }
    }
}

Zero-copy nə zaman həqiqətən işləyir?

  • Eyni diskdə fayllar arasında kopyalama zamanı.
  • Fayllar şəbəkə ilə göndərilərkən (məsələn, SocketChannel vasitəsilə).
  • OS zero-copy dəstəklədikdə (Linux, macOS, Windows — dəstəkləyir).

Üstünlüklər:

  • Minimum köçürmə: məlumatlar JVM buferlərindən keçmir.
  • Yüksək sürət: daha az keçid və CPU yükünün azalması.
  • Daha az yaddaş: böyük istifadəçi buferlərinə ehtiyac yoxdur.

Nümunə: “bir sətrə” fayl kopyalama

Files.copy(Paths.get("input.bin"), Paths.get("output.bin"), StandardCopyOption.REPLACE_EXISTING);
// Mümkün olduqda daxilən zero-copy istifadə oluna bilər

5. Tipik səhvlər

Səhv №1: buferdən oxumadan əvvəl flip() çağırılmayıb. Buferə yazdıqdan sonra mütləq flip() çağırın, əks halda oxu gözlənildiyi kimi işləməyəcək: position/limit “yazma rejimi”ndə qalacaq.

Səhv №2: allocateDirect() kiçik əməliyyatlar üçün istifadə olunur. Direct-buferlər böyük həcmlər üçün yaxşıdır, lakin xırda sorğular üçün onların yaradılması əsassız dərəcədə bahadır. Susmaya görə allocate() seçin.

Səhv №3: kanal bağlanmayıb. Deskriptor sızmalarının qarşısını almaq üçün kanallar və axınlar üçün həmişə try-with-resources istifadə edin.

Səhv №4: position/limit/capacity qarışdırılır. Oxu/yazmadan əvvəl buferin hansı rejimdə olduğuna əmin olun: yazıdan sonra flip(), oxudan sonra yeni yazı üçün — clear() və ya compact() lazımdır.

Səhv №5: zero-copy-nin “həmişə işlədiyini” güman etmək. Bəzi konfiqurasiyalarda (fərqli qurğular/müxtəlif fayl sistemləri/xüsusi flaqlar) zero-copy əlçatan olmaya bilər — o zaman adi kopyalama baş verəcək və performans fərqli olacaq.

1
Tapşırıq
JAVA 25 SELF, səviyyə, dərs
Bağlanıb
Məlumat cərrahiyyəsi: İkili parşömenin heykəlləşdirilməsi ✂️
Məlumat cərrahiyyəsi: İkili parşömenin heykəlləşdirilməsi ✂️
1
Tapşırıq
JAVA 25 SELF, səviyyə, dərs
Bağlanıb
Rəqəmsal kuryer: Nüsxələrin dərhal çatdırılması 📦
Rəqəmsal kuryer: Nüsxələrin dərhal çatdırılması 📦
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION