12 Şubat 2020 Çarşamba

C ile TCP Socket Kullanımı

Giriş
Linux'da socket kullanabilmek için genellikle şu dosyalar dahil edilir.
#include<sys/socket.h>
#include<netinet/in.h>
#include<sys/types.h>
TCP sunucucu açmak için çağrı sırası şöyle
socket -> bind(serverfd) -> listen(serverfd) -> accept(serverfd) -> recv (clientfd)

Socket Handle
Winsock API'si SOCKET yapısı ile çalışıyor gibi görünüyor. Ancak altta SOCKET aslında unsigned int. Linux'ta ise socket API'si int ile çalışıyor. Ortak bir yapı için int olarak tutulabilir.
SOCKADDR
Bir çok Windows kodunda sockaddr yapısına cast etmek yerine SOCKADDR yapısına cast edildiğini görebiliriz.
struct sockaddr_in local;
...
bind(sockfd, (SOCKADDR*)&sockinfo, sizeof(sockinfo));
SOCKADDR yerine sockaddr rahatlıkla kullanılabilir.

Winsock'un başlatılması
Windows'ta socketleri kullanmaya başlamadan önce Winsock kütüphanesinin başlatılması gerekiyor. Kütüphaneyi kullanmak için şu dosya dahil edilir ve linklenir. Şöyle yaparız.
#pragma comment(lib, "ws2_32.lib")
#include <WinSock2.h>
mingw'de şöyle yaparız.
x86_64-w64-mingw32-gcc test.c -lws2_32 -o a.exe
Örnek
Şöyle yaparız.
WSADATA wsaData;

if (WSAStartup(MAKEWORD(1,1), &wsaData) != 0) {
  fprintf(stderr, "WSAStartup failed.\n");
}
Örnek
Şöyle yaparız. Winsock sürümü olarak 2.2 veya 2.1 kullanılabilir.
WSADATA wasData;
int wsaRes = WSAStartup(MAKEWORD(2, 2), &wasData);
Çağrı başarısız ise 0'dan farklı bir sonuç döner. Şöyle yaparız.
if(wsaRes != 0) {...}
Eğer hata olursa şöyle yazdırabiliriz.
 printf("Last error code was(1): %d\n", WSAGetLastError());
Uygulamadan çıkarken şöyle yapmak gerekir.
WSACleanup();
Socket Yaratmak
Örnek - AF_INET + Family + Flag
Winsock ve Linux'ta şöyle yaparız.
int sockfd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
Örnek
IPv6 için şöyle yaparız.
int serverSockFd = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
Örnek - AF_INET + Family + Flag
Flag sabitleri yerine rakam da kullanılabilir. Şöyle yaparız.
int sockfd = socket(AF_INET , SOCK_STREAM , 0);
Şöyle yaparız.
SOCKET socfd = socket(AF_INET, SOCK_STREAM, NULL);
Örnek - PF_INET + Family + Flag
AF_INET yerine yeni sabitleri de kullanabiliriz.
int sockfd = ::socket(PF_INET, SOCK_STREAM, 0);
Windows'ta işlemin başarılı olduğu şöyle kontrol edilir.
if (INVALID_SOCKET == sockfd) {...}
Invalid socket -1 değerine sahiptir.
#define INVALID_SOCKET  (SOCKET)(~0)
accept metodu
Açıklaması şöyle. non blocking socket ile kullanılması tavsiye ediliyor.
There may not always be a connection waiting after a SIGIO is delivered or select(2), poll(2), or epoll(7) return a readability event because the connection might have been removed by an asynchronous network error or another thread before accept() is called. If this happens, then the call will block waiting for the next connection to arrive. To ensure that accept() never blocks, the passed socket sockfd needs to have the O_NONBLOCK flag set (see socket(7)).
Üçüncü parametrenin açıklaması şöyle.
The addrlen argument is a value-result argument: the caller must initialize it to contain the size (in bytes) of the structure pointed to by addr; on return it will contain the actual size of the peer address.'
Yani çağrı sonucunda değeri değişiyor. Her accept çağrısından önce üçüncü parametreyi ilklendirmek gerekir. Parametre tipi olarak int veya socklen_t kullanılabilir.
Şu kod ilklendirme yapılmadığı için yanlış.
struct sockaddr_in clientinfo;
int clientsockSize;
for(;;)
{
  int child_fd = accept(sockfd, addr, &addrlen);
  ...
}
Şöyle yaparız.
struct sockaddr_in clientinfo;
int clientsockSize = sizeof(clientinfo);
int clientfd = accept(sockfd, (struct sockaddr*)&clientinfo, &clientsockSize); 
Çağrı başarılı ise -1'den farklı bir değer döner, hata varsa -1 döner. Windows'ta çağrı sonucu şöyle kontrol edilir.
SOCKADDR_IN client_sock = accept(...);
if (c_socket == INVALID_SOCKET)
{
  printf("Error %d", WSAGetLastError());
}
Çağrı bağlanan soketi döndüğü için bu değeri saklamak gerekir. Şu kod dönülen socket fd'yi saklamadığı için yanlıştır.
if(accept(...) >= 0) {
  ...
}
bind
Sunucu socketlerde kullanılır. 
Örnek
Şöyle yaparız. Hata varsa 0'dan farklı bir değer döner.
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(1234); // 
local.sin_addr.s_addr = htonl(INADDR_ANY);

bind(sockfd, (struct sockaddr*)&local, sizeof(local));
Çağrı başarılı ise 0 döner, hata varsa 0'dan farklı bir değer döner. Windows'ta çağrı sonucu şöyle kontrol edilir.
if (SOCKET_ERROR == bind(...)) {...}
Socket Error şu değere sahiptir.
#define SOCKET_ERROR            (-1)
Örnek - Hata kontrolü
Şöyle yaparız.
int bind_success = bind(...);
if (bind_success != 0)
{
  printf("Error %d", WSAGetLastError());
}
Örnek - Hata kontrolü
Şöyle yaparız.
if( bind(...) < 0)
{
    //print the error message
    perror("bind failed. Error");
    return 1;
}
blocking mode
Windows'ta ioctlsocket (sockfd,FIONBIO,...) ile yapılır. Linux'ta fcntl (sockfd,F_SETFL,...) ile yapılır. Çağrı başarılı ise 0 döner.

buffering mode
Windows ve Linux'ta aynıdır. setsockopt (sockfd,IP_PROTO_TCP,TCP_NODELAY,..) ile yapılır.

connect
İstemci soketlerde kullanılır.
Örnek
Şöyle yaparız.
struct sockaddr_in remote;

remote.sin_family = AF_INET;
remote.sin_port = htons(2000);
remote.sin_addr.s_addr = inet_addr("127.0.0.1");

connect(sockfd, (struct sockaddr*) &remote, sizeof(remote));
Örnek - Hata kontrolü
Çağrı başarılı ise 0 döner, hata varsa 0'dan farklı bir değer döner.
int connection_success = connect(...);
if (connection_success != 0)
{
  printf("Error %d", WSAGetLastError());
}
Eğer socket non-blocking ise asenkron çalışmak gerekir. Bu durumda select() metodunu kullanırsak ve soketimizi write seti'ine verirsek bağlantının başarılı olduğunu şöyle anlarız. Windows'ta ise except_fd setini kullanmak gerekir.
// call this select() has indicated that (fd) is ready-for-write because
// (fd)'s asynchronous-TCP connection has either succeeded or failed.
// Returns true if the connection succeeded, false if the connection failed.
// If this returns true, you can then continue using (fd) as a normal
// connected/non-blocking TCP socket.  If this returns false, you should
// close(fd) because the connection failed.
bool FinalizeAsyncConnect(int fd)
{
#if defined(__FreeBSD__) || defined(BSD)
   // Special case for FreeBSD7, for which send() doesn't do the trick
   struct sockaddr_in junk;
   socklen_t length = sizeof(junk);
   memset(&junk, 0, sizeof(junk));
   return (getpeername(fd, (struct sockaddr *)&junk, &length) == 0);
#else
   // For most platforms, the code below is all we need
   char junk;
   return (send(fd, &junk, 0, 0L) == 0);
#endif
}

close
Winsock'ta şöyle yaparız.
::closesocket(sockfd);
Linux'ta şöyle yaparız.
::close(sockfd);
listen metodu
İmzası şöyle.
int listen (int socket, int backlog);
backlog için açıklama şöyle
There is a thing called "backlog" which is a queue in which incoming connections temporarily stay until you have had a chance to accept them. It exists precisely so as to allow you to spend some time doing stuff between successive calls to accept.

The length of the backlog is a parameter to the listen() method. If the value that you specify is too small, and a large number of simultaneous requests to connect are received, you risk losing some connections due to appearing busy.

Eğer back log değeri 1 ise, 1 tane handshake yapılır ancak ikincisi reddedilir. Açıklaması şöyle.
So if your backlog is 1 and your server thread isn't waiting on accept, than means that an incoming client connection will at least establish the TCP handshake. If you attempt to have two pending connections when the backlog queue is 1, the second client connection will likely timeout or be refused if the server code isn't actively invoking accept to promote those connections out of the backlog and into sockets.
POSIX stadandardına göre backlog parameretresi tavsiye niteliğinde. Açıklaması şöyle.
The backlog argument provides a hint to the implementation which the implementation shall use to limit the number of outstanding connections in the socket's listen queue.
Farklı işletim sistemleri farlı davranabilir. Linux için açıklama şöyle
The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection requests. The maximum length of the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog. When syncookies are enabled there is no logical maximum length and this setting is ignored. See tcp(7) for more information.
Örnek
Şöyle yaparız. Hata varsa -1 döner.
listen(sockfd , 3);
Örnek
Windows'ta şöyle yaparız.
listen(sockfd, SOMAXCONN)
Örnek - Hata kontrolü
Çağrı başarılı ise 0 döner, hata varsa 0'dan farklı bir değer döner. Şöyle yaparız.
int listen_success = listen(...);
if (listen_success != 0)
{
  printf("Error %d ", WSAGetLastError());
}
Şöyle yaparız.
if (listen(sockfd, SOMAXCONN) == SOCKET_ERROR) 
{
  ...
}
read metodu
Örnek
Şöyle yaparız.
int readString (int fd, char *buf, int bufsize)
{
  int m = 0;                    /* total number of bytes received */
  int n = 0;                    /* number of bytes received in a single read */

  while (m < bufsize - 1)
  {
    n = read(fd, buf + m, bufsize - m - 1);
    if (n == -1)                /* socket read error */
    {
      perror("Error reading socket");
      break;
    }
    if (n == 0)                 /* socket is closed */
      break;
    m += n;
  }

  if (m >= bufsize)
    perror("Error buffer overflow");

  buf[m] = '\0';
  return m;
}
recv metodu
Örnek
Şöyle yaparız.
char message [1024];
int messageLen = recv(sockfd, message, sizeof(message) +1, 0);
Eğer 0 yerine MSG_WAITALL bayrağını verirsek belirtilen byte sayısını okuyuncaya kadar çağrı bloke olur.
Örnek
Şöyle yaparız.
int read_size;
char buf[2000];

//Receive a message from client
while( (read_size = recv(sockfd, buf, sizeof(buf) , 0)) > 0 )
{
  ...  
}
send metodu
İmzası şöyle.
ssize_t send(int sockfd, const void *buffer, size_t length, int flags);
Şöyle yaparız.
char* message = "hel";
send(sockfd, &message, sizeof(message), 0);  
setsockopt 
TCP Socket İçin setsockopt metodu yazısına taşıdım.

setsockopt - reuse
Hata varsa -1 döner. Metoda geçerken (char*) olarak geçilebilir. Şöyle yaparız.
int reuse = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
                         (char *)&reuse, sizeof(int))) {
Metoda geçerken (void*) olarak geçilebilir. Arada ne fark var bilmiyorum. Şöyle yaparız.
int optVal = 1;
const socklen_t optLen = sizeof(optVal);

int rtn = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (void*) &optVal,
                optLen);
if (rtn < 0)
{
  perror("Error");
}
setsockopt - send timeout
Şöyle yaparız.
struct timeval timeout;
//Time outs on socket operations
timeout.tv_sec = 10;
timeout.tv_usec = 0;

//Set time-out parameters
if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout,
                    sizeof(timeout)) == -1) {
...error...
} 
setsockopt - sigpipe
Yazarken SIGPIPE sinyali atar. Eğer SIGPIPE atmayı kapatırsak EPIPE değerine bakmak gerekir. Şöyle yaparız.
int on = 1;
setsockopt(socketfd, SOL_SOCKET, SO_NOSIGPIPE, &on, sizeof(int));
shutdown metodu
İmzası şöyle.
int shutdown(int s, int how); // s is socket descriptor
Açıklaması şöyle.
int shutdown(int socket, int how);
...
The shutdown() function shall cause all or part of a full-duplex connection on the socket associated with the file descriptor socket to be shut down.
The shutdown() function takes the following arguments:
  • socket Specifies the file descriptor of the socket.
  • how Specifies the type of shutdown. The values are as follows:
    • SHUT_RD Disables further receive operations.
    • SHUT_WR Disables further send operations.
    • SHUT_RDWR Disables further send and receive operations.
...
Örnek
Şöyle yaparız.
shutdown(socketfd, SHUT_WR);
Örnek
Windows'ta SHUT_RDWR sabiti yerine SD_BOTH kullanılır. Şöyle yaparız.
shutdown(socketfd, SD_BOTH);
write metodu
Şöyle yaparız.
int writeBuffer (int fd, char *buf, int len)
{
  int m = 0;
  int n = 0;

  while (m < len)
  {
    n = write(fd, buf + m, len - m);
    if (n == -1) {
      perror("Error socket send");
      break;
    }
    if (n == 0)                 /* socket is closed */
      break;
    m += n;
  }
  return m;
}


Hiç yorum yok:

Yorum Gönder