C TCP сервер: FD_ISSET() не всегда работает на базовом сервере

#c #sockets #tcp

#c #розетки #tcp

Вопрос:

Это моя реализация будущего HTTP-сервера.

 void    Webserv::resetFdSets()
{

    int         fildes;

    FD_ZERO(amp;to_read);
    FD_ZERO(amp;to_write);
    max_fd = -1;
    Server_map::iterator it;
    for (it = servers.begin(); it != servers.end();   it) //set server sockets to be read
    {
        fildes = it->second.getFd();
        FD_SET(fildes, amp;to_read);
        if (fildes > max_fd)
            max_fd = fildes;
    }
    std::list<int>::iterator iter;
    for (iter = accepted.begin(); iter != accepted.end();   iter) // set client sockets if any
    {
        fildes = (*iter);
        FD_SET(fildes, amp;to_read);
        FD_SET(fildes, amp;to_write);
        if (fildes > max_fd)
            max_fd = fildes;
    }
}
 

принять ()

 void    Webserv::acceptConnections()
{

   int         client_fd;
   sockaddr_in cli_addr;
   socklen_t   cli_len = sizeof(sockaddr_in);

   Server_map::iterator    it;
   for (it = servers.begin(); it != servers.end();   it)
   {
       ft_bzero(amp;cli_addr, sizeof(cli_addr));
       if (FD_ISSET(it->second.getFd(), amp;to_read)) // if theres data in server socket
       {
           client_fd = accept(it->second.getFd(), reinterpret_cast<sockaddr*>(amp;cli_addr), amp;cli_len);
           if (client_fd > 0) // accept and add client
           {
               fcntl(client_fd, F_SETFL, O_NONBLOCK);
               accepted.push_back(client_fd);
           }
           else
               throw (std::runtime_error("accept() failed"));
       }
    }
}
 

recv()

 void    Webserv::checkReadSockets()
{

    char        buf[4096];
    std::string raw_request;
    ssize_t     bytes;
    static int  connections;

    std::list<int>::iterator    it;
    for (it = accepted.begin(); it != accepted.end();   it)
    {
        if (FD_ISSET(*it, amp;to_read))
        {
              connections;
            std::cout << "Connection counter : " << connections << std::endl;
            while ((bytes = recv(*it, buf, sizeof(buf) - 1, MSG_DONTWAIT)) > 0)
            {
                buf[bytes] = '';
                raw_request  = buf;
            }
            std::cout << raw_request << std::endl;
        }
    }
}
 

отправить ()

 void    Webserv::checkWriteSockets()
{

   char            buf[8096];
   char            http_success[] = {
       "HTTP/1.1 200 OKrn"
       "Server: This op serverrn"
       "Content-Length: 580rn"
       "Content-Type: text/htmlrn"
       "Connection: Closedrnrn" 
   };

   std::list<int>::iterator it = accepted.begin();
   while (it != accepted.end())
   {
       int cliSock = (*it);
       if (FD_ISSET(cliSock, amp;to_write))
       {
           int     response_fd = open("hello.html", O_RDONLY);

           int bytes = read(response_fd, buf, sizeof(buf));
           send(cliSock, http_success, sizeof(http_success) - 1, MSG_DONTWAIT); // header
           send(cliSock, buf, bytes, MSG_DONTWAIT); // hello world

           close(cliSock);
           close(response_fd);
        
           it = accepted.erase(it);
       }
       else
             it;
   }
}
 

main loop:

 void    Webserv::operate()
{

   int     rc;

   while (true)
   {
       resetFdSets(); // add server and client fd for select()
       if ((rc = select(max_fd   1, amp;to_read, amp;to_write, NULL, NULL)) > 0) // if any of the ports are active
       {
           acceptConnections(); // create new client sockets with accept()
           checkReadSockets(); // parse and route client requests recv() ...
           checkWriteSockets(); // send() response and delete client
       }
       else if (rc < 0)
       {
           std::cerr << "select() failed" << std::endl;
           exit(1);
       }
   }
}
 

Тот же код выделил определение класса Webserv в pastebin: https://pastebin.com/9B8uYumF

На данный момент алгоритм таков:

  • сбросьте настройки fd_sets для чтения и записи.
  • если какой-либо из файловых дескрипторов запускает select, мы проверяем, установлены ли FD сервера, принимаем соединения и сохраняем их в списке целых чисел.
  • если какой-либо из клиентских FD является FD_ISSET() для чтения — мы считываем их запрос и печатаем его.
  • наконец, если какой-либо из клиентских FD готов к приему — мы помещаем туда html-страницу и закрываем соединение.

При проверке всех дескрипторов клиента FD_ISSET() возвращает false в 80% случаев, когда этого не должно быть. Следовательно, я не могу получать запросы клиентов последовательно. Как ни странно, он возвращает true с помощью fd_set для сокетов записи гораздо чаще. Вот демонстрационная версия с 2 из 10 успешных чтений с использованием приведенного выше кода: https://imgur.com/a/nzc21LV

Я знаю, что новые клиенты всегда принимаются, потому что он всегда вводит условие if в acceptConnections() . Другими словами, listen FD инициализируются правильно.

редактировать: Вот как он инициализируется:

 void    Server::init()
{
    if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        throw (std::runtime_error("socket() failed"));
    }
    fcntl(listen_fd, F_SETFL, O_NONBLOCK);

    int yes = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, amp;yes, sizeof(int));
    if (bind(listen_fd, reinterpret_cast<sockaddr*>(amp;server_addr), sizeof(server_addr)))
    {
        throw (std::runtime_error("bind() failed"));
    }

    if (listen(listen_fd, 0))
        throw (std::runtime_error("listen() failed"));
}
 

Комментарии:

1. Это даже отдаленно не похоже на действительный HTTP-сервер. Вы вообще не анализируете HTTP-запросы, и в вашем HTTP-ответе содержится ошибка. Но что касается обработки fd_set s, вы игнорируете возвращаемое значение recv() для обнаружения отключений / ошибок. И причина FD_ISSET , по которой возвращает true для возможности записи больше, чем для чтения, заключается просто в том, что сокеты обычно находятся в состоянии, доступном для записи, только когда заполняется входящий буфер удаленного узла, они не находятся в состоянии, доступном для записи, пока в буфере не освободится место.

2. Ваша обработка HTTP неверна, что приводит к тому, что остальная часть вашего кода демонстрирует поведение, которого вы не ожидаете. Исправьте обработку HTTP-запросов, чтобы определять, когда чтение запросов фактически завершено (см. RFC 2616, Раздел 4.4 ) перед отправкой ответов, не полагайтесь на select() сообщение о том, когда отправлять ответы. Это неправильная логика для использования. Но поскольку вы используете неблокирующие сокеты, вам нужно будет буферизировать свои ответы и select() сообщать вам, когда вы можете отправлять буферы.

3. @RemyLebeau Спасибо за ответ. Я знаю, что над этим еще нужно поработать, но для анализа HTTP-запросов мне нужно, чтобы они действительно считывались последовательно, не так ли? 🙂 Я намеренно игнорирую возвращаемое значение: поскольку сокеты неблокирующие, recv() в какой-то момент вернет -1 с EAGAIN . Итак, мои мысли были прочитаны, пока я могу> проанализировать все, что было прочитано> ответить, но я думаю, что это не сработает.

4. вы не сбрасываете raw_request настройки для каждого нового клиента, с которого вы читаете. Вы не проводите синтаксический raw_request анализ, чтобы узнать, КОГДА прекратить чтение, согласно RFC (использование тайм-аута блокировки — это не выход). Вы не проверяете recv() сбой / отключение. Любая ошибка чтения, отличная от EWOULDBLOCK или EAGAIN (которые не всегда имеют одинаковое значение на всех платформах), требует немедленного закрытия клиентского сокета и удаления его из accepted списка. Вся ваша checkWriteSockets() обработка неверна и подвержена ошибкам. Даже перед исправлением HTTP-содержимого, ваша базовая обработка TCP требует дополнительной работы.

5. Я бы предложил изменить ваш accepted список на хранение объектов, где каждый объект имеет дескриптор сокета и 2 string буфера, один для входящих и один для исходящих. Все recv() возвращаемые данные поступают во входящий буфер и анализируются на предмет запросов. Для каждого завершенного запроса удаляйте его из входящего буфера, обрабатывайте и помещайте ответ в исходящий буфер. Проверяйте select() возможность записи только для клиентов с непустым исходящим буфером. Когда select() отчеты клиента доступны для записи, отправляйте и удаляйте данные из исходящего буфера этого клиента до тех пор, пока они не будут очищены или send() не завершатся сбоем.