Пишем чат для локальной сети, используя C++ Builder. Серверная часть

от автора

Несколько месяцев назад понадобилось разработать чат для локальной сети одного офиса, а также выступить с этой программой на научной конференции. Делать его я решил в среде разработки Builder C++ 2006. При написании статьи у меня возникла одна самая главная проблема — полное отсутствие опыта в работе с сетями в билдере, поэтому статью пишу для таких же «программистов», как я. Отмечу сразу, в интернете найдется множество программ, которые, несомненно, будут лучше моей, но задание было не найти программу, а разработать. Статья получится большая, поэтому разделю ее на 2 части — серверную и клиентскую.

Первым делом надо было решить, что это будет за приложение.

Идея

Первое, что пришло в голову — открывать сервер на каждом компьютере, объединяя все компьютеры в кольцо. Сообщение передавать тому клиенту, к которому мы сейчас подключены. Там проверять, нам ли это сообщение, или нет; если нет — то отправлять дальше до тех пор, пока оно не достигнет адресата. Идея не понравилась, главным образом из-за того, что не хотелось, чтобы сообщение проходило через промежуточных пользователей.

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

Конечно же, должен присутствовать графический интерфейс, причем такой, чтобы работал по принципу «plug and play» — запустил программу и сразу можно приниматься за переписку. Поэтому в окне программы будет минимум компонентов, даже не будет меню бара.

Как будем пересыпать сообщения? Используя сокеты, а именно стандартные компоненты билдера ClientSocket и ServerSocket, которые будут использоваться в программах клиента и сервера соответственно.

Реализация

Сервер

Программа-сервер рассчитана на одноразовое использование. Т.е. при выходе из нее не сохраняется никакая информация о клиентах, в самой же программе все хранится в массиве. Вообще, сам интерфейс сокетов достаточно интересен. Для того, чтобы отправить сообщение клиенту, используется команда SendText, принимающая строку сообщения типа AnsiString (где-то я вычитал про длину в 4,3 миллиарда символов, что само собой впечатляет), но чтобы отправить его именно тому, кому нужно, следует указывать номер клиента, а не, например, его IP-адрес. При этом номера клиентам выдаются в том порядке, в каком они подключились. В .h файле объявлен массив m типа AnsiString, состоящий из 100 элементов. Честно говоря, я не проверил максимально возможное количество подключений к серверу, поэтому будем считать, что оно ограничивается только величиной этого массива. При подключении клиента первым делом отправляется его имя на сервер. Оно вносится в первую свободную ячейку массива, при этом номер элемента и будет являться номером клиента, по которому мы будем отправлять сообщения. Чтобы найти первую пустую ячейку, я написал функцию analog(), которая просто перебирает массив и возвращает номер пустой ячейки.

int TFormMain::analog() { int a; for(int i=0;i<mass;i++) { if(m[i]=="") { a=i; break; } } return a; } 

В событии OnConnect сервера запускается таймер. Почему-то код исполняемый таймером у меня не получилось выполнять сразу в событии, поэтому таймер выполняет его и сразу же отключается. По таймеру сервер с помощью цикла отправляет всем клиентам сообщение, в котором в одной строке содержатся все имена подключенных в данный момент клиентов. Зачем это сделано — я расскажу в описании клиента. Список же клиентов формирует функция online() «склеивающая» имена клиентов из массива.

Подключение клиентов

 void __fastcall TFormMain::ServerSocketClientConnect(TObject *Sender,       TCustomWinSocket *Socket) { Timer1->Enabled=true; } //---------------------------------------------------------------------------  void __fastcall TFormMain::Timer1Timer(TObject *Sender) {  if(ServerSocket->Socket->ActiveConnections!=0) for(int i=0;i<ServerSocket->Socket->ActiveConnections;i++) ServerSocket->Socket->Connections[i]->SendText("8714"+online()); Timer1->Enabled=false; } //---------------------------------------------------------------------------  

AnsiString TFormMain::online() { char str[500]=""; for(int i=0;i<analog();i++) { strcat(str,m[i].c_str()); strcat(str,","); } return str; } 

Однако, все самое интересное происходит в событии сервера OnRead. Структура каждого сообщения, посылаемого как клиентом, так и сервером, обязательно содержит в начале 4 цифры. Это случайная комбинация цифр, придуманная мной для того, чтобы сервер и клиент могли различать сообщения, необходимые для «авторизации», или же сообщения, содержащие в себе текст для пересылки. Всего клиент может посылать серверу 4 типа сообщений. Сообщения с кодом 6141 посылаются серверу при первом подключении, они также сообщают серверу имя нового клиента, а сервер вносит его в массив и выводит в Memo (декоративном элементе, созданном просто чтобы знать, кто в данный момент подключен). Сообщение с кодами 5280 и 5487 потеряли свою актуальность, но почему то не были убраны мной из кода сервера. Сообщения с кодом 3988 самые важные. Это и есть сообщения содержащие в себе всю информацию для обмена сообщениями между пользователями. Структура такого сообщения:

3988<Имя отправителя>%<Имя получателя>:<Текст сообщения>.

Вообще, из каждого полученного сообщения сервер первым делом выделяет код методом SubString, от этом в дальнейшем зависят его дальнейшие действия. Из этого же сообщения сервер также выделяет меня отправителя и получателя, а также текст сообщения. Затем формируется сообщение вида 7788<Имя отправителя>:<Текст сообщения>. Оно отправляется клиенту-получателю. Как, если известно только его номер а не имя? Для этого написана функция numer(AnsiString), принимающая имя, перебирающая массив и возвращающая номер ячейки в котором это имя находится.

Обработка входящих сообщений

void __fastcall TFormMain::ServerSocketClientRead(TObject *Sender,       TCustomWinSocket *Socket) { message=Socket->ReceiveText(); time=Now().CurrentDateTime(); if(message.SubString(1,4).AnsiCompare("6141")==0) { m[analog()]=message.SubString(5,message.Length()); ListBox1->Clear(); for(int i=0;i<ServerSocket->Socket->ActiveConnections;i++) { ListBox1->Items->Add(m[i]); } } else if(message.SubString(1,4).AnsiCompare("5487")==0) { for(int i=0;i<ServerSocket->Socket->ActiveConnections;i++) ServerSocket->Socket->Connections[i]->SendText("8714"+online()); } else if(message.SubString(1,4).AnsiCompare("3988")==0) { nametowho=message.SubString(message.AnsiPos('Й')+1,message.AnsiPos(':')-message.AnsiPos('Й')-1); name=message.SubString(5,message.AnsiPos('Й')-5); if(nametowho.IsEmpty()==false && (message.SubString(message.AnsiPos(':')+1,message.Length()).IsEmpty())==false) { ServerSocket->Socket->Connections[numer(nametowho)]->SendText("7788"+name+":"+message.SubString(message.AnsiPos(':')+1,message.Length())); ofstream fout("chat.txt",ios::app); fout<<time.c_str()<<"   "<<message.c_str()<<endl; fout.close(); } } else if(message.SubString(1,4).AnsiCompare("5280")==0) {  ServerSocket->Socket->Connections[numer(message.SubString(message.Pos('#')+1,message.Pos('%')-message.Pos('#')-1))]->SendText( "6734"+message.SubString(message.Pos('%')+1,message.Length()-message.Pos('%'))); } } 

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

void __fastcall TFormMain::ServerSocketClientDisconnect(TObject *Sender,       TCustomWinSocket *Socket) { if(ServerSocket->Socket->ActiveConnections!=0) { for(int i=0;i<mass;i++) { m[i]=""; } TestNames();  Timer1->Enabled=true;  } } 

Графическая часть

Внешний вид окна сервера:

Изначально у меня не было в планах выводить какую либо информацию в окне сервера, но в итоге решил выводить самое важное: IP-адрес сервера, количество активных подключений, порт сервера, имя компьютера ка котором он запущен (почему-то всегда «1», исправить я это пока не смог), и список имен подключенных клиентов. Сервер сворачивается в область уведомлений. Реализовано это несколькими функциями, подробно разбирать я их не буду. Также на сервере полностью отключен отлов ошибок (которых вообще за 2 недели непрерывной работы не возникало, но мало ли, все таки полностью парализуется работа сети).

Графическая часть

 void __fastcall TFormMain::DrawItem(TMessage& Msg) {      IconDrawItem((LPDRAWITEMSTRUCT)Msg.LParam);      TForm::Dispatch(&Msg); } //--------------------------------------------------------------------------- void __fastcall TFormMain::MyNotify(TMessage& Msg) {     POINT MousePos;      switch(Msg.LParam)     {         case WM_RBUTTONUP:             if (GetCursorPos(&MousePos))             {                 PopupMenu1->PopupComponent = FormMain;                 SetForegroundWindow(Handle);                 PopupMenu1->Popup(MousePos.x, MousePos.y);             }             else                 Show();             break;         case WM_LBUTTONDBLCLK:         Show();          break;         default:             break;     }     TForm::Dispatch(&Msg); } //---------------------------------------------------------------------------  //--------------------------------------------------------------------------- bool __fastcall TFormMain::TrayMessage(DWORD dwMessage) {    NOTIFYICONDATA tnd;    PSTR pszTip;     pszTip = TipText();     tnd.cbSize          = sizeof(NOTIFYICONDATA);    tnd.hWnd            = Handle;    tnd.uID             = IDC_MYICON;    tnd.uFlags          = NIF_MESSAGE | NIF_ICON | NIF_TIP;    tnd.uCallbackMessage	= MYWM_NOTIFY;     if (dwMessage == NIM_MODIFY)     {         tnd.hIcon		= (HICON)IconHandle();         if (pszTip)            lstrcpyn(tnd.szTip, pszTip, sizeof(tnd.szTip)); 	    else         tnd.szTip[0] = '\0';     }    else     {         tnd.hIcon = NULL;         tnd.szTip[0] = '\0';     }     return (Shell_NotifyIcon(dwMessage, &tnd)); } //--------------------------------------------------------------------------- HICON __fastcall TFormMain::IconHandle(void) { return (Image2->Picture->Icon->Handle); }  //--------------------------------------------------------------------------- PSTR __fastcall TFormMain::TipText(void) {         return ("Office Chat");  } //--------------------------------------------------------------------------- LRESULT IconDrawItem(LPDRAWITEMSTRUCT lpdi) { return 0; } //---------------------------------------------------------------------------  //---------------------------------------------------------------------------   void __fastcall TFormMain::FormDestroy(TObject *Sender) { 	TrayMessage(NIM_DELETE); } //---------------------------------------------------------------------------  void __fastcall TFormMain::N1Click(TObject *Sender) { Show(); } //---------------------------------------------------------------------------  void __fastcall TFormMain::N2Click(TObject *Sender) { Application->Terminate(); } //---------------------------------------------------------------------------  void __fastcall TFormMain::FormCloseQuery(TObject *Sender, bool &CanClose) { CanClose=false; FormMain->Hide(); } //---------------------------------------------------------------------------    void __fastcall TFormMain::FormCreate(TObject *Sender) { unsigned long Size = 256;    char *Buffer = new char[Size];  Label5->Caption=GetUserName(Buffer, &Size); delete [] Buffer; } //---------------------------------------------------------------------------  

В заключении хочу сказать, что получилось достаточно примитивно написаный, однако стабильно работающий сервер, позволяющий одновременно переписываться 20 людям (больше я просто не проверял). Все исходники, exe-файлы и полный разбор кода клиента будут во второй статье.

Спасибо за внимание.

ссылка на оригинал статьи http://habrahabr.ru/post/259641/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *