6 Temmuz 2015 Pazartesi

TCP/UDP/Netlink ve Linux

Socket Çeşitleri
Bir socketi açmak için 3 tane parametre lazım.

İlk parametre socket ailesini, ikinci parametre aileden hangi tip socketin istendiğini belirtir. Üçüncü parametre protokol türünü belirtir.

Üçüncü parametre genellikle 0 verilir, böylece socket tipinin protokolü otomatik olarak seçilir, ancak bazen protokol içindeki headerları kendimiz doldurmak isteriz. İşte o zaman IPPROTO_TCP,IPPROTO_UDP gibi sabitleri geçmek gerekir.

getprotobyname metodunun sabitleri isim ile almak için kullanıldığını da gördüm. Örnek:


AF_INET, PF_INET Ailesi

AF_INET ve PF_INET aynı şeyler ve IP protokol ailesine ait soket çeşitlerini içeriyorlar.

Bu aileye ait socket çeşitlerinin sabitleri aşağıda.

Socketleri sabitlerini getprotobyname metodu ile alabilmek te mümkün.
Örnek:


Aşağıdaki şekilde Linux üzerinde açılabilecek soket çeşitleri gösteriliyor, ve AF_INET ailesinin IP seviyesine indiğini görmek mümkün.





 

AF_UNIX
Bu aileye ait soketler Unix Domain Socket olarak anılıyorlar.

PF_PACKET veya SOCK_PACKET socketleri
PF_PACKET socketi yazısına taşıdım.

NETLINK Soketleri 

Netlink socketlerini kullanarak kernel ile bilgi alışverişinde bulunmak mümkün. Bu socketi açmak için netlink(7) sayfasına bakılabilir.

PF_NETLINK socketler ile de ethernet kartına ait bir çok bilgiye erişmek te mümkün. Get event for NIC/ethernet card link status on linux başlıklı yazıda bir örnek görülebilir.

Socket Aileleri ve Adresler
Socket aileleri farklı olunca ister istemez farklı adres sınıfları da kullanılmak zorunda kalıyor.

Java
Java'da sadece AF_INET ailesi var. Dolayısıyla sadece InetSocketAddress sınıfı var. Buna rağmen ilerde genişleyebilir diye InetSocketAddress sınıfı SocketAddress ata sınıfından kalıtıyor.

Tüm Protokoller İçin Socket Metodları Nasıl Çalışıyor ?
Her protokolün farklı özelliği olmasına rağmen, hepsi için aynı API'yi kullanıyoruz. İşin sırrı socket veri yapısının içinin bize gizli olmasında yatıyor. Bu sayede haberimizin olmadığı alanları kullanarak, socket kütüphanesi doğru protokole yöneliyor.Örneğin bir kütüphanede socket metodu şöyle yazılmıştı.

int socket (int domain, int type, int proto) {
 socket_t* so; i32t fd;
 so = socketCreate (domain, type, proto) ; //Burada so yapısı function pointerlar ile dolduruluyor

fd = fdAlloc ();
so->so_fd = fd;
return fd;
}

typedef struct socket_s {
 protosw_t* so_proto; //Protocol switch used by socket
} socket_t;

typedef struct protosw_s {
 protoFuncs_t * pFuncs;
}

typedef struct protoFuncs_s {
 pfs_t initFunc; //Initialize routine
 pfs_t closeFunc;//close routine
 pfi_t sendFunc;//send routine
 pfi_t recvFunc;//recv routine
 pfs_t bindFunc;//bind routine
 pfs_t connectFunc;//connect routine
 pfs_tshutdownFunc;//shutdown routine
 pfi_t acceptFunc;//accept routine
 pfs_t listeFunc;//listen routine
} protoFuncs_s;

Görüldüğü gibi function pointer'lar ile socket kendi protolüne ait metodu çağırabiliyor.

UNIX Domain Soketi Açma
C ile Unix Domain Socket Kullanımı yazısına taşıdım.

IP Soketi Açma
Yukarıda, bu aileye ait bir çok soket çeşidi olduğu görünüyor. Konuyla ilgili örnekler aşağıda.

Ham Soket Açmak
Ethernet Header Verilebilen Ham Soket Açmak
PF_PACKET socketi yazısına taşıdım.
   
IP Header Verilebilen Ham Soket Açmak
Ham IP soketi açarak IP header'ını kendimiz sağlayabiliyoruz. SOCK_RAW ve IPPROTO_RAW parametrelerini kullanmak lazım. Örnek:

IP Header 20 byte uzunluğunda.

Burada IP header'ının kodda nasıl gösterileceğine dair bir örnek var.

Buradaki örnekte bir IP header'ının nasıl doldurulacağı var.

char sendbuf[1024];
struct iphdr *iph = (struct iphdr *) (sendbuf);

// IP Header
iph->version = 4;
iph->ihl = 5;
iph->tos = 16; // Low delay
iph->id = htons(54321);
iph->ttl = ttl; // hops
iph->protocol = 17; // UDP

// Source IP address, can be spoofed
iph->saddr = inet_addr("192.168.0.112");

//Destination IP address
iph->daddr = inet_addr("192.168.0.111");
 

//Gönderilecek veriyi tampona yaz
 
// Length of IP payload and header
iph->tot_len = htons(tx_len - sizeof(struct ether_header));

//Calculate IP checksum on completed header
iph->check = csum((unsigned short *)(sendbuf+sizeof(struct ether_header)), sizeof(struct iphdr)/2);
 

TCP Header Verilebilen Ham Soket Açmak
Bu socket ile hem paket gönderebilir, hem de alabiliriz. Socket şöyle açılır.
socket(AF_INET,SOCK_RAW,IPPROTO_TCP);
Bu socket tipiinde TCP header'ını kendimiz sağlayabiliyoruz. 
1. Parametre - Socket ailesi
AF_INET yerine bazı kodlarda PF_INET sabiti de görülebilir.

2. Parametre - Socket tipi
SOCK_RAW ise header'ı kendimizin oluşturacağını belirtir.

3. Parametre - Protocol tipi
TCP headerını kendimizin oluşturacağını belirtiriz. TCP header da IP header gibi 20 byte uzunluğunda.  IPPROTO_TCP sabiti aşağıda görülebilir.
/*
 * Protocols (RFC 1700)
 */
#define IPPROTO_IP              0               /* dummy for IP */
#define IPPROTO_UDP             17              /* user datagram protocol */
#define IPPROTO_TCP             6               /* tcp */
Eğer IP header'ını da kendimiz oluşturmak isteseydik şöyle yapardık.

int on = 1;
error =  setsockopt(sock,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on)); 
UDP Header Verilebilen Ham Soket Açmak
UDP'ye de header verilebiliyor. Bu header içinde ve TCP header içinden neden iki defa length alanı var diye bir soru sorulmuş. Cevabı burada.



Bir başka görünüşü ise buradan aldım.

Socket şöyle açılır.


sd = socket(AF_INET, SOCK_RAW, IPPROTO_UDP);
 
TCP Soketi
Konuyu TCP Socket Kullanımı başlıklı yazıya taşıdım.

UDP Soketi
Konuyu UDP Socket Kullanımı başlıklı yazıya taşıdım.

Soketin Özelliklerini Atamak
Sadece soketin bir özelliğini atamak için SOL_SOCKET sabitini kullanmak lazım. 
Socketlere atanabilen özellikleri gösteren şekil aşağıda.

Socket ve NonBlocking
Windows
unsigned long nonBlocking = true;
ioctlsocket (socket,FIONBIO,&nonBlocking);
Linux
Burada gösterildiği gibi hem ioctl hem de fcntl kullanılabilir.
 Socket ve Zaman Aşımı
Socket ile okuma yazma işlemi yaparken zaman aşımı verilmek istenebilir. Bu durumda setsockopt() metodu kullanılıyor. Eğer  okuma işlemine zaman aşımı tanımlanmak istenirse SO_RCVTIMEO, yazma işlemi içinse SO_SNDTIMEO parametreleri kullanılıyor. Bu parametreler çok portable değiller çünkü Linux'ta zaman aşımı için struct timeval kullanılırken, Windows'ta DWORD kullanılıyor.
Linux Örnek:

Windows Örnek:
DWORD nTimeout = 30000; // 30 seconds
setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&nTimeout, sizeof(nTimeout));

Java
setSoTimeout()  metoduna milisaniye olarak süre verilir. Verilen süre içinde işlem gerçekleşmezse, SocketTimeoutException  atılır.Örnek:
Socket socket = //...
socket.setSoTimeout(10*1000);

Socket ve Linger
C
Örnek:
struct linger li;
li.l_onoff = 1;
li.l_linger = 0;
setsockopt(socket,SOL_SOCKET, SO_LINGER, (char *) &li, sizeof(li));

Socket Veri Yapıları
Socket metodları ile kullanılan veri yapıları karışık. Aşağıda özetlemeye çalıştım.
sockaddr_ll
Bu veri yapısı ethernet seviyesinde veri göndermek için kullanılır. Örnek:

in_addr
Bu veri yapısı IPv4 adresini binary olarak saklamaya yarar ancak platformdan platforma farklı şekilde tanımlanmıştır. En basit tanımlanmış hali aşağıdaki gibidir.
struct in_addr {
    unsigned long s_addr;  //IP adresinin binary hali
};
Eğer sunucu bir socket açıyorsak, herhangi bir ağ arayüzünü kullanması için INADDR_ANY sabitini de kullanabiliririz. Örnek:

struct sockaddr_in serv_addr;
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_port = htons(portno);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
Bu sabit yerine IPv4 için "0.0.0.0.0" IPv6 için ise "0.0.0.0.0.0.0" kullanılabilir. Burada "::" kullanılabileceği de yazılı.

C#'ta bu değer IPAddress.Any ve IPAddress.IPv6Any alanlarında tanımlıdır. Eğer sockete
var listener = new TcpListener(IPAddress.Any, 2222);
listener.Server.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, 0);

ile sadece IPv6 kullanması talimatı verilmişse sadece IPAddress.IPv6Any kullanılabilir.

sockaddr_in
Bu veri yapısı IP seviyesinde veri göndermek için kullanılır. <netinet/in.h> dosyasında tanımlıdır. Kullanmak için struct sockaddr_in sin; şeklinde tanımlamak gerekir. Örnek:

struct sockaddr_in {
      short            sin_family;    // IP türü. AF_INET, AF_INET6
      unsigned short   sin_port; // Gideceği port. htons(3490)
      struct in_addr   sin_addr; // Gideceği adres
      char             sin_zero[8];  // ?
  };
Windows dünyasında bu yapı büyük harflerler yazılmış. Yani SOCKADDR_IN şeklinde.
Veri yapısının byte cinsinden büyüklüğünü gösteren bir şekil aşağıda.

sockaddr_in6
Bu veri yapısında da ilk eleman sin_family olduğu için, kernel

sockaddr_storage
Bu veri yapısı IPv4 ve IPv6 için ayrım yapmadan kod yazabilmeyi sağlıyor. Örnek:
// General socket address holding structure, big enough to hold either
// struct sockaddr_in or struct sockaddr_in6 data:


struct sockaddr_storage {
    sa_family_t  ss_family;     // address family

    // all this is padding, implementation specific, ignore it:
    char      __ss_pad1[_SS_PAD1SIZE];
    int64_t   __ss_align;
    char      __ss_pad2[_SS_PAD2SIZE];
};

sockaddr
Socket metodlarını çoklamamak için yukarıdaki struct'ların ortak atası gibi bir yapı düşünülmüş. sockaddr_in, sockaddr_in6 ve sockaddr_un veri yapılarının hepsinin ilk elemanı "family" yani soket çeşidini gösteren alan ile başlar.  

Bu yapı ile sendto, bind vs. gibi metodların hepsine bu ortak yapı geçiliyor ancak gerçekte yukarıdaki yapının pointer'ı ve büyüklüğü geçmiş oluyor.
 
UDPINPUT

UDP ve Maximum Transmission Unit

UDP ile kullanılabilen MTU değeri eğer ethernet kullanılıyorsa 1472 byte oluyor. Bu rakam şöyle hesaplanıyor. Ethernet MTU değeri 1500 byte.  20 byte IP header ve 20 byte ta UDP header için kullanılsa UDP verisi için 1472 byte kalıyor.  

Eğer daha büyük bir paket gönderirse bu paket parçalara ayrılır, ancak parçaların kaybolmasını UDP kontrol etmediği için verinin kaybolma ihtimali var.

Ek bilgi olarak şunu yazmak istedim. IPv4 için en az gönderilebilecek paket büyüklüğü ise 576 byte uzunluğunda

All hosts must be prepared to accept datagrams of up to 576 octets 
(whether they arrive whole or in fragments).


Eğer UDP'nin hiç parçalanmadan gönderilmesi mümkün olsaydı burada da açıklandığı gibi 
0xffff - (sizeof(IP Header) + sizeof(UDP Header)) = 65535-(20+8) = 65507 byte

gönderilebilirdi.IP başlığındaki Total Length alanı 2 byte olduğu için 0xffff sayısı geldi. IP header 20 byte, UDP header da 8 byte olduğu için 28 byte'lık alanı veri için kullanamıyoruz.
sendto() returns values greater than MTU on SOCK_DGRAM (UDP) socket  sorusunda ise Linux çekirdeğinde MTU keşfi için bir bir kod bulunduğu ve eğer istenirse  IP_MTU_DISCOVER seçeneğinin socket için kapatılabileceği söyleniyor.

UDP Input Queue ve Zamanlama
Linux üzerindeki input kuyruğu için eğer isternirse aşağıdaki kod parçasıyla SO_TIMESTAMP bayrağı atanabilir. Benzer bir açıklama ise burada var.

int timestampOn = 1;
setsockopt(inSocket, SOL_SOCKET, SO_TIMESTAMP, (int *) &timestampOn, sizeof(timestampOn));

Bu işlem ile çekirdek tarafından okunan her paket okunma zamanı ile işaretlenir. Böylece paketi çekirdek alıp kuyruğa koyduktan sonra uygulama paketi okuyuncaya kadar geçen süre hesaplanabilir. Örnek için buraya bakılabilir.

UDP Receive Buffer Büyüklüğü
UDP'nin receive buffer büyüklüğü ayarlanabilir ancak verilen değer MTU'dan büyük olursa bu işlemin hiç bir anlamı kalmıyor.
C
int option = 262144;
if(setsockopt(m_SocketHandle,SOL_SOCKET,SO_RCVBUF ,(char*)&option,sizeof(option)) < 0)
{
    printf("setsockopt failed\n");

}
Java
Buradaki soruda açıklandığı gibi DatagramSocket sınıfının setReceiveBufferSize() metodu kullanılarak büyüklüğü ayarlanabilir.

UDP ve Broadcast
UDP socketleri broadcast için ayarlanabilirler. Verinin gönderileceği adres ağın kullandığı broadcast adresi olmalı. Bu adres genelde 255.255.255.255'dir. send() metoduna broadcast için hazırlanmış ip adresi verilir.
struct sockaddr_in dst;
memset(&dst, 0, sizeof(dst));
dst.sin_family = AF_INET;
dst.sin_addr.s_addr = inet_addr("255.255.255.255");
dst.sin_port = htons(67);
int option  = 1;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &option , sizeof(option ));
C#
Örnek:
var udpClient = new UdpClient();
var endPoint = new IPEndPoint(IPAddress.Broadcast, 67);
byte[] dgram = //...
udpClient.Send(byte[] dgram, int bytes, endPoint);
UDP ve Multicast
UDP ve Multicast yazısına taşıdım.


2 yorum:

  1. uzun zamandan beri sitenizi takip etmekteyim.Emeginize saglık cok yararli bilgiler paylaşıyorsunuz.

    YanıtlaSil