Форум программистов
 

Восстановите пароль или Зарегистрируйтесь на форуме, о проблемах и с заказом рекламы пишите сюда - alarforum@yandex.ru, проверяйте папку спам!

Вернуться   Форум программистов > C/C++ программирование > C/C++ Сетевое программирование
Регистрация

Восстановить пароль
Повторная активизация e-mail

Купить рекламу на форуме - 42 тыс руб за месяц

Ответ
 
Опции темы Поиск в этой теме
Старый 10.02.2011, 10:11   #1
Гром
Старожил
 
Аватар для Гром
 
Регистрация: 21.03.2009
Сообщений: 2,193
По умолчанию Общая структура/архитектура клиент-серверных приложений

Доброго времени суток!
Хотелось бы задать знающим людям несколько вопросов по поводу логической структуры и некоторых аспектов построения клиент-серверных приложений.
История вопроса
Решил я недавно изучить программирование сокетов Беркли, в частности, с применением Winsock. Теорию взял из статьи в разделе "Общие вопросы C/C++", а также из книги "C++ глазами хакера", плюс гугл и msdn. Начал с создания пары консольных приложений в MS VS 2008, благополучно добился взаимодействия по алгоритму "сервер начинает слушать порт" - "клиент подсоединяется к порту" - "клиент отсылает серверу сообщение" - "сервер принимает сообщение, пишет клиенту в ответ" - "клиент принимает ответ, отключается" - "сервер продолжает висеть пока не будет закрыт или пока клиент еще раз не постучится". Естественно, этого мне оказалось мало, я решил написать далее более-менее приличный чат. Поскольку отличительной чертой чата является возможность отправки любой стороной любого количества сообщений подряд не дожидаясь ответа, то вариант с консольным приложением оказывается слишком сложным, поскольку пока пользователь печатает сообщение, от собеседника может прийти еще парочка, и должны будут отобразиться, к тому же еще и перед недопечатанным сообщением. Реально, но слишком геморно для ситуации, когда этот аспект второстепенен.
Так что в результате я переполз на оконные приложения. Поскольку MFC я недолюбливаю, а с WinAPI знаком не настолько тесно (да и опять же - слишком много кода, не относящегося к изучаемой теме), то выбрал C++ Builder 6 (старый, да и тоже не без греха, но тем не менее). Ну и тут полезли очередные проблемы и вопросы.
Немного про структуру моего варианта клиент-сервера
Первое, что я сделал, убедившись в работоспособности всех этих функций типа bind, listen, send, recv и т.д. - это написал функции-обертки для основных функций работы с сокетами, которые проверяли бы успешность выполнения стандартных функций и в случае ошибки генерировали бы исключения с соответствующими сообщениями (класс Sockets::SockErr - прямой наследник std::runtime_error, даже не привносящий ничего нового). Затем, уже для варианта оконного приложения, написал (точнее, все еще пишу) классы клиента и сервера, которые в конструкторах и функциях-членах типа ConnectToServer осуществляют захват и инициализацию ресурсов (типа загрузки библиотеки Winsock, создания сокетов, привязки к портам и т.п.), а в деструкторах и функциях типа Disconnect - освобождение ресурсов. Создание сервера, и соединение с ним происходит по нажатию соответствующих кнопочек, отсоединение - по повторному нажатию.
Протокол передачи данных - самый примитивный. Сообщение представляет собой, как известно, цепочку байт (точнее, массив char, что в самом общем случае не есть одно и то же), первый из которых - это код команды. 1 - "клиент оповещает сервер о подключении к нему", 2 - "клиент оповещает сервер об отключении", 3 - "текстовое сообщение". Байты 2-5 - это по сути число типа int (по-хорошему, строго четырехбитовое), означающее длину текстового сообщения (имеет значение только для команды с кодом 3). Последующие необязательные байты - текстовое сообщение указанной длины (в случае команд 1 и 2 вообще ни одного дополнительного байта).
И наконец, собственно, вопросы:
1. Наиболее насущный - как правильно организовать возможность непрерывного получения сообщений с возможностью отправки в любой момент? Как возможный вариант - создавать для приема сообщений отдельный поток (thread), который будет внутри себя в цикле (вплоть до получения команды отключения) с помощью блокирующего сокета принимать сообщение, разбирает его по указанному протоколу, добавляет новую строку в поле чата; отправка сообщений происходит по событию нажатия на кнопку.
Альтернативный вариант - использовать неблокирующие сокеты, и тогда, как я понял, нужно будет использовать событие TApplication::OnMessage.
Собственно, ни один из этих вариантов еще не пробовал, потому подскажите, будут ли они работать, и:
а) Будет ли блокирующий сокет адекватно работать в своем потоке, пока работает основная программа (сильно подозреваю, что будет)?
б) Не начнется ли драка за разделяемые ресурсы при использовании потоков (а именно - порт, окно вывода сообщений, еще какие-нибудь)?
в) разное.
Простые и красивые программы - коды программ + учебник C++
Создание игры - взгляд изнутри - сайт проекта
Тема на форуме, посвященная ему же
Гром вне форума Ответить с цитированием
Старый 10.02.2011, 10:13   #2
Гром
Старожил
 
Аватар для Гром
 
Регистрация: 21.03.2009
Сообщений: 2,193
По умолчанию

2. Как правильно освобождать ресурсы? В частности, как проконтролировать корректное освобождение в случае ошибки только тех ресурсов, которые были захвачены? Мой вариант:
Код:
//Обертка над WSAStartup()
void Sockets::LoadWSA(WORD ver, WSAData* wsaData) throw (Sockets::SockErr);

//Отправка сообщения согласно описанному выше протоколу
void Chat::ChatClient::SendFormatedMsg(int CmdCode, int msglen, const char* msg) throw (Sockets::SockErr);

Chat::ChatClient::ChatClient() throw (Sockets::SockErr):
   ServerIP("127.0.0.1"), NServerPort(2345),
   Sock(0), WSALoaded(false), Connected(false)
   {
   Sockets::LoadWSA(MAKEWORD(2, 2), &wsaData);
   WSALoaded = true;
   Sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
   }
Chat::ChatClient::~ChatClient()
   {
   if (Connected)
      Disconnect();
   if (Sock)
      {
      shutdown(Sock, SD_BOTH);
      closesocket(Sock);
      }
void Chat::ChatClient::Disconnect() throw (Sockets::SockErr)
   {
   SendFormatedMsg(2, 0, "");
   shutdown(Sock, SD_BOTH);
   closesocket(Sock);
   Sock = 0;
   Connected = false;
   }
   if (WSALoaded)
      {
      WSACleanup();
      }
   }
У сервера - что-то аналогичное.
Не вполне уверен в корректности этого варианта, потому прошу меня просветить на этот счет.
3. На данный момент все это безобразие я могу тестировать только на локальном хосте, дальнейшей же экспансии мешает роутер, который гнусно сжирает предоставляемый провайдером белый айпишник и не дает моему компу самолично слушать порт. Насколько решабельна такая проблема? Что-то я краем уха слышал в интернете про проброску портов, но пока слабо себе ее представляю.

Пока вроде бы все, что вспомнил из интересующего меня.

P.S. И, да, первой теме в разделе - быть!
Простые и красивые программы - коды программ + учебник C++
Создание игры - взгляд изнутри - сайт проекта
Тема на форуме, посвященная ему же
Гром вне форума Ответить с цитированием
Старый 10.02.2011, 10:39   #3
veniside
Старожил
 
Регистрация: 03.01.2011
Сообщений: 2,508
По умолчанию

> создавать для приема сообщений отдельный поток (thread), который будет внутри себя в цикле (вплоть до получения команды отключения) с помощью блокирующего сокета принимать сообщение

вариант. Только перед recv() и прочими блокирующими вызовами желательно делать select(), иначе ваш поток прийдётся вырубать через TerminateThread() (ну или посылать самому себе 1 байт, чтобы разблокировать вызов). Обычно используют пул потоков, чтобы не создавать новый поток каждый раз при подключении нового клиента.

> Альтернативный вариант - использовать неблокирующие сокеты, и тогда, как я понял, нужно будет использовать событие TApplication::OnMessage.

Для высокопроизводительных серверов по-хорошему делают привязку OVERLAPPED вызовов WinSock2 с IOCP. Такая связка отлично масштабируется на любое количество ядер, установленных на машине (вариант выше мастабируется не так легко).


> а) Будет ли блокирующий сокет адекватно работать в своем потоке, пока работает основная программа (сильно подозреваю, что будет)?

а че ему не работать


> б) Не начнется ли драка за разделяемые ресурсы при использовании потоков (а именно - порт, окно вывода сообщений, еще какие-нибудь)?

зависит от рук программиста.


> и не дает моему компу самолично слушать порт

роутер никак не может помешать слушать порт. Только другая прога, открывшая этот порт до вас может этот сделать.


> Насколько решабельна такая проблема?

Решается элементарно, смотрите настройки роутера в разделе NAT/Firewall/SUA. Все, что ему нужно указать — это номер (или диапазон номеров) порта и IP адрес локальной машины, на которую отправлять все запросы, приходящие на этот порт.
"Когда приходит положенное время, человек перестаёт играть в пинбол. Только и всего."

Последний раз редактировалось veniside; 10.02.2011 в 14:11.
veniside вне форума Ответить с цитированием
Старый 10.02.2011, 14:13   #4
Пепел Феникса
Старожил
 
Аватар для Пепел Феникса
 
Регистрация: 28.01.2009
Сообщений: 21,000
По умолчанию

Цитата:
роутер никак не может помешать слушать порт.
легко может, если он не пропускает подключения извне.
Цитата:
б) Не начнется ли драка за разделяемые ресурсы при использовании потоков (а именно - порт, окно вывода сообщений, еще какие-нибудь)?
за какие ресурсы? клиентские сокеты?(они поидее обычно у потоков свои)
Цитата:
Мой вариант
сокеты закрываем когда они больше не нужны.
а WSACleanup делаем тогда когда больше не нужен WS, в принципе в самом конце приложения этого достаточно.
Хорошо поставленный вопрос это уже половина ответа. | Каков вопрос, таков ответ.
Программа делает то что написал программист, а не то что он хотел.
Функции/утилиты ждут в параметрах то что им надо, а не то что вы хотите.
Пепел Феникса вне форума Ответить с цитированием
Старый 10.02.2011, 14:17   #5
veniside
Старожил
 
Регистрация: 03.01.2011
Сообщений: 2,508
По умолчанию

> легко может, если он не пропускает подключения извне

не, ну понятно. Имелось в виду, что порт на машине будет открыт. А кто к нему будет иметь доступ (только локалка, или локалка и внешние соединения), это уже другой вопрос.
"Когда приходит положенное время, человек перестаёт играть в пинбол. Только и всего."
veniside вне форума Ответить с цитированием
Старый 11.02.2011, 13:07   #6
Гром
Старожил
 
Аватар для Гром
 
Регистрация: 21.03.2009
Сообщений: 2,193
По умолчанию

Цитата:
вариант. Только перед recv() и прочими блокирующими вызовами желательно делать select(), иначе ваш поток прийдётся вырубать через TerminateThread() (ну или посылать самому себе 1 байт, чтобы разблокировать вызов). Обычно используют пул потоков, чтобы не создавать новый поток каждый раз при подключении нового клиента.
Хорошо, покурю информацию на эту тему. В первоначальном варианте, правда, клиент предусмотрен всего один, и конечным его собеседником является сервер, а не другой клиент. Ну да это пока только упрощенный вариант.
Цитата:
а че ему не работать
Вот я тоже так думаю, но на всякий случай уточняю, чтоб знать точно.
Цитата:
Решается элементарно, смотрите настройки роутера в разделе NAT/Firewall/SUA. Все, что ему нужно указать — это номер (или диапазон номеров) порта и IP адрес локальной машины, на которую отправлять все запросы, приходящие на этот порт.
Спасибо, думаю, это то, что нужно. Посмотрю, попробую.
Цитата:
за какие ресурсы? клиентские сокеты?(они поидее обычно у потоков свои)
Под разделяемыми ресурсами я имею в виду сокеты, порты, компоненты типа RichEdit. Собственно, с внутренней работой сокетов и иже с ними я еще не до конца разобрался, и не знаю, как они работают внутри себя. К примеру, возможны ли одновременно прием и отправка данных через один и тот же клиентский сокет? Т.е., например, сервер посылает данные клиенту, и в этот же момент клиент присылает новую порцию данных с использованием того же сокета. Или же отправка и получение данных у них как-то неявно разнесены?
Ну а по поводу того же RichEdit - к примеру, пришло сообщение от сервера, клиент начинает его выводить в окошко RichEdit'а, и в этот же момент пользователь ввел в другое окно клиента сообщение и нажал кнопку "Отправить". Соответственно, в окошке должно одновременно появиться и это сообщение.
Чувствую, что у меня паранойя, но все-таки хочу уточнить.
Цитата:
сокеты закрываем когда они больше не нужны.
а WSACleanup делаем тогда когда больше не нужен WS, в принципе в самом конце приложения этого достаточно.
С этим, действительно, все понятно. Экземпляры классов сервера и клиента являются статическими глобальными членами в приложении, потому их конструкторы и деструкторы вызываются при запуске и выходе из приложения, дисконнект происходит либо при нажатии на кнопку, либо, если пользователь - дурак, в деструкторе.
Меня же больше интересует аварийное поведение программы. К примеру, если Winsock не была нормально загружена, в конструкторе будет сгенерировано исключение, объект клиента не будет создан, деструктор не будет вызван, WSACleanup не вызовется (и не надо). Собственно, не вполне уверен насчет того, нужно ли вызывать и shutdown, и closesocket равноправно в случаях, когда сокет был только создан при помощи socket(), и когда он был, к примеру, привязан к порту, или подсоединен к серверу.
Цитата:
не, ну понятно. Имелось в виду, что порт на машине будет открыт. А кто к нему будет иметь доступ (только локалка, или локалка и внешние соединения), это уже другой вопрос.
Собственно, это я и имел в виду По указанному айпишнику клиент находит роутер, который и не думал слушать указанный порт, а сервер за его могучей спиной усердно его слушает. С указанным выше решением буду разбираться)

Кстати, заметил тут, что я скопипастил код Disconnect в середину кода деструктора. Правильно оно выглядит так:
Код:
Chat::ChatClient::~ChatClient()
   {
   if (Connected)
      Disconnect();
   if (Sock)
      {
      shutdown(Sock, SD_BOTH);
      closesocket(Sock);
      }
   if (WSALoaded)
      {
      WSACleanup();
      }
   }

void Chat::ChatClient::Disconnect() throw (Sockets::SockErr)
   {
   SendFormatedMsg(2, 0, "");
   shutdown(Sock, SD_BOTH);
   closesocket(Sock);
   Sock = 0;
   Connected = false;
   }
Простые и красивые программы - коды программ + учебник C++
Создание игры - взгляд изнутри - сайт проекта
Тема на форуме, посвященная ему же
Гром вне форума Ответить с цитированием
Старый 11.02.2011, 17:18   #7
veniside
Старожил
 
Регистрация: 03.01.2011
Сообщений: 2,508
По умолчанию

Вобще, для небольшого числа клиентов и блокирующих сокетов с пулами потоков понаписано, я думаю, необозримое число примеров и библиотек, вот первый попавшийся. Тысячи их. Если аккуратно писать, за пару дней получается кусок кода, на котором удобно оттачивать свои знания по синхронизации потоков и построения различных механизмов нотификации событий. Тут главное вовремя остановиться, и не начать изобретать свой протокол )


> К примеру, возможны ли одновременно прием и отправка данных через один и тот же клиентский сокет

конечно, так общение с веб-сервером работает, например. Мы ему GET, а он нам файл )
"Когда приходит положенное время, человек перестаёт играть в пинбол. Только и всего."
veniside вне форума Ответить с цитированием
Старый 01.03.2011, 16:00   #8
Гром
Старожил
 
Аватар для Гром
 
Регистрация: 21.03.2009
Сообщений: 2,193
По умолчанию

Вот потихоньку вроде бы почти разобрался со всем этим делом, однако, есть одна небольшая проблема.
Вообще, общая концепция в итоге такая: сервер по нажатию кнопки "Создать подключение" создает серверный сокет, делает ему bind-listen и запускает поток, в котором сокет будет принимать (accept) подключения от клиентов (на настоящий момент - от одного-единственного). Поток в цикле проверяет, есть ли желающий подключиться клиент (через select с тайм-аутом), если есть - вызывает accept, создает поток, в котором будут приниматься данные от клиента. В этом потоке в свою очередь в цикле select'ом с тайм-аутом проверяется, пришли ли от клиента данные, и если да, то принимаются и соответствующим данным выводятся. Цикл с accept'ом длится, пока не будет передана информация о том, что соединение надо закрыть (нажатие кнопки -> вызов функции CloseConnection -> установка флага типа MTBool -> цикл, проверяющий значение флага, завершается); цикл с recv - до того же условия, или пока клиент не пришлет сообщение, что он отключается. По завершению циклов функции потоков завершаются.
MTBool - это флаг для безопасной работы через потоки. Что называется, проще показать, чем объяснять:
Код:
template <class T> class MTSafeData
   {
   public:
   MTSafeData(T val): Value(val)
      {
      InitializeCriticalSection(&CriticalSection);
      }
   ~MTSafeData()
      {
      DeleteCriticalSection(&CriticalSection);
      }
   void SetVal(T val)
      {
      EnterCriticalSection(&CriticalSection);
      Value = val;
      LeaveCriticalSection(&CriticalSection);
      }
   T GetVal()
      {
      T val;
      EnterCriticalSection(&CriticalSection);
      val = Value;;
      LeaveCriticalSection(&CriticalSection);
      return val;
      }
   private:
   MTSafeData& operator=(const MTSafeData&);
   CRITICAL_SECTION CriticalSection;
   T Value;
   };

typedef MTSafeData<bool> MTBool;
Однако, при всем при этом возникает следующая ошибка: в самом простом вариант - подключение благополучно создается, MessageBox успешно рапортует о создании потока для приема подключений, по нажатию кнопки вызывается функция CloseConnection, при этом цикл с accept благополучно завершается, MessageBox объявляет о закрытии потока, однако, прямо перед этим появляется MessageBox, объявляющий о создании потока приема сообщений от клиента. При этом вплоть до нажатия кнопки закрытия подключения все работает нормально - поток создается при подключении клиента, сообщения принимаются, по получении сообщения от клиента об отсоединении поток закрывается в штатном порядке.
Фрагменты кода:
Код:
DWORD WINAPI Chat::ChatServer::AcceptInThread(LPVOID param)   //Статическая функция потока с accept'ом
   {
   MessageBox(0, "Accept thread started.", "!!!", MB_OK);
   ChatServer* Serv = static_cast<ChatServer*>(param);
   sockaddr_in ClientAddr;
   int AddrSize = sizeof(ClientAddr);

   fd_set AcceptSock;
   timeval TimeOut;
   TimeOut.tv_sec = 0;
   TimeOut.tv_usec = 200;

   bool Conn = true;
   while (Conn)
      {
      FD_ZERO(&AcceptSock);
      FD_SET(Serv -> ServerSock, &AcceptSock);

      if (select(0, &AcceptSock, 0, 0, &TimeOut) > 0)
         {
         Serv -> ClientSock = accept(Serv -> ServerSock, (SOCKADDR*)&ClientAddr, &AddrSize);
         Serv -> ClientRecvThread = CreateThread(0, 0, RecvInThread, Serv, 0, 0);
         }

      Conn = Serv -> ConnCreated.GetVal();   //ConnCreated - флаг типа MTBool
      }
   MessageBox(0, "Accept thread ended.", "!!!", MB_OK);
   return 0;
   }


void Chat::ChatServer::CloseConnection()   //Функция закрытия подключения
   {
   ConnCreated.SetVal(false);
   WaitForSingleObject(ClientRecvThread, 300);
   WaitForSingleObject(ListeningThread, 300);
   shutdown(ClientSock, SD_BOTH);
   closesocket(ClientSock);
   ClientSock = 0;
   shutdown(ServerSock, SD_BOTH);
   closesocket(ServerSock);
   ServerSock = 0;
   }
Вполне возможно, я до сих пор не до конца что-то понимаю в работе с сокетами или потоками, или есть какие-то проектные недочеты. В этом случае прошу направить меня на путь истинный.
Простые и красивые программы - коды программ + учебник C++
Создание игры - взгляд изнутри - сайт проекта
Тема на форуме, посвященная ему же
Гром вне форума Ответить с цитированием
Старый 01.03.2011, 16:54   #9
veniside
Старожил
 
Регистрация: 03.01.2011
Сообщений: 2,508
По умолчанию

> прямо перед этим появляется MessageBox, объявляющий о создании потока приема сообщений от клиента

это который внутри RecvInThread()? Покажите тогда уже заодно и код RecvInThread().

Да, и при создании

Код:
Serv -> ClientSock = accept(Serv -> ServerSock, (SOCKADDR*)&ClientAddr, &AddrSize);
неплохо бы проверять, что ClientSock != INVALID_SOCKET.
"Когда приходит положенное время, человек перестаёт играть в пинбол. Только и всего."
veniside вне форума Ответить с цитированием
Старый 01.03.2011, 17:28   #10
Гром
Старожил
 
Аватар для Гром
 
Регистрация: 21.03.2009
Сообщений: 2,193
По умолчанию

Код RecvInThread:
Код:
DWORD WINAPI Chat::ChatServer::RecvInThread(LPVOID param)   //Поток приема данных от клиента.
   {   //param - указатель на экземпляр класса (функция должна быть static чтобы создавать поток, но при этом работать с данными экземпляра класса)
   MessageBox(0, "Recv thread started.", "!!!", MB_OK);
   ChatServer* Serv = static_cast<ChatServer*>(param);
   bool ClientConn = true;   //Флаг, показывающий, что работа с клиентом еще идет. Становится false при получении сообщения от клиента об отсоединении или после закрытия подключения сервера

   fd_set RecvSock;
   timeval TimeOut;
   TimeOut.tv_sec = 0;
   TimeOut.tv_usec = 200;
   while (ClientConn)    //Пока клиент не пришлет сообщение о завершении сеанса или пока не будет отключен сервер (через установку ConnCreated)
      {
      FD_ZERO(&RecvSock);
      FD_SET(Serv -> ClientSock, &RecvSock);

      if (select(0, &RecvSock, 0, 0, &TimeOut) > 0)   //В течение 200мс пытаемся принять данные, если не получается - проверяем флаг и заходим на новый круг
         {
         int CmdCode;   //Код команды
         int MsgLen;    //Длина сообщения
         char Msg[InMsgLen-4];    //Собственно сообщение

         EnterCriticalSection(&Serv -> InMsgCritSec);    //Закрываем другим потокам работу с InMsg

         try
            {
            Serv -> RecvFormatedMsg(CmdCode, MsgLen, Msg);
            Serv -> MTChatWindow -> AddMessage(CmdCode, Msg);
            if (CmdCode == 2)
               {
               ClientConn = false;
               }
            }
         catch (const Sockets::SockErr& err)
            {
            LeaveCriticalSection(&(Serv -> InMsgCritSec));
            MessageBox(0, err.what(), "Socket error!", MB_OK);
            return -1;
            }
         catch(...)
            {
            LeaveCriticalSection(&(Serv -> InMsgCritSec));
            MessageBox(0, "Unexpected error!", "Error!", MB_OK);
            return -1;
            }
         LeaveCriticalSection(&(Serv -> InMsgCritSec));
         }

      if (!Serv -> ConnCreated.GetVal())   //Если сервер был отключен, работа со всеми клиентами должна быть завершена
         {
         Serv -> SendFormatedMsg(2, 0, "");   //При этом клиенту отправляется сообщение о том, что соединение разорвано
         ClientConn = false;
         }
      }
   MessageBox(0, "Recv thread ended.", "!!!", MB_OK);
   return 0;
   }
void RecvFormatedMsg(int& CmdCode, int& msglen, char* msg) throw (Sockets::SockErr);
это функция, получающая через сокет массив байт и разделяющая впоследствии их на код команды, длину сообщения и собственно сообщение (как было написано ранее).
Хотя по идее этот код не должен влиять, т.к. проблему вызывает даже простое нажатие кнопок "Создать подключение"/"Закрыть подключение" (которые вообще-то есть всего одна кнопка с меняющейся надписью, проверяющая наличие подключения на данный момент).
Цитата:
неплохо бы проверять, что ClientSock != INVALID_SOCKET.
Согласен, равно как и в select'е, возможно, стоит проверять на завершение с ошибкой. Вскорости исправлю.
Простые и красивые программы - коды программ + учебник C++
Создание игры - взгляд изнутри - сайт проекта
Тема на форуме, посвященная ему же
Гром вне форума Ответить с цитированием
Ответ


Купить рекламу на форуме - 42 тыс руб за месяц

Опции темы Поиск в этой теме
Поиск в этой теме:

Расширенный поиск


Похожие темы
Тема Автор Раздел Ответов Последнее сообщение
Создание клиент/серверных БД ti_sweta Помощь студентам 23 09.11.2010 15:00
Есть общая структура программы,как написать функции к ней? Aleksandr_Yanov Общие вопросы C/C++ 0 13.06.2010 16:53
Ищу книгу Андрей Шкрыль "Разработка клиент-серверных приложений в Delphi" virus_t Свободное общение 9 11.08.2009 21:42
Вопрос по нагрузке клиент/серверных программ. stonix Работа с сетью в Delphi 2 23.12.2007 23:15