Лабораторный практикум по технологиям 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 );
}
