Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

«Boost.Asio C Network Programming». Глава 4: Заказчик и Сервер

Anna | 24.06.2014 | нет комментариев

Продолжаю перевод книги John Torjo «Boost.Asio C Network Programming».

Оглавление:

В этой главе мы собираемся углубиться в создание нетривиальных клиент/серверных приложений с применением Boost.Asio. Вы можете запускать и тестировать их, и как только вы разберетесь в них, вы сумеете применять их как основу для создания собственных приложений.


В следующих приложениях:

  • Заказчик заходит на сервер с именем пользователя (без пароля)
  • Все соединения инициируются заказчиком, где заказчик запрашивает результат от сервера
  • Все запросы и результаты на них заканчиваются символом ‘n’
  • Сервер отключает всякого заказчика, тот, что не пингуется в течение 5 секунд

Заказчик может делать следующие запросы:

  • Получить список всех подключенных заказчиков
  • Заказчик может пинговаться, и когда он припингуется сервер ответить либо ping_ok либо ping client_list_chaned (в последнем случае заказчик вторично запрашивает список подключенных заказчиков).

Для интереса добавим несколько выкрутасов:

  • В всякое клиентское приложение входит 6 подключаемых пользователей, таких как Джон, Джеймс, Люси, Трейси Франк и Эбби.
  • Всякий заказчик проверяет связь с сервером в беспричинный момент времени (раз в 1-7 секунд, таким образом, время от времени соединение с сервером будет разрываться)

Синхронные сервер/клиент

Во-первых, мы реализуем синхронное приложение. Вы увидите, что код является простым и внятным для усваивания. Тем не менее, сетевая часть должна выполняться в отдельном потоке, так как все сетевые вызовы блокируются.

Синхронный заказчик

Синхронный заказчик, как вы и ждали, делает все ступенчато; подключается к серверу, заходит на него, а после этого исполняет цикл связи, а именно уснуть, сделать запрос, прочитать результат сервера, вновь уснуть и так дальше.

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

ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 8001);
void run_client(const std::string & client_name)
{
	talk_to_svr client(client_name);
	try
	{
		client.connect(ep);
		client.loop();
	}
	catch(boost::system::system_error & err)
	{
		std::cout << "client terminated " << std::endl;
	}
}

Дальнейший пример это класс talk_to_svr:

struct talk_to_svr
{
	talk_to_svr(const std::string & username): sock_(service), started_(true), username_(username) {}
	void connect(ip::tcp::endpoint ep)
	{
		sock_.connect(ep);
	}
	void loop()
	{
		write("login "   username_   "n");
		read_answer();
		while ( started_)
		{
			write_request();
			read_answer();
			boost::this_thread::sleep(millisec(rand() % 7000));
		}
	}
	std::string username() const { return username_; }
	...
private:
	ip::tcp::socket sock_;
	enum { max_msg = 1024 };
	int already_read_;
	char buff_[max_msg];
	bool started_;
	std::string username_;
};

В цикле мы легко пингуемся, читаем результат от сервера и засыпаем. Засыпаем мы на неbool timed_out() const { ptime now = microsec_clock::local_time(); long long ms = (now – last_ping).total_milliseconds(); return ms > 5000 ; } void stop() { boost::system::error_code err; sock_.close(err); } void read_request() { if ( sock_.available()) already_read_ = sock_.read_some( buffer(buff_ already_read_, max_msg – already_read_)); } … private: // … same as in Synchronous Client bool clients_changed_; ptime last_ping; };
Приведенный выше код достаточно явствен. Особенно значимая функция это read_request(). Чтение будет протекать только если есть данные, таким образом, сервер никогда не будет заблокирован:

void process_request() 
{
	bool found_enter = std::find(buff_, buff_   already_read_, 'n') < buff_   already_read_;
	if ( !found_enter)
		return; // message is not full
	// process the msg
	last_ping = microsec_clock::local_time();
	size_t pos = std::find(buff_, buff_   already_read_, 'n') - buff_;
	std::string msg(buff_, pos);
	std::copy(buff_   already_read_, buff_   max_msg, buff_);
	already_read_ -= pos   1;
	if ( msg.find("login ") == 0) on_login(msg);
	else if ( msg.find("ping") == 0) on_ping();
	else if ( msg.find("ask_clients") == 0) on_clients();
	else std::cerr << "invalid msg " << msg << std::endl;
}
void on_login(const std::string & msg) 
{
	std::istringstream in(msg);
	in >> username_ >> username_;
	write("login okn");
	update_clients_changed();
}
void on_ping() 
{
	write(clients_changed_ ? "ping client_list_changedn" : "ping okn");
	clients_changed_ = false;
}
void on_clients() 
{
	std::string msg;
	{ 
		boost::recursive_mutex::scoped_lock lk(cs);
		for( array::const_iterator b = clients.begin(), e = clients.end() ;b != e;   b)
			msg  = (*b)->username()   " "; 
	}
	write("clients "   msg   "n");
}
void write(const std::string & msg) { sock_.write_some(buffer(msg)); }

Взгляните на process_request(). Позже того как мы считали те данные, которые были доступны, мы обязаны проверить считали ли мы сообщение до конца (если да, то found_enteris установится в true). Если это так, то мы охраняем себя от чтения, может быть, огромнее чем одного сообщения (позже символа ‘n’ сохраняться в буфер ничего не будет), а после этого мы интерпретируем всецело прочитанное сообщение. Остальная часть кода достаточно примитивна.

Асинхронные сервер/клиент

А сейчас самая увлекательная (и трудная) часть, пойдем асинхронным путем.

Асинхронный заказчик

Вещи теперь будут рассматриваться немножко труднее, но, безоговорочно, управляемые. И у нас будет приложение, которое не блокируется.

Вам теснее должен быть внятен дальнейший код:

#define MEM_FN(x) boost::bind(&self_type::x, shared_from_this())
#define MEM_FN1(x,y) boost::bind(&self_type::x, shared_from_this(),y)
#define MEM_FN2(x,y,z) boost::bind(&self_type::x, shared_from_this(),y,z)
class talk_to_svr : public boost::enable_shared_from_this<talk_to_svr>, boost::noncopyable 
{
	typedef talk_to_svr self_type;
	talk_to_svr(const std::string & username) : sock_(service), started_(true), username_(username), timer_(service) {}
	void start(ip::tcp::endpoint ep) 
	{
		sock_.async_connect(ep, MEM_FN1(on_connect,_1));
	}
public:
	typedef boost::system::error_code error_code;
	typedef boost::shared_ptr<talk_to_svr> ptr;
	static ptr start(ip::tcp::endpoint ep, const std::string & username) 
	{
		ptr new_(new talk_to_svr(username));
		new_->start(ep);
		return new_;
	}
	void stop() 
	{
		if ( !started_) return;
		started_ = false;
		sock_.close();
	}
	bool started() { return started_; }
	...
private:
	size_t read_complete(const boost::system::error_code & err, size_t bytes) 
	{
		if ( err) return 0;
		bool found = std::find(read_buffer_, read_buffer_   bytes, 'n') < read_buffer_   bytes;
		return found ? 0 : 1;
	}
private:
	ip::tcp::socket sock_;
	enum { max_msg = 1024 };
	char read_buffer_[max_msg];
	char write_buffer_[max_msg];
	bool started_;
	std::string username_;
	deadline_timer timer_;
};

Вы увидите дополнительную функцию-таймер deadline_timer для осуществления пинга сервера; и вновь же, мы будем проверять связь с сервером в беспричинный момент времени.
Ну а сейчас посмотрим, как выглядит основная логика класса:

void on_connect(const error_code & err) 
{
	if ( !err) do_write("login "   username_   "n");
	else stop();
}
void on_read(const error_code & err, size_t bytes) 
{
	if ( err) stop();
	if ( !started() ) return;
	// process the msg
	std::string msg(read_buffer_, bytes);
	if ( msg.find("login ") == 0) on_login();
	else if ( msg.find("ping") == 0) on_ping(msg);
	else if ( msg.find("clients ") == 0) on_clients(msg);
}
void on_login() 
{
	do_ask_clients();
}
void on_ping(const std::string & msg) 
{
	std::istringstream in(msg);
	std::string answer;
	in >> answer >> answer;
	if ( answer == "client_list_changed") do_ask_clients();
	else postpone_ping();
}
void on_clients(const std::string & msg) 
{
	std::string clients = msg.substr(8);
	std::cout << username_ << ", new client list:" << clients ;
	postpone_ping();
}

В on_read() в первых 2-х строчках кода сделано все дюже прекрасно. В первой строке мы проверяем, есть ли оплошность, если да, то останавливаемся. Во 2-й строке мы проверяем, остановились ли мы (до этого либо только что), если да, то возвращаемся. В отвратном случае, если все отлично, мы обрабатываем входящее сообщение.
И наконец, функции do_* следующие:

void do_ping() { do_write("pingn"); }
void postpone_ping() 
{
	timer_.expires_from_now(boost::posix_time::millisec(rand() % 7000));
	timer_.async_wait( MEM_FN(do_ping));
}
void do_ask_clients() { do_write("ask_clientsn"); }
void on_write(const error_code & err, size_t bytes) { do_read(); }
void do_read() 
{
	async_read(sock_, buffer(read_buffer_), MEM_FN2(read_complete,_1,_2), MEM_FN2(on_read,_1,_2));
}
void do_write(const std::string & msg) 
{
	if ( !started() ) return;
	std::copy(msg.begin(), msg.end(), write_buffer_);
	sock_.async_write_some( buffer(write_buffer_, msg.size()), MEM_FN2(on_write,_1,_2));
}

Обратите внимание, что всякая операция read вызывает пинг:

  • Когда операция read завершится, вызовется on_read()
  • on_read() перенаправляется в on_login(), on_ping(), либо on_clients()
  • Всякая из функций либо откладывает пинг, либо запрашивает заказчиков
  • Если мы запросим заказчиков, когда их получила операция read, она отложит пинг
Асинхронный сервер

Схема достаточно трудна, вы видите, что от Boost.Asio отходит четыре стрелки к on_accept, on_read, on_write и on_check_ping. В основном это обозначает, что вы никогда не узнаете вызовом какой из этих асинхронных операций все закончится, но вы верно знаете, что это будет одна из них.

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

ip::tcp::acceptor acceptor(service, ip::tcp::endpoint(ip::tcp::v4(), 8001));
void handle_accept(talk_to_client::ptr client, const error_code & err) 
{
	client->start();
	talk_to_client::ptr new_client = talk_to_client::new_();
	acceptor.async_accept(new_client->sock(), 
	boost::bind(handle_accept,new_client,_1));
}
int main(int argc, char* argv[]) 
{
	talk_to_client::ptr client = talk_to_client::new_();
	acceptor.async_accept(client->sock(), boost::bind(handle_accept,client,_1));
	service.run();
}

Приведенный выше код будет неизменно асинхронно ожидать новых заказчиков (всякое новое подключение заказчика будет вызывать другое асинхронное ожидание).
Мы обязаны следить за событием client list changed (подключился новейший заказчик либо один из заказчиков получил список и отключился) и уведомить остальных заказчиков, когда это произойдет. Таким образом, мы обязаны беречь массив заказчиков, в отвратном случае не было бы никакой необходимости в этом массиве, если вы не хотели бы знать всех подключенных заказчиков в данный момент времени:

class talk_to_client; typedef boost::shared_ptr<talk_to_client> client_ptr;
typedef std::vector<client_ptr> array;
array clients;

Скелет класса connection выглядит дальнейшим образом:

class talk_to_client : public boost::enable_shared_from_this<talk_to_
client>, boost::noncopyable 
{
	talk_to_client() { ... }
public:
	typedef boost::system::error_code error_code;
	typedef boost::shared_ptr<talk_to_client> ptr;
	void start() 
	{
		started_ = true;
		clients.push_back( shared_from_this());
		last_ping = boost::posix_time::microsec_clock::local_time();
		do_read(); // first, we wait for client to login
	}
	static ptr new_() { ptr new_(new talk_to_client); return new_; }
	void stop() 
	{
		if ( !started_) return;
		started_ = false;
		sock_.close();
		ptr self = shared_from_this();
		array::iterator it = std::find(clients.begin(), clients.end(), self);
		clients.erase(it);
		update_clients_changed();
	}
	bool started() const { return started_; }
	ip::tcp::socket & sock() { return sock_;}
	std::string username() const { return username_; }
	void set_clients_changed() { clients_changed_ = true; }
	...
private:
	ip::tcp::socket sock_;
	enum { max_msg = 1024 };
	char read_buffer_[max_msg];
	char write_buffer_[max_msg];
	bool started_;
	std::string username_;
	deadline_timer timer_;
	boost::posix_time::ptime last_ping;
	bool clients_changed_;
};

Я вызываю talk_to_client либо talk_to_server из класса connection, Дабы сделать больше ясным то, что я говорю.
Мы обязаны будем применять предшествующий код теперь; он сходствен тому, что мы применяли для клиентского приложения. У нас есть добавочная функция stop(), которая удаляет подключенного заказчика из массива заказчиков.
Сервер безостановочно ждет асинхронных операций чтения:

void on_read(const error_code & err, size_t bytes) 
{
	if ( err) stop();
	if ( !started() ) return;
	std::string msg(read_buffer_, bytes);
	if ( msg.find("login ") == 0) on_login(msg);
	else if ( msg.find("ping") == 0) on_ping();
	else if ( msg.find("ask_clients") == 0) on_clients();
}
void on_login(const std::string & msg) 
{
	std::istringstream in(msg);
	in >> username_ >> username_;
	do_write("login okn");
	update_clients_changed();
}
void on_ping() 
{
	do_write(clients_changed_ ? "ping client_list_changedn" : "ping okn");
	clients_changed_ = false;
}
void on_clients() 
{
	std::string msg;
	for(array::const_iterator b =clients.begin(),e =clients.end(); b != e;   b)
	msg  = (*b)->username()   " ";
	do_write("clients "   msg   "n");
}

Код достаточно примитивен; одна вещь состоит в том, что, когда новейший заказчик входит в систему, мы вызываем update_clients_changed(), которая устанавливает clients_changed_ в true для всех заказчиков.
Как только он получает запрос, он сразу же отвечает на него, как показано в дальнейшем фрагменте кода:

void do_ping() { do_write("pingn"); }
void do_ask_clients() { do_write("ask_clientsn"); }
void on_write(const error_code & err, size_t bytes) { do_read(); }
void do_read() 
{
	async_read(sock_, buffer(read_buffer_), MEM_FN2(read_complete,_1,_2), MEM_FN2(on_read,_1,_2));
	post_check_ping();
}
void do_write(const std::string & msg) 
{
	if ( !started() ) return;
	std::copy(msg.begin(), msg.end(), write_buffer_);
	sock_.async_write_some( buffer(write_buffer_, msg.size()), MEM_FN2(on_write,_1,_2));
}
size_t read_complete(const boost::system::error_code & err, size_t bytes) 
{
	// ... as before
}

В конце всякой операции записи вызывается on_write(), которая вызывает другое асинхронное чтение, и, таким образом ждет запрос – отвечает на него, цикл продолжается, пока заказчик не отключится либо не сработает таймер.
От того что всякое чтение начинается с асинхронного ожидания в течении 5 секунд, то дозволено увидеть сработает ли у заказчика таймер. Если это так, то мы закрываем соединение:

void on_check_ping() 
{
	ptime now = microsec_clock::local_time();
	if ( (now - last_ping).total_milliseconds() > 5000) stop();
	last_ping = boost::posix_time::microsec_clock::local_time();
}
void post_check_ping() 
{
	timer_.expires_from_now(boost::posix_time::millisec(5000));
	timer_.async_wait( MEM_FN(on_check_ping));
}

Вот и каждый сервер. Вы можете запустить его и начать трудиться с ним!

Резюме

В этой главе мы посмотрели, как написать несколько основных клиент/серверных приложений. Мы чурались таких западней как утрата памяти и дедлоков. Все программы подойдут в качестве основы для ваших грядущих приложений, их дозволено расширять и адаптировать.
В дальнейшей главе мы получим больше большое осознавание синхронного по сопоставлению с асинхронным, отличия при применении Boost.Asio; посмотрим, как вы можете подключить свою собственную асинхронную операцию.

Источники к этой статье: ссылка

Каждому огромное спасибо за внимание, до новых встреч!

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB