C/C++ Soket Programlama

İşlemler ve bilgisayarlar arasında iletişim için kullanılan soket programlama nedir, nasıl oluşturulur, nerelerde kullanılır ve neler yapılabilir gibi sorularla ilgili cevaplar yer alıyor.

Soket nedir?

Soket veya Socket bilgisayar işlemleri veya bilgisayarlar arası işlemler için kullanılan haberleşme dosyalarıdır.

Soket programlama nedir?

Soket programlama temel olarak alıcı ve gönderici arasındaki iletişim yönetmek için kullanılan programlama tekniğine denir.

Soket programlama alt seviye dillerde sistem çağrıları ile yapılırken C#, Java, Node.js, Python gibi dillerde yine sistem çağrılarını kullanarak oluşturan arayüzlerle yapılır.

Soket programlama nerelerde kullanılır?

Soket programlama bilgisayarlar arası veri haberleşmesinde kullanılır.

HTTP, FTP, SMTP gibi iletişim protokolleri soket yapısı üzerine inşa edilmiştir.

Soket programlama ile neler yapılabilir?

Soket yapısı kullanılarak bilgisayarlar arası erişim sağlanarak veri haberleşmesi yapılır.

Ayrıca kendi kurallarımızı belirleyerek chat-sohbet, dosya transferi gibi uygulamalar yapabiliriz.

Soket programlama birçok modern programlama dili ile yapılmaktadır. Ancak bu diller temel olarak işletim sistemi seviyesinde çalışan çeşitli sistem çağrılarının arayüzlerini kullanır. Yani bu dillerde kullanılan soket işlemleri de sistem çağrıları ile yapılır.

Soket programlama sistem çağrıları seviyesinde oldukça karmaşık olabilir. İletişim ortamının oluşturulması, uygun veri taşıma formatını belirlenmesi bunlardan bir kaçıdır.

Soket nasıl çalışır?

Soket yapısının nasıl çalıştığına bakmadan önce C/C++ ve işletim sistemi bilgimizi gözden geçirelim.

İşletim sistemleri bir program çalıştırırken veri girişi, veri yazdırma ve hata işleme için 3 adet (stdin, stdout, stderr) sabit tanımlar.

Bunlar sayesinde yazdırma veya okuma işlemini yaparız.

#include <stdio.h>

int main(){

    fprintf(stdout, "Merhaba Dünya");

    return 0;
}

Benzer şekilde bir dosya açıldığında biz dosya işlemleri için bir yol açmış oluruz.

#include <stdio.h>

int main(){

    FILE *dosya = fopen("metin.txt", "w");

    fprintf(dosya, "Merhaba ben C ile yazılıyorum.");

    fclose(dosya);

    return 0;
}

Örnekte fopen fonksiyonu dosya işlemleri için bize bir yol/adres sunar.

Biz bu yol/adresi fprintf, fwrite gibi dosya işlemleri ile okuma, yazdırma işlemleri yapar ve fclose ile bu yolu/adresi kapatırız.

Temel olarak socket işlemleri de örnekteki gibi gerçekleşir.

C/C++ soket programlama

C ve C++ gibi makine diline yakın diller sistem çağrılarına imkan verdiği ve birçok işletim sisteminin temelini oluşturduğu için bu diller kullanılacaktır.

Soket programlama kavramının temelini Berkeley sockets tarafından belirlenen Socket API oluşturur.

Berkeley sockets tarafından oluşturulan bu kurallar Unix ve Linux türevi işletim sistemlerinde POSIX sockets iken Windows tabanlı işletim sistemlerinde Winsock arayüzü kullanılır.

Kullanılan API arayüzündeki fonksiyonlar aynıdır. Sadece Winsock arayüzünde çeşitli ayarların yapılması gerekir.

Soket oluşturma

Socket işlemleri için kullanılacak fonksiyon, sabit ve veri yapıları Linux, unix tabanlı işletim sistemleri için aşağıdaki dosyalar kullanılır.

#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>

Windows tabanlı işletim sistemlerinde aşağıdaki dosyalar kullanılır.

#include <winsock2.h>
#include <ws2tcpip.h>

İki bilgisayar arası haberleşme için her iki bilgisayarında soket yolunu socket fonksiyonu ile açması gerekir.

int socket(int domain, int type, int protocol);

Fonksiyonun domain parametresi kullanılacak olan protokol türünü, type kullanılacak veri gönderme protokolünü, protocol parametresi ise kullanılacak taşıma protokolünü belirtir.

Domain parametresi AF_UNIX (Unix dosyası), AF_INET (IPv4), AF_INET6 (IPv6) sabitlerini alır.

Type parametresi ise SOCK_STREAM (TCP), SOCK_DGRAM (UDP), SOCK_RAW sabitlerini alır.

Protocol parametresi de genellikle 0 değerini verilir.

#include <stdio.h>
#include <sys/socket.h>

int main(){

    int yol = socket(AF_INET, SOCK_STREAM, 0);

    return 0;
}

Soket oluşturma işlemi ile iletişim için yol açılmış olur.

İletişim bir tarafın alıcı bir tarafın verici olması ile sağlanır.

Alıcı temel olarak socket ile açılan yolu dinlerken verici ise socket ile açılan yol üzerinden alıcıya veri gönderir.

Sunucunun oluşturulması

Sunucu veya alıcı temel olarak belirlenen port adresine gelen istekleri dinler ve kabul eder.

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

Fonksiyon soketi belirlenen IP adresi ve port ile ilişkilendirir.

Fonksiyonun ilk parametresi socket fonksiyonu ile oluşturulan değeri, ikinci parametresi adres bilgilerini, son parametre ise adres bilgisi uzunluğunu alır.

Adres bilgisi kullanılan IP yapılandırmasına göre IPV4 (AF_INET) için sockaddr_in, IPV6 (AF_INET6) için sockaddr_in6 veri yapısını ile oluşturulur.

Ayarlar oluşturulduktan sonra sockaddr türüne dönüşüm yapılır.

İşlemciler verileri bellekte little endian ve big endian türüne göre tuttuğundan IP ayarlarının oluşturulması sırasında htonl, htons, ntohl, ntohs fonksiyonlarının kullanılması olası hataları en aza indirecektir.

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>  // memset için

#define PORT 1453

int main(){

    int yol = socket(AF_INET, SOCK_STREAM, 0);

    // Veri yapısı ayarları
    struct sockaddr_in sunucu_bilgileri;
    memset(&sunucu_bilgileri, 0, sizeof(sunucu_bilgileri));  // Veri yapısını sıfırlama
    sunucu_bilgileri.sin_family = AF_INET;
    sunucu_bilgileri.sin_addr.s_addr = htonl(INADDR_ANY);  // veya inet_addr("127.0.0.1")
    sunucu_bilgileri.sin_port = htons(PORT);

    // Adres bağlama
    if( bind(yol, (struct sockaddr*)&sunucu_bilgileri, sizeof(sunucu_bilgileri)) < 0 ){
        fprintf(stderr, "Bağlama sırasında hata oluştu.");
        return 1;
    }

    fprintf(stderr, "Bağlama başarılı.");

    return 0;
}

Soket IP adresi ve port ile ilişkilendirildikten sonra istekleri dinlemek için listen fonksiyonu kullanılır.

int listen(int sockfd, int backlog);

Fonksiyonun ilk parametresi socket fonksiyonu ile oluşturulan değeri, ikinci parametresi ise kuyrukta bekleyecek kişi sayısını belirtir.

Bu sayı 5 verildiğinde 6ncı sıradaki istek otomatik olarak ret cevabı verilir.

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>  // memset için

#define PORT 1453

int main(){

    int yol = socket(AF_INET, SOCK_STREAM, 0);

    // Veri yapısı ayarları
    struct sockaddr_in sunucu_bilgileri;
    memset(&sunucu_bilgileri, 0, sizeof(sunucu_bilgileri));  // Veri yapısını sıfırlama
    sunucu_bilgileri.sin_family = AF_INET;
    sunucu_bilgileri.sin_addr.s_addr = htonl(INADDR_ANY);  // veya inet_addr("127.0.0.1")
    sunucu_bilgileri.sin_port = htons(PORT);

    // Adres bağlama
    if( bind(yol, (struct sockaddr*)&sunucu_bilgileri, sizeof(sunucu_bilgileri)) < 0 ){
        fprintf(stderr, "Bağlama sırasında hata oluştu.");
        return 1;
    }

    // Dinleme
    if( listen(yol, 5) < 0 ){
        fprintf(stderr, "Dinleme sırasında hata oluştu.");
        return 1;
    }

    fprintf(stderr, "Gelen istekler dinleniyor.");

    return 0;
}

Dinleme işleminden sonra artık sunucu hazır hale gelmiş oldu.

Sunucu bu işlemlerden sonra gelen istekleri accept fonksiyonu ile kabul etmesi gerekir.

int accept(int sockfd, struct sockaddr *addr , socklen_t *addrlen);

Fonksiyonun ilk parametresi socket fonksiyonu ile oluşturulan değeri, ikinci parametresi bağlanacak istemcinin bilgilerinin tutulduğu veri yapısını, son parametre ise bu bilgilerin boyutunu alır.

Burada biraz kafalar karışabilir.

Çünkü accept fonksiyonu socket fonksiyonu gibi yeni bir yol oluşturur.

Bu yol istemciye ulaşan bir yoldur ve socket fonksiyonu ile oluşturulan yol ile karıştırılmamalıdır.

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>  // memset için

#define PORT 1453

int main(){

    int yol = socket(AF_INET, SOCK_STREAM, 0);

    // Veri yapısı ayarları
    struct sockaddr_in sunucu_bilgileri;
    memset(&sunucu_bilgileri, 0, sizeof(sunucu_bilgileri));  // Veri yapısını sıfırlama
    sunucu_bilgileri.sin_family = AF_INET;
    sunucu_bilgileri.sin_addr.s_addr = htonl(INADDR_ANY);  // veya inet_addr("127.0.0.1")
    sunucu_bilgileri.sin_port = htons(PORT);

    // Adres bağlama
    if( bind(yol, (struct sockaddr*)&sunucu_bilgileri, sizeof(sunucu_bilgileri)) < 0 ){
        fprintf(stderr, "Bağlama sırasında hata oluştu.");
        return 1;
    }

    // Dinleme
    if( listen(yol, 5) < 0 ){
        fprintf(stderr, "Dinleme sırasında hata oluştu.");
        return 1;
    }

    // istekler kabul ediliyor.
    struct sockaddr_in istemci_bilgileri;
    socklen_t boyut = sizeof(istemci_bilgileri);
    int istemci_yol = accept(yol, (struct sockaddr*)&istemci_bilgileri, &boyut);
    if(istemci_yol < 0){
        fprintf(stderr, "İsteklerin kabulü sırasında hata oluştu.");
        return 1;
    }

    return 0;
}

Program çalıştırıldıktan sonra accept fonksiyonu programı bloklayarak bir isteğin gelmesini bekleyecektir.

Bir istek geldiğinde ise sonra accept fonksiyonundan sonraki komutlar işlenecgöndermesiektir.

İstemcinin gönderdiği verileri almak için Linux ve Unix tabanlı işletim sistemlerinde yer alan read ve write fonksiyonları kullanılabilir.

Ancak TCP soket programlama işlemleri için kullanılan recv ve send fonksiyonlarının kullanılması daha fazla imkan vereceği için tercih edilmelidir.

UDP soket programlama işlemlerinde sendto ve recvfrom kullanılabilir.

ssize_t recv(int sockfd, void *buffer, size_t length, int flags);
ssize_t send(int sockfd, const void *buffer, size_t length, int flags);

Fonksiyonların ilk parametresi verinin gönderileceği ve alınacağı soket adresi (accept ile alınan), ikinci parametreleri gönderilen veya alınan veriyi, üçüncü parametre ise veri boyutunu alır.

Son parametre ise veri gönderimi sırasında ve alımı sırasında verinin nasıl gönderileceği ve alınacağı, işlem yapılıp yapılmayacağı gibi çeşitli sabit değerleri alır bu alana genellikle 0 değeri verilir.

Tüm işlemlerden sonra dosya işlemlerinde olduğu gibi soketin kapatılması gerekir.

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>  // memset için
#include <unistd.h>  // close için

#define PORT 1453

int main(){

    int yol = socket(AF_INET, SOCK_STREAM, 0);

    // Veri yapısı ayarları
    struct sockaddr_in sunucu_bilgileri;
    memset(&sunucu_bilgileri, 0, sizeof(sunucu_bilgileri));  // Veri yapısını sıfırlama
    sunucu_bilgileri.sin_family = AF_INET;
    sunucu_bilgileri.sin_addr.s_addr = htonl(INADDR_ANY);  // veya inet_addr("127.0.0.1")
    sunucu_bilgileri.sin_port = htons(PORT);

    // Adres bağlama
    if( bind(yol, (struct sockaddr*)&sunucu_bilgileri, sizeof(sunucu_bilgileri)) < 0 ){
        fprintf(stderr, "Bağlama sırasında hata oluştu.");
        return 1;
    }

    // Dinleme
    if( listen(yol, 5) < 0 ){
        fprintf(stderr, "Dinleme sırasında hata oluştu.");
        return 1;
    }

    // istekler kabul ediliyor.
    struct sockaddr_in istemci_bilgileri;
    socklen_t boyut = sizeof(istemci_bilgileri);
    int istemci_yol = accept(yol, (struct sockaddr*)&istemci_bilgileri, &boyut);
    if(istemci_yol < 0){
        fprintf(stderr, "İsteklerin kabulü sırasında hata oluştu.");
        return 1;
    }

    const char* mesaj = "Merhaba ben Yusuf SEZER";
    if( send(istemci_yol, mesaj, strlen(mesaj), 0) < 0 ){
        fprintf(stderr, "Mesaj gönderimi sırasında hata oluştu.");
        return 1;
    }

    close(istemci_yol);
    close(yol);

    return 0;
}

Sunucumuz gelen istekleri kabul edecek ve send fonksiyonu ile istemciye “Merhaba ben Yusuf SEZER” gönderecektir.

Sunucu ayrıca recv fonksiyonu ile kullanıcıdan bilgi de alabilir.

İstemcinin oluşturulması

İstemcinin oluşturulması sunucunun oluşturulmasından daha kolaydır.

Çünkü sadece socket fonksiyonu ile soket oluşturmak ve belirlenen adrese connect fonksiyonu ile bağlanmak yeterli olacaktır.

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>  // memset için
#include <unistd.h>  // close için

#define BOYUT 512

int main(){

    const char* sunucu_adres = "localhost";
    const int sunucu_port = 1453;
    
    // Bağlanılacak sunucu ayarları
    struct sockaddr_in sunucu_bilgileri;
    memset(&sunucu_bilgileri, 0, sizeof(sunucu_bilgileri));
    sunucu_bilgileri.sin_family = AF_INET;
    inet_pton(AF_INET, sunucu_adres, &sunucu_bilgileri.sin_addr);
    sunucu_bilgileri.sin_port = htons(sunucu_port);

    int yol = socket(AF_INET, SOCK_STREAM, 0);

    // Sunucuya bağlanma
    if( connect(yol, (struct sockaddr*)&sunucu_bilgileri, sizeof(sunucu_bilgileri)) < 0 ){
        fprintf(stderr, "Sunucu bağlantısı sırasında hata oluştu.");
        return 1;
    }
    
    char alinan_veri[BOYUT];
    int veri_boyutu = recv(yol, alinan_veri, BOYUT, 0);

    if(veri_boyutu < 0){
        fprintf(stderr, "Veri alımı sırasında hata oluştu.");
        return 1;
    }

    fprintf(stdout, "Alınan veri: %s", alinan_veri);

    close(yol);
    return 0;
}

Soket programlamaya başlangıç niteliğinde basit bir sunucu istemci oluşturmuş olduk.

Sunucu ve istemcinin hem veri göndermesi hem veri alması sunucunun birden fazla istemci ile haberleşmesi, sunucunun gelen isteğe göre işlemler yapması gibi özellikler eklenebilir.

Bu işlemler için paralel programlama yöntemlerinden Thread kullanmak faydalı olacaktır.

C++ ile geliştirmiş olduğum Soket programlama örneğine buradan ulaşabilirsiniz.

Hayırlı günler dilerim.

Yusuf SEZER

Yusuf SEZER

Computer Engineer who interested about web technologies, algorithms, artificial intelligence and embedded systems; constantly exploring new technologies.


Bunlara'da bakmalısın!