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)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string>

#include <iostream>

#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.

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.

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:

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.

#include<signal.h>
#include<stdio.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<string.h>
#include<sys/types.h>
#include<stdlib.h>

#include <iostream>
#include <stdexcept>


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:

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:

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.

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.