Programowanie gniazd sieciowych za pomocą gniazd w systemie Linux (C/C++) ========================================================================= Oprogramowywanie gniazd sieciowych za pomocą ``C/C++`` jest zdecydowanie najdtudniejsza. Jeśli państwo chcecie napiać pracę domową w C, możecie spróbować napisać ją za pomocą `BOOST:Asio `_. Nawiązywanie połączenia w C/C++ (klient) ---------------------------------------- .. code-block:: c #include #include #include #include #include #include #include #include #include #include #include #include #include #define MAXRCVLEN 1000 #define PORTNUM 80 int main(int argc, char **argv) { char buffer[MAXRCVLEN + 1]; /* +1 so we can add null terminator */ int mysocket; struct sockaddr_in dest; struct hostent *hostinfo; hostinfo = gethostbyname ("google.pl"); if (hostinfo == 0) { std::cerr << "Couldn't find google"; exit (1); }else{ std::cout << "Found google" << std::endl; } mysocket = socket(AF_INET, SOCK_STREAM, 0); memset(&dest, 0, sizeof(dest)); /* zero the struct */ dest.sin_family = AF_INET; dest.sin_addr = *(struct in_addr *) hostinfo->h_addr; /* set destination IP number - localhost, 127.0.0.1*/ dest.sin_port = htons(PORTNUM); /* set destination port number */ connect(mysocket, (struct sockaddr *)&dest, sizeof(struct sockaddr)); std::string get = "GET / HTTP/1.0\n\n"; send(mysocket, get.c_str(), get.size(), 0); len = recv(mysocket, buffer, MAXRCVLEN, 0); /* We have to null terminate the received data ourselves */ buffer[len] = '\0'; std::cout << buffer << std::endl; std::cout << "We have reclieved " << len << " bytes"<< std::endl; close(mysocket); return 0; } Pobranie ID hosta ***************** Domyślnie by połączyć się z serwerem musimy znać jego adres IP, by pobrać adres IP znając nazwę danego komputera należy wywołać funkcję: ``gethostbyname``. .. code-block:: c struct hostent *hostinfo; hostinfo = gethostbyname ("google.pl"); Funkcja ta zwraca adres ``NULL`` (czyli ``0``), jeśli nie uda się wyznaczyć  adresu IP. Stworzenie socketa ****************** Do stworzenia socketa służy funkcja ``socket``. .. code-block:: c mysocket = socket(AF_INET, SOCK_STREAM, 0); Pierwszy argumend definuje którą warstwę sieci dany socket będzie wykożystywać, ``AF_INET`` oznacza wykorzystanie prokokołu IP. Drugi rgument oznacza którą warstwę transportu ``SOCK_STREAM`` oznacza wykorzystanie protokoły TCP oraz abstrakcji strumieni danych którą on udostępnia. Trzeci argument to pozwala wyspecufikować dodatkowo protokół. Więcej na `man socket `_. Nawiązanie połączenia ********************* Do nawiązywania połączenia służy funkcja ``connect``: .. code-block:: c connect(mysocket, (struct sockaddr *)&dest, sizeof(struct sockaddr)); Przyjmuje ona takie argumenty: * utworzone wcześniej gniazdo * adres do którego się łączymy * długość struktury z adresem .. note:: Funkcja ``socket`` pozwala stworzyć gniazdo wielu protokołów warstwy sieci (np. IP czy Ipv6), które mają różne rodzaje adresowania (oraz różne długości adresów). Stąd konieczność podania zarówno adresu, jak i określenia jego długości. Wysłanie komunikatu ******************* Do wysłania komunikatu służy funkcja ``send``, przyjmuje ona: * gniazdo * bufor z komunikatem (typ ``char*``) * długość komunikatu * opcjonalne flagi Odbiór komunikatu ***************** Do wysłania komunikatu służy funkcja ``recv``, przyjmuje ona: * gniazdo * bufor z do którego zostanie zapisany komunikat (typ ``char*``) * długość bufora * opcjonalne flagi Funkcja zwtaca ilość odczytanych bajtów (nie więcej niż długość bufora). Nawiązywanie połączenia w C/C++ (server) ---------------------------------------- Serwer wysyłający Hello World pierwszej osobie która się doń połączy. .. code-block:: c #include #include #include #include #include #include #include #include #include int open_server_socket(std::string host, int port){ struct sockaddr_in myaddr ,clientaddr; int sockid; sockid=socket(AF_INET,SOCK_STREAM,0); memset(&myaddr,'0',sizeof(myaddr)); myaddr.sin_family=AF_INET; myaddr.sin_port=htons(5555); myaddr.sin_addr.s_addr=inet_addr("127.0.0.1"); if(sockid==-1) { throw std::runtime_error("Couldnt socket socket"); } int len=sizeof(myaddr); if(bind(sockid,( struct sockaddr*)&myaddr,len)==-1) { throw std::runtime_error("Couldnt bind socket"); } if(listen(sockid,10)==-1) { throw std::runtime_error("Couldnt listen socket"); } return sockid; } void handle_connection(int client_socket){ std::string hello_world = "Hello world\n"; send(client_socket, hello_world.c_str(), hello_world.size(), 0); } int main() { int sockid = open_server_socket("localhost", 5555); int newsockid = accept(sockid,0,0); handle_connection(newsockid); } Obsługa błędów ************** W kliencie (żeby Państwa nie przeciążać) nie zawarłem obsługi błędów, tutaj jest ona już widoczna. metody ``bind``, ``listen``, ``socket`` zwracają ``-1`` jeśli nastąpi błąd. Tworzenie gniazda ***************** Tak jak kliencie. Binding gniazda *************** By oznaczyć gniazdo jako gniazdo serwerowe należy wywołać funkcję ``bind``, następnie oznaczamy gniazdo jako mające odbierać połączenia zapomocą ``listen``. Odbieranie połączenia ********************* Do odbierania połączenuia służy metoda ``accept``. Zwraca ona ``inta`` reprezentującego nowe gniazdo, dające połączenie klienta z serweremn. Na takim gnieździe można wykonywać juz ``send`` i ``recv``. Serwer wykonujący echo ---------------------- Względem poprzedniego przykładu zmienia się funkcja ``main``, oraz ``handle_connection``: .. code-block:: c int main() { int sockid = open_server_socket("localhost", 5555); while(true){ int newsockid = accept(sockid,0,0); handle_connection(newsockid); } } teraz poszczególne połączenia wykonywane są w pętli. W funkcji ``handle_connection`` dodajemy obsługę kończenia połączenia, oraz odpisywania na wiadomości: .. code-block:: c std::string read_message(int client_socket){ char buffer = 0; std::string result; while(buffer!='\n'){ int recv_result = recv(client_socket, &buffer, 1, 0); if (recv_result == 0){ throw std::runtime_error("Client closed socket earlier"); } if(recv_result == -1){ throw std::runtime_error("Eroor reading from socket"); } result+=buffer; } return result; } void handle_connection(int client_socket){ std::string hello_world = "Wpisz coś\n"; send(client_socket, hello_world.c_str(), hello_world.size(), 0); while (true){ std::string message = read_message(client_socket); if (message.find("END") != std::string::npos){ std::string msg = "Kończymy!\n"; send(client_socket, msg.c_str(), msg.size(), 0); close(client_socket); return; } send(client_socket, message.c_str(), message.size(), 0); } } Wielowątkowy serwer ------------------- W porównaniu z poprzednim przykładem zmienia nam się tylko funkcja ``main``. .. code-block:: c int main() { int sockid = open_server_socket("localhost", 5555); while(true){ int newsockid = accept(sockid,0,0); int pid = fork(); if (pid == -1){ throw std::runtime_error("Forking error"); } if (pid == 0){ handle_connection(newsockid); return 0; } } } Teraz główna pętla serwera wykorzystuje funkcje ``fork`` funkcja fork działa w sposób następujący: * Podczas jej wykonywania cały proces który ją wywołał jest kopiowany (pojawiają się dwa procesy) * Procesy te różnią się tylko jedną rzeczą: wynikiem funkcji fork, jeśli funkcja ta zwróciła ``0`` jesteśmy w procesie *dziecku*, jeśli coś więskzego od zera w procesie matce, a jeśli ``-1`` nastąpił błąd jej wykonania (i nie ma procesu dziecka). Logika działania funkcji main jest następująca: * Wykonujemy funkcję ``fork()`` * Jeśli zwróciła zero, jesteśmy w procesie dziecku, więc obsługujemy połączenie **a następnie kończymy proces dziecko!**. * Jeśli zwróciła coś innego to znaczy że jesteśmy w procesie matce, który to proces powinien czekać na następne połączenie. * Jeśli wywołanie fork się nie powiodło serwer umiera.