"Тонкий" клиентВ предыдущей заметке Gregory Liokumovich рассказывал о применении событийной модели WinSock для программирования сетевых приложений. На самом деле, как мне кажется, организация сетевой программы не должна зависеть от используемой версии API. То есть, вычислительная часть и непосредственно вызовы сетевых функций должны быть максимально разделены. При этом, я не считаю абстракцию следующего вида (для клиента):
class ClientSocket
{
public:
open( /* ... */ );
bind( /* ... */ );
close( /* ... */ );
read( /* ... */ );
write( /* ... */ );
};
удовлетворительной. Связано это с тем, что по сути такой класс ничуть не скрывает деталей реализации, а, наоборот, подчеркивает их. На мой взгляд, организация взаимодействия между клиентом и сервером не должна содержать в себе "пересылки данных", а должна проектироваться исходя из сообщений, которыми те обмениваются. Следовательно, должен быть какой-то метод скрыть в себе детали пересылки таким образом, что бы программист, использующий код впоследствии даже не догадывался об использовании сокетов. На самом деле, "не догадываться" --- это уже слишком, а вот сделать так, что бы можно было хотя бы не особенно задумываться, вполне возможно. То что будет написано ниже не стоит рассматривать как панацею от всех бед; просто пример использования. Выделим две сущности: "соединение" и "менеджер". Соединение будет "знать" о том, как обрабатывать конкретный дескриптор, а менеджер будет заниматься управлением соединений, т.е. ожидать событий и передавать управление тому соединению, для которого было получено событие нужного типа. Соединение
Начнем с соединения, т.е. определим класс
Будем рассматривать объект класса
enum
{
connecting,
writing,
reading,
closing
} state;
Я думаю, понятно, что каждое из этих состояний будет характеризовать.
Одно "но": в списке этих состояний отображены только
состояния, существенные для менеджера соединений, остальные
нас пока что не интересуют. Опишем класс
class Connection
{
friend class Downloader;
public:
Connection(const ip_address& ia, unsigned int p /* , ... */ );
virtual ~Connection();
private:
int fd;
ip_address ip_addr;
unsigned int port;
time_t last_accessed;
bool open();
void close();
void read();
void write();
void connected();
enum
{
/*
* ...
*/
} state;
};
Несложно заметить, что в нем присутствуют методы
Деструктор сделан виртуальным для
того, что бы сделать возможным расширять возможности
На всякий случай приведу внутреннее устройство функций типа
bool Connection::open()
{
assert(fd == -1);
struct sockaddr_in servaddr;
fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) return false;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(port);
servaddr.sin_addr.s_addr = ip_addr;
int flags = fcntl(fd, F_GETFL, 0);
if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1)
{
::close(fd);
fd = -1;
return false;
}
if(connect(fd, (sockaddr*)&servaddr, sizeof(servaddr)) != 0)
{
if(errno == EINPROGRESS)
{
state = connecting;
}
else
{
::close(fd);
fd = -1;
return false;
}
}
else
state = writing;
time(&last_accessed);
return true;
}
Функция
В случае, если соединение с хостом прошло успешно, то реакцией на это событие будет:
void Connection::connected()
{
assert(state == connecting);
socklen_t n;
int error;
n = sizeof(error);
if(getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &n) < 0 || error != 0)
{
state = closing;
}
else
state = writing;
time(&last_accessed);
}
Хочу заметить, что вместо того, чтобы вызвать Соответственно, при завершении соединения, нужно закрыть дескриптор:
void Connection::close()
{
if(fd == -1)
return;
::close(fd);
fd = -1;
}
Стоит обратить внимание на отсутствие изменения Перейдем к чтению и записи. Эти операции я привожу для демонстрации того, как происходит работа с сокетами. Итак, сначала чтение из сокета:
void Connection::read()
{
int readed;
char buf[4096];
bool working = true;
assert(state == reading);
for( ; working ; )
{
readed = ::read(fd, buf, sizeof(buf));
switch(readed)
{
case 0:
{
/*
* Соединение завершено.
*/
state = closing;
working = false;
break;
}
case -1:
{
/*
* Либо ошибка соединения, либо
* надо подождать прихода новых данных.
*/
if(errno == EAGAIN || errno == EINTR)
{
// Пусто.
}
else
state = closing;
working = false;
break;
}
default:
{
assert(readed > 0);
response += std::string(buf, readed);
}
}
}
time(&last_accessed);
}
Начнем с того, что здесь как раз, как мне кажется, вместо использования
флага
Опять же, перевод состояния в
void Connection::write()
{
ssize_t written;
assert(state == writing);
written = ::write(fd, request.c_str() + write_pos, request.length() - write_pos);
if(written == 0)
{
if(errno == EAGAIN || errno == EINTR)
{
/*
* Это нормально, можно будет повторить вызов.
*/
}
else
{
/*
* Все плохо, прекращаем запись.
*/
state = closing;
}
}
else
{
write_pos += written;
if(written >= request.length())
{
/*
* Запись закончилась.
*/
shutdown(fd, 1);
state = reading;
}
}
time(&last_accessed);
}
Менеджер соединений
Функционально менеджер соединений представляет из
себя отдельный поток управления, который будет заниматься вызовом
class ConnectionsManager
{
/*
* ...
*/
private:
void* process();
typedef std::map<int, Connection*> connections_container;
typedef std::queue<Connection*> output_queue_container;
typedef std::queue<Connection*> input_queue_container;
connections_container connections;
output_queue_container output_queue;
input_queue_container input_queue;
bool finish;
/*
* ...
*/
};
Реальная функция потока (для реализации pthreads) принимает в качестве аргумента указатель,
который определяет объект класса
void* ConnectionsManager::process()
{
fd_set rfds, wfds;
struct timeval tv;
tv.tv_sec = 0; tv.tv_usec = 500;
/*
* Здесь таймаут задан жестко внутри кода. На самом деле,
* он как-то задается через параметры программы.
*/
const int timeout = 30;
for( ; ; )
{
Функция логически делится на две части: до
FD_ZERO(&rfds);
FD_ZERO(&wfds);
int max_fd = -1;
Connection* to_add;
for( ; (to_add = get_from_input_queue()) ; )
{
if(to_add->open())
connections[to_add->getDescriptor()] = to_add;
else
put_in_output_queue(to_add);
}
for(connections_container::iterator i = connections.begin();
i != connections.end(); )
{
switch(i->second->state)
{
case Connection::reading:
{
if(max_fd < i->first)
max_fd = i->first;
FD_SET(i->first, &rfds);
i++;
break;
}
case Connection::writing:
{
if(max_fd < i->first)
max_fd = i->first;
FD_SET(i->first, &wfds);
i++;
break;
}
case Connection::closing:
{
i->second->close();
put_in_output_queue(i->second);
connections_container::iterator j = i;
i++;
connections.erase(j);
break;
}
case Connection::connecting:
{
if(max_fd < i->first)
max_fd = i->first;
FD_SET(i->first, &wfds);
FD_SET(i->first, &rfds);
i++;
break;
}
}
}
if(max_fd == -1)
{
if(finish)
break;
sched_yield();
continue;
}
select(max_fd + 1, &rfds, &wfds, NULL, &tv);
Часть после
for(connections_container::iterator i = connections.begin();
i != connections.end(); i++)
{
switch(i->second->state)
{
case Connection::reading:
{
if (FD_ISSET(i->first, &rfds))
i->second->read();
break;
}
case Connection::writing:
{
if (FD_ISSET(i->first, &wfds))
i->second->write();
break;
}
case Connection::connecting:
{
if(FD_ISSET(i->first, &wfds) || FD_ISSET(i->first, &rfds))
i->second->connected();
break;
}
case Connection::closing:
{
break;
}
}
if(!(FD_ISSET(i->first, &wfds) || FD_ISSET(i->first, &rfds)))
if(time(NULL) > (i->second->last_accessed + timeout))
i->second->state = Connection::closing;
}
return NULL;
}
В качестве послесловия к этой функции хочется отметить, что установку
блокировок из нее (а это, все-таки, многопоточная программа) я удалил
специально. А вот
Подводя итоги, хочется поговорить об ограничениях. Все функции, которые
выполняются над соединениями, выполняются в том же потоке, что и РезюмеВ общем, конечно же предложенная схема это далеко не идеал. Но целью этого опуса я ставил немного другое: хотелось показать то, что можно разделить обработку соединений в техническом и логическом смыслах. Вообще говоря, эта схема хороша тем, что ее (немного усложнив) можно выделить в отдельную библиотеку и использовать для, я думаю, большинства клиентских приложений, которые тратят большую часть времени на работу с сетью. Т.е., получается мечта программиста: code reuse. Опять же, схема не нова и много программистов независимо друг от друга "изобретают" что-то в этом духе.
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
© 2000-2008, Andrey L. Kalinin mailto:andrey@kalinin.ru |
|