Socket Programlama'ya Giriş

Socket Programlama'ya Giriş


Bu dökümanda ağ üzerinden oynanan çok kullanıcılı (multiplayer) oyunları programlamak için kullanılan socket iletişim mekanizmasının kullanımını örneklerle inceleyeceğiz. Ortam olarak windows ve Visual C++ 6.0 kullanılacak. Fakat kod ufak değişiklikler ile başka sistemlere de kolaylıkla aktarılabilir.

Multiplayer?


Günümüzde internet kullanımının yaygınlaşması ile beraber programlar arasında haberleşme birçok uygulamanın vazgeçilmez parçası haline geldi. Oyunlarda internet çılgınlığından nasibini aldı. Bir anda her yandan MMORPG (Massive Multiplayer Online Role Playing Game : çok oyunculu devasa rol yapma oyunları) 'ler fırlamaya başladı. Counter Strike - Quake - Battlefield gibi oyunlar net cafelerde deliler gibi oynanılır oldu.

Artık insanlar evlerinde kendi başlarına oyun oynamak istemiyorlar. En azından eskisi kadar istemiyorlar. Kendileri kadar iyi oynayabilecek -gerçek- rakipler ile oynamak çok daha çekici geliyor. Belli başlı oyun firmaları da bu ihtiyacı görüp bir şekilde oyunlarına çok kişinin aynı anda oynayabilmesi özelliğini (multiplayer) eklemeye çalışıyor. Çok oyunculu ortam artık o kadar oyunların ayrılmaz bir özelliği olarak görünmeye başlandı ki, hepimiz oyun yorumlarındaki "güzel bir oyun, ama ne yazıkki çoklu oynama desteği yok" gibisinden lafları görmeye alışır hale geldik. Multiplayer desteği grafik, müzik, senaryo kadar oyunların bir parçası artık.

Multiplayer oyunların temelinde bilgisayarların haberleşmesi bulunuyor. Her bilgisayarda çalışan oyun, oyuncunun kararları, hareketleri doğrultusunda işleyişini sürdürürken, bir yandan da oyuncunun oyundaki konumunu, yaptığı işi, yani oyuna etkisini diğer oyunculara, oyuncuların bilgisayarlarına göndermek zorunda. Bu şekilde aynı oyunu oynayan birçok kişi aslında o oyunun ortak parçası olabiliyor.

Günümüz oyunlarında genel olarak kullanılan haberleşme mantığı istemci-sunucu (client-server) mimarisi olarak adlandırılır. Bu yöntemde sunucu bilgisayar olarak adlandırılan bilgisayarda çalışan bir program bulunur. İstemci bilgisayarlar ise teker teker oyuncu bilgisayarlarıdır. Oyuncular oyunlarını bu bilgisayarlarda çalıştırırlar ve multiplayer için sunucu bilgisayara bağlanırlar. Oyuncular arasındaki tüm haberleşme sunucu bilgisayar üzerinden gerçekleşir.

Örnek olarak multiplayer oyunların ilklerinden olan Quake oyununu ele alalım. Her oyuncuda Quake oyunu kurulu durumda. Oyunun tüm verileri, grafikler, sesler, harita dosyaları kişisel bilgisayarda bulunuyor. İnternet üzerinde herhangi bir yerde (yada kişisel ağda -LAN- bir yerlerde) ise Quake sunucu programı çalışmakta. Multiplayer oyun oynamak isteyenler quake oyunu içerisinden sunucunun IP adresini yada internet alan adını (domain name) girerek sunucuya bağlanırlar. Daha sonra oyun başlar..

Oyuncu bilgisayarlarındaki quake oyununun görevi temel olarak kullanıcının tuşlara basışlarını, ve mouse hareketlerini almak, bu bilgiler doğrultusunda bakış açısını ve varsa ilerleme yönünü belirlemek, daha sonra bu bilgileri sunucu bilgisayara göndermek.. Sunucu bilgisayar tüm oyunculardan gelen hareket bilgilerine göre oyuncu - oyuncu ve oyuncu - harita etkileşimlerini hesaplar (çarpışmalar, patlamalar, enerji ve bonus değişimleri..) ve güncellenmiş oyuncu konumlarını ve oyunun durumunu tekrar tüm oyuncu bilgisayarlarına gönderir. Oyunun ana mantığı ve karar verme mekanizması aslında sunucu bilgisayarda işlemektedir. Kişisel bilgisayarlardaki quake programları ise grafik gösterme ve kullanıcıdan girdi almada kullanılmaktadır.

Bizde bu haberleşme dünyasına bir giriş yapmak istiyoruz. Ama nasıl?. Aslında çok da zor değil.. İlk amacımız bilgisayarların bir şekilde haberleşmesini gerçekleştirmek olmalı. Bilgisayarlar arasındaki haberleşmeyi programlarken "socket mekanizması" olarak adlandırılan yapıyı kullanacağız.

İnternet ortamında iki bilgisayar arasında haberleşmek demek, bir bilgisayardan çıkan veri bloklarının istenilen başka bir bilgisayara ulaşması demektir. Veri her zaman bloklar halinde iletilir. Bu bloklara ise genel literatürde paket adı verilir. Quake örneğinde bu paketler içerisinde oyuncunun gidiş yönü, ateş edip etmediği gibi bilgiler bulunmakta idi.. Her paketin bir çıkış bilgisayarı birde gidiş bilgisayarı var. Peki bir paket hangi bilgisayara gideceğini nasıl biliyor? Her bilgisayarın internet üzerinde diğer bilgisayarlardan ayırdedilmesini sağlayan bir adresi var. Bu adrese IP (Internet Protokol) adresi diyoruz. Oluşturulan paketlerin başına gideceği bilgisayarın IP adresi bilgisi de işlenir. Bu sayede paketin adresine ulaştırılması kurulu internet - ağ altyapısı ile sağlanabilir. IP adresi yanında Port numarası olarak adlandırılan bir adres daha var. Port ları basit olarak nete bağlı bir bilgisayarın haberleşme kanalları olarak düşünebilirsiniz. Her program haberleşmek için belirli bir portu kullanmak zorundadır. Portlar numara:0 dan başlayıp numara 65537 ye kadar devam ederler. Bir program örneğin 13456 numaralı portu haberleşme için kullanıyor ise, başka bir program bu portu kullanamaz. Bilgisayarınız bir apartman ise: portları kapı numaraları olarak düşünebilirsiniz. Mahallenize gelen bir paket hangi apartmana (bilgisayara) gideceğini IP numarası (posta kodu+mahalle+apartman nosu) ile bulur.. Apartman içinde hangi daireye gidileceği ise port numarası (kapı nosu) ile belirlidir.. Bilgisayarınız bir apartman unutmayın :)..

Birde bağlantı türü denilen kavram var. Bu kavram aslında paketlerinizin iletilmesinden sorumlu alt yapının (işletim sistemi,ethernet kartı, driver programı) iletimi sağlama metodu ile ilgili. İki tip var. Bağlantı tabanlı (connection oriented) ve bağlantı tabanlı olmayan (connectionless).. İlk tip aynı zamanda TCP ikinci tipse UDP olarak ta adlandırılır. Aslında iki tiptede bizim için çok değişen bir şey yok. Sadece uygulamada ufak farklılıklar ile temel birkaç işlevsel farklılık bulunmakta. İlk tipte sanal olarak iki haberleşen bilgisayar arasında bir haberleşme köprüsü kurulumu sağlanır. Buna bağlantı diyoruz. Bağlantı başarı ile sağlandığı zaman artık iki bilgisayar arasında gönderilen paketler sistem tarafından güzelcene sıra ile iletilir.. Sizde bağlantı kopmadığı sürece bu paketlerin yerlerine gönderdiğiniz sıra ile gittiğinden emin olup mutlu olabilirsiniz. Her hangi bir sebepten dolayı bir paket gideceği yere iletilemediği zaman, iletişim sistemi otomatik olarak tekrar göndermeyi deneme işlemini gerçekleştirir. İkinci yapıda ise, bağlantı kurma diye bir şeyden söz edilmez. Paketlerinizi adresi belli olan bir bilgisayara gönderirsiniz. Sistem bu paketlerin o bilgisayara başarı ile ulaştığını garanti edecek ve doğrulayacak bir mekanizmaya sahip değildir. Böyle bir mekanizmayı sizin programınız ile kurmanınz gerekir. 1. yöntem daha güvenli iken , ikinci yöntem ise daha hızlıdır. Oyunlarda genel olarak 2. yöntem kullanılır. Çünkü oyun haberleşmesinde hız çok ama çok önemlidir.

Client (İstemci)


Bu kadar teori yeter.. Hadi uygulamaya geçelim.. İlk olarak basit bir istemci yazılımı (quake?.. hayır daha değil.. :) ) programlayacağız. Yaptığımız program ile sunucu bilgisayara "merhaba" mesajını yollamak istiyoruz.

// İlk olarak winsock kütüphanesini linker a belirtelim ve ana socket header ını include edelim..

#pragma comment( lib, "wsock32.lib" )
#include <stdio.h>
#include <winsock2.h>

int main(int argc,char **argv) {

// Şimdi de sistemdeki winsock alt yapısını aktif hale getirmek için aşağıdaki kodu yazalım..

WSAData wsaData;
if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {
    printf("socket sistemini başlatmada hata! (WSAStartup...)");
}

// Bu işlemin %99.9 başarı ile tamamlanması gerekir..
// Şimdi ise karşı haberleşmeye geçeceğimiz bilgisayarın (karşı taraf)
// adresini belirmek için bir yapıyı uygun şekilde dolduralım..

// dolduracağımız yapı bu
sockaddr_in sinIgInterfaceSend_remote;

// bu parametre AF_INET olmalı
sinIgInterfaceSend_remote.sin_family = AF_INET;

// bu parametreye karşı tarafın IP adresi yada host adı gelecek..
// 127.0.0.1 IP adresi aslında kendi bilgisayarımızı belirtir. Bu adresi
// kullanarak kendi bilgisayarımızdaki bir sunucu ya paket gönderebiliriz.
// programlarımızı test ederken bu IP adresi çok işimize yarayacak.
sinIgInterfaceSend_remote.sin_addr.s_addr = inet_addr("127.0.0.1");

// karşı tarafın port numarası
sinIgInterfaceSend_remote.sin_port = htons(19998);

// ve bloğun bir kısmını sıfırlamamız gerekiyor..
memset(&sinIgInterfaceSend_remote.sin_zero,0,8);

// Şimdide iletişimde kullanacığımız socket i oluşturalım..
// SOCK_DGRAM parametresi haberleşme yöntemi olarak UDP kullanacağımızı belirtiyor.
SOCKET socket_send = socket(AF_INET,SOCK_DGRAM,0);

//Evet artık UDP haberleşmesi için gerekli hazırlıkları tamamladık.
// Şimdi sıra karşı tarafa veri göndermekte.. Bu iş için sendto(...) foksiyonunu/ kullanacağız.

const char *mesaj = "merhaba";
int result = sendto(socket_send,mesaj,strlen(mesaj),0,(struct sockaddr *)&sinIgInterfaceSend_remote,sizeof(struct sockaddr));
if (result == SOCKET_ERROR)
    printf("socket hatası");
else
    printf("veri gönderildi: gönderilen byte: %d",result);

return 0;
}

// sendto(...) fonksiyonunu biraz açıklamamız gerekiyor.
// Bu fonksiyon ile bir socket üzerinden istediğimiz veriyi gönderebiliyoruz.
// Fonksiyonun aldığı parametreleri sırayla açıklamak gerekirse:

// socket_send: haberleşmede kullandığımız socket
// mesaj: göndermek istediğimiz veriyi işaret eden bir char* tipinde pointer
// strlen(mesaj): göndermek istediğimiz mesajın uzunluğu..
// Burayı byte cinsinden doldurmak gerekiyor..
// Ben kolaylık olsun diyerek strlen(..) fonksiyonunu kullandım, istersek direk 7 de yazabilirdik..
// 0: bu parametre flags olarak geçer .. bizim durumumuzda 0 olması gerekiyor.
// (struct sockaddr *)&sinIgInterfaceSend_remote: veri göndereceğimiz makineyi belirten adres yapısına pointer..
// sizeof(struct sockaddr): adres yapısının boyutu..

// Göndermede bir hata durumu söz konusu ise sendto(...) fonksiyonu
// SOCKET_ERROR hata kodunu döndürür.
// İşlem başarılı ise gönderilen byte sayısı geri döner (bizim örneğimizde 7 geri dönecektir..)

İşte oldukça basit bir istemci uygulaması yazdık. Çok da zor olmadı değil mi?.. Şimdi birde sunucu uygulaması yazalım.. Bu program da istemciden aldığı mesajı ekrana yazdırsın..

Server (Sunucu)


// İstemcide yaptığımız bazı ön işlemleri tekrar yapıyoruz..
#pragma comment( lib, "wsock32.lib" )
#include <stdio.h>
#include <winsock2.h>

int main(int argc,char **argv) {

WSAData wsaData;
if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {
    printf("socket sitemini başlatmada hata! (WSAStartup...)");
}

// Şimdide veri almak istediğimiz port numarasını sisteme belirtmek için bir yapıyı doldurmamız gerekiyor.
sockaddr_in sinIgInterfaceRecv_local;

// Bu parametre AF_INET olmalı
sinIgInterfaceRecv_local.sin_family = AF_INET;

// Bu paramtre de aynen korunacak.
sinIgInterfaceRecv_local.sin_addr.s_addr = htonl(INADDR_ANY);

// Bu parametrede veriyi okuyacağımız portu belirtiyoruz.
// Buraya istemci uygulamada kullandığımız port numarasını aynen yazmamız gerekiyor.
sinIgInterfaceRecv_local.sin_port = htons(19998);

// Bu tarafıda sıfırlayalım..
memset(&sinIgInterfaceRecv_local.sin_zero,0,8);

// İstemcide olduğu gibi socket imizi oluşturuyoruz..
SOCKET igsocket_recv = socket(AF_INET,SOCK_DGRAM,0);

// Bu noktaya kadar istemi ile sunucu kodları arasında pek bir fark yok.
// Sunucu uygulamasında veri almaya başlamadan önce yapmamız gereken
// son bir adım daha kaldı. Bu adım ile hazırladığımzı adres yapısını socket e
// bağlıyoruz. Yani socket imizi adreslendirmiş oluyoruz aslında.

if (bind(igsocket_recv, (struct sockaddr*) &sinIgInterfaceRecv_local, sizeof(struct sockaddr)) < 0) {
    printf("socket bağlama hatası!");
}

// bind(...) fonksiyonunun geri dönüş değeri negatif ise bağlamada bir hata oluşmuş demektir.
// bu tarz hatalar genelde kullanımda olan bir porta bağlanmaya çalıştığımızda
// ortaya çıkar..

// Ve şimdide son olarak sıra veri okumaya geldi.. Veri okumak için recvfrom(...) fonksiyonunu kullanacağız.

char mesaj[256];
int result = recvfrom(igsocket_recv,mesaj,7,0,0,0);
if (result != SOCKET_ERROR)
    printf("alınan mesaj: %s",mesaj);

    return 0;
}

// recvfrom(...) fonksiyonunun kullanımı sendto(...) fonksiyonuna göre daha kolay görünüyor.
// ilk parametre olarak kullanacağımız socket i belirtiyoruz.. daha sonra okunan verinin aktarılacağı
// bir bölge belirtmemiz gerekiyor.. Sonraki parametre okumak istediğimiz verinin boyutu..
// İstemcinin 7 byte uzunluğunda bir veri gönderdiğini bildiğimiz için direk olarak 7 sayısını verdik..
// Son 3 parametreyi 0 olarak verebilirsiniz.

// recvfrom(...) fonksiyonu veriyi başarı ile okuduğu zaman geriye kaç byte okuduğu bilgisini
// döndürür. Bir hata durumunda ise (örneğin bağlantının kopması gibi..) SOCKET_ERROR
// dönecektir. Mesaj uzunluğuna istediğiniz değeri girebilirsiniz.. 7 yerine 255 de girebilirdik..
// Bu durumda bize ulaşan veri her zaman 7 byte olacağından 7 byte okunup, mesaj
// alanına yazılacaktı. Okuyacağınız verinin uzunluğunu bilmediğiniz zamanlarda daha
// büyük bir alan ve sayı belirtmek işinize yarayabilir..

Evet basitçene bir client - server uygulaması yazdık. Bu uygulamanın özellikle sunucu kısmı ile ilgili açıklığa kavuşturulması gereken bir kaç ayrıntısı var diye düşünüyorum. recvfrom(...) fonksiyonu önemli. Bu fonksiyon socket den veri okumaya çalışıyor. Peki veri olmassa ne olur. Bu durumda bu fonksiyon veriyi okuyana kadar beklemeye girecektir.. Buna bloklama (blocking..) diyoruz. Yazacağınız sunucu nun temel döngüsü şu şekilde olacaktır..

while (oyun_devam_eder) {

    // veriyi al
    recvfrom(...)
    // veriyi işle
    // her bağlı oyuncuya veri gönder..
    sendto(...)

}

Bloklama mekanizmasını kullanmayarakta sunucumuzu oluşturabilirdik.. Socket imizi oluştururken kullanacağımız bikaç değişik parametre ile bunu sağlayabiliriz. Ama ben açıkçası bloklamalı socketleri kullanıyorum ve şimdiye kadar da bir zararlarını görmedim.. Sadece dikkat etmeniz gereken nokta: veriyi almak için ayrı bir thread kullanmak uygulamanızın türüne göre oldukça kullanışlı olabilir.. Thread kullanımı için sitemizde bir döküman var..

Örnek Kod


Bu dökümanda çok basit olarak windows altında UDP socket haberleşmesi konusunu anlatmaya çalıştım. Olabildiğince yalın bir dille konunun üzerinden geçtim. Birçok ayrıntıyı fazla kafa karıştırmamak için esgeçmek zorunda kaldım. Konu hakkında uygulama yapmayı düşünenlere önerim: kesinlikle bu makale ile yetinmeyin. Örnek proje üzerinde uğraştıktan sonra mutlaka bi google çekip (www.google.com) başka makale ve örneklere de göz atın. Aşağıdaki linkten örnek uygulamayı indirerek üzerinde değişikler yapıp konuyu daha iyi kavrayabilirsiniz. Kolay gelsin.

[ örnek client-server uygulaması VC++6 proje dosyaları ]

Deniz Aydınoğlu :: 2004 :: www.oyunyapimi.org




Bu yazının bulunduğu site: OyunYapimi.org
http://www.oyunyapimi.org

Bu yazı için adres:
http://www.oyunyapimi.org/modules.php?name=Sections&op=viewarticle&artid=55