В этой статье познакомимся с Unix сокетами. Рассмотрим способы их применения и пару их видов. Напишем сокетные клиент и сервер на python, а также проверим клиент в работе с rsyslog’ом.

Сокеты

Сокеты являются механизмом межпроцессорного взаимодействия (IPC - inter-process communication) внутри одного компьютера или между различными компьютерами по сети. Если вы имели дело или слышали о таком понятии как UDP и TCP порты, то вам может быть интересно узнать, что они являются дополнительным уровнем абстракции над сокетами. Детали этого рассмотрим далее.

Если речь заходит о Unix- как правило подразумевается взаимодействие между двумя процессам в рамках одного компьютера. В процессе создания такого сокета создается файл на файловой системе. При этом другой процесс может обратиться к этому файлу с целью записать в него данные или прочитать данные из сокета.

Передача информации между двумя процессами через unix- сокет при передаче информации биты “летят” даже не через loopback интерфейс, всё взаимодействие происходит внутри одного ядра, а значит, в рамках оперативной памяти и кэшей процессора, за исключением первичного обращения к файловой системе.

DATAGRAM и STREAM сокеты

Если в Linux выполнить команду netstat с флагом -x, то в ответ команда выведет список unix-сокетов. Причем, скорее всего все сокеты будут принадлежать к одному из двух типов: DGRAM и STREAM. Этих типов на самом деле больше, еще существуют RAW и SEQUENCED.

user@vm:~$ netstat -x
Active UNIX domain sockets (w/o servers)
Proto RefCnt Flags       Type       State         I-Node   Path
unix  3      [ ]         STREAM     CONNECTED     35012
unix  3      [ ]         DGRAM      CONNECTED     29295

DATAGRAM сокеты фактически используют протокол UDP (User Datagram Protocol) для передачи информации. Входное сообщение они делят на датаграммы и отправляют независимо друг от друга. Гарантий сохранения общего порядка информации нет. То есть вторая датаграмма может прийти получателю раньше первой. Проверки доставки нет, гарантий отсутствия дубликатов также нет. Ответственность по решению этих проблем лежит на пользовательском приложении, использующем сокеты.

STREAM сокеты в отличие от DATAGRAM, используют протокол TCP. Сокеты сами заботятся о сохранении верного порядка информации при передаче. Контролируют отсутствие дубликатов, запрашивают и подтверждают статус доставки.

Помните, я говорил, что TCP/UDP сокеты лишь дополнительный уровень абстракции? А это потому что тот же самый TCP сокет- это UNIX сокет типа STREAM, который “забинджен” не на файл файловой системы, а на пару ip-адрес - порт. Аналогично обстоят дела и с UDP сокетами.

Сервер

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

import socket
import os


DEFAULT_PATH = '/tmp/test_server_socket'


def get_server_socket(path):
    server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
    server_socket.bind(path)

    return server_socket


def serve():
    server = get_server_socket(DEFAULT_PATH)

    try:
        while True:
            data, address = server.recvfrom(1024)
            print(data.decode('utf-8'))
    except:
        server.close()



if __name__ == "__main__":
    serve()

В этой программе есть одна проблема. Если её закрыть после того, как будет исполнена функция .bind(path), файл в файловой системе так и останется. Соответственно, при повторном запуске возникнет исключение, говорящее о том, что такой файл-сокет-адрес уже заняты. Следовательно, придется вручную удалять создающийся сокет.

Способ решения этой проблемы на самом деле не очевидный. Например, на stackoverflow самый популярный способ решения этой проблемы предлагает удалять файл по пути “path” перед тем, как создавать новый сокет. Выглядит не слишком чисто. Куда лучше импортировать модуль stat и использовать функцию stat.S_ISSOCK(), чтобы определить, является ли файл по пути path сокет- файлом, чтобы удалять его.

Пример использования stat.S_ISSOCK()

Ссылка на документацию по stat

Клиент

Код клиента ничем принципиально не отличается от кода сервера.

import socket
import time
import random
import sys


def get_connected_client(socket_path):
    client = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
    client.connect(socket_path)
    return client


def send_to_socket(client, infinite=False):
    while True:
        message = f'Message from python {random.randint(1, 100)}'
        client.send(message.encode())

        if not infinite:
            break

        time.sleep(2)

    client.close()


if __name__ == '__main__':
    path_in_list = sys.argv[1:2]

    if not path_in_list:
        print('Path to socket is needed')
        sys.exit(1)

    path = path_in_list[0]

    client = get_connected_client(path)
    send_to_socket(client)

Уже на данном этапе вы можете проверить, работают ли сокеты, запустив сервер и клиент.

RSYSLOG

Теперь, проверим, можно ли с другими приложениями работать. Для этого я предпочел поэкспериментировать на rsyslog’ом. Если его нет в вашем дистрибутиве- найдите себе дистрибутив с rsyslog’ом или установите его.

Rsyslog- backend системы логирования linux. Он не единственный backend и логирует не всё, еще есть journald, например. Зачастую приложения требуется верно сконфигурировать, чтобы они отправляли свои логи в rsyslog. Даже journald можно сконфигурировать так, чтобы он слал логи в rsyslog, делается это в файле /etc/systemd/journald.conf. Можно в нём такой параметр прописать:

ForwardToSyslog=yes

Может возникнуть вопрос: а откуда journald знает, как логи в rsyslog отправить? Может возникнуть и другой вопрос, как rsyslog поймет, что ему хотят логи прислать?

Насчёт первого вопроса- journald знает, что syslog будет слушать сокет по пути /run/systemd/journal/syslog. Насчёт второго- откуда rsyslog знает, что ему нужно открыть сокет по заданному пути? Ответ- он не знает, но существуют договоренности в мире Linux. Требуется прописать верный конфиг в /etc/rsyslog.conf или в /etc/rsyslog.d/....

module(load="imuxsock")

if $inputname == "imuxsock" then {
        action(type="omfile" File="/tmp/file.log")
}

Директива module(load="imuxsock") помимо того, что подключит модуль imuxsock, еще и по пути /run/systemd/journal/syslog откроет DATAGRAM сокет, который станет слушать.

Синергия rsyslog и python клиента

Если звезды сошлись, а вы знаете, как иметь дело с rsyslog, то у вас должно быть всё успешно. Вы увидите те сообщения, которые отправляете через python в сокет rsyslog’а в файле /tmp/file.log. Правда, у меня при этом еще кто- то умудряется 2 байта из начала сообщения красть (это делает rsyslog по неясно причине).

Вывод

Выводы оставляю на совести читателя.