Слушайте несколько портов с одного сервера

Возможно ли связать и прослушать несколько портов в Linux в одном приложении?

Ответ 1

Для каждого порта, который вы хотите прослушать, вы:

  • Создайте отдельный сокет с socket.
  • Привяжите его к соответствующему порту с помощью bind.
  • Вызовите listen в сокете, чтобы он настроил очередь для прослушивания.

В этот момент ваша программа прослушивает несколько сокетов. Чтобы принимать соединения на этих сокетах, вам нужно знать, к какому сокету подключается клиент. Это где select входит. Как это бывает, у меня есть код, который делает именно это, сидя здесь, так что здесь полный проверенный пример ожидания соединений в нескольких сокетах и ​​возврата файлового дескриптора соединения. Удаленный адрес возвращается в дополнительных параметрах (буфер должен быть предоставлен вызывающим абонентом, как и accept).

(socket_type здесь typedef для int в системах Linux, а INVALID_SOCKET - -1. Это происходит потому, что этот код также был перенесен в Windows.)

socket_type
network_accept_any(socket_type fds[], unsigned int count,
                   struct sockaddr *addr, socklen_t *addrlen)
{
    fd_set readfds;
    socket_type maxfd, fd;
    unsigned int i;
    int status;

    FD_ZERO(&readfds);
    maxfd = -1;
    for (i = 0; i < count; i++) {
        FD_SET(fds[i], &readfds);
        if (fds[i] > maxfd)
            maxfd = fds[i];
    }
    status = select(maxfd + 1, &readfds, NULL, NULL, NULL);
    if (status < 0)
        return INVALID_SOCKET;
    fd = INVALID_SOCKET;
    for (i = 0; i < count; i++)
        if (FD_ISSET(fds[i], &readfds)) {
            fd = fds[i];
            break;
        }
    if (fd == INVALID_SOCKET)
        return INVALID_SOCKET;
    else
        return accept(fd, addr, addrlen);
}

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

Ответ 2

Вы только bind() для одного сокета, затем listen() и accept() - сокет для привязки для сервера, fd из accept() - для клиента. Вы делаете свой выбор на последнем, ища какой-либо клиентский сокет, на котором есть данные, ожидающие ввода.

Ответ 3

В такой ситуации вас может заинтересовать libevent. Он выполнит работу select() для вас, возможно, используя гораздо лучший интерфейс, например epoll().

Огромным недостатком select() является использование макросов FD_..., которые ограничивают число сокетов максимальным количеством бит в переменной fd_set (от 100 до 256). Если у вас небольшой сервер с 2 или 3 подключениями, все будет в порядке. Если вы намереваетесь работать на гораздо большем сервере, то fd_set может легко переполняться.

Кроме того, использование select() или poll() позволяет избежать потоков на сервере (т.е. вы можете poll() сокет и знать, можете ли вы accept(), read() или write() к ним.)

Но если вы действительно хотите сделать это как Unix, тогда вы хотите рассмотреть fork() -ing перед вызовом accept(). В этом случае вам не нужно абсолютно select() или poll() (если вы не слушаете много IP-адресов/портов и хотите, чтобы все дети могли отвечать на любые входящие соединения, но у вас есть недостатки с этими... ядро может отправить вам еще один запрос, когда вы уже обрабатываете запрос, тогда как только с accept() ядро ​​знает, что вы заняты, если не в самом вызове accept() - ну, это не работает точно так, но как пользователь, так, как он работает для вас.)

С помощью fork() вы создаете сокет в основном процессе и затем вызываете handle_request() в дочернем процессе для вызова функции accept(). Таким образом, у вас может быть любое количество портов и один или несколько детей для прослушивания каждого из них. Это лучший способ очень быстро реагировать на любое входящее соединение под Linux (то есть как пользователь и до тех пор, пока у вас есть дочерние процессы, ждут клиента, это мгновенно.)

void init_server(int port)
{
    int server_socket = socket();
    bind(server_socket, ...port...);
    listen(server_socket);
    for(int c = 0; c < 10; ++c)
    {
        pid_t child_pid = fork();
        if(child_pid == 0)
        {
            // here we are in a child
            handle_request(server_socket);
        }
    }

    // WARNING: this loop cannot be here, since it is blocking...
    //          you will want to wait and see which child died and
    //          create a new child for the same `server_socket`...
    //          but this loop should get you started
    for(;;)
    {
        // wait on children death (you'll need to do things with SIGCHLD too)
        // and create a new children as they die...
        wait(...);
        pid_t child_pid = fork();
        if(child_pid == 0)
        {
            handle_request(server_socket);
        }
    }
}

void handle_request(int server_socket)
{
    // here child blocks until a connection arrives on 'server_socket'
    int client_socket = accept(server_socket, ...);
    ...handle the request...
    exit(0);
}

int create_servers()
{
    init_server(80);   // create a connection on port 80
    init_server(443);  // create a connection on port 443
}

Обратите внимание, что функция handle_request() показана здесь как обработка одного запроса. Преимущество обработки одного запроса заключается в том, что вы можете сделать это способом Unix: распределите ресурсы по мере необходимости и после ответа на запрос exit(0). exit(0) вызовет для вас необходимые close(), free() и т.д.

В отличие от этого, если вы хотите обрабатывать несколько запросов в строке, вы должны убедиться, что ресурсы освобождены, прежде чем вы вернетесь к вызову accept(). Кроме того, функция sbrk() почти не будет вызываться для уменьшения объема памяти вашего ребенка. Это означает, что время от времени оно будет расти немного. Вот почему сервер, такой как Apache2, настроен на то, чтобы отвечать на определенное количество запросов на каждого ребенка перед запуском нового ребенка (по умолчанию он составляет от 100 до 1000 в эти дни.)