Опубликован: 11.01.2013 | Доступ: свободный | Студентов: 623 / 124 | Длительность: 12:06:00
Лекция 9:

Лабораторный практикум по технологиям Bluetooth и Wi-Fi

< Лекция 8 || Лекция 9: 12345

Удаленный редактор реестра через Bluetooth и Wi-Fi

Лабораторная работа №5. Этот проект – модификации программы-чата через Bluetooth и Wi-Fi (программы Wi-CQ). Необходимо изменить протокол сообщений (или придумать свой), для того, чтобы возможно было удаленно управлять реестром.

Постановка задачи

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

Написать программу-сервер, которая принимает подключения через Wi-Fi и Bluetooth и далее в соответствии с созданным протоколом принимает от клиентов сообщения и производит их рассылку.

Методические рекомендации

Здесь приводится протокол программы Wi-CQ и предполагаемые изменения.

Таблица 9.3. Команды, посылаемые клиентом серверу
Команда Описание
CONN|<имя> Войти в чат под именем <имя>.
CHAT|<от>|<сообщение> Отправить сообщение всем клиентам. В поле <от> указывается имя данного клиента, а в поле <сообщение> – сам текст сообщения. Напомним, что по умолчанию длина пакета в протоколе RFCOMM – 127 байт. Так что советуем вам увеличить это значение (больше 1000 указать нельзя!) и поставить ограничение на длину текста в поле ввода сообщения.
PRIV|<от>|<кому>|<сообщение> Послать личное сообщение пользователю с именем <от>. Поля <кому> и <сообщение> идентичны используемым в предыдущей команде. Это основная команда программы, в поле <сообщение> заносится запрос в некоторой удобной для парсинга форме. Вместо одной команды типа PRIV с множеством вариантов сообщений в теле можно сделать несколько команд.
GONE Выйти из чата
Таблица 9.4. Команды, посылаемые сервером клиентам
Команда Описание
LIST|<список пользователей> Послать клиенту текущий список пользователей. Поле от имеет формат <имя1>|<имя2>|...|<имяN>
CHAT|<от>|<сообщение> Отослать всем клиентам принятое сообщение. Заметим, что сообщение также отправляется и клиенту, от которого оно пришло. Это упрощает работу, т.к. не нужно фильтровать список пользователей при пересылке, а также клиенту не нужно дополнительным способом выводить свое сообщение на экран.
PRIV|<от>|<кому>|<сообщение> Отослать сообщение клиенту с именем <кому>.
JOIN|<имя> Послать всем клиентам сообщение, о том, что в чат вошел новый пользователь с именем <имя>.
GONE|<имя> Послать всем клиентам сообщение, о том, что пользователь с именем <имя> вышел из чата.
QUIT Послать всем клиентам сообщение, о том, что сервер закрывается.

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

Примерное представление работы Wi-CQ (Рис. 9.2 , цифры – имена компьютеров):

Примерное представление работы Wi-CQ

Рис. 9.2. Примерное представление работы Wi-CQ

Дополнение в работе – добавление новых команд для удобной передачи запросов к реестру.

Таблица 9.5. Новые команды для удобной передачи запросов к реестру
Команда Описание
GETVALUES|<имя отправителя>|<имя получателя>|<ветвь> Получить список ключей ветви <ветвь> компьютера, где запущен клиент <имя получателя.>
SENDVALUES||<имя отправителя>|<имя получателя>|<ветвь>|<значения> Ответить на запрос ключей.
DELETEVALUE||<имя отправителя>|<имя получателя>|<ветвь>|<ключ> Удалить <ключ> в <ветви>
CREATEVALUE|<имя отправителя>|<имя получателя>|<ветвь>|<ключ> Создать или изменить <ключ> в <ветви>
DELETESUBFOLDER|<имя отправителя>|<имя получателя>|<ветвь>|<новая ветвь> Удалить <новую ветвь> в <ветви>
CREATESUBFOLDER|<имя отправителя>|<имя получателя>|<ветвь>|<новая ветвь> Создать или изменить <ветвь> в <ветви>
GETSUBFOLDERNAMES|<имя отправителя>|<имя получателя>|<ветвь>| Получить список подветвей
SENDSUBFOLDERNAMES|<имя отправителя>|<имя получателя>|<ветвь>| Отослать список подветвей
EXECUTIONOK|<имя отправителя>|<имя получателя>| Операция выполнена успешно
EXECUTIONERROR|<имя отправителя>|<имя получателя>|<код ошибки> Операция завершилась ошибкой, например доступ к реестру запрещен политикой безопасности

И так далее.

Архитектура

Самое трудное – создать основу приложения, отвечающую за соединение компьютеров в сеть и передачу между ними сообщений. Для облегчения этого момента есть методические указания, общие для двух проектов (фрагменты кода Wi-CQ):

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

Процесс общения основной программы с объектом Connection удобно реализовать с помощью сообщений (events). При этом от главной программы будет скрыты детали реализации процесса соединения, а класс Connection не будет ничего знать о реализации основной программы, например в какое окно вывести какой текст.

Приведем исходный код класса Connection для клиента и сервера:

Клиент:

public abstract class Connection
{
    private string  _clientName;
    private Thread  _receiveThread;

    public delegate void UsersListHandler(string[] names);
    public delegate void MessageReceiveHandler(string from,
                 string message,
                 bool isPrivate);
    public delegate void UsersChangedHandler(string name, bool isJoin);
    public delegate void ServerQuitHandler();

    public event UsersListHandler      UsersListEvent;
    public event MessageReceiveHandler MessageReceiveEvent;
    public event UsersChangedHandler   UsersChangedEvent;
    public event ServerQuitHandler     ServerQuitEvent;

    public Connection(string clientName)
    {
        _clientName = clientName;
    }

    public virtual void Start()
    {
        _receiveThread = new Thread(new ThreadStart(ConnectionThread));
        _receiveThread.Start();

        string message = String.Format("CONN|{0}", _clientName);
        Send(message);
    }

    public virtual void Dispose()
    {
        _receiveThread.Abort();
        _receiveThread = null;
    }

    protected virtual void OnClose()
    {
    }

    public abstract void Send(string message);

    protected abstract string Receive();

    private void ConnectionThread()
    {
        bool keepAlive = true;
 
        while (keepAlive)
        {
            string   message = Receive();
            string[] tokens  = message.Split(new Char[]{'|'});

            switch (tokens[0])
            {
                case "LIST":
                    string[] names = new string[tokens.Length-1];
                    Array.Copy(tokens, 1, names, 0, tokens.Length-1);
                    UsersListEvent(names);
                    break;

                case "CHAT":
                    MessageReceiveEvent(tokens[1], tokens[2], false);
                    break;

                case "PRIV":
                    MessageReceiveEvent(tokens[1], tokens[3], true);
                    break;

                case "JOIN":
                    UsersChangedEvent(tokens[1], true);
                    break;

                case "GONE":
                    UsersChangedEvent(tokens[1], false);
                    break;

                case "QUIT":
                    OnClose();

                    ServerQuitEvent();
                    keepAlive = false;
                    break;
            }
        }
    }
}

Сервер:

public abstract class Connection
{
    protected Thread    _thread;
    protected ArrayList _connections; // Список всех соединений
    protected string    _clientName;

    public event EventHandler ClientsChanged;

    public string ClientName
    {
        get { return _clientName; }
    }

    public Connection(ArrayList connections)
    {
        _connections  = connections;
    }

    public virtual void Start()
    {
        _thread = new Thread(new ThreadStart(ConnectionThread));
        _thread.Start();
    }

    public virtual void Dispose()
    {
        if (_thread != null)
        {
            _thread.Abort();
            _thread = null;
        }

        OnClose();
    }

    protected virtual void OnClose()
    {
    }

    public abstract void Send(string message);

    protected abstract string Receive();

    protected virtual void ConnectionThread()
    {
        bool keepAlive = true;

        while (keepAlive)
        {
            string message = null;

            try
            {
                message = Receive();
            }
            catch
            {
                _connections.Remove(this);
                SendToAll(String.Format("GONE|{0}", _clientName));

                EventArgs e = new EventArgs();
                ClientsChanged(this, e);

                OnClose();
                return;
            }

            string[] tokens  = message.Split(new Char[]{'|'});

            switch (tokens[0])
            {
            case "CONN":
                _clientName = tokens[1];

                SendToAll("JOIN|" + _clientName);

                _connections.Add(this);

                Send("LIST|" + GetChatterList());

                EventArgs ea = new EventArgs();
                ClientsChanged(this, ea);

                break;

            case "CHAT":
                SendToAll(message);
                break;

            case "PRIV":
                string destinationClient = tokens[2];

                foreach (Connection c in _connections)
                {
                    if (c.ClientName.CompareTo(tokens[1]) == 0 ||
                        c.ClientName.CompareTo(tokens[2]) == 0)
                        c.Send(message);
                }
                break;

            case "GONE":
                SendToAll(message);

                _connections.Remove(this);
                OnClose();

                EventArgs e = new EventArgs();
                ClientsChanged(this, e);

                keepAlive = false;
                break;
            }
        }
    }

    private void SendToAll(string message)
    {
        ArrayList closedConnections = new ArrayList();

        foreach (Connection c in _connections)
        {
            try
            {
                c.Send(message);
            }
            catch (Exception)
            {
                closedConnections.Add(c);
            }
        }

        foreach (Connection c in closedConnections)
        {
            _connections.Remove(c);
            Dispose();
        }
    }

    private string GetChatterList()
    {
        StringBuilder chatters = new StringBuilder();

        foreach (Connection c in _connections)
            chatters.AppendFormat("{0}|", c.ClientName);

        chatters.Length--;

        return chatters.ToString();
    }
}

Реализацию классов LanConnection и BluetoothConnection нужно написать самим. Поскольку программирование под Wi-Fi сети абсолютно не отличается от программирования обычных локальных сетей, то пользоваться придется обычными классами TcpClient и TcpListener, работать с ними в .NET очень просто. Соединение через Bluetooth было подробно изучено в предыдущих работах.

Каждому соединению соответствует один поток. При создании соединения необходимо сначала создать объект нужного типа соединения, привязаться к его event и потом запустить, вызвав Start(). Унаследованные классы должны переопределить методы Send(), Receive() и желательно OnClose(). Первые два получают и отправляют сообщения, последний необходим для правильного освобождения ресурсов. Если клиент и сервер закрылись аварийно, соответствующее соединение автоматически закрывается.

Класс Connection у сервера содержит один event – сообщение о том, что список пользователей поменялся. Клиент же содержит следующие event: UsersListEvent – пришел список пользователей; MessageReceiveEvent – пришло сообщение; UsersChangedEvent – пользователь пришел/ушел; ServerQuitEvent – сервер закрылся.

Теперь рассмотрим реализацию самого сервера. Для соединения через Wi-Fi все просто – запускаем поток, который ожидает входящие соединения и, при их поступлении, создает новое соединение. Создание Bluetooth-сервера несколько отличается. Дело в том, что мы должны создать все сервера заранее, причина этому уже была описана. Поэтому мы сразу создаем нужное нам число соединений. А ConnectionThread() превращается в:

public class BluetoothConnection : Connection
{
    private BtPort _btPort;
 
    // ...

    protected override void ConnectionThread()
    {
        while (true)
        {
            _btPort.Listen();

            base.ConnectionThread();
        }
    }
}

Затронем еще один момент – получение списка компьютеров в локальной сети. В .NET это можно сделать, но это не очень удобно (см. ниже), поэтому приходится пользоваться обычными функциями Win32 API. Следующий код заполняет дерево TreeView именами компьютеров. Если нужен другой формат выходных данных, нужно ее немного переписать – в местах, отмеченным жирным шрифтом.

[DllImport("mpr.dll", CharSet=CharSet.Auto)]
public static extern int WNetEnumResource(IntPtr hEnum, ref int lpcCount, IntPtr lpBuffer, ref int lpBufferSize );

[DllImport("mpr.dll", CharSet=CharSet.Auto)]
public static extern int WNetOpenEnum(RESOURCE_SCOPE dwScope, RESOURCE_TYPE dwType, 
RESOURCE_USAGE dwUsage, [MarshalAs(UnmanagedType.AsAny)][In] Object lpNetResource, out IntPtr lphEnum);

[DllImport("mpr.dll", CharSet=CharSet.Auto)]
public static extern int WNetCloseEnum( IntPtr hEnum );
public enum RESOURCE_SCOPE
{
    RESOURCE_isConnected = 0x00000001,
    RESOURCE_GLOBALNET = 0x00000002,
    RESOURCE_REMEMBERED = 0x00000003,
    RESOURCE_RECENT= 0x00000004,
    RESOURCE_CONTEXT= 0x00000005
}
public enum RESOURCE_TYPE
{
    RESOURCETYPE_ANY= 0x00000000,
    RESOURCETYPE_DISK= 0x00000001,
    RESOURCETYPE_PRINT = 0x00000002,
    RESOURCETYPE_RESERVED = 0x00000008,
}
public enum RESOURCE_USAGE
{
    RESOURCEUSAGE_CONNECTABLE =0x00000001,
    RESOURCEUSAGE_CONTAINER=0x00000002,
    RESOURCEUSAGE_NOLOCALDEVICE =0x00000004,
    RESOURCEUSAGE_SIBLING=0x00000008,
    RESOURCEUSAGE_ATTACHED=0x00000010,
    RESOURCEUSAGE_ALL =(RESOURCEUSAGE_CONNECTABLE | 
    RESOURCEUSAGE_CONTAINER | RESOURCEUSAGE_ATTACHED),
}
public enum RESOURCE_DISPLAYTYPE
{
    RESOURCEDISPLAYTYPE_GENERIC= 0x00000000,
    RESOURCEDISPLAYTYPE_DOMAIN= 0x00000001,
    RESOURCEDISPLAYTYPE_SERVER= 0x00000002,
    RESOURCEDISPLAYTYPE_SHARE= 0x00000003,
    RESOURCEDISPLAYTYPE_FILE = 0x00000004,
    RESOURCEDISPLAYTYPE_GROUP= 0x00000005,
    RESOURCEDISPLAYTYPE_NETWORK= 0x00000006,
    RESOURCEDISPLAYTYPE_ROOT = 0x00000007,
    RESOURCEDISPLAYTYPE_SHAREADMIN = 0x00000008,
    RESOURCEDISPLAYTYPE_DIRECTORY = 0x00000009,
    RESOURCEDISPLAYTYPE_TREE = 0x0000000A,
    RESOURCEDISPLAYTYPE_NDSCONTAINER = 0x0000000B
}
public struct NETRESOURCE
{
    public RESOURCE_SCOPE dwScope;
    public RESOURCE_TYPE dwType;
    public RESOURCE_DISPLAYTYPE dwDisplayType;
    public RESOURCE_USAGE dwUsage;
    [MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPTStr)] 
    public string lpLocalName;
    [MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPTStr)] 
    public string lpRemoteName;
    [MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPTStr)] 
    public string lpComment;
    [MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPTStr)] 
    public string lpProvider;
}

private static void DiscoverComputersInternal(Object o, TreeNodeCollection tr)
{
    int iRet;
    IntPtr ptrHandle = new IntPtr();

    iRet = WNetOpenEnum(RESOURCE_SCOPE.RESOURCE_GLOBALNET, 
    RESOURCE_TYPE.RESOURCETYPE_ANY,RESOURCE_USAGE.RESOURCEUSAGE_ALL, o, out ptrHandle );

    if (iRet != 0)
    {
        return;
    }

    int entries;
    int buffer = 16384;
    IntPtr ptrBuffer = Marshal.AllocHGlobal(buffer);
    NETRESOURCE nr;

    while (true)
    {
        entries = -1;
        buffer = 16384;
        iRet = WNetEnumResource( ptrHandle, ref entries, ptrBuffer, ref buffer );

        if ((iRet != 0) || (entries < 1))
            break;

        Int32 ptr = ptrBuffer.ToInt32();

        for (int i = 0; i < entries; i++)
        {
            nr = (NETRESOURCE)Marshal.PtrToStructure(new IntPtr(ptr), typeof(NETRESOURCE));

            if (nr.dwDisplayType == RESOURCE_DISPLAYTYPE.RESOURCEDISPLAYTYPE_SERVER)
            {
                string serverName = nr.lpRemoteName.Remove(0, 2);

                TreeNode n = tr.Add(serverName);
                n.Tag = serverName;
            }
            else if (RESOURCE_USAGE.RESOURCEUSAGE_CONTAINER == 
            (nr.dwUsage & RESOURCE_USAGE.RESOURCEUSAGE_CONTAINER))
            {
                TreeNode n = tr.Add(nr.lpRemoteName);
                DiscoverComputersInternal(nr, n.Nodes);
            }

            ptr += Marshal.SizeOf(nr);
        }
    }
    Marshal.FreeHGlobal( ptrBuffer );
    iRet = WNetCloseEnum( ptrHandle );
}

Есть еще один вариант получения списка компьютеров – через ActiveDirectory. Получается просто, но очень медленно.

Реестр

Вот требования к клиенту по поводу реестра:

Сервисы

  • Просмотр ветвей
  • Просмотр ключей
  • Создание ветвей
  • Создание ключей
  • Удаление ветвей
  • Удаление ключей

User Interface

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

Доступ к Реестру

Для работы с реестром в .NET Framework потребуются классы Registry и RegistryKey. Находятся они в пространстве имен Microsoft.Win32. С помощью Registry можно получить требуемую информацию из реестра в объект класса RegistryKey и в дальнейшем использовать RegistryKey в работе..

RegistryKey rkey;

// открываем базовую ветвь LocalMachine
rkey = Registry.LocalMachine;
// аналогично с остальными базовыми ветвями
rkey = Registry.CurrentUser;
// и т.д.

// далее открываем ветвь
rkey = rkey.OpenSubKey(folder.ToString(), true);
// здесь true – означает разрешение на запись
// и работаем с ключами
string data = (string)rkey.GetValue(keyName);
rkey.SetValue(keyName, keyValue);
// если такого ключа нет, он создается
rkey.DeleteValue(keyName);

// с ветвями также
rkey.CreateSubKey(keyName); // это также открывает имеющуюся ветвь
rkey.DeleteSubKeyTree(keyName);

// получение имен подветвей и ключей в ветви
string[] data = rkey.GetSubKeyNames();
string[] data = rkey.GetValueNames();

Более подробно в MSDN.

Другой вариант использовать такую конструкцию:

public static RegistryKey OpenRemoteBaseKey(
   RegistryHive hKey,
   string machineName
);

RegistryHive – задает базовую ветвь (напр. Registry.LocalMachine). Но это будет работать при включенном режиме remote administration и при включенном режиме удаленного доступа к реестру и только через локальную сеть(Wi-Fi).

< Лекция 8 || Лекция 9: 12345