На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
  
    > как определить что select вернул сокеты с дисконнектом
      Всем здравствуйте !
      такая вот проблемка .. есть сервер который принимает клиентские подключения и записывает их все в общую
      fd_set ну и слушает когда поступит подключение или прием данных через, соответственно, select ..
      так вот .. если ктото из клиентов сделал у себя closesocket то функция select так же как и приеме данных вернет буфер с откликнувшимися сокетами
      ... может кто знает ... вот как определить именно в том месте где select вернула управление что сокет не данные прислал а именно отключился,
      без использования recv .. дело в том что чтение данных уже проводится в потоках, а хотелось бы определить отключенный сокет еще до того как он
      отправился на обработку в поток ... есть ли вообще такая возможность?
        ExpandedWrap disabled
          if(FD_ISSET(sock, &fde)){
          ....
          }

        а вообще в поиск по select + FD_ISSET
        Сообщение отредактировано: popsa -
          FD_ISSET в любом случае вернет истину ... ведь сокет в массиве и так есть !!!
          ну вот например так : (реальный код не могу выложить потому что он слишком громоздкий, много будет не по теме )

          ExpandedWrap disabled
            fd_set g__arr;
             
             
            void Init ()
            {
                
                SOCKET sock = // ... создаем сокет в режиме listen на какойто порт
                FD_SET( sock, &g__arr );
             
            }
             
            void Wait ()
            {
             
               while( 1 )
               {
             
                  fd_set pWait;
                  memcpy( &pWait, &g__arr, sizeof(fd_set) );
             
               // вот тут ждем вечно
                  int ret = select( &pWait, 0,0,0 );
               // как только чтото пришло
               // вот тут например и обрабатываем все что нам пришло на порты которые слушаются
             
                  for( int i = 0 ; i < pWait.fd_count ; i ++ )
                  {
             
               // если это попытка соедениться то делаем :
                       SOCKET cl = accept( m_buf.fd_array[i], ... );
                       FD_SET( cl, &g__arr );
               // если это был клиентский сокет на который должны были прийти какието данные ...
               // то читаем данные и обрабатывает
               // но если ктото из клиентских сокетов сделал closesocket то select вернет в pWait массив сокеты
               // которые сделали closesocket и ret будет равен количеству элементов в pWait  ....
               //   и вот как тут вот узнать что клиент именно разорвал соединение а не передает данные ????
              
                  }
             
               }
            }
             
            void main ()
            {
                 Init();
                 Wait();
             
            }
            Какая ОС?
            Если Windows - то функциональность select() недостаточна - и сетевое I/O не сможет эффективно обслуживаться.
            FD_ISSET - это и все, что можно сделать.
            Для Виндов надо использовать WSAAsyncSelect или WSAEventSelect
            Для Линуха - типа poll()
            2.5.10. Функция poll()
            poll() является вариантом select(). Задается массив из nfds структур типа
            ExpandedWrap disabled
               struct pollfd
                  {
                              int fd;           /* описатель файла */
                              short events;     /* запрошенные события */
                              short revents;    /* возвращенные события */
                      };

            и значения timeout в миллисекундах. Отрицательное значение указывает на бесконечный таймер. Поле fd содержит описатель открытого файла. Поле events - это входной параметр, указывающий на битовую маску событий, важных для приложения. Поле revents - это возвращаемый параметр, в который ядро помещает информацию о произошедших событиях, запрошенных или типа POLLERR, POLLHUP или POLLNVAL. (Эти три битовых флага не будут иметь смысла при использовании в поле events, поэтому будут установлены в поле revents, если соответствующее условие истинно). Если ни одно из запрошенных событий не случилось или не произошла ни одна из ошибок, то ядро ждет их появления до истечения срока таймера. Вот возможные биты, описанные в <sys/poll.h>:
            События ввода/вывода
            Событие Флаг poll() Когда происходит
            Чтение POLLIN Пришли новые данные.
            Чтение POLLIN Установка соединения завершена (для сокетов, ориентированных на соединения)
            Чтение POLLHUP Другая сторона инициировала запрос на разъединение.
            Чтение POLLHUP Соединение разорвано (только для протоколов, ориентированных на соединение). Если производится запись в сокет, то также посылается сигнал SIGPIPE.
            Запись POLLOUT Сокет имеет достаточно места в буфере передачи для записи в него новых данных.
            Чтение/запись POLLIN|POLLOUT Исходящий connect() завершен.
            Чтение/запись POLLERR Произошла асинхронная ошибка.
            Чтение/запись POLLHUP Другая сторона закрыла (shutdown) одно направление.
            Исключение POLLPRI Пришли неотложные данные. При этом посылается сигнал SIGURG.

            Здесь точно можно найти сетевое событие, которое вызвало сигнал.....
              к сожалению (как выясняется) пишу под windows!
              честно говоря не думал что настолько большая разница..
              но я вот думал что раз WSAAsyncSelect или WSAEventSelect умеют определять такие события то и без
              их помощи можно это определить ведь они, как мне кажется, являются оболочкой на winsock'овскими функциями типа select ...
              программа уже написана без использования WSA а тут такой глюк вылез ... а чтобы на асинхронные сокеты перейти много переписывать !!
                Цитата
                FD_ISSET в любом случае вернет истину ... ведь сокет в массиве и так есть !!!

                ExpandedWrap disabled
                  int ret = select( &pWait, 0,0,0 );

                :lool: при таком использовании селекта, да.

                судя по куску кода, ты имеешь очень смутное представление о функции select и о том как с ней работать

                Вот маленький кусок кода из работающей программы
                и не забудь почитать хотя бы описание используемых тобой функций
                ExpandedWrap disabled
                  while(connected)
                  {
                       fd_set fdr, fde;
                       FD_ZERO(&fdr);
                       FD_ZERO(&fde);
                       FD_SET(gw_socket, &fdr);
                       FD_SET(gw_socket, &fde);    
                       int res = select(gw_socket + 1, &fdr, 0, &fde, 0);
                       if (res > 0)
                       {
                            if (FD_ISSET(gw_socket, &fdr))
                            {
                                  //recv                      
                            }          
                   
                            if (FD_ISSET(gw_socket, &fde))
                            {
                                 //error - disconnect;                
                            }
                   
                       }
                  }
                  Идея распихать по нескольким наборам - она в принципе работает.
                  //error - disconnect;
                  Но вот для определения ошибки на Error-сокете надо использовать getsockopt
                  например:
                  · Ошибка сокета, ожидающая обработки. Операция записи в сокет не блокируется и возвратит ошибку (-1) со значением переменной еrrnо, указывающей на конкретное условие ошибки. Эти ошибки, ожидающие обработки, можно также получить и сбросить, вызвав функцию getsockopt() с параметром сокета SO_ERROR.

                  Но это для UNIX-систем. Там есть errno.
                  Как для винды?????????
                    ;) По мне
                    ExpandedWrap disabled
                      if (FD_ISSET(gw_socket, &fde))
                    уже зватает чтобы закрывать соединение
                    А про винду + getsockopt + SO_ERROR, ничего толкового не скажу, но когда то переносил код из соляриса(там как раз по наличию ошибки(SO_ERROR) отвал делался), то в винде это уже не работало, ибо getsockopt возвращал в коде ошибки 0, даже когда на другой стороне уже закрыли соединение.
                      Цитата popsa @
                      По мне
                      if (FD_ISSET(gw_socket, &fde))
                      уже зватает чтобы закрывать соединение

                      Логично.
                      Т.е достаточно - но не необходимо. Теоретически может быть и иная причина помещения в набор....
                      Но - как я уже выше согласился - в первом приближении это работает
                        чтото не работает ни один ни другой способ ... ни с использованием exceptfds в select'е
                        ни getsockopt + SO_ERROR ... я их кстати пробовал уже и при разрыве соединения в exceptfds всегда оставался пустым и соответственно
                        способ - if (FD_ISSET(gw_socket, &fde)) не прокатил бы ... может при создании сокета какието опции нужно выставить ?
                        я нашел другой способ ... но только для проверки сокетов которые ожидают данные:

                        ExpandedWrap disabled
                              // ...
                              // после того как select вернул управление
                           
                              int isListen;
                           
                              // проверяем не слушающий ли сокет
                              getsockopt( cl_socket, SOL_SOCKET, SO_ACCEPTCONN, (char*)&isListen, sizeof(isListen);
                              if( isListen == 0 )
                              {
                                  // проверяем состояние буфера чтения
                                  int nCountBytes;
                                  ioctlsocket( cl_socket, FIONREAD, (u_long*)&nCountBytes );
                                  if( nCountBytes == 0 )
                                  {
                                      // если попали сюда то сокет наверное отсоеденился
                                      // это всеравно что при выполнении recv будет возвращен 0
                                  }
                           
                              }



                        сколько не смотрел тем на форумах или исходников уже рабочих программ в основном все проверяют состояние сокета ожидающего прием данных
                        именно попыткой чтения из него ну и если recv возвращает 0 то значит клиент закрыл соединение.
                        вот подскажите , опытные люди, корректен ли этот способ или тут есть какието подводные камни ?

                        по мне так этот способ будет даже относительно шустрее работать чем предложенный if (FD_ISSET(gw_socket, &fde)) ..
                        Сообщение отредактировано: hash_2000 -
                          Цитата
                          чтото не работает ни один ни другой способ ... ни с использованием exceptfds в select'е

                          показывай полный код ;) . У всех работает у вас нет, что то неправильно значит
                          Цитата
                          ни getsockopt + SO_ERROR

                          :) про это я тоже писал, что в винде не получилось при обрыве получить ошибку таким способом

                          :) и вабще я немного логику не понимаю, если в селект переадються все fd, то ведь однозначно можно определить, почему селект вернул управление. В общем давай полный код.
                            ну вот пожалуйста ... примерно так , проверяем ващ способ :

                            ExpandedWrap disabled
                              // это все сервер
                               
                              #include <winsock2.h>
                              #pragma comment(lib, "wsock32.lib")
                               
                               
                               
                               
                               
                              SOCKET sv_sock = INVALID_SOCKET;
                              fd_set fd_buf;
                               
                               
                              void Close( BOOL bForce = FALSE )
                              {
                                  if( ( sv_sock == INVALID_SOCKET || sv_sock == SOCKET_ERROR ) ||
                                      bForce ) {
                                      closesocket ( sv_sock );
                                  }
                              }
                               
                               
                              int _tmain(int argc, _TCHAR* argv[])
                              {
                                  WSADATA w_data;
                                  WSAStartup( MAKEWORD( 2, 0 ), &w_data );
                               
                               
                                  sv_sock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
                                  if( sv_sock == SOCKET_ERROR ) {
                                      return 0;
                                  }
                               
                                  int sock_value = 1;
                                  if ( setsockopt( sv_sock, SOL_SOCKET, SO_REUSEADDR,(char*)&sock_value, sizeof(sock_value)) == SOCKET_ERROR ) {
                                      Close();
                                      return 0;
                                  }
                                  if ( setsockopt( sv_sock, IPPROTO_TCP, TCP_NODELAY,(char*)&sock_value, sizeof(sock_value)) == SOCKET_ERROR ) {
                                      Close();
                                      return 0;
                                  }
                                  if( ioctlsocket( sv_sock, FIONBIO, (u_long *) &sock_value ) == SOCKET_ERROR ) {
                                      Close();
                                      return 0;
                                  }
                               
                                  sockaddr_in svaddr;
                                  svaddr.sin_family = AF_INET;
                                  svaddr.sin_port = htons(15555);
                                  svaddr.sin_addr.s_addr = ADDR_ANY;
                               
                                  if( bind( sv_sock, (LPSOCKADDR)&svaddr, sizeof(SOCKADDR) ) == SOCKET_ERROR ) {
                                      Close();
                                      return 0;
                                  }
                                  if ( listen( sv_sock, FD_SETSIZE ) == SOCKET_ERROR ) {
                                      Close();
                                      return 0;
                                  }
                                  
                               
                                  FD_ZERO( &fd_buf );
                                  FD_SET( sv_sock, &fd_buf );
                               
                                  char radbuf[ 16 * 1024 ];
                                  
                                  while( 1 )
                                  {
                               
                                      fd_set waitSet, exctSet;
                                      memcpy( &waitSet, &fd_buf, sizeof(fd_set) );
                                      memcpy( &exctSet, &fd_buf, sizeof(fd_set) );
                               
                                      int ret = select( 0, &waitSet, 0, &exctSet, 0 );
                                      if( ret <= 0 )
                                          break;
                               
                                      for( u_int fd = 0 ; fd < waitSet.fd_count ; fd ++ )
                                      {
                                          SOCKET sc = waitSet.fd_array[ fd ];
                               
                                          int isListener;
                                          int dmSize = sizeof(isListener);
                                          if( getsockopt( sc, SOL_SOCKET, SO_ACCEPTCONN,(char*) &isListener, &dmSize) == SOCKET_ERROR ) {
                                              Close();
                                              return 0;
                                          }
                               
                                          if( isListener )
                                          {
                                              // ктото зацепился
                                              int clAddrLen = sizeof(sockaddr_in);
                                              sockaddr_in cl_addr;
                                              SOCKET cl = accept( sc, (LPSOCKADDR)&cl_addr, &clAddrLen );
                                              if( cl == INVALID_SOCKET )
                                                  continue;
                                              FD_SET( cl, &fd_buf );
                                              continue;
                                          }
                               
                                          // ктото из клиентов передает данные
                                          // или отвалился
                               
                                          // анука проверимка !!!
                                          if( FD_ISSET( sc, &exctSet ) )
                                          {
                                              FD_CLR( sc, &fd_buf );
                                              closesocket( sc );
                                              continue;
                                          }
                               
                                          int read_size = recv ( sc, radbuf, 16 * 1024, 0 );
                               
                                      }
                                  }
                               
                                  Close( TRUE );
                                  WSACleanup();
                                  return 0;
                              }



                            ExpandedWrap disabled
                              // а все это клиент
                               
                              #include <winsock2.h>
                              #pragma comment(lib, "wsock32.lib")
                               
                               
                              SOCKET cl_sock = INVALID_SOCKET;
                               
                               
                              void Close( BOOL bForce = TRUE )
                              {
                                  if( ( cl_sock == INVALID_SOCKET || cl_sock == SOCKET_ERROR ) ||
                                      bForce ) {
                                      closesocket ( cl_sock );
                                  }
                              }
                               
                               
                               
                              int _tmain(int argc, _TCHAR* argv[])
                              {
                                  WSADATA w_data;
                                  WSAStartup( MAKEWORD( 2, 0 ), &w_data );
                               
                               
                                  cl_sock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
                                  if( cl_sock == SOCKET_ERROR ) {
                                      return 0;
                                  }
                               
                               
                                  sockaddr_in svaddr;
                                  svaddr.sin_family = AF_INET;
                                  svaddr.sin_port = htons(15555);
                                  svaddr.sin_addr.s_addr = inet_addr( "127.0.0.1" );
                               
                                  if( connect(cl_sock,(LPSOCKADDR)&svaddr,sizeof(SOCKADDR)) == SOCKET_ERROR ) {
                                      Close();
                                      return 0;
                                  }
                               
                               
                               
                               
                               
                                  Close( TRUE );
                                  WSACleanup();
                                  return 0;
                              }


                            начинаем трассировать !!!
                            когда проходит connect на сервере select возвращает управление , ну и собственно добавляет полученное соединение
                            но зато когда проходим через closesocket на клиенте ... select опять возвращает управление но exctSet.fd_count = 0 поэтому и FD_ISSET( sc, &exctSet ) вернет ложь !!!


                            а вот исправленная версия сервера

                            ExpandedWrap disabled
                              #include <winsock2.h>
                              #pragma comment(lib, "wsock32.lib")
                               
                               
                               
                               
                               
                              SOCKET sv_sock = INVALID_SOCKET;
                              fd_set fd_buf;
                               
                               
                              void Close( BOOL bForce = FALSE )
                              {
                                  if( ( sv_sock == INVALID_SOCKET || sv_sock == SOCKET_ERROR ) ||
                                      bForce ) {
                                      closesocket ( sv_sock );
                                  }
                              }
                               
                               
                              int _tmain(int argc, _TCHAR* argv[])
                              {
                                  WSADATA w_data;
                                  WSAStartup( MAKEWORD( 2, 0 ), &w_data );
                               
                               
                                  sv_sock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
                                  if( sv_sock == SOCKET_ERROR ) {
                                      return 0;
                                  }
                               
                                  int sock_value = 1;
                                  if ( setsockopt( sv_sock, SOL_SOCKET, SO_REUSEADDR,(char*)&sock_value, sizeof(sock_value)) == SOCKET_ERROR ) {
                                      Close();
                                      return 0;
                                  }
                                  if ( setsockopt( sv_sock, IPPROTO_TCP, TCP_NODELAY,(char*)&sock_value, sizeof(sock_value)) == SOCKET_ERROR ) {
                                      Close();
                                      return 0;
                                  }
                                  if( ioctlsocket( sv_sock, FIONBIO, (u_long *) &sock_value ) == SOCKET_ERROR ) {
                                      Close();
                                      return 0;
                                  }
                               
                                  sockaddr_in svaddr;
                                  svaddr.sin_family = AF_INET;
                                  svaddr.sin_port = htons(15555);
                                  svaddr.sin_addr.s_addr = ADDR_ANY;
                               
                                  if( bind( sv_sock, (LPSOCKADDR)&svaddr, sizeof(SOCKADDR) ) == SOCKET_ERROR ) {
                                      Close();
                                      return 0;
                                  }
                                  if ( listen( sv_sock, FD_SETSIZE ) == SOCKET_ERROR ) {
                                      Close();
                                      return 0;
                                  }
                                  
                               
                                  FD_ZERO( &fd_buf );
                                  FD_SET( sv_sock, &fd_buf );
                               
                                  char radbuf[ 16 * 1024 ];
                                  
                                  while( 1 )
                                  {
                               
                                      fd_set waitSet;
                                      memcpy( &waitSet, &fd_buf, sizeof(fd_set) );
                               
                                      int ret = select( 0, &waitSet, 0, 0, 0 );
                                      if( ret <= 0 )
                                          break;
                               
                                      for( u_int fd = 0 ; fd < waitSet.fd_count ; fd ++ )
                                      {
                                          SOCKET sc = waitSet.fd_array[ fd ];
                               
                                          int isListener;
                                          int dmSize = sizeof(isListener);
                                          if( getsockopt( sc, SOL_SOCKET, SO_ACCEPTCONN,(char*) &isListener, &dmSize) == SOCKET_ERROR ) {
                                              Close();
                                              return 0;
                                          }
                               
                                          if( isListener )
                                          {
                                              // ктото зацепился
                                              int clAddrLen = sizeof(sockaddr_in);
                                              sockaddr_in cl_addr;
                                              SOCKET cl = accept( sc, (LPSOCKADDR)&cl_addr, &clAddrLen );
                                              if( cl == INVALID_SOCKET )
                                                  continue;
                                              FD_SET( cl, &fd_buf );
                                              continue;
                                          }
                               
                                          // ктото из клиентов передает данные
                                          // или отвалился
                               
                                          //
                                          // зато вот тут вроде как все работает !!!
                                          u_long read_data_size;
                                          if( ioctlsocket(sc, FIONREAD, &read_data_size ) == SOCKET_ERROR ) {
                                              Close();
                                              return 0;
                                          }
                               
                                          if( read_data_size == 0 ) {
                                              FD_CLR( sc, &fd_buf );
                                              closesocket( sc );
                                              continue;
                                          }
                                          //
                                          //
                               
                                          int read_size = recv ( sc, radbuf, 16 * 1024, 0 );
                               
                                      }
                                  
                                  
                                  }
                               
                               
                                  Close( TRUE );
                                  WSACleanup();
                                  return 0;
                              }


                            тут вроде как и цикл не нужно проходить для проверки , я имею ввиду FD_ISSET .. ведь DS_SETSIZE можно сделать и больше 64 .. просто возвращает размер данных в буфере сокета и все ..
                            или всетаки тут есть какойто серьезный баг
                            Сообщение отредактировано: hash_2000 -
                              :) помойму ужасно
                              Быстренько тебе набросал, как в моем представлении все это должно выглядеть, работает - не работает уж не знаю, отладчиком походишь отладишь
                              зы это даже компилиться на солярисе :D
                              ExpandedWrap disabled
                                typedef std::vector<int> CSocketList;
                                    CSocketList socket_list;
                                 
                                    int sv_sock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
                                    if( sv_sock == -1 ) {
                                        return(EXIT_FAILURE);
                                    }
                                 
                                    int sock_value = 1;
                                    if ( setsockopt( sv_sock, SOL_SOCKET, SO_REUSEADDR,(char*)&sock_value, sizeof(sock_value)) != 0) {
                                        return(EXIT_FAILURE);
                                    }
                                    if ( setsockopt( sv_sock, IPPROTO_TCP, TCP_NODELAY,(char*)&sock_value, sizeof(sock_value)) != 0) {
                                        return(EXIT_FAILURE);
                                    }
                                 
                                    sockaddr_in svaddr;
                                    svaddr.sin_family = AF_INET;
                                    svaddr.sin_port = htons(15555);
                                    svaddr.sin_addr.s_addr = INADDR_ANY;
                                 
                                    if( bind( sv_sock, (sockaddr *)&svaddr, sizeof(svaddr) ) != 0) {
                                         return(EXIT_FAILURE);
                                    }
                                    if ( listen( sv_sock, FD_SETSIZE ) != 0 ) {
                                         return(EXIT_FAILURE);
                                    }
                                    
                                    socket_list.push_back(sv_sock);
                                    bool working = true;
                                    while(working) {
                                 
                                        fd_set fdr, fde, fdw;
                                        FD_ZERO(&fdr);
                                        FD_ZERO(&fdw);
                                        FD_ZERO(&fde);
                                 
                                        const size_t socket_list_size = socket_list.size();
                                        for(size_t i = 0; i < socket_list_size; ++i) {
                                            FD_SET(socket_list[i], &fdr);
                                            FD_SET(socket_list[i], &fdw);
                                            FD_SET(socket_list[i], &fde);
                                        }
                                 
                                        int r = select(0, &fdr, &fdw, &fde, 0);
                                        if(r > 0) {
                                 
                                            for(size_t i = 0; i < socket_list_size; ++i) {
                                 
                                                int isListener;
                                                int dmSize = sizeof (isListener);
                                                if(getsockopt(socket_list[i], SOL_SOCKET, SO_ACCEPTCONN, (char*) & isListener, &dmSize) != 0) {
                                                    return (EXIT_FAILURE);
                                                }
                                 
                                                if(FD_ISSET(socket_list[i], &fdw)) {
                                                    
                                                }
                                 
                                                if(FD_ISSET(socket_list[i], &fdr)) {
                                 
                                                    if(!isListener)
                                                    {
                                                       //if recv failed - disconnected
                                                    }
                                                    else {
                                                        int new_sd = accept(socket_list[i], 0, 0);
                                                        if(new_sd > 0)
                                                            socket_list.push_back(new_sd);
                                                    }
                                                }
                                 
                                                if(FD_ISSET(socket_list[i], &fde)) {
                                 
                                                    if(isListener)
                                                        return(EXIT_FAILURE);
                                                    else {
                                                        close(socket_list[i]);
                                                        socket_list[i] = -1;
                                                    }
                                                }
                                            }
                                            
                                            for(int i = socket_list_size - 1; i > 0; i--) {
                                                if(socket_list[i] == -1)
                                                    socket_list.erase(socket_list.begin() + i);
                                            }
                                        }
                                    }


                              а не, ошибся с fdw, когда кто то присоединяеться срабатывает fdr и судя по докам из msdn
                              Цитата

                              In summary, a socket will be identified in a particular set when select returns if:

                              readfds:
                              If listen has been called and a connection is pending, accept will succeed.
                              Data is available for reading (includes OOB data if SO_OOBINLINE is enabled).
                              Connection has been closed/reset/terminated.

                              writefds:
                              If processing a connect call (nonblocking), connection has succeeded.
                              Data can be sent.

                              exceptfds:
                              If processing a connect call (nonblocking), connection attempt failed.
                              OOB data is available for reading (only if SO_OOBINLINE is disabled).


                              по fde нельзя отвал определить. В общем сиди ковыряйся, потом расскажешь :D о результатах.
                              Сообщение отредактировано: popsa -
                                ну ладно ... мне мой способ нравится .. потом какнибудь попробую его еще под нагрузкой протестировать
                                и вообще тему закрывается тут целая куча способов ! пусть каждый по себе выбирает !! :D
                                Всем участвовавшим спасибо !!!
                                Сообщение отредактировано: hash_2000 -
                                0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
                                0 пользователей:


                                Рейтинг@Mail.ru
                                [ Script execution time: 0,0914 ]   [ 16 queries used ]   [ Generated: 28.06.25, 14:42 GMT ]