欢迎来访,清水煮面
联系jacob95047@gmail.com

从零写个C++服务器之Echo服务

从零写个C++服务器之环境搭建

所谓Echo服务,差不多算是最基础、简单的服务器功能了,实现的目标就是服务器起来后,等待客户端的连接,连接成功后,客户端发送一些字符到服务器,服务器接受数据并打印输出。这里我是准备采用boost的asio库来实现一个异步的网络服务。在实现编码前,需要先对boost库里面的几个重要的对象做一些学习和基本了解。

Asio基础

io_service
io_service可以理解为是和操作系统打交道的一个对象,它屏蔽了用户和系统io操作的细节,使得开发者在开放时不用太多关心系统级别的处理过程。io_service可以被多个线程调用执行,如果多个线程都调用了同一个io_service.run方法,那么当与这个io_service绑定的一个异步io事件完成时,会通知到所有调用了该io_service的线程当前有一个异步io完成。

async_read
从一个流对象中异步读取数据。参数1是流对象,可以是socket也可以是普通的io对象;参数2是一个用于接收数据数据的buffer对象;参数3是一个读取完成的检测函数,该函数可以自定义,用来判断当前读取是否完成,比如我们可以指定直到buffer里面读取到了'<‘字符才算一次读取完成,当完成读取时,会调用参数4指定的完成处理函数。
async_write等函数功能都类似,不再赘述

异步io
与异步io相对应的就是同步io,同步io很好理解,就是程序实际的执行流和代码书写的顺序是一致的,但是对于io操作,其底层其实是蛮复杂的,会涉及两个大块,一个是等待操作系统准备好数据,一个是把数据从内核态拷贝到用户态。这些步骤都不是马上能完成的,如果同步写法来处理io,对cpu就很不友好,因为等待io完成期间,我们后面的程序是无法运行的。这时候异步io就出来了,简言之,异步io就是可以保证在数据准备期间,我们程序能继续往下执行,等待操作系统把数据准备好后,通知到我们,我们在回调函数里去处理即可。我们不用一直干等着,但是我们的程序流程是会因为异步操作的原因而被打断掉的。所以,异步代码相对于同步代码而言,复杂不少。
相对于的,我们在用户态就需要保证,在异步回调被触发之前,一些对象的生命周期需要维护好,比如一个用于接收异步回调的buffer对象,不能说在发起异步操作时存在,在回调到达时就已经被销毁了。

初版服务器结构

贴一个初版的服务器客户端代码的UML图,方便看代码的同时便于后续看图重构结构。

Connection类是用来处理连接和数据读取/发送的核心类,Client和Server对象只是持有该对象。其中服务器和客户端的主要区别在于,客户端只需要处理一条连接,服务器需要维护多条连接。

踩坑记录

在写服务器时,曾需要一个问题,让我花费了老半天才解决。现象是服务器完成端口监听,并且成功和一个客户端建立了连接,此时客户端发送了一串字符,但是服务器总是无法正常读取,此时再新建一个客户端尝试连接,发现服务器已经无法连接。贴下当时有问题的代码:

void InitSever::async_accept()
{
	std::cout << __FUNCTION__ << std::endl;

	connection_ptr conn = connection_ptr(new Connection(io_service_ptr(&service)));
	acceptor_->async_accept(conn->get_socket_ref(), boost::bind(&InitSever::handle_accept, shared_from_this(), conn, _1));
}

void InitSever::handle_accept(connection_ptr conn, const boost::system::error_code& e)
{
	if (e)
	{
		std::cout << __FUNCTION__ << " handle accept failed. e: " << e << std::endl;
		return;
	}

	std::cout << __FUNCTION__ << std::endl;
	conn->receive_msg();

	async_accept();
}

上述代码,就是在main函数里面先触发了async_accept等待客户端连接,acceptor的async_accept是传入了一个Connection对象用来接收客户端连接的数据,等待连接完成后在handle_accept里面处理后续流程,比如数据的读取。看着这段代码再正常不过了,但仔细阅读就会发现存在一个异步操作的大坑。在hanle_accept里面,我们调用了conn->reveive_msg,这个数据读取是异步的,而我们的conn对象是由shared_ptr来管理生命周期的,在离开handle_accpet这个函数作用域后其对象就被销毁了,而这时候异步io并没有完成,所以就出现了怪异的读取异常问题!
那么,怎么解决呢,其实解决起来也很简单,就是要在io完成前(其实是连接断开前),都要保持该Connection对象活着,所以,引入了一个connections_容器,我们要手动保持下这个智能指针,如此问题便修复。调整后的代码如下:

void InitSever::handle_accept(connection_ptr conn, const boost::system::error_code& e)
{
	if (e)
	{
		std::cout << __FUNCTION__ << " handle accept failed. e: " << e << std::endl;
		return;
	}

	std::cout << __FUNCTION__ << std::endl;
	connections_.push_back(conn);	// important!!!
	conn->receive_msg();

	async_accept();
}

相关代码都已经同步至Github

赞(2)
未经允许不得转载(与我联系):清水面 » 从零写个C++服务器之Echo服务

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

Warning: error_log(/usr/local/lighthouse/softwares/wordpress/wp-content/plugins/spider-analyser/#log/log-1915.txt): failed to open stream: No such file or directory in /usr/local/lighthouse/softwares/wordpress/wp-content/plugins/spider-analyser/spider.class.php on line 2900