LINUX.ORG.RU

Сервер на СИ

 ,


1

2

Здравствуйте.

В данное время я медленно, но с удовольствием постигаю прекрасный язык СИ (до этого было делфи, пхп, js и ещё что-то).

ПРОЛОГ

Мне понадобился маленький тср-серверок, который бы слушал порт, принимал от клиента некий символ (например букву А) и далее отправлял этот символ в микроконтроллер подключённый к серверу по USB. (Всё это связано с пресловутым «умным домом»)

СУТЬ

На просторах интернета я нашёл не малое количество примеров и остановился вот на этом - http://masandilov.ru/network/guide_to_network_programming6#6.1

После некоторых изменений, код приобрёл нижеследующий облик:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <time.h>

#define BACKLOG 10     // как много может быть ожидающих соединений

#define MAXBUFLEN 100

char PORT[5]={0,};
char device[14]={0,};
char er_log_str[50]={0,};

void sigchld_handler()
{
    while(waitpid(-1, NULL, WNOHANG) > 0);
}

void *get_in_addr(struct sockaddr *sa) // получаем адрес сокета, ipv4 или ipv6:
{
    if(sa->sa_family == AF_INET) 
     {
       return &(((struct sockaddr_in*)sa)->sin_addr);
     }

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

void error_log() // запись ошибки в файл ErrorSer.log
{  
    time_t t;
    time(&t);
    FILE *f;
    f = fopen("ErrorSer.log", "a"); 
    fprintf(f , "%s. ", er_log_str);
    fprintf(f , "%s", ctime( &t));  
    printf("Write to ErrorSer.log\n");
    fclose(f);
    exit(0);
}

int main(int argc, char *argv[])
{
   if(argc!=3) 
    {
      printf("Primer zapyska - ./arduserver 3490 /dev/ttyACM1\n");
      sprintf(er_log_str,"%s","Not use PORT");
      error_log();
    }
  
   sprintf(PORT,"%s", argv[1]);
   sprintf(device,"%s", argv[2]);

    int sockfd, new_fd;  // слушаем на sock_fd, новые соединения - на new_fd
    struct addrinfo hints, *servinfo, *p;
    struct sockaddr_storage their_addr; // информация об адресе клиента
    socklen_t sin_size;
    struct sigaction sa;
    int yes=1;
    // char s[INET6_ADDRSTRLEN];
    int rv;

    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE; // use my IP

    if((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0) 
      {
        sprintf(er_log_str,"%s","Error - getaddrinfo");
        error_log();
      }

    for(p = servinfo; p != NULL; p = p->ai_next) // цикл через все результаты, чтобы забиндиться на первом возможном
      {
        if((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) 
          {
            perror("server: socket");
            continue;
          }

        if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) 
          {
            perror("setsockopt");
            exit(1);
          }

        if(bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) 
          {
            close(sockfd);
            perror("server: bind");
            continue;
          }

        break;
      }

    if(p == NULL)  
     {
        sprintf(er_log_str,"%s","Error - failed to bindn");
        error_log();
     }
   
    freeaddrinfo(servinfo); // всё, что можно, с этой структурой мы сделали

    if(listen(sockfd, BACKLOG) == -1) 
     {
        sprintf(er_log_str,"%s","Error - listen");
        error_log();
     }

    sa.sa_handler = sigchld_handler; // обрабатываем мёртвые процессы
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;

    if(sigaction(SIGCHLD, &sa, NULL) == -1) 
     {
        sprintf(er_log_str,"%s","Error - sigaction");
        error_log();
     }

    printf("server: waiting for connections...\n");

    while(1) // главный цикл accept()
     {  
        sin_size = sizeof their_addr;
        new_fd = accept(sockfd, NULL,  &sin_size);

        if(new_fd == -1) 
         {
            perror("accept");
            continue;
         }

        printf("server: OK connection\n");

///////////////////////////////////////////////////////////////////////////////////////////

        if(!fork()) // тут начинается дочерний процесс
           { 
            close(sockfd); // дочернему процессу не нужен слушающий сокет
          
            char buffer[7] = {0,};

            int n = read(new_fd, buffer, 6);

            if(n < 0)
              {
                sprintf(er_log_str,"%s","Error - reading from socket");
                error_log();
              }

            printf("\n"); 
            printf("Ot klienta polycheno: %c\n\n", buffer[5]); 

            char to_Ardu[14] = {0,}; 
            sprintf(to_Ardu,"echo 'Y+=Z%c' > %s", buffer[5], device);
            system(to_Ardu); // отправляем ардуине пакетик

            close(new_fd);

            exit(0);
        }

        close(new_fd);  // а этот сокет больше не нужен родителю

    } // конец while

    return 0;
}

/// ./arduserver 3490 /dev/ttyACM1

ВОПРОС

Очень хочется, чтоб уважаемое сообщество указало на ошибки в коде и объяснило некоторые моменты.

В частности:

1. Корректно ли я инициализирую массивы.

char PORT[5]={0,};

2. Нужен ли в данном примере дочерний процесс

if(!fork())
...
Это пока для меня тёмный лес, поэтому я был бы премного благодарен, если кто-то растолкует популярным языком, в каких случая надо запускать дочерние процессы.

Зараннее спасибо.



Последнее исправление: CYB3R (всего исправлений: 6)

Нужен ли в данном примере дочерний процесс

Очевидно, нет. Функцию можно в потоке запустить.

Freyr69 ★★★
()

Имхо char PORT[5] = "" куда очевиднее.

anonymous
()

никогда, НИКОГДА не используйте sprintf если не уверенны в длине данных. Даже если уверены берите snprintf

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

   sprintf(PORT,"%s", argv[1]);
   sprintf(device,"%s", argv[2]);

MKuznetsov ★★★★★
()

Биндиться по рандому при AF_UNSPEC это имхо весьма странно. Надо или конкретный адрес/фэмили указывать, или слушать все, что socket() поймет. Если хочешь tcp46, то укажи AF_INET6/in6addr_any в sockaddr_in6 (считаем что фамилий всего две, по мотивам get_in_addr) и выкинь getaddrinfo вообще, если сервисы парсить не надо. Если надо, то парси отдельно без пассива и забирай порт руками.

anonymous
()
Ответ на: комментарий от anonymous

Эх, вот если бы Вы объяснили то же самое, только чуть более понятным языком...

stD
() автор топика
Ответ на: комментарий от MKuznetsov

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

А еще тут sprintf просто не нужен, т.к. логичней взять strncpy.

anonymous
()
Ответ на: комментарий от Freyr69

Очевидно, нет. Функцию можно в потоке запустить.

Тогда, если не затруднит, растолкуйте, а нужен ли здесь вообще дополнительный поток? Понимаю что вопрос звучит глупо и вроде бы на него могу ответить только я сам, но всё же. Как я уже писал в посте, код был взят в сети и немного переделан, то есть сам я его не писал и не всё в нём понимаю (так как учусь). Так вот, нужен ли здесь доп. поток, при условии, что к серверу будут подключаться максимум три клиента за час, а если и сделают это одновременно, то для клиента это не будет страшным (он просто сделает ещё один запрос). Или для того чтоб как-то «не навредить программе» второй поток нужен в любом случае?

stD
() автор топика

нельзя такие ужасы с утра показывать

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

*.ru

ну ты понял

wakuwaku ★★★★
()
Последнее исправление: wakuwaku (всего исправлений: 1)

Я такие штуки вебсокетами делаю. И нет необходимости поллить сервер по 10 раз в секунду, чтобы более-менее приличную асинхронность обеспечить.

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

Eddy_Em ☆☆☆☆☆
()
Ответ на: комментарий от stD

Не нужен. Пока один клиент обслуживается, остальные будут некоторое время висеть в баклоге, размер которого ты передал в listen(). Кому не повезло попасть в баклог получат ECONNRESET.

anonymous
()
Ответ на: комментарий от anonymous

Только сначала нужно прочитать ман strncpy и понять, что это вовсе не то, о чем ты подумал, и что маны нужно таки читать. Логичнее взять strlcpy или snprintf, например.

anonymous
()
Ответ на: комментарий от anonymous

Пока один клиент обслуживается, остальные будут некоторое время висеть в баклоге

Скажите, а где указывается это самое «некоторое время» ?

stD
() автор топика
Ответ на: комментарий от Eddy_Em

Я такие штуки вебсокетами делаю

Имеет смысл только для управления через браузер, а если есть нормальный клиент, то лучше юзать TCP

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

В данном случае вообще одного потока с event loop хватит за глаза

Да и ресурсов потоки меньше жрут.

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

annulen ★★★★★
()

Бери libev (или libevent, или libuv (если ты хипстер, то крайне рекомендуется), или что-нибудь еще в этом роде), и делай асинхронный сервер без форков и потоков. Будет проще и эффективнее для данной задачи

annulen ★★★★★
()
Ответ на: комментарий от Eddy_Em

Да и ресурсов потоки меньше жрут.


чем кто?

anonymous
()
Ответ на: комментарий от annulen

если есть нормальный клиент

Нафиг оно нужно — еще клиентом париться?

event loop

Делать нефиг, очереди перебирать?

Eddy_Em ☆☆☆☆☆
()
Ответ на: комментарий от annulen

Бери libev (или libevent, или libuv (если ты хипстер, то крайне рекомендуется)

Спасибо.

Вынужден признаться, что не знаю как воспользоваться Вашим советом, подскажите, где про это почитать?

stD
() автор топика
Ответ на: комментарий от anonymous

Только сначала нужно прочитать ман strncpy и понять, что это вовсе не то, о чем ты подумал, и что маны нужно таки читать. Логичнее взять strlcpy или snprintf, например.

Наркоман? Или просто идиот? strlcpy нестандартная функция, snprintf очевидно слишком нерационален, а так вызвал strncpy, выставил 0 в конце и получил быстрый и безопасный вариант.

anonymous
()
Ответ на: комментарий от Deleted

Разработка проще и быстрее.

Но я же хочу си изучать.

stD
() автор топика
Ответ на: комментарий от anonymous

strncpy это функция для работы с fixed-width non-terminated, которая добивает поле нулями вплоть до [n].
К сишным строкам и рациональности она не имеет никакого отношения года с 1970, кретин.

anonymous
()
Ответ на: комментарий от anonymous

выставил 0 в конце

Что значит «выставил 0 в конце»?

Вот так:

strncpy(PORT, argv[1], 0);
Но так ведь не будет работать...

stD
() автор топика
Ответ на: комментарий от stD

На стороне клиента рано или поздно стрельнет таймаут, а на стороне сервера не знаю.

anonymous
()
Ответ на: комментарий от stD

Например, можно почитать документацию этих библиотек. Общий смысл такой: у тебя есть цикл сообщений, в котором ты регистрируешь обработчики событий (на слушающем сокете появился клиент; в клиентском сокете появились данные для чтения; и т.п.). В результате сколько бы клиентов не подключилось к серверу, все работает в одном потоке, и не нужны никакие синхронизации.

annulen ★★★★★
()
Ответ на: комментарий от Eddy_Em

Нафиг оно нужно — еще клиентом париться?

Можно хоть из башевого скрипта через nc данные отправлять. Это же намного более Ъ, чем каким-то веб-говном пользоваться, не так ли?

Делать нефиг, очереди перебирать?

Жжешь, пеши исчо.

annulen ★★★★★
()
Ответ на: комментарий от annulen

Например, можно почитать документацию этих библиотек.

Спасибо, читаю.

stD
() автор топика
Ответ на: комментарий от stD

Да не парься ты, сделай так:

char *PORT, *device;
...
    PORT = strdup(argv[1]);
    device = strdup(argv[2]);

А вообще, если ты не вызываешь всякие getopt_long, которые могут изменить argv/argc, то нефиг дурью маяться:

    PORT = argv[1];
    device = argv[2];

Но таки я бы посоветовал сразу разруливать опции при помощи getopt/getopt_long. Или даже сделать обертку вроде моей (я сделал эту обертку для того, чтобы уменьшить вероятность пропустить что-нибудь при добавлении/удалении аргументов, здесь и справка, и длинные, и короткие аргументы в одной куче + есть возможность обрабатывать аргументы с подаргументами; единственный косяк — нет возможности несколько раз один и тот же аргумент писать).

Eddy_Em ☆☆☆☆☆
()
Ответ на: комментарий от stD

strdup вызывает malloc и делает strcpy, т.е. заранее думать о размере буфера не нужно. snprintf в данной ситуации вообще нафиг не нужен, т.к. тебе просто нужно куда-то скопировать строку, а не заниматься форматным выводом.

Eddy_Em ☆☆☆☆☆
()
Ответ на: комментарий от stD

strndup — то же самое, за исключением того, что если у тебя строка без заключительного нуля, проблем при копировании не будет.

Eddy_Em ☆☆☆☆☆
()
Ответ на: комментарий от Eddy_Em

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

stD
() автор топика
Ответ на: комментарий от stD

Забыл спросить, а после применения strdup();, нужно ли применять free ();?

stD
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.