add pages
This commit is contained in:
parent
694a426dc6
commit
dda2cd1009
516
content/posts/2023/c/sockets.md
Normal file
516
content/posts/2023/c/sockets.md
Normal file
@ -0,0 +1,516 @@
|
|||||||
|
---
|
||||||
|
title: "↔️ Программирование сокетов в Linux"
|
||||||
|
date: 2023-06-18T17:52:44+03:00
|
||||||
|
draft: true
|
||||||
|
tags: [c, development, linux]
|
||||||
|
---
|
||||||
|
|
||||||
|
> Автор: Александр Шаргин
|
||||||
|
> Опубликовано: 16.05.2001
|
||||||
|
> Исправлено: 04.02.2006
|
||||||
|
> Версия текста: 1.1
|
||||||
|
> Оригинальная страница: https://rsdn.org/article/unix/sockets.xml
|
||||||
|
|
||||||
|
## Введение
|
||||||
|
|
||||||
|
**Socket API** был впервые реализован в операционной системе Berkley UNIX.
|
||||||
|
Сейчас этот программный интерфейс доступен практически в любой модификации Unix,
|
||||||
|
в том числе в Linux. Хотя все реализации чем-то отличаются друг от друга,
|
||||||
|
основной набор функций в них совпадает.
|
||||||
|
Изначально сокеты использовались в программах на C/C++,
|
||||||
|
но в настоящее время средства для работы с ними предоставляют многие языки (Perl, Java и др.).
|
||||||
|
|
||||||
|
Сокеты предоставляют весьма мощный и гибкий механизм межпроцессного взаимодействия (IPC).
|
||||||
|
Они могут использоваться для организации взаимодействия программ на одном компьютере,
|
||||||
|
по локальной сети или через Internet, что позволяет вам создавать распределённые приложения
|
||||||
|
различной сложности. Кроме того, с их помощью можно организовать взаимодействие с программами,
|
||||||
|
работающими под управлением других операционных систем.
|
||||||
|
Например, под Windows существует интерфейс Window Sockets,
|
||||||
|
спроектированный на основе socket API. Ниже мы увидим, насколько легко можно
|
||||||
|
адаптировать существующую Unix-программу для работы под Windows.
|
||||||
|
|
||||||
|
Сокеты поддерживают многие стандартные сетевые протоколы
|
||||||
|
(конкретный их список зависит от реализации)
|
||||||
|
и предоставляют унифицированный интерфейс для работы с ними.
|
||||||
|
Наиболее часто сокеты используются для работы в IP-сетях.
|
||||||
|
В этом случае их можно использовать для взаимодействия приложений
|
||||||
|
не только по специально разработанным, но и по стандартным протоколам -
|
||||||
|
HTTP, FTP, Telnet и т. д. Например, вы можете написать собственный Web-браузер или Web-сервер,
|
||||||
|
способный обслуживать одновременно множество клиентов.
|
||||||
|
|
||||||
|
Как видим, сокеты - весьма мощное и удобное средство для сетевого программирования.
|
||||||
|
В этой статье я покажу, как ими пользоваться. Начав с понятия сокета
|
||||||
|
и самых основных функций для работы с ним, мы постепенно перейдём
|
||||||
|
к обсуждению более сложных тем. В частности, мы рассмотрим использование низкоуровневых сокетов,
|
||||||
|
различные способы организации параллельного обслуживания клиентов,
|
||||||
|
использование стандартных протоколов Internet и взаимодействие с программами,
|
||||||
|
работающими под управлением операционной системы Microsoft Windows.
|
||||||
|
|
||||||
|
**ПРИМЕЧАНИЕ**
|
||||||
|
|
||||||
|
Большая часть материала, изложенного в статье, применимо ко всему семейству ОС Unix.
|
||||||
|
Тем не менее, все приводимые далее факты и демонстрационные программы проверялись
|
||||||
|
только под Linux, поэтому название этой ОС и вынесено в заголовок статьи.
|
||||||
|
|
||||||
|
## Основы socket API
|
||||||
|
|
||||||
|
### Понятие сокета
|
||||||
|
Сокет (socket) - это конечная точка сетевых коммуникаций.
|
||||||
|
Он является чем-то вроде "портала", через которое можно отправлять байты во внешний мир.
|
||||||
|
Приложение просто пишет данные в сокет; их дальнейшая буферизация,
|
||||||
|
отправка и транспортировка осуществляется используемым стеком протоколов и сетевой аппаратурой.
|
||||||
|
Чтение данных из сокета происходит аналогичным образом.
|
||||||
|
|
||||||
|
В программе сокет идентифицируется дескриптором - это просто переменная типа `int`.
|
||||||
|
Программа получает дескриптор от операционной системы при создании сокета,
|
||||||
|
а затем передаёт его сервисам socket API для указания сокета,
|
||||||
|
над которым необходимо выполнить то или иное действие.
|
||||||
|
|
||||||
|
### Атрибуты сокета
|
||||||
|
С каждым сокет связываются три атрибута: `домен`, `тип` и `протокол`.
|
||||||
|
Эти атрибуты задаются при создании сокета и остаются неизменными
|
||||||
|
на протяжении всего времени его существования.
|
||||||
|
Для создания сокета используется функция `socket`, имеющая следующий прототип.
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
|
||||||
|
int socket(int domain, int type, int protocol);
|
||||||
|
```
|
||||||
|
|
||||||
|
Домен определяет пространство адресов, в котором располагается сокет,
|
||||||
|
и множество протоколов, которые используются для передачи данных.
|
||||||
|
Чаще других используются домены Unix и Internet, задаваемые константами
|
||||||
|
`AF_UNIX` и `AF_INET` соответственно
|
||||||
|
(префикс AF означает "address family" - "семейство адресов").
|
||||||
|
При задании `AF_UNIX` для передачи данных используется файловая система ввода/вывода Unix.
|
||||||
|
В этом случае сокеты используются для межпроцессного взаимодействия
|
||||||
|
на одном компьютере и не годятся для работы по сети.
|
||||||
|
Константа `AF_INET` соответствует Internet-домену.
|
||||||
|
Сокеты, размещённые в этом домене, могут использоваться для работы в любой IP-сети.
|
||||||
|
Существуют и другие домены (`AF_IPX` для протоколов Novell,
|
||||||
|
`AF_INET6` для новой модификации протокола IP - IPv6 и т. д.),
|
||||||
|
но в этой статье мы не будем их рассматривать.
|
||||||
|
|
||||||
|
Тип сокета определяет способ передачи данных по сети. Чаще других применяются:
|
||||||
|
|
||||||
|
- `SOCK_STREAM`. Передача потока данных с предварительной установкой соединения.
|
||||||
|
Обеспечивается надёжный канал передачи данных, при котором фрагменты отправленного блока
|
||||||
|
не теряются, не переупорядочиваются и не дублируются.
|
||||||
|
Поскольку этот тип сокетов является самым распространённым,
|
||||||
|
до конца раздела мы будем говорить только о нём.
|
||||||
|
Остальным типам будут посвящены отдельные разделы.
|
||||||
|
- `SOCK_DGRAM`. Передача данных в виде отдельных сообщений (датаграмм).
|
||||||
|
Предварительная установка соединения не требуется. Обмен данными происходит быстрее,
|
||||||
|
но является ненадёжным: сообщения могут теряться в пути, дублироваться и переупорядочиваться.
|
||||||
|
Допускается передача сообщения нескольким получателям (multicasting)
|
||||||
|
и широковещательная передача (broadcasting).
|
||||||
|
- `SOCK_RAW`. Этот тип присваивается низкоуровневым (т. н. "сырым") сокетам.
|
||||||
|
Их отличие от обычных сокетов состоит в том, что с их помощью программа может
|
||||||
|
взять на себя формирование некоторых заголовков, добавляемых к сообщению.
|
||||||
|
|
||||||
|
Обратите внимание, что не все домены допускают задание произвольного типа сокета.
|
||||||
|
Например, совместно с доменом Unix используется только тип `SOCK_STREAM`.
|
||||||
|
С другой стороны, для Internet-домена можно задавать любой из перечисленных типов.
|
||||||
|
В этом случае для реализации `SOCK_STREAM` используется протокол TCP,
|
||||||
|
для реализации `SOCK_DGRAM` - протокол UDP,
|
||||||
|
а тип `SOCK_RAW` используется для низкоуровневой работы с протоколами IP, ICMP и т. д.
|
||||||
|
|
||||||
|
Наконец, последний атрибут определяет протокол, используемый для передачи данных.
|
||||||
|
Как мы только что видели, часто протокол однозначно определяется по домену и типу сокета.
|
||||||
|
В этом случае в качестве третьего параметра функции `socket` можно передать 0,
|
||||||
|
что соответствует протоколу по умолчанию. Тем не менее, иногда
|
||||||
|
(например, при работе с низкоуровневыми сокетами) требуется задать протокол явно.
|
||||||
|
Числовые идентификаторы протоколов зависят от выбранного домена;
|
||||||
|
их можно найти в документации.
|
||||||
|
|
||||||
|
### Адреса
|
||||||
|
|
||||||
|
Прежде чем передавать данные через сокет, его необходимо связать с адресом в выбранном домене
|
||||||
|
(эту процедуру называют именованием сокета). Иногда связывание осуществляется неявно
|
||||||
|
(внутри функций `connect` и `accept`), но выполнять его необходимо во всех случаях.
|
||||||
|
Вид адреса зависит от выбранного вами домена.
|
||||||
|
В Unix-домене это текстовая строка - имя файла, через который происходит обмен данными.
|
||||||
|
В Internet-домене адрес задаётся комбинацией IP-адреса и 16-битного номера порта.
|
||||||
|
IP-адрес определяет хост в сети, а порт - конкретный сокет на этом хосте.
|
||||||
|
Протоколы TCP и UDP используют различные пространства портов.
|
||||||
|
|
||||||
|
Для явного связывания сокета с некоторым адресом используется функция `bind`.
|
||||||
|
Её прототип имеет вид:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
|
||||||
|
int bind(int sockfd, struct sockaddr *addr, int addrlen);
|
||||||
|
```
|
||||||
|
|
||||||
|
В качестве первого параметра передаётся дескриптор сокета,
|
||||||
|
который мы хотим привязать к заданному адресу.
|
||||||
|
Второй параметр, `addr`, содержит указатель на структуру с адресом,
|
||||||
|
а третий - длину этой структуры. Посмотрим, что она собой представляет.
|
||||||
|
|
||||||
|
```c
|
||||||
|
struct sockaddr {
|
||||||
|
unsigned short sa_family; // Семейство адресов, AF_xxx
|
||||||
|
char sa_data[14]; // 14 байтов для хранения адреса
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Поле `sa_family` содержит идентификатор домена, тот же, что и первый параметр функции `socket`.
|
||||||
|
В зависимости от значения этого поля по-разному интерпретируется содержимое массива `sa_data`.
|
||||||
|
Разумеется, работать с этим массивом напрямую не очень удобно,
|
||||||
|
поэтому вы можете использовать вместо `sockaddr` одну из альтернативных структур вида
|
||||||
|
`sockaddr_XX` (`XX` - суффикс, обозначающий домен: `un` - Unix, `in` - Internet и т. д.).
|
||||||
|
При передаче в функцию `bind` указатель на эту структуру приводится к указателю на `sockaddr`.
|
||||||
|
Рассмотрим для примера структуру `sockaddr_in`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
struct sockaddr_in {
|
||||||
|
short int sin_family; // Семейство адресов
|
||||||
|
unsigned short int sin_port; // Номер порта
|
||||||
|
struct in_addr sin_addr; // IP-адрес
|
||||||
|
unsigned char sin_zero[8]; // "Дополнение" до размера структуры sockaddr
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Здесь поле `sin_family` соответствует полю `sa_family` в `sockaddr`,
|
||||||
|
в `sin_port` записывается номер порта, а в `sin_addr` - IP-адрес хоста.
|
||||||
|
Поле `sin_addr` само является структурой, которая имеет вид:
|
||||||
|
|
||||||
|
```c
|
||||||
|
struct in_addr {
|
||||||
|
unsigned long s_addr;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Зачем понадобилось заключать всего одно поле в структуру?
|
||||||
|
Дело в том, что раньше `in_addr` представляла собой объединение (union),
|
||||||
|
содержащее гораздо большее число полей. Сейчас, когда в ней осталось всего одно поле,
|
||||||
|
она продолжает использоваться для обратной совместимости.
|
||||||
|
|
||||||
|
И ещё одно важное замечание. Существует два порядка хранения байтов в слове и двойном слове.
|
||||||
|
Один из них называется порядком хоста (host byte order),
|
||||||
|
другой - сетевым порядком (network byte order) хранения байтов.
|
||||||
|
При указании IP-адреса и номера порта необходимо преобразовать число
|
||||||
|
из порядка хоста в сетевой. Для этого используются функции
|
||||||
|
`htons` (Host TO Network Short) и `htonl` (Host TO Network Long).
|
||||||
|
Обратное преобразование выполняют функции `ntohs` и `ntohl`.
|
||||||
|
|
||||||
|
**ПРИМЕЧАНИЕ**
|
||||||
|
|
||||||
|
На некоторых машинах (к PC это не относится) порядок хоста и сетевой порядок хранения байтов
|
||||||
|
совпадают. Тем не менее, функции преобразования лучше применять и там,
|
||||||
|
поскольку это улучшит переносимость программы.
|
||||||
|
Это никак не скажется на производительности, так как препроцессор сам уберёт
|
||||||
|
все "лишние" вызовы этих функций, оставив их только там,
|
||||||
|
где преобразование действительно необходимо.
|
||||||
|
|
||||||
|
### Установка соединения (сервер)
|
||||||
|
|
||||||
|
Установка соединения на стороне сервера состоит из четырёх этапов,
|
||||||
|
ни один из которых не может быть опущен. Сначала сокет создаётся и привязывается
|
||||||
|
к локальному адресу. Если компьютер имеет несколько сетевых интерфейсов
|
||||||
|
с различными IP-адресами, вы можете принимать соединения только с одного из них,
|
||||||
|
передав его адрес функции `bind`. Если же вы готовы соединяться с клиентами
|
||||||
|
через любой интерфейс, задайте в качестве адреса константу `INADDR_ANY`.
|
||||||
|
Что касается номера порта, вы можете задать конкретный номер
|
||||||
|
или 0 (в этом случае система сама выберет произвольный неиспользуемый
|
||||||
|
в данный момент номер порта).
|
||||||
|
|
||||||
|
На следующем шаге создаётся очередь запросов на соединение.
|
||||||
|
При этом сокет переводится в режим ожидания запросов со стороны клиентов.
|
||||||
|
Всё это выполняет функция `listen`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
int listen(int sockfd, int backlog);
|
||||||
|
```
|
||||||
|
|
||||||
|
Первый параметр - дескриптор сокета, а второй задаёт размер очереди запросов.
|
||||||
|
Каждый раз, когда очередной клиент пытается соединиться с сервером,
|
||||||
|
его запрос ставится в очередь, так как сервер может быть занят обработкой других запросов.
|
||||||
|
Если очередь заполнена, все последующие запросы будут игнорироваться.
|
||||||
|
Когда сервер готов обслужить очередной запрос, он использует функцию `accept`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <sys/socket.h>
|
||||||
|
|
||||||
|
int accept(int sockfd, void *addr, int *addrlen);
|
||||||
|
```
|
||||||
|
|
||||||
|
Функция `accept` создаёт для общения с клиентом новый сокет и возвращает его дескриптор.
|
||||||
|
Параметр `sockfd` задаёт слушающий сокет. После вызова он остаётся
|
||||||
|
в слушающем состоянии и может принимать другие соединения.
|
||||||
|
В структуру, на которую ссылается `addr`, записывается адрес сокета клиента,
|
||||||
|
который установил соединение с сервером. В переменную, адресуемую указателем `addrlen`,
|
||||||
|
изначально записывается размер структуры;
|
||||||
|
функция `accept` записывает туда длину, которая реально была использована.
|
||||||
|
Если вас не интересует адрес клиента, вы можете просто передать `NULL`
|
||||||
|
в качестве второго и третьего параметров.
|
||||||
|
|
||||||
|
Обратите внимание, что полученный от `accept` новый сокет связан с тем же самым адресом,
|
||||||
|
что и слушающий сокет. Сначала это может показаться странным.
|
||||||
|
Но дело в том, что адрес TCP-сокета не обязан быть уникальным в Internet-домене.
|
||||||
|
Уникальными должны быть только соединения, для идентификации которых используются
|
||||||
|
два адреса сокетов, между которыми происходит обмен данными.
|
||||||
|
|
||||||
|
### Установка соединения (клиент)
|
||||||
|
|
||||||
|
На стороне клиента для установления соединения используется функция `connect`,
|
||||||
|
которая имеет следующий прототип.
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
|
||||||
|
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
|
||||||
|
```
|
||||||
|
|
||||||
|
Здесь `sockfd` - сокет, который будет использоваться для обмена данными с сервером,
|
||||||
|
`serv_addr` содержит указатель на структуру с адресом сервера,
|
||||||
|
а `addrlen` - длину этой структуры. Обычно сокет не требуется предварительно привязывать
|
||||||
|
к локальному адресу, так как функция `connect` сделает это за вас,
|
||||||
|
подобрав подходящий свободный порт. Вы можете принудительно назначить
|
||||||
|
клиентскому сокету некоторый номер порта, используя `bind` перед вызовом `connect`.
|
||||||
|
Делать это следует в случае, когда сервер соединяется с только с клиентами,
|
||||||
|
использующими определённый порт (примерами таких серверов являются `rlogind` и `rshd`).
|
||||||
|
В остальных случаях проще и надёжнее предоставить системе выбрать порт за вас.
|
||||||
|
|
||||||
|
### Обмен данными
|
||||||
|
|
||||||
|
После того как соединение установлено, можно начинать обмен данными.
|
||||||
|
Для этого используются функции `send` и `recv`.
|
||||||
|
В Unix для работы с сокетами можно использовать также файловые функции `read` и `write`,
|
||||||
|
но они обладают меньшими возможностями, а кроме того не будут работать на других платформах
|
||||||
|
(например, под Windows), поэтому я не рекомендую ими пользоваться.
|
||||||
|
|
||||||
|
Функция `send` используется для отправки данных и имеет следующий прототип.
|
||||||
|
|
||||||
|
```c
|
||||||
|
int send(int sockfd, const void *msg, int len, int flags);
|
||||||
|
```
|
||||||
|
|
||||||
|
Здесь `sockfd` - это, как всегда, дескриптор сокета, через который мы отправляем данные,
|
||||||
|
`msg` - указатель на буфер с данными,
|
||||||
|
`len` - длина буфера в байтах,
|
||||||
|
а `flags` - набор битовых флагов, управляющих работой функции
|
||||||
|
(если флаги не используются, передайте функции 0).
|
||||||
|
Вот некоторые из них (полный список можно найти в документации):
|
||||||
|
|
||||||
|
- `MSG_OOB`. Предписывает отправить данные как срочные (out of band data, OOB).
|
||||||
|
Концепция срочных данных позволяет иметь два параллельных канала данных в одном соединении.
|
||||||
|
Иногда это бывает удобно. Например, Telnet использует срочные данные для передачи команд типа
|
||||||
|
Ctrl+C. В настоящее время использовать их не рекомендуется из-за проблем с совместимостью
|
||||||
|
(существует два разных стандарта их использования, описанные в RFC793 и RFC1122).
|
||||||
|
Безопаснее просто создать для срочных данных отдельное соединение.
|
||||||
|
- `MSG_DONTROUTE`. Запрещает маршрутизацию пакетов.
|
||||||
|
Нижележащие транспортные слои могут проигнорировать этот флаг.
|
||||||
|
|
||||||
|
Функция `send` возвращает число байтов, которое на самом деле было отправлено
|
||||||
|
(или -1 в случае ошибки). Это число может быть меньше указанного размера буфера.
|
||||||
|
Если вы хотите отправить весь буфер целиком,
|
||||||
|
вам придётся написать свою функцию и вызывать в ней `send`,
|
||||||
|
пока все данные не будут отправлены. Она может выглядеть примерно так.
|
||||||
|
|
||||||
|
```c
|
||||||
|
int sendall(int s, char *buf, int len, int flags)
|
||||||
|
{
|
||||||
|
int total = 0;
|
||||||
|
int n;
|
||||||
|
|
||||||
|
while(total < len)
|
||||||
|
{
|
||||||
|
n = send(s, buf+total, len-total, flags);
|
||||||
|
if(n == -1) { break; }
|
||||||
|
total += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (n==-1 ? -1 : total);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Использование `sendall` ничем не отличается от использования `send`,
|
||||||
|
но она отправляет весь буфер с данными целиком.
|
||||||
|
|
||||||
|
Для чтения данных из сокета используется функция `recv`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
int recv(int sockfd, void *buf, int len, int flags);
|
||||||
|
```
|
||||||
|
|
||||||
|
В целом её использование аналогично `send`.
|
||||||
|
Она точно так же принимает дескриптор сокета, указатель на буфер и набор флагов.
|
||||||
|
Флаг `MSG_OOB` используется для приёма срочных данных,
|
||||||
|
а `MSG_PEEK` позволяет "подсмотреть" данные, полученные от удалённого хоста,
|
||||||
|
не удаляя их из системного буфера
|
||||||
|
(это означает, что при следующем обращении к `recv` вы получите те же самые данные).
|
||||||
|
Полный список флагов можно найти в документации.
|
||||||
|
По аналогии с `send` функция `recv` возвращает количество прочитанных байтов,
|
||||||
|
которое может быть меньше размера буфера.
|
||||||
|
Вы без труда сможете написать собственную функцию `recvall`, заполняющую буфер целиком.
|
||||||
|
Существует ещё один особый случай, при котором `recv` возвращает 0.
|
||||||
|
Это означает, что соединение было разорвано.
|
||||||
|
|
||||||
|
### Закрытие сокета
|
||||||
|
|
||||||
|
Закончив обмен данными, закройте сокет с помощью функции `close`.
|
||||||
|
Это приведёт к разрыву соединения.
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
int close(int fd);
|
||||||
|
```
|
||||||
|
|
||||||
|
Вы также можете запретить передачу данных в каком-то одном направлении, используя `shutdown`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
int shutdown(int sockfd, int how);
|
||||||
|
```
|
||||||
|
|
||||||
|
Параметр `how` может принимать одно из следующих значений:
|
||||||
|
|
||||||
|
- 0 - запретить чтение из сокета.
|
||||||
|
- 1 - запретить запись в сокет.
|
||||||
|
- 2 - запретить и то и другое.
|
||||||
|
|
||||||
|
Хотя после вызова `shutdown` с параметром `how`, равным 2,
|
||||||
|
вы больше не сможете использовать сокет для обмена данными,
|
||||||
|
вам всё равно потребуется вызвать `close`,
|
||||||
|
чтобы освободить связанные с ним системные ресурсы.
|
||||||
|
|
||||||
|
### Обработка ошибок
|
||||||
|
|
||||||
|
До сих пор я ни слова не сказал об ошибках,
|
||||||
|
которые могут происходить (и часто происходят) в процессе работы с сокетами.
|
||||||
|
Так вот: если что-то пошло не так, все рассмотренные нами функции возвращают -1,
|
||||||
|
записывая в глобальную переменную `errno` код ошибки.
|
||||||
|
Соответственно, вы можете проанализировать значение этой переменной
|
||||||
|
и предпринять действия по восстановлению нормальной работы программы,
|
||||||
|
не прерывая её выполнения. А можете просто выдать диагностическое сообщение
|
||||||
|
(для этого удобно использовать функцию `perror`),
|
||||||
|
а затем завершить программу с помощью `exit`.
|
||||||
|
Именно так я буду поступать в демонстрационных примерах.
|
||||||
|
|
||||||
|
### Отладка программ
|
||||||
|
|
||||||
|
Начинающие программисты часто спрашивают, как можно отлаживать сетевую программу,
|
||||||
|
если под рукой нет сети. Оказывается, можно обойтись и без неё.
|
||||||
|
Достаточно запустить клиента и сервера на одной машине,
|
||||||
|
а затем использовать для соединения адрес интерфейса внутренней петли (loopback interface).
|
||||||
|
В программе ему соответствует константа `INADDR_LOOPBACK`
|
||||||
|
(не забудьте применять к ней функцию `htonl!`).
|
||||||
|
Пакеты, направляемые по этому адресу, в сеть не попадают.
|
||||||
|
Вместо этого они передаются стеку протоколов TCP/IP как только что принятые.
|
||||||
|
Таким образом моделируется наличие виртуальной сети,
|
||||||
|
в которой вы можете отлаживать ваши сетевые приложения.
|
||||||
|
|
||||||
|
Для простоты я буду использовать в демонстрационных примерах интерфейс внутренней петли.
|
||||||
|
|
||||||
|
### Эхо-клиент и эхо-сервер
|
||||||
|
|
||||||
|
Теперь, когда мы изучили основные функции для работы с сокетами,
|
||||||
|
самое время посмотреть, как они используются на практике.
|
||||||
|
Для этого я написал две небольшие демонстрационные программы.
|
||||||
|
Эхо-клиент посылает сообщение **«Hello there!»** и выводит на экран ответ сервера.
|
||||||
|
Его код приведён в **листинге 1**.
|
||||||
|
Эхо-сервер читает всё, что передаёт ему клиент,
|
||||||
|
а затем просто отправляет полученные данные обратно.
|
||||||
|
Его код содержится в **листинге 2**.
|
||||||
|
|
||||||
|
**Листинг 1. Эхо-клиент.**
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
|
||||||
|
char message[] = "Hello there!\n";
|
||||||
|
char buf[sizeof(message)];
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
int sock;
|
||||||
|
struct sockaddr_in addr;
|
||||||
|
|
||||||
|
sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if(sock < 0)
|
||||||
|
{
|
||||||
|
perror("socket");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
addr.sin_port = htons(3425); // или любой другой порт...
|
||||||
|
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||||
|
if(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0)
|
||||||
|
{
|
||||||
|
perror("connect");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
send(sock, message, sizeof(message), 0);
|
||||||
|
recv(sock, buf, sizeof(message), 0);
|
||||||
|
|
||||||
|
printf(buf);
|
||||||
|
close(sock);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Листинг 2. Эхо-сервер.**
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
int sock, listener;
|
||||||
|
struct sockaddr_in addr;
|
||||||
|
char buf[1024];
|
||||||
|
int bytes_read;
|
||||||
|
|
||||||
|
listener = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if(listener < 0)
|
||||||
|
{
|
||||||
|
perror("socket");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
addr.sin_port = htons(3425);
|
||||||
|
addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||||
|
if(bind(listener, (struct sockaddr *)&addr, sizeof(addr)) < 0)
|
||||||
|
{
|
||||||
|
perror("bind");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(listener, 1);
|
||||||
|
|
||||||
|
while(1)
|
||||||
|
{
|
||||||
|
sock = accept(listener, NULL, NULL);
|
||||||
|
if(sock < 0)
|
||||||
|
{
|
||||||
|
perror("accept");
|
||||||
|
exit(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
while(1)
|
||||||
|
{
|
||||||
|
bytes_read = recv(sock, buf, 1024, 0);
|
||||||
|
if(bytes_read <= 0) break;
|
||||||
|
send(sock, buf, bytes_read, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(sock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
134
content/posts/2023/privacy/gpg.md
Normal file
134
content/posts/2023/privacy/gpg.md
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
---
|
||||||
|
title: "🔐 GnuPG || GPG"
|
||||||
|
date: 2023-04-05T12:05:35+03:00
|
||||||
|
draft: false
|
||||||
|
tags: [gpg, privacy, tips]
|
||||||
|
---
|
||||||
|
|
||||||
|
## GnuPG
|
||||||
|
|
||||||
|
GnuPG — полная и свободная реализация [OpenPGP](https://openpgp.org/about/) стандарта,
|
||||||
|
определенного в [RFC4880](https://tools.ietf.org/html/rfc4880) (также известного, как PGP).
|
||||||
|
GnuPG позволяет вам шифровать и подписывать данные и сообщения.
|
||||||
|
Он оснащен универсальной системой управления ключами,
|
||||||
|
а также модулями доступа для всех типов открытых ключей.
|
||||||
|
GnuPG, также известный как GPG,
|
||||||
|
это инструмент командной строки с возможностью легкой интеграции с другими приложениями.
|
||||||
|
Доступен богатый выбор пользовательских приложений и библиотек.
|
||||||
|
Также 2 версия GnuPG поддерживает S/MIME и Secure Shell (ssh).
|
||||||
|
|
||||||
|
## Расположение каталогов
|
||||||
|
|
||||||
|
`$GNUPGHOME` используется GnuPG для определения каталога,
|
||||||
|
в котором хранятся конфигурационные файлы.
|
||||||
|
По умолчанию `$GNUPGHOME` не назначена и вместо этого используется `$HOME`;
|
||||||
|
таким образом, вы найдете каталог `~/.gnupg` сразу после установки.
|
||||||
|
|
||||||
|
Чтобы изменить стандартное расположение, выполните `gpg --homedir путь/к/файлу`
|
||||||
|
или установите переменную окружения `GNUPGHOME`.
|
||||||
|
|
||||||
|
## Файлы конфигурации
|
||||||
|
|
||||||
|
Файлы конфигурации по умолчанию `~/.gnupg/gpg.conf`.
|
||||||
|
По умолчанию разрешения доступа каталога gnupg установлены в `700`,
|
||||||
|
а файлов, которые он содержит - `600`.
|
||||||
|
Только владелец каталога имеет разрешение на просмотр содержимого,
|
||||||
|
радактирование и доступ к файлам. В целях безопасности,
|
||||||
|
эти разрешения не должны быть изменены.
|
||||||
|
В случае, если этот каталог или любые файлы внутри не следуют данной мере безопасности,
|
||||||
|
вы получите предупреждение о наличии небезопасных файлов и разрешений домашнего каталога.
|
||||||
|
|
||||||
|
## Создание пары ключей
|
||||||
|
|
||||||
|
Чтобы создать пару ключей, необходимо ввести команду:
|
||||||
|
```sh
|
||||||
|
gpg --full-gen-key
|
||||||
|
```
|
||||||
|
|
||||||
|
ℹ️ Используйте опцию `--expert`, чтобы выбрать другие шифры,
|
||||||
|
такие как [ECC](https://en.wikipedia.org/wiki/Elliptic_curve_cryptography).
|
||||||
|
|
||||||
|
ℹ️ Более простая опция `--gen-key` использует параметры по умолчанию для шифра,
|
||||||
|
размера и срока действия ключа и запрашивает только имя и адрес электронной почты.
|
||||||
|
|
||||||
|
## Экспорт открытого ключа
|
||||||
|
|
||||||
|
Основное назначение GnuPG — обеспечение конфиденциальности обмена сообщениями
|
||||||
|
с помощью криптографии с открытым ключом.
|
||||||
|
С его помощью каждый пользователь распространяет открытый ключ своей связки ключей,
|
||||||
|
который может быть использован другими пользователями для шифрования сообщений пользователю.
|
||||||
|
Закрытый ключ всегда должен оставаться в тайне, иначе конфиденциальность будет нарушена.
|
||||||
|
|
||||||
|
Таким образом, чтобы другие могли отправлять вам зашифрованные сообщения,
|
||||||
|
им нужен ваш открытый ключ.
|
||||||
|
|
||||||
|
Чтобы сгенерировать ASCII-версию открытого ключа пользователя в файл `public.key`
|
||||||
|
(например, для отправки по электронной почте):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gpg --export --armor --output public.key user-id
|
||||||
|
```
|
||||||
|
|
||||||
|
### Импорт открытого ключа
|
||||||
|
|
||||||
|
Чтобы зашифровать сообщения другим людям, а также проверить их подписи,
|
||||||
|
вам нужен их открытый ключ. Чтобы импортировать открытый ключ из файла public.key в свой список открытых ключей, выполните команду:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gpg --import public.key
|
||||||
|
```
|
||||||
|
|
||||||
|
## Резервное копирование закрытого ключа
|
||||||
|
|
||||||
|
Чтобы создать резервную копию вашего закрытого ключа, выполните:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gpg --export-secret-keys --armor --output privkey.asc user-id
|
||||||
|
```
|
||||||
|
|
||||||
|
Обратите внимание, что вышеуказанная команда требует ввода пароля от ключа.
|
||||||
|
В противном случае любой, кто получит доступ к экспортированному файлу,
|
||||||
|
сможет шифровать и подписывать документы, как если бы он был вами,
|
||||||
|
без необходимости знать пароль.
|
||||||
|
|
||||||
|
⚠️ **Важно:**
|
||||||
|
Пароль — обычно самое слабое звено в защите закрытого ключа.
|
||||||
|
Поместите закрытый ключ в безопасное место на другой системе или на другом устройстве,
|
||||||
|
например, в заблокированный контейнер или на зашифрованный диск.
|
||||||
|
Это единственное средство защиты, которое поможет
|
||||||
|
вам восстановить контроль над списком ваших ключей в случае,
|
||||||
|
например, поломки диска, кражи или ещё чего-нибудь похуже.
|
||||||
|
Этот способ резервного копирования ключей имеет некоторые ограничения по безопасности.
|
||||||
|
Более безопасный способ резервного копирования и импорта ключей с помощью gpg описан
|
||||||
|
[здесь](https://web.archive.org/web/20210803213236/https://habd.as/post/moving-gpg-keys-privately/).
|
||||||
|
|
||||||
|
## Импорт закрытого ключа из резервной копии:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gpg --import privkey.asc
|
||||||
|
```
|
||||||
|
|
||||||
|
ℹ️ Совет: [Paperkey](https://www.jabberwocky.com/software/paperkey/) позволяет экспортировать ключ в виде простого текста
|
||||||
|
или машиночитаемого штрих-кода, которые можно отпечатать на бумаге.
|
||||||
|
|
||||||
|
## Резервное копирование сертификата отзыва
|
||||||
|
|
||||||
|
Сертификаты отзыва автоматически генерируются для вновь создаваемых ключей.
|
||||||
|
По умолчанию они находятся в `~/.gnupg/openpgp-revocs.d/`.
|
||||||
|
Имя файла сертификата — это отпечаток ключа, который он отзывает.
|
||||||
|
Сертификаты отзыва также можно сгенерированы вручную с помощью следующей команды:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gpg --gen-revoke --armor --output revcert.asc user-id
|
||||||
|
```
|
||||||
|
|
||||||
|
Этот сертификат используется, чтобы выполнить отзыв ключа в случае,
|
||||||
|
если он оказался потерян или скомпрометирован.
|
||||||
|
Резервная копия будет полезна, если у вас больше нет доступа к закрытому ключу,
|
||||||
|
из-за чего вы не можете сгенерировать
|
||||||
|
новый сертификат отзыва с помощью приведённой выше команды.
|
||||||
|
Он достаточно короткий, чтобы его можно было распечатать и набрать от руки при необходимости.
|
||||||
|
|
||||||
|
⚠️ **Важно:** Любой человек, имеющий доступ к сертификату отзыва,
|
||||||
|
может публично отозвать ключ, и это действие нельзя отменить.
|
||||||
|
Защищайте свой сертификат отзыва так же, как вы защищаете свой закрытый ключ.
|
Loading…
Reference in New Issue
Block a user