Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tek İş Parçacıklı Bir Web Sunucusu Geliştirmek

İşe çalışan, tek iş parçacıklı bir web sunucusuyla başlayacağız. Kod yazmadan önce, web sunucularında karşımıza çıkan iki temel protokole çok kısa bakalım: Aktarım Denetim Protokolü (Transmission Control Protocol, TCP) ve Hiper Metin Aktarım Protokolü (Hypertext Transfer Protocol, HTTP). Bu protokollerin bütün ayrıntıları bu kitabın kapsamı dışında; ama ihtiyacımız olan kadarını bilirsek devam etmek kolay olur.

TCP, verinin bir makineden diğerine nasıl taşındığını anlatan daha düşük seviyeli protokoldür. HTTP ise TCP’nin üstünde çalışır ve istekle yanıtın içeriğini tanımlar. Teknik olarak HTTP başka protokollerle de kullanılabilir; fakat pratikte büyük çoğunlukla TCP üzerinden taşınır. Bu bölümde TCP ve HTTP istek/yanıtlarının ham baytlarıyla doğrudan çalışacağız.

TCP Bağlantısını Dinlemek

Web sunucumuz önce bir TCP bağlantısını dinlemeli. Bunun için standart kütüphanedeki std::net modülünü kullanabiliriz. Önce her zamanki gibi yeni bir proje oluşturalım:

$ cargo new merhaba
     Created binary (application) `merhaba` project
$ cd merhaba

Şimdi Liste 21-1’deki kodu src/main.rs içine yazın. Bu kod, yerel 127.0.0.1:7878 adresinde gelen TCP akışlarını dinler. Yeni bir akış geldiğinde de Bağlantı kuruldu! yazar.

Filename: src/main.rs
use std::net::TcpListener;

fn main() {
    let dinleyici = TcpListener::bind("127.0.0.1:7878").unwrap();

    for akis in dinleyici.incoming() {
        let akis = akis.unwrap();

        println!("Bağlantı kuruldu!");
    }
}
Listing 21-1: Gelen akışları dinlemek ve bir akış aldığımızda ileti yazdırmak

TcpListener ile 127.0.0.1:7878 adresindeki TCP bağlantılarını dinliyoruz. İki nokta üst üste işaretinden önceki bölüm bilgisayarınızın IP adresidir; 7878 ise port numarasıdır. Bu portu iki nedenle seçtik: genelde HTTP trafiği bu portta çalışmaz, dolayısıyla başka bir sunucuyla çakışma ihtimali düşüktür; ayrıca 7878, telefon tuş takımında rust kelimesine karşılık gelir.

Buradaki bind fonksiyonu, anlam olarak new gibidir; yeni bir TcpListener döndürür. Ağ programlamasında bir portu dinlemeye başlamak “bir porta bağlanmak” olarak adlandırıldığı için fonksiyon adı bind’dır.

bind, Result<T, E> döndürür; çünkü bağlanma işlemi başarısız olabilir. Örneğin aynı programdan iki tane açıp aynı portu dinlemeye kalkarsanız biri başarısız olur. Biz burada eğitim amaçlı basit bir sunucu yazdığımız için ayrıntılı hata yönetimi yapmayacağız; hata olursa program dursun diye unwrap kullanacağız.

TcpListener üzerindeki incoming metodu, bize bir akışlar dizisi veren yineleyici döndürür. Daha doğrusu bunlar TcpStream türündeki akışlardır. Tek bir akış, istemciyle sunucu arasındaki açık bağlantıyı temsil eder. İstemcinin bağlanması, sunucunun yanıt üretmesi ve bağlantının kapanması sürecinin tamamına bağlantı deriz. Dolayısıyla istemcinin ne gönderdiğini görmek için TcpStream üzerinden okuyacak, yanıtı geri göndermek için de aynı akışa yazacağız.

Şimdilik yaptığımız tek şey, akışta hata varsa unwrap ile programı durdurmak; hata yoksa ileti yazmak. Bir sonraki listede başarılı durumda daha fazlasını yapacağız.

Bu kodu cargo run ile çalıştırıp tarayıcıda 127.0.0.1:7878 adresini açın. Tarayıcı hata gösterecektir; çünkü sunucu henüz veri döndürmüyor. Ama terminalde birkaç kez Bağlantı kuruldu! yazdığını görmelisiniz.

Bir tarayıcı isteği için birden fazla ileti görmeniz normaldir. Tarayıcı bazen sayfanın kendisine ek olarak sekmedeki küçük simge gibi başka kaynakları da istemeye çalışır. Ayrıca, sunucu henüz geçerli veri dönmediği için bazı tarayıcılar bağlantıyı yeniden denemeye çalışır.

Önemli nokta şu: artık gerçekten bir TCP bağlantısını ele alabiliyoruz.

İsteği Okumak

Sırada tarayıcının gönderdiği isteği okumak var. Bağlantıyı almak ile bağlantı üzerinde iş yapmak sorumluluklarını ayırmak için, bağlantıları işleyecek ayrı bir fonksiyon yazacağız. baglantiyi_isle adlı bu yeni fonksiyonda TCP akışından gelen veriyi okuyup ekrana basacağız. Böylece tarayıcının neler yolladığını görebileceğiz. Kodunuzu Liste 21-2’deki gibi değiştirin.

Filename: src/main.rs
use std::{
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
};

fn main() {
    let dinleyici = TcpListener::bind("127.0.0.1:7878").unwrap();

    for akis in dinleyici.incoming() {
        let akis = akis.unwrap();

        baglantiyi_isle(akis);
    }
}

fn baglantiyi_isle(mut akis: TcpStream) {
    let tamponlu_okuyucu = BufReader::new(&akis);
    let http_istegi: Vec<_> = tamponlu_okuyucu
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    println!("İstek: {http_istegi:#?}");
}
Listing 21-2: TcpStream içinden okumak ve gelen veriyi yazdırmak

std::io::BufReader ile std::io::prelude::* öğelerini kapsam içine alıyoruz; çünkü akıştan okuma ve yazma için bunlara ihtiyacımız var. main içindeki for döngüsünde artık ileti yazdırmak yerine yeni baglantiyi_isle fonksiyonunu çağırıp akis değerini ona veriyoruz.

baglantiyi_isle içinde, akis referansını saran bir BufReader oluşturuyoruz. BufReader, std::io::Read trait’i çağrılarını tamponlayarak bize daha rahat bir okuma arayüzü sunar.

Tarayıcının gönderdiği istek satırlarını toplamak için http_istegi adlı bir değişken oluşturuyoruz. Satırları vektörde toplayacağımızı belirtmek için Vec<_> tür açıklamasını ekliyoruz.

BufReader, std::io::BufRead trait’ini uygular; bu trait de lines metodunu verir. lines, her satır sonu görüldüğünde akışı bölen ve Result<String, std::io::Error> döndüren bir yineleyicidir. Her String değerini alabilmek için map içinde unwrap kullanıyoruz. Gerçek bir uygulamada bu hataları daha nazik biçimde ele almak gerekirdi; ama burada örneği sade tutmak istiyoruz.

Tarayıcı, arka arkaya iki yeni satır göndererek HTTP isteğinin bittiğini belirtir. Bu yüzden boş satır gelene kadar satırları alıyoruz. Sonra bunları vektörde toplayıp biçimli hata ayıklama çıktısıyla ekrana yazdırıyoruz.

Programı yeniden çalıştırıp tarayıcıdan bir istek gönderin. Tarayıcı yine hata sayfası gösterecek; ama terminalde buna benzer bir çıktı göreceksiniz:

$ cargo run
   Compiling merhaba v0.1.0 (file:///projects/merhaba)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/merhaba`
İstek: [
    "GET / HTTP/1.1",
    "Host: 127.0.0.1:7878",
    ...
]

Tarayıcınıza göre satırlar biraz farklı olabilir. Ama artık sunucuya hangi HTTP isteğinin geldiğini açıkça görebiliyoruz.

HTTP İsteğine Daha Yakından Bakmak

HTTP metin tabanlı bir protokoldür ve istek genel olarak şu biçimdedir:

Method Request-URI HTTP-Version CRLF
headers CRLF
message-body

İlk satır istek satırı dır. İstemcinin ne istediğine dair temel bilgiyi taşır. İlk bölüm kullanılan metottur; örneğin GET ya da POST. Tarayıcımız burada GET kullanıyor; yani bilgi istiyor.

Sonraki bölüm istenen URI değeridir. Burada / geliyor; yani kök yol isteniyor. Son bölüm HTTP sürümüdür. Satır da CRLF ile biter. CRLF, daktilo günlerinden gelen carriage return ve line feed ifadelerinin kısaltmasıdır; Rust içinde bunu çoğu zaman \r\n olarak görürüz.

İstek satırından sonra Host: ile başlayan kısım başlıklar bölümüdür. GET isteğinde çoğu zaman gövde bulunmaz.

Şimdi farklı bir adres, örneğin 127.0.0.1:7878/test, isteyip gelen verinin nasıl değiştiğine bakabilirsiniz.

Yanıt Yazmak

Artık tarayıcı isteğini okuyabildiğimize göre istemciye veri de geri gönderebiliriz. HTTP yanıtları kabaca şu biçimdedir:

HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body

İlk satır durum satırı dır. Burada HTTP sürümü, sayısal durum kodu ve metinsel açıklama yer alır.

Örnek olarak, HTTP 1.1 kullanan, durum kodu 200, açıklaması OK olan ve gövdesi bulunmayan minimal bir başarılı yanıt şöyledir:

HTTP/1.1 200 OK\r\n\r\n

Şimdi bunu akışa yazarak istemciye ilk yanıtımızı gönderelim. Liste 21-3’te, baglantiyi_isle fonksiyonu bunu yapıyor.

Filename: src/main.rs
use std::{
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let dinleyici = TcpListener::bind("127.0.0.1:7878").unwrap();

    for akis in dinleyici.incoming() {
        let akis = akis.unwrap();

        baglantiyi_isle(akis);
    }
}

fn baglantiyi_isle(mut akis: TcpStream) {
    let tamponlu_okuyucu = BufReader::new(&akis);
    let http_istegi: Vec<_> = tamponlu_okuyucu
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    let yanit = "HTTP/1.1 200 OK\r\n\r\n";

    akis.write_all(yanit.as_bytes()).unwrap();
}
Listing 21-3: Akışa küçük ama geçerli bir HTTP yanıtı yazmak

yanit adlı string’i as_bytes ile baytlara çevirip write_all ile akışa yazıyoruz. Hata durumunda yine unwrap ile ilerliyoruz.

Bu kodla tarayıcı artık hata yerine boş bir sayfa gösterecektir. Yani HTTP isteği alıp geçerli HTTP yanıtı dönmeyi elle başarmış olduk.

Gerçek HTML Döndürmek

Boş sayfa yerine gerçek içerik gösterelim. Projenin kök dizininde, src içinde değil, merhaba.html adlı yeni dosya oluşturun. Liste 21-4 örnek bir içerik gösteriyor.

Filename: merhaba.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Merhaba!</title>
  </head>
  <body>
    <h1>Merhaba!</h1>
    <p>Rust'tan selam</p>
  </body>
</html>
Listing 21-4: Yanıtta döndürülecek örnek HTML dosyası

Sunucu bir istek aldığında bu dosyanın içeriğini okuyup yanıt gövdesi olarak ekleyeceğiz. Liste 21-5 bunu gösteriyor.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};
// --snip--

fn main() {
    let dinleyici = TcpListener::bind("127.0.0.1:7878").unwrap();

    for akis in dinleyici.incoming() {
        let akis = akis.unwrap();

        baglantiyi_isle(akis);
    }
}

fn baglantiyi_isle(mut akis: TcpStream) {
    let tamponlu_okuyucu = BufReader::new(&akis);
    let http_istegi: Vec<_> = tamponlu_okuyucu
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    let durum_satiri = "HTTP/1.1 200 OK";
    let icerik = fs::read_to_string("merhaba.html").unwrap();
    let uzunluk = icerik.len();

    let yanit =
        format!("{durum_satiri}\r\nContent-Length: {uzunluk}\r\n\r\n{icerik}");

    akis.write_all(yanit.as_bytes()).unwrap();
}
Listing 21-5: Yanıt gövdesi olarak merhaba.html içeriğini göndermek

Burada fs modülünü de kullanıma ekledik. Dosyayı string olarak okuyup format! ile HTTP yanıtının gövdesine ekliyoruz. Ayrıca geçerli bir HTTP yanıtı olması için Content-Length başlığını da ekliyoruz.

Kodu çalıştırıp tarayıcıyı yenilerseniz artık HTML içeriğinin çizildiğini görmelisiniz.

İsteği Doğrulamak ve Seçici Yanıt Vermek

Şu anda istemci ne isterse istesin, sunucu hep aynı HTML dosyasını döndürüyor. Biraz daha gerçekçi davranıp yalnızca / yoluna doğru biçimde gelen isteğe HTML dönelim; diğer tüm isteklerde hata sayfası gösterelim. Bunun için baglantiyi_isle fonksiyonunu Liste 21-6’daki gibi değiştiriyoruz.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let dinleyici = TcpListener::bind("127.0.0.1:7878").unwrap();

    for akis in dinleyici.incoming() {
        let akis = akis.unwrap();

        baglantiyi_isle(akis);
    }
}
// --snip--

fn baglantiyi_isle(mut akis: TcpStream) {
    let tamponlu_okuyucu = BufReader::new(&akis);
    let istek_satiri = tamponlu_okuyucu.lines().next().unwrap().unwrap();

    if istek_satiri == "GET / HTTP/1.1" {
        let durum_satiri = "HTTP/1.1 200 OK";
        let icerik = fs::read_to_string("merhaba.html").unwrap();
        let uzunluk = icerik.len();

        let yanit = format!(
            "{durum_satiri}\r\nContent-Length: {uzunluk}\r\n\r\n{icerik}"
        );

        akis.write_all(yanit.as_bytes()).unwrap();
    } else {
        // some other request
    }
}
Listing 21-6: / yoluna gelen istekleri diğerlerinden farklı ele almak

Artık isteğin yalnızca ilk satırını okuyoruz. Tamamını vektöre toplamak yerine next ile ilk öğeyi alıyoruz. istek_satiri, / için gelen GET isteğiyle eşleşiyorsa HTML dosyasını döndürüyoruz; aksi durumda başka bir şey yapacağız.

Şimdi else bloğunu da dolduralım. Liste 21-7, başka her istek için 404 NOT FOUND durum kodu ve hata sayfası döndürüyor.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let dinleyici = TcpListener::bind("127.0.0.1:7878").unwrap();

    for akis in dinleyici.incoming() {
        let akis = akis.unwrap();

        baglantiyi_isle(akis);
    }
}

fn baglantiyi_isle(mut akis: TcpStream) {
    let tamponlu_okuyucu = BufReader::new(&akis);
    let istek_satiri = tamponlu_okuyucu.lines().next().unwrap().unwrap();

    if istek_satiri == "GET / HTTP/1.1" {
        let durum_satiri = "HTTP/1.1 200 OK";
        let icerik = fs::read_to_string("merhaba.html").unwrap();
        let uzunluk = icerik.len();

        let yanit = format!(
            "{durum_satiri}\r\nContent-Length: {uzunluk}\r\n\r\n{icerik}"
        );

        akis.write_all(yanit.as_bytes()).unwrap();
    // --snip--
    } else {
        let durum_satiri = "HTTP/1.1 404 NOT FOUND";
        let icerik = fs::read_to_string("404.html").unwrap();
        let uzunluk = icerik.len();

        let yanit = format!(
            "{durum_satiri}\r\nContent-Length: {uzunluk}\r\n\r\n{icerik}"
        );

        akis.write_all(yanit.as_bytes()).unwrap();
    }
}
Listing 21-7: / dışında bir şey istenirse 404 ve hata sayfası döndürmek

Bu durumda yanıtın gövdesi 404.html dosyasından geliyor. Önce bu dosyayı oluşturmanız gerekir. Liste 21-8 örnek içerik gösteriyor.

Filename: 404.html
<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>Merhaba!</title>
    </head>
    <body>
        <h1>Hay aksi!</h1>
        <p>Üzgünüm, ne istediğinizi anlayamadım.</p>
    </body>
</html>
Listing 21-8: 404 yanıtı için döndürülebilecek örnek HTML içerik

Bu değişikliklerden sonra 127.0.0.1:7878 adresi merhaba.html içeriğini, örneğin 127.0.0.1:7878/foo gibi diğer adresler ise 404.html içeriğini göstermelidir.

Yeniden Düzenleme

Şu an if ile else bloklarında ciddi tekrar var: iki durumda da dosya okuyor ve yanıt yazıyoruz; yalnızca durum satırı ile dosya adı değişiyor. Bunu sadeleştirmek için, farklı olan kısmı bir demet içinde iki değere ayırıp geri kalan ortak kodu tek noktaya toplayabiliriz. Liste 21-9 sonuçta ortaya çıkan sürümü gösteriyor.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let dinleyici = TcpListener::bind("127.0.0.1:7878").unwrap();

    for akis in dinleyici.incoming() {
        let akis = akis.unwrap();

        baglantiyi_isle(akis);
    }
}
// --snip--

fn baglantiyi_isle(mut akis: TcpStream) {
    // --snip--
    let tamponlu_okuyucu = BufReader::new(&akis);
    let istek_satiri = tamponlu_okuyucu.lines().next().unwrap().unwrap();

    let (durum_satiri, dosya_adi) = if istek_satiri == "GET / HTTP/1.1" {
        ("HTTP/1.1 200 OK", "merhaba.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND", "404.html")
    };

    let icerik = fs::read_to_string(dosya_adi).unwrap();
    let uzunluk = icerik.len();

    let yanit =
        format!("{durum_satiri}\r\nContent-Length: {uzunluk}\r\n\r\n{icerik}");

    akis.write_all(yanit.as_bytes()).unwrap();
}
Listing 21-9: if ve else bloklarını yalnızca farklı kodu içerecek şekilde yeniden düzenlemek

Artık if ve else, yalnızca durum_satiri ile dosya_adi değerlerini seçiyor. Dosyayı okuma ve yanıt yazma kodu ortak olduğu için dışarı alındı. Böylece iki durum arasındaki fark daha görünür, ortak davranış daha kolay değiştirilebilir hâle geliyor.

Yaklaşık 40 satırlık Rust koduyla, bir isteğe içerik döndüren ve diğer bütün isteklerde 404 veren basit bir web sunucumuz oldu.

Ancak sunucu hâlâ tek iş parçacıklı çalışıyor; yani aynı anda yalnızca tek isteği işleyebiliyor. Şimdi bunun neden sorun olabileceğine bakalım, sonra da iş parçacığı havuzu ile sunucuyu çok iş parçacıklı hâle getirelim.