 |
 |
Thursday, 26 August 2010
Otomasi Browsing
(3607 total kata pada text ini)
Oleh Edwin Pratomo
Sehari-hari kita sudah terbiasa menggunakan web browser seperti Netscape Navigator, Lynx, atau Internet Explorer jika Anda menggunakan
Windows. Web browser adalah salah satu jenis aplikasi web dari sisi client yang ditujukan untuk dipakai secara interaktif. Jenis aplikasi lainnya adalah web robot [1]. Berbeda dengan web browser, web robot dibuat untuk melakukan browsing secara otomatis, secara unattended. Web robot digunakan untuk bermacam-macam keperluan seperti analisa statistik (netcraft), mirroring (misalkan wget, pavuk), resource discovery (seperti google, altavista), maintenance (check broken links), dan uji performance web server. Tentu saja orang dapat membuat robot untuk melakukan serangan DoS (jangan!), atau untuk mengumpulkan alamat email di web untuk di-spam (juga jangan!).
Sejalan dengan berkembangnya popularitas web, orang membuat web client untuk berbagai keperluan yang beberapa tahun yang lalu belum dirasakan, seperti mengirim SMS message [2], dan juga sebagai bagian dari implementasi message transport XML/RDF [3].
Tulisan ini mencoba memperkenalkan pemrograman web client, diawali dengan tur singkat ke transaksi HTTP, dan diakhiri dengan contoh implementasi web robot dalam bahasa Perl.
HTTP Menggunakan Telnet
Komunikasi antara web client dengan server dilakukan menggunakan protokol HTTP (Hypertext Transfer Protocol). Dalam tulisan ini kita gunakan istilah "HTTP server" yang mengacu pada HTTP daemon itu sendiri, dan bukannya "web server" yang mengacu pada sistem server secara keseluruhan di mana termasuk juga sistem operasi berikut back-end database jika digunakan.
HTTP biasanya menggunakan TCP sebagai transport protocol-nya, dan termasuk protokol tingkat tinggi seperti halnya SMTP, POP3, FTP, dan
NNTP. Saat ini server-server HTTP yang utama (seperti Apache) telah mendukung HTTP versi 1.1. Spesifikasi lengkap HTTP/1.1 dapat dibaca di RFC 2616 [4].
Sebuah transaksi HTTP dimulai dengan client mengirimkan request ke server HTTP, yang kemudian menjawab dengan mengirimkan respons balik ke client. Transaksi berikutnya independen dari transaksi sebelumnya, dengan demikian HTTP disebut sebagai stateless protocol. Kita dapat menggunakan telnet untuk membuat koneksi ke server HTTP serta mengirim request dan menerima respons HTTP sebagaimana yang dilakukan oleh web browser. Karena server HTTP biasanya dijalankan pada port 80, maka kita jalankan perintah telnet sebagai berikut:
$ telnet postfix.cumi.org 80
Gantilah postfix.cumi.org dengan nama mesin yang dikenali pada jaringan lokal Anda, di mana server HTTP dijalankan di mesin tersebut pada port 80. Tentu saja Anda dapat mencobanya juga pada mesin dengan IP riil, seperti www.infolinux.co.id. Jika komputer Anda tidak terhubung ke jaringan, Anda juga dapat mencobanya dengan menginstal sendiri Apache yang biasanya disertakan dalam berbagai distribusi Linux, dan kemudian menjalankan telnet ke localhost.
Berikut ini adalah tampilan yang muncul setelah perintah di atas, yang menunjukkan bahwa koneksi yang dimaksud telah berhasil dibuat:
Trying 10.0.7.21...
Connected to postfix.cumi.org.
Escape character is '^]'.
Kemudian kita ketikkan sebuah request HTTP sebagai berikut (diakhiri dengan penekanan Enter dua kali):
GET / HTTP/1.0
Host: postfix.cumi.org
Baris pertama sebuah request selalu berupa request-line yang terdiri dari tiga bagian dipisahkan oleh spasi, yaitu (dari kiri ke kanan): metode, URI, dan versi HTTP yang didukung client. Request-line dapat diikuti oleh sejumlah header yang sifatnya opsional. Request yang kita kirimkan di atas menunjukkan bahwa client bermaksud mengambil file pada lokasi / di host postfix.cumi.org, dan bahwa client mendukung HTTP versi 1.0. Request ini kurang lebih sama dengan yang dikirim oleh browser jika kita membuka URL http://postfix.cumi.org/ memakai browser.
Respons HTTP yang diterima adalah:
HTTP/1.1 200 OK
Date: Fri, 24 Nov 2000 07:04:22 GMT
Server: Apache/1.3.12 (Unix)
Last-Modified: Tue, 21 Nov 2000 07:18:11 GMT
ETag: "28703-54e-3a1a21b3"
Accept-Ranges: bytes
Content-Length: 1358
Connection: close
Content-Type: text/html
[message-body]
Connection closed by foreign host.
$
Baris pertama sebuah respons selalu berupa status-line yang terdiri dari tiga bagian yang dipisahkan spasi, yaitu (dari kiri ke kanan): versi HTTP, kode status, dan keterangan. Kode status ini menunjukkan hasil dari pemrosesan request oleh server, dan memiliki nilai seperti 200 (OK), 404 (Not Found), 500 (Internal Server Error). Daftar lengkap kode status dan keterangannya dapat dilihat pada RFC 2616 [4]. Status-line dapat diikuti oleh sejumlah header yang sifatnya opsional.
Pada respons di atas kita melihat header Date: yang menunjukkan waktu saat respons tersebut dihasilkan. Header Server: menunjukkan identitas server, yaitu Apache versi 1.3.12. Last-Modified: menunjukkan tanggal terakhir dokumen dimodifikasi. ETag berisi tag entitas dari dokumen yang diminta. Accept-Ranges: menunjukkan bahwa server mengizinkan dokumen diambil secara parsial dalam satuan byte. Content-Length: menunjukkan ukuran message-body. Connection: close menunjukkan bahwa koneksi segera ditutup setelah respons selesai dikirim. Content-Type: menunjukkan tipe media dari dokumen yang dikembalikan, yaitu text/html. Isi dokumen yang diminta terletak setelah baris terakhir header, dipisahkan oleh karakter CR-LF (carriage return + line feed).
Di sini kita melihat bahwa segera setelah respons selesai dikirimkan, koneksi dng server HTTP langsung terputus, yang berarti dibutuhkan satu koneksi TCP untuk setiap request HTTP. Tentu saja ini kurang efisien ketika browser men-download sebuah halaman HTML yang penuh
dengan tag IMG SRC. HTTP versi 1.1 telah mengatasi kelemahan ini dengan mendukung "persistent-connection", di mana sejumlah request HTTP dapat dibuat dalam koneksi TCP yang sama. Kalau kita ulangi lagi request GET tadi, namun kali ini dengan versi HTTP/1.1:
GET / HTTP/1.1
Host: postfix.cumi.org
maka kita akan mendapat respons yang mirip seperti tadi, dengan perbedaan penting:
tidak terdapat baris "Connection: close" pada header respons,
koneksi dengan server tidak langsung terputus setelah respons selesai dikirim, melainkan menunggu sampai batas waktu (timeout) tertentu.
Dengan demikian client berkesempatan mengirim request lagi, misalkan:
GET /manual/ HTTP/1.1
Host: postfix.cumi.org
Connection: close
Tambahan header "Connection: close" di sini merupakan petunjuk bagi server bahwa client tidak mengharapkan koneksi persisten, sehingga
segera setelah respons diterima, koneksi terputus. Browser-browser biasanya masih menggunakan request HTTP/1.0 agar tetap dapat berkomunikasi dengan proxy ataupun server yang hanya mendukung HTTP/1.0. Maka dari itu, untuk memperoleh koneksi persisten, browser menggunakan header Connection: Keep-Alive dalam request-nya:
GET / HTTP/1.0
Host: postfix.cumi.org
Connection: Keep-Alive
Selain GET, metode lainnya yang banyak digunakan adalah POST dan HEAD. HEAD mirip dengan GET, bedanya adalah pada HEAD, server tidak perlu mengirimkan message-body, atau dengan kata lain, client hanya tertarik pada header respons saja.
Pembaca yang tertarik mempelajari metode-metode lainnya atau ingin mempelajari lebih lanjut mengenai transaksi HTTP (tapi malas membaca RFC) dapat membaca buku "Web Client Programming with Perl" [5] yang tersedia gratis untuk di-download.
Lebih Mudah dengan LWP
Pemrograman web client di Perl sudah sangat dipermudah berkat LWP, atau libwww-perl, yang dibuat oleh Gisle Aas [6]. LWP adalah seperangkat modul yang menyediakan antarmuka pemrograman web client, hampir semuanya berorientasi objek. Distribusi LWP juga menyertakan beberapa skrip eksekutabel, yaitu lwp-download, lwp-mirror, lwp-request, lwp-rget, GET, HEAD, dan POST. Modul LWP::Simple yang non OOP cocok untuk menulis quick-and-dirty script, atau one-liner dari prompt shell, seperti:
$ perl -MLWP::Simple -e 'getprint "http://www.infolinux.co.id/"'
Sedangkan modul LWP::UserAgent memiliki fitur yang jauh lebih lengkap daripada LWP::Simple, serta memiliki antarmuka berorientasi objek.
use LWP::UserAgent;
$req = new HTTP::Request('GET', 'http://www.infolinux.co.id/');
$ua = new LWP::UserAgent;
$res = $ua->request($req);
print $res->is_success ? $res->content : $res->status_line;
Untuk mengirimkan request HTTP, sebuah objek LWP::UserAgent memanggil method request() yang membutuhkan objek HTTP::Request sebagai argumennya. Maka dari itu pertama-tama dibuat dulu objek HTTP::Request. Argumen pertama dari konstruktornya adalah metode HTTP yang mau dipakai, dan argumen kedua adalah URI request. Hasil pemanggilan method request() adalah sebuah objek HTTP::Response. Method is_success() dari objek ini mengembalikan 1 jika kode status dari respons yang diterima ada dalam rentang 200 sampai dengan 299. Method content() mengembalikan message body dari respons yang diterima, dan method status_line() mengembalikan string berisi kode status dan keterangan.
Informasi lengkap mengenai LWP::Simple, LWP::UserAgent, HTTP::Request, dan HTTP::Response dapat dilihat di halaman manual masing-masing modul.
Membuat Hyperlink Checker
Kita akan mencoba membuat web robot sederhana untuk memeriksa hyperlink-hyperlink pada sebuah halaman HTML. Di sini kita membutuhkan modul HTML::LinkExtor untuk memudahkan mengekstrak hyperlink dari halaman HTML. Robot kita ini dapat dilihat pada Listing 1.
Listing 1. cb - check broken
1| #!/usr/bin/perl -w
2| $|++;
3| use strict;
4| use Getopt::Std;
5| use HTML::LinkExtor;
6| use LWP::UserAgent;
7| use HTTP::Status;
8| use vars qw($opt_b $opt_p $opt_t $opt_s %all);
9| getopts('b:p:t:s');
10| my $ua = new LWP::UserAgent;
11| $ua->timeout($opt_t || 60);
12| $ua->proxy('http', $opt_p) if $opt_p;
13| my $p = HTML::LinkExtor->new(&cb, $opt_b);
14| sub cb {
15| my($tag, %links) = @_;
16| foreach my $url (values %links) {
17| if (not exists($all{$url})) {
18| my $req = new HTTP::Request('HEAD', $url);
19| my $res = $ua->request($req);
20| $all{$url}++;
21| RETRY:
22| {
23| if ($res->is_success) {
24| print $res->code, ": $url
";
25| } else {
26| print $res->code, ": $url ==> ", $res->status_line, "
";
27| if ($res->code == RC_METHOD_NOT_ALLOWED or
28| $res->code == RC_NOT_IMPLEMENTED) {
29| $req = new HTTP::Request('GET', $url);
30| $res = $ua->request($req);
31| redo RETRY;
32| }
33| }
34| }
35| }
36| }
37| }
38| if ($opt_s) {
39| local $/ = undef;
40| $p->parse();
41| } else {
42| my $fn = shift or die <<"USAGE";
43| Usage: $0 [-b base-ref] [-p http://proxy.com:port] [-t time-out-secs] [-s] file
44| USAGE
45| $p->parse_file($fn);
46| }
Baris 1 adalah path ke interpreter Perl yang dipanggil dengan opsi -w (enable warning). Baris 2 mematikan buffering pada STDOUT, dan baris 3 mengaktifkan pragma strict. Baris 4 sampai 7 memuat modul-modul yang diperlukan, yaitu Getopt::Std untuk mengolah opsi-opsi command-line, HTML::LinkExtor, LWP::UserAgent, dan HTTP::Status, yang mendefinisikan konstanta-konstanta kode status respons HTTP.
Baris 8 mendeklarasikan variabel-variabel global yang akan dipakai.
Baris 9 membaca opsi-opsi yang dilewatkan, dengan getopts() yang diekspor oleh Getopt::Std. Opsi yang diterima adalah -b, -p, -t, dan -s, di mana -s adalah flag boolean, yang artinya jika ia disebutkan di command-line, maka ia bernilai 1, jika tidak, maka bernilai 0. Nilai opsi-opsi yang dilewatkan ini berturut-turut akan disimpan pada variabel-variabel $opt_b, $opt_p, $opt_t, dan $opt_s.
Baris 10 sampai 12 berturut-turut membuat objek LWP::UserAgent, dan mengeset nilai timeout dan proxy yang akan dipakai, jika ada.
Baris 13 membuat objek HTML::LinkExtor, di mana argumen pertamanya adalah reference ke subrutin cb(). Subrutin ini akan digunakan oleh objek HTML::LinkExtor sebagai fungsi callback. Ketika objek ini mem-parse sebuah dokumen, maka setiap kali ia menjumpai hyperlink, fungsi callback ini akan dipanggil. Argumen kedua bagi konstruktor HTML::LinkExtor adalah URL basis yang digunakan untuk membuat URL absolut dari URL-URL relatif yang ditemukan nantinya.
Baris 14 sampai 37 adalah fungsi callback yang dimaksud. Argumen yang dilewatkan ke fungsi ini adalah nama tag HTML diikuti hash berisi pasangan atribut - URL. Sebagai contoh, jika dijumpai tag seperti ini:
maka pada baris 15 terjadi assignment:
$tag = 'img';
%links = ('src' => 'images/index.gif');
Pada baris 16, karena kita hanya tertarik dengan URL, maka kita hanya me-loop values %links menggunakan foreach dan menyimpannya dalam
$url.
Baris 17 memeriksa apakah sudah ada entri hash %all dengan key $url, jika belum, maka baris 18 dan 19 mengirim request HEAD ke $url, dan baris 20 menyimpan entri dengan key $url pada hash %all. Hash ini menjadi catatan URL-URL mana yang sudah diperiksa.
Blok 22 sampai 34 adalah blok RETRY. Baris 23 memeriksa apakah kode status dari respons yang diterima termasuk dalam rentang "sukses". Jika tidak, maka baris 26 mencetak $url, kode status berikut keterangannya, dan baris 27 memeriksa apakah respons tersebut disebabkan karena HEAD ditolak oleh server HTTP ataukah karena server HTTP yang dikontak tidak mengimplementasikan HEAD. Jika ya, maka baris 29
dan 30 mencoba mengirim request kembali ke URL yang sama, kali ini dengan metode GET. Baris 31 membawa kembali jalannya program ke awal blok RETRY.
Baris 38 memeriksa apakah opsi -s dilewatkan pada command-line, jika ya, maka baris 39 mengubah separator input line menjadi undef,
dan pada baris 40 objek HTML::LinkExtor yang diciptakan pada baris 13, yaitu $p, melakukan parsing pada hasil pembacaan STDIN.
Baris 42 mencoba mengambil nama file yang dilewatkan sebagai argumen command-line. Jika gagal, maka skrip exit sambil mencetak pesan singkat mengenai cara pemakaian skrip. Jika berhasil, maka $p melakukan parsing pada file tersebut.
Alternatif Implementasi
Anda mungkin mengamati bahwa pada Listing 1 di atas, request HTTP dilakukan satu persatu, yaitu setiap kali ditemukan hyperlink. Lalu boleh jadi kemudian Anda berpikir bagaimana jika dibuat beberapa koneksi TCP sekaligus, sehingga beberapa request HTTP dapat dikirim sekaligus dalam waktu yang kurang lebih bersamaan. Kedengarannya seperti ide yang bagus, tapi apakah untuk mengimplementasikan ini berarti kita perlu melakukan pemrograman socket secara langsung? Tidak juga, berkat modul LWP::Parallel::UserAgent yang dibuat oleh Marc Langheinrich [7].
Kita akan mencoba menulis ulang Listing 1, kali ini menggunakan LWP::Parallel::UserAgent. Kali ini kita tidak menggunakan callback pada HTML::LinkExtor, melainkan method links() untuk mengambil URL-URL yang berhasil diekstrak. Hasilnya seperti terlihat pada Listing 2.
Listing 2. par-cb - parallel check broken
1| #!/usr/bin/perl -w
2| $|++;
3| use strict;
4| package CheckBroken;
5| use LWP::Parallel::UserAgent qw(:CALLBACK);
6| use HTTP::Status;
7| use vars qw(@ISA);
8| @ISA = qw(LWP::Parallel::UserAgent);
9| sub on_failure {
10| my ($self, $request, $response, $entry) = @_;
11| print "Failed to connect to ",$request->url," ==> ",
12| $response->message,"
" if $response;
13| }
14| sub on_return {
15| my ($self, $request, $response, $entry) = @_;
16| if ($response) {
17| my $code = $response->code;
18| if ($code == RC_METHOD_NOT_ALLOWED or
19| $code == RC_NOT_IMPLEMENTED) {
20| print "Change method ($code)", ": ", $request->url, "
";
21| $self->register(HTTP::Request->new('GET', $request->url))
22| } elsif (
23| $code != RC_MOVED_PERMANENTLY and
24| $code != RC_FOUND) {
25| print $code, ": ", _get_origin_url($response), "
";
26| }
27| }
28| }
29| sub _get_origin_url {
30| my $res = shift;
31| for (my $r = $res; $r; $r = $r->previous) { $res = $r }
32| $res->request->url;
33| }
34| package main;
35| use Getopt::Std;
36| use HTML::LinkExtor;
37| use vars qw($opt_b $opt_p $opt_t $opt_m $opt_s);
38| getopts('b:p:t:m:s');
39| my $p = HTML::LinkExtor->new(undef, $opt_b);
40| if ($opt_s) {
41| local $/ = undef;
42| $p->parse();
43| } else {
44| my $fn = shift or die <<"USAGE";
45| Usage: $0 [options] file
46| Options:
47| -b Specify base href
48| -p Use a proxy server
49| -t Specify timeout, in seconds
50| -s Read input from STDIN
51| USAGE
52| $p->parse_file($fn);
53| }
54| my @urls = map { shift @$_; my %links = @$_; [values %links] } $p->links;
55| my $pua = new CheckBroken;
56| $pua->proxy('http', $opt_p) if $opt_p;
57| $pua->in_order(1);
58| $pua->duplicates(0);
59| $pua->max_hosts($opt_m) if $opt_m;
60| foreach my $url (@urls) {
61| map {
62| my $scheme = URI->new($_)->scheme;
63| $pua->register(HTTP::Request->new('HEAD', $_))
64| if $scheme eq 'http' or $scheme eq 'ftp'
65| } @$url;
66| }
67| $pua->wait($opt_t || 60);
Skrip ini terdiri dari dua bagian besar, yaitu package CheckBroken (baris 4 sampai 33), dan package main (baris 34 sampai 67). Titik entri program terdapat pada package main, sedangkan package CheckBroken adalah subclass atau kelas turunan dari LWP::Parallel::UserAgent, di mana kita meng-override method on_failure() (baris 9) dan on_return() (baris 14).
Agar package ini mewarisi method-method LWP::Parallel::UserAgent, maka pada baris 8, @ISA diisi dengan nama class ini.
Method on_failure() dipanggil jika koneksi ke server HTTP gagal dibuat, misalkan karena server down, atau terputusnya rute ke server tersebut. Baris 11 dan 12 mencetak URL berikut alasan kegagalan koneksi.
Method on_return() dipanggil setiap kali diterima respons dari request yang dikirim. Baris 17 mengambil kode status respons, dan baris 18 dan 19 memeriksa apakah kode status itu menunjukkan bahwa server HTTP yang dikontak itu tidak mengimplentasikan HEAD, atau menolak request HEAD. Jika ya, maka baris 21 me-register ulang request ke URL tersebut dengan method GET.
Baris 23 dan 24 memeriksa apakah kode status yang diterima bukan menunjukkan redirect, jika ya maka baris 25 mencetak kode status dan URL pada request awal yang didapat dari memanggil _get_origin_url().
Fungsi _get_origin_url() sendiri terletak pada baris 29 sampai 33. Fungsi ini diperlukan untuk mendapatkan URL awal dari sebuah request karena LWP::Parallel::UserAgent memiliki perilaku yang berbeda dengan LWP::UserAgent ketika menerima respons redirect. Sebagai contoh, misalkan kita memodifikasi baris 23 sampai 25, sehingga tidak ada pemeriksaan kode status dan langsung mencetak $request->url. Jika pada sebuah host, request ke /redirect-302 dikonfigurasi untuk di-redirect ke lokasi /hasil-redirect (yang adalah direktori fisik di
DocumentRoot), maka request ke URL tersebut menghasilkan:
302: http://postfix.cumi.org/redirect-302
301: http://postfix.cumi.org/hasil-redirect
200: http://postfix.cumi.org/hasil-redirect/
Sedangkan skrip cb yang menggunakan LWP::UserAgent akan menghasilkan:
200: http://postfix.cumi.org/redirect-302
Untuk mendapatkan hasil seperti ini, baris 31 menelusur balik ke respons yang paling awal, dan baris 32 mengembalikan URL request dari
respons yang paling awal tersebut.
Pada package main, kita melihat banyak kemiripan dengan skrip cb (listing 1), sehingga apa yang sama pada listing 1 tidak saya bahas lagi di sini.
Baris 39 membuat objek HTML::LinkExtor, yaitu $p, tanpa menggunakan callback (argumen pertama adalah undef).
Baris 54 mengambil hyperlink-hyperlink yang berhasil diekstrak, dan menyimpannya dalam @urls.
Baris 55 menciptakan objek CheckBroken, yaitu $pua, dan baris 56 mengeset proxy, jika digunakan proxy.
Baris 57 menyebabkan $pua mengirimkan request dalam urutan sama seperti urutan registrasi URL-URL.
Baris 58 menyebabkan $pua tidak mengirimkan request pada URL yang sama.
Baris 59 mengeset jumlah host maksimum yang dikontak secara paralel.
Baris 60 mengambil elemen @urls satu persatu dalam loop foreach, kemudian disimpan dalam $url. Karena $url adalah reference ke array, maka baris 61 me-loop @$url dengan map, dan baris 63 mendaftarkan request ke URL tersebut pada objek $pua, jika skema URL tersebut adalah http atau ftp (baris 64).
Baris 67 menunggu $pua yang sibuk bekerja :-)
Contoh Pemakaian
Memeriksa hyperlink pada bookmark Netscape Anda:
$ ./par-cb -m 10 ~/.netscape/bookmark.html
Memeriksa hyperlink pada sebuah dokumen online:
$ lwp-request http://postfix.cumi.org:1080 | ./cb -s -b http://postfix.cumi.org:1080
200: http://www.apache.org/httpd
200: http://postfix.cumi.org:1080/manual/index.html
200: http://postfix.cumi.org:1080/apache_pb.gif
$ lwp-request http://postfix.cumi.org:1080/manual | ./par-cb -s -b http://postfix.cumi.org:1080/manual/ -m 10
200: http://postfix.cumi.org:1080/manual/new_features_1_3.html
200: http://postfix.cumi.org:1080/manual/images/sub.gif
200: http://postfix.cumi.org:1080/manual/LICENSE
Anda dapat menyempurnakan skrip cb dan par-cb ini dengan mengimplementasikan pengecekan secara rekursif. Khusus untuk par-cb, dapat ditambahkan opsi untuk mengeset jumlah request maksimum pada satu host (method max_req() pada LWP::Parallel::UserAgent).
Alternatif Selain LWP
TMTOWTDI = There's More Than One Way To Do It. Ini salah satu "ajaran" yang hidup di komunitas Perl, dan ini tercermin juga dari redundancy modul-modul di CPAN.
Sebagai alternatif LWP, dapat digunakan HTTP::Lite [8] yang ukuran distribusinya jauh lebih kecil daripada LWP. Dokumentasi pada halaman manualnya menyebutkan bahwa modul ini dimaksudkan antara lain untuk dipakai pada aplikasi yang membutuhkan implementasi HTTP/1.1, namun ingin didistribusikan secara self-contained.
Ketika tulisan ini sedang dibuat, muncul modul baru lagi dengan fungsionalitas yang mirip, yaitu HTTP::GHTTP [9]. Juga berukuran kecil, namun bergantung pada library ghttp dari GNOME.
Referensi
- http://info.webcrawler.com/mak/projects/robots/threat-or-treat.html
- http://search.cpan.org/search?dist=Net-SMS
- http://www-4.ibm.com/software/developer/library/xml-messaging/?dwzone=xml
- http://www.ietf.org/rfc/rfc2616.txt?number=2616
- http://www.oreilly.com/openbook/webclient/
- http://search.cpan.org/search?author=GAAS
- http://search.cpan.org/search?author=MARCLANG
- http://search.cpan.org/search?dist=HTTP-Lite
- http://search.cpan.org/search?dist=HTTP-GHTTP
|
|
|