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

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

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

Чат через Bluetooth и Wi-Fi

Лабораторная работа №4. В данной работе необходимо написать полноценное приложение, работающее через Bluetooth, а именно небольшой чат. Кроме того, для сравнения работы через различные сети, в него также необходимо будет добавить соединение через Wi-Fi. Данный чат будет состоять из двух частей – сервера, запущенного на одном из компьютеров и поддерживающего оба типа соединения, и клиента, который будет подключаться к серверу либо через Wi-Fi, либо через Bluetooth. Сервер используется для рассылки сообщений всем клиентам, а также для обеспечения дополнительных сервисных функций. В результате мы получаем соединение типа "звезда", благодаря которому, например, клиенты, подключившиеся через Wi-Fi, смогут посылать сообщения клиентам, подключенным через Bluetooth.

При написании данной работы следует опираться на уже созданный пример такого чата – программу "Wi-CQ". Эта программа придается к описанию данной курсовой работы. В данном руководстве будут рассмотрены различные аспекты, реализованные в этом проекте.

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

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

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

Обе программы должны быть реализованы в виде графических .NET приложений.

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

Вы сами выбираете протокол, по которому будут взаимодействовать клиент и сервер. Для примера рассмотрим протокол, используемый в программе "Wi-CQ". Советуем вам использовать именно его.

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

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

Рассмотрим пример диаграммы взаимодействия клиента и сервера Рис. 9.1 :

Диаграмма взаимодействия клиента и сервера

Рис. 9.1. Диаграмма взаимодействия клиента и сервера

Опишем реализацию соединения по такому протоколу. Во-первых, лучше разделить логику, касающуюся создания и поддержания соединения от основной программы. Во-вторых, обработка команд не зависит от типа соединения. Поэтому удобно создать базовый класс 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 было подробно изучено в работах 1-3.

Каждому соединению соответствует один поток. При создании соединения необходимо сначала создать объект нужного типа соединения, привязаться к его через 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 );
}
< Лекция 8 || Лекция 9: 12345