Основой Интернета и всех сетей является розетка. Сокеты предоставляют возможность отправлять и получать данные между одним адресом и другим, как на одной машине, так и на совершенно разных. Существует около дюжины видов сокетов для разных типов адресов и связи. Сокеты используют разные протоколы, такие как TCP, которые определяют стиль взаимодействия между ними. Тип адреса сокета частично определяет, поддерживает ли он межмашинное или внутримашинное взаимодействие. Сокеты Unix, также называемые сокетами домена unix, представляют собой тип внутримашинного сокета, который поддерживается в системах Unix и современных версиях Windows.

В отличие от сокета на основе порта, сокет unix представлен путем в файловой системе. Тем не менее, подобно тому, как только один сокет может одновременно привязываться к порту, только один сокет unix может одновременно привязываться к определенному пути файловой системы. Программа может подключиться к сокету unix, подключившись к определенному пути, а не к IP-адресу и порту, поскольку сокеты unix не основаны на IP.

В этом руководстве мы реализуем сервер сокетов unix, который разветвляет дочерний процесс и взаимодействует с ним через сокет. Это сделано только для простоты компиляции и запуска кода. Вполне возможно иметь отдельные клиентские и серверные программы сокетов unix.

Настройка сервера

Первый шаг к возможности прослушивания и приема соединений через сокет unix — это его создание. Нам нужно настроить несколько включений и определений, которые будут использоваться при создании сокета. К ним относятся некоторые системные файлы заголовков, характерные для unix, а также путь, по которому мы хотим, чтобы наш сокет находился.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <signal.h>
#include <errno.h>
//--------system headers -------//
#include <unistd.h>
#include <sys/un.h>
#include <sys/socket.h>
const char* SOCKET_FILE = "/tmp/fgfgdgdgff";
#define BUFFER_SPACE 100

В экспериментальных целях можно хранить файлы сокетов под /tmp. Но любая реальная установка потребует более безопасного местоположения или установки. Файлы сокетов могут иметь любое расширение, но обычно используется расширение ".sock".

Далее мы можем создать фактический сокет. Мы знаем, что нам нужен сокет unix, и для целей этого руководства мы создадим потоковый сокет. Это означает, что сокет подвергается связи на основе соединения. Также возможно создавать сокеты дейтаграмм unix, которые взаимодействуют без соединений.

int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1) {
     fprintf(stderr, "Failed to create socket, err=%d\n", errno);
     exit(4);
}

Если socket() возвращает -1, это означает, что сокет не удалось создать, и поэтому мы должны выйти и сообщить errno, чтобы помочь диагностировать проблему. Теперь, когда сокет создан, он должен быть привязан к unix-адресу.

Адрес сокета unix представлен типом struct sockaddr_un. Эта структура содержит ключевую информацию, такую ​​как путь к файловой системе, к которому привязан сокет. Следующим шагом является запись семейства сокетов и пути в структуру адреса.

struct sockaddr_un addr;
if (strlen(SOCKET_FILE) > sizeof(addr.sun_path) - 1) {
     fprintf(stderr, "Socket name too long, socket sun path is size %zu\n", sizeof(addr.sun_path) - 1);
     exit(1);
}
memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCKET_FILE, sizeof(addr.sun_path) - 1);

Теоретически наш путь к сокету может быть слишком длинным, чтобы поместиться в буфер адресной структуры, поэтому правильно проверить это. Как только адресный сокет настроен, мы можем привязать к нему наш сокет.

if (bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1) {
     fprintf(stderr, "Failed to bind, err=%d\n", errno);
     exit(3);
}

Этот шаг связывает объект сокета в программе с путем файловой системы адреса unix. Таким образом, можно не выполнить этот шаг и получить сообщение типа Failed to bind, err=48. Когда errno равно 48, это означает, что адрес уже используется. Как и в случае с портами, путь, выбранный для сокета, не должен быть уже использован.

После успешной привязки программа может прослушивать адрес для входящих подключений.

if (listen(sfd, 5) == -1) {
     fprintf(stderr, "Failed to listen err=%d\n", errno);
     exit(2);
}

Аргумент 5 означает количество подключений, которые могут стоять в очереди от клиентов, прежде чем мы вызовем accept(). Вызов listen() означает начало обслуживания соединения с внешними клиентами. Если очереди соединений превышают установленный лимит, эти дополнительные соединения будут отклонены.

Чтобы на самом деле получить эти соединения, accept() нужно вызвать в какой-то форме цикла. accept() также является блокирующим вызовом, то есть, если входящих соединений нет, вызывающий его поток будет заблокирован до тех пор, пока соединение не будет получено. Как только соединение получено, accept() возвращает дескриптор файла, представляющий соединение этого клиента. Пока accept не возвращает -1, это означает, что соединение между сервером и клиентом успешно установлено, и может иметь место чтение или запись.

Для простоты этот сервер будет просто читать данные, которые он получает через соединение, и записывать эти данные в stdout. При сохранении запроса проверка возвращаемых значений read и write может выявить возникающие ошибки.

while (1) {
      cfd = accept(sfd, NULL, NULL);
      if (cfd == -1) {
          fprintf(stderr, "Cannot accept, err=%d\n", errno);
          exit(5);
      }
      while ((numRead = read(cfd, buf, BUFFER_SPACE)) > 0) {
          if (write(STDOUT_FILENO, buf, numRead) != numRead) {
               fprintf(stderr, "Cannot write to stdout, err=%d\n", errno);
               exit(6);
          }
      }
      if (numRead == -1) {
            fprintf(stderr, "Error while reading, err=%d\n", errno);
            exit(8);
      }
      if (close(cfd) == -1) {
            fprintf(stderr, "Cannot close connected fd, err=%d\n", errno);
            exit(9);
       }
}

Первым шагом в цикле является вызов accept() и проверка на наличие сбоя. Затем цикл while начинает считывать данные из подключенного файлового дескриптора, пока еще есть данные для чтения. Для каждого прочитанного фрагмента данных эти данные записываются в stdout. После того, как весь цикл завершается, мы делаем последнюю проверку, если чтение не удалось, затем закрываем соединение.

Клиент

Теперь, когда показана серверная часть этого руководства, пришло время обсудить клиент и его реализацию. Для простоты клиент будет разветвлен от серверного процесса и будет связываться с сервером на нем. Для этого нам нужна отдельная функция, в которой будет происходить разветвление. Кроме того, мы хотим, чтобы серверный процесс не выполнял код, предназначенный для дочернего процесса. Для выполнения этого требования можно использовать функцию fork() и возвращаемое ею значение.

static void do_client(void) {
    pid_t child = fork();
    if (child != 0) {
       return;
    }
 ...........
}

Когда вызывается fork(), процесс разделяет свое состояние на собственный и дочерний процессы. Однако возвращаемое значение для fork() будет другим. В родительском процессе он возвращает pid дочернего процесса, в дочернем процессе он возвращает 0. Таким образом, мы можем надежно контролировать поток выполнения, проверяя это возвращаемое значение.

Есть несколько разных мест выполнения сервера, в которых мы можем вызвать нашу функцию do_client. Если он вызывается до того, как сокет сервера будет связан и прослушивается, клиент должен обработать возможность отказа в соединении при попытке подключиться к unix-адресу. Если клиент разветвляется после того, как сервер начинает прослушивать сокет, клиенту не нужно беспокоиться о том, что адрес сокета еще не существует. В общем, важно иметь в виду, что без каких-либо блокировок поведение между двумя процессами не синхронизируется.

Вот код для обработки возможности отказа в подключении при подключении.

struct sockaddr_un addr;
int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1) {
  fprintf(stderr, "Could not create socket, err=%d\n", errno);
  exit(4);
}
memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCKET_FILE, sizeof(addr.sun_path) - 1);
while (connect(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1) {
     if (errno == ECONNREFUSED) {
          fprintf(stderr, "connection refused, retrying\n");
           continue;
     } 
     fprintf(stderr, "Failed to connect to socket, err=%d\n", errno);
     exit(4);
}

Настройка аналогична серверу, и клиенты, и сервер для сокетов unix должны создать объект socket. Разница в том, что серверы связываются и прослушивают адреса, клиенты подключаются к адресам.

После того, как клиентский процесс установит соединение, он может записать некоторые данные на сервер, а затем корректно завершить работу.

if (write(sfd, "Foody", 5) != 5) {
     fprintf(stderr, "Failed to write to socket, err=%d\n", errno);
     exit(5);
}
if (write(sfd, "Foody", 5) != 5) {
     fprintf(stderr, "Failed to write to socket, err=%d\n", errno);
     exit(5);
}
if (write(sfd, "Foody", 5) != 5) {
     fprintf(stderr, "Failed to write to socket, err=%d\n", errno);
     exit(5);
}
exit(0);

Каждый из вызовов write() просто проверяет, что количество байтов, предназначенных для записи, действительно было записано.

В целом

Есть еще несколько частей, которые следует добавить, прежде чем этот пример программы будет считаться выполненным. Сокеты Unix существуют по определенным путям файловой системы. Но это означает, что когда сокет привязан к адресу, даже после выхода сервера файл все еще существует. Таким образом, чтобы убедиться, что программа будет работать после запуска более одного раза, удалите путь к сокету, если он существует, перед привязкой к серверу.

Наконец, эта программа не настроена на обработку SIGPIPE , который является сигналом, который процесс отправляет другому процессу, подключенному к тому же сокету, в случае разрыва канала. Так что можно было бы также игнорировать это

signal(SIGPIPE, SIG_IGN);
// Make sure to handle removal of socket 
if (remove(SOCKET_FILE) == -1 && errno != ENOENT) {
     fprintf(stderr, "Failed to remove socket path %s\n", SOCKET_FILE);
     exit(4);
}

Окончательный полный код приведен ниже