跳至主要內容

LincZero大约 9 分钟

视频:https://www.bilibili.com/video/BV1jm4y1r7AY 作者文档:https://llfc.club/category?catid=225RaiVNI8pFDD5L4m807g7ZwmF#!aid/2LfzYBkRCfdEDrtE6hWz8VrCLoS

传统的阻塞模型

网络编程的基本流程对于服务端是这样的

服务端

  1. socket——创建socket对象。
  2. bind——绑定本机ip+port。
  3. listen——监听来电,若在监听到来电,则建立起连接。
  4. accept——再创建一个socket对象给其收发消息。原因是现实中服务端都是面对多个客户端,那么为了区分各个客户端,则每个客户端都需再分配一个socket对象进行收发消息。
  5. read、write——就是收发消息了。

客户端

  1. socket——创建socket对象。
  2. connect——根据服务端ip+port,发起连接请求。
  3. write、read——建立连接后,就可发收消息了。

图示如下

https://cdn.llfc.club/1540562-20190417002428451-62583604.jpg
https://cdn.llfc.club/1540562-20190417002428451-62583604.jpg

相关的网络编程技术可以看看我之前写的文章
https://llfc.club/articlepage?id=2LXIKWJtKGblnWtHT7TplLKK6zeopen in new window

现代化模型

当然,现在的都是Proactor模式Reactor模式这种,使用epoll等多路复用技术,而且是异步

  • Reactor模式,是同步模型
  • Proactor模式,是异步模型

接下来按照上述流程,我们用boost::asio逐步介绍。这里会有本书:《Boost.Asio C++ Network Programming Cookbook》,博客很多地方也按这本书写的

终端节点的创建

所谓 终端节点 就是用来通信的端对端的节点,可以通过ip地址和端口构造,其的节点可以连接这个终端节点做通信.
如果我们是客户端,我们可以通过对端的ip和端口构造一个endpoint,用这个endpoint和其通信。

客户端

int  client_end_point() {
	// Step 1. 假设客户端应用程序已经获得了ip地址和协议端口号。
	std::string raw_ip_address = "127.0.0.1";
	unsigned short port_num = 3333;
	// 用于存储解析原始ip地址时发生的错误信息。
	boost::system::error_code ec;
	// Step 2. 使用独立于IP协议版本的地址表示。
	asio::ip::address ip_address = asio::ip::address::from_string(raw_ip_address, ec);
	if (ec.value() != 0) {
		// 提供的IP地址无效。打破执行
		std::cout
			<< "Failed to parse the IP address. Error code = "
			<< ec.value() << ". Message: " << ec.message();
		return ec.value();
	}
	// Step 3. 准备端点
	asio::ip::tcp::endpoint ep(ip_address, port_num);
	// Step 4. 端点已经准备好,可以用来指定客户机想要与之通信的网络中的特定服务器。
	return 0;
}

服务端,如果是服务端,则只需根据本地地址绑定就可以生成endpoint

int  server_end_point(){
	// Step 1. 这里我们假设服务器应用程序已经获得了协议端口号。
	unsigned short port_num = 3333;
	// Step 2. 创建asio::ip::address类的特殊对象,指定主机上所有可用的ip地址。注意,这里我们假设服务器工作在IPv6协议上。
	asio::ip::address ip_address = asio::ip::address_v6::any();
	// Step 3. 创建端点
	asio::ip::tcp::endpoint ep(ip_address, port_num);
	// Step 4. 端点被创建后,并可用于指定IP地址和端口号,服务器应用程序希望在这些地址和端口号上侦听传入的连接。
	return 0;
}

总结

  • 创建ip地址类,asio::ip::address
  • 创建端点类,asio::ip::tcp::endpoint

创建socket

创建socket分为4步

  1. 创建上下文iocontext
  2. 选择协议
  3. 生成socket
  4. 打开socket

具体点来讲是:

  • 创建IO服务类,asio::io_service
  • 创建tcp类,asio::ip::tcp
  • 创建socket类
    • 客户端:普通 socket类,asio::ip::tcp::socket
    • 服务端:accept socket类,asio::ip::tcp::acceptor
  • 打开socket类,open

客户端

// 创建TCP套接字
int create_tcp_socket() {
	// Step 1. 套接字构造函数需要'io_service'类的实例 
	asio::io_context  ios;
	// Step 2. 创建一个tcp类的对象,以IPv4作为底层协议,表示tcp协议
	asio::ip::tcp protocol = asio::ip::tcp::v4();
	// Step 3. 实例化一个活动的TCP套接字对象
	asio::ip::tcp::socket sock(ios);
	// 用于存储打开套接字时发生的错误信息
	boost::system::error_code ec;
	// Step 4. 打开 socket
	sock.open(protocol, ec);
	if (ec.value() != 0) {
		// 打开socket失败
		std::cout
			<< "Failed to open the socket! Error code = "
			<< ec.value() << ". Message: " << ec.message();
		return ec.value();
	}
	return 0;
}

服务端。上述socket只是通信的socket,如果是服务端,我们还需要生成一个acceptor的socket,用来接收新的连接。

// 创建TCP Accept套接字
int  create_acceptor_socket() {
	// Step 1. 套接字构造函数需要'io_service'类的实例。
	asio::io_context ios;
	// Step 2. 创建一个tcp类的对象,表示一个tcp协议,IPv6作为底层协议。
	asio::ip::tcp protocol = asio::ip::tcp::v6();
	// Step 3. 实例化一个接受套接字对象。
	asio::ip::tcp::acceptor acceptor(ios);
	// 用于存储打开接收方套接字时发生的错误信息。
	boost::system::error_code ec;
	// Step 4. 打开 acceptor socket.
	acceptor.open(protocol, ec);
	if (ec.value() != 0) {
		// 打开socket失败
		std::cout
			<< "Failed to open the acceptor socket!"
			<< "Error code = "
			<< ec.value() << ". Message: " << ec.message();
		return ec.value();
	}
	return 0;
}

绑定acceptor

对于acceptor类型的socket,服务器要将其绑定到指定的断点,所有连接这个端点的连接都可以被接收到。

int  bind_acceptor_socket() {
	// Step 1. 这里我们假设服务器应用程序已经获得了协议端口号。
	unsigned short port_num = 3333;
	// Step 2. 创建端点
	asio::ip::tcp::endpoint ep(asio::ip::address_v4::any(),
		port_num);
	// 由'acceptor'类构造函数使用。
	asio::io_context  ios;
	// Step 3. 创建并打开一个接收套接字
	asio::ip::tcp::acceptor acceptor(ios, ep.protocol());
	boost::system::error_code ec;
	// Step 4. 绑定接收器套接字
	acceptor.bind(ep, ec);
	// 处理错误
	if (ec.value() != 0) {
		// 绑定接收socket失败。打破执行。
		std::cout << "Failed to bind the acceptor socket."
			<< "Error code = " << ec.value() << ". Message: "
			<< ec.message();
		return ec.value();
	}
	return 0;
}

(完整)

流程

  • 获取IP和端口等
  • 创建端点,asio::ip::tcp::endpoint
  • 创建IO上下文,asio::io_context
  • 创建并打开套接字
    • 客户端:asio::ip::tcp::socket,connect
    • 服务端:asio::ip::tcp::acceptor,bind,listen,sock,accept

客户端:连接指定的端点,对连接服务器指定的端点进行连接

int  connect_to_end() {
	// Step 1. 假设客户端应用程序已经获得了目标服务器的IP地址和协议端口号
	std::string raw_ip_address = "127.0.0.1";
	unsigned short port_num = 3333;
	try {
		// Step 2. 创建指定目标服务器应用程序的端点
		asio::ip::tcp::endpoint ep(asio::ip::address::from_string(raw_ip_address), port_num);
		asio::io_context ios;
		// Step 3. 创建并打开套接字
		asio::ip::tcp::socket sock(ios, ep.protocol());
		// Step 4. 连接套接字
		sock.connect(ep);
		// 此时,套接字'sock'已连接到服务器应用程序,并可用于向其发送数据或从其接收数据。
	}
	// 这里使用的asio::ip::address::from_string()和asio::ip::tcp::socket::connect()的过载会在出现错误条件时抛出异常。
	catch (system::system_error& e) {
		std::cout << "Error occured! Error code = " << e.code()
			<< ". Message: " << e.what();
		return e.code().value();
	}
}

服务器:服务器接收连接。当有客户端连接时,服务器需要接收连接

int accept_new_connection(){
	// 包含挂起连接请求的队列的大小。
	const int BACKLOG_SIZE = 30;
	// Step 1. 这里我们假设服务器应用程序已经获得了协议端口号。
	unsigned short port_num = 3333;
	// Step 2. 创建服务器端点。
	asio::ip::tcp::endpoint ep(asio::ip::address_v4::any(), port_num);
	asio::io_context  ios;
	try {
		// Step 3. 实例化并打开一个接受套接字。
		asio::ip::tcp::acceptor acceptor(ios, ep.protocol());
		// Step 4. 将接收方套接字绑定到服务器端。
		acceptor.bind(ep);
		// Step 5. 开始监听传入的连接请求。
		acceptor.listen(BACKLOG_SIZE);
		// Step 6. 创建活动套接字。
		asio::ip::tcp::socket sock(ios);
		// Step 7. 处理下一个连接请求并将活动套接字连接到客户端
		acceptor.accept(sock);
		// 此时,'sock'套接字已连接到客户端应用程序,并可用于向其发送数据或从其接收数据
	}
	catch (system::system_error& e) {
		std::cout << "Error occured! Error code = " << e.code()
			<< ". Message: " << e.what();
		return e.code().value();
	}
}

关于buffer

任何网络库都有提供buffer的数据结构,所谓buffer就是接收和发送数据时缓存数据的结构。

boost::asio提供了asio::mutable_buffer 和 asio::const_buffer这两个结构,但是这两个结构都没有被asio的api直接使用

  • asio::mutable_buffer用于写服务
  • asio::const_buffer用于读服务 他们是一段连续的空间,首字节存储了后续数据的长度。

对于api的buffer参数,asio提出了MutableBufferSequence和ConstBufferSequence概念,他们是由多个asio::mutable_buffer和asio::const_buffer组成的。 也就是说boost::asio为了节省空间,将一部分连续的空间组合起来,作为参数交给api使用。
我们可以理解为MutableBufferSequence的数据结构为std::vectorasio::mutable_buffer

结构如下

https://cdn.llfc.club/1676257797218.jpg
https://cdn.llfc.club/1676257797218.jpg

每隔vector存储的都是mutable_buffer的地址,每个mutable_buffer的第一个字节表示数据的长度,后面跟着数据内容。
这么复杂的结构交给用户使用并不合适,所以asio提出了buffer()函数,该函数接收多种形式的字节流,该函数返回asio::mutable_buffers_1 o或者asio::const_buffers_1结构的对象。
如果传递给buffer()的参数是一个只读类型,则函数返回asio::const_buffers_1 类型对象。
如果传递给buffer()的参数是一个可写类型,则返回asio::mutable_buffers_1 类型对象。
asio::const_buffers_1和asio::mutable_buffers_1是asio::mutable_buffer和asio::const_buffer的适配器,提供了符合MutableBufferSequence和ConstBufferSequence概念的接口,所以他们可以作为boost::asio的api函数的参数使用。
简单概括一下,我们可以用buffer()函数生成我们要用的缓存存储数据。
比如boost的发送接口send要求的参数为ConstBufferSequence类型

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

我们需要将”Hello Word转化为该类型”

void use_const_buffer() {
	std::string buf = "hello world!";
	asio::const_buffer  asio_buf(buf.c_str(), buf.length());
	std::vector<asio::const_buffer> buffers_sequence;
	buffers_sequence.push_back(asio_buf);
}

最终buffers_sequence就是可以传递给发送接口send的类型。但是这太复杂了,可以直接用buffer函数转化为send需要的参数类型

void use_buffer_str() {
	asio::const_buffers_1 output_buf = asio::buffer("hello world");
}

output_buf可以直接传递给该send接口。我们也可以将数组转化为send接受的类型

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);
}

对于流式操作,我们可以用streambuf,将输入输出流和streambuf绑定,可以实现流式输入和输出。

void use_stream_buffer() {
	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'.
}