Лабораторный практикум по технологиям Bluetooth и Wi-Fi
Чат через Bluetooth и Wi-Fi
Лабораторная работа №4. В данной работе необходимо написать полноценное приложение, работающее через Bluetooth, а именно небольшой чат. Кроме того, для сравнения работы через различные сети, в него также необходимо будет добавить соединение через Wi-Fi. Данный чат будет состоять из двух частей – сервера, запущенного на одном из компьютеров и поддерживающего оба типа соединения, и клиента, который будет подключаться к серверу либо через Wi-Fi, либо через Bluetooth. Сервер используется для рассылки сообщений всем клиентам, а также для обеспечения дополнительных сервисных функций. В результате мы получаем соединение типа "звезда", благодаря которому, например, клиенты, подключившиеся через Wi-Fi, смогут посылать сообщения клиентам, подключенным через Bluetooth.
При написании данной работы следует опираться на уже созданный пример такого чата – программу "Wi-CQ". Эта программа придается к описанию данной курсовой работы. В данном руководстве будут рассмотрены различные аспекты, реализованные в этом проекте.
Постановка задачи
Написать чат-клиент, который, в соответствии с определенным протоколом, взаимодействует с сервером и через него производит отправку сообщений другим клиентам. Клиент может соединяться с сервером либо через Wi-Fi, либо через Bluetooth.
Написать чат-сервер, который принимает подключения через Wi-Fi и Bluetooth и далее в соответствии с определенным протоколом принимает от клиентов сообщения и производит их рассылку.
Обе программы должны быть реализованы в виде графических .NET приложений.
Методические рекомендации
Вы сами выбираете протокол, по которому будут взаимодействовать клиент и сервер. Для примера рассмотрим протокол, используемый в программе "Wi-CQ". Советуем вам использовать именно его.
Команда | Описание |
---|---|
CONN|<имя> | Войти в чат под именем <имя>. |
CHAT|<от>|<сообщение> | Отправить сообщение всем клиентам. В поле <от> указывается имя данного клиента, а в поле <сообщение> – сам текст сообщения. Напомним, что по умолчанию длина пакета в протоколе RFCOMM – 127 байт. Так что советуем вам увеличить это значение (больше 1000 указать нельзя!) и поставить ограничение на длину текста в поле ввода сообщения. |
PRIV|<от>|<кому>|<сообщение> | Послать личное сообщение пользователю с именем <от>. Поля <кому> и <сообщение> идентичны используемым в предыдущей команде. |
GONE | Выйти из чата |
Команда | Описание |
---|---|
LIST|<список пользователей> | Послать клиенту текущий список пользователей. Поле от имеет формат <имя1>|<имя2>|...|<имяN> |
CHAT|<от>|<сообщение> | Отослать всем клиентам принятое сообщение. Заметим, что сообщение также отправляется и клиенту, от которого оно пришло. Это упрощает работу, т.к. не нужно фильтровать список пользователей при пересылке, а также клиенту не нужно дополнительным способом выводить свое сообщение на экран. |
PRIV|<от>|<кому>|<сообщение> | Отослать сообщение клиенту с именем <кому> |
JOIN|<имя> | Послать всем клиентам сообщение, о том, что в чат вошел новый пользователь с именем <имя> |
GONE|<имя> | Послать всем клиентам сообщение, о том, что пользователь с именем <имя> вышел из чата |
QUIT | Послать всем клиентам сообщение, о том, что сервер закрывается |
Во всех командах символ '|' лучше заменить на один из тех, которые пользователь не может ввести на клавиатуре. В противном случае его сообщение может обрезаться.
Рассмотрим пример диаграммы взаимодействия клиента и сервера Рис. 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 ); }