Создание службы Windows

29

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

На рисунке ниже показан общий вид решения. Сборка QuoteServer содержит код, реализующий саму функциональность службы. Она отвечает за чтение файла цитат в кэше памяти и отправку ответов на запросы цитат с помощью сервера сокетов.

Сборка QuoteClient представляет собой многофункциональное клиентское приложение WPF. Это приложение отвечает за создание клиентского сокета для взаимодействия с QuoteServer. Третья сборка — QuoteService — представляет собой саму службу и отвечает за запуск и остановку QuoteServer, т.е. за управлением сервером QuoteServer.

Сервер цитат

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

Создание ключевой функциональности для службы

В службе Windows можно предусмотреть любую функциональность, такую как сканирование файлов для выполнения резервного копирования или проверка на предмет наличия вирусов или запуска сервера WCF. Однако все служебные программы обладают определенным сходством. В частности, все они должны предусматривать возможность запуска (и возврата управления вызывающему коду), останова и приостановки работы. В этом разделе показано, как реализовать такие возможности на примере сервера сокетов.

В Windows 7 в виде части компонентов может устанавливаться Simple TCP/IP Services. В состав этого компонента входит TCP/IP-сервер "quote of the day" (цитата дня), или qotd. Этот сервер представляет собой простую службу, которая прослушивает порт 17 и отвечает на каждый запрос случайным сообщением, которое берет из файла <windows>\system32\drivers\etc\quotes. В нашем примере службы будет создаваться подобный сервер, но только в отличие от quotd, возвращающего строку в кодировке ASCII, этот сервер должен возвращать строку Unicode.

Сначала создадим библиотеку классов по имени QuoteServer и реализуем в ней код для сервера. Ниже по частям рассматривается код класса QuoteServer, который должен содержаться в файле QuoteServer.cs:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace WinServices
{
    public class QuoteServer
    {
        private TcpListener listener;
        private int port;
        private string filename;
        private List<string> quotes;
        private Random random;
        private Thread listenerThread;
        ...

Конструктор QuoteServer() перегружен так, чтобы при вызове ему можно было передавать имя файла и номер порта. Конструктор, в котором передается только имя файла, по умолчанию предусматривает использование для сервера порта с номером 7890. В конструкторе по умолчанию имя файла по умолчанию выглядит как quotes.txt:

...
        public QuoteServer()
            : this("quotes.txt")
        {
        }
        public QuoteServer(string filename)
            : this(filename, 7890)
        {
        }
        public QuoteServer(string filename, int port)
        {
            this.filename = filename;
            this.port = port;
        }
        ...

Метод ReadQuotes() — это вспомогательный метод, который считывает все цитаты из файла, указанного в конструкторе. Затем все эти цитаты добавляются в коллекцию quotes типа List<string>. Кроме того, создается экземпляр класса Random, который будет использоваться для возврата случайных цитат. Следующий вспомогательный метод — GetRandomQuoteOf TheDay(). Этот метод возвращает случайную цитату из коллекции quotes:

...
        protected void ReadQuotes()
        {
            quotes = File.ReadAllLines(filename).ToList();
            random = new Random();
        }

        protected string GetRandomQuoteOfTheDay()
        {
            int index = random.Next(0, quotes.Count);
            return quotes[index];
        }
        ...

В методе Start() весь файл с цитатами полностью считывается в коллекцию цитат типа List<string> с использованием вспомогательного метода ReadQuaotes(). После этого запускается новый поток, который немедленно вызывает метод Listener().

Здесь используется поток, потому что метод Start() не может блокироваться и ожидать клиента; он должен немедленно возвращать управление вызвавшей его программе (диспетчеру SCM). Диспетчер SCM будет предполагать, что запуск не удался, если этот метод не будет возвращать ему управление в течение 30 секунд. Поток-слушатель конфигурируется как фоновый, чтобы приложение могло завершить свою работу без остановки этого потока. Свойству Name потока присваивается определенное значение для упрощения процесса отладки, поскольку тогда это значение будет появляться в отладчике.

...
        public void Start()
        {
            ReadQuotes();
            listenerThread = new Thread(ListenerThread);
            listenerThread.IsBackground = true;
            listenerThread.Name = "Listener";
            listenerThread.Start();
        }

Функция потока ListenerThread() создает экземпляр TcpListener. Метод AcceptSocket() ожидает подключения клиента. Как только клиент подключается, AcceptSocket() возвращает информацию о сокете, который ассоциируется с этим клиентом. Далее вызывается метод GetRandomQuoteOfTheDay() для отправки клиенту выбираемой произвольным образом цитаты с помощью socket.Send():

...
        protected void ListenerThread()
        {
            try
            {
                IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
                listener = new TcpListener(ipAddress, port);
                listener.Start();
                while (true)
                {
                    Socket clientSocket = listener.AcceptSocket();
                    string message = GetRandomQuoteOfTheDay();
                    UnicodeEncoding encoder = new UnicodeEncoding();
                    byte[] buffer = encoder.GetBytes(message);
                    clientSocket.Send(buffer, buffer.Length, 0);
                    clientSocket.Close();
                }
            }
            catch (SocketException ex)
            {
                Trace.TraceError(String.Format("QuoteServer {0}", ex.Message));
            }
        }

        public void Stop()
        {
            listener.Stop();
        }
        public void Suspend()
        {
            listener.Stop();
        }
        public void Resume()
        {
            listener.Start();
        }

        public void RefreshQuotes()
        {
            ReadQuotes();
        }
    }
}

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

Чтобы получить такую тестовую программу, давайте создадим новое консольное приложение на C# и назовем его TestQuoteServer. Добавим в него ссылку на сборку класса QuoteServer и скопируем файл, содержащий цитаты, в каталог c:\ProCSharp\Services (иначе придется изменить аргумент конструктора, указав в нем место, куда был скопирован этот файл). После вызова конструктора экземпляра QuoteServer в этом приложении должен вызываться его метод Start(). После создания потока этот метод должен немедленно возвращать управление, чтобы консольное приложение продолжало выполняться до тех пор, пока не будет нажата клавиша <Enter>:

static void Main()
{
            QuoteServer qs = new QuoteServer("quotes.txt", 4567);
            qs.Start();
            Console.WriteLine("Hit return to exit");
            Console.ReadLine();
            qs.Stop();

}

Обратите внимание, что QuoteServer будет работать с портом 4567 локального хоста, на котором запускается данная программа, поэтому именно такие настройки потребуется далее указать в клиенте.

Пример QuoteClient

Клиент в рассматриваемом примере представляет собой простое приложение WPF, в котором пользователь может запрашивать цитаты из сервера. Это приложение использует класс TcpClient для подключения к работающему серверу, принимает возвращаемое сообщение и отображает его в текстовом поле. Его пользовательский интерфейс содержит только два элемента управления — Button и TextBox. У элемента Button есть событие Click, которое назначается методу OnGetQuote, а у ТехВох — свойство x:Name, которое устанавливается в textQuote.

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

Информация о сервере и портах, необходимая для подключения

Здесь можно настраивать параметры ServerName (имя сервера) и PortNumber (номер порта), а также определять другие значения, подлежащие использованию по умолчанию. Если установить параметр Scope (область действия) в User (пользователь), то все производимые здесь настройки будут помещены в конфигурационный файл конкретного пользователя. Благодаря этому, для каждого пользователя приложения можно настроить собственные параметры. Вкладка Settings в Visual Studio предусматривает создание класса Settings, с помощью которого можно читать и записывать параметры. В код клиента потребуется добавить следующие директивы using:

using System;
using System.Net.Sockets;
using System.Text;
using System.Windows;
using System.Windows.Input;

Основная функциональность клиента находится в обработчике события щелчка на кнопке Get Quote (Получить цитату):

private void OnGetQuote(object sender, RoutedEventArgs e)
        {
            const int bufferSize = 1024;
            Cursor currentCursor = this.Cursor;
            this.Cursor = Cursors.Wait;

            string serverName = Properties.Settings.Default.ServerName;
            int port = Properties.Settings.Default.PortNumber;

            TcpClient client = new TcpClient();
            NetworkStream stream = null;
            try
            {
                client.Connect(serverName, port);
                stream = client.GetStream();
                byte[] buffer = new Byte[bufferSize];
                int received = stream.Read(buffer, 0, bufferSize);
                if (received <= 0)
                {
                    return;
                }
                textQuote.Text = Encoding.Unicode.GetString(buffer).Trim('\0');
            }
            catch (SocketException ex)
            {
                MessageBox.Show(ex.Message, "Ошибка при выдаче цитаты",
                      MessageBoxButton.OK, MessageBoxImage.Error);
            }
            finally
            {
                if (stream != null)
                {
                    stream.Close();
                }

                if (client.Connected)
                {
                    client.Close();
                }
            }
            this.Cursor = currentCursor;

        }

Теперь, запустив тестовый сервер и это клиентское приложение Windows, можно протестировать функциональность. На рис показан результат успешного выполнения данного приложения:

Загрузка цитат

Далее можно переходить к реализации в сервере функциональности службы. Программа уже работает, а что нужно еще?

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

Пройди тесты
Лучший чат для C# программистов