Начну с того, что была предложена работа на должность программиста с\с++. Задание это название темы.


Полез в интернет, кругом все напичкано чатами и общением по типу клиент-сервер, но увы кода с подобным заданием я так и не нашел. Был примитив типа ЭХО клиент-сервера, который я и решил взять за основу:
Это у нас клиент:


    struct sockaddr_in addr; // структура с адресом
    struct hostent* hostinfo;
    port = atoi(PORT);
    sock = socket(AF_INET, SOCK_STREAM, 0); // создание TCP-сокета

    if(sock < 0)
    {
        perror("socket");
        exit(1);
    }

    // Указываем параметры сервера
    addr.sin_family = AF_INET; // домены Internet
    addr.sin_port = htons(port); // или любой другой порт...
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");

 if(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) // установка соединения с сервером
    {
        perror("Подключение");
        exit(2);
    }

это код сервера:


if( (master_socket = socket(AF_INET , SOCK_STREAM , 0)) == 0)
    {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    //set master socket to allow multiple connections , this is just a good habit, it will work without this
    if( setsockopt(master_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt)) < 0 )
    {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    //type of socket created
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons( PORT );

    //bind the socket to localhost port 8888
    if (bind(master_socket, (struct sockaddr *)&address, sizeof(address))<0)
    {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    printf("Listener on port %d \n", PORT);

    //try to specify maximum of 3 pending connections for the master socket
    if (listen(master_socket, 3) < 0)
    {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    //accept the incoming connection
    addrlen = sizeof(address);
    puts("Waiting for connections ...");

    while(TRUE)
    {

            if ((new_socket = accept(master_socket, (struct sockaddr *)&address, (socklen_t*)&addrlen))<0)
            {
                perror("accept");
                exit(EXIT_FAILURE);
            }         
   }

После всего этого в клиенте нужно отправить сообщение серверу используя функции send или write а на стороне сервера принять сообщение и переотправить его обратно клиенту используя функции read и send.


Вообще есть разные функции отправки и приема, к примеру send и recv вместе с сообщением шлют еще и флаг подтверждения, а функции read и write не требуют подтверждения, то есть сообщение может потерять байты при отправке и это не будет зафиксировано.


Так как сокеты это дуплекс и создавая связь между клиентом и сервером мы не можем писать туда сообщения из других подключенных сокетов, необходимо создать массив со всеми активными сокетами подключенными к серверу. И еще одно замечание очень важное:


Для общения между несколькими сокетами необходимо использовать функцию select, которая выбирает сокет из списка и отсылает ему сообщение, и так далее, пока не закончатся все записанные сокеты

//clear the socket set
        FD_ZERO(&readfds);

        //add master socket to set
        FD_SET(master_socket, &readfds);
        max_sd = master_socket;

        //add child sockets to set
        for ( i = 0 ; i < max_clients ; i++)
        {
            //socket descriptor
            sd = client_socket[i];

            //if valid socket descriptor then add to read list
            if(sd > 0)
                FD_SET( sd , &readfds);

            //highest file descriptor number, need it for the select function
            if(sd > max_sd)
                max_sd = sd;
        }

        //wait for an activity on one of the sockets , timeout is NULL , so wait indefinitely
        activity = select( max_sd + 1 , &readfds , NULL , NULL , NULL);

        if ((activity < 0) && (errno!=EINTR))
        {
            printf("select error");
        }

После этого в массив сокетов будет записано правильное значение подключаемого сокета а далее остается лишь перебирать их при рассылке сообщений:



sd = client_socket[i];

            if (FD_ISSET( sd , &readfds))
            {
                //Check if it was for closing , and also read the incoming message
                if ((valread = read( sd , buffer, 1024)) == 0)
                {                   
                    //Somebody disconnected , get his details and print
                    getpeername(sd , (struct sockaddr*)&address , (socklen_t*)&addrlen);
                    printf("Host disconnected , ip %s , port %d \n" , inet_ntoa(address.sin_addr) , ntohs(address.sin_port));

                    //Close the socket and mark as 0 in list for reuse
                    close( sd );
                    user_count--;
                    client_socket[i] = 0;
                }

                //Echo back the message that came in
                else
                {

                    //set the string terminating NULL byte on the end of the data read

                        buffer[valread] = '\0';
                        for (i = 0; i < max_clients; i++)
                        {
                            sd = client_socket[i];
                        send(sd , buffer , strlen(buffer) , 0 );
                        }
                        buffer[1024] = {0};
                }
            }

Запишем все это в функцию и создадим отдельный поток:


void *server(void *);

 pthread_create(&threadA[0], NULL, server, NULL);

                                pthread_join(threadA[0], NULL);

Что касаемо клиента, то необходимо создать два разных потока для чтения и записи в сокет:


void *write(void *);
void *read(void *);

 pthread_create(&threadA[0], NULL, write, NULL);
 pthread_create(&threadA[1], NULL, read, NULL);

         pthread_join(threadA[1], NULL);
         pthread_join(threadA[0], NULL);

void *write (void *dummyPt)
{
    for(;;)
       {
           char s[BUF_SIZE];

           cout << "<----";
           bzero(s, BUF_SIZE + 1);
           cin.getline(s, BUF_SIZE);

           send(sock, s, strlen(s), 0);

       }
    close(sock);
}

void *read (void *dummyPt)
{
    char test[BUF_SIZE];
    bzero(test, BUF_SIZE + 1);
    bool loop = false;

    while(!loop)
    {
        bzero(test, BUF_SIZE + 1);

        int rc = read(sock, test, BUF_SIZE);
        if ( rc > 0)
        {
        string tester (test);
        cout << ": "<< tester << endl;

        if(tester == "exit_server")
            break;
        }

    }
    cout << "\nClosing thread and conn" << endl;
    close(sock);
}

Теперь все работает. Спасибо за снимание. Надеюсь что это пригодится тем, кто так же как и я пытался написать клиент-сервер, но не смог найти нужную информацию в сети.

Поделиться с друзьями
-->

Комментарии (34)


  1. sir_Maverick
    04.06.2016 12:59
    +6

    Вам интересно выкладывать один и тот же шлак с теми же грамматическими ошибками на оба ресурса? Почему?


    1. edd_k
      04.06.2016 20:21

      Не один и тот же. На GT статья «Просто код простой задачи» смотрится гораздо шлаковее, чем на Хабре


  1. DistortNeo
    04.06.2016 13:08

    Недостаток такой реализации — это однопоточность сервера. Если хоть какое-то соединение с клиентом «подвиснет» и будет переполнен его исходящий буфер, то сервер просто зависнет на вызове write.

    Решения данной проблемы:
    1. Создание отдельного потока для каждого клиента. Недостаток: необходимость понимания механизмов синхронизации, низкая эффективность при большом числе соединений. Новичкам будет тяжело разобраться.
    2. Использование select также и для записи в сокеты. Думаю, в данном случае самое простое решение.
    3. Использование более высокоуровневых языков с поддержкой асинхронного программирования.

    Вообще есть разные функции отправки и приема, к примеру send и recv вместе с сообщением шлют еще и флаг подтверждения, а функции read и write не требуют подтверждения, то есть сообщение может потерять байты при отправке и это не будет зафиксировано.

    Это не так: read() и write() эквивалентны recv() и send() с нулевыми флагами.

    Ну и ещё один момент: код очень разрозненный, фрагментарный. Новичку была бы полезна ссылка на репозиторий.


    1. A1ien
      04.06.2016 14:44
      +2

      Ну зачем другие языки? А чем вам boost::asio не подходит?


      1. DistortNeo
        04.06.2016 16:07

        В C++ пока ещё нет continuations, из-за чего многие конструкции асинхронного программирования выглядят неочевидно и монструозно.


    1. Hellsy22
      04.06.2016 20:05

      Если хоть какое-то соединение с клиентом «подвиснет» и будет переполнен его исходящий буфер, то сервер просто зависнет на вызове write
      Если будет переполнен исходящий буфер, то можно:
      — расширить буфер
      — отбросить новые данные
      — вообще дропнуть соединение

      Но городить по потоку для отправки данных каждому клиенту — это какой-то новый уровень неэффективности. Спасибо, что хоть не форки.

      Я вообще не понимаю зачем тут хотя бы один поток — отправкой данных занимается ОС, перекинуть данные из буфера по нынешним временам не стоит вообще ничего.


      1. DistortNeo
        04.06.2016 20:40

        А ещё можно подождать с отправкой данных до тех пор, пока буфер не освободится, что в подавляющем большинстве случаев обычно и делают. Если данные — это большой файл, то передачей данных придётся заниматься нам самим, а не лезть в системные буферы и уж тем более отказывать клиенту в обслуживании, потому что захотел слишком большой файл.

        И основных решения тут два:
        1. Каждому соединению — по потоку, а то и по два (чтение и запись), используя блокирующие операции чтения и записи данных.
        2. Использование select, epoll, libevent, неблокирующих операций и прочих радостей жизни.

        Второй способ сильно эффективнее, но первый сильно проще в реализации, поэтому с него и нужно начинать изучение.

        Кстати, Apache под Windows пользовался первым способом — создавал 100 потоков и обрабатывал каждый запрос в своём потоке.


        1. sba
          04.06.2016 23:05

          И мы прекрасно знаем что с этого вышло… Nginx наше все, а все потому что начинал не с того что проще, а сразу с правильных архитектурных решений.


          1. DistortNeo
            05.06.2016 01:18
            +2

            Во времена, когда апач начинал развиваться, правильных архитектурных решений попросту ещё не существовало. Например, во времена релиза Apache 2.0 не было ни libevent, ни epoll, только select, который обладал очень плохой масштабируемостью.


            1. rafuck
              05.06.2016 03:06

              Ну, справдливости ради, тогда уже был overlapped io, правда не поддерживался в win 9x.


          1. Hellsy22
            05.06.2016 08:05
            +1

            До Nginx был еще 0W, а до него thttpd, и они прекрасно работали.


        1. Hellsy22
          05.06.2016 08:05
          +1

          Чем же первый способ проще? Неблокирующие операции элементарны. Нет, я серьезно не понимаю как можно назвать работу с потоками более простой.


          1. DistortNeo
            05.06.2016 13:25

            Первый способ проще тем, что кода значительно меньше и он линеен. Меньше кода — меньше ошибок и времени для разработки.

            Вместо двух состояний (ошибка, успешное завершение) при использовании неблокирующих сокетов добавляется третье — «выполнение операции без блокирования невозможно», при котором нужно прервать действие и перейти в режим ожидания готовности. А это дополнительная проверка после каждой операции с сокетами.

            Как следствие — при использовании неблокирующих операций придётся вместо одной функции писать целый класс — машину состояний, сохраняющей точку входа. Желающие помучиться с отладкой, но писать в первом стиле могут ещё использовать нити.


            1. Hellsy22
              05.06.2016 22:15

              Я не совсем понимаю о чем вы говорите.
              Обычно в подобных задачах крутится цикл в котором принимаются новые соединения, закрываются соединения с ошибками, принимаются данные (сколько есть или сколько лезет) и отправляются данные (сколько есть или сколько лезет). Как бы все. Проще некуда. Хотите ООП — оформляете все это в базовый класс и в потомках меняете read / send, наворачивая любой уровень абстракции. Хотите — можете все это поместить в один тред. Или в десять. Но тред на клиента мне встречался лишь там, где подразумевалась долгая работа с конкретным клиентом при околонулевом взаимодействии между клиентами, короче там, где треды пришли на замену форкам ради роста производительности.


              1. DistortNeo
                06.06.2016 02:40

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

                Ну да, крутится цикл, всё просто и понятно, но ровно до тех пор, пока идёт обмен короткими сообщениями, как в обучающем примере. Усложнение протокола приведёт к значительному повышению сложности кода.

                Например, как прочитать пакет с длиной, передаваемой в начале пакета?

                Вариант 1:
                1. Прочитать 4 байта одной функцией. *
                2. Прочитать N байт.
                * например обёрткой над recv — bool recvall(...)

                Вариант 2:
                1. Проверить состояние.
                2. Если состояние == 1, тогда продолжать читать буфер длины, по завершении перейти в состояние 2.
                3. Если состояние == 2, тогда продолжать читать буфер пакета, по завершении перейти в состояние 3.

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


                1. Hellsy22
                  06.06.2016 23:32

                  Вариант 1. Сразу нет. Категорически. Потому как TCP/IP — не пакетная, а потоковая передача данных. И то, что клиент вам отправил длину и данные не означает, что вы получите их в одном или двух пакетах — может быть получите и в трех, и в десяти. Так что по-любому это все падает в промежуточный буфер, от которого «откусываются» куски по мере обработки данных.

                  Это ведет нас к варианту 3, т.е. как бы сделал я:

                  После каждого чтения вызывать метод типа on_data, который в базовом классе — пустышка.
                  На основе базового класса сделать класс, в котором on_data разбирается с длиной и забирает ровно столько из буфера, сколько надо. При этом в новом классе не будет или почти не будет низкоуровневой работы с сокетами — всем этим заведует базовый класс.

                  Собственно данный подход не привязан к конкретному языку — на языках высокого уровня будет ровно то же самое.


                  1. DistortNeo
                    07.06.2016 08:39

                    Именно поэтому я и указал recvall — функцию, которая заполняет буфер определённым числом байт из потока вне зависимости от того, сколько пакетов фактически придёт.


                    1. Hellsy22
                      07.06.2016 13:46

                      Тогда, какая в итоге разница? Все равно нужно читать все, что дают. А разбирать прочитанное будет уже не базовый класс (я еще раз пушну тему ООП).

                      Потоки же создают массу проблем при взаимодействии данных друг с другом. Т.е. когда у вас общение с каждым клиентом четко изолировано от других клиентов, то почему бы и нет? А если у вас клиенты как-то обмениваются данными, то уже придется страдать. Поэтому я вполне понимаю, когда непосредственно чтение и отправку сырых данных реализуют в отдельном потоке (но всех для всех клиентов вообще) или когда используются дополнительные потоки для доп. обработки данных (например, у меня как-то было 16 потоков — по числу ядер — занимавшихся gzip-ом). Но решительно не понимаю зачем вы хотите замучить несчастный шедулер, впарив ему 10к потоков (например, при чате на 10к клиентов).


                      1. DistortNeo
                        07.06.2016 21:02

                        Да не хочу я никого мучить. Я же тоже согласен, что нет нужды плодить потоки, которые 99.99% времени будут находиться в состоянии ожидания и отъедать ресурсы системы.

                        Я просто считаю, что реализация через метод on_data менее удобна, чем линейная, особенно если нет взаимодействия между соединениями.


  1. iSage
    04.06.2016 15:37
    +5

    > работа на должность программиста с\с++
    Надеюсь, вас не взяли.
    >c++
    От плюсов тут только cin/cout.


    1. samuraIII
      04.06.2016 17:07
      -12

      Спасибо конечно. Радуйтесь на работу меня не взяли и поэтому теперь я буду ходить у вашего двора и трести у вашей жены денег на пиво, потому что ваш коментарий меня расстроил. Нет чтобы подсказать так лучше засодить.


      1. bak
        04.06.2016 17:31
        +2

        Подсказываю.
        1) Не пишите статьи на темы в которых не разбираетесь. Ничего хорошего из этого не выйдет. Неужели это не было понятно после вашей предыдущей статьи?
        2) Если хотите работу c++ сетевого разработчика — потрудитесь потратить время (несколько месяцев а не два дня) на то чтоб в деталях разобраться в сети на низком уровне, попробовать существующие фреймворки (boost asio / libevent / etc.); написать несколько простых сетевых приложений (чат, http сервер, etc.) и выложить их на гитхаб. После этого моментально работу найдёте.


        1. edd_k
          04.06.2016 20:26

          Да-да, за пару месяцев опыта и тренировочные шаблоны чата/http-сервера — так прям и оторвут. Только таких и ищут, указывая «минимум 2 года опыта» и подразумевая, что возьмут и с 1 годом, но такого, который бесценен.


      1. Foolleren
        04.06.2016 17:37
        +2

        Из личных наблюдений.
        На хабре за неидеальный код на C/C++ больно бьют, за упоминание делфи тоже,
        Для волшебных пендалей есть Тостер.


      1. Hellsy22
        04.06.2016 20:11
        +2

        Вы написали код уровня «hello world». Примеров клиент-серверного взаимодействия в сети полно. Их много и подробно разбирали в литературе. Например, у меня вот такая книжка дома валялась — обратите внимание на год.


      1. Lisio
        04.06.2016 20:26
        +3

        В вашем двухстрочном комментарии 8 пунктуационных и грамматических ошибок. Теперь промасштабируйте это число на размер статьи и вам, возможно, все станет понятно. После этого можно переходить к разбору кода.


    1. semibiotic
      06.06.2016 09:01

      Зануда заметит, что перегрузка функций (read() и write()), как будто, тоже С++… но С++, действительно, предполагает совсем иной стиль кодинга.

      Лично мне все стало ясно после авторского «c\c++». Сразу видна степень (не-)знакомства и с «linux», и с программированием.

      Автор, видимо, считает нормальным для соискателя на должность программиста изучать язык накануне собеседования. Его ждет много открытий.


  1. tmin10
    04.06.2016 17:22

    Всё-таки эта статья по тематике подходит на хабр, а не на гиктаймс.


    1. DrPass
      04.06.2016 17:30
      +1

      По тематике она больше подходит на StackOverflow или sql.ru, в форуме на тему «Как мне написать клиент-сервер на С++»


      1. bak
        04.06.2016 17:31
        +4

        Конкретно эта статья вообще никуда не подходит.


      1. sir_Maverick
        06.06.2016 09:01

        Скорее на «Вопросы Мэйл.ру»


    1. A1ien
      04.06.2016 19:21
      -1

      Она там была:) Ее быстро прикрыли.


  1. Blast
    06.06.2016 09:01

    Если интересны сеть и многопоточность в C++, гляньте в сторону вот этого курса


    1. Blast
      06.06.2016 10:49

      Ссылка не прицепилась. Это «Многопоточное программирование на C++» от mail.ru на stepic.