Демоны / Работа с сокетам

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

К счастью существует более эффективный способ общения с юзером. С помощью так называемых сокетов.

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

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

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

Есть разные протоколы взаимодействия с сервером. Мы будем использовать протокол TCP/IP, потому что данный протокол предполагает создание диалога между пользователем (клиентом) и демоном (сервером), что идеально подходит под нашу ситуацию. Можно теперь забыть про наш старый код с FileSystemWatcher-ом и циклом с Thread.Sleep. Теперь у нас будет нормальный сервер который сам будет ждать соединения, в общем, пишем:

using System;
using System.Net.Sockets;
using System.Text;

namespace App
{
    class Program {
        public  static void Main(String[] args) {
            // проверяем не создали ли мы сокет файл уже
            // и если создали то удаляем старый файл
            if (File.Exists("daemon.socket")) {
                File.Delete("daemon.socket");
            }
            
            // собственно создаем канал для сокета
            UnixDomainSocketEndPoint endPoint = new("daemon.socket");
            // создаем объекта сокета
            var socket = new Socket(
                endPoint.AddressFamily,
                SocketType.Stream,
                ProtocolType.IP
            );
            
            // подключаем к каналу
            socket.Bind(endPoint);
            // и начинаем слушать соединения
            socket.Listen();

            while(true) {
                Console.WriteLine("Жду приказов");
                
                var handler = socket.Accept(); // эта строка блокирует код пока кто-нибудь не подключится к сокету
                
                // как только кто-то подключился мы можем начать обрабатывать команды от юзера
                while (true) {  
                    var bytes = new Byte[1024];   // буфер под введенные данные
                    int bytesRec = handler.Receive(bytes);   // эта штука по сути аналог Console.ReadLine()
                    var input = Encoding.UTF8.GetString(bytes); // только полученные байты превращаем в строку
                    var output = $"Вы написали {input}"; // формируем ответ
                    handler.Send(Encoding.UTF8.GetBytes(output)); // посылаем ответ, преобразуя его обратно в байты
                }  
            }
        }
    }
}

попробуем запустить

dotnet run

если глянуть, то увидим, что в папке появился файлик daemon.socket

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

Можно попробовать туда чего-нибудь echoнуть

правда оно не сработает так как это не настоящий файл.

В общем чтобы к нему подключится нам потребуется специальная программа для подключения к сокетам. Называется она socat. Ставим ее себе

sudo apt install socat

Теперь попробуем подключится к серверу. Пишем

socat UNIX-CLIENT:daemon.socket -

и попробуем чего-нибудь пописать

чтобы остановить общение надо нажать Ctrl+C

это разорвет соединения socat, и заодно убьет наш сервер так для него такая ситуация является нештатной, что конечно такое себе.

В общем надо ловить эту ошибку и в случае ее возникновения прерывать соединения корректно вот так

// ...
while(true) {
    // тут не трогаем
    Console.WriteLine("Жду приказов");

    var handler = socket.Accept(); 
    
    // а цикл правим
    while (true) {  
        try { // обернул в try
            // тут старый код который раньше был в while
            var bytes = new Byte[1024]; 
            int bytesRec = handler.Receive(bytes);
            var input = Encoding.UTF8.GetString(bytes);
            var output = $"Вы написали {input}";
            handler.Send(Encoding.UTF8.GetBytes(output));
        } catch (SocketException) { // ловлю ошибку
            // в случае ошибки пишу об этом
            Console.WriteLine("Произошёл разрыв соединения");  
            // закрываю соединения аккуратно
            handler.Shutdown(SocketShutdown.Both); 
            handler.Close();  
            break; // и выхожу из цикла перехвата ввода
        }
    }  
}
// ...

Пробуем запустить

Отлично!

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

Делается это так

using System;
using System.Net;  // не забыть добавть using
using System.Net.Sockets;
using System.Text;

namespace App
{
    class Program {
        public  static void Main(String[] args) {
            // узнаем системный IP
            IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());  
            IPAddress ipAddress = ipHostInfo.AddressList[0];  
            // создаем точку подключения
            IPEndPoint endPoint = new IPEndPoint(ipAddress, 8080);  
            Console.WriteLine($"Слушаю запросы на {ipAddress}:8080");
            
            // ну и дальше как обычно
            var socket = new Socket(
                endPoint.AddressFamily,
                SocketType.Stream,
                ProtocolType.IP
            );
            
            socket.Bind(endPoint);
            socket.Listen();
            
            //...
        }
    }
}

И теперь если захочется подключится к серверу просто пишем вот так:

socat TCP:127.0.1.1:8080 -

вот теперь другое дело, можно пилить задачу

4.4

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