Unix2017b/Программирование сетевых приложений

Материал из iRunner Wiki

Литература

Модель OSI применительно к UNIX

Семь классических уровней можно упростить до четырёх.

  • Application Layer (telnet, ftp, etc.)
  • Host-to-Host Transport Layer (TCP, UDP)
  • Internet Layer (IP and routing)
  • Network Access Layer (Ethernet, wi-fi, or whatever)

Действует принцип инкапсуляции: пакет следующего уровня оборачивает пакет предыдущего уровня хедером и (реже) футером.

Понятие сокета

Сокет (англ. socket — разъём) — название программного интерфейса для обеспечения обмена данными между процессами.

Интерфейс сокетов впервые появился в BSD Unix и называется «сокеты Беркли». Программный интерфейс сокетов описан в стандарте POSIX.1 и в той или иной мере поддерживается всеми современными операционными системами.

Дескрипторы

Поскольку в UNIX всё есть файл, то и для сетевого взаимодействия тоже используются файловые дескрипторы. Функция socket() возвращает такой дескриптор. Только вместо read() и write() для работы с ним применяют send() и recv()...

Типы сокетов

Мы будем рассматривать два.

  1. Интернет-сокеты.
  2. Сокеты домена UNIX.

Интернет-сокеты

Название «интернет» здесь надо понимать как «межсетевой», здесь не подразумевается глобальная сеть.

Два типа интернет-сокетов:

  • потоковый сокет (SOCK_STREAM);
  • датаграммный сокет (SOCK_DGRAM).

Потоковые сокеты обеспечивают надёжную двустороннюю передачу. Данные приходят без ошибок в том же порядке, в каком были отправлены. Используется протокол Transmission Control Protocol (TCP).

Датаграммные сокеты не основаны на установлении соединения. Отдельные пакеты могут теряться. Пакеты могут приходить не в порядке отправки. Если пакет пришёл, то данные в нём будут без ошибок. Используется протокол User Datagram Protocol (UDP). Протокол обеспечивает большую скорость и малые задержки.

Основные понятия

Протоколы IPv4 и IPv6

Длина адреса IPv4 — 32 бита, этого сегодня катастрофически не хватает ([math]\approx 4.3 \times 10^9[/math]). Длина адреса IPv6 — 128 бит. Это огромный массив адресов ([math]\approx 3.4 \times 10^{38}[/math]).

Для IPv6 используется шестнадцатеричная нотация:

2001:0db8:c9d2:aee5:73e3:934a:a5ae:9551

Нули могут быть опущены:

2001:0db8:c9d2:0012:0000:0000:0000:0051
2001:db8:c9d2:12::51

2001:0db8:ab00:0000:0000:0000:0000:0000
2001:db8:ab00::

0000:0000:0000:0000:0000:0000:0000:0001
::1

Адрес ::1 — это аналог 127.0.0.1 (loopback).

Номера портов

Порт — 16-битное число.

Многие сетевые сервисы имеют общеизвестные номера портов (например HTTP — порт 80, HTTPS — порт 443).

Для использования портов с номерами до 1024 нужны права суперпользователя.

Порядок байт

«Сетевой» порядок байт — big endian. На x86 — little endian.

Например, двухбайтовое целое число 45 90310 = 0xB34F16.

Big Endian (BE) Little Endian (LE)
B3 4F
4F B3

Для преобразования используются C-функции семейства

htons() host to network short
htonl() host to network long
ntohs() network to host short
ntohl() network to host long

Структуры

struct addrinfo {
  int ai_flags;             // AI_PASSIVE, AI_CANONNAME, etc.
  int ai_family;            // AF_INET, AF_INET6, AF_UNSPEC
  int ai_socktype;          // SOCK_STREAM, SOCK_DGRAM
  int ai_protocol;          // use 0 for "any"
  size_t ai_addrlen;        // size of ai_addr in bytes
  struct sockaddr *ai_addr; // struct sockaddr_in or _in6
  char *ai_canonname;       // full canonical hostname
  struct addrinfo *ai_next; // linked list, next node
};

struct addrinfo используется для подготовки настроек сокета перед его использованием.

Обычно предзаполняют некоторые поля и вызывают getaddrinfo(). Она возвращает связный список.

Структура struct sockaddr содержит информацию об адресе для многих типов сокетов.

struct sockaddr {
  unsigned short sa_family; // address family, AF_xxx
  char sa_data[14];         // 14 bytes of protocol address
};

Поле sa_family может быть AF_INET (IPv4) или AF_INET6 (IPv6).

Паковать адрес в байты вручную неудобно. Обычно программист создаёт параллельную структуру, имеющую тот же размер и ту же укладку в памяти, а затем кастит указатели.

struct sockaddr_in {
  short int sin_family;        // Address family, AF_INET
  unsigned short int sin_port; // Port number
  struct in_addr sin_addr;     // Internet address
  unsigned char sin_zero[8];   // Same size as struct sockaddr
};

Отметим, что sin_zero нужно заполнить нулями. Порт должен задаваться как число с сетевым порядком байт!

IP-адрес хранится в структуре (по историческим причинам).

// (IPv4 only--see struct in6_addr for IPv6)
// Internet address (a structure for historical reasons)
struct in_addr {
  uint32_t s_addr; // that's a 32-bit int (4 bytes)
};

Для IPv6 есть похожая структура:

// (IPv6 only--see struct sockaddr_in and struct in_addr for IPv4)
struct sockaddr_in6 {
  u_int16_t sin6_family;     // address family, AF_INET6
  u_int16_t sin6_port;       // port number, Network Byte Order
  u_int32_t sin6_flowinfo;   // IPv6 flow information
  struct in6_addr sin6_addr; // IPv6 address
  u_int32_t sin6_scope_id;   // Scope ID
};
struct in6_addr {
  unsigned char s6_addr[16]; // IPv6 address
};

Кроме того, есть структура struct sockaddr_storage, достаточно большая для хранения данных обеих вышеуказанных:

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];
};

Функции

getaddrinfo

Раньше для DNS-запроса нужно было вызывать функцию gethostbyname, затем вручную заполнять struct sockaddr_in. К счастью, это делать уже не нужно. Даже вредно, если вы хотите уметь работать по IPv4 и IPv6. Следует использовать getaddrinfo.

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
 
int getaddrinfo(const char *node,    // e.g. "www.example.com" or IP
                const char *service, // e.g. "http" or port number
                const struct addrinfo *hints,
                struct addrinfo **res);

Результатом является связный список res.

int status;
struct addrinfo hints;
struct addrinfo *servinfo; // will point to the results
 
memset(&hints, 0, sizeof hints); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // don't care IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
 
if ((status = getaddrinfo(NULL, "3490", &hints, &servinfo)) != 0) {
  fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
  exit(1);
}
// servinfo now points to a linked list of 1 or more struct addrinfos
// ... do everything until you don't need servinfo anymore ....
freeaddrinfo(servinfo); // free the linked-list

socket

#include <sys/types.h>
#include <sys/socket.h>
 
int socket(int domain, int type, int protocol);

Параметры:

  • domain: PF_INET или PF_INET6
  • type: SOCK_STREAM or SOCK_DGRAM
  • protocol: можно выставить в 0

Обычно берут все эти значения из структуры.

s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

Функция возвращает дескриптор.

bind

Ассоциирует сокет с портом на локальной машине. На клиенте это обычно не нужно: система сама выберет любой свободный порт.

#include <sys/types.h>
#include <sys/socket.h>
 
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

connect

#include <sys/types.h>
#include <sys/socket.h>
 
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

Подключение к удалённому хосту. Функция используется на клиенте.

struct addrinfo hints, *res;
int sockfd;
// first, load up address structs with getaddrinfo():
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
getaddrinfo("www.example.com", "3490", &hints, &res);
// make a socket:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
// connect!
connect(sockfd, res->ai_addr, res->ai_addrlen);

listen и accept

#include <sys/types.h>
#include <sys/socket.h>
 
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

Ожидание и приём соединения от клиента. Используется на сервере. В результате получается новый сокет.

#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
 
#define MYPORT "3490" // the port users will be connecting to
#define BACKLOG 10 // how many pending connections queue will hold
 
int main(void) {
    struct sockaddr_storage their_addr;
    socklen_t addr_size;
    struct addrinfo hints, *res;
    int sockfd, new_fd;
 
    // !! don't forget your error checking for these calls !!
 
    // first, load up address structs with getaddrinfo():
    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE; // fill in my IP for me
 
    getaddrinfo(NULL, MYPORT, &hints, &res);
 
    // make a socket, bind it, and listen on it:
 
    sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    bind(sockfd, res->ai_addr, res->ai_addrlen);
    listen(sockfd, BACKLOG);
 
    // now accept an incoming connection:
    addr_size = sizeof their_addr;
    new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &addr_size);
 
    // ready to communicate on socket descriptor new_fd!

send и recv

close и shutdown

Создание клиента и сервера

Потоковый режим (TCP)

Датаграммный режим (UDP)

Сокеты домена UNIX

Сокет домена UNIX (англ. UNIX domain socket, UDS) или IPC-сокет (сокет межпроцессного взаимодействия) — конечная точка обмена данными, подобная интернет-сокету, но не использующая сетевого протокола для взаимодействия (обмена данными). Используется в операционных системах, поддерживающих стандарт POSIX, для межпроцессного взаимодействия.

Подобно TCP-сокетам, эти сокеты поддерживают надёжную потоковую передачу (макрос SOCK_STREAM). Также они могут работать в режимах передачи датаграмм: упорядоченной и надёжной передачи (SOCK_SEQPACKET) или неупорядоченной и ненадёжной (SOCK_DGRAM).

Доменные соединения UNIX являются по сути байтовыми потоками, сильно напоминая сетевые соединения, но при этом все данные остаются внутри одного компьютера (то есть обмен данными происходит локально). UDS используют файловую систему как адресное пространство имен, то есть они представляются процессами как специальные файлы в файловой системе. Это позволяет двум различным процессам открывать один и тот же сокет для взаимодействия между собой. Однако конкретное взаимодействие (обмен данными) не использует файловую систему, а только буферы памяти ядра.

Сравнение с именованным каналом (FIFO)

  • Канал передаёт информацию только в одну сторону. сокет обеспечивает двунаправленную передачу.
  • При работе с каналами используются те же функции, что для файлов (open, read). Для сокетов UNIX применяется тот же набор функций, что для сетевых сокетов (socket, bind, recv).
  • Для UNIX-сокета действует дополнительное исторически сложившееся ограничение на длину пути — 108 символов (включая завершающий ноль).
  • В Ubuntu по умолчанию при выводе списка файлов ls сокет имеет фиолетовый цвет, канал — тёмно-жёлтый цвет.

Использование

Для использования сокета в коде создаётся и заполняется структура

struct sockaddr_un {
    unsigned short sun_family; /* AF_UNIX */
    char sun_path[108];
}

Указатель на неё передаётся в bind и connect.

Более продвинутые технологии

Неблокирующий ввод-вывод

По умолчанию у сокетов включен блокирующий режим.

#include <unistd.h>
#include <fcntl.h>
 
sockfd = socket(PF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);

Если читать из неблокирующего сокета и там нет данных, то функция не заблокируется, а вернёт -1 и выставит код ошибки в EAGAIN или EWOULDBLOCK.

select — синхронное мультиплексирование

Функция немного странная, но полезная. Представьте, что вы сервер и вы хотите ожидать новые входящие соединения и при этом читать из соединений, которые уже есть. Если перевести сокеты в неблокирующий режим и циклом проверять, на каком из сокетов появились данные, это будет пустой тратой CPU.

select — переносимая, но относительно медленная технология мониторинга сокетов. Возможно, стоит посмотреть на библиотеку libevent и пр.

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
 
int select(int numfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

Функция мониторит «множества» файловых дескрипторов.

/*
** select.c -- a select() demo
*/
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
 
#define STDIN 0 // file descriptor for standard input
 
int main(void)
{
    struct timeval tv;
    fd_set readfds;
    tv.tv_sec = 2;
    tv.tv_usec = 500000;
    FD_ZERO(&readfds);
    FD_SET(STDIN, &readfds);
    // don't care about writefds and exceptfds:
    select(STDIN+1, &readfds, NULL, NULL, &tv);
    if (FD_ISSET(STDIN, &readfds)) {
        printf("A key was pressed!\n");
    } else {
        printf("Timed out.\n");
    }
    return 0;
}