Bir Web Sunucusu Kodlama Denemesi

Her şey Altan Tanrıverdi‘nin Ubuntu üzerine nginx kurulumu yazısını görmemle başladı. Hayır, Nginx’i daha önce görmemiş değildim, ancak Tanrıverdi’yi Apache’den başka bir web sunucusu kurmaya yönelten şey Apache’nin sistem kaynakları açısından “pahalı” sayılabilecek bir sunucu olmasıydı.

Apache’de bir web sunucusundan beklediğimiz her şey ve hatta fazlası var. Apache modül listesine (http://httpd.apache.org/modules/ ve http://modules.apache.org/) daha önce göz atmadıysanız bir bakın; belki de adını bile duymadığınız, hiç işiniz düşmemiş olduğu için kullanmadığınız bir çok modül olduğunu göreceksiniz. Oysa çoğu LAMP/LAPP programcısı, günlük işlerinde php ve rewrite modulleri dışındakileri kullanmaya pek ihtiyaç duymaz.

Elbette asla yılların eskitemediği dost Apache’ye kafa tutmak için değildi, ancak kafama şu soruyu takmadan edemedim:

“Acaba yalnız PHP yorumlayan ve başka hiç bir iş yapmayan bir web sunucusunu yine PHP ile kodlasak nasıl olurdu?”

Tabii bu soru büyük olasılıkla benden başka bir sürü kişinin de aklından geçmişti veAmerika’yı yeniden keşfetmemek için önce Hz. Google’a sordum.

Google bana “Nano Web Server var.” dedi. Bir de baktım ki, gerçekten bu arkadaş Apache’yi neredeyse bire-bir PHP’ye port etmiş. Modüllerine kadar yazmış. Ubuntu için deb paketi bile var ve daemon olarak kuruluyor. Öte taraftan bu çalışma hem benim ölçeğimi aşıyor hem de başlangıçtaki mantığımla ters düşüyor: PHP’den başka şey çalıştırmak istesem zaten “runtime’da yorumlanan” değil “derlenmiş” kod kullanmayı tercih ederim. Benim fikrimin özü, zaten native olarak PHP yorumlayabilen bir kod yapısı kullanarak bir web sunucusu yapmak.

Bunun dışında bir de phpStack‘i buldum. phpStack benim amaçladığım şeye oldukça yakındı ancak bana göre işleri olması gerekenden daha karmaşık hale getiriyordu.

Böylece kendi denememi yapmaya karar verdim. Önce php5-cgi paketini kurdum. PHP Manual’dan jenerik bir soket kodu aldım. Kodu düzenledim, mime türlerine göre gönderilecek header’ları ayarladım, PHP yorumlayıcısı desteği verdim ve sonuç:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
<?php

interface server
{
    public function setWebRoot($path);
    public function setPort($port);
    public function serve();
}

final class webserver implements server
{
    private $_port , $_webroot;
    public function __construct()
    {

       //belki ileride lazım olur. şimdilik dummy
    }
   
    public function setPort($port)
    {
        $this->_port = $port;
    }
   
    public function setWebRoot($path)
    {
        $this->_webroot = $path;
    }
   
    private function _mimeHandler($input)
    {
            $fileinfo = pathinfo($input);
            switch ($fileinfo['extension'])
            {
                  case 'png':
                      $mime = "text/png";  
                      break;
                 
                  case 'jpg':
                  case 'jpeg':
                      $mime = "text/jpeg";  
                      break;
                 
                  case 'gif':
                      $mime = "text/gif";  
                      break;
                 
                  case 'css':
                      $mime = "text/css";  
                      break;
             
                  default:
                      $mime = "text/html; charset=UTF-8";
            }
        return $mime;
    }
   
    private function _handleCode($contents)
    {
         //eval() edilecek kodun başında <?php ya da <? varsa çakar. halledelim.
        $contents = trim($contents);
        if(substr($contents , 0 , 5) == '<?php')
        {
            $contents = substr($contents , 5);
        }
        elseif(substr($contents , 0 , 2) == '<?')
        {
            $contents = substr($contents , 2);
        }
       
        return $contents;
    }
   
    public function serve()
    {
        $socket = @socket_create_listen($this->_port);
        if (!$socket)
        {
          echo "Soket olamiyor!\n";
          exit;
        }
        while (1) //daire-i ilanihaye!
        {
          $client = socket_accept($socket);
          $input = trim(socket_read ($client, 4096));
          echo $input; //tarayıcı ne diyor? bu veriyi ileride $_SERVER array'ini doldurmak için kullanmak lazım.
          $input = explode(" " , $input);
          $input = $input[1];

          $mime = $this->_mimeHandler($input);
         
          if ($input == "" || $input == '/')
          {
            $input = "/index.html";
          }
          $input = ".$input";
          $inputArray = explode('?' , $input);
          $input = $this->_webroot . $inputArray[0];

          if (file_exists($input) && is_readable($input))
          {
            echo "Dosya: $input\n";
           
            $contents = file_get_contents($input);
            if(strstr($input , '.php'))
            {
             
                $contents = $this->_handleCode($contents);
               
                //PHP çalıştıktan sonra sonucu echo etmesini değil, $contents stringine atmasını istiyoruz.
                ob_start();
                eval($contents);
                //gidip $contents'in üstüne yazmak doğru alışkanlık değil ama yeni değişken register etmek istemiyorum
                $contents = ob_get_contents();
                ob_end_clean();
            }
            $output = "HTTP/1.0 200 OK\r\nServer: HyperTextServer\r\nConnection: close\r\nContent-Type: $mime\r\n\r\n$contents";
          }
          else
          {
            $contents = "<title>UHU 404</title><h2>UHU 404</h2>Iın-ıın-ıın-ıın-cav-cav-cav-cav-oooooo - oooooo - zört - zört - zört -zört (araba alarmı efekti) Çok afedersin müdürüm ama yok öyle bir dosya.";
            $output = "HTTP/1.0 404 OBJECT NOT FOUND\r\nServer: HyperTextServer\r\nConnection: close\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n$contents";
          }
          socket_write($client, $output);
          socket_close ($client);
        }
        socket_close ($socket);
    }
}
$server = new webserver();
$server->setWebRoot('/var/www/');
$server->setPort(8008);
$server->serve();

Bu kodu (örneğin /var/www altına) hyperTextServer.php adıyla kaydediyoruz.
Daha sonra varsayılan olarak görüntülenmesi için örnek bir /var/www/index.html dosyası oluşturuyoruz.
Son olarak

1
php-cgi /var/www/hyperTextServer.php

ile sunucuyu çalıştırdıktan sonra tarayıcıdan

http://localhost:8008

adresini çağırdığımızda oluşturduğumuz index.html dosyasının servis edildiğini görüyoruz.
Artık bir adım daha ileri giderek bir PHP dosyası çağırmayı deneyebiliriz:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$start = microtime(1);

for($n = 0; $n <= 10000; $n++)
{
    echo $n . ' ';
}
$end = microtime(1);
$elapsed = $end - $start;
echo "<br/>vakit(ms): $elapsed";
$fileHandle = fopen('serverBenchmark.txt' , 'a');
fputs($fileHandle , $elapsed . "\n");
fclose($fileHandle);

Bu kodu da /var/www/test.php adıyla kaydedip tarayıcıdan
http://localhost:8008/test.php adresini çağırdığımızda PHP kodumuzun da gayet başarılı şekilde yorumlandığını görüyoruz.
Gördüğünüz gibi test.php dosyası, serverBencmark.txt dosyasına, çalışmasının ne kadar zaman aldığını yeni bir satır olarak yazıyor. Böylece sevdiğimiz “benchmarkçılık” oyununu oynayabileceğiz.
test.php kodunu arka arkaya yüz kere önce kendi yazdığımız sunucu ile 8008′den, daha sonra da yine yüz kere apache üstünden (default 80.port) çalıştırıp basit bir aritmetik ortalama alacağız.
Benim lokal sistemimdeki sonuçlar söyle:
Apache ortalama: 0,0174917006493 ms
Bizim sunucu ortalama: 0,0098775601387 ms

Görüyoruz ki, beklediğimizin aksine PHP üstünde çalışan bizim sunucu, derlenmiş kod olarak çalışan Apache’den iki kat kadar hızlı çalışıyor. Bu durumun açıklamasının aslında çok da zor olmadığını düşünüyorum: Apache bir sürü kontrol yapıyor, aynı anda birden fazla bağlantı kabul edebiliyor, bir sürü port dinliyor, vs. Öte taraftan bizim sunucumuz yalnız PHP kodunu çalıştırmaya odaklanmış. $_SERVER’a, $_REQUEST’e bir değer atamak için tarayıcı verisini işlemiyor ve tek, basit bir süreç olarak çalışıyor.
Aynı işleri bizim sunucumuza yaptırdığımızda, büyük olasılıkla aradaki hız farkı kapanacak. Yine de şu andaki duruma bakarak iki kat yavaşlayacağını öngörmüyorum sonuç olarak başlangıçta umduğumun çok daha fazlasını alabildiğimi gözlemliyorum.

Bundan sonra yapılacaklar:
1- $_SERVER, $_REQUEST, $_POST, $_GET, $_FILES ve $_COOKIE arraylerinin doldurulması.
2- pcntl eklentisi ile süreçlerin “çatallanması”

Yukarıdaki iki maddelik yapılacaklar listesi de temizlendiğinde elimizde basbayağı düzgün çalışan bir web sunucumuz olacak.

Paylaş:
  • Print
  • FriendFeed
  • Twitter
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google Bookmarks
  • Blogplay
  • DZone
  • LinkedIn
  • MySpace
  • Ping.fm
  • Reddit
  • StumbleUpon
  • Technorati

Etiketler: , , , ,

  1. halid’s avatar

    kendnze hazirladgnz sunucu yazilimi ile apache yi request – response oranindaki hizi hesaplarsak bence php engine degerleri yükseltecektir. Tcp istekleri arttkca apache arayi kapatacaktir.

  2. Can’s avatar

    @halid: Kodun şu andaki haline ancak “proof of concept” diyebiliriz. Öte taraftan processleri düzgün biçimde halledebilmek açısından pcntl eklentisine bir bakın. Opcode’a çevrilmiş ve pcntl ile desteklenen bir PHP kodu ile hala her türlü benchmarkta yukarıdaki mantığın, Apache’den daha iyi sonuçlar vereceğini öngörüyorum. Tabii şimdilik bu yalnız bir tahmin. Yapınca göreceğiz.

  3. ediz’s avatar

    yaz geldi yaz,

    litespeed veya ngix kurup hayatı yaşamak varken bu icadlar ne diye

  4. Onur YALAZI’s avatar

    Çok içtenlikle söyleyeyim, naif bir düşünce tarzı. Ama sonuçlarının başarısız olacağı ne yazık ki garanti.

    Öncelikle, bir soket uygulaması için internal olarak “threading” ya da “forking” desteklemeyen bir yorumlayıcı kullanmak sonucun hüsran olmasını garanti ediyor.

    Bundan başka, php’nin eval ile ve yine naif bir düşünce ile sadece başında ve sonunda başlangıç – bitiş etiketi olacağını varsayarak çalıştırmak, işletmek çok da akıllıca bir hareket değil. PHP’nin kendi içindeki “form”suzluğunun, bu naif düşüncenin ilk formdan bağımsız kod parçasında çatlamasına neden olacağı ortada.

    Bunlardan başka, uygulamanın, bir güvenlik modeli, bir loglama modeli, bir eklenti modeli vb. sunmak gibi dertlerinin olmaması, gerçek bir sunucu olmasının önündeki bir set. Bu haliyle, sadece deneysel bir çalışma.

    Aynı fikrin,

    – perl, ruby, c ya da java gibi bir wrapper sayesinde forking ve/veya threading desteği edinmesi
    – php işletiminin fast-cgi yolu ile sağlanması
    – temel bir erişim/hata loglama mekanizması eklenmesi

    ile uygulanması, programın çok daha başarılı ve gelecek vaadeden bir hale gelmesini sağlardı.

    Yine de eline sağlık :)

  5. Can’s avatar

    @onur: fikrin tamamının görülmesi önemli: Mutlaka multi-thread/fork desteği şart ve bunu pcntl eklentisiyle sağlayabiliyoruz. Şu anda tam olarak buna çalışıyorum.
    - eval() yaklaşımı konusunda ısrarcıyım: Buradaki temel mantık client->server->interpreter->server->client çevrimini client->server->client’a çevirmek.
    - Başlangıç ve bitiş etiketleri konusunda sorunumuz yok. Eval()’deki tek dert başlangıçtaki php etiketi. Daha sonrasını da deneyeceğiz, en olmadık haliyle escape edeceğiz.
    Naif (“önyargısız” diye okuyalım) fikirler olmasa, kimse “boşa hayal” diye konuşuyor olamazdı, değil mi? :-;

  6. Can’s avatar

    @ediz: daha iyisi olsun diye:)

  7. Can’s avatar

    ara parantezi: ubuntu üstünde php-cgi paketi zaten fastcgi ile çalışır.

  8. Onur YALAZI’s avatar

    Bu şartlarda, eval yerine “include” kullanılabilir. Ancak eval ve include ikisinde de dert, çalıştırılan kodların, sunucunun global isim uzayına erişimi olması.

    php-cgi komut satırı uygulaması evet fast-cgi desteği ile geliyor. Ancak fast-cgi çalışabilmesi için, bu yorumlayıcı ile fastcgi protokolü ile iletişime girilmesi gerekiyor.

    Naif derken, boşa hayal demek gibi bir niyetim hiç olmadı.

  9. Omer’s avatar

    Bu yorumu yazan arkadaş (@ediz) acaba ne cevap verildi diye gelip bir kez bakmaz bile bu sayfaya öyle boş boş konuşurlar.