File-descriptor merupakan hal yang sering Kita jumpai di bahasa pemrograman C pada platform UNIX/POSIX ketika melakukan akses terhadap resource, baik itu file fisik ataupun virtual-file resource seperti melakukan komunikasi dengan serial, pipe dan tty. Bahkan standard input/output/error (stdout, stdin, stderr) sebenarnya merupakan pipe yang menggunakan file-descriptor ini.

Secara definisi File descriptor adalah unique number untuk mengidentifikasi file yang dibuka oleh sistem operasi. Sebagai penanda, dan cara resource itu agar dapat diakses.

Pada penjelasan kali ini, tidak akan membahas lebih detail tentang file-descriptor, tapi memberikan tips untuk hal yang sering diperlukan ketika berurusan dengan file-descriptor yang berhubungan dengan komunikasi dua arah (Bukan membaca file), dimana Kita ingin melakukan read/membaca konten komunikasi tapi dengan batas waktu tertentu. Bila batas waktu yang ditentukan terlewat, maka fungsi read ini harus selesai dan memberikan suatu nilai yang dapat digunakan.

Seperti Kita ketahui, fungsi read() pada file-descriptor akan terus menunggu sampai ada konten yang diterima, bayangkan bila koneksi misalnya dengan serial terputus dan tidak ada lagi data masuk, maka fungsi read() ini tidak akan pernah selesai.

Karena permasalahan ini, diperlukan cara lain untuk membatasi waktu tunggu read() ini, tapi sayangnya tidak ada fungsi bawaan C yang dapat langsung digunakan. Dikarenakan read() tidak dapat di interrupt/dibatalkan (walaupun file-descriptor menjadi tidak valid setelah read() dipanggil), maka cara terbaik adalah dengan melakukan select() untuk melakukan cek apakah ada buffer yang masuk atau tidak. Karena fungsi select() memiliki argument untuk timeout.

Oleh sebab itu Saya sudah membuat fungsi yang hampir identik dengan read() hanya ditambah satu argument tambahan berupa timeout dalam miliseconds, selanjutnya proses menunggu akan dihandle oleh select(), dan bila ada konten pada file-descriptor, fungsi ini akan langsung memanggil read() seperti pada source berikut ini:

/**
 * Read dengan timeout
 * 
 * @param int fd file descriptor
 * @param void* buf output buffer
 * @param size_t sz output buffer size
 * @param int timeout dalam miliseconds
 * @return int 0 Timeout, < 1 error, > 0 Jumlah terbaca
 */
int read_timeout(int fd, void *buf, size_t sz, int timeout) {
  // Persiapkan fd_set, karena select meminta fd_set bukan fd
  fd_set set;
  FD_ZERO(&set);
  FD_SET(fd, &set);

  // Persiapkan timeout. select() meminta timeval bukan miliseconds
  struct timeval tv;
  tv.tv_sec = timeout / 1000;
  tv.tv_usec = (timeout - (tv.tv_sec * 1000)) * 1000;

  // Panggil select()
  // Perintah ini akan menunggu sampai ada buffer atau timeout
  int rv = select(fd + 1, &set, NULL, NULL, &tv);
  
  // Bila return dari select < 1, maka timeout
  // Return 0
  if (rv < 1) return 0;
  
  // Bila return >= 1, panggil read
  return read(fd, buf, sz);
}

Pada source code di atas, dipersiapkan dua variabel tambahan yang bertipe fd_set dan struct timeval, dikarenakan select() meminta timeout berupa struktur timeval, maka Kita konversi dari miliseconds ke timeval. Dan untuk fd_set sebenarnya fungsi select() ini tujuannya untuk memilih beberapa/kumpulan file-descriptor, yang mana yang memiliki response/buffer duluan, maka yang itu lah yang akan di proses, jadi dia tidak menerima file-descriptor. Karena itu fungsi ini akan membuat fd_set yang hanya berisi satu saja file-descriptor.

Untuk penggunaannya hampir sama dengan read() biasa, tapi dengan tambahan argumen dan beberapa workaround yang dapat dilakukan seperti pada contoh cek status kertas pada printer pos berikut ini:

int cek_status_printer(){
  // buka file-descriptor untuk printer
  int fd = open("/dev/usb/lp0", O_RDWR | O_NOCTTY);
  if (fd <= 0) return -1;
  
  // Kirim command cek status kertas
  write(fd, "\x1D\x72\x01", 3);
  
  char paper_status = 0;
  
  // Baca response dengan timeout 1 detik
  int len = read_timeout(fd, &paper_status, 1, 1000);
  
  if (len < 0)
    printf("I/O ERROR\n");
  else if (len == 0)
    printf("TIMEOUT\n");
  else{
    printf("Status Kertas: %02X\n", paper_status);
    close(fd);
    return paper_status;
  }
  close(fd);
  return 0;
}

Pada contoh di atas, terlihat bahwa Kita dapat mengetahui ketika printer dikirimkan command cek status kertas dan tidak mengirimkan balasan setelah 1 detik menandakan ada permasalahan dengan koneksi, maka Kita dapat memberikan informasi atau handle/workaround pada program Kita.

Penutup dan Sedikit Tambahan Untuk Socket

Itu saja bahasan kali ini, yang menurut Saya walaupun sedikit tapi fungsi ini sangat berguna terutama bagi programmer yang sering memprogram peralatan-peralatan yang terhubung lewat serial dan sebagainya.

Bahkan logic yang sama sebenarnya dapat digunakan juga untuk socket dan recv(), dengan mengganti fungsi read() di atas dengan recv() seperti berikut:

/**
 * Recv dengan timeout
 * 
 * @param int fd file descriptor
 * @param void* buf output buffer
 * @param size_t sz output buffer size
 * @param int flags recv flags
 * @param int timeout dalam miliseconds
 * @return int 0 Timeout, < 1 error, > 0 Jumlah terbaca
 */
int recv_timeout(int sockfd, void *buf, size_t sz, 
    int flags, int timeout) {
  fd_set set;
  FD_ZERO(&set);
  FD_SET(sockfd, &set);

  struct timeval tv;
  tv.tv_sec = timeout / 1000;
  tv.tv_usec = (timeout - (tv.tv_sec * 1000)) * 1000;

  int rv = select(sockfd + 1, &set, NULL, NULL, &tv);
  
  if (rv < 1) return 0;
  return recv(sockfd, buf, sz, flags);
}

Terima Kasih.