Первым делом надо было решить, что это будет за приложение.
Идея
Первое, что пришло в голову — открывать сервер на каждом компьютере, объединяя все компьютеры в кольцо. Сообщение передавать тому клиенту, к которому мы сейчас подключены. Там проверять, нам ли это сообщение, или нет; если нет — то отправлять дальше до тех пор, пока оно не достигнет адресата. Идея не понравилась, главным образом из-за того, что не хотелось, чтобы сообщение проходило через промежуточных пользователей.
Вторая идея мне понравилась. Пусть это будет клиент-серверное приложение. Открываем на одном компьютере сервер, все клиенты подключаются к нему. С клиента отправляем сообщение на сервер, а он уже перенаправляет его адресату. Кстати говоря, второе преимущество этой сетевой архитектуры состоит в том, что каждому клиенту нужно знать только один 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-файлы и полный разбор кода клиента будут во второй статье.
Спасибо за внимание.
Комментарии (36)
agmt
05.06.2015 14:29+5Прочитал статью и сразу трава стала зеленее, деревья выше. И мысли какие-то шальные: что делать, куда поступать?
impwx
05.06.2015 14:47+16Статья задержалась на 15 лет.
toxicdream
05.06.2015 15:44Ну если прикрутить LDAP, аудио-видео-звонки, уведомление о новых письмах в MS Exchenge + события в Календаре, то можно и сейчас взлететь.
speakingfish
05.06.2015 16:15+3GUI-сервер с таймерами вместо потоков, отсутствие синхронизации с GUI, отсутствие обработки ошибок (объясняемое фразой, что за две недели они не возникали), вкрапления null-terminated строк…
Ностальгия.altanium Автор
05.06.2015 17:25-5Обработка ошибок выключена просто на всякий случай. Их было неимоверное количество, просто от всех удалось избавиться( в основном они возникали из за отправления пустых строк или из-за отключения какого либо клиента). Я наверное не совсем выразился насчет отключение отлова ошибок, я убрал часть кода выводящие их на экран, на самом то деле они отлавливаются, просто окно ошибки не стопорит работу сервера
altanium Автор
05.06.2015 17:12-4Статью пишу скорее для таких же 10-классников как я просто потому, что когда я это делал (а я вам скажу это программа достаточно высокого уровня на той олимпиаде где я с ней выступал), сам очень и очень нуждался в такой статье. Для школьной программы я вообще бог программирования :-), а все таки чтобы дорасти до высокого уровня с чего-то надо начать. Даже с того же чата на билдере.
midday
05.06.2015 17:25+1=) ну вы не бог. Не обольщайтесь. Есть куча школьников, которые в этом возрасте уже работают, но это их проблемы.
А так — браво. Но зачем в школьном возрасте сразу выбирать С++? Понимаю конечно, что молодой, хочется поэкспериментировать… но =) выбор одобряю.altanium Автор
05.06.2015 17:28+1Я уже год как работаю)) В школьном курсе вообще ненавистный мной паскаль, но с++ я начал изучать за 2 или 3 года до начала информатики в школе( мне лет 12 кажется было). А у нас в основном учатся те, кому паскаль до одного места, поэтому когда они видят даже ту же строку #include уже сразу начинаются преклонения.
midday
05.06.2015 17:44+2Рановато вы на хабру статьи писать начали. Вот в собственном блоге — пожалуйста. Заклюют щас =)
Ну и дело не в билдере. Просто это действительно не уровень хабра. А приглашение для чтения, комментирования, просьб. Вы в том возрасте, где хабр дает вам, а вы пока не можете ему предложить. Успехов!altanium Автор
05.06.2015 17:45-1Ну и пусть. Я вижу что рейтинг статьи падает, но может статья будет кому нибудь полезно
Error1024
05.06.2015 20:36+1Скажу вам секрет… C++ Builder IDE, все компоненты и формочки на ненавистном вам Object Pascal написаны, только тссс…
Gorthauer87
05.06.2015 18:03+4Лучше уж тогда более современный и понятный Qt взять, а не дремучий, проприетарный билдер.
altanium Автор
05.06.2015 18:05-1Пробовал. Понял, что еще не дорос. Просто банально не понимаю что делать. А здесь уже хотя бы пару лет опыта есть.
Gorthauer87
05.06.2015 18:08+2Там же вагон примеров, в том числе и локальный чат.
Есть ещё куча простых языков типа Go или Python.altanium Автор
05.06.2015 18:20-2C языком проблем совсем нет, я даже не понимаю иногда людей которые спрашивают чего выбрал такой язык сложный. А вот освоить QT, где программа ну прямо совсем космически для меня выглядит я не смог
IRainman
05.06.2015 18:55А вот освоить QT, где программа ну прямо совсем космически для меня выглядит я не смог
Вот об этом, фактически, и говорит Gorthauer87 во фразе:
Есть ещё куча простых языков типа Go или Python.
P.S. я и сам разработкой, в подавляющем большинстве случаев, на C++ занимаюсь, но важно понимать, что голого C++ очень мало, для минимума нужно очень хорошо понимать и владеть STL и, почти безальтернативно, Boost (считайте, без шуток, что это STL-ext :) ).
Ну а Qt это Qt, есть даже шутка на тему того, что надо писать либо на C++ либо на Qt.
t0ch1k
05.06.2015 19:12+1Просто Вы еще не сталкивались с действительно сложными приложениями. Извините, но делать выводы о сложности языка по однопоточному приложению из пары файлов по 3,5 процедуры в каждом несколько рановато
altanium Автор
05.06.2015 19:25-1Я не совсем из этого делаю выводы, мой уровень знаний пока что позволяет мне выполнять поставленные задачи, а приложение все таки свою цель выполняет, и очень даже неплохо. Хотя, опять же, я согласен со всеми комментариями и с тем что найти в интернете можно программы в 1000 раз лучше моей. Но меня просили не об этом
t0ch1k
05.06.2015 19:40Я же не говорил, что Ваша программа плохая :) Сам начинал с подобного и тоже в C++ Builder. Для образовательных целей вполне себе полезная программка. Мой коммент скорее к тому, что не стоит зацикливаться на C++, потому что пока не возникло никаких проблем. Попробуйте, например, что-нибудь из Lisp'ов. Как минимум для расширения кругозора будет полезно
altanium Автор
05.06.2015 19:44-1Обязательно попробую что нибудь еще. Больше всего Java интересует, я вот думаю насчет этого стоит ли писать что нибудь про arduino и netbeans. Если конкретнее то системы автопилотирования вертолетов на радиоуправлении.
A1ien
06.06.2015 13:09+2Java — это не образовательный язык, а продакшин, в нем нет ни каких особенностей которые дают возможность позноавать архитектурные и языковые особенности построения приложений(разве что веобъемлющего полиморфизма и разделения на интерфейсы), тогда как тот же Lisp вас погрузит с головой в функциональное программирование, Python JS, даст вам возмоность проникнуться динамическими языками с утиной типизацией, C# — мультипарадигменный — тоесть помесь касического ООП, функцианального программирования, и динамических языков а так же хороший пример построения архитектуры библиотеки, я бы сказал что C# во многом провоцирует на красивые архитектурные рашения. Ну а С++ — это все в одном, и процедурный язык, и ООП, и функуиональный (с++11, boost bind), и кодогенерация на шаблонах, и еще куча куча всего.
ploop
06.06.2015 00:57+1А вот освоить QT, где программа ну прямо совсем космически для меня выглядит я не смог
Да ладно, что же там космического? В первом приближении ничем не отличается от Builder'ов, расставляй кнопочки и пиши код, в котором можно использовать хоть свои велосипеды, хоть функционал огромнейшей библиотеки. Это уже потом приходит понимание, что Qt это нечто иное, чем простой C++ фреймворк, и меняет твой подход к разработке.
Зато реализовать относительно сложный проект, например многопоточный сервер, работающий с БД, сетью, сериализацией данных, да при том под нагрузкой, можно в краткие сроки, получая удовольствие, а не зарабатывая геморрой.
Ivan_83
06.06.2015 12:58Да, ностальгия %)
Мои первые чаты были на Visual Basic.
Самое первое это была «терминалка» — простое приложение которое могло подключатся на указанный IP:PORT или принимать подключения и отправлять и принимать текст. Чисто для обкатки и понимания как оно вообще работает.
Штука получилась настолько удобная и полезная что я её на бейсике переписывал пару раз, а когда бейсик совсем умер то переписал на сях: www.netlab.linkpc.net/wiki/ru:software:win:net:tcp_term
(теперь, чувствую ещё раз переписывать буду, но уже под GTK :) )
Отдельная тема то что было для WinPopUp — MailSlot хреновина, свой клиент я тоже налабал, как и версию чата на этих самых слотах.
Сервер чата — это в принципе не плохо, но всегда была проблема с его поиском в локалке.
Вбивать руками — не очень то удобно.
Первые мои клиенты искали так: получали список всех компов в сетевом окружении и тыркались к каждому из них.
Потом я прочитал умные книжки, знания немного структурировались в голове и я сделал поиск чат сервера через мультикаст:
— при старте сервер создаёт UDP сокет, биндит на определённый порт и присоединяет его к определённой мультикаст группе (например 225.0.0.7)
— клиент во время поиска сервера отправлять UDP пакет на указанный выше адрес мультикаст группы и порт
— сервер получает пакет и отвечает по UDP клиенту напрямую, сообщая своё название, сколько там народу и вообще что в голову придёт, например открытые порты
Сейчас, я бы писал свой чат в виде плагина к Miranda NG — там шикарный гуй на любой вкус и куча всяких функциональных плагинов, цена за вход — всего то дописать протокольный плагин. (не люблю я гуй писать: работы много, эффекта мало).
Для локалки я бы писал без серверный вариант на мультикасте:
— отдельный мультикаст адрес который слушают все — для списка контактов: раз в минуту все туда анонсят свой статус
— чат комнаты — каждая на отдельном мультикаст адресе
— приватные коммуникации — напрямую по TCP/UDP
Можно прикрутить крипту: каждый генерит пару ключей для ECDSA (у меня оно просто есть готовое, но Curve25513 даже лучше), публичный ключ = идент человека и анонсится вместе с ником
При приватный коммуникациях вычисляется ECDH и используется как ключ, например к chacha20 или aes.
Чаткомнаты тоже можно делать закрытыми, но придётся каждому присылать приватный ключ.
Если всё грамотно сделать то оно потом сможет и через сервера работать, после незначительных модификаций, и через интернет по DHT искать контакты, короче не хуже TOX будет.
Если кодите с использованием TCP — так и знайте, TCP это поток, если вы в одном месте отправили за раз «сообщение» в 10кб то у себя вы его можете получить кусочками скажем по 1кб, за несколько вызовов recv().
Только UDP и прочие дейтаграмные вещи прилетают сразу целыми сообщениями на выходе recv().GamePad64
06.06.2015 20:33Есть ещё весьма вкусный SCTP, который работает аналогично TCP, но при этом сохраняет границы сообщений подобно UDP.
barker
О, вот это ностальгия))
Чат на сокетах в билдере/дельфи — кто ж его не писал в то время, эх…
thunderspb
дааа, было дело, когдато давно делали систему управления комп клубом, с сервером, админкой и клиентской частью, где только иконки игр выводились…