02. I/O 操作

1. 引言

I/O操作是任何分布式应用程序的网络基础设施中的关键操作。他们直接参与数据交换的过程。输入操作用于从远程应用程序接收数据,而输出操作允许向远程应用程序发送数据。

1.1 I/O buffers

网络编程就是通过计算机网络组织进程间通信。这种情况下的通信意味着在两个或多个进程之间交换数据。从参与这种通信的进程的角度来看,进程执行I/O操作,向其他参与的进程发送数据并从其他进程接收数据。
与任何其他类型的I/O一样,网络I/O涉及到使用内存缓冲区,这是在进程的地址空间中分配的用于存储数据的连续内存块。当执行任何类型的输入操作(例如,从文件、管道或通过网络的远程计算机读取一些数据)时,数据到达进程,并且必须存储在其地址空间中的某个地方,以便可以进行进一步处理。也就是说,当缓冲区派上用场时。在执行输入操作之前,分配缓冲区,然后在操作期间将其用作数据目的点。当输入操作完成后,缓冲区中包含了应用程序可以处理的输入数据。同样,在执行输出操作之前,必须准备好数据并将其放入输出缓冲区,然后在输出操作中使用该缓冲区,它在其中扮演数据源的角色。

1.2 Synchronous and asynchronous I/O operations

同步操作阻塞调用它们的执行线程,只有在操作完成时才解除阻塞。因此,这种类型的操作的名称为:synchronous。

当异步操作被启动时,它与一个回调函数或函子相关联,在操作完成时使用Boost.Asio库。这些类型的I/O操作提供了很大的灵活性,但可能会使代码变得非常复杂。操作的初始化很简单,不会阻塞执行线程,这允许我们使用线程运行其他任务,而异步操作正在后台运行。

boost.Asio库是作为一个框架实现的,它利用了控制反转的方法。在启动一个或多个异步操作之后,应用程序将其中一个执行线程交给库,库使用该线程运行事件循环并调用应用程序提供的回调,以通知它先前启动的异步操作已经完成。异步操作的结果作为参数传递给回调函数。

1.3 Additional operations

除此之外,需要考虑取消异步操作、关闭和关闭套接字等操作。取消先前发起的异步操作的能力非常重要。它允许应用程序声明先前发起的操作不再相关,这可能会节省应用程序的资源(CPU和内存),否则(在这种情况下,即使知道没有人再对操作感兴趣,操作也会继续执行)将不可避免地浪费。当应用层协议没有为我们提供指示消息边界的其他方法时,如果分布式应用程序的一部分需要通知另一部分整个消息已经发送,关闭套接字是有用的。与任何其他操作系统资源一样,当应用程序不再需要套接字时,应将其返回给操作系统。闭包操作允许我们这样做。

2. 使用固定长度的I/O缓存

固定长度I/O缓冲区通常用于I/O操作,当要发送或接收的消息的大小已知时,它扮演数据源或目的地的角色。例如,这可以是在堆栈上分配的一个常量字符数组,其中包含一个表示要发送到服务器的请求的字符串。或者,它可以是在空闲内存中分配的可写缓冲区,在从套接字读取数据时用作数据目的点。

在boost.asio中,可以使用两个类表示固定长度的缓存。分别是asio::mutable_buffer和asio::const_buffer。第一个类代表可写缓存,第二个类表示只读。 然而,这两个类都不能直接用于I/O函数和方法。因此,引入了MutableBufferSequence 和 ConstBufferSequence 的概念。
MutableBufferSequence指明了一个表示asio::mutable_buffer对象的集合的对象。ConstBufferSequence指明了一个表示asio::const_buffer对象的集合的对象。
执行I/O操作的Boost.Asio函数和方法接受满足MutableBufferSequence或ConstBufferSequence概念要求的对象作为表示缓冲区的参数。
要做到这一点,例如,我们可以通过实例化类的对象来创建由单个缓冲区组成的缓冲区对象集合
std::vector<asio::mutable_buffer>类,并将我们的缓冲区对象放入其中。既然缓冲区是集合的一部分,那么可以在I/O操作中使用满足MutableBufferSequence的要求。然而,尽管这种方法可以很好地创建由两个或多个简单缓冲区组成的复合缓冲区,但是当涉及到表示单个简单缓冲区以便与Boost一起使用的简单任务时,它看起来过于复杂。幸运的是,Boost.Asio为我们提供了一种使用与I/O相关的函数和方法来简化单个缓冲区使用的方法(asio::buffer())。
asio::buffer()函数有几十种种重载方式,可以接收多种缓冲的表示形式,并返回一个asio::mutable_buffer_l或asio::const_buffer_l类的对象。

asio::mutable_buffer_l和asio::const_buffer_l类分别是asio::mutable_buffer和asio::const_buffer类的适配器。They provide an interface and behavior that satisfy the requirements of the MutableBufferSequence and ConstBufferSequence concepts, which allows us to pass these adapters as arguments to Boost.Asio I/O functions and methods.简单概括一下,我们可以用buffer()函数生成我们要用的缓存存储数据。 比如boost的发送接口send要求的参数为ConstBufferSequence类型。

2.1 用于输出操作的缓冲

  1. 分配一个缓冲区。注意,此步骤不涉及Boost.Asio的任何功能或数据类型。
  2. 用要用作输出的数据填充缓冲区。
  3. 将缓冲区表示为满足ConstBufferSequence概念要求的对象。
  4. 缓冲区已准备好与Boost.Asio的方法和函数一块使用。

例如将"hello"转化为该类型,代码示例如下:

void use_const_buffer() {
    std::string buf = "hello world!";
    asio::const_buffers_1 output_buf = asio::buffer(buf);
    return 0;
}

如果数据是使用数组存储的,代码示例如下:

void use_buffer_array()
{
	const size_t BUF_SIZE_BYTES = 20;//
	std::unique_ptr<char[]>buf(new char[BUF_SIZE_BYTES]);
	auto input_buf = asio::buffer(static_cast<void*>(buf.get()), BUF_SIZE_BYTES);
}

2.2 用于输入操作的缓冲

  1. 分配一个缓冲区。缓冲区的大小必须大到足以容纳要接收的数据块。注意,此步骤不涉及Boost.Asio的任何功能或数据类型。
  2. 使用满足MutableBufferSequence概念要求的对象表示缓冲区。
  3. 缓冲区已准备好与Boost.Asio的方法和函数一块使用。
int main() {
	// We expect to receive a block of data no more than 20 bytes
    // long.
	const size_t BUF_SIZE_BYTES = 20;
	// Step 1. Allocating the buffer.
	std::unique_ptr<char[]> buf(new char[BUF_SIZE_BYTES]);

	// Step 2. Creating buffer representation that satisfies
    // MutableBufferSequence concept requirements.
	auto input_buf = asio::buffer(static_cast<void*>(buf.get()),BUF_SIZE_BYTES);
	// Step 3. 'input_buf' is the representation of the buffer
	// 'buf' that can be used in Boost.Asio input operations.
	return 0;
}

3. 使用可扩展的面向流的I/O缓冲

可扩展缓冲区是那些在新数据写入时动态增加其大小的缓冲区。当传入消息的大小未知时,它们通常用于从套接字读取数据。
一些应用层协议没有定义消息的确切大小。相反,消息的边界由消息本身末尾的特定符号序列表示,或者由发送方在完成发送消息后发出的传输协议服务消息文件结束(EOF)表示。

可拓展的流定向缓冲在boost.asio中使用asio::streambuf类表示,是asio::basic_streambuf的typedef:

typedef basic_streambuf<> streambuf;

asio::basic_streambuf<>类继承于std::streambuf类,也就是意味着,它可以作为STL流类的流缓冲区。

int main(){
	asio::streambuf buf;
	std::ostream output(&buf);
	// Writing the message to the stream-based buffer.
	output << "Message1\nMessage2";
	// Now we want to read all data from a streambuf
	// until '\n' delimiter.
	// Instantiate an input stream which uses our
	// stream buffer.
	std::istream input(&buf);
	// We'll read data into this string.
	std::string message1;
	std::getline(input, message1);
	// Now message1 string contains 'Message1'.
	return 0;
}

4. Writing to a TCP socket synchronously(同步的向TCP socket写入)

向套接字执行同步写入的方法和函数会阻塞执行线程,并且在数据(至少是一定数量的数据)被写入套接字或发生错误之前不会返回
向socket写入最基本的方法是使用boost.asio库中asio::ip::tcp::socket类的write_some()方法。下面是write_some()的一个重载声明:

  template<typename ConstBufferSequence>
  std::size_t write_some(const ConstBufferSequence& buffers);

此方法接受表示复合缓冲区的对象作为参数,并且顾名思义,将一定数量的数据从缓冲区写入套接字。如果方法成功,返回值表示写入的字节数。这里要强调的一点是,该方法可能不会发送通过buffers参数提供给它的所有数据。该方法仅保证在不发生错误的情况下至少写入一个字节。这意味着,在一般情况下,为了将所有数据从缓冲区写入套接字,我们可能需要多次调用此方法。

下列步骤说明了如何使用此方法:

  1. 在客户机应用程序中,分配、打开并连接活动TCP套接字。在服务器应用程序中,通过使用接收方套接字接受连接请求来获得已连接的活动TCP套接字。
  2. 分配缓冲区并填充要写入套接字的数据。
  3. 在循环中,调用套接字的write_some()方法的次数是发送缓冲区中所有可用数据所需的次数。

代码示例如下:

#include <boost/asio.hpp>
#include <iostream>

void write_to_socket(asio::ip::tcp::socket& sock){
	std::string buf = "hello";
	std::size_t total_bytes_written = 0;//total_bytes_writen表示已经发送的字节数。

	while (total_bytes_written != buf.length()) {
		total_bytes_written += sock.write_some(asio::buffer(buf.c_str() + total_bytes_written, buf.length() - total_bytes_written));
	}
}

int main()
{
	std::string raw_ip_address = "192.168.1.10";
	unsigned short port_num = 3333;

	try {
		asio::ip::tcp::endpoint ep(asio::ip::address::from_string(raw_ip_address), port_num);

		asio::io_context ios;

		asio::ip::tcp::socket sock(ios, ep.protocol());

		sock.connect(ep);

		write_to_socket(sock);

	}
	catch (system::system_error& e) {

		std::cout << "failed to write to socket" << "error code is" << e.code() << ".Message:" << e.what();
		return e.code().value();

	}

	return 0;
}

上述方法可能无法一次性将缓存中的数据全部发送出去,可以使用另一种方式,即使用send()函数。

int sen_data_to_socket() {
	std::string raw_ip_address = "192.168.1.10";
	unsigned short port_num = 3333;

	try {
		asio::ip::tcp::endpoint ep(asio::ip::address::from_string(raw_ip_address), port_num);

		asio::io_context ios;

		asio::ip::tcp::socket sock(ios, ep.protocol());

		sock.connect(ep);

		std::string buf = "hello";

		int send_length = sock.send(asio::buffer(buf.c_str(), buf.length()));//*******
		if (send_length <= 0) {//****
			return 0;
		}

	}
	catch (system::system_error& e) {

		std::cout << "failed to write to socket" << "error code is" << e.code() << ".Message:" << e.what();
		return e.code().value();

	}

	return 0;
}

与套接字对象的write_some()方法将一定数量的数据从缓冲区写入套接字相反,asio::write()函数将缓冲区中可用的所有数据写入。这简化了对套接字的编写,使代码更短、更清晰。

代码示例如下:

void writeToSocketEnhanced(asio::ip::tcp::socket& sock) {
    // Allocating and filling the buffer.
    std::string buf = "Hello";
    // Write whole buffer to the socket.
    asio::write(sock, asio::buffer(buf));
}

5.Reading from a TCP socket synchronously(同步的从TCp socket中读)

从TCP套接字读取数据是一种输入操作,用于接收连接到该套接字的远程应用程序发送的数据。同步读取是使用Boost.Asio提供的套接字接收数据的最简单方法。从套接字执行同步读取的方法和函数会阻塞执行线程,并且在从套接字读取数据(至少一定量的数据)或发生错误之前不会返回。

向socket写入最基本的方法是使用boost.asio库中asio::ip::tcp::socket类的read_some()方法。下面是write_some()的一个重载声明:

template<typename MutableBufferSequence>
std::size_t read_some(const MutableBufferSequence & buffers);

此方法接受一个表示可写缓冲区(单个或组合)的对象作为参数,并且顾名思义,从套接字读取一定数量的数据到缓冲区。如果方法成功,返回值表示读取的字节数。需要注意的是,无法控制该方法将读取多少字节。该方法仅保证在不发生错误的情况下至少读取一个字节。这意味着,在一般情况下,为了从套接字读取一定量的数据,我们可能需要多次调用该方法。

下面的算法描述了在分布式应用程序中从TCP套接字同步读取数据所需的步骤:

  1. 在客户机应用程序中,分配、打开并连接活动TCP套接字。在服务器应用程序中,通过使用接收方套接字接受连接请求来获得已连接的活动TCP套接字。
  2. 分配足够大小的缓冲区,以容纳要读取的预期消息。
  3. 在循环中,调用套接字的read_some()方法的次数取决于读取消息所需的次数。

示例代码如下:

std::string read_from_socket(asio::ip::tcp::socket& sock) {
	const unsigned MESSAGE_SIZE = 7;
	char buf[MESSAGE_SIZE];

	std::size_t total_bytes_read = 0;
	while (total_bytes_read != MESSAGE_SIZE) {
		total_bytes_read += sock.read_some(asio::buffer(buf + total_bytes_read, MESSAGE_SIZE - total_bytes_read));
	}

	return std::string(buf, total_bytes_read);

}

int main() {
	std::string raw_ip_address = "192.168.1.10";
	unsigned short port_num = 3333;

	try {
		asio::ip::tcp::endpoint ep(asio::ip::address::from_string(raw_ip_address), port_num);
		asio::io_context ios;

		asio::ip::tcp::socket sock(ios, ep.protocol());

		sock.connect(ep);

		read_from_socket(sock);
		
	}
	catch (system::system_error& e) {
		std::cout << "failed to read" << e.code() << ".Message" << e.what();
		return e.code().value();

	}
	return 0;
}

就像上述的写操作一样,读操作也可以一次性的将数据全部读完。asio::ip::tcp::socket类包含另一个从套接字同步读取数据的方法,称为receive()。这个方法有三个重载。如前所述,其中一个方法相当于read_some()方法。。这些方法在某种意义上是同义词。代码示例如下:

int main() {
	std::string raw_ip_address = "192.168.1.10";
	unsigned short port_num = 3333;

	try {
		asio::ip::tcp::endpoint ep(asio::ip::address::from_string(raw_ip_address), port_num);
		asio::io_context ios;

		asio::ip::tcp::socket sock(ios, ep.protocol());

		sock.connect(ep);

		const unsigned char BUFF_SIZE = 7;

		char buffer_receive[BUFF_SIZE];

		int receive_length = sock.receive(asio::buffer(buffer_receive,BUFF_SIZE));
		if (receive_length <= 0) {
			std::cout << "wrong" << std::endl;
		}
	}
	catch (system::system_error& e) {
		std::cout << "failed to read" << e.code() << ".Message" << e.what();
		return e.code().value();

	}
	return 0;
}

对于这样一个简单的操作来说,使用套接字的read_some()方法从套接字读取数据似乎非常复杂。这种方法要求我们使用一个循环,一个变量来跟踪已经读取了多少字节,并为循环的每次迭代正确地构造一个缓冲区。这种方法容易出错,并且使代码更难以理解和维护。
幸运的是,Boost.Asio提供了一系列函数,可以简化从不同上下文中套接字同步读取数据的过程。有三个这样的函数,每个函数都有几个重载,它们提供了丰富的功能,便于从套接字读取数据。

  1. 第一个是asio::read()函数,它的其中一种重载的形式是:
template<typename SyncReadStream,typename MutableBufferSequence>
std::size_t read(SyncReadStream & s,const MutableBufferSequence & buffers);

此函数接收两个参数,第一个参数是一个对象的引用,此参数需要满足SyncReadStream概念的要求。表示tcp套接字的asio::ip::tcp::socket类的对象满足这些要求,因此可以用作函数的第一个参数。第二个参数buffers表示将从套接字读取数据到的缓冲区(简单或复合)。
与套接字的read_some()方法(从套接字读取一定数量的数据到缓冲区)不同,asio::read()函数在一次调用期间从套接字读取数据,直到作为参数传递给它的缓冲区被填满或发生错误。这简化了从套接字读取,使代码更短、更清晰。

使用此函数,上述的read_some()函数的代码可以转换为如下的形式:

std::string  buf = "hello";
std::string readFromSocketEnhanced(asio::ip::tcp::socket& sock){
	const unsigned char MESSAGE_SIZE = 7;
	char buf[MESSAGE_SIZE];
	asio::read(sock,asio::buffer(buf,MESSAGE_SIZE));
	return std::string(buf,MESSAGE_SIZE);
}
  1. 第二个是asio::read_until()函数
    asio::read_until()函数提供了一种从套接字读取数据直到数据中遇到指定模式的方法。这个函数有8个重载。让我们考虑其中一个:
template<typename SyncReadStream,typename Allocator>
std::size_t read_until(SyncReadStream & s,boost::asio::basic_streambuf< Allocator > & b,char delim);

此函数接收三个参数,第一个参数是一个对象的引用,此参数需要满足SyncReadStream概念的要求。表示tcp套接字的asio::ip::tcp::socket类的对象满足这些要求,因此可以用作函数的第一个参数。第
第二个参数b表示将在其中读取数据的面向流的可扩展缓冲区。最后一个名为delim的参数指定一个分隔符字符。

asio::read_until()函数将从s套接字读取数据到缓冲区b,直到它在数据的读取部分遇到由delim参数指定的字符。当遇到指定的字符时,函数返回。

std::string readFromSocketDelim(asio::ip::tcp::socket& sock) {
    asio::streambuf buf;
	// Synchronously read data from the socket until
	// '\n' symbol is encountered.
	asio::read_until(sock, buf, '\n');
	std::string message;
	// Because buffer 'buf' may contain some other data
	// after '\n' symbol, we have to parse the buffer and
	// extract only symbols before the delimiter.
	std::istream input_stream(&buf);
	std::getline(input_stream, message);
	return message;
}
  1. 第三个是asio::read_at()函数
    极少用到,不再赘述。
posted @ 2024-09-06 20:26  yyyyyllll  阅读(89)  评论(0)    收藏  举报