Skip to content

Latest commit

 

History

History
 
 

sockets-tcp

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

Сокеты с установкой соединения

Сокет

Сокет - это файловый дескриптор, открытый как для чтения, так и для записи. Предназначен для взаимодействия:

  • разных процессов, работающих на одном компьютере (хосте);
  • разных процессов, работающих на разных хостах.

Создается сокет с помощью системного вызова socket:

#include <sys/types.h>
#include <sys/socket.h>

int socket(
  int domain,    // тип пространства имён
  int type,      // тип взаимодействия через сокет
  int protocol   // номер протокола или 0 для авто-выбора
)

Механизм сокетов появился ещё в 80-е годы XX века, когда не было единого стандарта для сетевого взаимодействия, и сокеты являлись абстракцией поверх любого механизма сетевого взаимодействия, поддерживая огромное количество разных протоколов.

В современных системах используемыми можно считать несколько механизмов, определяющих пространство имен сокетов; все остальное - это legacy, которое мы дальше рассматривать не будем.

  • AF_UNIX (man 7 unix) - пространство имен локальных UNIX-сокетов, которые позволяют взаимодействовать разным процессам в пределах одного компьютера, используя в качестве адреса уникальное имя (длиной не более 107 байт) специального файла.
  • AF_INET (man 7 ip) - пространство кортежей, состоящих из 32-битных IPv4 адресов и 16-битных номеров портов. IP-адрес определяет хост, на котором запущен процесс для взаимодействия, а номер порта связан с конкретным процессом на хосте.
  • AF_INET6 (man 7 ipv6) - аналогично AF_INET, но используется 128-разрядная адресация хостов IPv6; пока этот стандарт поддерживается не всеми хостерами и провайдерами сети Интернет.
  • AF_PACKET (man 7 packet) - взаимодействие на низком уровне.

Через сокеты обычно происходит взаимодействие одним из двух способов (указывается в качестве второго параметра type):

  • SOCK_STREAM - взаимодействие с помощью системных вызовов read и write как с обычным файловым дескриптором. В случае взаимодействия по сети, здесь подразумевается использование протокола TCP.
  • SOCK_DGRAM - взаимодейтсвие без предвариательной установки взаимодействия для отправки коротких сообщений. В случае взаимодействия по сети, здесь подразумевается использование протокола UDP.

Пара сокетов

Иногда сокеты удобно использовать в качестве механизма взаимодействия между разными потоками или родственными процессами: в отличии от каналов, они являются двусторонними, и кроме того, поддерживают обработку события "закрытие соединения". Пара сокетов создается с помощью системного вызова socketpair:

int socketpair(
  int domain,    // В Linux поддерживатся только AF_UNIX
  int type,      // SOCK_STREAM или SOCK_DGRAM
  int protocol,  // Только значение 0 в Linux
  int sv[2]      // По аналогии с pipe, массив из двух int
)

В отличии от неименованных каналов, которые создаются системным вызовом pipe, для пары сокетов не имеет значения, какой элемент массива sv использовать для чтения, а какой - для записи, - они являются равноправными.

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

Сокеты могут участвовать во взаимодействии в одной из двух ролей. Процесс может быть сервером, то есть объявить некоторый адрес (имя файла, или кортеж из IP-адреса и номера порта) для приема входящих соединений, либо выступать в роли клиента, то есть подключиться к какому-то серверу.

Сразу после создания сокета, он ещё не готов к взамиодействию с помощью системных вызовов read и write. Установка взаимодействия с сервером осуществляется с помощью системного вызова connect. После успешного выполнения этого системного вызова - взаимодействие становится возможным до выполнения системного вызова shutdown.

int connect(
  int sockfd,                  // файловый дескриптор сокета

  const struct sockaddr *addr, // указатель на *абстрактную*
                               // структуру, описывающую
                               // адрес подключения

  socklen_t addrlen            // размер реальной структуры,
                               // которая передается в
                               // качестве второго параметра
)

Поскольку язык Си не является объектно-ориентированным, то нужно в качестве адреса передавать:

  1. Структуру, первое поле которой содержит целое число со значением, совпадающим с domain соответствующего сокета
  2. Размер этой структуры.

Конкретными стурктурами, которые "наследуются" от абстрактной структуры sockaddr могут быть:

  1. Для адресного пространства UNIX - стрктура sockaddr_un
#include <sys/socket.h>
#include <sys/un.h>

struct sockaddr_un {
  sa_family_t   sun_family;    // нужно записать AF_UNIX
  char          sun_path[108]; // путь к файлу сокета
};
  1. Для адресации в IPv4 - структура sockaddr_in:
#include <sys/socket.h>
#include <netinet/in.h>

struct sockaddr_in {
  sa_family_t    sin_family; // нужно записать AF_INET
  in_port_t      sin_port;   // uint16_t номер порта
  struct in_addr sin_addr;   // структура из одного поля:
                             // - in_addr_t s_addr;
                             //   где in_addr_t - это uint32_t
};
  1. Для адресации в IPv6 - структура sockaddr_in6:
#include <sys/socket.h>
#include <netinet/in.h>

struct sockaddr_in6 {
  sa_family_t    sin6_family; // нужно записать AF_INET6
  in_port_t      sin6_port;   // uint16_t номер порта
  uint32_t       sin6_flowinfo; // дополнительное поле IPv6
  struct in6_addr sin6_addr;  // структура из одного поля,
                              // объявленного как union {
                              //     uint8_t  [16];
                              //     uint16_t [8];
                              //     uint32_t [4];
                              // };
                              // т.е. размер in6_addr - 128 бит
  uint32_t       sin6_scope_id; // дополнительное поле IPv6
};

Адреса в сети IPv4

Адрес хоста в сети IPv4 - это 32-разрядное беззнаковое целое число в сетевом порядке байт, то есть Big-Endian. Для номеров портов - аналогично.

Конвертация порядка байт из сетевого в системный и наоборот осуществляется с помощью одной из функций, объявленных в <arpa/inet.h>:

  • uint32_t htonl(uint32_t hostlong) - 32-битное из системного в сетевой порядок байт;
  • uint32_t ntohl(uint32_t netlong) - 32-битное из сетевого в системный порядок байт;
  • uint16_t htons(uint16_t hostshort) - 16-битное из системного в сетевой порядок байт;
  • uint16_t ntohs(uint16_t netshort) - 16-битное из сетевого в системный порядок байт.

IPv4 адреса обычно записывают в десятичной записи, отделяя каждый байт точкой, например: 192.168.1.1. Такая запись может быть конвертирована из текста в 32-битный адрес с помощью функций inet_aton или inet_addr.

Закрытие сетевого соединения

Системный вызов close предназначен для закрытия файлового дескриптора, и его нужно вызывать для того, чтобы освободить запись в таблице файловых дескрипторов. Это является необходимым, но не достаточным требованием при работе с TCP-сокетами.

Помимо закрытия файлового дескриптора, хорошим тоном считается уведомление противоположной стороны о том, что сетевое соединение закрывается

Это уведомление осуществляется с помощью системного вызова shutdown.

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

Для использования сокета в роли сервера, необходимо выполнить следующие действия:

  1. Связать сокет с некоторым адресом. Для этого используется системный вызов bind, параметры которого точно такие же, как для системного вызова connect. Если на компьютере более одного IP-адреса, то адрес 0.0.0.0 означает "все адреса". Часто при отладке и возникает проблема, что порт с определенным номером уже был занят на предыдущем запуске программы (и, например, не был корректно закрыт). Это решается принудительным повторным использованием адреса:
// В релизной сборке такого обычно быть не должно!
#ifdef DEBUG
int val = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));
setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &val, sizeof(val));
#endif
  1. Создать очередь, в которой будут находиться входящие, но ещё не принятые подключения. Это делается с помощью системного вызова listen, который принимает в качестве параметра максимальное количество ожидающих подключений. Для Linux это значение равно 128, определено в константе SOMAXCONN.

  2. Принимать по одному соединению с помощью системного вызова accept. Второй и третий параметры этого системного вызова могуть быть NULL, если нас не интересует адрес того, кто к нам подключился. Системный вызов accept блокирует выполнение до тех пор, пока не появится входящее подключение. После чего - возвращает файловый дескриптор нового сокета, который связан с конкретным клиентом, который к нам подключился.