Nikita Shirikov Ed blog

Sockets

В этой лекции мы попишем много кода: клиент и сервер. Поговорим немного про TCP. И Unix domain sockets.

Продолжаем играться с функцией getaddrinfo:

#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>

int main(int argc, char* argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s NODE SERVICE\n", argv[0]);
        return 1;
    }

    // perform address resolution
    struct addrinfo* res = NULL;
    int gai_err;
    struct addrinfo hints = {
        .ai_family = PF_UNSPEC,
        .ai_socktype = SOCK_STREAM,
        .ai_flags = 0,   // try AI_ALL to include IPv6 on non-v6-enabled systems
    };
    if ((gai_err = getaddrinfo(argv[1], argv[2], &hints, &res))) {
        fprintf(stderr, "gai error: %s\n", gai_strerror(gai_err));
        return 2;
    }

    // iterate over the resulting addresses
    for (struct addrinfo* ai = res; ai; ai = ai->ai_next) {
        struct protoent* proto = getprotobynumber(ai->ai_protocol);
        if (proto) {
            printf("ai_flags=%d, ai_family=%d, ai_socktype=%d, ai_protocol=%s\n",
                   ai->ai_flags,
                   ai->ai_family,
                   ai->ai_socktype,
                   proto->p_name);
        }

        char host[1024], port[10];
        if ((gai_err = getnameinfo(ai->ai_addr,
                                   ai->ai_addrlen,
                                   host,
                                   sizeof(host),
                                   port,
                                   sizeof(port),
                                   NI_NUMERICHOST | NI_NUMERICSERV))) 
// флаги, чтобы не преобразовывать обратно в символьную форму
                                    {
            fprintf(stderr, "getnameinfo error: %s\n", gai_strerror(gai_err));
            return 3;
        }

        printf("\taddress: %s, port: %s\n", host, port);
    }
    freeaddrinfo(res);
}

Функция getaddrinfo - очень умная, может на вход принимать, как ip цифрами, так и доменные имена, как порт, так и название сервиса.

Пример использования:

./gai 127.0.0.1 31337
./gai ya.ru http

Главное, что мы получили - это преобразовали символьные имена в бинарные.

Переходим к написанию своего клиента

Код нашего клиента:

// client.c
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

int create_connection(char* node, char* service) {
    struct addrinfo* res = NULL;
    int gai_err;
    struct addrinfo hint = {
        .ai_family = AF_UNSPEC,       // можно и AF_INET, и AF_INET6
        .ai_socktype = SOCK_STREAM,   // но мы хотим поток (соединение)
    };
    if ((gai_err = getaddrinfo(node, service, &hint, &res))) {
        fprintf(stderr, "gai error: %s\n", gai_strerror(gai_err));
        return -1;
    }
    int sock = -1;
    for (struct addrinfo* ai = res; ai; ai = ai->ai_next) {
        sock = socket(ai->ai_family, ai->ai_socktype, 0);
        if (sock < 0) {
            perror("socket");
            continue;
        }
        if (connect(sock, ai->ai_addr, ai->ai_addrlen) < 0) {
            perror("connect");
            close(sock);
            sock = -1;
            continue;
        }
        break;
    }
    freeaddrinfo(res);
    return sock;
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s NODE SERVICE\n", argv[0]);
        return 1;
    }
    int sock = create_connection(argv[1], argv[2]);
    if (sock < 0) {
        return 1;
    }
    char buf[1024] = {0};
    if (read(sock, &buf, sizeof(buf) - 1) > 0) {
        printf("received message: %s\n", buf);
    }
    close(sock);
}

С помощью network можем создать сервер из консоли.

strace -e network nc -l 31337

strace - чтобы отслеживать какие вызовы происходят внутри нашей программы.

теперь использование client:

./client 127.0.0.1 31337

Теперь поробуем написать соотвествующий сервер

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

int create_listener(char* service) {
    struct addrinfo* res = NULL;
    int gai_err;
    struct addrinfo hint = {
        .ai_family = AF_INET6,
        .ai_socktype = SOCK_STREAM,
        .ai_flags = AI_PASSIVE,   // get addresses suitable for a server to bind to
    };
    if ((gai_err = getaddrinfo(NULL, service, &hint, &res))) {
        fprintf(stderr, "gai error: %s\n", gai_strerror(gai_err));
        return -1;
    }
    int sock = -1;
    for (struct addrinfo* ai = res; ai; ai = ai->ai_next) {
        // create socket of the suitable family (AF_INET or AF_INET6)
        sock = socket(ai->ai_family, ai->ai_socktype, 0);
        if (sock < 0) {
            perror("socket");
            continue;
        }

        // make port immediately reusable after we release it
        int one = 1;
        if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one))) {
            perror("setsockopt");
            goto err;
        }

        // try to bind and listen
        if (bind(sock, ai->ai_addr, ai->ai_addrlen) < 0) {
            perror("bind");
            goto err;
        }
        if (listen(sock, SOMAXCONN) < 0) {
            perror("listen");
            goto err;
        }
        break;
err:
        close(sock);
        sock = -1;
    }
    freeaddrinfo(res);
    return sock;
}

int main(int argc, char* argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s SERVICE\n", argv[0]);
        return 1;
    }
    int sock = create_listener(argv[1]);
    if (sock < 0) {
        return 1;
    }

    struct sockaddr_in6 address;
    socklen_t addrlen = sizeof(address);
    int connection = accept(sock, (struct sockaddr*)&address, &addrlen);
    char buf[512] = {0};
    inet_ntop(address.sin6_family, &address.sin6_addr, buf, sizeof(buf));
    printf("accepted connection from %s\n", buf);

    char* msg = "hello world\n";
    write(connection, msg, strlen(msg));
    close(sock);
}

Серверу нужно создать специальный слушающий сокет.

  1. Для начала серверу надо найти адресс, на котором он будет слушать входящие соединения.

Можем посмотреть сетевые интерфейсы на машине с помощью ifconfig:

ОС для нас заботится, мы можем создавать только сокеты для ipv6. Запросы с обычного ip4 будут автоматически передоваться.

  1. Потом биндимся на нужный сокет (syscall bind)
  2. И переключаем сокет в слущающий режим (syscall listen)

Настройка сокета готова. Мы готовы принимать соединения.

Создать соединение мы можем с помощью система вызова accept.

Функция возвращает fd. Можем писать при помощи обычного write.

Usage:

./server PORT

Если убить сервер, у которого есть незакрытые сокеты, то какое-то время на эти порты нельзя привязываться. Чтобы избежать этого стандартного поведения, надо переконфигурировать сокет используя setsockport. Очень замороченная функция. См. больше в man.

fork’ающий сервер

Чаще всего мы хотим параллельно принимать несколько различных соединений. Самая примитивная и не самая эффективная идея - это использовать fork:

//fork_server.c
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int create_listener(char* service) {
    struct addrinfo* res = NULL;
    int gai_err;
    struct addrinfo hint = {
        .ai_family = AF_INET6,
        .ai_socktype = SOCK_STREAM,
        .ai_flags = AI_PASSIVE,
    };
    if ((gai_err = getaddrinfo(NULL, service, &hint, &res))) {
        fprintf(stderr, "gai error: %s\n", gai_strerror(gai_err));
        return -1;
    }
    int sock = -1;
    for (struct addrinfo* ai = res; ai; ai = ai->ai_next) {
        sock = socket(ai->ai_family, ai->ai_socktype, 0);
        if (sock < 0) {
            perror("socket");
            continue;
        }
        int one = 1;
        if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one))) {
            perror("setsockopt");
            goto err;
        }
        if (bind(sock, ai->ai_addr, ai->ai_addrlen) < 0) {
            perror("bind");
            goto err;
        }
        if (listen(sock, 1) < 0) {
            perror("listen");
            goto err;
        }
        break;
err:
        close(sock);
        sock = -1;
    }
    freeaddrinfo(res);
    return sock;
}

int main(int argc, char* argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s SERVICE\n", argv[0]);
        return 1;
    }
    int sock = create_listener(argv[1]);
    if (sock < 0) {
        return 1;
    }
    while (1) {
        int connection = accept(sock, NULL, NULL);
        if (fork() == 0) {
            // worker
            close(sock);   // we won't be accepting anything here
            // здесь можно проводить какую-нибудь работу
            char* msg = "hello world";
            write(connection, msg, strlen(msg));
            return 0;
        } else {
            // собираем зомбиков
            close(connection);
            while (waitpid(-1, NULL, WNOHANG) > 0)
                ;
        }
    }
    close(sock);
}

Пора поговорить про TCP и как он не теряет данные

Совместный просмотр absolute cinema:

Анимашка крайне проста, клиент и сервер перекидываются пакетами и подтверждениями, соотвественно.

И теперь немножко про UDP

Можем работать при помощи вызовов:

// Датаграммные сокеты

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

И теперь также немножкооо про Unix domain sockets

Совершенно другое семейство адресс, чтобы программы на одном компьютере могли взаимодействовать между собой. Их можно отоброжать в файловую систему.

Пример сервера:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>

int create_listener(const char* path) {
    struct sockaddr_un address = {.sun_family = AF_UNIX};
    if (strlen(path) >= sizeof(address.sun_path)) {
        fprintf(stderr, "pathname too long\n");
        exit(1);
    }
    strcpy(address.sun_path, path);

    int sock = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("socket");
        return sock;
    }

    // try to bind and listen
    if (bind(sock, (struct sockaddr*)&address, sizeof(address)) < 0) {
        perror("bind");
        goto err;
    }
    if (listen(sock, SOMAXCONN) < 0) {
        perror("listen");
        goto err;
    }
    return sock;
err:
    close(sock);
    sock = -1;
    return sock;
}

int main(int argc, char* argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s PATH\n", argv[0]);
        return 1;
    }
    int sock = create_listener(argv[1]);
    if (sock < 0) {
        return 1;
    }

    struct sockaddr_un address;
    socklen_t addrlen = sizeof(address);
    int connection = accept(sock, (struct sockaddr*)&address, &addrlen);
    printf("addrlen is %d\n", addrlen);
    printf("client address is %s\n", address.sun_path + 1);

    char* msg = "hello world\n";
    write(connection, msg, strlen(msg));

    close(connection);
    close(sock);
    unlink(argv[1]);
}

Еще есть вызов socketpair - аналог pipe.

#Os