Третий способ основан на использовании неблокирующих сокетов (nonblocking sockets) и функции select. Сначала разберёмся, что такое неблокирующие сокеты. Сокеты, которые мы до сих пор использовали, являлись блокирующими (blocking). Это название означает, что на время выполнения операции с таким сокетом ваша программа блокируется. Например, если вы вызвали recv, а данных на вашем конце соединения нет, то в ожидании их прихода ваша программа "засыпает". Аналогичная ситуация наблюдается, когда вы вызываете accept, а очередь запросов на соединение пуста. Это поведение можно изменить, используя функцию fcntl.
#include <unistd.h>
#include <fcntl.h>
sockfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);
Эта несложная операция превращает сокет в неблокирующий. Вызов любой функции с таким сокетом будет возвращать управление немедленно. Причём если затребованная операция не была выполнена до конца, функция вернёт -1 и запишет в errno значение EWOULDBLOCK. Чтобы дождаться завершения операции, мы можем опрашивать все наши сокеты в цикле, пока какая-то функция не вернёт значение, отличное от EWOULDBLOCK. Как только это произойдёт, мы можем запустить на выполнение следующую операцию с этим сокетом и вернуться к нашему опрашивающему циклу. Такая тактика (называемая в англоязычной литературе polling) работоспособна, но очень неэффективна, поскольку процессорное время тратится впустую на многократные (и безрезультатные) опросы.
|
|
Чтобы исправить ситуацию, используют функцию select. Эта функция позволяет отслеживать состояние нескольких файловых дескрипторов (а в Unix к ним относятся и сокеты) одновременно.
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int n, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(int fd);
Функция select работает с тремя множествами дескрипторов, каждое из которых имеет тип fd_set. В множество readfds записываются дескрипторы сокетов, из которых нам требуется читать данные (слушающие сокеты добавляются в это же множество). Множество writefds должно содержать дескрипторы сокетов, в которые мы собираемся писать, а exceptfds - дескрипторы сокетов, которые нужно контролировать на возникновение ошибки. Если какое-то множество вас не интересуют, вы можете передать вместо указателя на него NULL. Что касается других параметров, в n нужно записать максимальное значение дескриптора по всем множествам плюс единица, а в timeout - величину таймаута. Структура timeval имеет следующий формат.
struct timeval {
int tv_sec; // секунды
int tv_usec; // микросекунды
};
Поле "микросекунды" смотрится впечатляюще. Но на практике вам не добиться такой точности измерения времени при использовании select. Реальная точность окажется в районе 100 миллисекунд.
|
|
Теперь займёмся множествами дескрипторов. Для работы с ними предусмотрены функции FD_XXX, показанные выше; их использование полностью скрывает от нас детали внутреннего устройства fd_set. Рассмотрим их назначение.
FD_ZERO(fd_set *set) - очищает множество set
FD_SET(int fd, fd_set *set) - добавляет дескриптор fd в множество set
FD_CLR(int fd, fd_set *set) - удаляет дескриптор fd из множества set
FD_ISSET(int fd, fd_set *set) - проверяет, содержится ли дескриптор fd в множестве set
Если хотя бы один сокет готов к выполнению заданной операции, select возвращает ненулевое значение, а все дескрипторы, которые привели к "срабатыванию" функции, записываются в соответствующие множества. Это позволяет нам проанализировать содержащиеся в множествах дескрипторы и выполнить над ними необходимые действия. Если сработал таймаут, select возвращает ноль, а в случае ошибки -1. Расширенный код записывается в errno.
Программы, использующие неблокирующие сокеты вместе с select, получаются весьма запутанными. Если в случае с fork мы строим логику программы, как будто клиент всего один, здесь программа вынуждена отслеживать дескрипторы всех клиентов и работать с ними параллельно. Чтобы проиллюстрировать эту методику, я в очередной раз переписал код сервера с использованием select. Новая версия приведена в листинге 3. Эта программа, также написана на C++ (а не на C). В программе использовался класс set из библиотеки STL языка C++, чтобы облегчить работу с набором дескрипторов и сделать её более понятной.
Листинг 3. Код сервера (неблокирующие сокеты и select).
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <algorithm>
#include <set>
using namespace std;
int main()
{
int listener;
struct sockaddr_in addr;
char buf[1024];
int bytes_read;
listener = socket(AF_INET, SOCK_STREAM, 0);
if(listener < 0)
{
perror("socket");
exit(1);
}
fcntl(listener, F_SETFL, O_NONBLOCK);
addr.sin_family = AF_INET;
addr.sin_port = htons(3425);
addr.sin_addr.s_addr = INADDR_ANY;
if(bind(listener, (struct sockaddr *)&addr, sizeof(addr)) < 0)
{
perror("bind");
exit(2);
}
listen(listener, 2);
set<int> clients;
clients.clear();
while(1)
{
// Заполняем множество сокетов
fd_set readset;
FD_ZERO(&readset);
FD_SET(listener, &readset);
for(set<int>::iterator it = clients.begin(); it!= clients.end(); it++)
FD_SET(*it, &readset);
// Задаём таймаут
timeval timeout;
timeout.tv_sec = 15;
timeout.tv_usec = 0;
// Ждём события в одном из сокетов
int mx = max(listener, *max_element(clients.begin(), clients.end()));
if(select(mx+1, &readset, NULL, NULL, &timeout) <= 0)
{
perror("select");
exit(3);
}
// Определяем тип события и выполняем соответствующие действия
if(FD_ISSET(listener, &readset))
{
// Поступил новый запрос на соединение, используем accept
int sock = accept(listener, NULL, NULL);
if(sock < 0)
{
perror("accept");
exit(3);
}
fcntl(sock, F_SETFL, O_NONBLOCK);
clients.insert(sock);
}
for(set<int>::iterator it = clients.begin(); it!= clients.end(); it++)
{
if(FD_ISSET(*it, &readset))
{
// Поступили данные от клиента, читаем их
bytes_read = recv(*it, buf, 1024, 0);
if(bytes_read <= 0)
{
// Соединение разорвано, удаляем сокет из множества
close(*it);
clients.erase(*it);
continue;
}
// Другие лействия сервера
} }
} return 0;
}
14. Программирование сокетов. Способы повышения эффективности обмена данными.