Программирование сокетов на C / C ++: обработка нескольких клиентов на сервере без многопоточности

Опубликовано: 15 Июля, 2021

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

  • Потоки сложно кодировать, отлаживать и иногда они дают непредсказуемые результаты.
  • Служебное переключение контекста
  • Не масштабируется для большого количества клиентов
  • Могут возникнуть взаимоблокировки

Выбирать()

Лучший способ работать с несколькими клиентами - использовать команду select () linux.

  • Команда Select позволяет отслеживать несколько файловых дескрипторов, ожидая, пока один из файловых дескрипторов станет активным.
  • Например, если есть какие-то данные для чтения в одном из сокетов, select предоставит эту информацию.
  • Select работает как обработчик прерывания, который активируется, как только дескриптор файла отправляет какие-либо данные.

Структура данных, используемая для выбора: fd_set
Он содержит список файловых дескрипторов, которые нужно отслеживать на предмет некоторых действий.
С fd_set связаны четыре функции:

fd_set readfds;

// Очистить fd_set
FD_ZERO (& readfds);  

// Добавляем дескриптор в fd_set
FD_SET (master_sock, & readfds);   

// Удаляем дескриптор из fd_set
FD_CLR (master_sock, & readfds); 

// Если что-то случилось с главным сокетом, то это входящее соединение  
FD_ISSET (master_sock, & readfds); 

Активация select: прочтите справочную страницу для select, чтобы проверить все аргументы для команды select.

activity = select (max_fd + 1, & readfds, NULL, NULL, NULL);

Выполнение:




//Example code: A simple server side code, which echos back the received message.
//Handle multiple socket connections with select and fd_set on Linux
#include <stdio.h>
#include <string.h> //strlen
#include <stdlib.h>
#include <errno.h>
#include <unistd.h> //close
#include <arpa/inet.h> //close
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/time.h> //FD_SET, FD_ISSET, FD_ZERO macros
#define TRUE 1
#define FALSE 0
#define PORT 8888
int main( int argc , char *argv[])
{
int opt = TRUE;
int master_socket , addrlen , new_socket , client_socket[30] ,
max_clients = 30 , activity, i , valread , sd;
int max_sd;
struct sockaddr_in address;
char buffer[1025]; //data buffer of 1K
//set of socket descriptors
fd_set readfds;
//a message
char *message = "ECHO Daemon v1.0 " ;
//initialise all client_socket[] to 0 so not checked
for (i = 0; i < max_clients; i++)
{
client_socket[i] = 0;
}
//create a master socket
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 " , 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)
{
//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" );
}
//If something happened on the master socket ,
//then its an incoming connection
if (FD_ISSET(master_socket, &readfds))
{
if ((new_socket = accept(master_socket,
( struct sockaddr *)&address, (socklen_t*)&addrlen))<0)
{
perror ( "accept" );
exit (EXIT_FAILURE);
}
//inform user of socket number - used in send and receive commands
printf ("New connection , socket fd is %d , ip is : %s , port : %d
" , new_socket , inet_ntoa(address.sin_addr) , ntohs
(address.sin_port));
//send new connection greeting message
if ( send(new_socket, message, strlen (message), 0) != strlen (message) )
{
perror ( "send" );
}
puts ( "Welcome message sent successfully" );
//add new socket to array of sockets
for (i = 0; i < max_clients; i++)
{
//if position is empty
if ( client_socket[i] == 0 )
{
client_socket[i] = new_socket;
printf ( "Adding to list of sockets as %d " , i);
break ;
}
}
}
//else its some IO operation on some other socket
for (i = 0; i < max_clients; i++)
{
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 " ,
inet_ntoa(address.sin_addr) , ntohs(address.sin_port));
//Close the socket and mark as 0 in list for reuse
close( sd );
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] = '' ;
send(sd , buffer , strlen (buffer) , 0 );
}
}
}
}
return 0;
}

Скомпилируйте файл и запустите сервер.
Используйте telnet для подключения к серверу в качестве клиента.

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

 telnet localhost 8888

Пояснение к коду:

  • Мы создали переменную readfds fd_set, которая будет отслеживать все активные файловые дескрипторы клиентов плюс дескрипторы основного слушающего сокета сервера.
  • Каждый раз, когда подключается новый клиент, master_socket будет активирован, и для этого клиента будет открыт новый fd. Мы сохраним его fd в нашем client_list и на следующей итерации добавим его в readfds для отслеживания активности этого клиента.
  • Точно так же, если старый клиент отправляет некоторые данные, readfds будет активирован, и мы проверим из списка существующих клиентов, какой клиент отправил данные.

    Альтернативы:
    Есть и другие функции, которые могут выполнять задачи, похожие на выбор. pselect, poll, ppoll

    Эта статья предоставлена Акшатом Синхой . Если вам нравится GeeksforGeeks, и вы хотели бы внести свой вклад, вы также можете написать статью с помощью provide.geeksforgeeks.org или отправить ее по электронной почте на deposit@geeksforgeeks.org. Посмотрите, как ваша статья появляется на главной странице GeeksforGeeks, и помогите другим гикам.

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

C++ C